From 8aaeca6b26e7bcebd81adbcbd4e3c6bb19b48fde Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Sat, 28 Feb 2026 23:24:48 +0100 Subject: [PATCH] feat: simplify tube stock editing UI (#357) * feat: add package amount persistence and backend route support * test: align backend test schemas with medication metadata fields * fix(backend): restore intake usage normalizer for planner endpoint * fix(backend): keep export typing compatible before liquid-unit stack step * feat: simplify tube stock editing in desktop and mobile forms --- frontend/src/components/MobileEditModal.tsx | 355 +++++++++++++----- frontend/src/hooks/useMedicationForm.ts | 92 ++++- frontend/src/i18n/de.json | 40 +- frontend/src/i18n/en.json | 36 +- frontend/src/pages/MedicationsPage.tsx | 300 ++++++++++++--- frontend/src/styles/schedule-mobile-edit.css | 2 + .../test/components/MobileEditModal.test.tsx | 53 +++ .../src/test/hooks/useMedicationForm.test.ts | 72 ++++ 8 files changed, 805 insertions(+), 145 deletions(-) diff --git a/frontend/src/components/MobileEditModal.tsx b/frontend/src/components/MobileEditModal.tsx index d0ce8c2..9a3e66f 100644 --- a/frontend/src/components/MobileEditModal.tsx +++ b/frontend/src/components/MobileEditModal.tsx @@ -5,7 +5,7 @@ /* biome-ignore-all lint/a11y/noLabelWithoutControl: modal uses custom DateInput and static value fields */ import { Bell, Minus, Plus, Trash2 } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useEscapeKey } from "../hooks/useEscapeKey"; import { useScrollLock } from "../hooks/useScrollLock"; @@ -68,7 +68,7 @@ export interface MobileEditModalProps { /** Calculate total pills from form state */ function deriveTotalFromForm(form: FormState) { - if (form.packageType === "bottle") { + if (form.packageType === "bottle" || form.packageType === "tube" || form.packageType === "liquid_container") { // For bottle type, looseTablets is the current stock return Number(form.looseTablets) || 0; } @@ -125,6 +125,28 @@ export function MobileEditModal({ const [showNameValidation, setShowNameValidation] = useState(false); const activeTabIndexRef = useRef(0); + const allowFractionalIntake = useMemo(() => { + if (form.packageType === "liquid_container") return true; + if (form.packageType === "tube") return form.medicationForm === "liquid"; + return form.pillForm === "tablet"; + }, [form.packageType, form.medicationForm, form.pillForm]); + + const usageLabel = useMemo(() => { + if (form.packageType === "liquid_container") { + return t("form.blisters.usageMl"); + } + if (form.packageType === "tube") { + return form.medicationForm === "liquid" ? t("form.blisters.usageMl") : t("form.blisters.usageApplication"); + } + if (form.pillForm === "capsule") return t("form.blisters.usageCapsules"); + return t("form.blisters.usageTablets"); + }, [form.packageType, form.medicationForm, form.pillForm, t]); + + const usesAmountLabels = form.packageType === "tube" || form.packageType === "liquid_container"; + const totalCapacityLabel = usesAmountLabels ? t("form.totalAmount") : t("form.totalCapacity"); + const currentStockLabel = usesAmountLabels ? t("form.currentAmount") : t("form.currentPills"); + const totalLabel = usesAmountLabels ? t("form.totalAmountLabel") : t("form.total"); + // Reset tab when modal opens useEffect(() => { if (show) { @@ -392,6 +414,7 @@ export function MobileEditModal({ onHandleValueChange("medicationStartDate", e.target.value)} + placeholder={t("common.optional")} /> {!readOnlyMode && dateConsistencyError && ( {dateConsistencyError} @@ -406,8 +429,59 @@ export function MobileEditModal({ > + + + {form.packageType !== "tube" && form.packageType !== "liquid_container" && ( + + )} + {form.packageType === "tube" && ( + + )} + {form.packageType === "liquid_container" && ( + + )} + + {form.medicationEndDate && ( + + )}