feat(medications): enhance medication stock calculation and improve UI layout for better readability
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -1293,7 +1293,7 @@ function AppContent() {
|
||||
return (
|
||||
<div key={row.medicationId} className="table-row clickable" onClick={() => med && setSelectedMed(med)}>
|
||||
<span data-label={t('planner.table.medication')} className="cell-with-avatar"><MedicationAvatar name={row.medicationName} imageUrl={med?.imageUrl} />{row.medicationName}</span>
|
||||
<span data-label={t('planner.table.usage')}><strong>{row.plannerUsage}</strong> {t('common.pills')}</span>
|
||||
<span data-label={t('planner.table.usage')}><strong>{row.plannerUsage}</strong> {t('common.pills')}</span>
|
||||
<span data-label={t('planner.table.blisters')}>{row.stripsNeeded} × {row.stripSize}</span>
|
||||
<span data-label={t('planner.table.available')}>
|
||||
{row.fullBlisters} {t('common.blisters')}{row.loosePills > 0 && ` + ${row.loosePills} ${t('common.pills')}`}
|
||||
|
||||
+29
-2
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user