From 57eafabb9da991de3a829adc9909424e78950b12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 12 Jun 2026 21:08:37 +0200 Subject: [PATCH] feat(medications): packaging economics editable from the supply tab Units-per-dose and the default pack size were settable only through the wizard's dose step; a manufacturer switch with a different blister size happens while looking at the supply, so the supply tab now carries a packaging row with an edit dialog that writes sparsely onto the medication. --- messages/de.json | 14 +- messages/en.json | 14 +- messages/es.json | 14 +- messages/fr.json | 14 +- messages/it.json | 14 +- messages/pl.json | 14 +- .../sections/inventory-section.tsx | 203 +++++++++++++++++- 7 files changed, 280 insertions(+), 7 deletions(-) diff --git a/messages/de.json b/messages/de.json index 260718ff5..7d201e76a 100644 --- a/messages/de.json +++ b/messages/de.json @@ -1635,7 +1635,19 @@ "quantityInUnits": "= {units} Einheiten", "quantityInDoses": "≈ {doses} Dosen", "quantityUnderOneDose": "< 1 Dosis", - "expiredSuffix": "+ {units} Einheiten abgelaufen" + "expiredSuffix": "+ {units} Einheiten abgelaufen", + "packagingTitle": "Packung & Dosis", + "packagingUnitsPerDose": "1 Dosis = {units} Einheiten", + "packagingDefaultPack": "Standard-Packungsgröße: {units} Einheiten", + "packagingNoDefaultPack": "Keine Standard-Packungsgröße hinterlegt", + "packagingEdit": "Bearbeiten", + "packagingDescription": "Lege fest, wie viele Einheiten eine Dosis verbraucht und wie groß eine neue Packung üblicherweise ist — z. B. nach einem Herstellerwechsel mit anderer Blistergröße.", + "packagingUnitsPerDoseLabel": "Einheiten pro Dosis", + "packagingUnitsPerDoseHelper": "Z. B. 2 Tabletten à 2 mg für eine 4-mg-Dosis (1–100). Bereits erfasste Einnahmen bleiben unverändert.", + "packagingDefaultPackLabel": "Standard-Packungsgröße (Einheiten)", + "packagingDefaultPackHelper": "Vorbefüllung beim Erfassen neuer Packungen (1–1000). Leer lassen, wenn keine üblich ist.", + "packagingSuccess": "Packungsangaben gespeichert", + "packagingFailed": "Speichern fehlgeschlagen" }, "erweitert": { "group": { diff --git a/messages/en.json b/messages/en.json index dbea482f7..17a61bd74 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1635,7 +1635,19 @@ "quantityInUnits": "= {units} units", "quantityInDoses": "≈ {doses} doses", "quantityUnderOneDose": "< 1 dose", - "expiredSuffix": "+ {units} units expired" + "expiredSuffix": "+ {units} units expired", + "packagingTitle": "Packaging & dose", + "packagingUnitsPerDose": "1 dose = {units} units", + "packagingDefaultPack": "Default pack size: {units} units", + "packagingNoDefaultPack": "No default pack size set", + "packagingEdit": "Edit", + "packagingDescription": "Set how many units one dose consumes and how large a new pack usually is — e.g. after a manufacturer switch with a different blister size.", + "packagingUnitsPerDoseLabel": "Units per dose", + "packagingUnitsPerDoseHelper": "E.g. two 2 mg tablets for a 4 mg dose (1–100). Doses already recorded stay unchanged.", + "packagingDefaultPackLabel": "Default pack size (units)", + "packagingDefaultPackHelper": "Prefills the quantity when registering new packs (1–1000). Leave empty if none applies.", + "packagingSuccess": "Packaging details saved", + "packagingFailed": "Saving failed" }, "erweitert": { "group": { diff --git a/messages/es.json b/messages/es.json index 0e74cb0dd..f42e61dc8 100644 --- a/messages/es.json +++ b/messages/es.json @@ -1635,7 +1635,19 @@ "quantityInUnits": "= {units} unidades", "quantityInDoses": "≈ {doses} dosis", "quantityUnderOneDose": "< 1 dosis", - "expiredSuffix": "+ {units} unidades caducadas" + "expiredSuffix": "+ {units} unidades caducadas", + "packagingTitle": "Envase y dosis", + "packagingUnitsPerDose": "1 dosis = {units} unidades", + "packagingDefaultPack": "Tamaño de envase predeterminado: {units} unidades", + "packagingNoDefaultPack": "Sin tamaño de envase predeterminado", + "packagingEdit": "Editar", + "packagingDescription": "Define cuántas unidades consume una dosis y el tamaño habitual de un envase nuevo, p. ej. tras un cambio de fabricante con otro tamaño de blíster.", + "packagingUnitsPerDoseLabel": "Unidades por dosis", + "packagingUnitsPerDoseHelper": "P. ej. dos comprimidos de 2 mg para una dosis de 4 mg (1–100). Las tomas ya registradas no cambian.", + "packagingDefaultPackLabel": "Tamaño de envase predeterminado (unidades)", + "packagingDefaultPackHelper": "Rellena la cantidad al registrar envases nuevos (1–1000). Déjalo vacío si no aplica.", + "packagingSuccess": "Datos del envase guardados", + "packagingFailed": "Error al guardar" }, "erweitert": { "group": { diff --git a/messages/fr.json b/messages/fr.json index 31be826ff..c3f9db034 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -1635,7 +1635,19 @@ "quantityInUnits": "= {units} unités", "quantityInDoses": "≈ {doses} doses", "quantityUnderOneDose": "< 1 dose", - "expiredSuffix": "+ {units} unités périmées" + "expiredSuffix": "+ {units} unités périmées", + "packagingTitle": "Conditionnement et dose", + "packagingUnitsPerDose": "1 dose = {units} unités", + "packagingDefaultPack": "Taille de boîte par défaut : {units} unités", + "packagingNoDefaultPack": "Aucune taille de boîte par défaut", + "packagingEdit": "Modifier", + "packagingDescription": "Définis combien d’unités consomme une dose et la taille habituelle d’une nouvelle boîte — p. ex. après un changement de fabricant avec une autre taille de plaquette.", + "packagingUnitsPerDoseLabel": "Unités par dose", + "packagingUnitsPerDoseHelper": "P. ex. deux comprimés de 2 mg pour une dose de 4 mg (1–100). Les prises déjà enregistrées restent inchangées.", + "packagingDefaultPackLabel": "Taille de boîte par défaut (unités)", + "packagingDefaultPackHelper": "Préremplit la quantité lors de l’enregistrement de nouvelles boîtes (1–1000). Laisse vide si aucune ne s’applique.", + "packagingSuccess": "Conditionnement enregistré", + "packagingFailed": "Échec de l’enregistrement" }, "erweitert": { "group": { diff --git a/messages/it.json b/messages/it.json index ac19d1ffe..fc94d2431 100644 --- a/messages/it.json +++ b/messages/it.json @@ -1635,7 +1635,19 @@ "quantityInUnits": "= {units} unità", "quantityInDoses": "≈ {doses} dosi", "quantityUnderOneDose": "< 1 dose", - "expiredSuffix": "+ {units} unità scadute" + "expiredSuffix": "+ {units} unità scadute", + "packagingTitle": "Confezione e dose", + "packagingUnitsPerDose": "1 dose = {units} unità", + "packagingDefaultPack": "Dimensione confezione predefinita: {units} unità", + "packagingNoDefaultPack": "Nessuna dimensione confezione predefinita", + "packagingEdit": "Modifica", + "packagingDescription": "Imposta quante unità consuma una dose e la dimensione abituale di una nuova confezione, ad es. dopo un cambio di produttore con blister diverso.", + "packagingUnitsPerDoseLabel": "Unità per dose", + "packagingUnitsPerDoseHelper": "Ad es. due compresse da 2 mg per una dose da 4 mg (1–100). Le assunzioni già registrate restano invariate.", + "packagingDefaultPackLabel": "Dimensione confezione predefinita (unità)", + "packagingDefaultPackHelper": "Precompila la quantità quando registri nuove confezioni (1–1000). Lascia vuoto se non applicabile.", + "packagingSuccess": "Dati confezione salvati", + "packagingFailed": "Salvataggio non riuscito" }, "erweitert": { "group": { diff --git a/messages/pl.json b/messages/pl.json index ea8c170ef..9bba252ae 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -1635,7 +1635,19 @@ "quantityInUnits": "= {units} jednostek", "quantityInDoses": "Dawki: ≈ {doses}", "quantityUnderOneDose": "< 1 dawka", - "expiredSuffix": "+ przeterminowane jednostki: {units}" + "expiredSuffix": "+ przeterminowane jednostki: {units}", + "packagingTitle": "Opakowanie i dawka", + "packagingUnitsPerDose": "1 dawka = {units} jedn.", + "packagingDefaultPack": "Domyślny rozmiar opakowania: {units} jedn.", + "packagingNoDefaultPack": "Brak domyślnego rozmiaru opakowania", + "packagingEdit": "Edytuj", + "packagingDescription": "Określ, ile jednostek zużywa jedna dawka i jaki jest typowy rozmiar nowego opakowania — np. po zmianie producenta z innym blistrem.", + "packagingUnitsPerDoseLabel": "Jednostki na dawkę", + "packagingUnitsPerDoseHelper": "Np. dwie tabletki 2 mg na dawkę 4 mg (1–100). Zarejestrowane dawki pozostają bez zmian.", + "packagingDefaultPackLabel": "Domyślny rozmiar opakowania (jednostki)", + "packagingDefaultPackHelper": "Wstępnie wypełnia ilość przy dodawaniu nowych opakowań (1–1000). Zostaw puste, jeśli nie dotyczy.", + "packagingSuccess": "Zapisano dane opakowania", + "packagingFailed": "Zapis nie powiódł się" }, "erweitert": { "group": { diff --git a/src/components/medications/sections/inventory-section.tsx b/src/components/medications/sections/inventory-section.tsx index 13b56094b..2516fa08f 100644 --- a/src/components/medications/sections/inventory-section.tsx +++ b/src/components/medications/sections/inventory-section.tsx @@ -63,7 +63,13 @@ import { import { SettingsGroup } from "@/components/medications/settings-group"; import { useTranslations } from "@/lib/i18n/context"; import { queryKeys } from "@/lib/query-keys"; -import { apiDelete, apiGet, apiPatch, apiPost } from "@/lib/api/api-fetch"; +import { + apiDelete, + apiGet, + apiPatch, + apiPost, + apiPut, +} from "@/lib/api/api-fetch"; import { summariseSupply } from "@/lib/medications/inventory/summary"; type InventoryState = "ACTIVE" | "IN_USE" | "EXPIRED" | "USED_UP"; @@ -123,6 +129,7 @@ export function InventorySection({ const queryClient = useQueryClient(); const [addOpen, setAddOpen] = useState(false); const [adjustItem, setAdjustItem] = useState(null); + const [packagingOpen, setPackagingOpen] = useState(false); async function deleteItem(item: InventoryItem) { try { @@ -194,6 +201,14 @@ export function InventorySection({ onClose={() => setAdjustItem(null)} /> )} + {packagingOpen && ( + setPackagingOpen(false)} + /> + )} ); @@ -258,6 +273,39 @@ export function InventorySection({ + {/* v1.16.11 — packaging economics surfaced where the stock lives: + the wizard's dose step owns these on create/edit, but a + manufacturer switch (a different blister size) happens while + looking at the supply, so the supply tab carries them too. */} + +
+

+ {t("medications.detail.bestand.packagingUnitsPerDose", { + units: perDose, + })} + + {dosesPerUnit != null + ? t("medications.detail.bestand.packagingDefaultPack", { + units: dosesPerUnit, + }) + : t("medications.detail.bestand.packagingNoDefaultPack")} + +

+ +
+
+ ); } + +/** + * Edit the medication-level packaging economics from the supply tab: + * units one dose consumes, and the default container size the register + * flow prefills. Both PUT sparsely onto the medication — the wizard's + * dose step stays the source on create; this is the correction surface + * for a manufacturer switch (same medication, different blister size). + */ +function PackagingDialog({ + medicationId, + unitsPerDose, + dosesPerUnit, + onClose, +}: { + medicationId: string; + unitsPerDose: number; + dosesPerUnit: number | null; + onClose: () => void; +}) { + const { t } = useTranslations(); + const queryClient = useQueryClient(); + const [perDoseValue, setPerDoseValue] = useState(String(unitsPerDose)); + const [packValue, setPackValue] = useState( + dosesPerUnit === null ? "" : String(dosesPerUnit), + ); + const [busy, setBusy] = useState(false); + + const parsedPerDose = Number(perDoseValue); + const perDoseValid = + Number.isInteger(parsedPerDose) && parsedPerDose >= 1 && parsedPerDose <= 100; + const parsedPack = packValue.trim() === "" ? null : Number(packValue); + const packValid = + parsedPack === null || + (Number.isInteger(parsedPack) && parsedPack >= 1 && parsedPack <= 1000); + + async function submit(e: FormEvent) { + e.preventDefault(); + if (!perDoseValid || !packValid || busy) return; + setBusy(true); + try { + await apiPut(`/api/medications/${medicationId}`, { + unitsPerDose: parsedPerDose, + dosesPerUnit: parsedPack, + }); + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: queryKeys.medicationDetail(medicationId), + }), + queryClient.invalidateQueries({ + queryKey: queryKeys.medications(), + }), + queryClient.invalidateQueries({ + queryKey: queryKeys.medicationInventory(medicationId), + }), + ]); + toast.success(t("medications.detail.bestand.packagingSuccess")); + onClose(); + } catch { + toast.error(t("medications.detail.bestand.packagingFailed")); + } finally { + setBusy(false); + } + } + + return ( + !open && onClose()}> + + + + {t("medications.detail.bestand.packagingTitle")} + + + {t("medications.detail.bestand.packagingDescription")} + + +
+
+ + setPerDoseValue(e.target.value)} + aria-describedby="packaging-units-per-dose-helper" + /> +

+ {t("medications.detail.bestand.packagingUnitsPerDoseHelper")} +

+
+
+ + setPackValue(e.target.value)} + aria-describedby="packaging-default-pack-helper" + /> +

+ {t("medications.detail.bestand.packagingDefaultPackHelper")} +

+
+ + + + +
+
+
+ ); +}