diff --git a/messages/de.json b/messages/de.json index 42cf23cf1..6718bb644 100644 --- a/messages/de.json +++ b/messages/de.json @@ -4494,6 +4494,31 @@ "excludedMetrics": "Ausgeschlossene Messwerte", "dataClusters": "Datenbereiche", "customClusters": "Eigene Auswahl von Datenbereichen verwenden", + "metrics": { + "bp": "Blutdruck", + "weight": "Gewicht", + "pulse": "Puls", + "mood": "Stimmung", + "compliance": "Medikamenteneinnahme", + "hrv": "Herzfrequenzvariabilität", + "sleep": "Schlaf", + "restingHr": "Ruhepuls", + "steps": "Schritte", + "medications": "Medikamente", + "anthropometrics": "Körpermaße" + }, + "clusters": { + "cardio": "Herz-Kreislauf", + "body": "Körperzusammensetzung", + "activity": "Aktivität", + "workouts": "Trainings", + "sleep": "Schlaf", + "mood": "Stimmung", + "glucose": "Glukose", + "medication": "Medikation", + "mobility": "Mobilität", + "environment": "Umgebung" + }, "value": "{value}" }, "insights": { @@ -4501,7 +4526,22 @@ "sections": "Übersichtsbereiche", "tiles": "Navigationskacheln", "item": "{id}", - "order": "Reihenfolge" + "order": "Reihenfolge", + "unknownItem": "Nicht verfügbare Erkenntnis", + "sectionLabels": { + "wellnessScores": "Wellnesswerte", + "dailyBriefing": "Tagesüberblick", + "vitals": "Vitalwerte", + "trends": "Trends", + "periodReview": "Periodenrückblick", + "cycleSummary": "Zyklusübersicht", + "signals": "Signale", + "rhythmEvents": "Rhythmusereignisse", + "healthStatus": "Gesundheitsstatus", + "breathing": "Atmung", + "labsChanges": "Laborveränderungen", + "ecg": "EKG" + } } }, "unavailableTitle": "In diesem geteilten Datensatz nicht verfügbar", diff --git a/messages/en.json b/messages/en.json index 1339c30d4..d5642c958 100644 --- a/messages/en.json +++ b/messages/en.json @@ -4494,6 +4494,31 @@ "excludedMetrics": "Excluded metrics", "dataClusters": "Data clusters", "customClusters": "Use a custom cluster selection", + "metrics": { + "bp": "Blood pressure", + "weight": "Weight", + "pulse": "Pulse", + "mood": "Mood", + "compliance": "Medication adherence", + "hrv": "Heart rate variability", + "sleep": "Sleep", + "restingHr": "Resting heart rate", + "steps": "Steps", + "medications": "Medications", + "anthropometrics": "Body measurements" + }, + "clusters": { + "cardio": "Cardiovascular health", + "body": "Body composition", + "activity": "Activity", + "workouts": "Workouts", + "sleep": "Sleep", + "mood": "Mood", + "glucose": "Glucose", + "medication": "Medication", + "mobility": "Mobility", + "environment": "Environment" + }, "value": "{value}" }, "insights": { @@ -4501,7 +4526,22 @@ "sections": "Overview sections", "tiles": "Navigation tiles", "item": "{id}", - "order": "Order" + "order": "Order", + "unknownItem": "Unavailable insight", + "sectionLabels": { + "wellnessScores": "Wellness scores", + "dailyBriefing": "Daily briefing", + "vitals": "Vitals", + "trends": "Trends", + "periodReview": "Period review", + "cycleSummary": "Cycle summary", + "signals": "Signals", + "rhythmEvents": "Rhythm events", + "healthStatus": "Health status", + "breathing": "Breathing", + "labsChanges": "Lab changes", + "ecg": "ECG" + } } }, "unavailableTitle": "Unavailable in this shared record", diff --git a/messages/es.json b/messages/es.json index 3364f7f2a..56e8130ae 100644 --- a/messages/es.json +++ b/messages/es.json @@ -4494,6 +4494,31 @@ "excludedMetrics": "Métricas excluidas", "dataClusters": "Grupos de datos", "customClusters": "Usar una selección personalizada de grupos", + "metrics": { + "bp": "Presión arterial", + "weight": "Peso", + "pulse": "Pulso", + "mood": "Ánimo", + "compliance": "Adherencia a la medicación", + "hrv": "Variabilidad de la frecuencia cardíaca", + "sleep": "Sueño", + "restingHr": "Frecuencia cardíaca en reposo", + "steps": "Pasos", + "medications": "Medicamentos", + "anthropometrics": "Medidas corporales" + }, + "clusters": { + "cardio": "Salud cardiovascular", + "body": "Composición corporal", + "activity": "Actividad", + "workouts": "Entrenamientos", + "sleep": "Sueño", + "mood": "Ánimo", + "glucose": "Glucosa", + "medication": "Medicación", + "mobility": "Movilidad", + "environment": "Entorno" + }, "value": "{value}" }, "insights": { @@ -4501,7 +4526,22 @@ "sections": "Secciones del resumen", "tiles": "Mosaicos de navegación", "item": "{id}", - "order": "Orden" + "order": "Orden", + "unknownItem": "Análisis no disponible", + "sectionLabels": { + "wellnessScores": "Puntuaciones de bienestar", + "dailyBriefing": "Resumen diario", + "vitals": "Signos vitales", + "trends": "Tendencias", + "periodReview": "Revisión del período", + "cycleSummary": "Resumen del ciclo", + "signals": "Señales", + "rhythmEvents": "Eventos de ritmo", + "healthStatus": "Estado de salud", + "breathing": "Respiración", + "labsChanges": "Cambios de laboratorio", + "ecg": "ECG" + } } }, "unavailableTitle": "No disponible en este registro compartido", diff --git a/messages/fr.json b/messages/fr.json index 601b80743..6658275f7 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -4494,6 +4494,31 @@ "excludedMetrics": "Mesures exclues", "dataClusters": "Groupes de données", "customClusters": "Utiliser une sélection personnalisée de groupes", + "metrics": { + "bp": "Tension artérielle", + "weight": "Poids", + "pulse": "Pouls", + "mood": "Humeur", + "compliance": "Adhérence aux médicaments", + "hrv": "Variabilité de la fréquence cardiaque", + "sleep": "Sommeil", + "restingHr": "Fréquence cardiaque au repos", + "steps": "Pas", + "medications": "Médicaments", + "anthropometrics": "Mesures corporelles" + }, + "clusters": { + "cardio": "Santé cardiovasculaire", + "body": "Composition corporelle", + "activity": "Activité", + "workouts": "Entraînements", + "sleep": "Sommeil", + "mood": "Humeur", + "glucose": "Glucose", + "medication": "Médicaments", + "mobility": "Mobilité", + "environment": "Environnement" + }, "value": "{value}" }, "insights": { @@ -4501,7 +4526,22 @@ "sections": "Sections d’aperçu", "tiles": "Tuiles de navigation", "item": "{id}", - "order": "Ordre" + "order": "Ordre", + "unknownItem": "Analyse indisponible", + "sectionLabels": { + "wellnessScores": "Scores de bien-être", + "dailyBriefing": "Brief quotidien", + "vitals": "Signes vitaux", + "trends": "Tendances", + "periodReview": "Bilan de période", + "cycleSummary": "Résumé du cycle", + "signals": "Signaux", + "rhythmEvents": "Événements de rythme", + "healthStatus": "État de santé", + "breathing": "Respiration", + "labsChanges": "Évolutions de laboratoire", + "ecg": "ECG" + } } }, "unavailableTitle": "Indisponible dans ce dossier partagé", diff --git a/messages/it.json b/messages/it.json index 6b0b5d2c2..3fdd58c9e 100644 --- a/messages/it.json +++ b/messages/it.json @@ -4494,6 +4494,31 @@ "excludedMetrics": "Metriche escluse", "dataClusters": "Gruppi di dati", "customClusters": "Usa una selezione personalizzata di gruppi", + "metrics": { + "bp": "Pressione arteriosa", + "weight": "Peso", + "pulse": "Polso", + "mood": "Umore", + "compliance": "Aderenza ai farmaci", + "hrv": "Variabilità della frequenza cardiaca", + "sleep": "Sonno", + "restingHr": "Frequenza cardiaca a riposo", + "steps": "Passi", + "medications": "Farmaci", + "anthropometrics": "Misure corporee" + }, + "clusters": { + "cardio": "Salute cardiovascolare", + "body": "Composizione corporea", + "activity": "Attività", + "workouts": "Allenamenti", + "sleep": "Sonno", + "mood": "Umore", + "glucose": "Glucosio", + "medication": "Farmaci", + "mobility": "Mobilità", + "environment": "Ambiente" + }, "value": "{value}" }, "insights": { @@ -4501,7 +4526,22 @@ "sections": "Sezioni della panoramica", "tiles": "Riquadri di navigazione", "item": "{id}", - "order": "Ordine" + "order": "Ordine", + "unknownItem": "Analisi non disponibile", + "sectionLabels": { + "wellnessScores": "Punteggi di benessere", + "dailyBriefing": "Riepilogo giornaliero", + "vitals": "Parametri vitali", + "trends": "Tendenze", + "periodReview": "Riepilogo del periodo", + "cycleSummary": "Riepilogo del ciclo", + "signals": "Segnali", + "rhythmEvents": "Eventi del ritmo", + "healthStatus": "Stato di salute", + "breathing": "Respirazione", + "labsChanges": "Variazioni di laboratorio", + "ecg": "ECG" + } } }, "unavailableTitle": "Non disponibile in questo record condiviso", diff --git a/messages/pl.json b/messages/pl.json index 6f1cdc657..04b16dcdc 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -4494,6 +4494,31 @@ "excludedMetrics": "Wykluczone metryki", "dataClusters": "Grupy danych", "customClusters": "Użyj własnego wyboru grup", + "metrics": { + "bp": "Ciśnienie krwi", + "weight": "Masa ciała", + "pulse": "Tętno", + "mood": "Nastrój", + "compliance": "Przestrzeganie przyjmowania leków", + "hrv": "Zmienność rytmu serca", + "sleep": "Sen", + "restingHr": "Tętno spoczynkowe", + "steps": "Kroki", + "medications": "Leki", + "anthropometrics": "Wymiary ciała" + }, + "clusters": { + "cardio": "Zdrowie sercowo-naczyniowe", + "body": "Skład ciała", + "activity": "Aktywność", + "workouts": "Treningi", + "sleep": "Sen", + "mood": "Nastrój", + "glucose": "Glukoza", + "medication": "Leki", + "mobility": "Mobilność", + "environment": "Środowisko" + }, "value": "{value}" }, "insights": { @@ -4501,7 +4526,22 @@ "sections": "Sekcje przeglądu", "tiles": "Kafelki nawigacji", "item": "{id}", - "order": "Kolejność" + "order": "Kolejność", + "unknownItem": "Niedostępna analiza", + "sectionLabels": { + "wellnessScores": "Wyniki dobrostanu", + "dailyBriefing": "Codzienny przegląd", + "vitals": "Parametry życiowe", + "trends": "Trendy", + "periodReview": "Przegląd okresu", + "cycleSummary": "Podsumowanie cyklu", + "signals": "Sygnały", + "rhythmEvents": "Zdarzenia rytmu", + "healthStatus": "Stan zdrowia", + "breathing": "Oddychanie", + "labsChanges": "Zmiany wyników laboratoryjnych", + "ecg": "EKG" + } } }, "unavailableTitle": "Niedostępne w tym udostępnionym rekordzie", diff --git a/src/app/api/record-settings/[family]/route.ts b/src/app/api/record-settings/[family]/route.ts index f44f5a877..16399acc1 100644 --- a/src/app/api/record-settings/[family]/route.ts +++ b/src/app/api/record-settings/[family]/route.ts @@ -297,11 +297,16 @@ export const PATCH = apiHandler( ...((current?.thresholdsJson ?? {}) as ThresholdOverridesJson), ...patch.overrides, }; - await prisma.user.update({ + const record = await prisma.user.update({ where: { id: access.recordId }, data: { thresholdsJson: toJson(overrides) }, + select: { heightCm: true, dateOfBirth: true, gender: true }, }); - settings = { overrides }; + invalidateUserProfile(access.recordId); + settings = { + overrides, + effective: getAllEffectiveRanges(record, overrides), + }; changed = Object.keys(patch.overrides); break; } diff --git a/src/components/settings/__tests__/managed-record-settings-section.test.tsx b/src/components/settings/__tests__/managed-record-settings-section.test.tsx index 53f31505a..c34b07cf0 100644 --- a/src/components/settings/__tests__/managed-record-settings-section.test.tsx +++ b/src/components/settings/__tests__/managed-record-settings-section.test.tsx @@ -7,12 +7,15 @@ import { ALL_METRICS } from "@/lib/validations/thresholds"; import { ManagedRecordSettingsForm } from "../managed-record-settings-section"; +const LOCALES = ["de", "en", "es", "fr", "it", "pl"] as const; + const render = ( family: Parameters[0]["family"], settings: Record, + locale: (typeof LOCALES)[number] = "en", ) => renderToStaticMarkup( - + { expect(thresholds).toContain('value="10"'); expect(thresholds).toContain('value="20"'); }); + + it("uses localized display inventories instead of raw Coach and Insight IDs", () => { + for (const locale of LOCALES) { + const coach = render( + "coach", + { + preferences: { + excludeMetrics: ["bp"], + dataClusters: ["cardio"], + }, + }, + locale, + ); + const insights = render( + "insights", + { + layout: { + version: 2, + sections: [{ id: "unknown-section", visible: true, order: 0 }], + tiles: [{ id: "unknown-tile", visible: true, order: 0 }], + }, + }, + locale, + ); + + expect(coach).not.toContain(">bp<"); + expect(coach).not.toContain(">cardio<"); + expect(insights).not.toContain(">unknown-section<"); + expect(insights).not.toContain(">unknown-tile<"); + expect(insights).not.toContain("settings.sharedRecord"); + } + }); }); diff --git a/src/components/settings/managed-record-settings-section.tsx b/src/components/settings/managed-record-settings-section.tsx index df97bd33a..3ce399b07 100644 --- a/src/components/settings/managed-record-settings-section.tsx +++ b/src/components/settings/managed-record-settings-section.tsx @@ -7,6 +7,10 @@ import { Settings } from "lucide-react"; import { useAuth } from "@/hooks/use-auth"; import { apiGet, apiPatch } from "@/lib/api/api-fetch"; import { useTranslations } from "@/lib/i18n/context"; +import { + normalizeInsightsTileId, + type InsightsSectionId, +} from "@/lib/insights-layout"; import { MODULE_REGISTRY } from "@/lib/modules/registry"; import { queryKeys } from "@/lib/query-keys"; import { @@ -20,6 +24,7 @@ import { type ThresholdMetric, } from "@/lib/analytics/effective-range"; import { ALL_METRICS } from "@/lib/validations/thresholds"; +import { SUB_PAGE_TABS } from "@/components/insights/insights-tab-strip"; import { Button } from "@/components/ui/button"; import { QueryErrorCard } from "@/components/ui/query-error-card"; import { SettingsCardHeader } from "./_card-header"; @@ -67,6 +72,63 @@ const COACH_DATA_CLUSTERS = [ "mobility", "environment", ] as const; +const COACH_EXCLUDE_METRIC_LABEL_KEYS: Record< + (typeof COACH_EXCLUDE_METRICS)[number], + string +> = { + bp: "settings.sharedRecord.managedSettings.coach.metrics.bp", + weight: "settings.sharedRecord.managedSettings.coach.metrics.weight", + pulse: "settings.sharedRecord.managedSettings.coach.metrics.pulse", + mood: "settings.sharedRecord.managedSettings.coach.metrics.mood", + compliance: "settings.sharedRecord.managedSettings.coach.metrics.compliance", + hrv: "settings.sharedRecord.managedSettings.coach.metrics.hrv", + sleep: "settings.sharedRecord.managedSettings.coach.metrics.sleep", + resting_hr: "settings.sharedRecord.managedSettings.coach.metrics.restingHr", + steps: "settings.sharedRecord.managedSettings.coach.metrics.steps", + medications: + "settings.sharedRecord.managedSettings.coach.metrics.medications", + anthropometrics: + "settings.sharedRecord.managedSettings.coach.metrics.anthropometrics", +}; +const COACH_DATA_CLUSTER_LABEL_KEYS: Record< + (typeof COACH_DATA_CLUSTERS)[number], + string +> = { + cardio: "settings.sharedRecord.managedSettings.coach.clusters.cardio", + body: "settings.sharedRecord.managedSettings.coach.clusters.body", + activity: "settings.sharedRecord.managedSettings.coach.clusters.activity", + workouts: "settings.sharedRecord.managedSettings.coach.clusters.workouts", + sleep: "settings.sharedRecord.managedSettings.coach.clusters.sleep", + mood: "settings.sharedRecord.managedSettings.coach.clusters.mood", + glucose: "settings.sharedRecord.managedSettings.coach.clusters.glucose", + medication: "settings.sharedRecord.managedSettings.coach.clusters.medication", + mobility: "settings.sharedRecord.managedSettings.coach.clusters.mobility", + environment: + "settings.sharedRecord.managedSettings.coach.clusters.environment", +}; +const INSIGHTS_SECTION_LABEL_KEYS: Record = { + "wellness-scores": + "settings.sharedRecord.managedSettings.insights.sectionLabels.wellnessScores", + "daily-briefing": + "settings.sharedRecord.managedSettings.insights.sectionLabels.dailyBriefing", + vitals: "settings.sharedRecord.managedSettings.insights.sectionLabels.vitals", + trends: "settings.sharedRecord.managedSettings.insights.sectionLabels.trends", + "period-review": + "settings.sharedRecord.managedSettings.insights.sectionLabels.periodReview", + "cycle-summary": + "settings.sharedRecord.managedSettings.insights.sectionLabels.cycleSummary", + signals: + "settings.sharedRecord.managedSettings.insights.sectionLabels.signals", + "rhythm-events": + "settings.sharedRecord.managedSettings.insights.sectionLabels.rhythmEvents", + "health-status": + "settings.sharedRecord.managedSettings.insights.sectionLabels.healthStatus", + breathing: + "settings.sharedRecord.managedSettings.insights.sectionLabels.breathing", + "labs-changes": + "settings.sharedRecord.managedSettings.insights.sectionLabels.labsChanges", + ecg: "settings.sharedRecord.managedSettings.insights.sectionLabels.ecg", +}; const METRIC_LABEL_KEYS: Record = { WEIGHT: "thresholds.metricWeight", BLOOD_PRESSURE_SYS: "thresholds.metricBpSys", @@ -109,6 +171,40 @@ function formNumber(form: FormData, name: string, fallback: number): number { return Number.isFinite(value) ? value : fallback; } +function coachMetricLabel( + t: ReturnType["t"], + metric: (typeof COACH_EXCLUDE_METRICS)[number], +): string { + return t(COACH_EXCLUDE_METRIC_LABEL_KEYS[metric]); +} + +function coachClusterLabel( + t: ReturnType["t"], + cluster: (typeof COACH_DATA_CLUSTERS)[number], +): string { + return t(COACH_DATA_CLUSTER_LABEL_KEYS[cluster]); +} + +function insightItemLabel( + t: ReturnType["t"], + kind: "section" | "tile", + id: string, +): string { + if (kind === "section") { + const labelKey = INSIGHTS_SECTION_LABEL_KEYS[id as InsightsSectionId]; + return labelKey + ? t(labelKey) + : t("settings.sharedRecord.managedSettings.insights.unknownItem"); + } + + const normalizedId = normalizeInsightsTileId(id); + if (normalizedId === "overview") return t("insights.navOverview"); + const tab = SUB_PAGE_TABS[normalizedId as keyof typeof SUB_PAGE_TABS]; + return tab + ? t(tab.labelKey) + : t("settings.sharedRecord.managedSettings.insights.unknownItem"); +} + function SaveButton({ disabled, label }: { disabled: boolean; label: string }) { return (