8c5deed4c2
- Replace dark/light toggle with Light/Dark/System dropdown menu - System theme follows OS prefers-color-scheme setting - Apply theme dropdown to shared schedule page - Fix 7 packageType (bottle) bugs across stock calc, share, refills, export/import - Fix planner bottle-type stock calculation and display - Fix dailyRate double-counting with per-intake takenBy - Fix About modal update check stale caching - Fix intake reminder past-intake seeding and push title - Fix phantom DB path in drizzle.config.ts - Fix mobile dose field visibility - Make medication name clickable in dashboard reminder bar - Improve planner checkbox UX with inline tooltip - Add 20+ new tests covering all fixes
139 lines
4.5 KiB
TypeScript
139 lines
4.5 KiB
TypeScript
import { and, desc, eq } from "drizzle-orm";
|
|
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
|
import { z } from "zod";
|
|
import { db } from "../db/client.js";
|
|
import { medications, refillHistory } from "../db/schema.js";
|
|
import { getAnonymousUserId, requireAuth } from "../plugins/auth.js";
|
|
import { env } from "../plugins/env.js";
|
|
import type { AuthUser } from "../types/fastify.js";
|
|
|
|
const refillSchema = z
|
|
.object({
|
|
packsAdded: z.number().int().min(0).default(0),
|
|
loosePillsAdded: z.number().int().min(0).default(0),
|
|
})
|
|
.refine((data) => data.packsAdded > 0 || data.loosePillsAdded > 0, {
|
|
message: "Must add at least one pack or some loose pills",
|
|
});
|
|
|
|
export async function refillRoutes(app: FastifyInstance) {
|
|
// All refill routes require auth
|
|
app.addHook("preHandler", requireAuth);
|
|
|
|
// Helper to get user ID from request
|
|
async function getUserId(request: FastifyRequest, reply: FastifyReply): Promise<number> {
|
|
if (!env.AUTH_ENABLED) {
|
|
return getAnonymousUserId();
|
|
}
|
|
const authUser = request.user as unknown as AuthUser | null;
|
|
if (!authUser) {
|
|
reply.status(401).send({ error: "User not authenticated", code: "AUTH_REQUIRED" });
|
|
throw new Error("AUTH_REQUIRED");
|
|
}
|
|
return authUser.id;
|
|
}
|
|
|
|
// POST /medications/:id/refill - Add stock to medication
|
|
app.post<{ Params: { id: string } }>("/medications/:id/refill", async (req, reply) => {
|
|
const parsed = refillSchema.safeParse(req.body);
|
|
if (!parsed.success) return reply.status(400).send(parsed.error.format());
|
|
|
|
const medId = Number(req.params.id);
|
|
if (Number.isNaN(medId)) return reply.badRequest("Invalid medication id");
|
|
|
|
const userId = await getUserId(req, reply);
|
|
|
|
// Verify ownership
|
|
const [med] = await db
|
|
.select()
|
|
.from(medications)
|
|
.where(and(eq(medications.id, medId), eq(medications.userId, userId)));
|
|
if (!med) return reply.notFound("Medication not found");
|
|
|
|
const { packsAdded, loosePillsAdded } = parsed.data;
|
|
|
|
// Update medication stock
|
|
const newPackCount = med.packCount + packsAdded;
|
|
const newLooseTablets = med.looseTablets + loosePillsAdded;
|
|
|
|
await db
|
|
.update(medications)
|
|
.set({
|
|
packCount: newPackCount,
|
|
looseTablets: newLooseTablets,
|
|
stockAdjustment: 0, // Reset offset since we're adding to base stock
|
|
lastStockCorrectionAt: new Date(), // Reset consumed counter to now
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(and(eq(medications.id, medId), eq(medications.userId, userId)));
|
|
|
|
// Create refill history entry
|
|
const [refill] = await db
|
|
.insert(refillHistory)
|
|
.values({
|
|
medicationId: medId,
|
|
userId,
|
|
packsAdded,
|
|
loosePillsAdded,
|
|
})
|
|
.returning();
|
|
|
|
// Calculate pills added for response (packageType-aware)
|
|
const isBottle = (med.packageType ?? "blister") === "bottle";
|
|
const pillsPerPack = isBottle ? 0 : med.blistersPerPack * med.pillsPerBlister;
|
|
const totalPillsAdded = isBottle ? loosePillsAdded : packsAdded * pillsPerPack + loosePillsAdded;
|
|
const newTotalPills = isBottle
|
|
? newLooseTablets + (med.stockAdjustment ?? 0)
|
|
: newPackCount * pillsPerPack + newLooseTablets + (med.stockAdjustment ?? 0);
|
|
|
|
return {
|
|
success: true,
|
|
refill: {
|
|
id: refill.id,
|
|
packsAdded,
|
|
loosePillsAdded,
|
|
totalPillsAdded,
|
|
refillDate: refill.refillDate,
|
|
},
|
|
newStock: {
|
|
packCount: newPackCount,
|
|
looseTablets: newLooseTablets,
|
|
totalPills: newTotalPills,
|
|
},
|
|
};
|
|
});
|
|
|
|
// GET /medications/:id/refills - Get refill history for a medication
|
|
app.get<{ Params: { id: string } }>("/medications/:id/refills", async (req, reply) => {
|
|
const medId = Number(req.params.id);
|
|
if (Number.isNaN(medId)) return reply.badRequest("Invalid medication id");
|
|
|
|
const userId = await getUserId(req, reply);
|
|
|
|
// Verify ownership
|
|
const [med] = await db
|
|
.select()
|
|
.from(medications)
|
|
.where(and(eq(medications.id, medId), eq(medications.userId, userId)));
|
|
if (!med) return reply.notFound("Medication not found");
|
|
|
|
// Get refill history, newest first
|
|
const refills = await db
|
|
.select()
|
|
.from(refillHistory)
|
|
.where(eq(refillHistory.medicationId, medId))
|
|
.orderBy(desc(refillHistory.refillDate));
|
|
|
|
const isBottle = (med.packageType ?? "blister") === "bottle";
|
|
const pillsPerPack = isBottle ? 0 : med.blistersPerPack * med.pillsPerBlister;
|
|
|
|
return refills.map((r) => ({
|
|
id: r.id,
|
|
packsAdded: r.packsAdded,
|
|
loosePillsAdded: r.loosePillsAdded,
|
|
totalPillsAdded: isBottle ? r.loosePillsAdded : r.packsAdded * pillsPerPack + r.loosePillsAdded,
|
|
refillDate: r.refillDate,
|
|
}));
|
|
});
|
|
}
|