mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
Publish the resolved per-slot dose units and a slot-aware runway on the medication wire
A medication whose slots carry different doses (a whole tablet in the morning, a half at noon — #219) was consumed correctly but reported wrongly: the wire shipped only the raw nullable per-slot column (clients had to derive the inheritance), stockDosesRemaining divided by the medication-level units alone, and no runway figure existed outside the low-stock push. - Every schedule now carries resolvedUnitsPerDose, resolved server-side exactly like the intake consumption resolver. - stockDosesRemaining divides the units pool by the schedule-weighted average units per dose. - runwayDays (whole days of supply under the slot-aware burn rate) rides the list AND detail payloads; the detail GET also gains the stock aggregates the list already had. - The low-stock engine runs the same slot-aware math, so the push and the wire cannot disagree; the web cards, table, and detail Übersicht prefer the server figure over their client estimate. - OpenAPI regenerated with the new fields.
This commit is contained in:
1 parent
542ca91be5
commit
4b009c3a45
15 files changed
+510
-85
No files matched your search
@@ -110,7 +110,12 @@ export const GET = apiHandler(
|
||||
prefsRow?.notificationPrefs ?? null,
|
||||
medication.reorderLeadDays,
|
||||
);
|
||||
const schedules: RunwaySchedule[] = medication.schedules;
|
||||
const schedules: RunwaySchedule[] = medication.schedules.map((sched) => ({
|
||||
...sched,
|
||||
// Decimal → number so the slot-aware runway math reads the wire shape.
|
||||
unitsPerDose:
|
||||
sched.unitsPerDose === null ? null : Number(sched.unitsPerDose),
|
||||
}));
|
||||
/** runway ≤ effective trigger AND the alert is enabled. */
|
||||
const isLowStock = (dosesRemaining: number): boolean => {
|
||||
if (runwayFloor === null) return false;
|
||||
|
||||
@@ -33,6 +33,10 @@ import {
|
||||
recomputeMedicationComplianceForDay,
|
||||
} from "@/lib/rollups/medication-compliance-rollups";
|
||||
import { assertMedicationOwnership } from "@/lib/medications/route-guards";
|
||||
import {
|
||||
effectiveUnitsPerDose,
|
||||
estimateUnitsRunwayDays,
|
||||
} from "@/components/medications/detail/supply-runway";
|
||||
import { serializeScheduleUnitsPerDose } from "@/lib/medications/schedule-units-dto";
|
||||
import { hhmmToMinutesOrNull } from "@/lib/medications/scheduling/hhmm";
|
||||
import { getUserTodayBounds } from "@/lib/tz/local-day";
|
||||
@@ -177,13 +181,59 @@ export const GET = apiHandler(
|
||||
},
|
||||
});
|
||||
|
||||
// v1.37.19 — stock + runway on the detail wire, mirroring the list
|
||||
// payload's semantics: NULL stock = inventory tracking off, 0 = the
|
||||
// supply ran out; runwayDays via the slot-aware burn rate. Published
|
||||
// resolved so no client re-derives the inheritance / cadence math.
|
||||
const [usableStock, anyItemCount] = await Promise.all([
|
||||
prisma.medicationInventoryItem.aggregate({
|
||||
where: {
|
||||
userId: user.id,
|
||||
medicationId: id,
|
||||
state: { in: ["ACTIVE", "IN_USE"] },
|
||||
unitsRemaining: { gt: 0 },
|
||||
},
|
||||
_sum: { unitsRemaining: true },
|
||||
}),
|
||||
prisma.medicationInventoryItem.count({
|
||||
where: { userId: user.id, medicationId: id },
|
||||
}),
|
||||
]);
|
||||
const schedulesDto = serializeScheduleUnitsPerDose(
|
||||
medication.schedules,
|
||||
medication.unitsPerDose,
|
||||
);
|
||||
const stockUnitsRemaining =
|
||||
anyItemCount > 0 ? Number(usableStock._sum.unitsRemaining ?? 0) : null;
|
||||
const stockDosesRemaining =
|
||||
stockUnitsRemaining === null
|
||||
? null
|
||||
: Math.floor(
|
||||
stockUnitsRemaining /
|
||||
effectiveUnitsPerDose(
|
||||
schedulesDto,
|
||||
Number(medication.unitsPerDose),
|
||||
),
|
||||
);
|
||||
const runwayDays =
|
||||
stockUnitsRemaining === null
|
||||
? null
|
||||
: estimateUnitsRunwayDays(
|
||||
stockUnitsRemaining,
|
||||
schedulesDto,
|
||||
Number(medication.unitsPerDose),
|
||||
);
|
||||
|
||||
return apiSuccess({
|
||||
...medication,
|
||||
unitsPerDose: Number(medication.unitsPerDose),
|
||||
schedules: serializeScheduleUnitsPerDose(medication.schedules),
|
||||
schedules: schedulesDto,
|
||||
category,
|
||||
nextDueAt: display ? display.at.toISOString() : null,
|
||||
nextDueOverdue: display?.overdue ?? false,
|
||||
stockUnitsRemaining,
|
||||
stockDosesRemaining,
|
||||
runwayDays,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -836,7 +886,10 @@ export const PUT = apiHandler(
|
||||
return apiSuccess({
|
||||
...medication,
|
||||
unitsPerDose: Number(medication.unitsPerDose),
|
||||
schedules: serializeScheduleUnitsPerDose(medication.schedules),
|
||||
schedules: serializeScheduleUnitsPerDose(
|
||||
medication.schedules,
|
||||
medication.unitsPerDose,
|
||||
),
|
||||
category: normalizedCategory,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -95,7 +95,10 @@ async function respondWithExistingMirror(
|
||||
...medication,
|
||||
unitsPerDose: Number(medication.unitsPerDose),
|
||||
// #219 — Decimal → number for the per-schedule column too.
|
||||
schedules: serializeScheduleUnitsPerDose(medication.schedules),
|
||||
schedules: serializeScheduleUnitsPerDose(
|
||||
medication.schedules,
|
||||
Number(medication.unitsPerDose),
|
||||
),
|
||||
category,
|
||||
});
|
||||
}
|
||||
@@ -413,7 +416,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
...medication,
|
||||
unitsPerDose: Number(medication.unitsPerDose),
|
||||
// #219 — Decimal → number for the per-schedule column too.
|
||||
schedules: serializeScheduleUnitsPerDose(medication.schedules),
|
||||
schedules: serializeScheduleUnitsPerDose(
|
||||
medication.schedules,
|
||||
medication.unitsPerDose,
|
||||
),
|
||||
category: normalizedCategory,
|
||||
},
|
||||
201,
|
||||
|
||||
Reference in new issue
Block a user