From a37883e3f8d17e30d02bb5df876836cd263133fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 28 Jun 2026 16:03:25 +0200 Subject: [PATCH] feat(records): surface allergies + family history in Settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new "Medical history" (Anamnese) Settings section that hosts the two structured-record managers — allergies and family history — each a list + add/edit sheet + delete-with-confirm built on the shared query-key factory and the ResponsiveSheet pattern. It sits between the illness journal and the preventive-care reminders and is always shown (not module-gated, like Vorsorge), since these are foundational health-profile records. The section carries a plain non-diagnostic note ("your own records, not a medical diagnosis"). Full i18n across all six locales (de/en/es/fr/it/pl). --- messages/de.json | 113 +++++++ messages/en.json | 113 +++++++ messages/es.json | 113 +++++++ messages/fr.json | 113 +++++++ messages/it.json | 113 +++++++ messages/pl.json | 113 +++++++ src/app/settings/[section]/page.tsx | 2 + src/components/records/allergy-form.tsx | 282 ++++++++++++++++++ src/components/records/allergy-manager.tsx | 179 +++++++++++ .../records/family-history-form.tsx | 205 +++++++++++++ .../records/family-history-manager.tsx | 162 ++++++++++ .../__tests__/settings-shell.test.tsx | 2 + src/components/settings/anamnesis-section.tsx | 56 ++++ .../settings/section-placeholder.tsx | 2 + src/components/settings/section-slugs.ts | 4 + src/components/settings/settings-shell.tsx | 10 + 16 files changed, 1582 insertions(+) create mode 100644 src/components/records/allergy-form.tsx create mode 100644 src/components/records/allergy-manager.tsx create mode 100644 src/components/records/family-history-form.tsx create mode 100644 src/components/records/family-history-manager.tsx create mode 100644 src/components/settings/anamnesis-section.tsx diff --git a/messages/de.json b/messages/de.json index 146bbed3a..1e0fb223e 100644 --- a/messages/de.json +++ b/messages/de.json @@ -4266,6 +4266,11 @@ "mcp": { "title": "MCP-Connector", "subtitle": "Verbinde einen externen Assistenten mit deinen eigenen Gesundheitsdaten." + }, + "anamnesis": { + "title": "Anamnese", + "subtitle": "Verwalte deine Allergien und deine Familienanamnese.", + "description": "Strukturierte Datensätze zu Allergien und Familienanamnese." } }, "profile": "Profil", @@ -7426,5 +7431,113 @@ "reload": "Neu laden", "dismiss": "Schließen" } + }, + "records": { + "disclaimer": "Das sind deine eigenen Angaben, keine ärztliche Diagnose. HealthLog stellt keine Diagnose, behandelt nicht und deutet nichts.", + "allergies": { + "cardTitle": "Allergien & Unverträglichkeiten", + "cardDescription": "Stoffe, auf die du reagierst, mit Reaktion und Schweregrad.", + "description": "Eine Allergie oder Unverträglichkeit erfassen.", + "add": "Allergie hinzufügen", + "addFirst": "Erste Allergie hinzufügen", + "addTitle": "Allergie hinzufügen", + "editTitle": "Allergie bearbeiten", + "formDescription": "Der Stoff wird im Klartext gespeichert; Reaktion und Notiz werden verschlüsselt.", + "emptyTitle": "Keine Allergien erfasst", + "emptyDescription": "Erfasse Stoffe, auf die du reagierst, und die Reaktionen.", + "loadError": "Deine Allergien konnten nicht geladen werden.", + "deletedToast": "Allergie gelöscht", + "deleteError": "Die Allergie konnte nicht gelöscht werden.", + "deleteConfirmTitle": "Diese Allergie löschen?", + "deleteConfirmDescription": "Damit wird der Eintrag entfernt. Das lässt sich nicht rückgängig machen.", + "category": { + "FOOD": "Nahrungsmittel", + "MEDICATION": "Medikament", + "ENVIRONMENT": "Umwelt", + "BIOLOGIC": "Biologikum", + "OTHER": "Sonstiges" + }, + "type": { + "ALLERGY": "Allergie", + "INTOLERANCE": "Unverträglichkeit" + }, + "severity": { + "NONE": "Nicht bewertet", + "MILD": "Leicht", + "MODERATE": "Mittel", + "SEVERE": "Schwer" + }, + "status": { + "ACTIVE": "Aktiv", + "INACTIVE": "Inaktiv", + "RESOLVED": "Abgeklungen" + }, + "form": { + "substance": "Stoff", + "substancePlaceholder": "z. B. Penicillin, Erdnüsse", + "category": "Kategorie", + "type": "Art", + "severity": "Schweregrad", + "status": "Status", + "onset": "Erstmals bemerkt", + "reaction": "Reaktion", + "reactionPlaceholder": "z. B. Nesselsucht, Schwellung, Anaphylaxie", + "note": "Notiz", + "notePlaceholder": "Was du dir sonst merken möchtest", + "requiredError": "Bitte den Stoff angeben.", + "savedToast": "Allergie gespeichert", + "updatedToast": "Allergie aktualisiert", + "saveError": "Die Allergie konnte nicht gespeichert werden." + } + }, + "family": { + "cardTitle": "Familienanamnese", + "cardDescription": "Erkrankungen bei Verwandten, ein Eintrag je Erkrankung.", + "description": "Eine Erkrankung bei einem Verwandten erfassen.", + "add": "Eintrag hinzufügen", + "addFirst": "Ersten Eintrag hinzufügen", + "addTitle": "Familienanamnese hinzufügen", + "editTitle": "Familienanamnese bearbeiten", + "formDescription": "Verwandtschaft und Erkrankung werden im Klartext gespeichert; die Notiz wird verschlüsselt.", + "emptyTitle": "Keine Familienanamnese erfasst", + "emptyDescription": "Erfasse Erkrankungen, die in deiner Familie vorkommen.", + "loadError": "Deine Familienanamnese konnte nicht geladen werden.", + "deletedToast": "Eintrag gelöscht", + "deleteError": "Der Eintrag konnte nicht gelöscht werden.", + "deleteConfirmTitle": "Diesen Eintrag löschen?", + "deleteConfirmDescription": "Damit wird der Eintrag entfernt. Das lässt sich nicht rückgängig machen.", + "ageAtOnsetShort": "Alter {age}", + "relationship": { + "MOTHER": "Mutter", + "FATHER": "Vater", + "SISTER": "Schwester", + "BROTHER": "Bruder", + "DAUGHTER": "Tochter", + "SON": "Sohn", + "GRANDMOTHER_MATERNAL": "Großmutter (mütterlich)", + "GRANDFATHER_MATERNAL": "Großvater (mütterlich)", + "GRANDMOTHER_PATERNAL": "Großmutter (väterlich)", + "GRANDFATHER_PATERNAL": "Großvater (väterlich)", + "AUNT": "Tante", + "UNCLE": "Onkel", + "COUSIN": "Cousin/Cousine", + "HALF_SIBLING": "Halbgeschwister", + "OTHER": "Andere/r Verwandte/r" + }, + "form": { + "relationship": "Verwandte/r", + "condition": "Erkrankung", + "conditionPlaceholder": "z. B. Typ-2-Diabetes, Brustkrebs", + "ageAtOnset": "Alter bei Beginn", + "ageAtOnsetPlaceholder": "z. B. 55", + "note": "Notiz", + "notePlaceholder": "Was du dir sonst merken möchtest", + "requiredError": "Bitte die Erkrankung angeben.", + "ageError": "Bitte ein Alter zwischen 0 und 120 angeben.", + "savedToast": "Eintrag gespeichert", + "updatedToast": "Eintrag aktualisiert", + "saveError": "Der Eintrag konnte nicht gespeichert werden." + } + } } } diff --git a/messages/en.json b/messages/en.json index 23ae4f2b7..0ef19875b 100644 --- a/messages/en.json +++ b/messages/en.json @@ -4266,6 +4266,11 @@ "mcp": { "title": "MCP Connector", "subtitle": "Connect an external assistant to your own health data." + }, + "anamnesis": { + "title": "Medical history", + "subtitle": "Manage your allergies and family history.", + "description": "Structured allergies and family history records." } }, "profile": "Profile", @@ -7426,5 +7431,113 @@ "reload": "Reload", "dismiss": "Dismiss" } + }, + "records": { + "disclaimer": "These are your own records, not a medical diagnosis. HealthLog does not diagnose, treat, or interpret them.", + "allergies": { + "cardTitle": "Allergies & intolerances", + "cardDescription": "Substances you react to, with the reaction and how severe it is.", + "description": "Record an allergy or intolerance.", + "add": "Add allergy", + "addFirst": "Add your first allergy", + "addTitle": "Add allergy", + "editTitle": "Edit allergy", + "formDescription": "The substance is stored as plain text; the reaction and note are encrypted.", + "emptyTitle": "No allergies recorded", + "emptyDescription": "Add substances you react to and their reactions.", + "loadError": "Couldn't load your allergies.", + "deletedToast": "Allergy deleted", + "deleteError": "Couldn't delete the allergy.", + "deleteConfirmTitle": "Delete this allergy?", + "deleteConfirmDescription": "This removes the record. It can't be undone.", + "category": { + "FOOD": "Food", + "MEDICATION": "Medication", + "ENVIRONMENT": "Environment", + "BIOLOGIC": "Biologic", + "OTHER": "Other" + }, + "type": { + "ALLERGY": "Allergy", + "INTOLERANCE": "Intolerance" + }, + "severity": { + "NONE": "Not assessed", + "MILD": "Mild", + "MODERATE": "Moderate", + "SEVERE": "Severe" + }, + "status": { + "ACTIVE": "Active", + "INACTIVE": "Inactive", + "RESOLVED": "Resolved" + }, + "form": { + "substance": "Substance", + "substancePlaceholder": "e.g. Penicillin, Peanuts", + "category": "Category", + "type": "Type", + "severity": "Severity", + "status": "Status", + "onset": "First noticed", + "reaction": "Reaction", + "reactionPlaceholder": "e.g. hives, swelling, anaphylaxis", + "note": "Note", + "notePlaceholder": "Anything else worth remembering", + "requiredError": "Enter the substance.", + "savedToast": "Allergy saved", + "updatedToast": "Allergy updated", + "saveError": "Couldn't save the allergy." + } + }, + "family": { + "cardTitle": "Family history", + "cardDescription": "Conditions in your relatives, one entry per condition.", + "description": "Record a condition in a relative.", + "add": "Add entry", + "addFirst": "Add your first entry", + "addTitle": "Add family history", + "editTitle": "Edit family history", + "formDescription": "The relationship and condition are stored as plain text; the note is encrypted.", + "emptyTitle": "No family history recorded", + "emptyDescription": "Add conditions that run in your family.", + "loadError": "Couldn't load your family history.", + "deletedToast": "Entry deleted", + "deleteError": "Couldn't delete the entry.", + "deleteConfirmTitle": "Delete this entry?", + "deleteConfirmDescription": "This removes the record. It can't be undone.", + "ageAtOnsetShort": "Age {age}", + "relationship": { + "MOTHER": "Mother", + "FATHER": "Father", + "SISTER": "Sister", + "BROTHER": "Brother", + "DAUGHTER": "Daughter", + "SON": "Son", + "GRANDMOTHER_MATERNAL": "Grandmother (maternal)", + "GRANDFATHER_MATERNAL": "Grandfather (maternal)", + "GRANDMOTHER_PATERNAL": "Grandmother (paternal)", + "GRANDFATHER_PATERNAL": "Grandfather (paternal)", + "AUNT": "Aunt", + "UNCLE": "Uncle", + "COUSIN": "Cousin", + "HALF_SIBLING": "Half-sibling", + "OTHER": "Other relative" + }, + "form": { + "relationship": "Relative", + "condition": "Condition", + "conditionPlaceholder": "e.g. Type 2 diabetes, Breast cancer", + "ageAtOnset": "Age at onset", + "ageAtOnsetPlaceholder": "e.g. 55", + "note": "Note", + "notePlaceholder": "Anything else worth remembering", + "requiredError": "Enter the condition.", + "ageError": "Enter an age between 0 and 120.", + "savedToast": "Entry saved", + "updatedToast": "Entry updated", + "saveError": "Couldn't save the entry." + } + } } } diff --git a/messages/es.json b/messages/es.json index e2f12b186..02a843ffc 100644 --- a/messages/es.json +++ b/messages/es.json @@ -4266,6 +4266,11 @@ "mcp": { "title": "Conector MCP", "subtitle": "Conecta un asistente externo a tus propios datos de salud." + }, + "anamnesis": { + "title": "Historial médico", + "subtitle": "Gestiona tus alergias y tus antecedentes familiares.", + "description": "Registros estructurados de alergias y antecedentes familiares." } }, "profile": "Perfil", @@ -7426,5 +7431,113 @@ "reload": "Recargar", "dismiss": "Descartar" } + }, + "records": { + "disclaimer": "Son tus propios registros, no un diagnóstico médico. HealthLog no diagnostica, no trata ni los interpreta.", + "allergies": { + "cardTitle": "Alergias e intolerancias", + "cardDescription": "Sustancias a las que reaccionas, con la reacción y su gravedad.", + "description": "Registrar una alergia o intolerancia.", + "add": "Añadir alergia", + "addFirst": "Añade tu primera alergia", + "addTitle": "Añadir alergia", + "editTitle": "Editar alergia", + "formDescription": "La sustancia se guarda como texto; la reacción y la nota se cifran.", + "emptyTitle": "No hay alergias registradas", + "emptyDescription": "Añade sustancias a las que reaccionas y sus reacciones.", + "loadError": "No se han podido cargar tus alergias.", + "deletedToast": "Alergia eliminada", + "deleteError": "No se ha podido eliminar la alergia.", + "deleteConfirmTitle": "¿Eliminar esta alergia?", + "deleteConfirmDescription": "Esto elimina el registro. No se puede deshacer.", + "category": { + "FOOD": "Alimento", + "MEDICATION": "Medicamento", + "ENVIRONMENT": "Ambiental", + "BIOLOGIC": "Biológico", + "OTHER": "Otro" + }, + "type": { + "ALLERGY": "Alergia", + "INTOLERANCE": "Intolerancia" + }, + "severity": { + "NONE": "Sin evaluar", + "MILD": "Leve", + "MODERATE": "Moderada", + "SEVERE": "Grave" + }, + "status": { + "ACTIVE": "Activa", + "INACTIVE": "Inactiva", + "RESOLVED": "Resuelta" + }, + "form": { + "substance": "Sustancia", + "substancePlaceholder": "p. ej. Penicilina, Cacahuetes", + "category": "Categoría", + "type": "Tipo", + "severity": "Gravedad", + "status": "Estado", + "onset": "Primera vez observada", + "reaction": "Reacción", + "reactionPlaceholder": "p. ej. urticaria, hinchazón, anafilaxia", + "note": "Nota", + "notePlaceholder": "Cualquier otra cosa que merezca recordar", + "requiredError": "Introduce la sustancia.", + "savedToast": "Alergia guardada", + "updatedToast": "Alergia actualizada", + "saveError": "No se ha podido guardar la alergia." + } + }, + "family": { + "cardTitle": "Antecedentes familiares", + "cardDescription": "Enfermedades de tus familiares, una entrada por enfermedad.", + "description": "Registrar una enfermedad de un familiar.", + "add": "Añadir entrada", + "addFirst": "Añade tu primera entrada", + "addTitle": "Añadir antecedente familiar", + "editTitle": "Editar antecedente familiar", + "formDescription": "El parentesco y la enfermedad se guardan como texto; la nota se cifra.", + "emptyTitle": "No hay antecedentes familiares", + "emptyDescription": "Añade enfermedades que aparecen en tu familia.", + "loadError": "No se han podido cargar tus antecedentes familiares.", + "deletedToast": "Entrada eliminada", + "deleteError": "No se ha podido eliminar la entrada.", + "deleteConfirmTitle": "¿Eliminar esta entrada?", + "deleteConfirmDescription": "Esto elimina el registro. No se puede deshacer.", + "ageAtOnsetShort": "Edad {age}", + "relationship": { + "MOTHER": "Madre", + "FATHER": "Padre", + "SISTER": "Hermana", + "BROTHER": "Hermano", + "DAUGHTER": "Hija", + "SON": "Hijo", + "GRANDMOTHER_MATERNAL": "Abuela (materna)", + "GRANDFATHER_MATERNAL": "Abuelo (materno)", + "GRANDMOTHER_PATERNAL": "Abuela (paterna)", + "GRANDFATHER_PATERNAL": "Abuelo (paterno)", + "AUNT": "Tía", + "UNCLE": "Tío", + "COUSIN": "Primo/a", + "HALF_SIBLING": "Medio hermano/a", + "OTHER": "Otro familiar" + }, + "form": { + "relationship": "Familiar", + "condition": "Enfermedad", + "conditionPlaceholder": "p. ej. Diabetes tipo 2, Cáncer de mama", + "ageAtOnset": "Edad de inicio", + "ageAtOnsetPlaceholder": "p. ej. 55", + "note": "Nota", + "notePlaceholder": "Cualquier otra cosa que merezca recordar", + "requiredError": "Introduce la enfermedad.", + "ageError": "Introduce una edad entre 0 y 120.", + "savedToast": "Entrada guardada", + "updatedToast": "Entrada actualizada", + "saveError": "No se ha podido guardar la entrada." + } + } } } diff --git a/messages/fr.json b/messages/fr.json index 5b953e533..6d5e46f33 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -4266,6 +4266,11 @@ "mcp": { "title": "Connecteur MCP", "subtitle": "Connectez un assistant externe à vos propres données de santé." + }, + "anamnesis": { + "title": "Antécédents médicaux", + "subtitle": "Gérez vos allergies et vos antécédents familiaux.", + "description": "Dossiers structurés des allergies et des antécédents familiaux." } }, "profile": "Profil", @@ -7426,5 +7431,113 @@ "reload": "Recharger", "dismiss": "Ignorer" } + }, + "records": { + "disclaimer": "Ce sont vos propres informations, pas un diagnostic médical. HealthLog ne diagnostique pas, ne traite pas et ne les interprète pas.", + "allergies": { + "cardTitle": "Allergies & intolérances", + "cardDescription": "Substances auxquelles vous réagissez, avec la réaction et sa gravité.", + "description": "Enregistrer une allergie ou une intolérance.", + "add": "Ajouter une allergie", + "addFirst": "Ajoutez votre première allergie", + "addTitle": "Ajouter une allergie", + "editTitle": "Modifier l'allergie", + "formDescription": "La substance est stockée en clair ; la réaction et la note sont chiffrées.", + "emptyTitle": "Aucune allergie enregistrée", + "emptyDescription": "Ajoutez les substances auxquelles vous réagissez et leurs réactions.", + "loadError": "Impossible de charger vos allergies.", + "deletedToast": "Allergie supprimée", + "deleteError": "Impossible de supprimer l'allergie.", + "deleteConfirmTitle": "Supprimer cette allergie ?", + "deleteConfirmDescription": "Cela supprime l'enregistrement. C'est irréversible.", + "category": { + "FOOD": "Aliment", + "MEDICATION": "Médicament", + "ENVIRONMENT": "Environnement", + "BIOLOGIC": "Biologique", + "OTHER": "Autre" + }, + "type": { + "ALLERGY": "Allergie", + "INTOLERANCE": "Intolérance" + }, + "severity": { + "NONE": "Non évaluée", + "MILD": "Légère", + "MODERATE": "Modérée", + "SEVERE": "Sévère" + }, + "status": { + "ACTIVE": "Active", + "INACTIVE": "Inactive", + "RESOLVED": "Résolue" + }, + "form": { + "substance": "Substance", + "substancePlaceholder": "p. ex. Pénicilline, Arachides", + "category": "Catégorie", + "type": "Type", + "severity": "Gravité", + "status": "Statut", + "onset": "Première fois remarquée", + "reaction": "Réaction", + "reactionPlaceholder": "p. ex. urticaire, gonflement, anaphylaxie", + "note": "Note", + "notePlaceholder": "Tout autre élément à retenir", + "requiredError": "Saisissez la substance.", + "savedToast": "Allergie enregistrée", + "updatedToast": "Allergie mise à jour", + "saveError": "Impossible d'enregistrer l'allergie." + } + }, + "family": { + "cardTitle": "Antécédents familiaux", + "cardDescription": "Maladies de vos proches, une entrée par maladie.", + "description": "Enregistrer une maladie d'un proche.", + "add": "Ajouter une entrée", + "addFirst": "Ajoutez votre première entrée", + "addTitle": "Ajouter un antécédent familial", + "editTitle": "Modifier l'antécédent familial", + "formDescription": "Le lien de parenté et la maladie sont stockés en clair ; la note est chiffrée.", + "emptyTitle": "Aucun antécédent familial", + "emptyDescription": "Ajoutez les maladies présentes dans votre famille.", + "loadError": "Impossible de charger vos antécédents familiaux.", + "deletedToast": "Entrée supprimée", + "deleteError": "Impossible de supprimer l'entrée.", + "deleteConfirmTitle": "Supprimer cette entrée ?", + "deleteConfirmDescription": "Cela supprime l'enregistrement. C'est irréversible.", + "ageAtOnsetShort": "Âge {age}", + "relationship": { + "MOTHER": "Mère", + "FATHER": "Père", + "SISTER": "Sœur", + "BROTHER": "Frère", + "DAUGHTER": "Fille", + "SON": "Fils", + "GRANDMOTHER_MATERNAL": "Grand-mère (maternelle)", + "GRANDFATHER_MATERNAL": "Grand-père (maternel)", + "GRANDMOTHER_PATERNAL": "Grand-mère (paternelle)", + "GRANDFATHER_PATERNAL": "Grand-père (paternel)", + "AUNT": "Tante", + "UNCLE": "Oncle", + "COUSIN": "Cousin/cousine", + "HALF_SIBLING": "Demi-frère/demi-sœur", + "OTHER": "Autre proche" + }, + "form": { + "relationship": "Proche", + "condition": "Maladie", + "conditionPlaceholder": "p. ex. Diabète de type 2, Cancer du sein", + "ageAtOnset": "Âge d'apparition", + "ageAtOnsetPlaceholder": "p. ex. 55", + "note": "Note", + "notePlaceholder": "Tout autre élément à retenir", + "requiredError": "Saisissez la maladie.", + "ageError": "Saisissez un âge entre 0 et 120.", + "savedToast": "Entrée enregistrée", + "updatedToast": "Entrée mise à jour", + "saveError": "Impossible d'enregistrer l'entrée." + } + } } } diff --git a/messages/it.json b/messages/it.json index 1c446bda3..e54efa09b 100644 --- a/messages/it.json +++ b/messages/it.json @@ -4266,6 +4266,11 @@ "mcp": { "title": "Connettore MCP", "subtitle": "Collega un assistente esterno ai tuoi dati sulla salute." + }, + "anamnesis": { + "title": "Anamnesi", + "subtitle": "Gestisci le tue allergie e la tua anamnesi familiare.", + "description": "Record strutturati di allergie e anamnesi familiare." } }, "profile": "Profilo", @@ -7426,5 +7431,113 @@ "reload": "Ricarica", "dismiss": "Ignora" } + }, + "records": { + "disclaimer": "Sono i tuoi dati, non una diagnosi medica. HealthLog non diagnostica, non cura e non li interpreta.", + "allergies": { + "cardTitle": "Allergie e intolleranze", + "cardDescription": "Sostanze a cui reagisci, con la reazione e la sua gravità.", + "description": "Registra un'allergia o un'intolleranza.", + "add": "Aggiungi allergia", + "addFirst": "Aggiungi la tua prima allergia", + "addTitle": "Aggiungi allergia", + "editTitle": "Modifica allergia", + "formDescription": "La sostanza è salvata in chiaro; la reazione e la nota sono cifrate.", + "emptyTitle": "Nessuna allergia registrata", + "emptyDescription": "Aggiungi le sostanze a cui reagisci e le reazioni.", + "loadError": "Impossibile caricare le tue allergie.", + "deletedToast": "Allergia eliminata", + "deleteError": "Impossibile eliminare l'allergia.", + "deleteConfirmTitle": "Eliminare questa allergia?", + "deleteConfirmDescription": "Questo rimuove il record. Non può essere annullato.", + "category": { + "FOOD": "Alimento", + "MEDICATION": "Farmaco", + "ENVIRONMENT": "Ambiente", + "BIOLOGIC": "Biologico", + "OTHER": "Altro" + }, + "type": { + "ALLERGY": "Allergia", + "INTOLERANCE": "Intolleranza" + }, + "severity": { + "NONE": "Non valutata", + "MILD": "Lieve", + "MODERATE": "Moderata", + "SEVERE": "Grave" + }, + "status": { + "ACTIVE": "Attiva", + "INACTIVE": "Inattiva", + "RESOLVED": "Risolta" + }, + "form": { + "substance": "Sostanza", + "substancePlaceholder": "es. Penicillina, Arachidi", + "category": "Categoria", + "type": "Tipo", + "severity": "Gravità", + "status": "Stato", + "onset": "Notata per la prima volta", + "reaction": "Reazione", + "reactionPlaceholder": "es. orticaria, gonfiore, anafilassi", + "note": "Nota", + "notePlaceholder": "Qualsiasi altra cosa da ricordare", + "requiredError": "Inserisci la sostanza.", + "savedToast": "Allergia salvata", + "updatedToast": "Allergia aggiornata", + "saveError": "Impossibile salvare l'allergia." + } + }, + "family": { + "cardTitle": "Anamnesi familiare", + "cardDescription": "Patologie dei tuoi parenti, una voce per patologia.", + "description": "Registra una patologia di un parente.", + "add": "Aggiungi voce", + "addFirst": "Aggiungi la tua prima voce", + "addTitle": "Aggiungi anamnesi familiare", + "editTitle": "Modifica anamnesi familiare", + "formDescription": "La parentela e la patologia sono salvate in chiaro; la nota è cifrata.", + "emptyTitle": "Nessuna anamnesi familiare", + "emptyDescription": "Aggiungi le patologie presenti nella tua famiglia.", + "loadError": "Impossibile caricare la tua anamnesi familiare.", + "deletedToast": "Voce eliminata", + "deleteError": "Impossibile eliminare la voce.", + "deleteConfirmTitle": "Eliminare questa voce?", + "deleteConfirmDescription": "Questo rimuove il record. Non può essere annullato.", + "ageAtOnsetShort": "Età {age}", + "relationship": { + "MOTHER": "Madre", + "FATHER": "Padre", + "SISTER": "Sorella", + "BROTHER": "Fratello", + "DAUGHTER": "Figlia", + "SON": "Figlio", + "GRANDMOTHER_MATERNAL": "Nonna (materna)", + "GRANDFATHER_MATERNAL": "Nonno (materno)", + "GRANDMOTHER_PATERNAL": "Nonna (paterna)", + "GRANDFATHER_PATERNAL": "Nonno (paterno)", + "AUNT": "Zia", + "UNCLE": "Zio", + "COUSIN": "Cugino/a", + "HALF_SIBLING": "Fratellastro/sorellastra", + "OTHER": "Altro parente" + }, + "form": { + "relationship": "Parente", + "condition": "Patologia", + "conditionPlaceholder": "es. Diabete di tipo 2, Cancro al seno", + "ageAtOnset": "Età di insorgenza", + "ageAtOnsetPlaceholder": "es. 55", + "note": "Nota", + "notePlaceholder": "Qualsiasi altra cosa da ricordare", + "requiredError": "Inserisci la patologia.", + "ageError": "Inserisci un'età tra 0 e 120.", + "savedToast": "Voce salvata", + "updatedToast": "Voce aggiornata", + "saveError": "Impossibile salvare la voce." + } + } } } diff --git a/messages/pl.json b/messages/pl.json index cb6fd0d43..fa05344d5 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -4266,6 +4266,11 @@ "mcp": { "title": "Łącznik MCP", "subtitle": "Połącz zewnętrznego asystenta z własnymi danymi zdrowotnymi." + }, + "anamnesis": { + "title": "Wywiad medyczny", + "subtitle": "Zarządzaj swoimi alergiami i wywiadem rodzinnym.", + "description": "Ustrukturyzowane rekordy alergii i wywiadu rodzinnego." } }, "profile": "Profil", @@ -7426,5 +7431,113 @@ "reload": "Załaduj ponownie", "dismiss": "Odrzuć" } + }, + "records": { + "disclaimer": "To Twoje własne wpisy, a nie diagnoza lekarska. HealthLog nie diagnozuje, nie leczy i ich nie interpretuje.", + "allergies": { + "cardTitle": "Alergie i nietolerancje", + "cardDescription": "Substancje, na które reagujesz, wraz z reakcją i jej nasileniem.", + "description": "Zapisz alergię lub nietolerancję.", + "add": "Dodaj alergię", + "addFirst": "Dodaj swoją pierwszą alergię", + "addTitle": "Dodaj alergię", + "editTitle": "Edytuj alergię", + "formDescription": "Substancja jest przechowywana jako tekst; reakcja i notatka są szyfrowane.", + "emptyTitle": "Brak zapisanych alergii", + "emptyDescription": "Dodaj substancje, na które reagujesz, i ich reakcje.", + "loadError": "Nie udało się wczytać Twoich alergii.", + "deletedToast": "Alergia usunięta", + "deleteError": "Nie udało się usunąć alergii.", + "deleteConfirmTitle": "Usunąć tę alergię?", + "deleteConfirmDescription": "To usuwa wpis. Nie można tego cofnąć.", + "category": { + "FOOD": "Pokarm", + "MEDICATION": "Lek", + "ENVIRONMENT": "Środowisko", + "BIOLOGIC": "Biologiczny", + "OTHER": "Inne" + }, + "type": { + "ALLERGY": "Alergia", + "INTOLERANCE": "Nietolerancja" + }, + "severity": { + "NONE": "Nieoceniona", + "MILD": "Łagodna", + "MODERATE": "Umiarkowana", + "SEVERE": "Ciężka" + }, + "status": { + "ACTIVE": "Aktywna", + "INACTIVE": "Nieaktywna", + "RESOLVED": "Ustąpiła" + }, + "form": { + "substance": "Substancja", + "substancePlaceholder": "np. Penicylina, Orzeszki ziemne", + "category": "Kategoria", + "type": "Typ", + "severity": "Nasilenie", + "status": "Status", + "onset": "Pierwszy raz zauważono", + "reaction": "Reakcja", + "reactionPlaceholder": "np. pokrzywka, obrzęk, anafilaksja", + "note": "Notatka", + "notePlaceholder": "Cokolwiek innego, co warto zapamiętać", + "requiredError": "Podaj substancję.", + "savedToast": "Alergia zapisana", + "updatedToast": "Alergia zaktualizowana", + "saveError": "Nie udało się zapisać alergii." + } + }, + "family": { + "cardTitle": "Wywiad rodzinny", + "cardDescription": "Choroby u Twoich krewnych, jeden wpis na chorobę.", + "description": "Zapisz chorobę u krewnego.", + "add": "Dodaj wpis", + "addFirst": "Dodaj swój pierwszy wpis", + "addTitle": "Dodaj wywiad rodzinny", + "editTitle": "Edytuj wywiad rodzinny", + "formDescription": "Pokrewieństwo i choroba są przechowywane jako tekst; notatka jest szyfrowana.", + "emptyTitle": "Brak wywiadu rodzinnego", + "emptyDescription": "Dodaj choroby występujące w Twojej rodzinie.", + "loadError": "Nie udało się wczytać Twojego wywiadu rodzinnego.", + "deletedToast": "Wpis usunięty", + "deleteError": "Nie udało się usunąć wpisu.", + "deleteConfirmTitle": "Usunąć ten wpis?", + "deleteConfirmDescription": "To usuwa wpis. Nie można tego cofnąć.", + "ageAtOnsetShort": "Wiek {age}", + "relationship": { + "MOTHER": "Matka", + "FATHER": "Ojciec", + "SISTER": "Siostra", + "BROTHER": "Brat", + "DAUGHTER": "Córka", + "SON": "Syn", + "GRANDMOTHER_MATERNAL": "Babcia (ze strony matki)", + "GRANDFATHER_MATERNAL": "Dziadek (ze strony matki)", + "GRANDMOTHER_PATERNAL": "Babcia (ze strony ojca)", + "GRANDFATHER_PATERNAL": "Dziadek (ze strony ojca)", + "AUNT": "Ciotka", + "UNCLE": "Wujek", + "COUSIN": "Kuzyn/kuzynka", + "HALF_SIBLING": "Przyrodnie rodzeństwo", + "OTHER": "Inny krewny" + }, + "form": { + "relationship": "Krewny", + "condition": "Choroba", + "conditionPlaceholder": "np. Cukrzyca typu 2, Rak piersi", + "ageAtOnset": "Wiek zachorowania", + "ageAtOnsetPlaceholder": "np. 55", + "note": "Notatka", + "notePlaceholder": "Cokolwiek innego, co warto zapamiętać", + "requiredError": "Podaj chorobę.", + "ageError": "Podaj wiek od 0 do 120.", + "savedToast": "Wpis zapisany", + "updatedToast": "Wpis zaktualizowany", + "saveError": "Nie udało się zapisać wpisu." + } + } } } diff --git a/src/app/settings/[section]/page.tsx b/src/app/settings/[section]/page.tsx index ad65b2c60..2959110f4 100644 --- a/src/app/settings/[section]/page.tsx +++ b/src/app/settings/[section]/page.tsx @@ -2,6 +2,7 @@ import { notFound } from "next/navigation"; import type { JSX } from "react"; import { AccountSection } from "@/components/settings/account-section"; +import { AnamnesisSection } from "@/components/settings/anamnesis-section"; import { SecuritySection } from "@/components/settings/security-section"; import { AboutSection } from "@/components/settings/about-section"; import { AdvancedSection } from "@/components/settings/advanced-section"; @@ -74,6 +75,7 @@ const SECTION_COMPONENTS: Record< mood: MoodSection, labs: LabsSection, illness: IllnessSection, + anamnesis: AnamnesisSection, vorsorge: VorsorgeSection, thresholds: ThresholdsSection, api: ApiSection, diff --git a/src/components/records/allergy-form.tsx b/src/components/records/allergy-form.tsx new file mode 100644 index 000000000..2d6393b09 --- /dev/null +++ b/src/components/records/allergy-form.tsx @@ -0,0 +1,282 @@ +"use client"; + +import { useId, useState } from "react"; +import { createPortal } from "react-dom"; +import { Loader2 } from "lucide-react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { ApiError, apiPost, apiPut } from "@/lib/api/api-fetch"; +import { + allergyCategoryEnum, + allergySeverityEnum, + allergyStatusEnum, + allergyTypeEnum, +} from "@/lib/validations/allergy"; +import { useTranslations } from "@/lib/i18n/context"; + +import type { AllergyDTO } from "@/lib/records/dto"; + +const TEXT_MAX_LENGTH = 2000; +/** Select sentinel for the optional "not assessed" severity. */ +const SEVERITY_NONE = "NONE"; + +interface AllergyFormProps { + /** When set the form edits this record; otherwise it creates a new one. */ + existing?: AllergyDTO; + onSuccess?: (saved: AllergyDTO) => void; + onCancel?: () => void; + /** The sheet footer slot the Cancel / Save row portals into when present. */ + footerSlot?: HTMLElement | null; +} + +/** ISO date input (YYYY-MM-DD) → start-of-day UTC instant, or undefined. */ +function toInstant(value: string): string | undefined { + if (!value) return undefined; + const d = new Date(`${value}T00:00:00.000Z`); + return Number.isNaN(d.getTime()) ? undefined : d.toISOString(); +} + +/** + * v1.25 (W-RECORDS) — create or edit a structured allergy/intolerance record. + * + * `substance` is the queryable label; the free-text reaction + note are + * encrypted server-side. Patient-reported reference data, not a clinical + * diagnosis. + */ +export function AllergyForm({ + existing, + onSuccess, + onCancel, + footerSlot, +}: AllergyFormProps) { + const { t } = useTranslations(); + const formId = useId(); + + const [substance, setSubstance] = useState(existing?.substance ?? ""); + const [category, setCategory] = useState(existing?.category ?? "OTHER"); + const [type, setType] = useState(existing?.type ?? "ALLERGY"); + const [severity, setSeverity] = useState(existing?.severity ?? SEVERITY_NONE); + const [status, setStatus] = useState(existing?.status ?? "ACTIVE"); + const [onsetDate, setOnsetDate] = useState( + existing?.onsetAt ? existing.onsetAt.slice(0, 10) : "", + ); + const [reaction, setReaction] = useState(existing?.reaction ?? ""); + const [note, setNote] = useState(existing?.note ?? ""); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + + if (!substance.trim()) { + setError(t("records.allergies.form.requiredError")); + return; + } + + const body = { + substance: substance.trim(), + category, + type, + severity: severity === SEVERITY_NONE ? null : severity, + status, + onsetAt: onsetDate ? (toInstant(onsetDate) ?? null) : null, + reaction: reaction.trim() ? reaction.trim() : null, + note: note.trim() ? note.trim() : null, + }; + + setSubmitting(true); + try { + const saved = existing + ? await apiPut(`/api/allergies/${existing.id}`, body) + : await apiPost("/api/allergies", body); + toast.success( + existing + ? t("records.allergies.form.updatedToast") + : t("records.allergies.form.savedToast"), + ); + onSuccess?.(saved); + } catch (err) { + const message = + err instanceof ApiError + ? err.message + : t("records.allergies.form.saveError"); + setError(message); + } finally { + setSubmitting(false); + } + } + + const footerNode = ( + <> + {onCancel ? ( + + ) : null} + + + ); + + return ( +
+
+
+ + setSubstance(e.target.value)} + placeholder={t("records.allergies.form.substancePlaceholder")} + maxLength={160} + autoFocus + required + /> +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + setOnsetDate(e.target.value)} + /> +
+
+ +