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. *