diff --git a/messages/de.json b/messages/de.json index 45c6923d3..b2b7f48fe 100644 --- a/messages/de.json +++ b/messages/de.json @@ -8,6 +8,7 @@ "create": "Erstellen", "close": "Schließen", "loading": "Laden...", + "loadFailed": "Konnte nicht geladen werden", "error": "Fehler", "saved": "Gespeichert", "no": "Nein", @@ -1109,6 +1110,117 @@ "gatedCta": "Forschungsmodus in den Einstellungen öffnen", "unknownDrug": "{name} ist nicht im EMA-zugelassenen GLP-1-Katalog enthalten. HealthLog kann dafür keine Populations-Pharmakokinetik-Kurve erzeugen." } + }, + "detail": { + "status": { + "active": "Aktiv", + "paused": "Pausiert", + "ended": "Beendet" + }, + "today": { + "groupLabel": "Heutige Dosis", + "taken": "Genommen", + "skipped": "Übersprungen", + "toastTaken": "Heutige Dosis erfasst", + "toastSkipped": "Heutige Dosis übersprungen", + "recordedTaken": "Heute genommen um {time}", + "recordedSkipped": "Heute übersprungen um {time}", + "recordedOneShot": "Einmalig genommen um {time}", + "noneScheduled": "Heute keine Einnahme geplant.", + "pausedHint": "Pausiert — heute keine Erinnerung.", + "error": "Dosis konnte nicht erfasst werden." + }, + "cadence": { + "title": "Rhythmus", + "oneShotOn": "Einmalige Dosis am {date}.", + "oneShotPending": "Einmalige Dosis ausstehend." + }, + "intake": { + "title": "Einnahmeverlauf", + "importButton": "Importieren", + "viewAllLink": "Vollständigen Verlauf öffnen →" + }, + "notifications": { + "title": "Erinnerungen", + "switchLabel": "Erinnern, wenn die Dosis fällig ist", + "helperOn": "Erinnerungen sind aktiv.", + "helperOff": "Erinnerungen sind aus.", + "enabledToast": "Erinnerungen aktiv", + "disabledToast": "Erinnerungen aus", + "toggleFailed": "Erinnerungs-Einstellung konnte nicht geändert werden.", + "clientManagedChip": "Dein iPhone steuert die Erinnerungen für dieses Medikament." + }, + "settings": { + "title": "Einstellungen", + "csvImport": { + "title": "CSV-Import", + "stub": "Importiere Einnahmen direkt aus dem Verlauf oben." + }, + "phases": { + "openButton": "Phasen konfigurieren", + "requiresCourseWindow": "Phasen sind nur mit Kurs-Fenster verfügbar." + }, + "grace": { + "label": "Erinnerungsfenster — gilt für deinen Hauptzeitplan", + "primaryScheduleNote": "Andere Zeitpläne behalten ihr Standard-Fenster.", + "unit": "Minuten", + "saved": "Erinnerungsfenster gespeichert", + "failed": "Erinnerungsfenster konnte nicht gespeichert werden." + } + }, + "zone": { + "title": "Verwaltung & Gefahrenzone", + "pause": { + "title": "Pausieren", + "helper": "Erinnerungen anhalten, bis du sie wieder aktivierst. Verlauf bleibt erhalten.", + "pausedToast": "Erinnerungen pausiert", + "resumedToast": "Erinnerungen reaktiviert", + "failed": "Status konnte nicht geändert werden." + }, + "end": { + "title": "Medikament beenden", + "helper": "Erinnerungen stoppen und das Medikament als beendet markieren. Verlauf bleibt sichtbar.", + "button": "Beenden", + "dialogTitle": "{name} beenden — Erinnerungen stoppen, Verlauf bleibt sichtbar", + "dialogBody": "Keine weiteren Erinnerungen. Alte Einträge bleiben in der Übersicht.", + "toast": "Medikament beendet", + "failed": "Medikament konnte nicht beendet werden." + }, + "purge": { + "title": "Verlauf löschen", + "helper": "Es sind {count} Einnahmen für dieses Medikament gespeichert.", + "button": "Verlauf löschen", + "dialogTitle": "Verlauf wirklich löschen?", + "dialogBody": "Die {count} Einnahmen werden unwiderruflich gelöscht. Das Medikament selbst bleibt.", + "toast": "Verlauf gelöscht", + "failed": "Verlauf konnte nicht gelöscht werden." + }, + "delete": { + "title": "Medikament löschen", + "helper": "Entfernt das Medikament und alle zugehörigen Daten.", + "button": "Löschen", + "dialogTitle": "{name} löschen?", + "dialogBody": "Medikament, Zeitpläne, Verlauf, API-Tokens, Phasen-Konfiguration und Erinnerungen werden unwiderruflich gelöscht.", + "toast": "Medikament gelöscht", + "failed": "Medikament konnte nicht gelöscht werden." + } + }, + "phases": { + "modeLabel": "Modus für {phase}", + "saveFailed": "Phasen konnten nicht gespeichert werden." + }, + "api": { + "caption": "Endpunkt für „{name}\"", + "copyUrl": "URL kopieren", + "urlCopied": "URL kopiert", + "mintToken": "Token erzeugen", + "mintAnotherToken": "Weiteres Token erzeugen", + "mintFailed": "Token konnte nicht erzeugt werden.", + "copyFailed": "Kopieren fehlgeschlagen.", + "tokenCopied": "Token kopiert", + "copyToken": "Token kopieren", + "mintedHint": "Token jetzt kopieren — es wird nur einmal angezeigt." + } } }, "charts": { diff --git a/messages/en.json b/messages/en.json index a7c152585..83e75e71c 100644 --- a/messages/en.json +++ b/messages/en.json @@ -8,6 +8,7 @@ "create": "Create", "close": "Close", "loading": "Loading...", + "loadFailed": "Could not load", "error": "Error", "saved": "Saved", "no": "No", @@ -1109,6 +1110,117 @@ "gatedCta": "Open Research Mode in Settings", "unknownDrug": "{name} is not in the EMA-approved GLP-1 catalog, so HealthLog cannot render a population-PK curve for it." } + }, + "detail": { + "status": { + "active": "Active", + "paused": "Paused", + "ended": "Ended" + }, + "today": { + "groupLabel": "Today's dose", + "taken": "Taken", + "skipped": "Skipped", + "toastTaken": "Today's dose logged", + "toastSkipped": "Today's dose skipped", + "recordedTaken": "Taken today at {time}", + "recordedSkipped": "Skipped today at {time}", + "recordedOneShot": "One-shot dose taken at {time}", + "noneScheduled": "No dose scheduled for today.", + "pausedHint": "Paused — no reminder today.", + "error": "Could not log the dose." + }, + "cadence": { + "title": "Cadence", + "oneShotOn": "One-time dose on {date}.", + "oneShotPending": "One-time dose pending." + }, + "intake": { + "title": "Intake history", + "importButton": "Import", + "viewAllLink": "View full history →" + }, + "notifications": { + "title": "Notifications", + "switchLabel": "Send a reminder when this dose is due", + "helperOn": "Reminders are on.", + "helperOff": "Reminders are off.", + "enabledToast": "Reminders on", + "disabledToast": "Reminders off", + "toggleFailed": "Could not change the reminder setting.", + "clientManagedChip": "Your iPhone manages reminders for this medication." + }, + "settings": { + "title": "Settings", + "csvImport": { + "title": "CSV import", + "stub": "Import intakes directly from the history above." + }, + "phases": { + "openButton": "Configure phases", + "requiresCourseWindow": "Phases are available once a course window is set." + }, + "grace": { + "label": "Reminder window — applies to your primary schedule", + "primaryScheduleNote": "Multi-schedule medications keep their other schedules' default window.", + "unit": "minutes", + "saved": "Reminder window saved", + "failed": "Could not save the reminder window." + } + }, + "zone": { + "title": "Manage & danger zone", + "pause": { + "title": "Pause", + "helper": "Pause reminders until you turn them back on. History stays intact.", + "pausedToast": "Reminders paused", + "resumedToast": "Reminders resumed", + "failed": "Could not change the paused state." + }, + "end": { + "title": "End medication", + "helper": "Stop reminders and mark the medication as ended. History stays visible.", + "button": "End", + "dialogTitle": "End {name} — stop reminders, history stays visible", + "dialogBody": "No more reminders. Old entries remain in the timeline.", + "toast": "Medication ended", + "failed": "Could not end the medication." + }, + "purge": { + "title": "Delete history", + "helper": "{count} intake events are stored for this medication.", + "button": "Delete history", + "dialogTitle": "Really delete the history?", + "dialogBody": "The {count} intake events will be removed permanently. The medication itself stays.", + "toast": "History deleted", + "failed": "Could not delete the history." + }, + "delete": { + "title": "Delete medication", + "helper": "Removes the medication and every related record.", + "button": "Delete", + "dialogTitle": "Delete {name}?", + "dialogBody": "The medication, its schedules, intake history, API tokens, phase config and reminders will be removed permanently.", + "toast": "Medication deleted", + "failed": "Could not delete the medication." + } + }, + "phases": { + "modeLabel": "{phase} mode", + "saveFailed": "Could not save the reminder phases." + }, + "api": { + "caption": "Endpoint for \"{name}\"", + "copyUrl": "Copy URL", + "urlCopied": "URL copied", + "mintToken": "Mint token", + "mintAnotherToken": "Mint another token", + "mintFailed": "Could not mint a token.", + "copyFailed": "Could not copy.", + "tokenCopied": "Token copied", + "copyToken": "Copy token", + "mintedHint": "Copy this token now — it is shown only once." + } } }, "charts": { diff --git a/messages/es.json b/messages/es.json index 6f7c8653f..71430b852 100644 --- a/messages/es.json +++ b/messages/es.json @@ -8,6 +8,7 @@ "create": "Crear", "close": "Cerrar", "loading": "Cargando…...", + "loadFailed": "Could not load", "error": "Error", "saved": "Guardado", "no": "No", @@ -1109,6 +1110,117 @@ "gatedCta": "Abrir el modo Investigación en los ajustes", "unknownDrug": "{name} no está en el catálogo de GLP-1 aprobado por la EMA, por lo que HealthLog no puede trazar una curva de farmacocinética poblacional." } + }, + "detail": { + "status": { + "active": "Active", + "paused": "Paused", + "ended": "Ended" + }, + "today": { + "groupLabel": "Today's dose", + "taken": "Taken", + "skipped": "Skipped", + "toastTaken": "Today's dose logged", + "toastSkipped": "Today's dose skipped", + "recordedTaken": "Taken today at {time}", + "recordedSkipped": "Skipped today at {time}", + "recordedOneShot": "One-shot dose taken at {time}", + "noneScheduled": "No dose scheduled for today.", + "pausedHint": "Paused — no reminder today.", + "error": "Could not log the dose." + }, + "cadence": { + "title": "Cadence", + "oneShotOn": "One-time dose on {date}.", + "oneShotPending": "One-time dose pending." + }, + "intake": { + "title": "Intake history", + "importButton": "Import", + "viewAllLink": "View full history →" + }, + "notifications": { + "title": "Notifications", + "switchLabel": "Send a reminder when this dose is due", + "helperOn": "Reminders are on.", + "helperOff": "Reminders are off.", + "enabledToast": "Reminders on", + "disabledToast": "Reminders off", + "toggleFailed": "Could not change the reminder setting.", + "clientManagedChip": "Your iPhone manages reminders for this medication." + }, + "settings": { + "title": "Settings", + "csvImport": { + "title": "CSV import", + "stub": "Import intakes directly from the history above." + }, + "phases": { + "openButton": "Configure phases", + "requiresCourseWindow": "Phases are available once a course window is set." + }, + "grace": { + "label": "Reminder window — applies to your primary schedule", + "primaryScheduleNote": "Multi-schedule medications keep their other schedules' default window.", + "unit": "minutes", + "saved": "Reminder window saved", + "failed": "Could not save the reminder window." + } + }, + "zone": { + "title": "Manage & danger zone", + "pause": { + "title": "Pause", + "helper": "Pause reminders until you turn them back on. History stays intact.", + "pausedToast": "Reminders paused", + "resumedToast": "Reminders resumed", + "failed": "Could not change the paused state." + }, + "end": { + "title": "End medication", + "helper": "Stop reminders and mark the medication as ended. History stays visible.", + "button": "End", + "dialogTitle": "End {name} — stop reminders, history stays visible", + "dialogBody": "No more reminders. Old entries remain in the timeline.", + "toast": "Medication ended", + "failed": "Could not end the medication." + }, + "purge": { + "title": "Delete history", + "helper": "{count} intake events are stored for this medication.", + "button": "Delete history", + "dialogTitle": "Really delete the history?", + "dialogBody": "The {count} intake events will be removed permanently. The medication itself stays.", + "toast": "History deleted", + "failed": "Could not delete the history." + }, + "delete": { + "title": "Delete medication", + "helper": "Removes the medication and every related record.", + "button": "Delete", + "dialogTitle": "Delete {name}?", + "dialogBody": "The medication, its schedules, intake history, API tokens, phase config and reminders will be removed permanently.", + "toast": "Medication deleted", + "failed": "Could not delete the medication." + } + }, + "phases": { + "modeLabel": "{phase} mode", + "saveFailed": "Could not save the reminder phases." + }, + "api": { + "caption": "Endpoint for \"{name}\"", + "copyUrl": "Copy URL", + "urlCopied": "URL copied", + "mintToken": "Mint token", + "mintAnotherToken": "Mint another token", + "mintFailed": "Could not mint a token.", + "copyFailed": "Could not copy.", + "tokenCopied": "Token copied", + "copyToken": "Copy token", + "mintedHint": "Copy this token now — it is shown only once." + } } }, "charts": { @@ -3389,4 +3501,4 @@ "dailyTitle": "Registrar el ánimo", "dailyBody": "¿Cómo te sientes hoy?" } -} +} \ No newline at end of file diff --git a/messages/fr.json b/messages/fr.json index 217aad16a..633479886 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -8,6 +8,7 @@ "create": "Créer", "close": "Fermer", "loading": "Chargement…...", + "loadFailed": "Could not load", "error": "Erreur", "saved": "Enregistré", "no": "Non", @@ -1109,6 +1110,117 @@ "gatedCta": "Ouvrir le mode Recherche dans les paramètres", "unknownDrug": "{name} n'est pas dans le catalogue GLP-1 approuvé par l'EMA, donc HealthLog ne peut pas tracer de courbe de pharmacocinétique de population." } + }, + "detail": { + "status": { + "active": "Active", + "paused": "Paused", + "ended": "Ended" + }, + "today": { + "groupLabel": "Today's dose", + "taken": "Taken", + "skipped": "Skipped", + "toastTaken": "Today's dose logged", + "toastSkipped": "Today's dose skipped", + "recordedTaken": "Taken today at {time}", + "recordedSkipped": "Skipped today at {time}", + "recordedOneShot": "One-shot dose taken at {time}", + "noneScheduled": "No dose scheduled for today.", + "pausedHint": "Paused — no reminder today.", + "error": "Could not log the dose." + }, + "cadence": { + "title": "Cadence", + "oneShotOn": "One-time dose on {date}.", + "oneShotPending": "One-time dose pending." + }, + "intake": { + "title": "Intake history", + "importButton": "Import", + "viewAllLink": "View full history →" + }, + "notifications": { + "title": "Notifications", + "switchLabel": "Send a reminder when this dose is due", + "helperOn": "Reminders are on.", + "helperOff": "Reminders are off.", + "enabledToast": "Reminders on", + "disabledToast": "Reminders off", + "toggleFailed": "Could not change the reminder setting.", + "clientManagedChip": "Your iPhone manages reminders for this medication." + }, + "settings": { + "title": "Settings", + "csvImport": { + "title": "CSV import", + "stub": "Import intakes directly from the history above." + }, + "phases": { + "openButton": "Configure phases", + "requiresCourseWindow": "Phases are available once a course window is set." + }, + "grace": { + "label": "Reminder window — applies to your primary schedule", + "primaryScheduleNote": "Multi-schedule medications keep their other schedules' default window.", + "unit": "minutes", + "saved": "Reminder window saved", + "failed": "Could not save the reminder window." + } + }, + "zone": { + "title": "Manage & danger zone", + "pause": { + "title": "Pause", + "helper": "Pause reminders until you turn them back on. History stays intact.", + "pausedToast": "Reminders paused", + "resumedToast": "Reminders resumed", + "failed": "Could not change the paused state." + }, + "end": { + "title": "End medication", + "helper": "Stop reminders and mark the medication as ended. History stays visible.", + "button": "End", + "dialogTitle": "End {name} — stop reminders, history stays visible", + "dialogBody": "No more reminders. Old entries remain in the timeline.", + "toast": "Medication ended", + "failed": "Could not end the medication." + }, + "purge": { + "title": "Delete history", + "helper": "{count} intake events are stored for this medication.", + "button": "Delete history", + "dialogTitle": "Really delete the history?", + "dialogBody": "The {count} intake events will be removed permanently. The medication itself stays.", + "toast": "History deleted", + "failed": "Could not delete the history." + }, + "delete": { + "title": "Delete medication", + "helper": "Removes the medication and every related record.", + "button": "Delete", + "dialogTitle": "Delete {name}?", + "dialogBody": "The medication, its schedules, intake history, API tokens, phase config and reminders will be removed permanently.", + "toast": "Medication deleted", + "failed": "Could not delete the medication." + } + }, + "phases": { + "modeLabel": "{phase} mode", + "saveFailed": "Could not save the reminder phases." + }, + "api": { + "caption": "Endpoint for \"{name}\"", + "copyUrl": "Copy URL", + "urlCopied": "URL copied", + "mintToken": "Mint token", + "mintAnotherToken": "Mint another token", + "mintFailed": "Could not mint a token.", + "copyFailed": "Could not copy.", + "tokenCopied": "Token copied", + "copyToken": "Copy token", + "mintedHint": "Copy this token now — it is shown only once." + } } }, "charts": { @@ -3389,4 +3501,4 @@ "dailyTitle": "Enregistrer votre humeur", "dailyBody": "Comment vous sentez-vous aujourd’hui ?" } -} +} \ No newline at end of file diff --git a/messages/it.json b/messages/it.json index 8bb1a9b95..b30686d47 100644 --- a/messages/it.json +++ b/messages/it.json @@ -8,6 +8,7 @@ "create": "Crea", "close": "Chiudi", "loading": "Caricamento…...", + "loadFailed": "Could not load", "error": "Errore", "saved": "Salvato", "no": "No", @@ -1109,6 +1110,117 @@ "gatedCta": "Apri la modalità Ricerca nelle impostazioni", "unknownDrug": "{name} non è nel catalogo GLP-1 approvato dall'EMA, quindi HealthLog non può tracciare una curva di farmacocinetica di popolazione." } + }, + "detail": { + "status": { + "active": "Active", + "paused": "Paused", + "ended": "Ended" + }, + "today": { + "groupLabel": "Today's dose", + "taken": "Taken", + "skipped": "Skipped", + "toastTaken": "Today's dose logged", + "toastSkipped": "Today's dose skipped", + "recordedTaken": "Taken today at {time}", + "recordedSkipped": "Skipped today at {time}", + "recordedOneShot": "One-shot dose taken at {time}", + "noneScheduled": "No dose scheduled for today.", + "pausedHint": "Paused — no reminder today.", + "error": "Could not log the dose." + }, + "cadence": { + "title": "Cadence", + "oneShotOn": "One-time dose on {date}.", + "oneShotPending": "One-time dose pending." + }, + "intake": { + "title": "Intake history", + "importButton": "Import", + "viewAllLink": "View full history →" + }, + "notifications": { + "title": "Notifications", + "switchLabel": "Send a reminder when this dose is due", + "helperOn": "Reminders are on.", + "helperOff": "Reminders are off.", + "enabledToast": "Reminders on", + "disabledToast": "Reminders off", + "toggleFailed": "Could not change the reminder setting.", + "clientManagedChip": "Your iPhone manages reminders for this medication." + }, + "settings": { + "title": "Settings", + "csvImport": { + "title": "CSV import", + "stub": "Import intakes directly from the history above." + }, + "phases": { + "openButton": "Configure phases", + "requiresCourseWindow": "Phases are available once a course window is set." + }, + "grace": { + "label": "Reminder window — applies to your primary schedule", + "primaryScheduleNote": "Multi-schedule medications keep their other schedules' default window.", + "unit": "minutes", + "saved": "Reminder window saved", + "failed": "Could not save the reminder window." + } + }, + "zone": { + "title": "Manage & danger zone", + "pause": { + "title": "Pause", + "helper": "Pause reminders until you turn them back on. History stays intact.", + "pausedToast": "Reminders paused", + "resumedToast": "Reminders resumed", + "failed": "Could not change the paused state." + }, + "end": { + "title": "End medication", + "helper": "Stop reminders and mark the medication as ended. History stays visible.", + "button": "End", + "dialogTitle": "End {name} — stop reminders, history stays visible", + "dialogBody": "No more reminders. Old entries remain in the timeline.", + "toast": "Medication ended", + "failed": "Could not end the medication." + }, + "purge": { + "title": "Delete history", + "helper": "{count} intake events are stored for this medication.", + "button": "Delete history", + "dialogTitle": "Really delete the history?", + "dialogBody": "The {count} intake events will be removed permanently. The medication itself stays.", + "toast": "History deleted", + "failed": "Could not delete the history." + }, + "delete": { + "title": "Delete medication", + "helper": "Removes the medication and every related record.", + "button": "Delete", + "dialogTitle": "Delete {name}?", + "dialogBody": "The medication, its schedules, intake history, API tokens, phase config and reminders will be removed permanently.", + "toast": "Medication deleted", + "failed": "Could not delete the medication." + } + }, + "phases": { + "modeLabel": "{phase} mode", + "saveFailed": "Could not save the reminder phases." + }, + "api": { + "caption": "Endpoint for \"{name}\"", + "copyUrl": "Copy URL", + "urlCopied": "URL copied", + "mintToken": "Mint token", + "mintAnotherToken": "Mint another token", + "mintFailed": "Could not mint a token.", + "copyFailed": "Could not copy.", + "tokenCopied": "Token copied", + "copyToken": "Copy token", + "mintedHint": "Copy this token now — it is shown only once." + } } }, "charts": { @@ -3389,4 +3501,4 @@ "dailyTitle": "Registra il tuo umore", "dailyBody": "Come ti senti oggi?" } -} +} \ No newline at end of file diff --git a/messages/pl.json b/messages/pl.json index 11c8dc5f3..f72fe0d28 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -8,6 +8,7 @@ "create": "Utwórz", "close": "Zamknij", "loading": "Ładowanie…...", + "loadFailed": "Could not load", "error": "Błąd", "saved": "Zapisano", "no": "Nie", @@ -1109,6 +1110,117 @@ "gatedCta": "Otwórz tryb Badawczy w Ustawieniach", "unknownDrug": "{name} nie znajduje się w katalogu GLP-1 zatwierdzonym przez EMA, więc HealthLog nie może wyrysować krzywej farmakokinetyki populacyjnej." } + }, + "detail": { + "status": { + "active": "Active", + "paused": "Paused", + "ended": "Ended" + }, + "today": { + "groupLabel": "Today's dose", + "taken": "Taken", + "skipped": "Skipped", + "toastTaken": "Today's dose logged", + "toastSkipped": "Today's dose skipped", + "recordedTaken": "Taken today at {time}", + "recordedSkipped": "Skipped today at {time}", + "recordedOneShot": "One-shot dose taken at {time}", + "noneScheduled": "No dose scheduled for today.", + "pausedHint": "Paused — no reminder today.", + "error": "Could not log the dose." + }, + "cadence": { + "title": "Cadence", + "oneShotOn": "One-time dose on {date}.", + "oneShotPending": "One-time dose pending." + }, + "intake": { + "title": "Intake history", + "importButton": "Import", + "viewAllLink": "View full history →" + }, + "notifications": { + "title": "Notifications", + "switchLabel": "Send a reminder when this dose is due", + "helperOn": "Reminders are on.", + "helperOff": "Reminders are off.", + "enabledToast": "Reminders on", + "disabledToast": "Reminders off", + "toggleFailed": "Could not change the reminder setting.", + "clientManagedChip": "Your iPhone manages reminders for this medication." + }, + "settings": { + "title": "Settings", + "csvImport": { + "title": "CSV import", + "stub": "Import intakes directly from the history above." + }, + "phases": { + "openButton": "Configure phases", + "requiresCourseWindow": "Phases are available once a course window is set." + }, + "grace": { + "label": "Reminder window — applies to your primary schedule", + "primaryScheduleNote": "Multi-schedule medications keep their other schedules' default window.", + "unit": "minutes", + "saved": "Reminder window saved", + "failed": "Could not save the reminder window." + } + }, + "zone": { + "title": "Manage & danger zone", + "pause": { + "title": "Pause", + "helper": "Pause reminders until you turn them back on. History stays intact.", + "pausedToast": "Reminders paused", + "resumedToast": "Reminders resumed", + "failed": "Could not change the paused state." + }, + "end": { + "title": "End medication", + "helper": "Stop reminders and mark the medication as ended. History stays visible.", + "button": "End", + "dialogTitle": "End {name} — stop reminders, history stays visible", + "dialogBody": "No more reminders. Old entries remain in the timeline.", + "toast": "Medication ended", + "failed": "Could not end the medication." + }, + "purge": { + "title": "Delete history", + "helper": "{count} intake events are stored for this medication.", + "button": "Delete history", + "dialogTitle": "Really delete the history?", + "dialogBody": "The {count} intake events will be removed permanently. The medication itself stays.", + "toast": "History deleted", + "failed": "Could not delete the history." + }, + "delete": { + "title": "Delete medication", + "helper": "Removes the medication and every related record.", + "button": "Delete", + "dialogTitle": "Delete {name}?", + "dialogBody": "The medication, its schedules, intake history, API tokens, phase config and reminders will be removed permanently.", + "toast": "Medication deleted", + "failed": "Could not delete the medication." + } + }, + "phases": { + "modeLabel": "{phase} mode", + "saveFailed": "Could not save the reminder phases." + }, + "api": { + "caption": "Endpoint for \"{name}\"", + "copyUrl": "Copy URL", + "urlCopied": "URL copied", + "mintToken": "Mint token", + "mintAnotherToken": "Mint another token", + "mintFailed": "Could not mint a token.", + "copyFailed": "Could not copy.", + "tokenCopied": "Token copied", + "copyToken": "Copy token", + "mintedHint": "Copy this token now — it is shown only once." + } } }, "charts": { @@ -3389,4 +3501,4 @@ "dailyTitle": "Zapisz nastrój", "dailyBody": "Jak się dzisiaj czujesz?" } -} +} \ No newline at end of file diff --git a/src/app/medications/[id]/page.tsx b/src/app/medications/[id]/page.tsx new file mode 100644 index 000000000..56bdf5ccf --- /dev/null +++ b/src/app/medications/[id]/page.tsx @@ -0,0 +1,392 @@ +"use client"; + +/** + * v1.5.5 D-3 §6 + §9 — medication detail page. + * + * Replaces the v1.5.4 "form retirement" gap that left every action + * (Pausieren / Beenden / Notifications / API tokens / Phasen / CSV + * import / Verlauf löschen / Medikament löschen) reachable only + * through the list-page kebab — which the kebab didn't actually wire + * up. The detail page surfaces every restored action in section + * order; one phone-scroll reaches every one of them. + * + * Recurring variant: 8 sections (header → today's dose → cadence → + * dose ladder / phases → intake history preview → notifications → + * settings → destructive zone). + * + * One-shot variant: 5 sections (header → today's-dose-or-logged → + * cadence static line → intake history single row → destructive + * zone). Per D-3 §6 the wizard handles cadence edit via the header + * pencil so the cadence row hides its own edit affordance. + * + * Paused variant: same as the recurring/one-shot layout; the header + * status pill flips and the today's-dose card disables both buttons + * with a muted helper. Settings and destructive zone keep rendering. + * + * Reads: + * + * - `medicationDetail(id)` for the medication snapshot. + * - `medicationIntakeList(id, …)` for the last-14-grouped preview + * (the v2 list owns its own paging). + * - `notificationsStatus()` + `authMe()` on the notifications + * section. + * - `medicationTitration(id)` on the GLP-1 dose ladder. + * + * Mutations all cascade through `medicationDependentKeys` (which now + * includes the `compliance-chart-inline` prefix per D-3 §10). + */ + +import { use, useEffect, useState } from "react"; +import Link from "next/link"; +import { useQuery } from "@tanstack/react-query"; +import { useRouter } from "next/navigation"; +import { ArrowLeft, Loader2 } from "lucide-react"; + +import { useAuth } from "@/hooks/use-auth"; +import { useTranslations, useFormatters } from "@/lib/i18n/context"; +import { queryKeys } from "@/lib/query-keys"; +import { parseScheduleRecurrence } from "@/lib/medication-schedule"; + +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import { DrugLevelChart } from "@/components/medications/DrugLevelChart"; +import { TitrationSection } from "@/components/medications/TitrationSection"; +import { MedicationDetailHeader } from "@/components/medications/medication-detail-header"; +import { TodaysDoseCard } from "@/components/medications/todays-dose-card"; +import { CadenceSummaryRow } from "@/components/medications/cadence-summary-row"; +import { IntakeHistoryPreview } from "@/components/medications/sections/intake-history-preview"; +import { NotificationsSection } from "@/components/medications/sections/notifications-section"; +import { SettingsSection } from "@/components/medications/sections/settings-section"; +import { DestructiveZoneSection } from "@/components/medications/sections/destructive-zone-section"; +import { MedicationWizardDialog } from "@/components/medications/wizard/MedicationWizardDialog"; +import type { MedicationPayload } from "@/components/medications/wizard/wizard-payload"; + +interface ScheduleSnapshot { + id: string; + windowStart: string; + windowEnd: string; + label: string | null; + dose: string | null; + daysOfWeek: string | null; + timesOfDay?: string[]; + rrule?: string | null; + rollingIntervalDays?: number | null; + reminderGraceMinutes?: number | null; +} + +interface MedicationDetailSnapshot { + id: string; + name: string; + dose: string; + category: string; + treatmentClass?: string; + active: boolean; + notificationsEnabled: boolean; + pausedAt: string | null; + startsOn?: string | null; + endsOn?: string | null; + oneShot?: boolean; + schedules: ScheduleSnapshot[]; +} + +interface IntakeListResponse { + events: Array<{ + id: string; + scheduledFor: string; + takenAt: string | null; + skipped: boolean; + }>; + meta: { total: number; limit: number; offset: number }; +} + +const INTAKE_PREVIEW_PARAMS = { + sortBy: "takenAt", + sortDir: "desc" as const, + limit: 14, + offset: 0, + status: "completed", +}; + +function snapshotToWizardPayload( + med: MedicationDetailSnapshot, +): MedicationPayload { + return { + id: med.id, + name: med.name, + dose: med.dose, + category: med.category, + treatmentClass: med.treatmentClass, + notificationsEnabled: med.notificationsEnabled, + startsOn: med.startsOn ? new Date(med.startsOn) : null, + endsOn: med.endsOn ? new Date(med.endsOn) : null, + oneShot: med.oneShot ?? false, + schedules: med.schedules.map((s) => ({ + id: s.id, + windowStart: s.windowStart, + windowEnd: s.windowEnd, + label: s.label ?? null, + dose: s.dose ?? null, + ...parseScheduleRecurrence(s.daysOfWeek), + timesOfDay: s.timesOfDay, + rrule: s.rrule ?? null, + rollingIntervalDays: s.rollingIntervalDays ?? null, + })), + }; +} + +function isToday(iso: string): boolean { + const d = new Date(iso); + const today = new Date(); + return ( + d.getFullYear() === today.getFullYear() && + d.getMonth() === today.getMonth() && + d.getDate() === today.getDate() + ); +} + +export default function MedicationDetailPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = use(params); + const { isAuthenticated, isLoading: authLoading } = useAuth(); + const router = useRouter(); + const { t } = useTranslations(); + const formatters = useFormatters(); + + const [wizardOpen, setWizardOpen] = useState(false); + const [wizardIntent, setWizardIntent] = useState< + "cadence" | "name" | undefined + >(undefined); + const [importOpen, setImportOpen] = useState(false); + + useEffect(() => { + if (!authLoading && !isAuthenticated) { + router.push("/auth/login"); + } + }, [authLoading, isAuthenticated, router]); + + const { + data: medication, + isLoading, + isError, + } = useQuery({ + queryKey: queryKeys.medicationDetail(id), + queryFn: async () => { + const res = await fetch(`/api/medications/${id}`); + if (!res.ok) throw new Error("medication_detail_failed"); + return (await res.json()).data as MedicationDetailSnapshot; + }, + enabled: isAuthenticated, + }); + + const { data: intakeList } = useQuery({ + queryKey: queryKeys.medicationIntakeList(id, INTAKE_PREVIEW_PARAMS), + queryFn: async () => { + const params = new URLSearchParams({ + sortBy: INTAKE_PREVIEW_PARAMS.sortBy, + sortDir: INTAKE_PREVIEW_PARAMS.sortDir, + limit: String(INTAKE_PREVIEW_PARAMS.limit), + offset: String(INTAKE_PREVIEW_PARAMS.offset), + status: INTAKE_PREVIEW_PARAMS.status, + }); + const res = await fetch( + `/api/medications/${id}/intake?${params.toString()}`, + ); + if (!res.ok) throw new Error("intake_list_failed"); + return (await res.json()).data as IntakeListResponse; + }, + enabled: isAuthenticated && !!medication, + }); + + if (authLoading || isLoading) { + return ( +
+
+ ); + } + + if (isError || !medication) { + return ( +
+ + +

+ {t("medications.loadFailed")} +

+
+
+ ); + } + + const oneShot = medication.oneShot === true; + const todayEvent = intakeList?.events.find( + (e) => + (e.takenAt && isToday(e.takenAt)) || + (e.skipped && isToday(e.scheduledFor)), + ); + const intakeCount = intakeList?.meta.total ?? 0; + const primaryGrace = + medication.schedules[0]?.reminderGraceMinutes ?? null; + + function openWizardWithIntent(intent: "cadence" | "name") { + setWizardIntent(intent); + setWizardOpen(true); + } + + function closeWizard() { + setWizardOpen(false); + setWizardIntent(undefined); + } + + return ( +
+ + + {/* §9.1 — Header band */} + openWizardWithIntent("name")} + /> + + {/* §9.2 — Today's dose card */} + + + {/* §9.3 — Cadence summary. One-shot: static line; the + `hideEdit` flag suppresses the row's own edit affordance + because the header pencil routes the wizard. */} + {oneShot ? ( + +

+ {medication.startsOn + ? t("medications.detail.cadence.oneShotOn", { + date: formatters.dateTime(medication.startsOn), + }) + : t("medications.detail.cadence.oneShotPending")} +

+
+ ) : ( + openWizardWithIntent("cadence")} + /> + )} + + {/* §9.4 — Dose ladder / Phasen (GLP-1 only, recurring path + only — one-shot variant drops this) */} + {!oneShot && medication.treatmentClass === "GLP1" && ( + <> + + + + )} + + {/* §9.5 — Intake history preview */} + + + {/* §9.6 + §9.7 — Notifications + Settings: recurring path only. */} + {!oneShot && ( + <> + + + + + )} + + {/* §9.8 — Verwaltung & Gefahrenzone */} + + + {/* Wizard mount — edit-only on the detail page. Intent flips + between "name" (header pencil → Step 1) and "cadence" + (cadence row pencil → Step 5). Cancel returns to the detail + page (the success branch refreshes the medication detail + via the `medicationDependentKeys` cascade). */} + { + if (!open) closeWizard(); + }} + mode="edit" + initial={snapshotToWizardPayload(medication)} + landingIntent={wizardIntent} + onSuccess={() => closeWizard()} + /> +
+ ); +} diff --git a/src/components/medications/cadence-summary-row.tsx b/src/components/medications/cadence-summary-row.tsx new file mode 100644 index 000000000..2e3471772 --- /dev/null +++ b/src/components/medications/cadence-summary-row.tsx @@ -0,0 +1,71 @@ +"use client"; + +/** + * v1.5.5 D-3 §9.3 — Cadence summary row. + * + * Renders the medication's cadence as a plain-language line (e.g. + * "Alle 12 h nach letzter Einnahme", "Alle 2 Wochen, mittwochs", + * "Monatlich am 15.") with an optional course-window sub-line, and an + * edit pencil that opens the wizard with `landingIntent: "cadence"`. + * For one-shot medications the line collapses to + * `Einmalig am DD.MM.` and the edit affordance is suppressed (the + * header pencil owns the wizard route per D-3 §6 one-shot variant). + * + * The DOM order is `

→

→ edit`. The button comes last so a + * screen-reader tab walk announces the title and the line before the + * affordance (C-E4-3). + */ + +import { Pencil } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { MedicationDetailSection } from "@/components/medications/medication-detail-section"; +import { + hydrateWizardPayload, + summariseCadence, + type MedicationPayload, +} from "@/components/medications/wizard/wizard-payload"; +import { useTranslations } from "@/lib/i18n/context"; + +export interface CadenceSummaryRowProps { + medication: MedicationPayload; + /** Hides the edit pencil — the one-shot variant relies on the header. */ + hideEdit?: boolean; + onEdit: () => void; +} + +export function CadenceSummaryRow({ + medication, + hideEdit = false, + onEdit, +}: CadenceSummaryRowProps) { + const { t } = useTranslations(); + const payload = hydrateWizardPayload(medication); + const line = summariseCadence(payload, t); + + return ( + +

+ {line} +

+ + ); +} diff --git a/src/components/medications/medication-detail-header.tsx b/src/components/medications/medication-detail-header.tsx new file mode 100644 index 000000000..7f1870e02 --- /dev/null +++ b/src/components/medications/medication-detail-header.tsx @@ -0,0 +1,113 @@ +"use client"; + +/** + * v1.5.5 D-3 §9.1 — medication-detail page header band. + * + * Renders the drug name as the H1, dose as a muted sub-line, and a + * status row carrying a Dracula-token dot + always-visible label + * (Aktiv / Pausiert / Beendet) and an edit pencil that opens the + * wizard with the `"name"` intent (Step 1 landing). The DOM order is + * `name → dose → status → edit` so the screen reader announces the + * drug first and the affordance last (C-E4-3). + * + * The status pill text is always rendered; the dot carries + * `aria-hidden="true"` and reads from Dracula tokens via + * `bg-[hsl(var(--success))]` / `bg-[hsl(var(--warning))]` / + * `bg-muted-foreground`. The "since DD.MM." drift the earlier draft + * mixed into the header is dropped — lifecycle dates live on the + * destructive zone, not on the header (E-2 M-1). + */ + +import { Pencil } from "lucide-react"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { useTranslations } from "@/lib/i18n/context"; + +export interface MedicationDetailHeaderProps { + name: string; + dose: string; + active: boolean; + endsOn?: string | null; + /** + * Fires when the user taps the Bearbeiten pencil. The detail page + * routes this to the wizard with `landingIntent: "name"` so the + * user lands on Step 1. + */ + onEdit: () => void; +} + +type Status = "active" | "paused" | "ended"; + +function resolveStatus(active: boolean, endsOn?: string | null): Status { + // `endsOn` set + in the past = the medication is archived. The + // server flips `active=false` in that case but a paused medication + // also reads false; distinguish here so the pill carries the right + // label. + if (endsOn) { + const end = new Date(endsOn); + if (!Number.isNaN(end.getTime()) && end.getTime() <= Date.now()) { + return "ended"; + } + } + return active ? "active" : "paused"; +} + +export function MedicationDetailHeader({ + name, + dose, + active, + endsOn, + onEdit, +}: MedicationDetailHeaderProps) { + const { t } = useTranslations(); + const status = resolveStatus(active, endsOn); + + const statusLabel = + status === "active" + ? t("medications.detail.status.active") + : status === "paused" + ? t("medications.detail.status.paused") + : t("medications.detail.status.ended"); + + const dotClass = + status === "active" + ? "bg-[hsl(var(--success))]" + : status === "paused" + ? "bg-[hsl(var(--warning))]" + : "bg-muted-foreground"; + + return ( +
+
+

{name}

+

{dose}

+
+ + +
+
+ +
+ ); +} diff --git a/src/components/medications/sections/api-tokens-row.tsx b/src/components/medications/sections/api-tokens-row.tsx new file mode 100644 index 000000000..57455f81f --- /dev/null +++ b/src/components/medications/sections/api-tokens-row.tsx @@ -0,0 +1,205 @@ +"use client"; + +/** + * v1.5.5 D-3 §9.7 — Settings → Externe Integration sub-row. + * + * Wraps the v1.5.4 `ApiEndpointDialog` body into a settings-row shape. + * The row mounts unconditionally so every medication can be reached + * over the external API. Caption above the URL reads + * `Endpunkt für „{name}"`; two buttons sit below — `[URL kopieren]` + * and `[Token erzeugen]`. When the user mints a token the value + * surfaces in a one-shot modal that auto-closes after copy (D-3 §7 + * Feature 6). + * + * Cache: invalidates `tokens()` + `medicationDependentKeys` on every + * mint / disable so the Tokens settings page reflects the new state + * without a manual refresh. + */ + +import { useEffect, useMemo, useState } from "react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { Copy, KeyRound } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { useTranslations } from "@/lib/i18n/context"; +import { + invalidateKeys, + medicationDependentKeys, + queryKeys, +} from "@/lib/query-keys"; + +export interface ApiTokensRowProps { + medicationId: string; + medicationName: string; +} + +interface ApiEndpointStatus { + enabled: boolean; + activeTokenCount: number; +} + +export function ApiTokensRow({ + medicationId, + medicationName, +}: ApiTokensRowProps) { + const { t } = useTranslations(); + const queryClient = useQueryClient(); + const [mintedToken, setMintedToken] = useState(null); + const [busy, setBusy] = useState(false); + + // Per-medication api-endpoint status reads under a dedicated key + // that rides the `["medications", id, …]` prefix so the bundle + // invalidation in `medicationDependentKeys` catches it. + const queryKey = useMemo( + () => ["medications", medicationId, "api-endpoint"] as const, + [medicationId], + ); + + const { data: status } = useQuery({ + queryKey, + queryFn: async () => { + const res = await fetch(`/api/medications/${medicationId}/api-endpoint`); + if (!res.ok) throw new Error("status_failed"); + const json = await res.json(); + return { + enabled: json.data.enabled === true, + activeTokenCount: json.data.activeTokenCount ?? 0, + }; + }, + staleTime: 0, + }); + + const enabled = status?.enabled ?? false; + const baseUrl = + typeof window !== "undefined" ? window.location.origin : "https://..."; + const endpoint = `${baseUrl}/api/ingest/medication`; + + async function copy(text: string, label: string) { + try { + await navigator.clipboard.writeText(text); + toast.success(label); + } catch { + toast.error(t("medications.detail.api.copyFailed")); + } + } + + async function mintToken() { + if (busy) return; + setBusy(true); + try { + const res = await fetch(`/api/medications/${medicationId}/api-endpoint`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ enabled: true }), + }); + const json = await res.json(); + if (!res.ok) { + toast.error(json.error ?? t("medications.detail.api.mintFailed")); + return; + } + if (json.data.token) { + setMintedToken(json.data.token); + // Auto-copy on mint so the user has it without an extra tap. + try { + await navigator.clipboard.writeText(json.data.token); + } catch { + // Clipboard rejection is silent — the value is still visible + // in the read-only one-shot panel below. + } + } + await invalidateKeys(queryClient, [ + ...medicationDependentKeys, + queryKeys.tokens(), + ]); + queryClient.setQueryData(queryKey, { + enabled: json.data.enabled === true, + activeTokenCount: + typeof json.data.activeTokenCount === "number" + ? json.data.activeTokenCount + : (status?.activeTokenCount ?? 0) + 1, + }); + } catch { + toast.error(t("medications.detail.api.mintFailed")); + } finally { + setBusy(false); + } + } + + // Auto-clear the minted token after 30 s so the value never lingers + // in the DOM longer than needed. + useEffect(() => { + if (!mintedToken) return; + const id = setTimeout(() => setMintedToken(null), 30_000); + return () => clearTimeout(id); + }, [mintedToken]); + + return ( +
+

+ {t("medications.detail.api.caption", { name: medicationName })} +

+ + POST {endpoint} + +
+ + +
+ + {mintedToken && ( +
+

+ {t("medications.detail.api.mintedHint")} +

+ + {mintedToken} + + +
+ )} +
+ ); +} diff --git a/src/components/medications/sections/destructive-zone-section.tsx b/src/components/medications/sections/destructive-zone-section.tsx new file mode 100644 index 000000000..984dd336f --- /dev/null +++ b/src/components/medications/sections/destructive-zone-section.tsx @@ -0,0 +1,413 @@ +"use client"; + +/** + * v1.5.5 D-3 §9.8 — Verwaltung & Gefahrenzone. + * + * One heading, two cards under it. Card A (`Verwaltung`) carries the + * reversible actions (Pausieren + Beenden); Card B (`Gefahrenzone`, + * `border-destructive/40`) carries the irreversible ones (Verlauf + * löschen + Medikament löschen). + * + * Mutation contracts per D-3 §3: + * + * - Tier 1 Pausieren: PUT `{ active: false | true }`. Server derives + * `pausedAt`. Switch wraps in `