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 ? (