From 68660202cfe21ec669c826b7a4ab6a20bf37fce2 Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Fri, 26 Dec 2025 23:06:02 +0100 Subject: [PATCH] feat(medications): enhance medication stock calculation and improve UI layout for better readability --- backend/src/routes/medications.ts | 36 +++++++++++++++++++++++++------ frontend/src/App.tsx | 2 +- frontend/src/styles.css | 31 ++++++++++++++++++++++++-- 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/backend/src/routes/medications.ts b/backend/src/routes/medications.ts index b75ce57..4f0a9f1 100644 --- a/backend/src/routes/medications.ts +++ b/backend/src/routes/medications.ts @@ -302,6 +302,8 @@ export async function medicationRoutes(app: FastifyInstance) { const userId = getUserId(req); const rows = await db.select().from(medications).where(eq(medications.userId, userId)).orderBy(medications.id); + const now = new Date(); + const payload = rows.map((row) => { const slices = parseSlices(row); const usageTotal = calculateUsageInRange(slices, start, end); @@ -309,17 +311,39 @@ export async function medicationRoutes(app: FastifyInstance) { const packCount = row.packCount ?? 1; const stripsPerPack = row.stripsPerPack ?? row.strips ?? 1; const looseTablets = row.looseTablets ?? 0; - const totalPills = row.count; + const originalTotalPills = packCount * stripsPerPack * tabsPerStrip + looseTablets; + // Calculate consumption up to now (same logic as frontend) + let consumedUntilNow = 0; + slices.forEach((slice) => { + const sliceStart = new Date(slice.start); + if (Number.isNaN(sliceStart.getTime()) || sliceStart > now) return; + const msPerDay = 86400000; + const period = Math.max(1, slice.every) * msPerDay; + const occurrences = Math.floor((now.getTime() - sliceStart.getTime()) / period) + 1; + consumedUntilNow += occurrences * slice.usage; + }); + + const currentPills = Math.max(0, originalTotalPills - consumedUntilNow); const stripsNeeded = tabsPerStrip > 0 ? Math.ceil(usageTotal / tabsPerStrip) : 0; - const fullBlisters = packCount * stripsPerPack; - const loosePills = looseTablets; - const totalAvailablePills = fullBlisters * tabsPerStrip + loosePills; - const enough = totalAvailablePills >= usageTotal; + + // Calculate current stock using realistic consumption order (loose first, then blisters) + const consumed = originalTotalPills - currentPills; + const looseConsumed = Math.min(consumed, looseTablets); + const loosePillsRemaining = looseTablets - looseConsumed; + const blisterPillsConsumed = consumed - looseConsumed; + const originalBlisterPills = originalTotalPills - looseTablets; + const blisterPillsRemaining = Math.max(0, originalBlisterPills - blisterPillsConsumed); + + const fullBlisters = tabsPerStrip > 0 ? Math.floor(blisterPillsRemaining / tabsPerStrip) : 0; + const openBlisterPills = tabsPerStrip > 0 ? blisterPillsRemaining % tabsPerStrip : 0; + const loosePills = loosePillsRemaining + openBlisterPills; // Combine open blister + remaining loose + + const enough = currentPills >= usageTotal; return { medicationId: row.id, medicationName: row.name, - totalPills, + totalPills: currentPills, plannerUsage: usageTotal, stripSize: tabsPerStrip, stripsNeeded, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2395b7a..fb01725 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1293,7 +1293,7 @@ function AppContent() { return (
med && setSelectedMed(med)}> {row.medicationName} - {row.plannerUsage} {t('common.pills')} + {row.plannerUsage} {t('common.pills')} {row.stripsNeeded} × {row.stripSize} {row.fullBlisters} {t('common.blisters')}{row.loosePills > 0 && ` + ${row.loosePills} ${t('common.pills')}`} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 4b5dcb6..96f09cd 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -179,7 +179,7 @@ body { border-radius: 14px; padding: 1.25rem; box-shadow: 0 14px 36px var(--shadow); - overflow-x: auto; + overflow: hidden; transition: background 200ms ease, border-color 200ms ease; } @@ -575,7 +575,7 @@ textarea { .card p { margin: 0; } -.table { width: 100%; display: flex; flex-direction: column; gap: 0; margin-top: 0.5rem; overflow-x: auto; } +.table { width: 100%; display: flex; flex-direction: column; gap: 0; margin-top: 0.5rem; } .table-head, .table-row { display: grid; grid-template-columns: minmax(180px, 2fr) 100px 140px 140px 120px; @@ -696,6 +696,33 @@ textarea { margin-right: 1rem; flex-shrink: 0; } + /* First span (name cell) - centered horizontal layout */ + .table-row span:first-child { + justify-content: center; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border-primary); + margin-bottom: 0.25rem; + } + .table-row span:first-child::before { + display: none; /* Hide "NAME" label on mobile */ + } + /* Avatar + name layout - horizontal centered */ + .table-row .cell-with-avatar { + display: inline-flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: 0.4rem; + } + .table-row .cell-with-avatar .med-avatar { + flex-shrink: 0; + } + /* Badges and icons wrap to next line if needed but stay together */ + .table-row .taken-by-badge, + .table-row .reminder-icon, + .table-row .notes-icon { + flex-shrink: 0; + } .table-4 .table-head, .table-4 .table-row, .table-5 .table-head, .table-5 .table-row, .table-6 .table-head, .table-6 .table-row,