diff --git a/src/components/settings/__tests__/layout-section.test.tsx b/src/components/settings/__tests__/layout-section.test.tsx index c4c9a35ed..12e9be175 100644 --- a/src/components/settings/__tests__/layout-section.test.tsx +++ b/src/components/settings/__tests__/layout-section.test.tsx @@ -20,17 +20,14 @@ function render(locale: "en" | "de" = "en") { } describe("", () => { - it("links to the personalization editors it still hosts", () => { + it("links to the arrangement editors it still hosts", () => { const html = render(); - for (const href of [ - "/settings/dashboard", - "/settings/insights", - "/settings/medications", - ]) { + for (const href of ["/settings/dashboard", "/settings/insights"]) { expect(html).toContain(`href="${href}"`); } - // v1.18.0 (S5) — Mood (Stimmung) graduated to its own module-gated - // nav entry and is no longer linked from the Layout hub. + // v1.18.0 (S5) — Medications (Medikamente) and Mood (Stimmung) + // graduated to their own nav entries and are no longer linked here. + expect(html).not.toContain('href="/settings/medications"'); expect(html).not.toContain('href="/settings/mood"'); }); @@ -38,11 +35,11 @@ describe("", () => { const html = render(); expect(html).toContain("Dashboard"); expect(html).toContain("Insights"); - expect(html).toContain("Medications"); }); it("resolves its copy in German too", () => { const html = render("de"); - expect(html).toContain("Medikamente"); + expect(html).toContain("Layout"); + expect(html).not.toContain("settings.sections.layout."); }); }); diff --git a/src/components/settings/__tests__/medications-section.test.tsx b/src/components/settings/__tests__/medications-section.test.tsx index 901ed17ef..b66aee43f 100644 --- a/src/components/settings/__tests__/medications-section.test.tsx +++ b/src/components/settings/__tests__/medications-section.test.tsx @@ -36,7 +36,13 @@ vi.mock("@tanstack/react-query", () => ({ } return { data: null, isLoading: false }; }, - useQueryClient: () => ({ setQueryData: vi.fn(), getQueryData: vi.fn() }), + useQueryClient: () => ({ + setQueryData: vi.fn(), + getQueryData: vi.fn(), + invalidateQueries: vi.fn(), + }), + // v1.18.0 (S5) — the gathered injection-sites card uses `useMutation`. + useMutation: () => ({ mutate: vi.fn(), isPending: false }), })); vi.mock("sonner", () => ({ @@ -47,6 +53,17 @@ vi.mock("@/lib/charts/reduced-motion", () => ({ prefersReducedMotion: () => true, })); +// v1.18.0 (S5) — the section now reads `useAuth()` to gate the +// injection-sites card it gathered from the account profile. +vi.mock("@/hooks/use-auth", () => ({ + useAuth: () => ({ + user: { id: "u1", role: "USER" }, + isAuthenticated: true, + isLoading: false, + refetch: vi.fn(), + }), +})); + import { I18nProvider } from "@/lib/i18n/context"; import { MedicationsSection } from "../medications-section"; @@ -111,6 +128,13 @@ describe(" — SSR smoke", () => { expect(lastActive).toBeLessThan(html.indexOf("Amoxicillin")); }); + it("gathers the injection-sites card (moved from the account profile)", () => { + // v1.18.0 (S5) — injection-site exclusions are a medication setting and + // now live on this screen alongside the list view + order. + const html = render(); + expect(html).toContain("Globally excluded injection sites"); + }); + it("feeds the editor the saved order (active block re-sorted by layout.order)", () => { // Alphabetical default would put Aspirin first; the saved order pins // Ramipril to the top — the editor must open showing exactly what diff --git a/src/components/settings/__tests__/settings-shell.test.tsx b/src/components/settings/__tests__/settings-shell.test.tsx index 09f1e65c4..6576ce5c3 100644 --- a/src/components/settings/__tests__/settings-shell.test.tsx +++ b/src/components/settings/__tests__/settings-shell.test.tsx @@ -177,11 +177,7 @@ describe("", () => { // On `/settings/dashboard` (a Layout child reached through the hub) the // Layout nav entry must read active even though Dashboard has no nav // entry of its own. Both layouts emit the active link → two matches. - for (const child of [ - "/settings/dashboard", - "/settings/insights", - "/settings/medications", - ]) { + for (const child of ["/settings/dashboard", "/settings/insights"]) { const html = renderShell({ pathname: child }); const layoutActive = /]*\baria-current="page"[^>]*\bhref="\/settings\/layout"|]*\bhref="\/settings\/layout"[^>]*\baria-current="page"/g; @@ -193,8 +189,8 @@ describe("", () => { // The page passes `active={section}` explicitly; a child slug must // still resolve onto the Layout hub for nav highlighting. const html = renderShell({ - active: "medications", - pathname: "/settings/medications", + active: "dashboard", + pathname: "/settings/dashboard", }); const layoutActive = /]*\baria-current="page"[^>]*\bhref="\/settings\/layout"|]*\bhref="\/settings\/layout"[^>]*\baria-current="page"/g; @@ -238,15 +234,14 @@ describe("", () => { // (single-line; the longer "Notification channels" wrapped). The // `/notifications` inbox now shares the "Notifications" label. expect(html).toContain("Notifications"); - // v1.17.1 (F-2) — the personalization editors (Dashboard, Insights, - // Medications) are reached through one Layout hub entry; the hub - // itself is the single nav entry for that concept. + // v1.17.1 (F-2) — the dashboard + insights arrangement editors are + // reached through one Layout hub entry. expect(html).toContain('href="/settings/layout"'); expect(html).toContain("Layout & Personalization"); - // Dashboard / Insights / Medications stay Layout-hub children. - expect(html).not.toContain('href="/settings/medications"'); - // v1.18.0 (S5) — Mood (Stimmung) graduated to its own module-gated - // nav entry. With the default fail-open mock it renders. + // v1.18.0 (S5) — Medications (Medikamente, always shown — a CORE + // domain) and Mood (Stimmung, gated; fail-open mock shows it) are their + // own nav entries now. + expect(html).toContain('href="/settings/medications"'); expect(html).toContain('href="/settings/mood"'); // The ampersand is HTML-escaped by React SSR — assert on the encoded // form so we don't accidentally match a parser that double-escapes. @@ -276,12 +271,12 @@ describe("", () => { // compound "Benachrichtigungs-Kanäle" wrapped). The `/notifications` // inbox now shares the "Benachrichtigungen" label. expect(html).toContain("Benachrichtigungen"); - // v1.17.1 (F-2) — the personalization editors are reached through one + // v1.17.1 (F-2) — the arrangement editors are reached through one // Layout hub entry ("Layout & Personalisierung"). expect(html).toContain('href="/settings/layout"'); expect(html).toContain("Layout & Personalisierung"); - expect(html).not.toContain('href="/settings/medications"'); - // v1.18.0 (S5) — Mood (Stimmung) is its own nav entry now. + // v1.18.0 (S5) — Medications + Mood are their own nav entries now. + expect(html).toContain('href="/settings/medications"'); expect(html).toContain('href="/settings/mood"'); // v1.18.0 (S3) — Targets keeps its German nav entry ("Zielwerte"); // Sources folded into Integrations as a sub-tab. diff --git a/src/components/settings/account-section.tsx b/src/components/settings/account-section.tsx index 4d909f1bb..72ce6cdc3 100644 --- a/src/components/settings/account-section.tsx +++ b/src/components/settings/account-section.tsx @@ -53,7 +53,6 @@ import { SettingsCardHeader } from "@/components/settings/_card-header"; import { TimezonePicker } from "@/components/settings/timezone-picker"; import { TimeFormatSelect } from "@/components/settings/time-format-select"; import { UnitPreferenceSelect } from "@/components/settings/unit-preference-select"; -import { InjectionSitesCard } from "@/components/settings/injection-sites-card"; import { CycleTrackingCard } from "@/components/settings/cycle-tracking-card"; import { detectBrowserTimezone, DEFAULT_TIMEZONE } from "@/lib/tz/format"; import { apiDelete, apiFetchRaw, apiGet } from "@/lib/api/api-fetch"; @@ -633,11 +632,9 @@ export function AccountSection() { - {/* Global injection-site exclusions live with the account - profile — a personal preference that applies app-wide. The - unit system moved into the profile form above, beside the - timezone dropdown. */} - + {/* v1.18.0 (S5) — injection-site exclusions moved to the dedicated + Medikamente settings section, where every medication-specific + preference now lives. */} {/* Cycle-tracking enable on-ramp — auto-on for female accounts, but this lets any account opt in (or opt out) before the gated /cycle page is diff --git a/src/components/settings/layout-section.tsx b/src/components/settings/layout-section.tsx index 8ed42237e..cf4474ec4 100644 --- a/src/components/settings/layout-section.tsx +++ b/src/components/settings/layout-section.tsx @@ -4,7 +4,6 @@ import Link from "next/link"; import { ChevronRight, LayoutDashboard, - Pill, TrendingUp, type LucideIcon, } from "lucide-react"; @@ -15,13 +14,15 @@ import { useTranslations } from "@/lib/i18n/context"; /** * v1.17.1 (F-2) — the one "Layout & Personalization" home. * - * Before this section, "how my app is laid out" was scattered across four - * unrelated settings sections — Dashboard, Insights, Medications, Mood — - * with no shared framing, so the same concept (arrange / reorder / show- - * hide) read as four disconnected places. This section is the single - * front door: it presents the four personalization surfaces as one - * consistent set of cards and links to each editor, which all keep their - * own routes and mutation flows intact. One concept, one home. + * Before this section, "how my app is laid out" was scattered across + * several unrelated settings sections with no shared framing. This + * section is the single front door for the arrangement editors: it + * presents them as one consistent set of cards and links to each editor, + * which keep their own routes and mutation flows intact. + * + * v1.18.0 (S5) — Medications (Medikamente) and Mood (Stimmung) graduated + * to their own top-level nav entries, so the hub now hosts only the + * dashboard + insights arrangement editors. */ interface LayoutLink { href: string; @@ -43,14 +44,10 @@ const LAYOUT_LINKS: ReadonlyArray = [ titleKey: "settings.sections.layout.insights.title", descriptionKey: "settings.sections.layout.insights.description", }, - { - href: "/settings/medications", - icon: Pill, - titleKey: "settings.sections.layout.medications.title", - descriptionKey: "settings.sections.layout.medications.description", - }, - // v1.18.0 (S5) — Mood (Stimmung) graduated to its own top-level, - // module-gated nav entry, so it is no longer linked from the Layout hub. + // v1.18.0 (S5) — Medications (Medikamente) and Mood (Stimmung) graduated + // to their own top-level nav entries, so they are no longer linked from + // the Layout hub. Only the dashboard + insights arrangement editors + // remain here. ]; export function LayoutSection() { diff --git a/src/components/settings/medications-section.tsx b/src/components/settings/medications-section.tsx index 224d0817a..966c00174 100644 --- a/src/components/settings/medications-section.tsx +++ b/src/components/settings/medications-section.tsx @@ -8,7 +8,8 @@ import { type ReorderMedication, } from "@/components/medications/medication-order-editor"; import { MedicationViewToggle } from "@/components/medications/medication-view-toggle"; -import { SettingsHubBackLink } from "@/components/settings/settings-hub-back-link"; +import { InjectionSitesCard } from "@/components/settings/injection-sites-card"; +import { useAuth } from "@/hooks/use-auth"; import { apiGet } from "@/lib/api/api-fetch"; import { useTranslations } from "@/lib/i18n/context"; import { applyMedicationOrder } from "@/lib/medications/medication-order"; @@ -16,17 +17,21 @@ import { queryKeys } from "@/lib/query-keys"; import { useMedicationListLayout } from "@/lib/queries/use-medication-list-layout"; /** - * v1.16.10 — the "Medikamente" settings section (analogous to the - * Dashboard and Insights sections). Hosts two blocks: + * v1.16.10 — the "Medikamente" settings section. v1.18.0 (S5) promoted it + * from a Layout-hub child to its own standalone nav entry (always shown — + * medications is a CORE domain) and gathered the medication-specific + * preferences here. Hosts three blocks: * 1. The list VIEW preference — cards vs table, written optimistically * through the same `PUT /api/medications/layout` the page header * toggle uses (the toggle component is shared). * 2. The manual ORDER editor — two grouped lists (Aktiv / Inaktiv) * with drag + arrow reordering, flushed by an explicit Save, * persisted as active-ids-then-inactive-ids on the same layout row. + * 3. Injection-site exclusions — moved here from the account profile so + * every medication-specific preference lives on one screen. * - * Both write through the same `/api/medications/layout` contract the - * /medications page reads, so a save here repaints both list views. + * The list view + order both write through the same `/api/medications/layout` + * contract the /medications page reads, so a save here repaints both views. */ /** The slice of the medications list the order editor needs. */ @@ -39,6 +44,7 @@ interface MedicationListEntry { export function MedicationsSection() { const { t } = useTranslations(); + const { isAuthenticated } = useAuth(); const { layout, isLayoutLoading, setView } = useMedicationListLayout(); const { data: medications, isLoading } = useQuery({ @@ -69,11 +75,7 @@ export function MedicationsSection() { aria-labelledby="settings-section-medications-title" className="space-y-6" > -
- +

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

@@ -129,6 +131,11 @@ export function MedicationsSection() { )} + + {/* v1.18.0 (S5) — injection-site exclusions are a medication setting; + they moved here from the account profile so all medication-specific + preferences live in one place. */} + ); } diff --git a/src/components/settings/settings-shell.tsx b/src/components/settings/settings-shell.tsx index 1fc8840c3..df8d1e46e 100644 --- a/src/components/settings/settings-shell.tsx +++ b/src/components/settings/settings-shell.tsx @@ -26,6 +26,7 @@ import { KeyRound, LayoutDashboard, Link2, + Pill, Settings2, SlidersHorizontal, Smile, @@ -121,6 +122,14 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [ titleKey: "settings.sections.layout.title", icon: LayoutDashboard, }, + // v1.18.0 (S5) — Medikamente: medication-specific settings (list view + + // order + injection sites). Medications is a CORE domain, so the entry is + // always shown. Was a hidden child of the Layout hub. + { + slug: "medications", + titleKey: "settings.sections.medications.title", + icon: Pill, + }, // v1.18.0 (S5) — Stimmung: the mood-tag management surface gets its own // nav entry, shown only when the mood module is enabled. Was a hidden // child of the Layout hub. @@ -176,12 +185,12 @@ export interface SettingsShellProps { // when the user is on one of them the Layout nav entry is the one that // reads active. // -// v1.18.0 (S5) — `mood` (Stimmung) graduated to its own nav entry, so it -// is no longer a Layout-hub child for highlighting purposes. +// v1.18.0 (S5) — `mood` (Stimmung) and `medications` (Medikamente) +// graduated to their own nav entries, so they are no longer Layout-hub +// children for highlighting purposes. const LAYOUT_CHILD_SLUGS: ReadonlySet = new Set([ "dashboard", "insights", - "medications", ]); /** Map a Layout child editor onto the Layout hub for nav highlighting. */