From 604778f1666b151cdd40d44e7d191f0eda0655f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 15 Jun 2026 23:06:45 +0200 Subject: [PATCH] feat(settings): make Benachrichtigungen the single module-gated reminder-types home MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidate every reminder TYPE onto the Notifications screen and drop the redundant stacked breadcrumb for one clean heading. Each reminder-type card is shown only when its module is enabled, read from the resolved useAuth().user.modules map (mood card on mood, Coach nudge on coach); the gate fails open so a stale /me payload never blanks a card. Low-stock maps to the medications CORE domain and Vorsorge (preventive care) is not a toggleable module, so both stay shown — the Vorsorge editor embeds in place. The delivery channels keep a single concise pointer to Integrations rather than a duplicated section. Reword the heading copy and drop the now-unused settings.sections.reminders.* keys across all six locales. --- messages/de.json | 35 +----- messages/en.json | 35 +----- messages/es.json | 35 +----- messages/fr.json | 35 +----- messages/it.json | 35 +----- messages/pl.json | 35 +----- ...tifications-section-module-gating.test.tsx | 84 +++++++++++++ .../settings/__tests__/sections.test.tsx | 22 +++- .../settings/notifications-section.tsx | 110 ++++++++++-------- 9 files changed, 180 insertions(+), 246 deletions(-) create mode 100644 src/components/settings/__tests__/notifications-section-module-gating.test.tsx diff --git a/messages/de.json b/messages/de.json index 8bd10c0ba..395fc13e3 100644 --- a/messages/de.json +++ b/messages/de.json @@ -3871,9 +3871,10 @@ }, "notifications": { "title": "Benachrichtigungen", - "description": "Welche Erinnerungen ausgelöst werden. Die Zustellkanäle liegen unter Integrationen.", + "description": "Alle Erinnerungen an einem Ort — was wann erinnert wird. Die Zustellkanäle liegen unter Integrationen.", "channelsHint": "Telegram, ntfy, Web-Push, Webhook und E-Mail richtest du jetzt unter", - "channelsHintLink": "Integrationen → Kanäle" + "channelsHintLink": "Integrationen → Kanäle", + "inboxLink": "Posteingang öffnen" }, "layout": { "title": "Layout & Personalisierung", @@ -4106,36 +4107,6 @@ "title": "Stimmungs-Tags", "description": "Gruppen, Tags und Reihenfolge der Tag-Auswahl." }, - "reminders": { - "title": "Erinnerungen & Benachrichtigungen", - "description": "Ein Ort für jede Erinnerung — woran und wann erinnert wird — und die Kanäle, die sie zustellen.", - "categoriesGroup": "Erinnerungen", - "channelsGroup": "Zustellkanäle", - "medication": { - "title": "Medikamenten-Erinnerungen", - "description": "Einnahmezeiten und Zeitfenster, je Medikament eingestellt." - }, - "vorsorge": { - "title": "Vorsorge", - "description": "Wiederkehrende Untersuchungen und Messungen, inklusive Ort." - }, - "mood": { - "title": "Stimmungs-Check-in", - "description": "Eine tägliche Erinnerung, dein Befinden zu erfassen." - }, - "lowStock": { - "title": "Reichweite bei Restbestand", - "description": "Werde gewarnt, bevor ein Medikament zur Neige geht." - }, - "coach": { - "title": "Coach-Hinweis", - "description": "Proaktive Impulse vom KI-Coach." - }, - "channels": { - "title": "Benachrichtigungskanäle", - "description": "APNs, Telegram, ntfy, Web Push, Webhook und E-Mail." - } - }, "modules": { "title": "Module", "saved": "Gespeichert.", diff --git a/messages/en.json b/messages/en.json index d63cc8d01..61f6d3588 100644 --- a/messages/en.json +++ b/messages/en.json @@ -3871,9 +3871,10 @@ }, "notifications": { "title": "Notifications", - "description": "Which reminders fire. The delivery channels live under Integrations.", + "description": "Every reminder in one place — what gets reminded and when. The delivery channels live under Integrations.", "channelsHint": "Telegram, ntfy, Web Push, Webhook, and Email are now set up under", - "channelsHintLink": "Integrations → Channels" + "channelsHintLink": "Integrations → Channels", + "inboxLink": "Open the inbox" }, "layout": { "title": "Layout & Personalization", @@ -4106,36 +4107,6 @@ "title": "Mood tags", "description": "Groups, tags, and order of the mood picker." }, - "reminders": { - "title": "Reminders & Notifications", - "description": "One home for every reminder — what gets reminded and when — plus the channels that deliver them.", - "categoriesGroup": "Reminders", - "channelsGroup": "Delivery channels", - "medication": { - "title": "Medication reminders", - "description": "Dose times and windows, configured on each medication." - }, - "vorsorge": { - "title": "Preventive care", - "description": "Recurring check-ups and measurements, with where to go." - }, - "mood": { - "title": "Mood check-in", - "description": "A daily nudge to log how you feel." - }, - "lowStock": { - "title": "Low-stock runway", - "description": "Get warned before a medication runs out." - }, - "coach": { - "title": "Coach nudge", - "description": "Proactive prompts from the AI coach." - }, - "channels": { - "title": "Notification channels", - "description": "APNs, Telegram, ntfy, Web Push, Webhook, and Email." - } - }, "modules": { "title": "Modules", "saved": "Saved.", diff --git a/messages/es.json b/messages/es.json index 58fb8e67b..b9f3b6c4e 100644 --- a/messages/es.json +++ b/messages/es.json @@ -3871,9 +3871,10 @@ }, "notifications": { "title": "Notificaciones", - "description": "Qué recordatorios se activan. Los canales de entrega están en Integraciones.", + "description": "Todos los recordatorios en un solo lugar: qué se recuerda y cuándo. Los canales de entrega están en Integraciones.", "channelsHint": "Telegram, ntfy, Web Push, Webhook y correo se configuran ahora en", - "channelsHintLink": "Integraciones → Canales" + "channelsHintLink": "Integraciones → Canales", + "inboxLink": "Abrir la bandeja de entrada" }, "layout": { "title": "Diseño y personalización", @@ -4106,36 +4107,6 @@ "title": "Etiquetas de ánimo", "description": "Grupos, etiquetas y orden del selector de ánimo." }, - "reminders": { - "title": "Recordatorios y notificaciones", - "description": "Un único lugar para cada recordatorio — qué se recuerda y cuándo — y los canales que lo entregan.", - "categoriesGroup": "Recordatorios", - "channelsGroup": "Canales de entrega", - "medication": { - "title": "Recordatorios de medicación", - "description": "Horas y ventanas de dosis, configuradas en cada medicamento." - }, - "vorsorge": { - "title": "Cuidado preventivo", - "description": "Revisiones y mediciones periódicas, con el lugar al que acudir." - }, - "mood": { - "title": "Registro de ánimo", - "description": "Un aviso diario para registrar cómo te sientes." - }, - "lowStock": { - "title": "Margen por bajo stock", - "description": "Recibe un aviso antes de que se acabe un medicamento." - }, - "coach": { - "title": "Sugerencia del coach", - "description": "Indicaciones proactivas del coach de IA." - }, - "channels": { - "title": "Canales de notificación", - "description": "APNs, Telegram, ntfy, Web Push, Webhook y correo." - } - }, "modules": { "title": "Módulos", "saved": "Guardado.", diff --git a/messages/fr.json b/messages/fr.json index 90f37fe30..2014175bc 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -3871,9 +3871,10 @@ }, "notifications": { "title": "Notifications", - "description": "Quels rappels se déclenchent. Les canaux de diffusion se trouvent dans Intégrations.", + "description": "Tous les rappels au même endroit — quoi et quand. Les canaux de diffusion se trouvent dans Intégrations.", "channelsHint": "Telegram, ntfy, Web Push, Webhook et e-mail se configurent désormais dans", - "channelsHintLink": "Intégrations → Canaux" + "channelsHintLink": "Intégrations → Canaux", + "inboxLink": "Ouvrir la boîte de réception" }, "layout": { "title": "Disposition et personnalisation", @@ -4106,36 +4107,6 @@ "title": "Étiquettes d’humeur", "description": "Groupes, étiquettes et ordre du sélecteur d’humeur." }, - "reminders": { - "title": "Rappels et notifications", - "description": "Un seul endroit pour chaque rappel — quoi et quand — ainsi que les canaux qui les délivrent.", - "categoriesGroup": "Rappels", - "channelsGroup": "Canaux de diffusion", - "medication": { - "title": "Rappels de médication", - "description": "Heures et fenêtres de prise, configurées sur chaque médicament." - }, - "vorsorge": { - "title": "Prévention", - "description": "Bilans et mesures récurrents, avec le lieu où se rendre." - }, - "mood": { - "title": "Suivi de l'humeur", - "description": "Un rappel quotidien pour noter votre ressenti." - }, - "lowStock": { - "title": "Marge de stock faible", - "description": "Soyez averti avant qu'un médicament ne s'épuise." - }, - "coach": { - "title": "Suggestion du coach", - "description": "Invitations proactives du coach IA." - }, - "channels": { - "title": "Canaux de notification", - "description": "APNs, Telegram, ntfy, Web Push, Webhook et e-mail." - } - }, "modules": { "title": "Modules", "saved": "Enregistré.", diff --git a/messages/it.json b/messages/it.json index 78fbc6e9e..9c9d306b4 100644 --- a/messages/it.json +++ b/messages/it.json @@ -3871,9 +3871,10 @@ }, "notifications": { "title": "Notifiche", - "description": "Quali promemoria vengono attivati. I canali di recapito si trovano in Integrazioni.", + "description": "Tutti i promemoria in un unico posto — cosa e quando. I canali di recapito si trovano in Integrazioni.", "channelsHint": "Telegram, ntfy, Web Push, Webhook ed e-mail ora si configurano in", - "channelsHintLink": "Integrazioni → Canali" + "channelsHintLink": "Integrazioni → Canali", + "inboxLink": "Apri la posta in arrivo" }, "layout": { "title": "Layout e personalizzazione", @@ -4106,36 +4107,6 @@ "title": "Tag dell’umore", "description": "Gruppi, tag e ordine del selettore dell’umore." }, - "reminders": { - "title": "Promemoria e notifiche", - "description": "Un'unica sede per ogni promemoria — cosa ricordare e quando — e i canali che li recapitano.", - "categoriesGroup": "Promemoria", - "channelsGroup": "Canali di consegna", - "medication": { - "title": "Promemoria dei farmaci", - "description": "Orari e finestre delle dosi, impostati su ogni farmaco." - }, - "vorsorge": { - "title": "Prevenzione", - "description": "Controlli e misurazioni ricorrenti, con il luogo dove recarsi." - }, - "mood": { - "title": "Check-in dell'umore", - "description": "Un promemoria quotidiano per registrare come ti senti." - }, - "lowStock": { - "title": "Autonomia scorte", - "description": "Ricevi un avviso prima che un farmaco finisca." - }, - "coach": { - "title": "Suggerimento del coach", - "description": "Stimoli proattivi dal coach IA." - }, - "channels": { - "title": "Canali di notifica", - "description": "APNs, Telegram, ntfy, Web Push, Webhook ed e-mail." - } - }, "modules": { "title": "Moduli", "saved": "Salvato.", diff --git a/messages/pl.json b/messages/pl.json index c85a7106f..d4144723f 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -3871,9 +3871,10 @@ }, "notifications": { "title": "Powiadomienia", - "description": "Które przypomnienia są uruchamiane. Kanały dostarczania znajdują się w Integracjach.", + "description": "Wszystkie przypomnienia w jednym miejscu — co i kiedy. Kanały dostarczania znajdują się w Integracjach.", "channelsHint": "Telegram, ntfy, Web Push, Webhook i e-mail konfigurujesz teraz w", - "channelsHintLink": "Integracje → Kanały" + "channelsHintLink": "Integracje → Kanały", + "inboxLink": "Otwórz skrzynkę odbiorczą" }, "layout": { "title": "Układ i personalizacja", @@ -4106,36 +4107,6 @@ "title": "Tagi nastroju", "description": "Grupy, tagi i kolejność w wyborze nastroju." }, - "reminders": { - "title": "Przypomnienia i powiadomienia", - "description": "Jedno miejsce dla każdego przypomnienia — o czym i kiedy — oraz kanały, które je dostarczają.", - "categoriesGroup": "Przypomnienia", - "channelsGroup": "Kanały dostarczania", - "medication": { - "title": "Przypomnienia o lekach", - "description": "Godziny i okna dawek, ustawiane przy każdym leku." - }, - "vorsorge": { - "title": "Profilaktyka", - "description": "Cykliczne badania i pomiary wraz z miejscem, gdzie się udać." - }, - "mood": { - "title": "Zapis nastroju", - "description": "Codzienne przypomnienie, by zapisać samopoczucie." - }, - "lowStock": { - "title": "Zapas na wyczerpaniu", - "description": "Otrzymaj ostrzeżenie, zanim lek się skończy." - }, - "coach": { - "title": "Podpowiedź trenera", - "description": "Proaktywne wskazówki od trenera AI." - }, - "channels": { - "title": "Kanały powiadomień", - "description": "APNs, Telegram, ntfy, Web Push, Webhook i e-mail." - } - }, "modules": { "title": "Moduły", "saved": "Zapisano.", diff --git a/src/components/settings/__tests__/notifications-section-module-gating.test.tsx b/src/components/settings/__tests__/notifications-section-module-gating.test.tsx new file mode 100644 index 000000000..185d6b65e --- /dev/null +++ b/src/components/settings/__tests__/notifications-section-module-gating.test.tsx @@ -0,0 +1,84 @@ +/** + * v1.18.0 (S4) — module-gated per-type visibility on the consolidated + * "Benachrichtigungen" reminder-types home. + * + * The mood reminder shows only when the `mood` module is enabled; the Coach + * nudge only when `coach` is enabled. Low-stock maps to the medications CORE + * domain (always shown). The gate fails OPEN, so a `/me` payload without a + * module map keeps every card visible. + */ +import { describe, expect, it, vi } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ push: vi.fn(), replace: vi.fn() }), + usePathname: () => "/settings/notifications", + useSearchParams: () => new URLSearchParams(), +})); + +vi.mock("@tanstack/react-query", () => ({ + useQuery: () => ({ data: null, isLoading: false, refetch: vi.fn() }), + useQueryClient: () => ({ invalidateQueries: vi.fn() }), + useMutation: () => ({ mutate: vi.fn(), isPending: false }), +})); + +// The auth mock is driven per-test via a mutable ref so each render can pick +// a different module map without re-mocking the module. +const authRef: { modules?: Record } = {}; +vi.mock("@/hooks/use-auth", () => ({ + useAuth: () => ({ + user: { id: "u1", username: "t", role: "USER", modules: authRef.modules }, + isAuthenticated: true, + isLoading: false, + refetch: vi.fn(), + }), +})); + +import { I18nProvider } from "@/lib/i18n/context"; +import { NotificationsSection } from "../notifications-section"; + +function render(modules?: Record) { + authRef.modules = modules; + return renderToStaticMarkup( + + + , + ); +} + +describe(" module-gated reminder types", () => { + it("shows mood + coach cards when no module map (fails open)", () => { + const html = render(undefined); + expect(html).toContain('id="mood-reminder"'); + expect(html).toContain('id="coach-nudge"'); + expect(html).toContain('id="low-stock"'); + }); + + it("hides the mood card when the mood module is disabled", () => { + const html = render({ mood: false }); + expect(html).not.toContain('id="mood-reminder"'); + // Coach + low-stock are unaffected. + expect(html).toContain('id="coach-nudge"'); + expect(html).toContain('id="low-stock"'); + }); + + it("hides the coach card when the coach module is disabled", () => { + const html = render({ coach: false }); + expect(html).not.toContain('id="coach-nudge"'); + expect(html).toContain('id="mood-reminder"'); + expect(html).toContain('id="low-stock"'); + }); + + it("always shows low-stock (medications is a CORE domain)", () => { + const html = render({ mood: false, coach: false }); + expect(html).toContain('id="low-stock"'); + expect(html).not.toContain('id="mood-reminder"'); + expect(html).not.toContain('id="coach-nudge"'); + }); + + it("shows the channels pointer (delivery channels live under Integrations)", () => { + const html = render({}); + expect(html).toContain('data-slot="notifications-channels-cross-link"'); + expect(html).toContain('href="/settings/integrations"'); + }); +}); diff --git a/src/components/settings/__tests__/sections.test.tsx b/src/components/settings/__tests__/sections.test.tsx index d1d436bff..fab34abc4 100644 --- a/src/components/settings/__tests__/sections.test.tsx +++ b/src/components/settings/__tests__/sections.test.tsx @@ -182,12 +182,24 @@ describe("settings sections — SSR smoke", () => { expect(html).toContain("Withings"); }); - it(" renders heading", () => { + it(" renders the single reminder-types home", () => { const html = render(); - // v1.4.33 IW7 — section renamed from "Notifications" to - // "Notification channels" so it doesn't collide with the inbox at - // `/notifications` ("Notifications"). - expect(html).toContain("Notification channels"); + // v1.18.0 (S4) — one clean heading (sr-only h1 + visible description), + // no stacked breadcrumb. The screen is the single module-gated home for + // reminder TYPES. + expect(html).toContain("settings-section-notifications-title"); + // The old stacked breadcrumb middle crumb is gone. + expect(html).not.toContain("Notification channels"); + // A single concise pointer to the channels (now under Integrations). + expect(html).toContain('href="/settings/integrations"'); + expect(html).toContain('data-slot="notifications-channels-cross-link"'); + // The mood + coach cards fail OPEN (mock user carries no module map), + // so both reminder-type cards render. + expect(html).toContain('id="mood-reminder"'); + expect(html).toContain('id="coach-nudge"'); + expect(html).toContain('id="low-stock"'); + // No raw i18n key leaks past the provider. + expect(html).not.toContain("settings.sections.notifications."); }); it(" renders heading and embeds the layout customizer", () => { diff --git a/src/components/settings/notifications-section.tsx b/src/components/settings/notifications-section.tsx index 35add96c8..d57d23b4b 100644 --- a/src/components/settings/notifications-section.tsx +++ b/src/components/settings/notifications-section.tsx @@ -1,72 +1,63 @@ "use client"; import Link from "next/link"; -import { ChevronRight } from "lucide-react"; import { useAuth } from "@/hooks/use-auth"; import { useTranslations } from "@/lib/i18n/context"; import { CoachNudgeCard } from "@/components/settings/coach-nudge-card"; import { LowStockCard } from "@/components/settings/low-stock-card"; import { MoodReminderCard } from "@/components/settings/mood-reminder-card"; +import { VorsorgeSection } from "@/components/measurement-reminders/vorsorge-section"; /** - * `` — Settings → Notifications. + * `` — Settings → Notifications ("Benachrichtigungen"). * - * v1.18.0 (S3) — the delivery CHANNELS (Telegram / ntfy / Web Push / Webhook / - * Email + the live per-channel status surface) moved to Settings → - * Integrations → Channels: a channel is a delivery provider, the same family as - * a connected device, not a notification preference. What stays here is the - * reminder-TYPE content (which events fire): mood reminder, medication - * low-stock runway, proactive Coach nudge. S4 will consolidate these onto the - * Reminders hub. + * v1.18.0 (S4) — the single module-gated home for reminder TYPES. Earlier the + * settings carried two near-identical entries: this Notifications screen and a + * separate "Erinnerungen" hub that only deep-linked into the canonical editors + * ("doppelt gemoppelt"). The hub is gone (its route 301-redirects here); the + * reminder TYPES now live centrally on this one screen. + * + * Each reminder-type card is shown ONLY when its module is enabled, read from + * the resolved `useAuth().user.modules` map (the same `/auth/me` map nav and + * Insights pills gate off). Mood → `mood`, Coach nudge → `coach`. Low-stock + * runway maps to the medications CORE domain, so it is always shown. Vorsorge + * (preventive-care) reminders are not a toggleable module, so they are always + * shown. The gate fails OPEN: a missing key reads as enabled, so a stale + * `/me` payload never blanks a card. + * + * Delivery CHANNELS (Telegram / ntfy / Web Push / Webhook / Email) are a + * different concept — a channel is a delivery provider, the same family as a + * connected device — and live under Settings → Integrations → Channels (moved + * there in S3). A single concise pointer below the heading links to them. */ export function NotificationsSection() { const { t } = useTranslations(); - const { isAuthenticated } = useAuth(); + const { isAuthenticated, user } = useAuth(); + + // v1.18.0 (S4) — module-gated per-type visibility. Fail OPEN (`!== false`) + // so a stale `/me` payload without the module map keeps every card visible. + const moodEnabled = user?.modules?.mood !== false; + const coachEnabled = user?.modules?.coach !== false; return (
- {/* v1.4.33 IW7 — disambiguate this settings screen from the inbox at - `/notifications`. The crumb spells out where the user is and offers a - one-tap jump to the inbox. */} -
+ {/* sr-only h1 — the visible section title is painted by the settings + shell nav; the in-page heading mirrors every other settings + section's scaffold so the screen reads as one of the family. */}

{t("settings.sections.notifications.title")}

{t("settings.sections.notifications.description")}

- {/* v1.18.0 (S3) — the delivery channels now live under Integrations. - Cross-link so a user looking for "where do I connect Telegram" - finds them in one tap. */} + {/* One concise pointer to the delivery channels (now under + Integrations) and one to the notification inbox — not a duplicated + section, just where-to-go links. */}

{t("settings.sections.notifications.channelsHint")}{" "} {t("settings.sections.notifications.channelsHintLink")} + {" · "} + + {t("settings.sections.notifications.inboxLink")} +

-
- -
- {/* v1.16.11 — medication low-stock runway threshold. */} + {/* Vorsorge (preventive-care) reminders — always shown; not a + toggleable module. The canonical in-place editor, embedded here so + "what gets reminded and when" reads as one screen. */} + + + {/* Mood check-in reminder — only when the mood module is enabled. */} + {moodEnabled ? ( +
+ +
+ ) : null} + + {/* Medication low-stock runway — medications is a CORE domain, so this + is always shown. */}
- {/* v1.15.20 — proactive Coach nudge opt-out. */} -
- -
+ + {/* Proactive Coach nudge — only when the coach module is enabled. */} + {coachEnabled ? ( +
+ +
+ ) : null}
); }