diff --git a/messages/de.json b/messages/de.json index e6e20165d..b10ea57da 100644 --- a/messages/de.json +++ b/messages/de.json @@ -1716,8 +1716,8 @@ "addSuccess": "Bestand erfasst.", "addFailed": "Bestand konnte nicht erfasst werden.", "adjustButton": "Anpassen", - "adjustTitle": "Bestand korrigieren", - "adjustDescription": "Setze die verbleibenden Einheiten neu — für Korrekturen oder eine Entnahme. 0 markiert den Vorrat als aufgebraucht.", + "adjustTitle": "Behälter korrigieren", + "adjustDescription": "Verbleibende Einheiten setzen und die Daten dieses Behälters korrigieren. 0 Einheiten markiert ihn als aufgebraucht.", "adjustQuantityLabel": "Verbleibende Einheiten", "adjustHelper": "Kapazität: {total} Einheiten.", "adjustSubmit": "Speichern", @@ -1757,7 +1757,15 @@ "packagingFailed": "Speichern fehlgeschlagen", "addManufacturerLabel": "Hersteller", "addDoseStrengthLabel": "Wirkstärke", - "addDoseStrengthHint": "Wie auf dem Behälter aufgedruckt, z. B. 5 mg/0,5 ml." + "addDoseStrengthHint": "Wie auf dem Behälter aufgedruckt, z. B. 5 mg/0,5 ml.", + "openedOn": "Geöffnet {date}", + "expiresOn": "Haltbar bis {date}", + "summaryAllExpired": "Kein nutzbarer Bestand: {units} Einheiten als abgelaufen markiert", + "editExpiryLabel": "Aufgedrucktes Haltbarkeitsdatum", + "editExpiryHelper": "Das Datum auf der Packung. Leer lassen, wenn keines erfasst ist.", + "editOpenedLabel": "Geöffnet am", + "editOpenedHelper": "Leer lassen, wenn der Behälter noch verschlossen ist.", + "editOpenedHelperClock": "Ein Pen oder eine Ampulle läuft 30 Tage nach dem Öffnen ab. Leer lassen, wenn noch verschlossen." }, "erweitert": { "group": { @@ -7008,7 +7016,8 @@ "title": "Vorrat niedrig: {medName}", "bodyToday": "Dein Vorrat an {medName} reicht keinen ganzen Tag mehr ({units} Einheiten übrig).", "bodyReorder": "Dein Vorrat an {medName} ({units} Einheiten) ist um den {runsOutOn} aufgebraucht — bestelle bis zum {reorderBy} nach.", - "bodyLastDose": "Dein Vorrat an {medName} ({units} Einheiten) reicht noch etwa für die letzte Dosis. Aufgebraucht um den {runsOutOn}." + "bodyLastDose": "Dein Vorrat an {medName} ({units} Einheiten) reicht noch etwa für die letzte Dosis. Aufgebraucht um den {runsOutOn}.", + "bodyExpired": "{medName}: kein nutzbarer Bestand mehr. {units} Einheiten sind als abgelaufen markiert." }, "moodReminders": { "dailyTitle": "Stimmung erfassen", diff --git a/messages/en.json b/messages/en.json index b1b50f6fc..ee47dc886 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1716,8 +1716,8 @@ "addSuccess": "Supply registered.", "addFailed": "The supply could not be registered.", "adjustButton": "Adjust", - "adjustTitle": "Correct stock", - "adjustDescription": "Set the remaining units — for corrections or a withdrawal. 0 marks the item as used up.", + "adjustTitle": "Correct container", + "adjustDescription": "Set the remaining units and correct the dates printed on or recorded for this container. 0 units marks it as used up.", "adjustQuantityLabel": "Remaining units", "adjustHelper": "Capacity: {total} units.", "adjustSubmit": "Save", @@ -1757,7 +1757,15 @@ "packagingFailed": "Saving failed", "addManufacturerLabel": "Manufacturer", "addDoseStrengthLabel": "Strength", - "addDoseStrengthHint": "As printed on the container, e.g. 5 mg/0.5 ml." + "addDoseStrengthHint": "As printed on the container, e.g. 5 mg/0.5 ml.", + "openedOn": "Opened {date}", + "expiresOn": "Expires {date}", + "summaryAllExpired": "No usable supply: {units} units marked expired", + "editExpiryLabel": "Printed expiry date", + "editExpiryHelper": "The date printed on the carton. Leave empty if none is recorded.", + "editOpenedLabel": "Opened on", + "editOpenedHelper": "Leave empty if the container is still sealed.", + "editOpenedHelperClock": "A pen or ampoule expires 30 days after it is opened. Leave empty if it is still sealed." }, "erweitert": { "group": { @@ -7008,7 +7016,8 @@ "title": "Low supply: {medName}", "bodyToday": "{medName}: {units} units left — less than one day of supply.", "bodyReorder": "{medName}: {units} units left. Supply runs out around {runsOutOn} — reorder by {reorderBy}.", - "bodyLastDose": "{medName}: {units} units left — about your final dose. Supply runs out around {runsOutOn}." + "bodyLastDose": "{medName}: {units} units left — about your final dose. Supply runs out around {runsOutOn}.", + "bodyExpired": "{medName}: no usable supply left. {units} units are marked expired." }, "moodReminders": { "dailyTitle": "Log your mood", diff --git a/messages/es.json b/messages/es.json index 9f56ae1f6..50a18d9f4 100644 --- a/messages/es.json +++ b/messages/es.json @@ -1716,8 +1716,8 @@ "addSuccess": "Existencias registradas.", "addFailed": "No se pudieron registrar las existencias.", "adjustButton": "Ajustar", - "adjustTitle": "Corregir existencias", - "adjustDescription": "Establece las unidades restantes — para correcciones o una retirada. 0 marca el envase como agotado.", + "adjustTitle": "Corregir envase", + "adjustDescription": "Ajusta las unidades restantes y corrige las fechas de este envase. 0 unidades lo marca como agotado.", "adjustQuantityLabel": "Unidades restantes", "adjustHelper": "Capacidad: {total} unidades.", "adjustSubmit": "Guardar", @@ -1757,7 +1757,15 @@ "packagingFailed": "Error al guardar", "addManufacturerLabel": "Fabricante", "addDoseStrengthLabel": "Concentración", - "addDoseStrengthHint": "Tal como aparece impreso en el envase, p. ej. 5 mg/0,5 ml." + "addDoseStrengthHint": "Tal como aparece impreso en el envase, p. ej. 5 mg/0,5 ml.", + "openedOn": "Abierto el {date}", + "expiresOn": "Caduca el {date}", + "summaryAllExpired": "Sin existencias utilizables: {units} unidades marcadas como caducadas", + "editExpiryLabel": "Fecha de caducidad impresa", + "editExpiryHelper": "La fecha impresa en el envase. Déjalo vacío si no hay ninguna registrada.", + "editOpenedLabel": "Abierto el", + "editOpenedHelper": "Déjalo vacío si el envase sigue cerrado.", + "editOpenedHelperClock": "Una pluma o una ampolla caduca 30 días después de abrirse. Déjalo vacío si sigue cerrada." }, "erweitert": { "group": { @@ -7008,7 +7016,8 @@ "title": "Quedan pocas existencias: {medName}", "bodyToday": "{medName}: quedan {units} unidades — menos de un día de suministro.", "bodyReorder": "{medName}: quedan {units} unidades. Las existencias se agotan hacia el {runsOutOn} — vuelve a pedir antes del {reorderBy}.", - "bodyLastDose": "{medName}: quedan {units} unidades — alrededor de tu última dosis. Las existencias se agotan hacia el {runsOutOn}." + "bodyLastDose": "{medName}: quedan {units} unidades — alrededor de tu última dosis. Las existencias se agotan hacia el {runsOutOn}.", + "bodyExpired": "{medName}: no quedan existencias utilizables. {units} unidades están marcadas como caducadas." }, "moodReminders": { "dailyTitle": "Registrar el ánimo", diff --git a/messages/fr.json b/messages/fr.json index f5348e6d8..713e46e24 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -1716,8 +1716,8 @@ "addSuccess": "Stock enregistré.", "addFailed": "Le stock n'a pas pu être enregistré.", "adjustButton": "Ajuster", - "adjustTitle": "Corriger le stock", - "adjustDescription": "Définissez les unités restantes — pour une correction ou un prélèvement. 0 marque l'article comme épuisé.", + "adjustTitle": "Corriger le contenant", + "adjustDescription": "Ajustez les unités restantes et corrigez les dates de ce contenant. 0 unité le marque comme épuisé.", "adjustQuantityLabel": "Unités restantes", "adjustHelper": "Capacité : {total} unités.", "adjustSubmit": "Enregistrer", @@ -1757,7 +1757,15 @@ "packagingFailed": "Échec de l’enregistrement", "addManufacturerLabel": "Fabricant", "addDoseStrengthLabel": "Dosage", - "addDoseStrengthHint": "Tel qu'imprimé sur le contenant, par ex. 5 mg/0,5 ml." + "addDoseStrengthHint": "Tel qu'imprimé sur le contenant, par ex. 5 mg/0,5 ml.", + "openedOn": "Ouvert le {date}", + "expiresOn": "Périme le {date}", + "summaryAllExpired": "Aucun stock utilisable : {units} unités marquées périmées", + "editExpiryLabel": "Date de péremption imprimée", + "editExpiryHelper": "La date imprimée sur la boîte. Laissez vide si aucune n'est enregistrée.", + "editOpenedLabel": "Ouvert le", + "editOpenedHelper": "Laissez vide si le contenant est encore scellé.", + "editOpenedHelperClock": "Un stylo ou une ampoule périme 30 jours après ouverture. Laissez vide s'il est encore scellé." }, "erweitert": { "group": { @@ -7008,7 +7016,8 @@ "title": "Stock faible : {medName}", "bodyToday": "{medName} : {units} unités restantes — moins d’un jour de réserve.", "bodyReorder": "{medName} : {units} unités restantes. Le stock sera épuisé vers le {runsOutOn} — recommandez avant le {reorderBy}.", - "bodyLastDose": "{medName} : {units} unités restantes — environ votre dernière dose. Le stock sera épuisé vers le {runsOutOn}." + "bodyLastDose": "{medName} : {units} unités restantes — environ votre dernière dose. Le stock sera épuisé vers le {runsOutOn}.", + "bodyExpired": "{medName} : plus de stock utilisable. {units} unités sont marquées périmées." }, "moodReminders": { "dailyTitle": "Enregistrer votre humeur", diff --git a/messages/it.json b/messages/it.json index 03c3462ee..6bc653e8c 100644 --- a/messages/it.json +++ b/messages/it.json @@ -1716,8 +1716,8 @@ "addSuccess": "Scorta registrata.", "addFailed": "Impossibile registrare la scorta.", "adjustButton": "Correggi", - "adjustTitle": "Correggi la scorta", - "adjustDescription": "Imposta le unità rimanenti — per correzioni o un prelievo. 0 segna la scorta come esaurita.", + "adjustTitle": "Correggi il contenitore", + "adjustDescription": "Imposta le unità rimanenti e correggi le date di questo contenitore. 0 unità lo segna come esaurito.", "adjustQuantityLabel": "Unità rimanenti", "adjustHelper": "Capacità: {total} unità.", "adjustSubmit": "Salva", @@ -1757,7 +1757,15 @@ "packagingFailed": "Salvataggio non riuscito", "addManufacturerLabel": "Produttore", "addDoseStrengthLabel": "Dosaggio", - "addDoseStrengthHint": "Come stampato sul contenitore, ad es. 5 mg/0,5 ml." + "addDoseStrengthHint": "Come stampato sul contenitore, ad es. 5 mg/0,5 ml.", + "openedOn": "Aperto il {date}", + "expiresOn": "Scade il {date}", + "summaryAllExpired": "Nessuna scorta utilizzabile: {units} unità contrassegnate come scadute", + "editExpiryLabel": "Data di scadenza stampata", + "editExpiryHelper": "La data stampata sulla confezione. Lascia vuoto se non ne è registrata nessuna.", + "editOpenedLabel": "Aperto il", + "editOpenedHelper": "Lascia vuoto se il contenitore è ancora sigillato.", + "editOpenedHelperClock": "Una penna o una fiala scade 30 giorni dopo l'apertura. Lascia vuoto se è ancora sigillata." }, "erweitert": { "group": { @@ -7008,7 +7016,8 @@ "title": "Scorte in esaurimento: {medName}", "bodyToday": "{medName}: {units} unità rimaste — meno di un giorno di scorta.", "bodyReorder": "{medName}: {units} unità rimaste. Le scorte finiscono intorno al {runsOutOn} — riordina entro il {reorderBy}.", - "bodyLastDose": "{medName}: {units} unità rimaste — circa la tua ultima dose. Le scorte finiscono intorno al {runsOutOn}." + "bodyLastDose": "{medName}: {units} unità rimaste — circa la tua ultima dose. Le scorte finiscono intorno al {runsOutOn}.", + "bodyExpired": "{medName}: nessuna scorta utilizzabile. {units} unità sono contrassegnate come scadute." }, "moodReminders": { "dailyTitle": "Registra il tuo umore", diff --git a/messages/pl.json b/messages/pl.json index f1d9fb61a..b05dc3814 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -1716,8 +1716,8 @@ "addSuccess": "Zapas zarejestrowany.", "addFailed": "Nie udało się zarejestrować zapasu.", "adjustButton": "Skoryguj", - "adjustTitle": "Skoryguj zapas", - "adjustDescription": "Ustaw pozostałe jednostki — przy korekcie lub pobraniu. 0 oznacza zapas jako zużyty.", + "adjustTitle": "Popraw opakowanie", + "adjustDescription": "Ustaw pozostałe jednostki i popraw daty tego opakowania. 0 jednostek oznacza je jako zużyte.", "adjustQuantityLabel": "Pozostałe jednostki", "adjustHelper": "Pojemność: {total} jednostek.", "adjustSubmit": "Zapisz", @@ -1757,7 +1757,15 @@ "packagingFailed": "Zapis nie powiódł się", "addManufacturerLabel": "Producent", "addDoseStrengthLabel": "Moc", - "addDoseStrengthHint": "Zgodnie z nadrukiem na opakowaniu, np. 5 mg/0,5 ml." + "addDoseStrengthHint": "Zgodnie z nadrukiem na opakowaniu, np. 5 mg/0,5 ml.", + "openedOn": "Otwarto {date}", + "expiresOn": "Ważne do {date}", + "summaryAllExpired": "Brak zapasu do użycia: {units} jednostek oznaczono jako przeterminowane", + "editExpiryLabel": "Data ważności z opakowania", + "editExpiryHelper": "Data nadrukowana na opakowaniu. Zostaw puste, jeśli nie została zapisana.", + "editOpenedLabel": "Data otwarcia", + "editOpenedHelper": "Zostaw puste, jeśli opakowanie jest jeszcze zamknięte.", + "editOpenedHelperClock": "Pen lub ampułka traci ważność 30 dni po otwarciu. Zostaw puste, jeśli jest jeszcze zamknięta." }, "erweitert": { "group": { @@ -7008,7 +7016,8 @@ "title": "Niski zapas: {medName}", "bodyToday": "{medName}: pozostało {units} jednostek — mniej niż jeden dzień zapasu.", "bodyReorder": "{medName}: pozostało {units} jednostek. Zapas skończy się około {runsOutOn} — zamów ponownie do {reorderBy}.", - "bodyLastDose": "{medName}: pozostało {units} jednostek — to mniej więcej Twoja ostatnia dawka. Zapas skończy się około {runsOutOn}." + "bodyLastDose": "{medName}: pozostało {units} jednostek — to mniej więcej Twoja ostatnia dawka. Zapas skończy się około {runsOutOn}.", + "bodyExpired": "{medName}: brak zapasu do użycia. {units} jednostek oznaczono jako przeterminowane." }, "moodReminders": { "dailyTitle": "Zapisz nastrój", diff --git a/src/components/medications/__tests__/inventory-section-dates.test.tsx b/src/components/medications/__tests__/inventory-section-dates.test.tsx new file mode 100644 index 000000000..b52cbcff2 --- /dev/null +++ b/src/components/medications/__tests__/inventory-section-dates.test.tsx @@ -0,0 +1,208 @@ +/** + * Bestand item-row dates: visible on the row, editable in the correction + * dialog. + * + * Both dates decide the container's state — the printed expiry directly, + * the opening date through the post-opening window on a pen or an + * ampoule — and the opening date is written by the intake consumption + * hook without the user ever typing it. Until now neither was rendered + * anywhere and neither could be corrected, so a container could read + * EXPIRED with nothing on screen to explain why. + * + * Project convention is SSR-only component tests (`renderToStaticMarkup`). + */ +import { describe, expect, it, vi } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +import { I18nProvider } from "@/lib/i18n/context"; + +// The Radix Dialog / Select portal at runtime, so their bodies never +// materialise in static markup. Collapse the primitives to plain +// wrappers (same trick as the AddInventoryDialog suite). +vi.mock("@/components/ui/dialog", () => { + const Pass = ({ children }: { children?: React.ReactNode }) => ( +
- {t("medications.detail.bestand.summary", { remaining, total })} - {perDose !== 1 && ( + {/* "0 of 0 doses" beside "+ 54 units expired" read as two + contradicting statements — the first says nothing was ever + configured, the second says stock was written off. When the + available pool is empty and expired stock is all that is + left, say that instead. */} + {allExpired + ? t("medications.detail.bestand.summaryAllExpired", { + units: expiredUnits, + }) + : t("medications.detail.bestand.summary", { remaining, total })} + {perDose !== 1 && !allExpired && ( {t("medications.detail.bestand.unitsDetail", { remaining: formatUnitCount(remainingUnits), @@ -311,7 +364,7 @@ export function InventorySection({ })} )} - {expiredUnits > 0 && ( + {expiredUnits > 0 && !allExpired && ( + {/* The two dates that decide the row's state. They used to + be invisible here — a container could read EXPIRED with + nothing on screen to explain why, and no way to correct + the value that caused it. */} + {(item.firstUseAt || item.printedExpiry) && ( + + {item.firstUseAt && ( + + {t("medications.detail.bestand.openedOn", { + date: formatDate( + toLocalDayInput(item.firstUseAt), + dateFormatPref, + locale, + ), + })} + + )} + {item.printedExpiry && ( + + {t("medications.detail.bestand.expiresOn", { + date: formatDate( + toLocalDayInput(item.printedExpiry), + dateFormatPref, + locale, + ), + })} + + )} + + )}
+ {t("medications.detail.bestand.editExpiryHelper")} +
++ {CLOCK_CONTAINER_TYPES.includes(item.containerType) + ? t("medications.detail.bestand.editOpenedHelperClock") + : t("medications.detail.bestand.editOpenedHelper")} +
+