diff --git a/messages/de.json b/messages/de.json index 206b9d8f2..f1cfb2c16 100644 --- a/messages/de.json +++ b/messages/de.json @@ -1917,7 +1917,19 @@ "skipGroupOther": "{count} Einträge übersprungen: {reason}", "reasonDuplicateInFile": "ein anderer Eintrag derselben Datei hat dasselbe Datum und dieselbe Uhrzeit", "reasonAlreadyRecorded": "für dieses Datum und diese Uhrzeit ist bereits eine Einnahme erfasst", - "reasonUnknown": "Grund nicht erfasst" + "reasonUnknown": "Grund nicht erfasst", + "reasonMedicationNotFound": "kein Medikament im Bestand passt zum Namen in der Datei", + "reasonMedicationAmbiguous": "der Name in der Datei passt auf mehr als ein Medikament", + "reasonMedicationIsMirrored": "das Medikament ist eine schreibgeschützte Kopie aus einer anderen App", + "reasonStatusNotRecorded": "die Datei hält zu dieser Dosis keine Entscheidung fest", + "reasonStatusUnknown": "der Status in der Datei gehört nicht zum dokumentierten Format", + "reasonMissingTimestamp": "die Zeile nennt keine Zeit", + "reasonMissingOffset": "die Zeit trägt keinen UTC-Offset, der Zeitpunkt ist damit unbekannt", + "reasonUnreadableTimestamp": "die Zeit entspricht nicht dem dokumentierten Format", + "reasonImplausibleTimestamp": "die Zeit nennt keinen echten Zeitpunkt oder liegt in der Zukunft", + "reasonMissingMedication": "die Zeile nennt kein Medikament", + "reasonUnreadableDosage": "die Dosis ist keine Zahl", + "reasonUnreadableRow": "die Zeile hat weniger Spalten als die Kopfzeile" } }, "charts": { @@ -4547,6 +4559,60 @@ "reasonUnstableExternalId": "Externe ID ist über Exporte hinweg nicht stabil", "reasonDuplicate": "bereits importiert", "reasonUnknown": "übersprungen" + }, + "doseHistory": { + "title": "Einnahmeverlauf", + "description": "Ein Einnahmeverlauf, wie ihn ein anderer Medikamenten-Tracker exportiert, direkt gelesen. Eine Datei deckt alle Medikamente darin ab.", + "pasteLabel": "Export einfügen", + "fileInputLabel": "Datei mit Einnahmeverlauf wählen", + "uploadFile": "Datei wählen", + "preview": "Vorschau", + "import": "Importieren", + "empty": "Erst den Export einfügen oder eine Datei wählen.", + "readFailed": "Diese Datei konnte nicht gelesen werden.", + "rateLimited": "Höchstens 5 Importe pro Stunde. Später erneut versuchen.", + "tooLarge": "Diese Datei überschreitet das Limit von 16 MB.", + "inProgress": "Ein Import des Einnahmeverlaufs läuft bereits. Auf sein Ende warten.", + "workerUnavailable": "Der Hintergrunddienst ist nicht verfügbar, es wurde nichts importiert.", + "failed": "Der Import ist fehlgeschlagen. Es wurde nichts geschrieben.", + "columnsHeading": "Was mit jeder Spalte passiert", + "jsonOnly": "nur JSON", + "previewLine": "Zeilen gelesen: {rows} · würde importieren: {queued} · abgewiesen: {refused}", + "submittedLine": "Zeilen gelesen: {rows} · zum Import eingereiht: {queued}", + "unmatched": "Kein Medikament im Bestand passt zu: {names}", + "ambiguous": "Passt auf mehr als ein Medikament: {names}", + "mirrored": "Aus einer anderen App gespiegelt, daher keine Dosis ergänzbar: {names}", + "unknownColumns": "Spalten ohne Regel im Importer, ungelesen: {names}", + "codingsNotRead": "Zeilen mit einem nicht gelesenen Codings-Wert: {count}", + "archivedRows": "Zeilen von einem in der Quell-App archivierten Medikament, trotzdem importiert: {count}", + "verdict": { + "honoured": "verwendet", + "reported": "gemeldet", + "ignored": "nicht gelesen" + }, + "column": { + "date": "Der Zeitpunkt der tatsächlichen Einnahme. Sein UTC-Offset wird genau so gelesen, wie er dasteht.", + "scheduledDate": "Der Slot, zu dem die Dosis gehörte, getrennt vom Zeitpunkt der Einnahme. Leer heißt: die Dosis gehörte zu keinem Slot.", + "medication": "Wird einem Medikament zugeordnet, das es im Bestand schon gibt. Aus der Datei wird nichts angelegt.", + "nickname": "Wird als zweiter Name versucht, wenn der erste zu nichts passt.", + "dosage": "Wird nur dann an der Dosis festgehalten, wenn sie von der geplanten Menge abweicht.", + "scheduledDosage": "Wird mit der tatsächlichen Menge verglichen, um eine Abweichung zu erkennen. Nie eigenständig gespeichert.", + "unit": "Wird mit der Menge geschrieben, geschrieben wie in der Datei. „count“ heißt Vielfache der konfigurierten Dosis.", + "status": "„Taken“ wird eine Dosis, „Skipped“ ein bewusstes Auslassen. Jeder andere Wert hält keine Entscheidung fest und die Zeile wird abgewiesen.", + "archived": "Wird gezählt und zurückgemeldet. Eine erfolgte Einnahme wird nicht dadurch ungültig, dass das Medikament später archiviert wurde.", + "codings": "Die CSV dokumentiert für diese Spalte keine Kodierung, also bleibt sie unberührt statt geraten. Gefüllte Zellen werden gezählt.", + "form": "Beschreibt das Medikament, nicht die Dosis.", + "start": "Das Datum des Behandlungsbeginns, nicht der Zeitpunkt dieser Dosis.", + "end": "Das Datum des Behandlungsendes, nicht der Zeitpunkt dieser Dosis." + }, + "fatal": { + "emptyFile": "Diese Datei enthält keine Zeilen.", + "missingColumns": "Dieser Datei fehlt eine Spalte, die der Importer braucht: Date, Medication oder Status.", + "unreadableJson": "Diese Datei ist kein gültiges JSON.", + "jsonNotAnArray": "Dieses JSON enthält keine Liste von Dosen.", + "jsonNoIntakeTime": "Der JSON-Export hält fest, wann eine Dosis fällig war, nicht wann sie genommen wurde, und eine geplante Zeit wird nicht als Einnahmezeit geschrieben. Stattdessen die CSV exportieren, die beides trägt.", + "tooManyRows": "Diese Datei enthält mehr Zeilen, als ein Import aufnehmen kann. Teilen und in Teilen importieren." + } } }, "subtitle": "Exportiere deine Daten oder importiere aus einer Sicherung." diff --git a/messages/en.json b/messages/en.json index 48bfe097b..d7da7aa75 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1917,7 +1917,19 @@ "skipGroupOther": "{count} entries skipped: {reason}", "reasonDuplicateInFile": "another entry in the same file has the same date and time", "reasonAlreadyRecorded": "an intake is already recorded for that date and time", - "reasonUnknown": "reason not recorded" + "reasonUnknown": "reason not recorded", + "reasonMedicationNotFound": "no medication on record matches the name in the file", + "reasonMedicationAmbiguous": "the name in the file matches more than one medication on record", + "reasonMedicationIsMirrored": "the medication is a read-only copy of another app's list", + "reasonStatusNotRecorded": "the file records no decision about that dose", + "reasonStatusUnknown": "the status in the file is not one the format defines", + "reasonMissingTimestamp": "the row states no time", + "reasonMissingOffset": "the time carries no UTC offset, so the instant is unknown", + "reasonUnreadableTimestamp": "the time is not in the format the export documents", + "reasonImplausibleTimestamp": "the time names no real instant, or lies in the future", + "reasonMissingMedication": "the row names no medication", + "reasonUnreadableDosage": "the dosage is not a number", + "reasonUnreadableRow": "the row has fewer columns than the header" } }, "charts": { @@ -4547,6 +4559,60 @@ "reasonUnstableExternalId": "external ID is not stable across exports", "reasonDuplicate": "already imported", "reasonUnknown": "skipped" + }, + "doseHistory": { + "title": "Medication dose history", + "description": "A dose history exported from another medication tracker, read as it comes. One file covers every medication in it.", + "pasteLabel": "Paste the export", + "fileInputLabel": "Choose a dose-history export file", + "uploadFile": "Choose file", + "preview": "Preview", + "import": "Import", + "empty": "Paste the export or choose a file first.", + "readFailed": "That file could not be read.", + "rateLimited": "Maximum 5 imports per hour. Try again later.", + "tooLarge": "That file is over the 16 MB limit.", + "inProgress": "A dose-history import is already running. Wait for it to finish.", + "workerUnavailable": "The background worker is not available, so nothing was imported.", + "failed": "The import failed. Nothing was written.", + "columnsHeading": "What each column does", + "jsonOnly": "JSON only", + "previewLine": "Rows read: {rows} · would import: {queued} · refused: {refused}", + "submittedLine": "Rows read: {rows} · queued for import: {queued}", + "unmatched": "No medication on record matches: {names}", + "ambiguous": "Matches more than one medication: {names}", + "mirrored": "Mirrored from another app, so no dose can be added: {names}", + "unknownColumns": "Columns the importer has no rule for, left unread: {names}", + "codingsNotRead": "Rows carrying a Codings value that was not read: {count}", + "archivedRows": "Rows from a medication archived in the source app, imported anyway: {count}", + "verdict": { + "honoured": "used", + "reported": "reported", + "ignored": "not read" + }, + "column": { + "date": "The time the dose was actually taken. Its UTC offset is read exactly as written.", + "scheduledDate": "The slot the dose belonged to, kept apart from the time it was taken. Empty means the dose belonged to no slot.", + "medication": "Matched against a medication already on your record. Nothing is created from the file.", + "nickname": "Tried as a second name when the first one matches nothing.", + "dosage": "Recorded on the dose only where it differs from the scheduled amount.", + "scheduledDosage": "Compared against the actual amount to decide whether that dose deviated. Never stored on its own.", + "unit": "Written with the amount, spelled as the file spells it. \"count\" means multiples of the configured dose.", + "status": "\"Taken\" becomes a dose and \"Skipped\" a deliberate skip. Any other value records no decision and the row is refused.", + "archived": "Counted and reported back. A dose that happened is not undone by archiving the medication afterwards.", + "codings": "The CSV gives this column no documented encoding, so it is left alone rather than guessed at. Populated cells are counted.", + "form": "Describes the medication, not the dose.", + "start": "The date you started the medication, not the time of this dose.", + "end": "The date you stopped the medication, not the time of this dose." + }, + "fatal": { + "emptyFile": "That file holds no rows.", + "missingColumns": "That file is missing a column the importer needs: Date, Medication or Status.", + "unreadableJson": "That file is not valid JSON.", + "jsonNotAnArray": "That JSON holds no list of doses.", + "jsonNoIntakeTime": "The JSON export records when a dose was due but not when it was taken, and a scheduled time will not be written as if it were the take. Export the CSV instead, which carries both.", + "tooManyRows": "That file holds more rows than one import can take. Split it and import the parts." + } } }, "subtitle": "Export your data or import from a backup." diff --git a/messages/es.json b/messages/es.json index fcad5dafa..f3e2c7dbc 100644 --- a/messages/es.json +++ b/messages/es.json @@ -1917,7 +1917,19 @@ "skipGroupOther": "{count} entradas omitidas: {reason}", "reasonDuplicateInFile": "otra entrada del mismo archivo tiene la misma fecha y hora", "reasonAlreadyRecorded": "ya hay una toma registrada para esa fecha y hora", - "reasonUnknown": "motivo no registrado" + "reasonUnknown": "motivo no registrado", + "reasonMedicationNotFound": "ningún medicamento registrado coincide con el nombre del archivo", + "reasonMedicationAmbiguous": "el nombre del archivo coincide con más de un medicamento", + "reasonMedicationIsMirrored": "el medicamento es una copia de solo lectura de otra aplicación", + "reasonStatusNotRecorded": "el archivo no registra ninguna decisión sobre esa dosis", + "reasonStatusUnknown": "el estado del archivo no es uno de los que define el formato", + "reasonMissingTimestamp": "la fila no indica ninguna hora", + "reasonMissingOffset": "la hora no lleva desplazamiento UTC, así que el instante es desconocido", + "reasonUnreadableTimestamp": "la hora no tiene el formato que documenta la exportación", + "reasonImplausibleTimestamp": "la hora no nombra un instante real, o está en el futuro", + "reasonMissingMedication": "la fila no nombra ningún medicamento", + "reasonUnreadableDosage": "la dosis no es un número", + "reasonUnreadableRow": "la fila tiene menos columnas que la cabecera" } }, "charts": { @@ -4547,6 +4559,60 @@ "reasonUnstableExternalId": "el ID externo no es estable entre exportaciones", "reasonDuplicate": "ya importada", "reasonUnknown": "omitida" + }, + "doseHistory": { + "title": "Historial de dosis", + "description": "Un historial de dosis exportado desde otra aplicación de seguimiento, leído tal cual. Un archivo abarca todos los medicamentos que contiene.", + "pasteLabel": "Pega la exportación", + "fileInputLabel": "Elegir un archivo de historial de dosis", + "uploadFile": "Elegir archivo", + "preview": "Previsualizar", + "import": "Importar", + "empty": "Pega la exportación o elige un archivo primero.", + "readFailed": "No se pudo leer ese archivo.", + "rateLimited": "Máximo 5 importaciones por hora. Inténtalo más tarde.", + "tooLarge": "Ese archivo supera el límite de 16 MB.", + "inProgress": "Ya hay una importación de historial en curso. Espera a que termine.", + "workerUnavailable": "El proceso en segundo plano no está disponible, así que no se importó nada.", + "failed": "La importación falló. No se escribió nada.", + "columnsHeading": "Qué hace cada columna", + "jsonOnly": "solo JSON", + "previewLine": "Filas leídas: {rows} · se importarían: {queued} · rechazadas: {refused}", + "submittedLine": "Filas leídas: {rows} · en cola para importar: {queued}", + "unmatched": "Ningún medicamento registrado coincide con: {names}", + "ambiguous": "Coincide con más de un medicamento: {names}", + "mirrored": "Copiado de otra aplicación, así que no se puede añadir ninguna dosis: {names}", + "unknownColumns": "Columnas sin regla en el importador, no leídas: {names}", + "codingsNotRead": "Filas con un valor de Codings no leído: {count}", + "archivedRows": "Filas de un medicamento archivado en la aplicación de origen, importadas igualmente: {count}", + "verdict": { + "honoured": "usada", + "reported": "informada", + "ignored": "no leída" + }, + "column": { + "date": "La hora en que se tomó realmente la dosis. Su desplazamiento UTC se lee tal como está escrito.", + "scheduledDate": "La franja a la que pertenecía la dosis, separada de la hora en que se tomó. Vacío significa que no pertenecía a ninguna franja.", + "medication": "Se empareja con un medicamento que ya está en tu registro. Nada se crea a partir del archivo.", + "nickname": "Se prueba como segundo nombre cuando el primero no coincide con nada.", + "dosage": "Se registra en la dosis solo cuando difiere de la cantidad prevista.", + "scheduledDosage": "Se compara con la cantidad real para decidir si esa dosis se desvió. Nunca se guarda por sí sola.", + "unit": "Se escribe con la cantidad, tal como la escribe el archivo. \"count\" significa múltiplos de la dosis configurada.", + "status": "\"Taken\" se convierte en una dosis y \"Skipped\" en una omisión deliberada. Cualquier otro valor no registra decisión alguna y la fila se rechaza.", + "archived": "Se cuenta y se informa. Una dosis que ocurrió no se deshace al archivar el medicamento después.", + "codings": "El CSV no documenta ninguna codificación para esta columna, así que se deja intacta en vez de adivinarla. Las celdas con contenido se cuentan.", + "form": "Describe el medicamento, no la dosis.", + "start": "La fecha en que empezaste el medicamento, no la hora de esta dosis.", + "end": "La fecha en que dejaste el medicamento, no la hora de esta dosis." + }, + "fatal": { + "emptyFile": "Ese archivo no contiene filas.", + "missingColumns": "A ese archivo le falta una columna que el importador necesita: Date, Medication o Status.", + "unreadableJson": "Ese archivo no es JSON válido.", + "jsonNotAnArray": "Ese JSON no contiene ninguna lista de dosis.", + "jsonNoIntakeTime": "La exportación JSON registra cuándo tocaba una dosis pero no cuándo se tomó, y una hora prevista no se escribirá como si fuera la toma. Exporta el CSV, que lleva ambas.", + "tooManyRows": "Ese archivo tiene más filas de las que admite una importación. Divídelo e importa las partes." + } } }, "subtitle": "Exporta tus datos o importa desde una copia de seguridad." diff --git a/messages/fr.json b/messages/fr.json index 5d2e86ce7..57333483f 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -1917,7 +1917,19 @@ "skipGroupOther": "{count} entrées ignorées : {reason}", "reasonDuplicateInFile": "une autre entrée du même fichier porte les mêmes date et heure", "reasonAlreadyRecorded": "une prise est déjà enregistrée pour cette date et cette heure", - "reasonUnknown": "motif non enregistré" + "reasonUnknown": "motif non enregistré", + "reasonMedicationNotFound": "aucun médicament enregistré ne correspond au nom du fichier", + "reasonMedicationAmbiguous": "le nom du fichier correspond à plusieurs médicaments", + "reasonMedicationIsMirrored": "le médicament est une copie en lecture seule provenant d'une autre application", + "reasonStatusNotRecorded": "le fichier ne consigne aucune décision sur cette dose", + "reasonStatusUnknown": "le statut du fichier ne fait pas partie de ceux que définit le format", + "reasonMissingTimestamp": "la ligne n'indique aucune heure", + "reasonMissingOffset": "l'heure ne porte pas de décalage UTC, l'instant est donc inconnu", + "reasonUnreadableTimestamp": "l'heure n'est pas au format documenté par l'export", + "reasonImplausibleTimestamp": "l'heure ne désigne aucun instant réel, ou se situe dans le futur", + "reasonMissingMedication": "la ligne ne nomme aucun médicament", + "reasonUnreadableDosage": "la dose n'est pas un nombre", + "reasonUnreadableRow": "la ligne a moins de colonnes que l'en-tête" } }, "charts": { @@ -4547,6 +4559,60 @@ "reasonUnstableExternalId": "l'identifiant externe n'est pas stable d'un export à l'autre", "reasonDuplicate": "déjà importée", "reasonUnknown": "ignorée" + }, + "doseHistory": { + "title": "Historique des prises", + "description": "Un historique de prises exporté depuis une autre application de suivi, lu tel quel. Un fichier couvre tous les médicaments qu'il contient.", + "pasteLabel": "Coller l'export", + "fileInputLabel": "Choisir un fichier d'historique des prises", + "uploadFile": "Choisir un fichier", + "preview": "Aperçu", + "import": "Importer", + "empty": "Collez d'abord l'export ou choisissez un fichier.", + "readFailed": "Ce fichier n'a pas pu être lu.", + "rateLimited": "Maximum 5 imports par heure. Réessayez plus tard.", + "tooLarge": "Ce fichier dépasse la limite de 16 Mo.", + "inProgress": "Un import d'historique est déjà en cours. Attendez qu'il se termine.", + "workerUnavailable": "Le service d'arrière-plan est indisponible, rien n'a été importé.", + "failed": "L'import a échoué. Rien n'a été écrit.", + "columnsHeading": "Ce que devient chaque colonne", + "jsonOnly": "JSON uniquement", + "previewLine": "Lignes lues : {rows} · seraient importées : {queued} · refusées : {refused}", + "submittedLine": "Lignes lues : {rows} · en file d'import : {queued}", + "unmatched": "Aucun médicament enregistré ne correspond à : {names}", + "ambiguous": "Correspond à plusieurs médicaments : {names}", + "mirrored": "Copié depuis une autre application, aucune prise ne peut y être ajoutée : {names}", + "unknownColumns": "Colonnes sans règle dans l'importeur, non lues : {names}", + "codingsNotRead": "Lignes portant une valeur Codings non lue : {count}", + "archivedRows": "Lignes d'un médicament archivé dans l'application source, importées quand même : {count}", + "verdict": { + "honoured": "utilisée", + "reported": "signalée", + "ignored": "non lue" + }, + "column": { + "date": "L'heure à laquelle la dose a réellement été prise. Son décalage UTC est lu exactement tel qu'il est écrit.", + "scheduledDate": "Le créneau auquel la dose appartenait, distinct de l'heure de prise. Vide signifie qu'elle n'appartenait à aucun créneau.", + "medication": "Rapproché d'un médicament déjà présent dans votre dossier. Rien n'est créé à partir du fichier.", + "nickname": "Essayé comme second nom lorsque le premier ne correspond à rien.", + "dosage": "Consigné sur la dose uniquement lorsqu'il diffère de la quantité prévue.", + "scheduledDosage": "Comparé à la quantité réelle pour décider s'il y a eu écart. Jamais conservé seul.", + "unit": "Écrit avec la quantité, orthographié comme dans le fichier. « count » signifie des multiples de la dose configurée.", + "status": "« Taken » devient une prise et « Skipped » une omission délibérée. Toute autre valeur ne consigne aucune décision et la ligne est refusée.", + "archived": "Compté et signalé. Une prise qui a eu lieu n'est pas annulée parce que le médicament a été archivé ensuite.", + "codings": "Le CSV ne documente aucun encodage pour cette colonne, elle est donc laissée telle quelle plutôt que devinée. Les cellules remplies sont comptées.", + "form": "Décrit le médicament, pas la prise.", + "start": "La date de début du traitement, pas l'heure de cette prise.", + "end": "La date d'arrêt du traitement, pas l'heure de cette prise." + }, + "fatal": { + "emptyFile": "Ce fichier ne contient aucune ligne.", + "missingColumns": "Il manque à ce fichier une colonne dont l'importeur a besoin : Date, Medication ou Status.", + "unreadableJson": "Ce fichier n'est pas du JSON valide.", + "jsonNotAnArray": "Ce JSON ne contient aucune liste de prises.", + "jsonNoIntakeTime": "L'export JSON consigne le moment où une dose était due, pas celui où elle a été prise, et une heure prévue ne sera pas écrite comme si c'était la prise. Exportez le CSV, qui porte les deux.", + "tooManyRows": "Ce fichier contient plus de lignes qu'un import peut en prendre. Découpez-le et importez les parties." + } } }, "subtitle": "Exportez vos données ou importez depuis une sauvegarde." diff --git a/messages/it.json b/messages/it.json index d838baa38..16e645775 100644 --- a/messages/it.json +++ b/messages/it.json @@ -1917,7 +1917,19 @@ "skipGroupOther": "{count} voci ignorate: {reason}", "reasonDuplicateInFile": "un’altra voce dello stesso file ha la stessa data e la stessa ora", "reasonAlreadyRecorded": "per quella data e ora è già registrata un’assunzione", - "reasonUnknown": "motivo non registrato" + "reasonUnknown": "motivo non registrato", + "reasonMedicationNotFound": "nessun farmaco registrato corrisponde al nome nel file", + "reasonMedicationAmbiguous": "il nome nel file corrisponde a più di un farmaco", + "reasonMedicationIsMirrored": "il farmaco è una copia in sola lettura da un'altra app", + "reasonStatusNotRecorded": "il file non registra alcuna decisione su quella dose", + "reasonStatusUnknown": "lo stato nel file non è uno di quelli definiti dal formato", + "reasonMissingTimestamp": "la riga non indica alcun orario", + "reasonMissingOffset": "l'orario non porta un offset UTC, quindi l'istante è ignoto", + "reasonUnreadableTimestamp": "l'orario non è nel formato documentato dall'esportazione", + "reasonImplausibleTimestamp": "l'orario non indica un istante reale, o è nel futuro", + "reasonMissingMedication": "la riga non nomina alcun farmaco", + "reasonUnreadableDosage": "il dosaggio non è un numero", + "reasonUnreadableRow": "la riga ha meno colonne dell'intestazione" } }, "charts": { @@ -4547,6 +4559,60 @@ "reasonUnstableExternalId": "l'ID esterno non è stabile tra un export e l'altro", "reasonDuplicate": "già importata", "reasonUnknown": "saltata" + }, + "doseHistory": { + "title": "Storico delle assunzioni", + "description": "Uno storico di assunzioni esportato da un'altra app di monitoraggio, letto così com'è. Un file copre tutti i farmaci che contiene.", + "pasteLabel": "Incolla l'esportazione", + "fileInputLabel": "Scegli un file con lo storico delle assunzioni", + "uploadFile": "Scegli file", + "preview": "Anteprima", + "import": "Importa", + "empty": "Prima incolla l'esportazione o scegli un file.", + "readFailed": "Non è stato possibile leggere quel file.", + "rateLimited": "Massimo 5 importazioni all'ora. Riprova più tardi.", + "tooLarge": "Quel file supera il limite di 16 MB.", + "inProgress": "Un'importazione dello storico è già in corso. Attendi che finisca.", + "workerUnavailable": "Il processo in background non è disponibile, quindi non è stato importato nulla.", + "failed": "L'importazione è fallita. Non è stato scritto nulla.", + "columnsHeading": "Cosa fa ogni colonna", + "jsonOnly": "solo JSON", + "previewLine": "Righe lette: {rows} · da importare: {queued} · rifiutate: {refused}", + "submittedLine": "Righe lette: {rows} · in coda per l'importazione: {queued}", + "unmatched": "Nessun farmaco registrato corrisponde a: {names}", + "ambiguous": "Corrisponde a più di un farmaco: {names}", + "mirrored": "Copiato da un'altra app, quindi non si può aggiungere alcuna dose: {names}", + "unknownColumns": "Colonne senza regola nell'importatore, non lette: {names}", + "codingsNotRead": "Righe con un valore Codings non letto: {count}", + "archivedRows": "Righe di un farmaco archiviato nell'app di origine, importate comunque: {count}", + "verdict": { + "honoured": "usata", + "reported": "segnalata", + "ignored": "non letta" + }, + "column": { + "date": "L'ora in cui la dose è stata effettivamente assunta. Il suo offset UTC viene letto esattamente come è scritto.", + "scheduledDate": "La fascia a cui la dose apparteneva, tenuta distinta dall'ora di assunzione. Vuoto significa che non apparteneva a nessuna fascia.", + "medication": "Abbinato a un farmaco già presente nella tua scheda. Dal file non viene creato nulla.", + "nickname": "Provato come secondo nome quando il primo non corrisponde a niente.", + "dosage": "Registrato sulla dose solo quando differisce dalla quantità prevista.", + "scheduledDosage": "Confrontato con la quantità effettiva per stabilire se quella dose si è discostata. Mai salvato da solo.", + "unit": "Scritto con la quantità, come lo scrive il file. \"count\" indica multipli della dose configurata.", + "status": "\"Taken\" diventa una dose e \"Skipped\" un'omissione deliberata. Qualsiasi altro valore non registra alcuna decisione e la riga viene rifiutata.", + "archived": "Contato e segnalato. Una dose che è avvenuta non si annulla archiviando il farmaco dopo.", + "codings": "Il CSV non documenta alcuna codifica per questa colonna, quindi viene lasciata stare invece di essere indovinata. Le celle piene vengono contate.", + "form": "Descrive il farmaco, non la dose.", + "start": "La data in cui hai iniziato il farmaco, non l'ora di questa dose.", + "end": "La data in cui hai smesso il farmaco, non l'ora di questa dose." + }, + "fatal": { + "emptyFile": "Quel file non contiene righe.", + "missingColumns": "A quel file manca una colonna che serve all'importatore: Date, Medication o Status.", + "unreadableJson": "Quel file non è JSON valido.", + "jsonNotAnArray": "Quel JSON non contiene alcun elenco di dosi.", + "jsonNoIntakeTime": "L'esportazione JSON registra quando una dose era prevista, non quando è stata assunta, e un orario previsto non verrà scritto come se fosse l'assunzione. Esporta il CSV, che porta entrambi.", + "tooManyRows": "Quel file contiene più righe di quante un'importazione possa accoglierne. Dividilo e importa le parti." + } } }, "subtitle": "Esporta i tuoi dati o importa da un backup." diff --git a/messages/pl.json b/messages/pl.json index 2fd455604..4a795f816 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -1917,7 +1917,19 @@ "skipGroupOther": "Pominięto {count} wpisów: {reason}", "reasonDuplicateInFile": "inny wpis w tym samym pliku ma tę samą datę i godzinę", "reasonAlreadyRecorded": "przyjęcie o tej dacie i godzinie jest już zapisane", - "reasonUnknown": "powód nieodnotowany" + "reasonUnknown": "powód nieodnotowany", + "reasonMedicationNotFound": "żaden zapisany lek nie odpowiada nazwie z pliku", + "reasonMedicationAmbiguous": "nazwa z pliku odpowiada więcej niż jednemu lekowi", + "reasonMedicationIsMirrored": "lek jest kopią tylko do odczytu z innej aplikacji", + "reasonStatusNotRecorded": "plik nie zapisuje żadnej decyzji o tej dawce", + "reasonStatusUnknown": "status w pliku nie należy do tych, które definiuje format", + "reasonMissingTimestamp": "wiersz nie podaje godziny", + "reasonMissingOffset": "godzina nie ma przesunięcia UTC, więc moment jest nieznany", + "reasonUnreadableTimestamp": "godzina nie ma formatu opisanego w dokumentacji eksportu", + "reasonImplausibleTimestamp": "godzina nie wskazuje rzeczywistego momentu albo leży w przyszłości", + "reasonMissingMedication": "wiersz nie nazywa żadnego leku", + "reasonUnreadableDosage": "dawka nie jest liczbą", + "reasonUnreadableRow": "wiersz ma mniej kolumn niż nagłówek" } }, "charts": { @@ -4547,6 +4559,60 @@ "reasonUnstableExternalId": "identyfikator zewnętrzny nie jest stabilny między eksportami", "reasonDuplicate": "już zaimportowano", "reasonUnknown": "pominięto" + }, + "doseHistory": { + "title": "Historia dawek", + "description": "Historia dawek wyeksportowana z innej aplikacji, czytana wprost. Jeden plik obejmuje wszystkie leki, które zawiera.", + "pasteLabel": "Wklej eksport", + "fileInputLabel": "Wybierz plik z historią dawek", + "uploadFile": "Wybierz plik", + "preview": "Podgląd", + "import": "Importuj", + "empty": "Najpierw wklej eksport albo wybierz plik.", + "readFailed": "Nie udało się odczytać tego pliku.", + "rateLimited": "Maksymalnie 5 importów na godzinę. Spróbuj później.", + "tooLarge": "Ten plik przekracza limit 16 MB.", + "inProgress": "Import historii dawek już trwa. Poczekaj, aż się zakończy.", + "workerUnavailable": "Proces w tle jest niedostępny, więc nic nie zostało zaimportowane.", + "failed": "Import nie udał się. Nic nie zostało zapisane.", + "columnsHeading": "Co się dzieje z każdą kolumną", + "jsonOnly": "tylko JSON", + "previewLine": "Odczytane wiersze: {rows} · do zaimportowania: {queued} · odrzucone: {refused}", + "submittedLine": "Odczytane wiersze: {rows} · w kolejce do importu: {queued}", + "unmatched": "Żaden zapisany lek nie odpowiada: {names}", + "ambiguous": "Odpowiada więcej niż jednemu lekowi: {names}", + "mirrored": "Skopiowany z innej aplikacji, więc nie można dodać dawki: {names}", + "unknownColumns": "Kolumny bez reguły w importerze, nieodczytane: {names}", + "codingsNotRead": "Wiersze z nieodczytaną wartością Codings: {count}", + "archivedRows": "Wiersze leku zarchiwizowanego w aplikacji źródłowej, zaimportowane mimo to: {count}", + "verdict": { + "honoured": "użyta", + "reported": "zgłoszona", + "ignored": "nieodczytana" + }, + "column": { + "date": "Godzina rzeczywistego przyjęcia dawki. Jej przesunięcie UTC czytane jest dokładnie tak, jak zapisane.", + "scheduledDate": "Okno, do którego należała dawka, trzymane osobno od godziny przyjęcia. Puste znaczy, że dawka nie należała do żadnego okna.", + "medication": "Dopasowane do leku, który już jest w Twojej kartotece. Z pliku nic nie jest tworzone.", + "nickname": "Próbowane jako druga nazwa, gdy pierwsza do niczego nie pasuje.", + "dosage": "Zapisywane przy dawce tylko wtedy, gdy różni się od zaplanowanej ilości.", + "scheduledDosage": "Porównywane z rzeczywistą ilością, by rozstrzygnąć, czy dawka odbiegła od planu. Nigdy nie zapisywane samo.", + "unit": "Zapisywana razem z ilością, dokładnie tak, jak w pliku. „count” oznacza wielokrotności skonfigurowanej dawki.", + "status": "„Taken” staje się dawką, a „Skipped” świadomym pominięciem. Każda inna wartość nie zapisuje żadnej decyzji i wiersz zostaje odrzucony.", + "archived": "Liczone i zgłaszane. Dawka, która się odbyła, nie znika przez późniejsze zarchiwizowanie leku.", + "codings": "CSV nie dokumentuje dla tej kolumny żadnego kodowania, więc zostaje nietknięta zamiast zgadywana. Wypełnione komórki są liczone.", + "form": "Opisuje lek, nie dawkę.", + "start": "Data rozpoczęcia leku, nie godzina tej dawki.", + "end": "Data zakończenia leku, nie godzina tej dawki." + }, + "fatal": { + "emptyFile": "Ten plik nie zawiera wierszy.", + "missingColumns": "W tym pliku brakuje kolumny, której potrzebuje importer: Date, Medication albo Status.", + "unreadableJson": "Ten plik nie jest poprawnym JSON-em.", + "jsonNotAnArray": "Ten JSON nie zawiera listy dawek.", + "jsonNoIntakeTime": "Eksport JSON zapisuje, kiedy dawka była zaplanowana, a nie kiedy została przyjęta, i zaplanowana godzina nie zostanie zapisana jako godzina przyjęcia. Wyeksportuj CSV, który zawiera obie.", + "tooManyRows": "Ten plik ma więcej wierszy, niż może przyjąć jeden import. Podziel go i zaimportuj części." + } } }, "subtitle": "Wyeksportuj dane lub zaimportuj z kopii zapasowej." diff --git a/src/app/api/__tests__/module-route-gate-inventory.test.ts b/src/app/api/__tests__/module-route-gate-inventory.test.ts index 87a44dfed..b60133b32 100644 --- a/src/app/api/__tests__/module-route-gate-inventory.test.ts +++ b/src/app/api/__tests__/module-route-gate-inventory.test.ts @@ -212,6 +212,10 @@ const EXEMPT_ROUTES: ReadonlyArray = [ "src/app/api/medications/compliance/route.ts", "src/app/api/medications/intake/route.ts", "src/app/api/medications/intake/bulk/route.ts", + // The account-wide dose-history import is the same class as the + // per-medication importer below: it writes intake rows and reads its own job. + "src/app/api/medications/intake/dose-history-import/route.ts", + "src/app/api/medications/intake/dose-history-import/[jobId]/status/route.ts", // NB: `medications/extract` is NOT exempt — it gates on // `requireAssistantSurface("coach")` (the NL-extraction is an assistant // surface), so the inventory already counts it as a delegated gate. diff --git a/src/app/api/medications/[id]/intake/import/__tests__/route.test.ts b/src/app/api/medications/[id]/intake/import/__tests__/route.test.ts index a11c60a78..7a7d1bf03 100644 --- a/src/app/api/medications/[id]/intake/import/__tests__/route.test.ts +++ b/src/app/api/medications/[id]/intake/import/__tests__/route.test.ts @@ -239,14 +239,20 @@ describe("POST /api/medications/[id]/intake/import — durable kickoff", () => { // A legacy `zaehler` on the first row rides along and decides nothing: // both keys come from the instant, so two different times can never // collapse onto one key again. + // This body carries one time per row and nothing to say which slot it + // belonged to, so `scheduledFor` mirrors the take: every dose it + // describes is ad-hoc, anchored on itself. The export-reading route is + // the one with two facts to write. payload: { entries: [ { idempotencyKey: `import-m1-${Date.parse("2026-01-01T07:00:00")}`, + scheduledFor: expect.any(String), takenAt: expect.any(String), }, { idempotencyKey: `import-m1-${Date.parse("2026-01-01T19:00:00")}`, + scheduledFor: expect.any(String), takenAt: expect.any(String), }, ], diff --git a/src/components/medications/intake-import-result.tsx b/src/components/medications/intake-import-result.tsx index 69bbf5754..c87c600ba 100644 --- a/src/components/medications/intake-import-result.tsx +++ b/src/components/medications/intake-import-result.tsx @@ -55,18 +55,64 @@ function useSkipReasonLabel(): (reason: string) => string { const labels: Record = { duplicate_in_file: t("medications.import.reasonDuplicateInFile"), already_recorded: t("medications.import.reasonAlreadyRecorded"), + medication_not_found: t("medications.import.reasonMedicationNotFound"), + medication_ambiguous: t("medications.import.reasonMedicationAmbiguous"), + medication_is_mirrored: t("medications.import.reasonMedicationIsMirrored"), + status_not_recorded: t("medications.import.reasonStatusNotRecorded"), + status_unknown: t("medications.import.reasonStatusUnknown"), + missing_timestamp: t("medications.import.reasonMissingTimestamp"), + missing_timezone_offset: t("medications.import.reasonMissingOffset"), + unreadable_timestamp: t("medications.import.reasonUnreadableTimestamp"), + implausible_timestamp: t("medications.import.reasonImplausibleTimestamp"), + missing_medication: t("medications.import.reasonMissingMedication"), + unreadable_dosage: t("medications.import.reasonUnreadableDosage"), + unreadable_row: t("medications.import.reasonUnreadableRow"), }; const fallback = t("medications.import.reasonUnknown"); return (reason: string) => labels[reason] ?? fallback; } +/** + * The grouped skip lines on their own, without the outcome sentence above them. + * + * Split out because a dry-run preview needs exactly these lines and must NOT + * borrow the outcome sentence: a preview has written nothing, so classifying it + * by the counts it WOULD produce would put a success tick on a run that never + * happened. The reason vocabulary is shared; the verdict is not. + */ +export function IntakeImportSkipGroups({ + groups, + testId = "intake-import-skip-groups", +}: { + groups: readonly IntakeImportSkipGroup[]; + testId?: string; +}) { + const { tCount } = useTranslations(); + const labelFor = useSkipReasonLabel(); + if (groups.length === 0) return null; + return ( + + ); +} + export function IntakeImportResultView({ result, }: { result: IntakeImportResultState; }) { - const { t, tCount } = useTranslations(); - const labelFor = useSkipReasonLabel(); + const { t } = useTranslations(); if (result.kind === "notice") { return ( @@ -106,21 +152,7 @@ export function IntakeImportResultView({ message={message} testId="intake-import-outcome" /> - {result.skipReasons.length > 0 && ( - - )} + ); } diff --git a/src/components/settings/import-panel/__tests__/import-result-guard.test.ts b/src/components/settings/import-panel/__tests__/import-result-guard.test.ts index defcda458..c090e1694 100644 --- a/src/components/settings/import-panel/__tests__/import-result-guard.test.ts +++ b/src/components/settings/import-panel/__tests__/import-result-guard.test.ts @@ -48,6 +48,10 @@ const IMPORT_SURFACES: ReadonlyArray<{ path: string; through: RegExp }> = [ path: "src/components/medications/intake-import-dialog.tsx", through: /IntakeImportResultView/, }, + { + path: "src/components/settings/import-panel/dose-history-import-card.tsx", + through: /IntakeImportResultView/, + }, ]; /** The success affordance: the tick component and the success colour token. */ diff --git a/src/components/settings/import-panel/dose-history-columns.tsx b/src/components/settings/import-panel/dose-history-columns.tsx new file mode 100644 index 000000000..b53cccf93 --- /dev/null +++ b/src/components/settings/import-panel/dose-history-columns.tsx @@ -0,0 +1,94 @@ +"use client"; + +/** + * What the dose-history importer does with every field of the export. + * + * Visible before a file is chosen, not discovered afterwards. A column read and + * silently dropped is indistinguishable, from the outside, from a column read and + * used — which is how a person ends up believing an importer honoured something + * it never looked at. + * + * The rulings come from `AUTO_EXPORT_COLUMN_RULINGS`, so the parser and this list + * cannot drift: a structural test asserts every column the format defines has + * exactly one ruling, and the sentence for each ruling is a literal `t()` call + * below so the i18n call-site guard sees all of them. + */ +import { AUTO_EXPORT_COLUMN_RULINGS } from "@/lib/medications/import/auto-export-format"; +import { useTranslations } from "@/lib/i18n/context"; + +/** One literal `t()` per note key. A computed key would be invisible to the guard. */ +function useColumnNote(): (noteKey: string) => string { + const { t } = useTranslations(); + const notes: Record = { + date: t("settings.sections.export.import.doseHistory.column.date"), + scheduledDate: t( + "settings.sections.export.import.doseHistory.column.scheduledDate", + ), + medication: t( + "settings.sections.export.import.doseHistory.column.medication", + ), + nickname: t("settings.sections.export.import.doseHistory.column.nickname"), + dosage: t("settings.sections.export.import.doseHistory.column.dosage"), + scheduledDosage: t( + "settings.sections.export.import.doseHistory.column.scheduledDosage", + ), + unit: t("settings.sections.export.import.doseHistory.column.unit"), + status: t("settings.sections.export.import.doseHistory.column.status"), + archived: t("settings.sections.export.import.doseHistory.column.archived"), + codings: t("settings.sections.export.import.doseHistory.column.codings"), + form: t("settings.sections.export.import.doseHistory.column.form"), + start: t("settings.sections.export.import.doseHistory.column.start"), + end: t("settings.sections.export.import.doseHistory.column.end"), + }; + return (noteKey: string) => notes[noteKey] ?? ""; +} + +function useVerdictLabel(): (verdict: string) => string { + const { t } = useTranslations(); + const labels: Record = { + honoured: t("settings.sections.export.import.doseHistory.verdict.honoured"), + reported: t("settings.sections.export.import.doseHistory.verdict.reported"), + ignored: t("settings.sections.export.import.doseHistory.verdict.ignored"), + }; + return (verdict: string) => labels[verdict] ?? verdict; +} + +export function DoseHistoryColumnRulings() { + const { t } = useTranslations(); + const noteFor = useColumnNote(); + const verdictFor = useVerdictLabel(); + + return ( +
+ + {t("settings.sections.export.import.doseHistory.columnsHeading")} + +
+ {AUTO_EXPORT_COLUMN_RULINGS.map((ruling) => ( +
+
+ {ruling.column} + + {verdictFor(ruling.verdict)} + + {ruling.jsonOnly && ( + + {t("settings.sections.export.import.doseHistory.jsonOnly")} + + )} +
+
+ {noteFor(ruling.noteKey)} +
+
+ ))} +
+
+ ); +} diff --git a/src/components/settings/import-panel/dose-history-import-card.tsx b/src/components/settings/import-panel/dose-history-import-card.tsx new file mode 100644 index 000000000..e4690ce92 --- /dev/null +++ b/src/components/settings/import-panel/dose-history-import-card.tsx @@ -0,0 +1,398 @@ +"use client"; + +/** + * Import a dose history exported from another medication tracker. + * + * Account-wide, because such a file covers a whole regimen — the per-medication + * import dialog on a medication's own page stays what it is, a way to add dates + * and times for that one medication. + * + * Preview first. The dry run returns the entire verdict — how many rows land, how + * many are refused and under which reason, which medication names match nothing + * on the record — before a single row is written. Importing years of history + * unseen is not something to ask of anybody. + * + * The outcome renders through `IntakeImportResultView`, the same component the + * per-medication dialog uses, so a run that wrote nothing cannot carry a success + * tick here either and the skip reasons are grouped with a count rather than + * repeated per row. + */ +import { useId, useRef, useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { AlertCircle, Loader2, PillBottle, Upload } from "lucide-react"; + +import { + IntakeImportResultView, + IntakeImportSkipGroups, + type IntakeImportResultState, +} from "@/components/medications/intake-import-result"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { apiFetchRaw } from "@/lib/api/api-fetch"; +import { useTranslations } from "@/lib/i18n/context"; +import { + MedicationIntakeImportError, + waitForMedicationIntakeImport, +} from "@/lib/medications/intake-import-poll"; +import { invalidateMedicationReads } from "@/lib/query-keys"; + +import { DoseHistoryColumnRulings } from "./dose-history-columns"; +import { ImportCardShell } from "./import-card-shell"; +import { MAX_PASTE_CHARS } from "./constants"; + +/** The file-level verdict, exactly as the route returns it. */ +interface DoseHistoryFileVerdict { + rowsRead: number; + queued: number; + refused: number; + refusedByReason: Array<{ reason: string; count: number }>; + unmatchedMedications: string[]; + ambiguousMedications: string[]; + mirroredMedications: string[]; + unknownColumns: string[]; + codingsNotRead: number; + fromArchivedMedications: number; +} + +interface DoseHistoryKickoff { + dryRun: boolean; + jobId: string | null; + statusUrl: string | null; + file: DoseHistoryFileVerdict; +} + +/** + * Plain language per machine-readable file-level refusal. Literal `t()` calls so + * the i18n call-site guard sees every one. + */ +function useFatalMessage(): (code: string | undefined) => string { + const { t } = useTranslations(); + const messages: Record = { + empty_file: t( + "settings.sections.export.import.doseHistory.fatal.emptyFile", + ), + missing_required_columns: t( + "settings.sections.export.import.doseHistory.fatal.missingColumns", + ), + unreadable_json: t( + "settings.sections.export.import.doseHistory.fatal.unreadableJson", + ), + json_not_an_array: t( + "settings.sections.export.import.doseHistory.fatal.jsonNotAnArray", + ), + json_carries_no_intake_time: t( + "settings.sections.export.import.doseHistory.fatal.jsonNoIntakeTime", + ), + too_many_rows: t( + "settings.sections.export.import.doseHistory.fatal.tooManyRows", + ), + }; + const fallback = t("settings.sections.export.import.doseHistory.failed"); + return (code: string | undefined) => + (code === undefined ? undefined : messages[code]) ?? fallback; +} + +/** `true` when the pasted or loaded text looks like the JSON shape. */ +function looksLikeJson(text: string): boolean { + const head = text.trimStart().charAt(0); + return head === "[" || head === "{"; +} + +export function DoseHistoryImportCard() { + const { t } = useTranslations(); + const fatalMessageFor = useFatalMessage(); + const fileInputRef = useRef(null); + const textareaId = useId(); + const queryClient = useQueryClient(); + + const [text, setText] = useState(""); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const [verdict, setVerdict] = useState(null); + const [previewOnly, setPreviewOnly] = useState(false); + const [outcome, setOutcome] = useState(null); + + async function onFileChange(event: React.ChangeEvent) { + const file = event.target.files?.[0]; + event.target.value = ""; + if (!file) return; + setError(null); + setVerdict(null); + setOutcome(null); + try { + setText(await file.text()); + } catch { + setError(t("settings.sections.export.import.doseHistory.readFailed")); + } + } + + async function send(dryRun: boolean) { + setError(null); + setVerdict(null); + setOutcome(null); + if (text.trim().length === 0) { + setError(t("settings.sections.export.import.doseHistory.empty")); + return; + } + setBusy(true); + try { + const res = await apiFetchRaw( + `/api/medications/intake/dose-history-import${dryRun ? "?dryRun=1" : ""}`, + { + method: "POST", + headers: { + "Content-Type": looksLikeJson(text) + ? "application/json" + : "text/csv", + }, + credentials: "include", + body: text, + }, + ); + if (res.status === 429) { + setError(t("settings.sections.export.import.doseHistory.rateLimited")); + return; + } + if (res.status === 413) { + setError(t("settings.sections.export.import.doseHistory.tooLarge")); + return; + } + if (res.status === 409) { + setError(t("settings.sections.export.import.doseHistory.inProgress")); + return; + } + if (res.status === 503) { + setError( + t("settings.sections.export.import.doseHistory.workerUnavailable"), + ); + return; + } + if (res.status === 422 || res.status === 415) { + const body = await res.json().catch(() => null); + const code = + typeof body?.meta?.errorCode === "string" + ? body.meta.errorCode + : undefined; + setError(fatalMessageFor(code)); + return; + } + if (!res.ok) { + setError(t("settings.sections.export.import.doseHistory.failed")); + return; + } + + const data = (await res.json()).data as DoseHistoryKickoff; + setVerdict(data.file); + setPreviewOnly(data.dryRun); + if (data.dryRun || !data.statusUrl) return; + + const result = await waitForMedicationIntakeImport(data.statusUrl); + setOutcome({ + kind: "outcome", + imported: result.imported, + skipped: result.skipped, + skipReasons: result.skipReasons, + }); + void invalidateMedicationReads(queryClient); + } catch (err) { + // A failed job carries a sanitised reason; anything else is a transport + // failure with nothing to add. Either way the run did not write, and the + // card says so rather than leaving the last verdict on screen. + setError( + err instanceof MedicationIntakeImportError && err.message.length > 0 + ? err.message + : t("settings.sections.export.import.doseHistory.failed"), + ); + setVerdict(null); + } finally { + setBusy(false); + } + } + + return ( + +
+ +