From b82cfcc706359f031db29c05903b218e04278c37 Mon Sep 17 00:00:00 2001 From: suskozaver Date: Thu, 17 Sep 2026 18:52:40 +0200 Subject: [PATCH] feat: tablets, because a pack is a container like any other Asked for directly: SLU-PP-332 is not in the library and comes as tablets. The library half already worked, you add your own compound from any picker. Stock had nowhere to put it: a vial is a mass optionally dissolved in a volume, and a pack of tablets is a mass in a count. A pack is a Vial with container: "pack", which is the shape the nasal spray bottle established and for the same reason. Mass remaining, doses left, cost per dose, days of supply and the dates are all arithmetic about a mass in a container, and none of them care what the container is. strengthMg is the whole pack and mgPerTablet is the unit it is counted in: sixty tablets of 10 mg is one pack of 600 mg. Nothing downstream learns that tablets exist. Two deliberate differences from the spray beside it, both tested. There is no default tablet size. A pump has a conventional 0.1 mL, so a bottle with nothing recorded can still be counted. Tablets come in whatever the manufacturer chose, so a pack with no size counts as nothing and the row says the size is missing, which is true, rather than showing a count that is not. And a tablet can be halved. spraysForDose rounds to whole presses because a pump delivers its volume or it fails; tabletsForDose keeps the fraction, because a scored tablet really is half a dose and 15 mg from a 10 mg tablet is one and a half. What is floored is the count left in the pack, since half a tablet you have not cut is not one you can take. The log sheet stands the syringe half down for tablets exactly as it does for a nasal dose, and on a different test: a nasal dose is known by its route, a tablet by how the compound is sold, because "oral" also covers a solution somebody swallows out of a syringe and that one does want a barrel. DoseLog gains tablets, recorded rather than recomputed, for the same reason presses are: the pack can be replaced by one with a different tablet size. The schedule and the log read in tablets, the add form asks for the tablet size and how many are in the pack, a pack is never offered reconstitution, and the CSV gains a tablets column appended rather than slotted in. Fifteen tests on the arithmetic. Twenty-three keys in four languages. One more untranslated string fell out on the way: the nasal hint was a template literal saying "a press, ... mL in total" in every language. --- src/app/log/page.tsx | 8 +- src/app/stock/page.tsx | 90 +++++++++++++++++--- src/components/CustomCompoundForm.tsx | 11 ++- src/components/DoseMarks.tsx | 19 +++++ src/components/LogDoseSheet.tsx | 79 +++++++++++++++--- src/lib/calc/custom.ts | 2 +- src/lib/calc/dosecsv.ts | 2 + src/lib/calc/tablet.test.ts | 116 ++++++++++++++++++++++++++ src/lib/calc/tablet.ts | 88 +++++++++++++++++++ src/lib/i18n/translations.ts | 92 ++++++++++++++++++++ src/lib/types.ts | 24 +++++- 11 files changed, 502 insertions(+), 29 deletions(-) create mode 100644 src/lib/calc/tablet.test.ts create mode 100644 src/lib/calc/tablet.ts diff --git a/src/app/log/page.tsx b/src/app/log/page.tsx index 51f9660..9065acf 100755 --- a/src/app/log/page.tsx +++ b/src/app/log/page.tsx @@ -320,7 +320,13 @@ export default function LogPage() { older data. */} {!l.skipped && - (l.route === "intranasal" + (l.tablets != null + ? ( + + {t("count_tablets", { n: trim(l.tablets, 2) })} + + ) + : l.route === "intranasal" ? l.presses != null && ( {t("count_presses", { n: l.presses })} diff --git a/src/app/stock/page.tsx b/src/app/stock/page.tsx index d3e0eae..010c65f 100755 --- a/src/app/stock/page.tsx +++ b/src/app/stock/page.tsx @@ -51,6 +51,7 @@ import { spraysRemaining, transferToSpray, } from "@/lib/calc/spray"; +import { isPack, mgPerTablet, packStrengthMg, tabletsRemaining } from "@/lib/calc/tablet"; import { converterUrl } from "@/lib/calc/converter"; import { formatConcentration, formatDate, formatDose, trim } from "@/lib/format"; import { @@ -350,7 +351,12 @@ export default function StockPage() { currency={currency} peptideName={findPeptide(custom, v.peptideId)?.name ?? v.peptideId} onRemove={() => removeVial(v.id)} - onReconstitute={() => setReconstituting(v.id)} + /* + A pack of tablets is not made up with anything, so it is + not offered the one action that would turn it into a + solution it can never be. + */ + onReconstitute={isPack(v) ? undefined : () => setReconstituting(v.id)} /> {reconstituting === v.id && ( 0 ? marksFromVial(vial, doseMcg, scale) : null; + const marks = !spray && !pack && doseMcg > 0 ? marksFromVial(vial, doseMcg, scale) : null; const perPress = spray ? mcgPerSpray(vial) : 0; const many = (group?.count ?? 1) > 1; @@ -550,7 +557,27 @@ function VialRow({ ) : (
- {t("stock_lyophilised")} + {/* + A pack is not a lyophilised vial waiting to be made up. It is + ready, so it says what is in it and how many are left rather than + inviting a reconstitution it must never have. + */} + {pack ? ( + <> + + {t("stock_tablets_left", { n: tabletsRemaining(vial) })} + + {mgPerTablet(vial) > 0 ? ( + + {t("stock_per_tablet", { mg: trim(mgPerTablet(vial), 3) })} + + ) : ( + {t("stock_no_tablet_size")} + )} + + ) : ( + {t("stock_lyophilised")} + )} {many && ( {t("stock_mg_in_total", { mg: trim(strengthMgTotal, 2) })} @@ -714,6 +741,16 @@ function AddVialForm({ const [peptideId, setPeptideId] = useState(peptides[0]?.id ?? ""); const [strengthMg, setStrengthMg] = useState(10); const [count, setCount] = useState(1); + /** + * Tablets in one pack, asked for only when the compound comes as tablets. + * + * The strength field above then means milligrams in one tablet, and the two + * multiply into the mass of the pack. Storing the product rather than the + * pair is what lets every figure downstream, doses left, cost per dose, the + * day the shelf runs dry, go on being arithmetic about a mass in a + * container without learning that tablets exist. + */ + const [tabletsPerPack, setTabletsPerPack] = useState(30); /** * Postage for the whole order, not for each vial. * @@ -751,6 +788,8 @@ function AddVialForm({ const [arrived, setArrived] = useState(true); const peptide = peptides.find((p) => p.id === peptideId); + const tablets = peptide?.preparation === "tablet"; + const packMg = packStrengthMg(strengthMg, tabletsPerPack); // Storage is always per vial, whichever way it was entered, so nothing // downstream has to know a kit was involved. @@ -790,8 +829,14 @@ function AddVialForm({
setStrengthMg(Number(e.target.value))} /> - + {/* Only for tablets, so nobody counting vials is asked about packs. */} + {tablets && ( + + setTabletsPerPack(Number(e.target.value))} + /> + + )} + { const base: Omit = { peptideId, - strengthMg, + /* + * A pack is stored as the mass it holds, which is the tablet + * size times how many are in it. Everything downstream then + * treats it as any other container, and the tablet size is kept + * beside it as the unit to count in. + */ + strengthMg: tablets ? packMg : strengthMg, + ...(tablets ? { container: "pack" as const, mgPerTablet: strengthMg } : {}), state: arrived ? "sealed" : "on-order", supplier: supplier.trim() || undefined, cost: perVial, @@ -959,11 +1022,16 @@ function AddVialForm({ ? null : { cost: Number(shipping), currency: payCurrency }); }} - disabled={!peptideId || !(strengthMg > 0)} + disabled={!peptideId || !(tablets ? packMg > 0 : strengthMg > 0)} > - {arrived - ? t("stock_add_vials", { vials: t("count_vials", { n: count }) }) - : t("stock_add_vials_on_order", { vials: t("count_vials", { n: count }) })} + {(() => { + const what = tablets + ? t("count_packs", { n: count }) + : t("count_vials", { n: count }); + return arrived + ? t("stock_add_vials", { vials: what }) + : t("stock_add_vials_on_order", { vials: what }); + })()}
diff --git a/src/components/CustomCompoundForm.tsx b/src/components/CustomCompoundForm.tsx index 05ef636..f6b7eeb 100755 --- a/src/components/CustomCompoundForm.tsx +++ b/src/components/CustomCompoundForm.tsx @@ -188,15 +188,22 @@ export function CustomCompoundForm({ 0) { + return ( + + {t("count_tablets", { n: trim(tabletsForDose(pack, doseMcg), 2) })} + + ); + } + const marks = marksForDose(vials, peptideId, doseMcg, scale, nowMs); if (marks == null) return null; diff --git a/src/components/LogDoseSheet.tsx b/src/components/LogDoseSheet.tsx index cf79b91..3f66cbd 100755 --- a/src/components/LogDoseSheet.tsx +++ b/src/components/LogDoseSheet.tsx @@ -19,7 +19,9 @@ import { 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"; import { suggestSite } from "@/lib/calc/sites"; import { @@ -203,6 +205,18 @@ export function LogDoseSheet({ */ const nasal = route === "intranasal"; + /* + * Tablets stand the same half of the form down, for the same reason and on a + * different test. A nasal dose is known by its route; a tablet is known by + * how the compound is sold, because "oral" also covers a solution somebody + * swallows out of a syringe, and that one does want a barrel. The pack is + * therefore chosen by the preparation rather than by the route. + */ + const tablets = peptide?.preparation === "tablet"; + /** No barrel, no marks, no injection site. */ + const noBarrel = nasal || tablets; + const container: ContainerKind = nasal ? "spray" : tablets ? "pack" : "vial"; + /* * What the library says, plus intranasal once a spray bottle of this compound * exists. Three compounds name that route, so without this a bottle made from @@ -234,9 +248,9 @@ export function LogDoseSheet({ vials.filter( (v) => v.peptideId === peptideId && - matchesContainer(v, nasal ? "spray" : "vial") && + matchesContainer(v, container) && vialUsable(v, Date.now())), - [vials, peptideId, nasal]); + [vials, peptideId, container]); // Reset the form each time it opens, prefilled from the protocol. useEffect(() => { @@ -336,7 +350,7 @@ export function LogDoseSheet({ * nobody could act on and nobody asked for. */ const draw = - !nasal && vial && vial.diluentMl + !noBarrel && vial && vial.diluentMl ? calculateDraw({ vialMcg: mgToMcg(vial.strengthMg), diluentMl: vial.diluentMl, @@ -353,6 +367,17 @@ export function LogDoseSheet({ * Working it out afterwards from the bottle would be answering with today's * concentration a question about last Tuesday's. */ + /* + * The count of tablets this dose was, recorded rather than worked out later. + * Same reasoning as the presses below it: the pack can be replaced by one + * with a different tablet size, and a count derived afterwards would answer + * with today's pack a question about last Tuesday's. + */ + const tabletCount = + tablets && vial && tabletsForDose(vial, doseMcg) > 0 + ? Number(tabletsForDose(vial, doseMcg).toFixed(3)) + : undefined; + const nasalPresses = nasal && vial && spraysForDose(vial, doseMcg) > 0 ? spraysForDose(vial, doseMcg) @@ -381,6 +406,7 @@ export function LogDoseSheet({ volumeMl: draw?.volumeRoundedMl, units: draw?.unitsRounded, presses: nasalPresses, + tablets: tabletCount, // Both, and the id is the one that carries the capacity and the marks. // Scale stays because it is what the Log reads and what the CSV exports. syringeId: draw ? syringe.id : undefined, @@ -405,6 +431,7 @@ export function LogDoseSheet({ volumeMl: draw?.volumeRoundedMl, units: draw?.unitsRounded, presses: nasalPresses, + tablets: tabletCount, syringeId: draw ? syringe.id : undefined, syringeScale: draw ? syringe.scale : undefined, skipped: skipped || undefined, @@ -531,7 +558,34 @@ export function LogDoseSheet({ /> - {nasal ? ( + {tablets ? ( + /* + Tablets, which is what a person counts out of a pack. Fractions + are kept, because a scored tablet really is half a dose, and + that is where a tablet parts company with a press. + */ + 0 + ? t("log_per_tablet_hint", { dose: formatDose(mcgPerTablet(vial)) }) + : t("log_no_tablet_size") + } + > + { + if (!vial) return; + setDoseMcg(Number(mcgForTablets(vial, Number(e.target.value)).toFixed(2))); + }} + /> + + ) : nasal ? ( /* Presses, because that is what a person does and counts. Whole ones only: a pump delivers its volume or it fails, so half a @@ -541,9 +595,10 @@ export function LogDoseSheet({ label={t("log_presses")} hint={ vial && mcgPerSpray(vial) > 0 - ? `${formatDose(mcgPerSpray(vial))} a press, ${trim( - mlForSprays(vial, spraysForDose(vial, doseMcg)), - 2)} mL in total.` + ? t("log_per_press_hint", { + dose: formatDose(mcgPerSpray(vial)), + ml: trim(mlForSprays(vial, spraysForDose(vial, doseMcg)), 2), + }) : t("log_fill_spray_first") } > @@ -552,7 +607,7 @@ export function LogDoseSheet({ min={0} step={1} suffix={t("log_presses_suffix")} - placeholder={vial ? undefined : "n/a"} + placeholder={vial ? undefined : t("log_na")} disabled={skipped || !vial} onChange={(e) => { if (!vial) return; @@ -586,7 +641,7 @@ export function LogDoseSheet({
- {!nasal && ( + {!noBarrel && ( - {!nasal && ( + {!noBarrel && ( )} - {draw && !nasal && ( + {draw && !noBarrel && (
diff --git a/src/lib/calc/custom.ts b/src/lib/calc/custom.ts index 68c1bb7..7ec036f 100755 --- a/src/lib/calc/custom.ts +++ b/src/lib/calc/custom.ts @@ -23,7 +23,7 @@ export interface CustomDraft { /** Blank when unknown, which is the honest default for most research peptides. */ halfLifeHours?: number | null; routes: Route[]; - preparation: "powder" | "solution"; + preparation: "powder" | "solution" | "tablet"; /** Typical dose per administration, in micrograms. */ doseLowMcg?: number; doseHighMcg?: number; diff --git a/src/lib/calc/dosecsv.ts b/src/lib/calc/dosecsv.ts index 2226881..9d5d940 100644 --- a/src/lib/calc/dosecsv.ts +++ b/src/lib/calc/dosecsv.ts @@ -34,6 +34,7 @@ export const DOSE_CSV_HEADER = [ * things, and the CSV is what somebody takes to a spreadsheet to add up. */ "presses", + "tablets", ] as const; /** Quote only when the value would otherwise break the row. */ @@ -73,6 +74,7 @@ export function doseCsv(logs: DoseLog[], nameFor: (peptideId: string) => string) (l.sideEffects ?? []).join("; "), l.notes ?? "", l.presses ?? "", + l.tablets ?? "", ] .map(escapeCsv) .join(","); diff --git a/src/lib/calc/tablet.test.ts b/src/lib/calc/tablet.test.ts new file mode 100644 index 0000000..5d9cb46 --- /dev/null +++ b/src/lib/calc/tablet.test.ts @@ -0,0 +1,116 @@ +import { describe, expect, it } from "vitest"; +import { + isPack, + mcgForTablets, + mcgPerTablet, + mgPerTablet, + packStrengthMg, + tabletsForDose, + tabletsRemaining, +} from "./tablet"; +import type { Vial } from "../types"; + +/** Sixty tablets of 10 mg, which is the pack the request was about. */ +const pack = (over: Partial = {}): Vial => ({ + id: "p1", + profileId: "me", + peptideId: "slu-pp-332", + strengthMg: 600, + state: "sealed", + container: "pack", + mgPerTablet: 10, + ...over, +}); + +describe("what a pack is", () => { + it("knows one from a vial and from a bottle", () => { + expect(isPack(pack())).toBe(true); + expect(isPack({ container: "spray" })).toBe(false); + expect(isPack({})).toBe(false); + }); + + it("is a mass, so the rest of the app needs no new unit", () => { + expect(packStrengthMg(10, 60)).toBe(600); + }); + + it("counts whole tablets into a pack and not fractions of one", () => { + expect(packStrengthMg(10, 60.7)).toBe(600); + }); + + it("refuses to build a pack out of nonsense", () => { + expect(packStrengthMg(-10, 60)).toBe(0); + expect(packStrengthMg(10, -1)).toBe(0); + }); +}); + +/* + * The one real difference from the pump beside it. A bottle with no volume + * recorded still counts, because 0.1 mL is the conventional press. Tablets have + * no conventional size, so a pack with none recorded counts as nothing and the + * screen says the size is missing. + */ +describe("a pack with no tablet size", () => { + const nameless = pack({ mgPerTablet: undefined }); + + it("reports no size rather than a guessed one", () => { + expect(mgPerTablet(nameless)).toBe(0); + expect(mcgPerTablet(nameless)).toBe(0); + }); + + it("counts nothing rather than counting wrongly", () => { + expect(tabletsRemaining(nameless)).toBe(0); + expect(tabletsForDose(nameless, 10_000)).toBe(0); + }); + + it("treats a zero or a negative size the same as none at all", () => { + expect(mgPerTablet(pack({ mgPerTablet: 0 }))).toBe(0); + expect(mgPerTablet(pack({ mgPerTablet: -5 }))).toBe(0); + }); +}); + +describe("tabletsForDose", () => { + it("answers one tablet for one tablet", () => { + expect(tabletsForDose(pack(), 10_000)).toBe(1); + }); + + /* + * Kept as a fraction, which is where a tablet parts company with a press. + * Rounding 1.5 up to 2 would be a fifth more drug than the plan asks for, and + * whether a half is takeable is a fact about the tablet rather than about the + * arithmetic. + */ + it("keeps a half, because a scored tablet really is half a dose", () => { + expect(tabletsForDose(pack(), 15_000)).toBe(1.5); + expect(tabletsForDose(pack({ mgPerTablet: 25 }), 12_500)).toBe(0.5); + }); + + it("never answers with a negative number of tablets", () => { + expect(tabletsForDose(pack(), -10_000)).toBe(0); + }); + + it("goes back the way it came", () => { + expect(mcgForTablets(pack(), tabletsForDose(pack(), 15_000))).toBeCloseTo(15_000, 6); + }); +}); + +describe("tabletsRemaining", () => { + it("counts a full pack", () => { + expect(tabletsRemaining(pack())).toBe(60); + }); + + it("takes the taken ones off", () => { + expect(tabletsRemaining(pack({ drawnMcg: 100_000 }))).toBe(50); + }); + + /* + * Floored rather than rounded. Four tablets and a half-used one answers four + * to "how many can I take", and a five would be a tablet that is not there. + */ + it("floors, because half a tablet in the pack is not a tablet", () => { + expect(tabletsRemaining(pack({ drawnMcg: 555_000 }))).toBe(4); + }); + + it("bottoms out at nothing rather than going negative", () => { + expect(tabletsRemaining(pack({ drawnMcg: 700_000 }))).toBe(0); + }); +}); diff --git a/src/lib/calc/tablet.ts b/src/lib/calc/tablet.ts new file mode 100644 index 0000000..d3467d5 --- /dev/null +++ b/src/lib/calc/tablet.ts @@ -0,0 +1,88 @@ +/** + * Tablets: a pack of them, and what one of them delivers. + * + * Asked for after somebody tried to record SLU-PP-332, which the library does + * not carry and which he had as tablets. The app could hold the compound + * straight away and had nowhere to put the tablets: stock was a mass in a + * vial, optionally dissolved in a volume, and a pack of tablets is a mass in + * a count. + * + * So a pack is a `Vial` with `container: "pack"`, which is the shape a nasal + * spray bottle already established and for the same reason: mass remaining, + * doses left, cost per dose, days of supply and the beyond-use rules are all + * arithmetic about a mass in a container, and none of them care what the + * container is. `strengthMg` is the whole pack, `mgPerTablet` is the unit it + * is counted in. Sixty tablets of 10 mg is one pack of 600 mg. + * + * Two deliberate differences from the spray beside it. + * + * There is no default tablet size. A pump has a conventional 0.1 mL, so a + * bottle with nothing recorded can still be counted; tablets come in whatever + * the manufacturer chose, so a pack with nothing recorded is counted as + * nothing rather than as a guess. The screen then says the size is missing, + * which is true, instead of a press-count that is false. + * + * And a tablet can be halved. A pump delivers a fixed volume or it fails, so + * `spraysForDose` rounds to whole presses; a scored tablet really is half a + * dose, so `tabletsForDose` keeps the fraction and lets the screen round it + * for reading. What is floored is the count left in the pack, because half a + * tablet you have not cut yet is not half a tablet you can take. + */ + +import type { Vial } from "../types"; +import { MCG_PER_MG, vialRemainingMcg } from "./inventory"; + +export const isPack = (v: Pick) => v.container === "pack"; + +/** Milligrams in one tablet, or zero when nobody recorded it. */ +export function mgPerTablet(v: Pick): number { + const mg = v.mgPerTablet; + return Number.isFinite(mg) && (mg ?? 0) > 0 ? mg! : 0; +} + +/** The mass one tablet delivers. */ +export function mcgPerTablet(v: Pick): number { + return mgPerTablet(v) * MCG_PER_MG; +} + +/** The mass a number of tablets delivers. */ +export function mcgForTablets(v: Pick, tablets: number): number { + return mcgPerTablet(v) * Math.max(0, tablets); +} + +/** + * How many tablets a dose comes to. + * + * The exact figure, fractions and all. A plan asking for 15 mg from a pack of + * 10 mg tablets is one and a half tablets, and saying "2" would be a fifth + * more drug than the plan says. Whether that fraction is takeable is a fact + * about the tablet in the hand, not about arithmetic, so the number is + * reported and the judgement is left where it belongs. + */ +export function tabletsForDose(v: Pick, doseMcg: number): number { + const per = mcgPerTablet(v); + return per > 0 ? Math.max(0, doseMcg / per) : 0; +} + +/** + * Whole tablets left in the pack. + * + * Floored, unlike the dose above. Mass remaining divided by the tablet size + * can land at 4.5 when four tablets and a half-used one are in the pack, and + * the answer to "how many can I take" is four. + */ +export function tabletsRemaining( + v: Pick): number { + const per = mcgPerTablet(v); + return per > 0 ? Math.floor(vialRemainingMcg(v) / per) : 0; +} + +/** + * The mass a pack of this many tablets holds. + * + * What the add form writes into `strengthMg`, so the pack is a mass like every + * other container and nothing downstream has to learn a second unit. + */ +export function packStrengthMg(mgEach: number, tablets: number): number { + return Math.max(0, mgEach) * Math.max(0, Math.floor(tablets)); +} diff --git a/src/lib/i18n/translations.ts b/src/lib/i18n/translations.ts index 82f2f5f..b8ca676 100644 --- a/src/lib/i18n/translations.ts +++ b/src/lib/i18n/translations.ts @@ -336,6 +336,27 @@ export const TRANSLATIONS = { stock_water_section: "Water and diluents", stock_water_section_desc: "Optional. Track bottles here and reconstituting will draw from one.", stock_add_bottle: "Add a bottle", + stock_mg_per_tablet: "Milligrams in one tablet", + dose_tablets_title: "{dose} in one tablet.", + stock_tablets_per_pack: "Tablets in the pack", + stock_how_many_packs: "How many packs", + stock_pack_holds: "One pack holds {mg} mg.", + stock_common_sizes: "Common: {sizes} mg", + stock_tablets_left: "{n} tablets left", + stock_per_tablet: "{mg} mg a tablet", + stock_no_tablet_size: "No tablet size recorded, so nothing can be counted.", + ccf_tablet: "Tablets", + ccf_tablet_hint: "Comes as tablets or capsules, counted rather than drawn. Nothing to make up, and no syringe when you log it.", + log_tablets: "Tablets", + log_tablets_suffix: "tablets", + log_per_tablet_hint: "{dose} a tablet.", + log_no_tablet_size: "Add the tablet size to the pack first.", + log_per_press_hint: "{dose} a press, {ml} mL in total.", + log_na: "n/a", + count_tablets_one: "{n} tablet", + count_tablets_other: "{n} tablets", + count_packs_one: "{n} pack", + count_packs_other: "{n} packs", stock_bottle_sealed: "sealed", stock_bottle_open: "open", stock_bottle_finished: "finished", @@ -1726,6 +1747,27 @@ export const TRANSLATIONS = { stock_water_section: "Wasser und Verdünnungsmittel", stock_water_section_desc: "Optional. Verfolge hier Flaschen und die Rekonstitution zieht aus einer davon.", stock_add_bottle: "Flasche hinzufügen", + stock_mg_per_tablet: "Milligramm pro Tablette", + dose_tablets_title: "{dose} pro Tablette.", + stock_tablets_per_pack: "Tabletten in der Packung", + stock_how_many_packs: "Wie viele Packungen", + stock_pack_holds: "Eine Packung enthält {mg} mg.", + stock_common_sizes: "Üblich: {sizes} mg", + stock_tablets_left: "{n} Tabletten übrig", + stock_per_tablet: "{mg} mg pro Tablette", + stock_no_tablet_size: "Keine Tablettengröße hinterlegt, also lässt sich nichts zählen.", + ccf_tablet: "Tabletten", + ccf_tablet_hint: "Kommt als Tabletten oder Kapseln, gezählt statt aufgezogen. Nichts anzusetzen, und beim Eintragen keine Spritze.", + log_tablets: "Tabletten", + log_tablets_suffix: "Tabletten", + log_per_tablet_hint: "{dose} pro Tablette.", + log_no_tablet_size: "Trag zuerst die Tablettengröße der Packung ein.", + log_per_press_hint: "{dose} pro Sprühstoß, {ml} mL insgesamt.", + log_na: "k.A.", + count_tablets_one: "{n} Tablette", + count_tablets_other: "{n} Tabletten", + count_packs_one: "{n} Packung", + count_packs_other: "{n} Packungen", stock_bottle_sealed: "versiegelt", stock_bottle_open: "offen", stock_bottle_finished: "leer", @@ -3131,6 +3173,31 @@ export const TRANSLATIONS = { stock_water_section: "Voda in topila", stock_water_section_desc: "Neobvezno. Tu spremljaj steklenice in rekonstitucija bo črpala iz ene.", stock_add_bottle: "Dodaj stekleničko", + stock_mg_per_tablet: "Miligramov v eni tableti", + dose_tablets_title: "{dose} v eni tableti.", + stock_tablets_per_pack: "Tablet v škatlici", + stock_how_many_packs: "Koliko škatlic", + stock_pack_holds: "Ena škatlica ima {mg} mg.", + stock_common_sizes: "Običajno: {sizes} mg", + stock_tablets_left: "ostalo {n} tablet", + stock_per_tablet: "{mg} mg na tableto", + stock_no_tablet_size: "Velikost tablete ni vpisana, zato ni česa šteti.", + ccf_tablet: "Tablete", + ccf_tablet_hint: "Pride v tabletah ali kapsulah, ki se štejejo in ne vlečejo. Ničesar ni treba pripraviti, ob beleženju pa ni brizge.", + log_tablets: "Tablete", + log_tablets_suffix: "tablet", + log_per_tablet_hint: "{dose} na tableto.", + log_no_tablet_size: "Najprej vpiši velikost tablete pri škatlici.", + log_per_press_hint: "{dose} na pritisk, skupaj {ml} mL.", + log_na: "ni podatka", + count_tablets_one: "{n} tableta", + count_tablets_two: "{n} tableti", + count_tablets_few: "{n} tablete", + count_tablets_other: "{n} tablet", + count_packs_one: "{n} škatlica", + count_packs_two: "{n} škatlici", + count_packs_few: "{n} škatlice", + count_packs_other: "{n} škatlic", stock_bottle_sealed: "zaprta", stock_bottle_open: "odprta", stock_bottle_finished: "prazna", @@ -4602,6 +4669,31 @@ export const TRANSLATIONS = { stock_water_section: "Woda i rozpuszczalniki", stock_water_section_desc: "Opcjonalnie. Śledź tu butelki, a rozpuszczanie będzie z nich pobierać.", stock_add_bottle: "Dodaj butelkę", + stock_mg_per_tablet: "Miligramy w jednej tabletce", + dose_tablets_title: "{dose} w jednej tabletce.", + stock_tablets_per_pack: "Tabletek w opakowaniu", + stock_how_many_packs: "Ile opakowań", + stock_pack_holds: "Jedno opakowanie ma {mg} mg.", + stock_common_sizes: "Typowo: {sizes} mg", + stock_tablets_left: "zostało {n} tabletek", + stock_per_tablet: "{mg} mg na tabletkę", + stock_no_tablet_size: "Nie podano wielkości tabletki, więc nie ma czego liczyć.", + ccf_tablet: "Tabletki", + ccf_tablet_hint: "Występuje w tabletkach albo kapsułkach, liczonych zamiast nabieranych. Nic nie trzeba rozpuszczać, a przy zapisie nie ma strzykawki.", + log_tablets: "Tabletki", + log_tablets_suffix: "tabletek", + log_per_tablet_hint: "{dose} na tabletkę.", + log_no_tablet_size: "Najpierw podaj wielkość tabletki przy opakowaniu.", + log_per_press_hint: "{dose} na psiknięcie, łącznie {ml} mL.", + log_na: "brak", + count_tablets_one: "{n} tabletka", + count_tablets_few: "{n} tabletki", + count_tablets_many: "{n} tabletek", + count_tablets_other: "{n} tabletki", + count_packs_one: "{n} opakowanie", + count_packs_few: "{n} opakowania", + count_packs_many: "{n} opakowań", + count_packs_other: "{n} opakowania", stock_bottle_sealed: "zaplombowana", stock_bottle_open: "otwarta", stock_bottle_finished: "pusta", diff --git a/src/lib/types.ts b/src/lib/types.ts index 774fb31..c963765 100755 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -168,7 +168,7 @@ export interface Peptide { * concentration from the moment you open it and never needs the reconstitution * step. Defaults to powder. */ - preparation?: "powder" | "solution"; + preparation?: "powder" | "solution" | "tablet"; /** * International units per milligram, for compounds conventionally dosed in IU @@ -418,6 +418,16 @@ export interface DoseLog { * from today's bottle would be inventing a number for a past event. */ presses?: number; + /** + * How many tablets this dose was, for a pack. + * + * Recorded for the same reason `presses` is: the pack a dose came out of can + * be replaced by one with a different tablet size, and a count worked out + * later from today's pack would answer a question about last Tuesday with + * today's arithmetic. Fractions are kept, because a scored tablet really is + * half a dose, which is where a tablet differs from a press. + */ + tablets?: number; /** * The exact barrel, by id from `SYRINGES`. * @@ -693,7 +703,17 @@ export interface Vial { * * What it does not reuse is the syringe. See `calc/spray.ts`. */ - container?: "vial" | "spray"; + container?: "vial" | "spray" | "pack"; + /** + * Milligrams in one tablet, for a pack of them. + * + * The unit a pack is counted in, the way `mlPerSpray` is the unit a bottle is + * counted in. No default and no guess: a pump has a conventional 0.1 mL and a + * tablet has no conventional size at all, so zero here means nobody said, and + * the screen shows a count of nothing rather than a count of something + * invented. + */ + mgPerTablet?: number; /** * Millilitres one press of the pump delivers, for a spray. *