From 4936929849f843f1d7263bf42797776484eac4d4 Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Wed, 4 Mar 2026 21:15:05 +0100 Subject: [PATCH] feat: replace hardcoded package assumptions with profile abstraction (#379) --- README.md | 6 +- backend/src/routes/export.ts | 7 +- backend/src/routes/medications.ts | 42 ++++---- backend/src/routes/planner.ts | 17 ++- backend/src/routes/share.ts | 12 +-- backend/src/services/reminder-scheduler.ts | 18 ++-- backend/src/utils/package-profiles.ts | 32 ++++++ backend/src/utils/scheduler-utils.ts | 5 +- frontend/src/components/MedDetailModal.tsx | 105 +++++++++---------- frontend/src/components/MobileEditModal.tsx | 48 +++++---- frontend/src/components/ReportModal.tsx | 34 +++--- frontend/src/components/SharedSchedule.tsx | 12 +-- frontend/src/hooks/useMedicationForm.ts | 43 ++++---- frontend/src/hooks/useRefill.ts | 22 ++-- frontend/src/pages/DashboardPage.tsx | 65 ++++++------ frontend/src/pages/MedicationsPage.tsx | 108 +++++++++++--------- frontend/src/pages/PlannerPage.tsx | 20 ++-- frontend/src/pages/SchedulePage.tsx | 18 ++-- frontend/src/pages/dashboard-helpers.ts | 6 +- frontend/src/types/index.ts | 19 +++- frontend/src/types/package-profiles.ts | 72 +++++++++++++ frontend/src/utils/schedule.ts | 10 +- frontend/src/utils/stock.ts | 8 +- 23 files changed, 440 insertions(+), 289 deletions(-) create mode 100644 backend/src/utils/package-profiles.ts create mode 100644 frontend/src/types/package-profiles.ts diff --git a/README.md b/README.md index df31f04..2bc70a9 100644 --- a/README.md +++ b/README.md @@ -120,10 +120,10 @@ Share your medication schedule with others via a public link. ### Smart Inventory -- Track exact stock: packs, blisters, bottles, and loose pills +- Track exact stock with package profiles (blister, bottle, tube, liquid container) - Display remaining days of supply - Automatic calculation based on intake schedule -- Manual stock correction supports partial blisters and loose pills +- Manual stock correction supports profile-specific stock semantics (sealed units + loose stock for blister, amount-based stock for bottle/tube/liquid) ### Medication Refill - One-click refill with pack or loose pill options @@ -141,7 +141,7 @@ Share your medication schedule with others via a public link. - Intake reminders via push notifications ### Trip Planner -- Calculate how many pills you need for a trip or date range +- Calculate medication demand for a trip or date range with package-aware units - Plan ahead for vacations, business trips, or hospital stays - Send demand reports via email or push notification diff --git a/backend/src/routes/export.ts b/backend/src/routes/export.ts index dbf5a45..3248c3d 100644 --- a/backend/src/routes/export.ts +++ b/backend/src/routes/export.ts @@ -10,6 +10,7 @@ import { doseTracking, medications, refillHistory, shareTokens, userSettings } f import { getAnonymousUserId, requireAuth } from "../plugins/auth.js"; import { env } from "../plugins/env.js"; import type { AuthUser } from "../types/fastify.js"; +import { normalizePackageType, PACKAGE_TYPES } from "../utils/package-profiles.js"; import { parseIntakesJson, parseTakenByJson } from "../utils/scheduler-utils.js"; const IMAGES_DIR = resolve(getDataDir(), "images"); @@ -39,7 +40,7 @@ const inventorySchema = z.object({ totalPills: z.number().int().nullable().optional(), // For bottle type: total capacity looseTablets: z.number().int().min(0).default(0), stockAdjustment: z.number().int().default(0), // Manual stock correction - packageType: z.enum(["blister", "bottle", "tube", "liquid_container"]).default("blister"), + packageType: z.enum(PACKAGE_TYPES).default("blister"), packageAmountValue: z.number().int().min(0).default(0), packageAmountUnit: z.enum(["ml", "g"]).default("ml"), }); @@ -319,7 +320,7 @@ export async function exportRoutes(app: FastifyInstance) { totalPills: med.totalPills ?? null, looseTablets: med.looseTablets ?? 0, stockAdjustment: med.stockAdjustment ?? 0, - packageType: med.packageType ?? "blister", + packageType: normalizePackageType(med.packageType), packageAmountValue: med.packageAmountValue ?? 0, packageAmountUnit: (med.packageAmountUnit ?? "ml") as "ml" | "g", }, @@ -595,7 +596,7 @@ export async function exportRoutes(app: FastifyInstance) { medicationForm: med.medicationForm ?? "tablet", pillForm: med.pillForm || null, lifecycleCategory: med.lifecycleCategory ?? "refill_when_empty", - packageType: med.inventory.packageType ?? "blister", + packageType: normalizePackageType(med.inventory.packageType), packageAmountValue: med.inventory.packageAmountValue ?? 0, packageAmountUnit: med.inventory.packageAmountUnit ?? "ml", packCount: med.inventory.packCount, diff --git a/backend/src/routes/medications.ts b/backend/src/routes/medications.ts index 11606ec..6ec11e0 100644 --- a/backend/src/routes/medications.ts +++ b/backend/src/routes/medications.ts @@ -14,6 +14,13 @@ import { streamToBuffer, writeOptimizedImageSet, } from "../utils/image-upload.js"; +import { + isAmountBasedPackageType, + isLiquidContainerPackageType, + isTubePackageType, + normalizePackageType, + PACKAGE_TYPES, +} from "../utils/package-profiles.js"; import { type Intake, normalizeIntakeUsageForStock, @@ -75,7 +82,7 @@ const blisterSchema = z.object({ start: z.string().datetime({ local: true }), }); -const packageTypeSchema = z.enum(["blister", "bottle", "tube", "liquid_container"]).default("blister"); +const packageTypeSchema = z.enum(PACKAGE_TYPES).default("blister"); const medicationFormSchema = z.enum(["capsule", "tablet", "liquid", "topical"]).default("tablet"); const pillFormSchema = z.enum(["capsule", "tablet"]); const lifecycleCategorySchema = z.enum(["refill_when_empty", "treatment_period"]).default("refill_when_empty"); @@ -163,7 +170,7 @@ const medicationSchema = z .refine( (data) => { if (data.medicationForm === "topical") { - return data.packageType === "tube"; + return isTubePackageType(data.packageType); } return true; }, @@ -175,7 +182,7 @@ const medicationSchema = z .refine( (data) => { if (data.medicationForm === "liquid") { - return data.packageType === "liquid_container"; + return isLiquidContainerPackageType(data.packageType); } return true; }, @@ -187,7 +194,7 @@ const medicationSchema = z .refine( (data) => { if (data.medicationForm === "capsule" || data.medicationForm === "tablet") { - return data.packageType !== "tube" && data.packageType !== "liquid_container"; + return !isTubePackageType(data.packageType) && !isLiquidContainerPackageType(data.packageType); } return true; }, @@ -294,7 +301,7 @@ export async function medicationRoutes(app: FastifyInstance) { medicationForm: row.medicationForm ?? "tablet", pillForm: row.pillForm ?? null, lifecycleCategory: row.lifecycleCategory ?? "refill_when_empty", - packageType: row.packageType ?? "blister", + packageType: normalizePackageType(row.packageType), packCount: row.packCount ?? 1, blistersPerPack: row.blistersPerPack ?? 1, pillsPerBlister: row.pillsPerBlister ?? 1, @@ -412,7 +419,7 @@ export async function medicationRoutes(app: FastifyInstance) { medicationForm: medicationForm ?? "tablet", pillForm: normalizedPillForm, lifecycleCategory: lifecycleCategory ?? "refill_when_empty", - packageType: packageType ?? "blister", + packageType: normalizePackageType(packageType), packCount, blistersPerPack, pillsPerBlister, @@ -448,7 +455,7 @@ export async function medicationRoutes(app: FastifyInstance) { medicationForm: inserted.medicationForm ?? "tablet", pillForm: inserted.pillForm ?? null, lifecycleCategory: inserted.lifecycleCategory ?? "refill_when_empty", - packageType: inserted.packageType ?? "blister", + packageType: normalizePackageType(inserted.packageType), packCount: inserted.packCount, blistersPerPack: inserted.blistersPerPack, pillsPerBlister: inserted.pillsPerBlister, @@ -583,7 +590,7 @@ export async function medicationRoutes(app: FastifyInstance) { medicationForm: medicationForm ?? "tablet", pillForm: normalizedPillForm, lifecycleCategory: lifecycleCategory ?? "refill_when_empty", - packageType: packageType ?? "blister", + packageType: normalizePackageType(packageType), packCount, blistersPerPack, pillsPerBlister, @@ -743,7 +750,7 @@ export async function medicationRoutes(app: FastifyInstance) { medicationForm: result[0].medicationForm ?? "tablet", pillForm: result[0].pillForm ?? null, lifecycleCategory: result[0].lifecycleCategory ?? "refill_when_empty", - packageType: result[0].packageType ?? "blister", + packageType: normalizePackageType(result[0].packageType), packCount: result[0].packCount, blistersPerPack: result[0].blistersPerPack, pillsPerBlister: result[0].pillsPerBlister, @@ -901,8 +908,8 @@ export async function medicationRoutes(app: FastifyInstance) { updatedAt: new Date(), }; - const packageType = existing.packageType ?? "blister"; - const allowsAmountBaseUpdate = packageType === "tube" || packageType === "liquid_container"; + const packageType = normalizePackageType(existing.packageType); + const allowsAmountBaseUpdate = isTubePackageType(packageType) || isLiquidContainerPackageType(packageType); if (allowsAmountBaseUpdate) { if (totalPills !== undefined) updateFields.totalPills = totalPills; if (looseTablets !== undefined) updateFields.looseTablets = looseTablets; @@ -1097,14 +1104,13 @@ export async function medicationRoutes(app: FastifyInstance) { const blistersPerPack = row.blistersPerPack ?? 1; const looseTablets = row.looseTablets ?? 0; const stockAdjustment = row.stockAdjustment ?? 0; - const packageType = row.packageType ?? "blister"; + const packageType = normalizePackageType(row.packageType); // For bottle type, looseTablets IS the current stock (no blister math) - const isTopical = medForm === "topical" || packageType === "tube"; - const originalTotalPills = - packageType === "bottle" || packageType === "liquid_container" - ? looseTablets + stockAdjustment - : packCount * blistersPerPack * pillsPerBlister + looseTablets + stockAdjustment; + const isTopical = medForm === "topical" || isTubePackageType(packageType); + const originalTotalPills = isAmountBasedPackageType(packageType) + ? looseTablets + stockAdjustment + : packCount * blistersPerPack * pillsPerBlister + looseTablets + stockAdjustment; // Calculate consumption with the same automatic/manual behavior as frontend coverage. const stockCorrectionCutoff = row.lastStockCorrectionAt ? new Date(row.lastStockCorrectionAt).getTime() : 0; @@ -1232,7 +1238,7 @@ export async function medicationRoutes(app: FastifyInstance) { let fullBlisters: number; let loosePills: number; - if (packageType === "bottle" || packageType === "tube" || packageType === "liquid_container") { + if (isAmountBasedPackageType(packageType)) { // Bottle type: no blisters, everything is loose pills fullBlisters = 0; loosePills = availableAfterPeriod; diff --git a/backend/src/routes/planner.ts b/backend/src/routes/planner.ts index cf71a55..fc3e398 100644 --- a/backend/src/routes/planner.ts +++ b/backend/src/routes/planner.ts @@ -15,6 +15,12 @@ import { getAnonymousUserId, requireAuth } from "../plugins/auth.js"; import { env } from "../plugins/env.js"; import { updateReminderSentTime, updateUserReminderSentTime } from "../services/reminder-scheduler.js"; import type { AuthUser } from "../types/fastify.js"; +import { + getPlannerUnitKind, + isAmountBasedPackageType, + isTubePackageType, + normalizePackageType, +} from "../utils/package-profiles.js"; import { loadUserSettings, sendShoutrrrNotification } from "./settings.js"; // Escape HTML to prevent XSS in email templates @@ -80,12 +86,13 @@ type PlannerRow = { }; function isContainerPackage(packageType?: string): boolean { - return packageType === "bottle" || packageType === "tube" || packageType === "liquid_container"; + return isAmountBasedPackageType(packageType); } function getPlannerUnit(packageType: string | undefined, tr: ReturnType): string { - if (packageType === "tube") return tr.common.units; - if (packageType === "liquid_container") return tr.common.ml; + const unitKind = getPlannerUnitKind(packageType); + if (unitKind === "units") return tr.common.units; + if (unitKind === "ml") return tr.common.ml; return tr.common.pills; } @@ -481,13 +488,13 @@ ${getFooterPlain(language)}`; .where(and(eq(medications.userId, userId), eq(medications.isObsolete, false))); const activeMedicationByName = new Map( activeMeds - .map((med) => [med.name || med.genericName || "", med.packageType ?? "blister"] as const) + .map((med) => [med.name || med.genericName || "", normalizePackageType(med.packageType)] as const) .filter(([name]) => name.length > 0) ); const filteredLowStock = lowStock.filter((item) => { const packageType = activeMedicationByName.get(item.name); if (!packageType) return false; - if (packageType === "tube") return false; + if (isTubePackageType(packageType)) return false; return true; }); if (filteredLowStock.length === 0) { diff --git a/backend/src/routes/share.ts b/backend/src/routes/share.ts index 9b08229..cf53c3c 100644 --- a/backend/src/routes/share.ts +++ b/backend/src/routes/share.ts @@ -7,6 +7,7 @@ import { medications, shareTokens, userSettings, users } from "../db/schema.js"; import { getAnonymousUserId, requireAuth } from "../plugins/auth.js"; import { env } from "../plugins/env.js"; import type { AuthUser } from "../types/fastify.js"; +import { isAmountBasedPackageType, normalizePackageType } from "../utils/package-profiles.js"; import { getAllTakenByForMedication, parseIntakesJson, @@ -119,12 +120,9 @@ export async function shareRoutes(app: FastifyInstance) { // Parse takenBy JSON array const takenByArray = parseTakenByJson(med.takenByJson); - const totalPills = - (med.packageType ?? "blister") === "bottle" || - (med.packageType ?? "blister") === "tube" || - (med.packageType ?? "blister") === "liquid_container" - ? med.looseTablets + (med.stockAdjustment ?? 0) - : med.packCount * med.blistersPerPack * med.pillsPerBlister + med.looseTablets + (med.stockAdjustment ?? 0); + const totalPills = isAmountBasedPackageType(med.packageType) + ? med.looseTablets + (med.stockAdjustment ?? 0) + : med.packCount * med.blistersPerPack * med.pillsPerBlister + med.looseTablets + (med.stockAdjustment ?? 0); return { id: med.id, name: med.name, @@ -133,7 +131,7 @@ export async function shareRoutes(app: FastifyInstance) { doseUnit: med.doseUnit ?? "mg", imageUrl: med.imageUrl, totalPills, - packageType: med.packageType ?? "blister", + packageType: normalizePackageType(med.packageType), packCount: med.packCount, blistersPerPack: med.blistersPerPack, looseTablets: med.looseTablets, diff --git a/backend/src/services/reminder-scheduler.ts b/backend/src/services/reminder-scheduler.ts index 54c2731..e054219 100644 --- a/backend/src/services/reminder-scheduler.ts +++ b/backend/src/services/reminder-scheduler.ts @@ -8,6 +8,12 @@ import { doseTracking, medications, userSettings } from "../db/schema.js"; import { getFooterHtml, getFooterPlain, getTranslations, type Language, t } from "../i18n/translations.js"; import { getAllUserSettings, sendShoutrrrNotification, type UserSettings } from "../routes/settings.js"; import type { ServiceLogger } from "../utils/logger.js"; +import { + isAmountBasedPackageType, + isLiquidContainerPackageType, + isTubePackageType, + normalizePackageType, +} from "../utils/package-profiles.js"; // Import shared utilities import { type Blister, @@ -268,9 +274,10 @@ async function getMedicationsNeedingReminder( const msPerDay = 86_400_000; for (const row of rows) { + const packageType = normalizePackageType(row.packageType); // Tube stock reminders are intentionally disabled: // topical usage in grams cannot be mapped reliably to schedule events. - if ((row.packageType ?? "blister") === "tube") continue; + if (isTubePackageType(packageType)) continue; const intakes = parseIntakesJson( row.intakesJson, @@ -283,10 +290,9 @@ async function getMedicationsNeedingReminder( start: i.start, })); - const originalTotalPills = - (row.packageType ?? "blister") === "bottle" - ? row.looseTablets + (row.stockAdjustment ?? 0) - : row.packCount * row.blistersPerPack * row.pillsPerBlister + row.looseTablets + (row.stockAdjustment ?? 0); + const originalTotalPills = isAmountBasedPackageType(packageType) + ? row.looseTablets + (row.stockAdjustment ?? 0) + : row.packCount * row.blistersPerPack * row.pillsPerBlister + row.looseTablets + (row.stockAdjustment ?? 0); const stockCorrectionCutoff = row.lastStockCorrectionAt ? new Date(row.lastStockCorrectionAt).getTime() : 0; const takenDoseIds = takenDoseIdsByMed.get(row.id) ?? new Set(); @@ -393,7 +399,7 @@ async function getMedicationsNeedingReminder( if (daysLeft === null) continue; - const isLiquid = (row.packageType ?? "blister") === "liquid_container"; + const isLiquid = isLiquidContainerPackageType(packageType); const { lowDays, criticalDays } = isLiquid ? getLiquidReminderThresholds(reminderDaysBefore) : { lowDays: lowStockDays, criticalDays: reminderDaysBefore }; diff --git a/backend/src/utils/package-profiles.ts b/backend/src/utils/package-profiles.ts new file mode 100644 index 0000000..2b32700 --- /dev/null +++ b/backend/src/utils/package-profiles.ts @@ -0,0 +1,32 @@ +export const PACKAGE_TYPES = ["blister", "bottle", "tube", "liquid_container"] as const; + +export type PackageType = (typeof PACKAGE_TYPES)[number]; + +const PACKAGE_TYPE_SET = new Set(PACKAGE_TYPES); + +export function normalizePackageType(packageType?: string | null): PackageType { + if (packageType && PACKAGE_TYPE_SET.has(packageType)) { + return packageType as PackageType; + } + return "blister"; +} + +export function isTubePackageType(packageType?: string | null): boolean { + return normalizePackageType(packageType) === "tube"; +} + +export function isLiquidContainerPackageType(packageType?: string | null): boolean { + return normalizePackageType(packageType) === "liquid_container"; +} + +export function isAmountBasedPackageType(packageType?: string | null): boolean { + const normalized = normalizePackageType(packageType); + return normalized === "bottle" || normalized === "tube" || normalized === "liquid_container"; +} + +export function getPlannerUnitKind(packageType?: string | null): "pills" | "ml" | "units" { + const normalized = normalizePackageType(packageType); + if (normalized === "tube") return "units"; + if (normalized === "liquid_container") return "ml"; + return "pills"; +} diff --git a/backend/src/utils/scheduler-utils.ts b/backend/src/utils/scheduler-utils.ts index fab6f6d..dd1a208 100644 --- a/backend/src/utils/scheduler-utils.ts +++ b/backend/src/utils/scheduler-utils.ts @@ -4,6 +4,7 @@ */ import { getDateLocale, type Language } from "../i18n/translations.js"; +import { isLiquidContainerPackageType, isTubePackageType } from "./package-profiles.js"; // Legacy type - individual blister schedule (DEPRECATED: use Intake instead) export type Blister = { usage: number; every: number; start: string }; @@ -36,9 +37,9 @@ export function normalizeIntakeUsageForStock( ): number { const usage = Number(intake.usage); if (!Number.isFinite(usage) || usage <= 0) return 0; - if (packageType === "tube") return 0; + if (isTubePackageType(packageType)) return 0; - const isLiquidStock = packageType === "liquid_container" || medicationForm === "liquid"; + const isLiquidStock = isLiquidContainerPackageType(packageType) || medicationForm === "liquid"; if (!isLiquidStock) return usage; if (intake.intakeUnit === "tsp") return usage * 5; diff --git a/frontend/src/components/MedDetailModal.tsx b/frontend/src/components/MedDetailModal.tsx index 84fa7d7..d210912 100644 --- a/frontend/src/components/MedDetailModal.tsx +++ b/frontend/src/components/MedDetailModal.tsx @@ -15,7 +15,14 @@ import { useTranslation } from "react-i18next"; import { Lightbox, MedicationAvatar } from "../components"; import { useEscapeKey } from "../hooks"; import type { Coverage, Medication, RefillEntry, StockThresholds } from "../types"; -import { getMedDisplayName, getMedTotal, getPackageSize } from "../types"; +import { + getMedDisplayName, + getMedTotal, + getPackageSize, + isAmountBasedPackageType, + isLiquidContainerPackageType, + isTubePackageType, +} from "../types"; import { formatNumber, generateICS, getExpiryClass, getSystemLocale } from "../utils"; import { getStockStatus } from "../utils/schedule"; import { splitCurrentBlisterStock } from "../utils/stock"; @@ -170,7 +177,7 @@ export function MedDetailModal({ }, [showEditStockModal]); const remainingPrescriptionRefills = Math.max(0, Number(selectedMed?.prescriptionRemainingRefills) || 0); - const prescriptionPackCapEnabled = selectedMed?.packageType === "blister" && usePrescriptionRefill; + const prescriptionPackCapEnabled = !isAmountBasedPackageType(selectedMed?.packageType) && usePrescriptionRefill; const cappedRefillPacks = prescriptionPackCapEnabled ? Math.min(refillPacks, remainingPrescriptionRefills) : refillPacks; @@ -179,7 +186,7 @@ export function MedDetailModal({ useEffect(() => { if (!selectedMed) return; if (!showRefillModal) return; - if (selectedMed.packageType !== "blister" || !usePrescriptionRefill) return; + if (isAmountBasedPackageType(selectedMed.packageType) || !usePrescriptionRefill) return; if (refillPacks <= remainingPrescriptionRefills) return; onRefillPacksChange(remainingPrescriptionRefills); }, [ @@ -192,9 +199,10 @@ export function MedDetailModal({ ]); if (!selectedMed) return null; - const isAmountPackage = selectedMed.packageType === "tube" || selectedMed.packageType === "liquid_container"; + const isAmountPackage = + isTubePackageType(selectedMed.packageType) || isLiquidContainerPackageType(selectedMed.packageType); const amountUnitLabel = - selectedMed.packageType === "liquid_container" || selectedMed.medicationForm === "liquid" + isLiquidContainerPackageType(selectedMed.packageType) || selectedMed.medicationForm === "liquid" ? t("form.packageAmountUnitMl") : t("form.packageAmountUnitG"); const stockUnitLabel = isAmountPackage ? amountUnitLabel : null; @@ -202,12 +210,9 @@ export function MedDetailModal({ const medCoverage = coverage.all.find((c) => c.name === getMedDisplayName(selectedMed)); const packageSize = getPackageSize(selectedMed); // Structural max = sealed package capacity only (excludes pre-existing looseTablets). - const structuralMax = - selectedMed.packageType === "bottle" || - selectedMed.packageType === "tube" || - selectedMed.packageType === "liquid_container" - ? (selectedMed.totalPills ?? packageSize) - : selectedMed.packCount * selectedMed.blistersPerPack * selectedMed.pillsPerBlister; + const structuralMax = isAmountBasedPackageType(selectedMed.packageType) + ? (selectedMed.totalPills ?? packageSize) + : selectedMed.packCount * selectedMed.blistersPerPack * selectedMed.pillsPerBlister; const currentStock = medCoverage ? Math.round(medCoverage.medsLeft) : getMedTotal(selectedMed); const status = medCoverage ? getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, settings) : null; const fallbackTextClass = status?.className === "warning" ? "warning-text" : "success-text"; @@ -216,12 +221,9 @@ export function MedDetailModal({ const currentFullBlisters = Math.max(0, stock.fullBlisters); const currentPartialPills = Math.max(0, stock.openBlisterPills); const currentLoosePills = Math.max(0, stock.loosePills); - const stockDisplayTotal = - selectedMed.packageType === "bottle" || - selectedMed.packageType === "tube" || - selectedMed.packageType === "liquid_container" - ? (selectedMed.totalPills ?? packageSize) - : Math.max(0, structuralMax); + const stockDisplayTotal = isAmountBasedPackageType(selectedMed.packageType) + ? (selectedMed.totalPills ?? packageSize) + : Math.max(0, structuralMax); const packageCount = Math.max(1, Number(selectedMed.packCount) || 1); const amountPerPackage = (() => { const configured = Number(selectedMed.packageAmountValue ?? 0); @@ -244,7 +246,7 @@ export function MedDetailModal({ const decrementLabel = t("editStock.decreaseValue"); const incrementLabel = t("editStock.increaseValue"); const getScheduleUsageLabel = (usage: number, intakeUnit?: "ml" | "tsp" | "tbsp" | null) => { - if (selectedMed.packageType === "liquid_container") { + if (isLiquidContainerPackageType(selectedMed.packageType)) { if (intakeUnit === "tsp") { return `${usage} ${t("form.blisters.teaspoons", { count: Math.abs(usage) })}`; } @@ -253,7 +255,7 @@ export function MedDetailModal({ } return `${usage} ${t("form.packageAmountUnitMl")}`; } - if (selectedMed.packageType === "tube") { + if (isTubePackageType(selectedMed.packageType)) { return `${usage} ${t("form.blisters.applications", { count: Math.abs(usage) })}`; } return `${usage} ${usage !== 1 ? t("common.pills") : t("common.pill")}`; @@ -400,7 +402,7 @@ export function MedDetailModal({ const renderEditStockModal = () => { if (!showEditStockModal) return null; - const isLiquidPackage = selectedMed.packageType === "liquid_container"; + const isLiquidPackage = isLiquidContainerPackageType(selectedMed.packageType); const liquidBottleCount = Math.max(1, editStockFullBlisters); const liquidAmountPerBottle = Math.max(1, Number.isFinite(amountPerPackage) ? amountPerPackage : 1); const liquidCapacity = Math.max(1, Math.round(liquidBottleCount * liquidAmountPerBottle)); @@ -439,7 +441,7 @@ export function MedDetailModal({

{t("editStock.title")}

{getMedDisplayName(selectedMed)}

{t("editStock.hint")}

- {selectedMed.packageType === "blister" && ( + {!isAmountBasedPackageType(selectedMed.packageType) && (

{t("editStock.currentComposition", { fullBlisters: currentFullBlisters, @@ -449,10 +451,10 @@ export function MedDetailModal({ })}

)} - {selectedMed.packageType === "bottle" && ( + {isAmountBasedPackageType(selectedMed.packageType) && !isTubePackageType(selectedMed.packageType) && (

{t("editStock.packageSize", { count: structuralMax })}

)} - {(selectedMed.packageType === "tube" || selectedMed.packageType === "liquid_container") && ( + {(isTubePackageType(selectedMed.packageType) || isLiquidContainerPackageType(selectedMed.packageType)) && (

{t("form.totalAmount")}: {formatNumber(isLiquidPackage ? liquidCapacity : structuralMax)}{" "} {amountUnitLabel} @@ -465,10 +467,7 @@ export function MedDetailModal({ {(() => { const dbTotal = getMedTotal(selectedMed); const currentTotal = medCoverage ? Math.round(medCoverage.medsLeft) : dbTotal; - const isBottle = - selectedMed.packageType === "bottle" || - selectedMed.packageType === "tube" || - selectedMed.packageType === "liquid_container"; + const isBottle = isAmountBasedPackageType(selectedMed.packageType); const enteredTotal = isLiquidPackage ? Math.min(liquidCapacity, editStockPartialBlisterPills) : isBottle @@ -813,7 +812,7 @@ export function MedDetailModal({

{t("modal.stockInfo")}

- {selectedMed.packageType === "blister" && ( + {!isAmountBasedPackageType(selectedMed.packageType) && ( <>
{t("table.fullBlisters")} @@ -832,7 +831,7 @@ export function MedDetailModal({
)} -
+
{isAmountPackage ? t("form.currentAmount") : t("modal.currentStock")} @@ -858,27 +857,27 @@ export function MedDetailModal({

{t("modal.packageDetails")} ( - {selectedMed.packageType === "bottle" - ? t("form.packageTypeBottle") - : selectedMed.packageType === "tube" - ? t("form.packageTypeTube") - : selectedMed.packageType === "liquid_container" - ? t("form.packageTypeLiquidContainer") + {isTubePackageType(selectedMed.packageType) + ? t("form.packageTypeTube") + : isLiquidContainerPackageType(selectedMed.packageType) + ? t("form.packageTypeLiquidContainer") + : isAmountBasedPackageType(selectedMed.packageType) + ? t("form.packageTypeBottle") : t("form.packageTypeBlister")} ) - {selectedMed.packageType === "tube" && ( + {isTubePackageType(selectedMed.packageType) && ( ℹ️ )} - {selectedMed.packageType === "liquid_container" && ( + {isLiquidContainerPackageType(selectedMed.packageType) && ( ℹ️ )}

- {selectedMed.packageType === "blister" ? ( + {!isAmountBasedPackageType(selectedMed.packageType) ? ( <>
{t("modal.packs")} @@ -893,7 +892,7 @@ export function MedDetailModal({ {selectedMed.pillsPerBlister}
- ) : selectedMed.packageType === "liquid_container" ? ( + ) : isLiquidContainerPackageType(selectedMed.packageType) ? ( <>
{t("form.bottles")} @@ -912,7 +911,7 @@ export function MedDetailModal({
- ) : selectedMed.packageType === "tube" ? ( + ) : isTubePackageType(selectedMed.packageType) ? ( <>
{t("form.tubes")} @@ -1115,13 +1114,10 @@ export function MedDetailModal({ {(() => { - const total = - selectedMed.packageType === "bottle" || - selectedMed.packageType === "tube" || - selectedMed.packageType === "liquid_container" - ? entry.loosePillsAdded - : entry.packsAdded * selectedMed.blistersPerPack * selectedMed.pillsPerBlister + - entry.loosePillsAdded; + const total = isAmountBasedPackageType(selectedMed.packageType) + ? entry.loosePillsAdded + : entry.packsAdded * selectedMed.blistersPerPack * selectedMed.pillsPerBlister + + entry.loosePillsAdded; return `+${total}${isAmountPackage ? ` ${stockUnitLabel}` : ` ${total === 1 ? t("common.pill") : t("common.pills")}`}`; })()} {entry.usedPrescription && ( @@ -1221,7 +1217,7 @@ export function MedDetailModal({

{getMedDisplayName(selectedMed)}

- {selectedMed.packageType === "blister" ? ( + {!isAmountBasedPackageType(selectedMed.packageType) ? ( <> - {form.packageType !== "tube" && form.packageType !== "liquid_container" && ( + {allowsPillFormSelection(form.packageType) && ( )} - {form.packageType === "liquid_container" && ( + {isLiquidContainerPackageType(form.packageType) && (