diff --git a/src/components/medications/sections/inventory-section.tsx b/src/components/medications/sections/inventory-section.tsx index 6473f207f..c31979542 100644 --- a/src/components/medications/sections/inventory-section.tsx +++ b/src/components/medications/sections/inventory-section.tsx @@ -75,6 +75,7 @@ import { apiPost, apiPut, } from "@/lib/api/api-fetch"; +import { startsInUseClock } from "@/lib/medications/inventory/clock-container-types"; import type { SupplySummary } from "@/lib/medications/inventory/summary"; /** @@ -169,15 +170,6 @@ const STATE_BADGE: Record< USED_UP: "outline", }; -/** - * Container kinds whose first opening starts a 30-day degradation clock. - * Mirrors `IN_USE_CLOCK_CONTAINER_TYPES` in - * `src/lib/medications/inventory/state-machine.ts` — the server owns the - * transition; the client only needs to know which helper text to show - * beside the opening date. - */ -const CLOCK_CONTAINER_TYPES: readonly ContainerType[] = ["PEN", "AMPOULE"]; - /** Default container kind for the register flow, from the delivery form. */ function defaultContainerType(deliveryForm: string | undefined): ContainerType { if (deliveryForm === "INJECTION") return "PEN"; @@ -971,7 +963,7 @@ export function AdjustInventoryDialog({ id="inventory-edit-opened-helper" className="text-muted-foreground text-xs" > - {CLOCK_CONTAINER_TYPES.includes(item.containerType) + {startsInUseClock(item.containerType) ? t("medications.detail.bestand.editOpenedHelperClock") : t("medications.detail.bestand.editOpenedHelper")}

diff --git a/src/lib/medications/inventory/clock-container-types.ts b/src/lib/medications/inventory/clock-container-types.ts new file mode 100644 index 000000000..ed37c367f --- /dev/null +++ b/src/lib/medications/inventory/clock-container-types.ts @@ -0,0 +1,33 @@ +/** + * The one owner of "which container kinds start a degradation clock when + * they are first opened". + * + * The state machine decides the transition and the settings client decides + * which helper text to put beside the opening date. Both need the same + * list, but the state machine types itself against the generated Prisma + * client, which a browser bundle cannot carry — so the client kept its own + * copy of the two literals with a comment promising they matched. Nothing + * held the promise, and a release spent fixing a fact that drifted is a bad + * place to keep a second one. + * + * So the list lives here: string literals and a pure predicate, no imports, + * safe on either side of the boundary. `state-machine.ts` re-exports it + * under the Prisma enum (`satisfies`), which turns a renamed or dropped + * container kind into a compile error rather than a member that silently + * stops matching. The per-kind reasoning stays in the state machine's + * docblock, where the transition it drives is. + */ + +export const IN_USE_CLOCK_CONTAINER_TYPES = ["PEN", "AMPOULE"] as const; + +/** + * Does opening this container start the in-use window? Takes a loose + * `string` so the client can ask about a container kind it read off the + * wire without narrowing first; an unrecognised kind answers `false`, + * which is the honest side (no deadline nobody entered). + */ +export function startsInUseClock(containerType: string): boolean { + return (IN_USE_CLOCK_CONTAINER_TYPES as readonly string[]).includes( + containerType, + ); +} diff --git a/src/lib/medications/inventory/state-machine.ts b/src/lib/medications/inventory/state-machine.ts index 10b49884c..e3c5ca27f 100644 --- a/src/lib/medications/inventory/state-machine.ts +++ b/src/lib/medications/inventory/state-machine.ts @@ -79,6 +79,10 @@ import type { MedicationContainerType, MedicationInventoryState, } from "@/generated/prisma/client"; +import { + IN_USE_CLOCK_CONTAINER_TYPES as CLOCK_CONTAINER_TYPES, + startsInUseClock, +} from "./clock-container-types"; /** Default in-use window per EMA EPAR §6.3 for the strictest GLP-1 * agonists (Mounjaro, Saxenda). 30 days. */ @@ -90,17 +94,18 @@ export const DEFAULT_IN_USE_WINDOW_DAYS = 30; * Every other kind holds individually sealed units (or an unknown * arrangement) and expires only on its printed date. See the module * docblock for the per-member reasoning. + * + * The list itself lives in `./clock-container-types`, which the settings + * client can import; `satisfies` here is the drift guard, so a container + * kind renamed or dropped from the column stops compiling instead of + * quietly never matching. */ -export const IN_USE_CLOCK_CONTAINER_TYPES = [ - "PEN", - "AMPOULE", -] as const satisfies readonly MedicationContainerType[]; +export const IN_USE_CLOCK_CONTAINER_TYPES = + CLOCK_CONTAINER_TYPES satisfies readonly MedicationContainerType[]; /** Does opening this container start the in-use window? */ export function hasInUseClock(containerType: MedicationContainerType): boolean { - return (IN_USE_CLOCK_CONTAINER_TYPES as readonly string[]).includes( - containerType, - ); + return startsInUseClock(containerType); } const MS_PER_DAY = 24 * 60 * 60 * 1000;