diff --git a/document/06-traps.md b/document/06-traps.md
index 81081a1..344b826 100755
--- a/document/06-traps.md
+++ b/document/06-traps.md
@@ -718,3 +718,28 @@ 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.
+
+## A parameter with a default that every caller leaves out
+
+Today read **Stock: 0 doses** for a compound with a full pack of sixty tablets
+on the shelf.
+
+`stockFor` takes a container and defaults it to `"vial"`, which was right when
+a vial was the only thing there was. Spray bottles added the parameter; tablets
+added a third value for it. All three call sites went on omitting it, so every
+figure on every screen counted vials and nothing else. Nothing failed, nothing
+warned, and the zero looked like an empty shelf rather than like a question
+that was never asked.
+
+Same shape as the container the log form picks a dose from, one file over. A
+default is a decision made once and inherited silently by everybody who does
+not know a decision is being made.
+
+The rule: **when a parameter gains a value, grep the call sites that omit it,
+not the ones that pass it.** The callers that already pass something have been
+thought about. The ones relying on the default have not, and they are invisible
+in a search for the parameter's name.
+
+`needsReconstitution` was the same sentence one line further down: with the
+container finally arriving, it had to stop being true for a pack, which has
+nothing to make up.
diff --git a/src/app/page.tsx b/src/app/page.tsx
index db48817..41948dd 100755
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -40,7 +40,7 @@ import {
startOfLocalDay,
unloggedDoseTimes,
} from "@/lib/calc/schedule";
-import { daysOfSupplyForProtocol, vialConcentration } from "@/lib/calc/inventory";
+import { containerForDose, daysOfSupplyForProtocol, vialConcentration } from "@/lib/calc/inventory";
import { routeHasSite, siteChoices, suggestSite } from "@/lib/calc/sites";
import {
currentStreak,
@@ -236,7 +236,17 @@ export default function NowPage() {
const targetMcg = scheduledDoseMcg(protocol, now);
const due = dueStatus(protocol, now, { lastLoggedAt });
- const stock = stockFor(vials, protocol.peptideId, targetMcg, now);
+ /*
+ * From the container this compound is actually taken out of. Left to its
+ * default this counted vials only, so a protocol on tablets read
+ * "0 doses" with a full pack on the shelf.
+ */
+ const stock = stockFor(
+ vials,
+ protocol.peptideId,
+ targetMcg,
+ now,
+ containerForDose(peptide?.preparation, protocol.route));
/**
* What "100% of a single-dose peak" is measured against.
@@ -957,7 +967,7 @@ export default function NowPage() {
{track.lastLoggedAt ? relativeTime(track.lastLoggedAt, now) : t("now_never")}
- {track.lastLog?.site && (
+ {track.lastLog?.site && routeHasSite(track.lastLog.route) && (
{" ยท "}
{siteLabel(track.lastLog.site)}
diff --git a/src/app/stock/page.tsx b/src/app/stock/page.tsx
index a2ad982..e9c8c75 100755
--- a/src/app/stock/page.tsx
+++ b/src/app/stock/page.tsx
@@ -25,6 +25,7 @@ import { useSyringeScale } from "@/components/DoseMarks";
import {
diluentAfterTopUp,
groupSealedVials,
+ containerForDose,
marksFromVial,
openPack,
stockFor,
@@ -170,14 +171,18 @@ export default function StockPage() {
if (hit) return hit;
const p = protocols.find((x) => x.active && x.peptideId === peptideId);
+ const container = containerForDose(findPeptide(custom, peptideId)?.preparation, p?.route ?? "subcutaneous");
const out: SupplyOutlook = p
- ? supplyOutlook(stockFor(vials, peptideId, scheduledDoseMcg(p, now), now), p, now)
+ ? supplyOutlook(
+ stockFor(vials, peptideId, scheduledDoseMcg(p, now), now, container),
+ p,
+ now)
: { kind: "unknown" };
cache.set(peptideId, out);
return out;
};
- }, [protocols, vials, now]);
+ }, [protocols, vials, custom, now]);
if (!hydrated) {
return {t("loading")}
;
diff --git a/src/components/LogDoseSheet.tsx b/src/components/LogDoseSheet.tsx
index a1df027..a73fbbe 100755
--- a/src/components/LogDoseSheet.tsx
+++ b/src/components/LogDoseSheet.tsx
@@ -326,7 +326,7 @@ export function LogDoseSheet({
// What the stock looks like once this dose is taken. This is the number the
// user actually wants: how many more of these are left.
- const stock = stockFor(vials, peptideId, doseMcg, at);
+ const stock = stockFor(vials, peptideId, doseMcg, at, container);
const willDeplete = !skipped && !!vialId && doseMcg > 0;
const dosesAfter = Math.max(0, stock.dosesRemaining - (willDeplete ? 1 : 0));
const vialLeftAfter = vial
diff --git a/src/lib/calc/inventory.test.ts b/src/lib/calc/inventory.test.ts
index 2696ea4..2db9173 100755
--- a/src/lib/calc/inventory.test.ts
+++ b/src/lib/calc/inventory.test.ts
@@ -938,3 +938,35 @@ describe("opening a pack", () => {
expect(pickVialForDose(vials, "klow", 10_000, NOW, "pack")?.id).toBe("started");
});
});
+
+/*
+ * The container has to be asked for. It defaults to a vial, and a screen that
+ * leaves it out counts nothing for a compound sold as tablets: Today read
+ * "0 doses" with a full pack on the shelf.
+ */
+describe("stockFor and the container", () => {
+ const pack = (over: Partial = {}) =>
+ vial({ id: "p", container: "pack", mgPerTablet: 10, strengthMg: 600, state: "sealed", ...over });
+
+ it("counts nothing for a pack when asked about vials", () => {
+ expect(stockFor([pack()], "klow", 10_000, NOW).dosesRemaining).toBe(0);
+ });
+
+ it("counts the pack when asked about packs", () => {
+ const s = stockFor([pack()], "klow", 10_000, NOW, "pack");
+ expect(s.dosesRemaining).toBe(60);
+ expect(s.availableMcg).toBe(600_000);
+ });
+
+ /* Nothing to make up, so nobody is told to reach for the water. */
+ it("never asks for a pack to be reconstituted", () => {
+ expect(stockFor([pack()], "klow", 10_000, NOW, "pack").needsReconstitution).toBe(false);
+ expect(stockFor([vial({ id: "v" })], "klow", 10_000, NOW).needsReconstitution).toBe(true);
+ });
+
+ it("leaves a vial and a pack of the same compound out of each other's count", () => {
+ const both = [pack(), vial({ id: "v", strengthMg: 10 })];
+ expect(stockFor(both, "klow", 10_000, NOW, "pack").dosesRemaining).toBe(60);
+ expect(stockFor(both, "klow", 10_000, NOW).dosesRemaining).toBe(1);
+ });
+});
diff --git a/src/lib/calc/inventory.ts b/src/lib/calc/inventory.ts
index cec4c3e..7bae828 100755
--- a/src/lib/calc/inventory.ts
+++ b/src/lib/calc/inventory.ts
@@ -405,7 +405,12 @@ export function stockFor(
openCount,
dosesRemaining: per(availableMcg),
dosesInOpenVials: per(openMcg),
- needsReconstitution: openCount === 0 && sealedCount > 0,
+ /*
+ * Only ever true of a vial. A pack and a spray bottle have nothing to make
+ * up, so telling their owner to reach for the water would be advice about
+ * a container they are not holding.
+ */
+ needsReconstitution: container === "vial" && openCount === 0 && sealedCount > 0,
dosesExpired: per(expiredMcg),
};
}
diff --git a/src/lib/store.ts b/src/lib/store.ts
index 52460a7..affc20a 100755
--- a/src/lib/store.ts
+++ b/src/lib/store.ts
@@ -41,6 +41,7 @@ import {
reconcileVials,
returnToVial,
stockFor as computeStock,
+ type ContainerKind,
vialConcentration,
vialExpired,
vialFractionRemaining,
@@ -966,9 +967,20 @@ export function vialStatus(vial: Vial, nowMs = Date.now()): VialStatus {
};
}
-/** Doses of a peptide still available across every usable vial. */
-export function stockFor(vials: Vial[], peptideId: string, doseMcg: number, nowMs = Date.now()) {
- return computeStock(vials, peptideId, doseMcg, nowMs);
+/**
+ * Doses of a peptide still available across every usable container.
+ *
+ * The container has to be passed, not defaulted, wherever the answer is shown
+ * to somebody: the default is a vial, and a caller that leaves it out counts
+ * nothing for a compound that comes as tablets or lives in a spray bottle.
+ */
+export function stockFor(
+ vials: Vial[],
+ peptideId: string,
+ doseMcg: number,
+ nowMs = Date.now(),
+ container: ContainerKind = "vial") {
+ return computeStock(vials, peptideId, doseMcg, nowMs, container);
}
export { vialCapacityMcg, pickVialForDose } from "./calc/inventory";