feat(medications): accept units per dose like 1.5 and let the editor enter them

A medication's units per dose, a schedule's override and a restored
backup now accept any value above 0 and at most 100 with at most four
decimal places, which is exactly what the Decimal(10,4) column stores.
Before, only whole numbers 1-100 and the fractions 1/4, 1/3, 1/2, 2/3,
3/4 passed, so one and a half tablets could not be recorded. A restore
now refuses a value no write route would accept.

The medication editor keeps its buttons and adds an Other field that
reads 1.5, 1,5, 1 1/2 and 1½, holds the step while the text does not
read, and previews the value it will save. The supply tab's packaging
dialog uses the same reader; it refused every non-whole value before,
so a medication at half a tablet could not have its packaging saved.
Cards and the supply tab show a whole number plus a fraction as 1½.

The rule, the input reader and the display format live in one module
without imports so the editor and the validators share them.

Refs #1034
This commit is contained in:
Marc-André Bombeck committed 2026-09-25 09:58:55 +02:00
1 parent e03cf7c2c8
commit 1e040605be
33 files changed
+1030 -276

No files matched your search

@@ -494,6 +494,42 @@ describe("POST /api/medications — as-needed (v1.16.11, #316)", () => {
expect(data.unitsPerDose).toBe(2);
});
// #1034 — a whole number plus a fraction reaches the Prisma write
// unchanged, on the medication and on a per-slot override; a value with
// a fifth decimal place is refused before any write.
it("creates a medication with a mixed units-per-dose value", async () => {
const res = await POST(
postReq({
name: "Ibuprofen",
dose: "400 mg",
category: "PAIN_RELIEF",
unitsPerDose: 1.5,
asNeeded: true,
}),
);
expect(res.status).toBe(201);
expect(lastCreateData().unitsPerDose).toBe(1.5);
});
it("422s a units-per-dose value with more than four decimal places", async () => {
const res = await POST(
postReq({
name: "Ibuprofen",
dose: "400 mg",
category: "PAIN_RELIEF",
unitsPerDose: 1.23456,
asNeeded: true,
}),
);
expect(res.status).toBe(422);
const body = (await res.json()) as {
details: { issues: Array<{ path: Array<string | number> }> };
};
expect(
body.details.issues.some((i) => i.path.includes("unitsPerDose")),
).toBe(true);
});
it("still 422s a scheduled create without any schedule (legacy contract)", async () => {
const res = await POST(postReq({ name: "Ramipril", dose: "5 mg" }));
expect(res.status).toBe(422);