feat: Stock Correction Modal (#47)

* feat: add stock correction modal with blister-based input

- Add 'Correct Stock' button to medication detail modal
- New modal with Full Blisters + Partial Blister Pills inputs
- Auto-conversion for edge cases (full/negative partial)
- New stockAdjustment field for DB corrections without touching looseTablets
- New lastStockCorrectionAt timestamp to ignore old consumed doses after correction
- Tracking data preserved for future statistics
- Add Drizzle migrations for new columns
- Add translations for en/de

* fix: add stock_adjustment columns to e2e/integration test schemas
This commit is contained in:
Daniel Volz
2026-01-18 12:53:25 +01:00
committed by GitHub
parent bb46b26ec6
commit 75bb7abebc
16 changed files with 2072 additions and 58 deletions
+43 -1
View File
@@ -96,6 +96,8 @@ export async function medicationRoutes(app: FastifyInstance) {
blistersPerPack: row.blistersPerPack ?? 1,
pillsPerBlister: row.pillsPerBlister ?? 1,
looseTablets: row.looseTablets ?? 0,
stockAdjustment: row.stockAdjustment ?? 0,
lastStockCorrectionAt: row.lastStockCorrectionAt?.toISOString() ?? null,
pillWeightMg: row.pillWeightMg,
blisters: parseBlisters(row),
imageUrl: row.imageUrl,
@@ -147,6 +149,8 @@ export async function medicationRoutes(app: FastifyInstance) {
blistersPerPack: inserted.blistersPerPack,
pillsPerBlister: inserted.pillsPerBlister,
looseTablets: inserted.looseTablets,
stockAdjustment: inserted.stockAdjustment ?? 0,
lastStockCorrectionAt: inserted.lastStockCorrectionAt?.toISOString() ?? null,
pillWeightMg: inserted.pillWeightMg,
blisters,
imageUrl: inserted.imageUrl,
@@ -235,6 +239,8 @@ export async function medicationRoutes(app: FastifyInstance) {
blistersPerPack: result[0].blistersPerPack,
pillsPerBlister: result[0].pillsPerBlister,
looseTablets: result[0].looseTablets,
stockAdjustment: result[0].stockAdjustment ?? 0,
lastStockCorrectionAt: result[0].lastStockCorrectionAt?.toISOString() ?? null,
pillWeightMg: result[0].pillWeightMg,
blisters,
imageUrl: result[0].imageUrl,
@@ -245,6 +251,41 @@ export async function medicationRoutes(app: FastifyInstance) {
};
});
// Stock correction endpoint - only updates stockAdjustment, preserves looseTablets
// Also sets lastStockCorrectionAt so consumed doses before this point don't count
app.patch<{ Params: { id: string }; Body: { stockAdjustment: number } }>("/medications/:id/stock-adjustment", async (req, reply) => {
const idNum = Number(req.params.id);
if (Number.isNaN(idNum)) return reply.badRequest("Invalid id");
const userId = await getUserId(req, reply);
// Verify ownership
const [existing] = await db.select().from(medications).where(and(eq(medications.id, idNum), eq(medications.userId, userId)));
if (!existing) return reply.notFound();
const { stockAdjustment } = req.body as { stockAdjustment: number };
if (typeof stockAdjustment !== "number") return reply.badRequest("stockAdjustment must be a number");
const result = await db
.update(medications)
.set({
stockAdjustment,
lastStockCorrectionAt: new Date(), // Mark when correction was made
updatedAt: new Date(),
})
.where(and(eq(medications.id, idNum), eq(medications.userId, userId)))
.returning();
if (!result.length) return reply.notFound();
return {
id: result[0].id,
stockAdjustment: result[0].stockAdjustment ?? 0,
lastStockCorrectionAt: result[0].lastStockCorrectionAt?.toISOString() ?? null,
updatedAt: result[0].updatedAt,
};
});
app.delete<{ Params: { id: string } }>("/medications/:id", async (req, reply) => {
const idNum = Number(req.params.id);
if (Number.isNaN(idNum)) return reply.badRequest("Invalid id");
@@ -339,7 +380,8 @@ export async function medicationRoutes(app: FastifyInstance) {
const packCount = row.packCount ?? 1;
const blistersPerPack = row.blistersPerPack ?? 1;
const looseTablets = row.looseTablets ?? 0;
const originalTotalPills = packCount * blistersPerPack * pillsPerBlister + looseTablets;
const stockAdjustment = row.stockAdjustment ?? 0;
const originalTotalPills = packCount * blistersPerPack * pillsPerBlister + looseTablets + stockAdjustment;
// Calculate consumption up to now (same logic as frontend)
let consumedUntilNow = 0;
+1 -1
View File
@@ -113,7 +113,7 @@ export async function shareRoutes(app: FastifyInstance) {
// Parse takenBy JSON array
const takenByArray = parseTakenByJson(med.takenByJson);
const totalPills = med.packCount * med.blistersPerPack * med.pillsPerBlister + med.looseTablets;
const totalPills = med.packCount * med.blistersPerPack * med.pillsPerBlister + med.looseTablets + (med.stockAdjustment ?? 0);
return {
id: med.id,
name: med.name,