From d36a6a8e18b0a56d4f162c5d1e07d711b7c0e853 Mon Sep 17 00:00:00 2001 From: suskozaver Date: Thu, 17 Sep 2026 19:54:33 +0200 Subject: [PATCH] Pick the pack a tablet dose comes out of Reported as still unable to enter Tablets, with the form saying no pack was in stock. The pack was in stock, the compound was marked as tablets, and the dropdown listed it. Which container a dose comes out of was written three times in LogDoseSheet: once for the list to choose from, once when a protocol is applied, once when the route is changed by hand. Tablets taught the first and left the other two reading route === "intranasal" ? "spray" : "vial". So the list offered the pack and nothing ever selected it, vialId stayed empty, and everything downstream reported an empty shelf. Sprays had survived the same shape only because a nasal dose is picked by route, which both copies already knew about. The rule now has a name, containerForDose(preparation, route), in calc/inventory.ts with a test, and the three call sites ask it. The route decides first and the preparation second, which is the order the two facts deserve: a nasal dose comes out of a bottle whatever the library says, while oral covers a solution swallowed out of a syringe as well as a tablet and cannot decide anything on its own. The words around the picker were the same omission. Nothing is drawn out of a box of tablets and no pack is filled, so the label, the hint and the empty-shelf notice each gained their third case. Written into 06-traps.md: a rule with three call sites and no name has no place to add a case to. --- document/06-traps.md | 23 +++++++++++++++++ src/components/LogDoseSheet.tsx | 46 +++++++++++++++++++++++++-------- src/lib/calc/inventory.test.ts | 37 ++++++++++++++++++++++++++ src/lib/calc/inventory.ts | 23 ++++++++++++++++- src/lib/i18n/translations.ts | 20 +++++++++++--- 5 files changed, 133 insertions(+), 16 deletions(-) diff --git a/document/06-traps.md b/document/06-traps.md index 8d8bf4a..81081a1 100755 --- a/document/06-traps.md +++ b/document/06-traps.md @@ -695,3 +695,26 @@ sentence has both an `n` and an amount, the amount is `formatDose`. Worth checking the neighbours when this shape appears. The log sheet's "n more doses of {dose} in this vial after this one" was already using `formatDose` and was correct. + +## A rule stated in three places, extended in one + +Tablets shipped and the form for logging a dose said **No pack of this in +stock** with the pack sitting on the shelf. The pack was there, the compound +was marked as tablets, the dropdown even listed it. + +Which container a dose comes out of was written three times in +`LogDoseSheet.tsx`: once for the list of containers to choose from, once when a +protocol is applied, once when the route is changed by hand. Adding tablets +taught the first one and left the other two saying `route === "intranasal" ? +"spray" : "vial"`. So the list offered the pack and nothing ever selected it, +`vialId` stayed empty, and every sentence downstream of `vial` reported an +empty shelf. The same shape had been survivable for sprays only because a +nasal dose is picked by route, which both copies already knew about. + +The rule is now `containerForDose(preparation, route)` in `calc/inventory.ts`, +with a test, and the three call sites ask it. + +The general shape: **a rule with three call sites and no name has no place to +add a case to.** When a third value joins a two-value decision, the first thing +to look for is the other spellings of that decision. `grep` for the ternary, +not for the function, because there is no function yet, which is the problem. diff --git a/src/components/LogDoseSheet.tsx b/src/components/LogDoseSheet.tsx index 0576545..a4bf822 100755 --- a/src/components/LogDoseSheet.tsx +++ b/src/components/LogDoseSheet.tsx @@ -14,12 +14,12 @@ import { isBlend } from "@/lib/calc/blend"; import { allPeptides, findPeptide, useProfileData, useStore, vialStatus } from "@/lib/store"; import { AddCompoundInline } from "./AddCompoundInline"; import { + containerForDose, matchesContainer, pickVialForDose, stockFor, vialRemainingMcg, vialUsable, - type ContainerKind, } from "@/lib/calc/inventory"; import { mcgForTablets, mcgPerTablet, tabletsForDose } from "@/lib/calc/tablet"; import { mcgForSprays, mcgPerSpray, mlForSprays, routeChoices, spraysForDose } from "@/lib/calc/spray"; @@ -93,6 +93,15 @@ export function LogDoseSheet({ * Shared by both directions of the pair of dropdowns, so picking a protocol * and picking a peptide that happens to have one cannot drift apart. */ + /** + * The rule above, asked with a peptide id rather than with a peptide. + * + * These handlers run before the render that computes `container`, so they + * have to ask for themselves. They ask the same function. + */ + const containerFor = (forPeptideId: string, forRoute: Route) => + containerForDose(findPeptide(custom, forPeptideId)?.preparation, forRoute); + function applyProtocol(proto: Protocol | undefined, forPeptideId: string, atMs: number) { const dose = proto ? scheduledDoseMcg(proto, atMs) : 0; /* @@ -109,10 +118,11 @@ export function LogDoseSheet({ const nextRoute = proto?.route ?? (onlySpray ? "intranasal" : "subcutaneous"); setDoseMcg(dose); setRoute(nextRoute); - // A nasal protocol draws from a bottle and never from a vial, so the - // container is decided by the route rather than by whatever is nearest. + // A nasal protocol draws from a bottle and never from a vial, and a + // compound sold as tablets comes out of a pack whatever the route says, so + // the container is decided by the rule rather than by whatever is nearest. setVialId( - pickVialForDose(vials, forPeptideId, dose, atMs, nextRoute === "intranasal" ? "spray" : "vial") + pickVialForDose(vials, forPeptideId, dose, atMs, containerFor(forPeptideId, nextRoute)) ?.id ?? ""); setSiteOverride(false); setSite(suggestSite(logs.filter((l) => l.peptideId === forPeptideId), atMs, 14, proto?.sites)); @@ -139,8 +149,8 @@ export function LogDoseSheet({ */ function chooseRoute(next: Route) { setRoute(next); - const want = next === "intranasal" ? "spray" : "vial"; - setVialId(pickVialForDose(vials, peptideId, doseMcg, at, want)?.id ?? ""); + setVialId( + pickVialForDose(vials, peptideId, doseMcg, at, containerFor(peptideId, next))?.id ?? ""); } /** @@ -215,7 +225,7 @@ export function LogDoseSheet({ const tablets = peptide?.preparation === "tablet"; /** No barrel, no marks, no injection site. */ const noBarrel = nasal || tablets; - const container: ContainerKind = nasal ? "spray" : tablets ? "pack" : "vial"; + const container = containerForDose(peptide?.preparation, route); /* * What the library says, plus intranasal once a spray bottle of this compound @@ -574,7 +584,7 @@ export function LogDoseSheet({ carry it out. */ !vial - ? t("log_no_pack_in_stock") + ? t("log_tablets_no_pack") : mcgPerTablet(vial) > 0 ? t("log_per_tablet_hint", { dose: formatDose(mcgPerTablet(vial)) }) : t("log_no_tablet_size") @@ -750,9 +760,19 @@ export function LogDoseSheet({ {usableVials.length > 0 ? (