refactor(medications): give the in-use clock list one owner

The supply tab carried its own copy of the two container kinds whose first
opening starts a degradation clock, with a comment saying it mirrored the
state machine's list. Nothing enforced the mirror — the state machine types
itself against the generated Prisma client, which the browser bundle cannot
carry, so a copy was the only way to say it.

Move the list into a client-safe module with no imports and let both sides
read it. The state machine re-exports it under the Prisma enum, so a renamed
or dropped container kind is now a compile error rather than a member that
quietly stops matching.
This commit is contained in:
Marc-André Bombeck committed 2026-07-27 14:47:54 +02:00
1 parent 6ab3bc87ff
commit 146a02319d
3 files changed
+47 -17

No files matched your search

@@ -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")}
</p>
@@ -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,
);
}
+12 -7
View File
@@ -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;