diff --git a/messages/de.json b/messages/de.json index f4f2254d2..af2696232 100644 --- a/messages/de.json +++ b/messages/de.json @@ -1955,7 +1955,6 @@ }, "intake": { "title": "Einnahmeverlauf", - "importButton": "Importieren", "rowActions": { "openMenu": "Zeilen-Aktionen öffnen", "edit": "Bearbeiten", @@ -1965,14 +1964,7 @@ "rowToggleLabel": "Zeile auswählen" }, "bulkDelete": { - "selectionCount": "{count} Einträge ausgewählt", - "deleteButton": "Auswahl löschen", - "cancelButton": "Abbrechen", - "confirmTitle": "Ausgewählte Einträge löschen?", - "confirmBody": "{count} Einträge werden dauerhaft entfernt. Das Medikament bleibt.", - "confirmAction": "Löschen", - "toast": "Auswahl gelöscht", - "failed": "Auswahl konnte nicht gelöscht werden." + "cancelButton": "Abbrechen" }, "edit": { "dialogTitle": "Einnahme bearbeiten", @@ -2846,11 +2838,6 @@ "bmiEmptyAction": "Account-Einstellungen öffnen", "noAnalysisYet": "Noch keine Einschätzung vorhanden.", "assessmentPreparing": "Einschätzung wird vorbereitet…", - "personalRecord": { - "badge": "PR", - "tooltip": "Persönlicher Rekord: {value} {unit} am {date}", - "benchmark": "Typisch in dem Alter: {low} bis {high} {unit} ({source})" - }, "loadError": "Insights konnten nicht geladen werden", "heroPersonalBaseline": "Basierend auf deinen letzten 90 Tagen", "heroRegenerating": "Wird generiert", diff --git a/messages/en.json b/messages/en.json index 891a3d113..f2431efbd 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1955,7 +1955,6 @@ }, "intake": { "title": "Intake history", - "importButton": "Import", "rowActions": { "openMenu": "Open row actions", "edit": "Edit", @@ -1965,14 +1964,7 @@ "rowToggleLabel": "Select row" }, "bulkDelete": { - "selectionCount": "{count} entries selected", - "deleteButton": "Delete selection", - "cancelButton": "Cancel", - "confirmTitle": "Delete the selected entries?", - "confirmBody": "{count} entries will be removed permanently. The medication itself stays.", - "confirmAction": "Delete", - "toast": "Selection deleted", - "failed": "Could not delete the selection." + "cancelButton": "Cancel" }, "edit": { "dialogTitle": "Edit intake", @@ -2846,11 +2838,6 @@ "bmiEmptyTitle": "Set your height to see BMI", "bmiEmptyDescription": "BMI needs your height. Add it under Settings → Account.", "bmiEmptyAction": "Open account settings", - "personalRecord": { - "badge": "PR", - "tooltip": "Personal record: {value} {unit} on {date}", - "benchmark": "Typical at that age: {low} to {high} {unit} ({source})" - }, "loadError": "Failed to load insights", "heroPersonalBaseline": "Based on your last 90 days", "heroRegenerating": "Regenerating", diff --git a/messages/es.json b/messages/es.json index ba52d239e..53854bcb5 100644 --- a/messages/es.json +++ b/messages/es.json @@ -1955,7 +1955,6 @@ }, "intake": { "title": "Historial de tomas", - "importButton": "Importar", "rowActions": { "openMenu": "Abrir acciones de la fila", "edit": "Editar", @@ -1965,14 +1964,7 @@ "rowToggleLabel": "Seleccionar fila" }, "bulkDelete": { - "selectionCount": "{count} entradas seleccionadas", - "deleteButton": "Eliminar selección", - "cancelButton": "Cancelar", - "confirmTitle": "¿Eliminar las entradas seleccionadas?", - "confirmBody": "Se eliminarán {count} entradas de forma permanente. El medicamento se conserva.", - "confirmAction": "Eliminar", - "toast": "Selección eliminada", - "failed": "No se pudo eliminar la selección." + "cancelButton": "Cancelar" }, "edit": { "dialogTitle": "Editar toma", @@ -2846,11 +2838,6 @@ "bmiEmptyTitle": "Añade tu altura para calcular el IMC", "bmiEmptyDescription": "El IMC necesita tu altura. Guárdala en Ajustes → Cuenta.", "bmiEmptyAction": "Abrir ajustes de cuenta", - "personalRecord": { - "badge": "PR", - "tooltip": "Récord personal: {value} {unit} el {date}", - "benchmark": "Lo típico a esa edad: de {low} a {high} {unit} ({source})" - }, "loadError": "No se han podido cargar los análisis", "heroPersonalBaseline": "Basado en tus últimos 90 días", "heroRegenerating": "Generando", diff --git a/messages/fr.json b/messages/fr.json index e9de10274..7ef41308c 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -1955,7 +1955,6 @@ }, "intake": { "title": "Historique des prises", - "importButton": "Importer", "rowActions": { "openMenu": "Ouvrir les actions de la ligne", "edit": "Modifier", @@ -1965,14 +1964,7 @@ "rowToggleLabel": "Sélectionner la ligne" }, "bulkDelete": { - "selectionCount": "{count} entrées sélectionnées", - "deleteButton": "Supprimer la sélection", - "cancelButton": "Annuler", - "confirmTitle": "Supprimer les entrées sélectionnées ?", - "confirmBody": "{count} entrées seront supprimées définitivement. Le médicament est conservé.", - "confirmAction": "Supprimer", - "toast": "Sélection supprimée", - "failed": "Impossible de supprimer la sélection." + "cancelButton": "Annuler" }, "edit": { "dialogTitle": "Modifier la prise", @@ -2846,11 +2838,6 @@ "bmiEmptyTitle": "Ajoutez votre taille pour l’IMC", "bmiEmptyDescription": "L’IMC nécessite votre taille. Renseignez-la dans Paramètres → Compte.", "bmiEmptyAction": "Ouvrir les paramètres du compte", - "personalRecord": { - "badge": "PR", - "tooltip": "Record personnel : {value} {unit} le {date}", - "benchmark": "Valeur typique à cet âge : de {low} à {high} {unit} ({source})" - }, "loadError": "Impossible de charger les analyses", "heroPersonalBaseline": "Basé sur vos 90 derniers jours", "heroRegenerating": "Génération en cours", diff --git a/messages/it.json b/messages/it.json index 8c812c639..0914581c9 100644 --- a/messages/it.json +++ b/messages/it.json @@ -1955,7 +1955,6 @@ }, "intake": { "title": "Storico delle assunzioni", - "importButton": "Importa", "rowActions": { "openMenu": "Apri azioni della riga", "edit": "Modifica", @@ -1965,14 +1964,7 @@ "rowToggleLabel": "Seleziona riga" }, "bulkDelete": { - "selectionCount": "{count} voci selezionate", - "deleteButton": "Elimina selezione", - "cancelButton": "Annulla", - "confirmTitle": "Eliminare le voci selezionate?", - "confirmBody": "{count} voci verranno rimosse definitivamente. Il farmaco resta.", - "confirmAction": "Elimina", - "toast": "Selezione eliminata", - "failed": "Impossibile eliminare la selezione." + "cancelButton": "Annulla" }, "edit": { "dialogTitle": "Modifica assunzione", @@ -2846,11 +2838,6 @@ "bmiEmptyTitle": "Aggiungi l’altezza per il BMI", "bmiEmptyDescription": "Per il BMI serve la tua altezza. Salvala in Impostazioni → Account.", "bmiEmptyAction": "Apri impostazioni account", - "personalRecord": { - "badge": "PR", - "tooltip": "Record personale: {value} {unit} il {date}", - "benchmark": "Tipico a quell’età: da {low} a {high} {unit} ({source})" - }, "loadError": "Impossibile caricare le analisi", "heroPersonalBaseline": "Basato sugli ultimi 90 giorni", "heroRegenerating": "Generazione in corso", diff --git a/messages/ko.json b/messages/ko.json index 9c3a66065..ca64435fc 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -1955,7 +1955,6 @@ }, "intake": { "title": "복용 기록", - "importButton": "불러오기", "rowActions": { "openMenu": "행 작업 열기", "edit": "편집", @@ -1965,14 +1964,7 @@ "rowToggleLabel": "행 선택" }, "bulkDelete": { - "selectionCount": "{count}개 항목 선택됨", - "deleteButton": "선택 항목 삭제", - "cancelButton": "취소", - "confirmTitle": "선택한 항목을 삭제할까요?", - "confirmBody": "{count}개 항목이 영구히 삭제돼요. 약 자체는 그대로 남아요.", - "confirmAction": "삭제", - "toast": "선택 항목을 삭제했어요", - "failed": "선택 항목을 삭제하지 못했어요." + "cancelButton": "취소" }, "edit": { "dialogTitle": "복용 기록 편집", @@ -2846,11 +2838,6 @@ "bmiEmptyTitle": "BMI를 보려면 키를 입력해 주세요", "bmiEmptyDescription": "BMI에는 키가 필요해요. 설정 → 계정에서 추가해 주세요.", "bmiEmptyAction": "계정 설정 열기", - "personalRecord": { - "badge": "신기록", - "tooltip": "개인 최고 기록: {date} {value}{unit}", - "benchmark": "그 나이의 일반적인 범위: {low}~{high} {unit} ({source})" - }, "loadError": "인사이트를 불러오지 못했어요", "heroPersonalBaseline": "최근 90일 기준", "heroRegenerating": "다시 생성 중", diff --git a/messages/nl.json b/messages/nl.json index e8140cc87..cd9ea2f36 100644 --- a/messages/nl.json +++ b/messages/nl.json @@ -1955,7 +1955,6 @@ }, "intake": { "title": "Innamegeschiedenis", - "importButton": "Importeren", "rowActions": { "openMenu": "Rijacties openen", "edit": "Bewerken", @@ -1965,14 +1964,7 @@ "rowToggleLabel": "Rij selecteren" }, "bulkDelete": { - "selectionCount": "{count} registraties geselecteerd", - "deleteButton": "Selectie verwijderen", - "cancelButton": "Annuleren", - "confirmTitle": "Geselecteerde registraties verwijderen?", - "confirmBody": "{count} registraties worden definitief verwijderd. Het medicijn zelf blijft bestaan.", - "confirmAction": "Verwijderen", - "toast": "Selectie verwijderd", - "failed": "De selectie kon niet worden verwijderd." + "cancelButton": "Annuleren" }, "edit": { "dialogTitle": "Inname bewerken", @@ -2846,11 +2838,6 @@ "bmiEmptyTitle": "Stel je lengte in om je BMI te zien", "bmiEmptyDescription": "Voor de BMI is je lengte nodig. Voeg die toe onder Instellingen → Account.", "bmiEmptyAction": "Accountinstellingen openen", - "personalRecord": { - "badge": "PR", - "tooltip": "Persoonlijk record: {value} {unit} op {date}", - "benchmark": "Gebruikelijk op die leeftijd: {low} tot {high} {unit} ({source})" - }, "loadError": "Inzichten kunnen niet worden geladen", "heroPersonalBaseline": "Op basis van je afgelopen 90 dagen", "heroRegenerating": "Opnieuw genereren", diff --git a/messages/pl.json b/messages/pl.json index 123971e26..d0f197732 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -1955,7 +1955,6 @@ }, "intake": { "title": "Historia przyjęć", - "importButton": "Importuj", "rowActions": { "openMenu": "Otwórz akcje wiersza", "edit": "Edytuj", @@ -1965,14 +1964,7 @@ "rowToggleLabel": "Zaznacz wiersz" }, "bulkDelete": { - "selectionCount": "{count} wpisów zaznaczono", - "deleteButton": "Usuń zaznaczone", - "cancelButton": "Anuluj", - "confirmTitle": "Usunąć zaznaczone wpisy?", - "confirmBody": "{count} wpisów zostanie trwale usuniętych. Sam lek pozostaje.", - "confirmAction": "Usuń", - "toast": "Zaznaczone usunięto", - "failed": "Nie udało się usunąć zaznaczonych." + "cancelButton": "Anuluj" }, "edit": { "dialogTitle": "Edytuj przyjęcie", @@ -2846,11 +2838,6 @@ "bmiEmptyTitle": "Dodaj wzrost, aby obliczyć BMI", "bmiEmptyDescription": "BMI wymaga twojego wzrostu. Zapisz go w Ustawieniach → Konto.", "bmiEmptyAction": "Otwórz ustawienia konta", - "personalRecord": { - "badge": "PR", - "tooltip": "Rekord osobisty: {value} {unit} dnia {date}", - "benchmark": "Typowo w tym wieku: od {low} do {high} {unit} ({source})" - }, "loadError": "Nie udało się załadować analiz", "heroPersonalBaseline": "Na podstawie ostatnich 90 dni", "heroRegenerating": "Generowanie", diff --git a/messages/pt-BR.json b/messages/pt-BR.json index ea15f0429..4c63dec15 100644 --- a/messages/pt-BR.json +++ b/messages/pt-BR.json @@ -1955,7 +1955,6 @@ }, "intake": { "title": "Histórico de tomadas", - "importButton": "Importar", "rowActions": { "openMenu": "Abrir ações da linha", "edit": "Editar", @@ -1965,14 +1964,7 @@ "rowToggleLabel": "Selecionar linha" }, "bulkDelete": { - "selectionCount": "{count} entradas selecionadas", - "deleteButton": "Excluir seleção", - "cancelButton": "Cancelar", - "confirmTitle": "Excluir as entradas selecionadas?", - "confirmBody": "{count} entradas serão removidas permanentemente. O medicamento em si permanece.", - "confirmAction": "Excluir", - "toast": "Seleção excluída", - "failed": "Não foi possível excluir a seleção." + "cancelButton": "Cancelar" }, "edit": { "dialogTitle": "Editar tomada", @@ -2846,11 +2838,6 @@ "bmiEmptyTitle": "Defina sua altura para ver o IMC", "bmiEmptyDescription": "O IMC precisa da sua altura. Adicione-a em Configurações → Conta.", "bmiEmptyAction": "Abrir configurações da conta", - "personalRecord": { - "badge": "Recorde", - "tooltip": "Recorde pessoal: {value} {unit} em {date}", - "benchmark": "Típico nessa idade: {low} a {high} {unit} ({source})" - }, "loadError": "Falha ao carregar os insights", "heroPersonalBaseline": "Com base nos seus últimos 90 dias", "heroRegenerating": "Regenerando", diff --git a/messages/ru.json b/messages/ru.json index 6ca4cdc82..0ccc1e798 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -1955,7 +1955,6 @@ }, "intake": { "title": "История приёма", - "importButton": "Импорт", "rowActions": { "openMenu": "Открыть действия строки", "edit": "Изменить", @@ -1965,14 +1964,7 @@ "rowToggleLabel": "Выбрать строку" }, "bulkDelete": { - "selectionCount": "Выбрано записей: {count}", - "deleteButton": "Удалить выбранное", - "cancelButton": "Отмена", - "confirmTitle": "Удалить выбранные записи?", - "confirmBody": "Записи ({count}) будут удалены навсегда. Само лекарство останется.", - "confirmAction": "Удалить", - "toast": "Выбранное удалено", - "failed": "Не удалось удалить выбранное." + "cancelButton": "Отмена" }, "edit": { "dialogTitle": "Изменить приём", @@ -2846,11 +2838,6 @@ "bmiEmptyTitle": "Укажите рост, чтобы увидеть ИМТ", "bmiEmptyDescription": "Для ИМТ нужен ваш рост. Добавьте его в разделе «Настройки» → «Аккаунт».", "bmiEmptyAction": "Открыть настройки аккаунта", - "personalRecord": { - "badge": "Рекорд", - "tooltip": "Личный рекорд: {value} {unit} {date}", - "benchmark": "Типично в этом возрасте: {low} до {high} {unit} ({source})" - }, "loadError": "Не удалось загрузить аналитику", "heroPersonalBaseline": "На основе ваших последних 90 дней", "heroRegenerating": "Обновление", diff --git a/src/__tests__/baselines/source-function-lines.json b/src/__tests__/baselines/source-function-lines.json index ec7212f25..3fd584a0a 100644 --- a/src/__tests__/baselines/source-function-lines.json +++ b/src/__tests__/baselines/source-function-lines.json @@ -153,7 +153,6 @@ "components/medications/dose-strength-curve.tsx :: DoseStrengthCurve": 266, "components/medications/glp1-medication-card.tsx :: Glp1MedicationCard": 405, "components/medications/injection-site-picker.tsx :: InjectionSitePicker": 287, - "components/medications/intake-history-editable.tsx :: IntakeHistoryEditable": 224, "components/medications/intake-history-list-v2.tsx :: IntakeHistoryListV2": 403, "components/medications/intake-import-dialog.tsx :: IntakeImportDialog": 208, "components/medications/log-intake-dialog.tsx :: LogIntakeDialog": 267, diff --git a/src/__tests__/baselines/version-stamp-comments.json b/src/__tests__/baselines/version-stamp-comments.json index 22e2f5bfa..684b663f2 100644 --- a/src/__tests__/baselines/version-stamp-comments.json +++ b/src/__tests__/baselines/version-stamp-comments.json @@ -869,7 +869,6 @@ "components/insights/__tests__/metric-stat-strip.test.tsx": 3, "components/insights/__tests__/metric-target-summary-unit-preference.test.tsx": 1, "components/insights/__tests__/metric-target-summary.test.tsx": 8, - "components/insights/__tests__/personal-record-badge.test.tsx": 1, "components/insights/__tests__/rhythm-events-card.test.tsx": 1, "components/insights/__tests__/sleep-stage-stacked-bar.test.tsx": 5, "components/insights/__tests__/sub-page-shell.test.tsx": 4, @@ -1031,7 +1030,6 @@ "components/insights/nutrients/micronutrients-card.tsx": 3, "components/insights/nutrients/types.ts": 4, "components/insights/period-narrative-card.tsx": 1, - "components/insights/personal-record-badge.tsx": 2, "components/insights/prose-blocks.tsx": 1, "components/insights/recovery/recovery-metric-block.tsx": 2, "components/insights/recovery/recovery-section.tsx": 3, @@ -1127,7 +1125,6 @@ "components/medications/__tests__/intake-edit-dialog-seed.test.tsx": 1, "components/medications/__tests__/intake-history-list-v2-row-actions.test.tsx": 1, "components/medications/__tests__/intake-history-list-v2.test.tsx": 4, - "components/medications/__tests__/intake-history-preview-bulk-toolbar.test.tsx": 2, "components/medications/__tests__/inventory-section-actions.test.tsx": 1, "components/medications/__tests__/inventory-section-summary.test.tsx": 2, "components/medications/__tests__/log-injection-site-dialog.test.tsx": 1, @@ -1163,7 +1160,6 @@ "components/medications/glp1-medication-card.tsx": 36, "components/medications/injection-site-picker.tsx": 2, "components/medications/intake-edit-dialog.tsx": 2, - "components/medications/intake-history-editable.tsx": 2, "components/medications/intake-history-list-v2.tsx": 13, "components/medications/intake-import-dialog.tsx": 1, "components/medications/log-injection-site-dialog.tsx": 2, @@ -1200,7 +1196,6 @@ "components/medications/sections/courses-section.tsx": 1, "components/medications/sections/data-portability-row.tsx": 1, "components/medications/sections/destructive-zone-section.tsx": 5, - "components/medications/sections/intake-history-preview.tsx": 3, "components/medications/sections/intake-tracking-section.tsx": 1, "components/medications/sections/inventory-dialogs.tsx": 3, "components/medications/sections/inventory-section.tsx": 8, diff --git a/src/__tests__/daily-reads-refetch-guard.test.ts b/src/__tests__/daily-reads-refetch-guard.test.ts index 5e030daba..11879bff6 100644 --- a/src/__tests__/daily-reads-refetch-guard.test.ts +++ b/src/__tests__/daily-reads-refetch-guard.test.ts @@ -234,7 +234,6 @@ describe("daily-reads refetch guard", () => { const routedThroughHelper = [ "components/medications/take-all-due.ts", "components/medications/intake-edit-dialog.tsx", - "components/medications/intake-history-editable.tsx", "components/medications/intake-import-dialog.tsx", "components/medications/sections/destructive-zone-section.tsx", "components/medications/use-medication-intake.ts", diff --git a/src/__tests__/destructive-control-guard.test.ts b/src/__tests__/destructive-control-guard.test.ts index 1dd48b750..3bdc790fb 100644 --- a/src/__tests__/destructive-control-guard.test.ts +++ b/src/__tests__/destructive-control-guard.test.ts @@ -528,13 +528,6 @@ const REGISTRY: DestructiveEntry[] = [ recovery: "permanent", confirm: ["AlertDialog"], }, - { - file: "components/medications/intake-history-editable.tsx", - destroys: "one intake event, or every selected intake event", - triggers: ["components/medications/intake-history-list-v2.tsx"], - recovery: "tombstoned-no-restore", - confirm: ["AlertDialog"], - }, { file: "components/medications/dose-history/use-dose-history-ledger.ts", destroys: "one intake event", diff --git a/src/__tests__/i18n-drift-guard.test.ts b/src/__tests__/i18n-drift-guard.test.ts index 2b20160a0..934296bba 100644 --- a/src/__tests__/i18n-drift-guard.test.ts +++ b/src/__tests__/i18n-drift-guard.test.ts @@ -15,7 +15,6 @@ * - insights.emptyState.* (B4 — per-metric empty states) * - notifications.admin.* (B5 — admin notification messages) * - notifications.user.* (B5 — user-facing Telegram test body) - * - personalRecords + workouts (BL-P4-8 — PR + Workout strings) */ import { readFileSync, readdirSync } from "node:fs"; import { join } from "node:path"; @@ -46,18 +45,6 @@ for (const locale of LOCALES) { FLAT_BY_LOCALE.set(locale, flat); } -function keysForPrefix(prefix: string): Set { - // Source of truth: EN. The locale-integrity test ensures every - // locale carries the same key shape, so EN's view is canonical. - const enFlat = FLAT_BY_LOCALE.get("en"); - if (!enFlat) throw new Error("EN bundle did not load"); - const set = new Set(); - for (const k of enFlat.keys()) { - if (k === prefix || k.startsWith(`${prefix}.`)) set.add(k); - } - return set; -} - const REQUIRED_KEY_SETS: Array<{ label: string; required: string[]; @@ -143,39 +130,3 @@ describe("v1.4.27 B6 i18n drift-guard — required keys present in every locale" }); } }); - -describe("v1.4.27 B6 i18n drift-guard — PR + Workout strings stay in lockstep", () => { - // BL-P4-8: PR and Workout strings were the trigger for the - // drift-guard ask. Personal-record badge / tooltip live under - // `insights.personalRecord.*`. Workout strings are not yet in the - // bundles — the v1.4.23 batch ingest landed the API, the UI - // surface ships in v1.4.28 / v1.5. The drift-guard keeps the - // namespace anchor live so the workout copy work in v1.4.28 lands - // with parity from the first commit. - const prKeys = [...keysForPrefix("insights.personalRecord")]; - - it("insights.personalRecord namespace has at least one key (sanity)", () => { - expect( - prKeys.length, - "insights.personalRecord.* keys exist in en.json", - ).toBeGreaterThan(0); - }); - - it.each(LOCALES)( - "messages/%s.json carries every insights.personalRecord.* key with a non-empty value", - (locale) => { - const flat = FLAT_BY_LOCALE.get(locale)!; - const missing: string[] = []; - const empty: string[] = []; - for (const key of prKeys) { - const v = flat.get(key); - if (v === undefined) missing.push(key); - else if (v.trim() === "") empty.push(key); - } - expect( - { missing, empty }, - `Locale ${locale} drifted from the personal-record key set.`, - ).toEqual({ missing: [], empty: [] }); - }, - ); -}); diff --git a/src/__tests__/resolved-field-consumer-guard.test.ts b/src/__tests__/resolved-field-consumer-guard.test.ts index ff204c0d8..0ff2f9ee6 100644 --- a/src/__tests__/resolved-field-consumer-guard.test.ts +++ b/src/__tests__/resolved-field-consumer-guard.test.ts @@ -444,12 +444,10 @@ const V143_FIELDS: ResolvedFieldEntry[] = [ { field: "GET /api/personal-records benchmark", props: ["awards.ts#benchmark"], - readers: [ - { - file: "src/components/insights/personal-record-badge.tsx", - pattern: /fresh\.benchmark/, - }, - ], + iosOnly: { + reason: + "the band was added for the native records screen (ios #116 item 5); no web surface reads `/api/personal-records` since the never-mounted badge that did was deleted", + }, }, { field: "GET /api/mood/insights patterns", diff --git a/src/__tests__/success-affordance-guard.test.ts b/src/__tests__/success-affordance-guard.test.ts index 44e33938c..8683ff27f 100644 --- a/src/__tests__/success-affordance-guard.test.ts +++ b/src/__tests__/success-affordance-guard.test.ts @@ -284,9 +284,6 @@ const PINNED_AFFORDANCES: Record< "toast.success": 4, }, "src/components/medications/intake-edit-dialog.tsx": { "toast.success": 1 }, - "src/components/medications/intake-history-editable.tsx": { - "toast.success": 2, - }, "src/components/medications/intake-history-list-v2.tsx": { "text-success": 1, }, diff --git a/src/components/insights/__tests__/personal-record-badge.test.tsx b/src/components/insights/__tests__/personal-record-badge.test.tsx deleted file mode 100644 index d195b3673..000000000 --- a/src/components/insights/__tests__/personal-record-badge.test.tsx +++ /dev/null @@ -1,109 +0,0 @@ -/** - * v1.4.25 — `` rendering. - * - * The badge renders a small "PR" pill when the user achieved an - * all-time best for the metric in the last 30 days. Tests cover: - * 1. Renders the pill when a fresh PR exists. - * 2. Stays silent when no PR exists for the metric. - * 3. Stays silent when the only PR is older than 30 days. - * 4. Surfaces metric-type and PR-value via data-attrs / tooltip. - * 5. EN + DE locale parity. - */ -import { describe, it, expect, vi } from "vitest"; -import { renderToStaticMarkup } from "react-dom/server"; - -import { I18nProvider } from "@/lib/i18n/context"; - -// React-Query is mocked the same way the other insights tests do it — -// we control the `data` field per-test so the SSR tree is deterministic. -type Row = { - id: string; - metricType: string; - metricSlot: string | null; - value: number; - unit: string; - achievedAt: string; -}; -let mockRows: Row[] = []; - -vi.mock("@tanstack/react-query", () => ({ - // `dataUpdatedAt` mirrors what TanStack Query produces after a - // successful refetch — `Date.now()` is fine inside the test - // (the React-purity rule is component-scoped). - useQuery: () => ({ - data: mockRows, - isLoading: false, - dataUpdatedAt: Date.now(), - }), -})); - -import { PersonalRecordBadge } from "../personal-record-badge"; - -function render(node: React.ReactNode, locale: "en" | "de" = "en") { - return renderToStaticMarkup( - {node}, - ); -} - -function freshPR(over: Partial = {}): Row { - return { - id: "pr-1", - metricType: "ACTIVITY_STEPS", - metricSlot: null, - value: 18432, - unit: "count", - achievedAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), - ...over, - }; -} - -describe("", () => { - it("renders the PR pill when an all-time best lands in the last 30 days", () => { - mockRows = [freshPR()]; - const html = render( - , - ); - expect(html).toMatch(/data-slot="insights-pr-badge"/); - expect(html).toMatch(/data-metric-type="ACTIVITY_STEPS"/); - expect(html).toContain("PR"); - }); - - it("renders nothing when no PR exists for the metric", () => { - mockRows = []; - const html = render( - , - ); - expect(html).not.toMatch(/data-slot="insights-pr-badge"/); - }); - - it("stays silent when the PR is older than 30 days", () => { - const stale = new Date(Date.now() - 45 * 24 * 60 * 60 * 1000).toISOString(); - mockRows = [freshPR({ achievedAt: stale })]; - const html = render( - , - ); - expect(html).not.toMatch(/data-slot="insights-pr-badge"/); - }); - - it("ignores workout-slot rows on the plain-metric badge", () => { - mockRows = [freshPR({ metricSlot: "longest_run_duration" })]; - const html = render( - , - ); - expect(html).not.toMatch(/data-slot="insights-pr-badge"/); - }); - - it("uses the same 'PR' literal across EN and DE locales", () => { - mockRows = [freshPR()]; - const en = render( - , - "en", - ); - const de = render( - , - "de", - ); - expect(en).toContain("PR"); - expect(de).toContain("PR"); - }); -}); diff --git a/src/components/insights/personal-record-badge.tsx b/src/components/insights/personal-record-badge.tsx deleted file mode 100644 index dc5fc50b5..000000000 --- a/src/components/insights/personal-record-badge.tsx +++ /dev/null @@ -1,171 +0,0 @@ -"use client"; - -/** - * v1.4.25 — Personal-record "PR" pill. - * - * Renders a small, text-only "PR" tag next to a metric label when the - * given metric reached an all-time best in the last 30 days. Pulls - * directly from `GET /api/personal-records?metricType=` so the - * server's `PersonalRecord` rows are the single source of truth — no - * client-side recomputation, no risk of drift against the doctor - * report's PR list. - * - * Design choices: - * - Text-only (no celebratory emoji / no animation). The badge is - * informational and the same Recharts-restrained aesthetic the - * rest of the trend tiles use. - * - Tooltip is opt-in via the `withTooltip` prop. The dashboard's - * tile strip already runs a `TooltipProvider`, so the badge can - * drop straight in. Standalone uses (correlation cards, doctor - * report) pass `withTooltip={false}` to keep the DOM minimal. - * - Query is per-metric — 14 PR-trackable metrics × ~1 KB response = - * small enough that 14 idle queries on the dashboard cost less - * than one Recharts re-render. TanStack Query coalesces. - */ -import { useQuery } from "@tanstack/react-query"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { useFormatters, useTranslations } from "@/lib/i18n/context"; -import { apiGet } from "@/lib/api/api-fetch"; -import { queryKeys } from "@/lib/query-keys"; -import type { MeasurementType } from "@/generated/prisma/client"; -import { cn } from "@/lib/utils"; -import { ScorePill } from "@/components/ui/score-pill"; - -interface PersonalRecordRow { - id: string; - metricType: MeasurementType; - metricSlot: string | null; - value: number; - unit: string; - achievedAt: string; - /** - * v1.43 — the cited band for the age the record was set at, resolved on - * the server; null (or absent on an older server) when none applies. - */ - benchmark?: { - low: number; - high: number; - unit: string; - source: string; - sourceLabel: string; - } | null; -} - -interface PersonalRecordBadgeProps { - metricType: MeasurementType; - /** Render a wrapping Tooltip with the PR value + date. Default true. */ - withTooltip?: boolean; - /** Optional className passthrough so callers can position the pill. */ - className?: string; -} - -const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; - -/** - * Pure freshness check kept out of the component body so the React - * purity linter doesn't trip on the `Date.now()` reference; callers - * pass `now` from the surrounding query refetch boundary where one - * stale-time tick = one new clock reading. - */ -function isPRWithin30Days(achievedAtIso: string, now: number): boolean { - const ts = new Date(achievedAtIso).getTime(); - return Number.isFinite(ts) && now - ts <= THIRTY_DAYS_MS; -} - -export function PersonalRecordBadge({ - metricType, - withTooltip = true, - className, -}: PersonalRecordBadgeProps) { - const { t } = useTranslations(); - const fmt = useFormatters(); - - const { data, dataUpdatedAt } = useQuery({ - // Per-metric queryKey so two tiles for two different metrics never - // share a cache slot (HealthLog convention — see - // `feedback_react_query_key_collision`). - queryKey: queryKeys.personalRecordsByMetric(metricType), - queryFn: async () => { - try { - const data = await apiGet( - `/api/personal-records?metricType=${encodeURIComponent(metricType)}`, - ); - return data ?? []; - } catch { - // The badge is decorative — degrade to "no records", as before. - return [] as PersonalRecordRow[]; - } - }, - // Per-tile data: stale-while-revalidate cadence matches the - // dashboard's existing trend queries. 5 minutes is enough to - // absorb the worker's 30-minute fallback cadence without - // surfacing stale state for long. - staleTime: 5 * 60 * 1000, - }); - - // `dataUpdatedAt` is TanStack Query's per-query refetch timestamp - // — a deterministic clock reading that React's purity linter - // accepts because it's hook-derived. Each refetch advances it, - // shifting the freshness window forward by exactly the stale time. - // On the very first render (before the query has resolved) - // `dataUpdatedAt` is 0, so the freshness filter rejects everything - // until the response lands — desired behaviour for the SSR path. - const fresh = (data ?? []).find( - (row) => - row.metricSlot === null && - isPRWithin30Days(row.achievedAt, dataUpdatedAt), - ); - - if (!fresh) return null; - - const pill = ( - - {t("insights.personalRecord.badge")} - - ); - - if (!withTooltip) return pill; - - const formattedDate = fmt.date(new Date(fresh.achievedAt)); - const tooltipText = t("insights.personalRecord.tooltip", { - value: fmt.number(fresh.value, 1), - unit: fresh.unit, - date: formattedDate, - }); - - return ( - - {pill} - - {tooltipText} - {fresh.benchmark ? ( - - {t("insights.personalRecord.benchmark", { - low: fmt.number(fresh.benchmark.low, 1), - high: fmt.number(fresh.benchmark.high, 1), - unit: fresh.benchmark.unit, - source: fresh.benchmark.sourceLabel, - })} - - ) : null} - - - ); -} diff --git a/src/components/medications/__tests__/intake-history-preview-bulk-toolbar.test.tsx b/src/components/medications/__tests__/intake-history-preview-bulk-toolbar.test.tsx deleted file mode 100644 index 572495c0a..000000000 --- a/src/components/medications/__tests__/intake-history-preview-bulk-toolbar.test.tsx +++ /dev/null @@ -1,108 +0,0 @@ -/** - * v1.5.5 — `` smoke tests. - * - * The preview mounts in SSR with an empty selection so the bulk- - * delete toolbar stays hidden by default. Selection / dialog flips - * are interactive surfaces and are exercised by the e2e suite. - * - * The kebab + selection wiring is covered by - * `intake-history-list-v2-row-actions.test.tsx`; this file pins the - * wrapper-level contract (no toolbar by default, wired through the - * v1.5.5 endpoints). - */ - -import { describe, expect, it, vi } from "vitest"; -import { renderToStaticMarkup } from "react-dom/server"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; - -import { I18nProvider } from "@/lib/i18n/context"; -import { IntakeHistoryPreview } from "@/components/medications/sections/intake-history-preview"; -import { queryKeys } from "@/lib/query-keys"; - -vi.mock("@/hooks/use-auth", () => ({ - useAuth: () => ({ - user: { id: "test-user", username: "tester", role: "USER" }, - isAuthenticated: true, - isLoading: false, - }), -})); - -function makeClient() { - return new QueryClient({ - defaultOptions: { - queries: { - retry: false, - staleTime: Number.POSITIVE_INFINITY, - refetchOnMount: false, - }, - }, - }); -} - -function seed(client: QueryClient) { - client.setQueryData( - queryKeys.medicationIntakeList("med-1", { - sortBy: "takenAt", - sortDir: "desc", - limit: 14, - offset: 0, - status: "completed", - }), - { - events: [ - { - id: "evt-1", - medicationId: "med-1", - scheduledFor: "2026-05-15T08:00:00.000Z", - takenAt: "2026-05-15T08:02:00.000Z", - skipped: false, - source: "WEB", - createdAt: "2026-05-15T08:02:00.000Z", - }, - ], - meta: { total: 1, limit: 14, offset: 0 }, - }, - ); -} - -function render(node: React.ReactNode, client: QueryClient) { - return renderToStaticMarkup( - - {node} - , - ); -} - -describe(" — F-1 C-2", () => { - it("hides the bulk-delete toolbar when nothing is selected (initial render)", () => { - const client = makeClient(); - seed(client); - const html = render( - {}} - />, - client, - ); - expect(html).not.toContain("intake-history-bulk-delete-toolbar"); - }); - - it("renders the row-action kebab and selection checkbox on every row", () => { - const client = makeClient(); - seed(client); - const html = render( - {}} - />, - client, - ); - // Per-row affordances feature 15 + 16 — the row carries both. - expect(html).toContain("intake-history-row-kebab"); - expect(html).toContain("intake-history-row-select"); - // The import header CTA stays alongside the new affordances. - expect(html).toContain("intake-history-import"); - }); -}); diff --git a/src/components/medications/intake-history-editable.tsx b/src/components/medications/intake-history-editable.tsx deleted file mode 100644 index 04d3f960a..000000000 --- a/src/components/medications/intake-history-editable.tsx +++ /dev/null @@ -1,283 +0,0 @@ -"use client"; - -/** - * v1.7.0 — shared editable intake-history surface. - * - * Extracted from `` so the detail-page preview - * (14-row, `takenAt` sort) and the full-history view (25-row, - * `scheduledFor` sort) share one state machine: multi-select - * bulk-delete, per-row edit (``) and per-row delete - * (``). The component owns the table + every editing - * dialog; it does NOT own the CSV-import dialog (that stays lifted to - * the hosting page so it mounts exactly once). - * - * Both surfaces render the same ``; the wrapper - * just changes `pageSize` + `defaultSortBy`. The full-history route - * defaults to `scheduledFor desc` so skipped rows (`takenAt: null`) - * never float to the top of the table (O-1). - */ - -import { useRecordCapabilities } from "@/hooks/use-record-capabilities"; -import { useState } from "react"; -import { useQueryClient } from "@tanstack/react-query"; -import { Trash2 } from "lucide-react"; -import { toast } from "sonner"; -import { showErrorToast } from "@/lib/errors/error-toast"; - -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; -import { Button } from "@/components/ui/button"; -import { IntakeEditDialog } from "@/components/medications/intake-edit-dialog"; -import { - IntakeHistoryListV2, - type IntakeEvent, - type IntakeHistorySortKey, -} from "@/components/medications/intake-history-list-v2"; -import { useTranslations } from "@/lib/i18n/context"; -import { invalidateMedicationReads } from "@/lib/query-keys"; -import { apiDelete, apiPost } from "@/lib/api/api-fetch"; - -export interface IntakeHistoryEditableProps { - medicationId: string; - /** Override the default page size — preview 14, full history 25. */ - pageSize?: number; - /** - * Initial sort column. The full-history view passes `"scheduledFor"` - * so the chronological order stays clean even for skipped rows - * (`takenAt: null`); the preview keeps the legacy `"takenAt"`. - */ - defaultSortBy?: IntakeHistorySortKey; -} - -export function IntakeHistoryEditable({ - medicationId, - pageSize, - defaultSortBy, -}: IntakeHistoryEditableProps) { - const { t } = useTranslations(); - // v1.36.x — bulk-deleting recorded doses is the owner's alone. - const { canManageDomain } = useRecordCapabilities(); - const canManageMedications = canManageDomain("medications"); - const queryClient = useQueryClient(); - - const [selected, setSelected] = useState>(new Set()); - const [editingEvent, setEditingEvent] = useState(null); - const [pendingDeleteId, setPendingDeleteId] = useState(null); - const [bulkConfirmOpen, setBulkConfirmOpen] = useState(false); - const [bulkBusy, setBulkBusy] = useState(false); - const [rowDeleteBusy, setRowDeleteBusy] = useState(false); - - function onToggle(id: string) { - setSelected((prev) => { - const next = new Set(prev); - if (next.has(id)) { - next.delete(id); - } else { - next.add(id); - } - return next; - }); - } - - function clearSelection() { - setSelected(new Set()); - } - - async function confirmBulkDelete() { - if (bulkBusy || selected.size === 0) return; - setBulkBusy(true); - const ids = Array.from(selected); - try { - await apiPost(`/api/medications/${medicationId}/intake/bulk-delete`, { - eventIds: ids, - }); - await invalidateMedicationReads(queryClient); - toast.success(t("medications.detail.intake.bulkDelete.toast")); - setBulkConfirmOpen(false); - clearSelection(); - } catch (failure) { - showErrorToast(t("medications.detail.intake.bulkDelete.failed"), { - cause: failure, - }); - } finally { - setBulkBusy(false); - } - } - - async function confirmRowDelete() { - if (rowDeleteBusy || !pendingDeleteId) return; - setRowDeleteBusy(true); - const id = pendingDeleteId; - try { - await apiDelete(`/api/medications/${medicationId}/intake/${id}`); - await invalidateMedicationReads(queryClient); - toast.success(t("medications.detail.intake.deleteRow.toast")); - setPendingDeleteId(null); - // Drop the deleted row from the pending selection so the - // bulk-toolbar count stays honest. - setSelected((prev) => { - if (!prev.has(id)) return prev; - const next = new Set(prev); - next.delete(id); - return next; - }); - } catch (failure) { - showErrorToast(t("medications.detail.intake.deleteRow.failed"), { - cause: failure, - }); - } finally { - setRowDeleteBusy(false); - } - } - - return ( - <> -
- {canManageMedications && selected.size > 0 && ( -
- - {t("medications.detail.intake.bulkDelete.selectionCount", { - count: selected.size, - })} - -
- - -
-
- )} - - -
- - setEditingEvent(null)} - /> - - { - if (!open) setBulkConfirmOpen(false); - }} - > - - - - {t("medications.detail.intake.bulkDelete.confirmTitle")} - - - {t("medications.detail.intake.bulkDelete.confirmBody", { - count: selected.size, - })} - - - - - {t("medications.detail.intake.bulkDelete.cancelButton")} - - { - e.preventDefault(); - void confirmBulkDelete(); - }} - disabled={bulkBusy} - aria-busy={bulkBusy || undefined} - className="bg-destructive dark:bg-destructive/60 hover:bg-destructive/90 text-white" - > - {t("medications.detail.intake.bulkDelete.confirmAction")} - - - - - - { - if (!open) setPendingDeleteId(null); - }} - > - - - - {t("medications.detail.intake.deleteRow.confirmTitle")} - - - {t("medications.detail.intake.deleteRow.confirmBody")} - - - - - {t("medications.detail.intake.bulkDelete.cancelButton")} - - { - e.preventDefault(); - void confirmRowDelete(); - }} - disabled={rowDeleteBusy} - aria-busy={rowDeleteBusy || undefined} - className="bg-destructive dark:bg-destructive/60 hover:bg-destructive/90 text-white" - > - {t("medications.detail.intake.deleteRow.confirmAction")} - - - - - - ); -} diff --git a/src/components/medications/sections/intake-history-preview.tsx b/src/components/medications/sections/intake-history-preview.tsx deleted file mode 100644 index 45a53e1fa..000000000 --- a/src/components/medications/sections/intake-history-preview.tsx +++ /dev/null @@ -1,74 +0,0 @@ -"use client"; - -/** - * v1.5.5 D-3 §9.5 — Intake-history preview. - * - * Section chrome via ``. Header CTA carries a - * CSV-import trigger. The body re-uses the shared - * `` (extracted in v1.7.0) so the table + sort + - * pagination + multi-select bulk-delete + per-row edit/delete stay - * identical between this preview and the full-history view. - * - * v1.7.0 — the footer "View full history →" link is gone; the detail - * header's History button now routes directly to the full view, so the - * redundant link is removed (R-medui §4.3). - */ - -import { useRecordCapabilities } from "@/hooks/use-record-capabilities"; -import { Upload } from "lucide-react"; - -import { Button } from "@/components/ui/button"; -import { IntakeHistoryEditable } from "@/components/medications/intake-history-editable"; -import { IntakeImportDialog } from "@/components/medications/intake-import-dialog"; -import { MedicationDetailSection } from "@/components/medications/medication-detail-section"; -import { useTranslations } from "@/lib/i18n/context"; - -export interface IntakeHistoryPreviewProps { - medicationId: string; - importOpen: boolean; - onImportOpenChange: (open: boolean) => void; -} - -export function IntakeHistoryPreview({ - medicationId, - importOpen, - onImportOpenChange, -}: IntakeHistoryPreviewProps) { - const { t } = useTranslations(); - // v1.36.x — importing dose history is not a delegated verb. - const { canManageDomain } = useRecordCapabilities(); - const canManageMedications = canManageDomain("medications"); - - return ( - <> - onImportOpenChange(true)} - className="min-h-11 sm:min-h-9" - data-slot="intake-history-import" - > - - - {importOpen && ( - onImportOpenChange(false)} - /> - )} - - ); -}