mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
feat(settings): make Benachrichtigungen the single module-gated reminder-types home
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.
This commit is contained in:
1 parent
fccdcb378e
commit
604778f166
9 files changed
+180
-246
No files matched your search
@@ -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<string, boolean> } = {};
|
||||
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<string, boolean>) {
|
||||
authRef.modules = modules;
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<NotificationsSection />
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("<NotificationsSection> 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"');
|
||||
});
|
||||
});
|
||||
@@ -182,12 +182,24 @@ describe("settings sections — SSR smoke", () => {
|
||||
expect(html).toContain("Withings");
|
||||
});
|
||||
|
||||
it("<NotificationsSection> renders heading", () => {
|
||||
it("<NotificationsSection> renders the single reminder-types home", () => {
|
||||
const html = render(<NotificationsSection />);
|
||||
// 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("<DashboardSection> renders heading and embeds the layout customizer", () => {
|
||||
|
||||
@@ -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";
|
||||
|
||||
/**
|
||||
* `<NotificationsSection>` — Settings → Notifications.
|
||||
* `<NotificationsSection>` — 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 (
|
||||
<section
|
||||
aria-labelledby="settings-section-notifications-title"
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* 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. */}
|
||||
<nav
|
||||
aria-label={t("nav.breadcrumb")}
|
||||
className="text-muted-foreground text-xs"
|
||||
>
|
||||
<ol className="flex items-center gap-1">
|
||||
<li>{t("notifications.breadcrumbSettings")}</li>
|
||||
<li aria-hidden="true">
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
</li>
|
||||
<li className="text-foreground font-medium">
|
||||
{t("notifications.breadcrumbChannels")}
|
||||
</li>
|
||||
<li aria-hidden="true">
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/notifications"
|
||||
className="hover:text-foreground underline-offset-2 hover:underline"
|
||||
>
|
||||
{t("notifications.breadcrumbInbox")}
|
||||
</Link>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<header className="space-y-1">
|
||||
{/* 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. */}
|
||||
<h1 id="settings-section-notifications-title" className="sr-only">
|
||||
{t("settings.sections.notifications.title")}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("settings.sections.notifications.description")}
|
||||
</p>
|
||||
{/* 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. */}
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.sections.notifications.channelsHint")}{" "}
|
||||
<Link
|
||||
@@ -76,20 +67,41 @@ export function NotificationsSection() {
|
||||
>
|
||||
{t("settings.sections.notifications.channelsHintLink")}
|
||||
</Link>
|
||||
{" · "}
|
||||
<Link
|
||||
href="/notifications"
|
||||
className="text-primary underline underline-offset-2"
|
||||
data-slot="notifications-inbox-cross-link"
|
||||
>
|
||||
{t("settings.sections.notifications.inboxLink")}
|
||||
</Link>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div id="mood-reminder" className="scroll-mt-28">
|
||||
<MoodReminderCard isAuthenticated={isAuthenticated} />
|
||||
</div>
|
||||
{/* 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. */}
|
||||
<VorsorgeSection enabled={isAuthenticated} />
|
||||
|
||||
{/* Mood check-in reminder — only when the mood module is enabled. */}
|
||||
{moodEnabled ? (
|
||||
<div id="mood-reminder" className="scroll-mt-28">
|
||||
<MoodReminderCard isAuthenticated={isAuthenticated} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Medication low-stock runway — medications is a CORE domain, so this
|
||||
is always shown. */}
|
||||
<div id="low-stock" className="scroll-mt-28">
|
||||
<LowStockCard isAuthenticated={isAuthenticated} />
|
||||
</div>
|
||||
{/* v1.15.20 — proactive Coach nudge opt-out. */}
|
||||
<div id="coach-nudge" className="scroll-mt-28">
|
||||
<CoachNudgeCard isAuthenticated={isAuthenticated} />
|
||||
</div>
|
||||
|
||||
{/* Proactive Coach nudge — only when the coach module is enabled. */}
|
||||
{coachEnabled ? (
|
||||
<div id="coach-nudge" className="scroll-mt-28">
|
||||
<CoachNudgeCard isAuthenticated={isAuthenticated} />
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user