mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
feat(settings): gather medication settings into a Medikamente entry
Promote the Medikamente section from a hidden Layout-hub child to its own standalone nav entry (always shown — medications is a CORE domain) and gather the injection-site exclusions card here from the account profile, so every medication-specific preference lives on one screen. The Layout hub keeps only the dashboard + insights arrangement editors. The low-stock card stays whole in Notifications: its runway config and the low-stock reminder toggle are one tightly-coupled control writing a single prefs row, so splitting only the config out would break the card.
This commit is contained in:
1 parent
16bd43c08f
commit
fce9ca8f1a
7 files changed
+89
-63
No files matched your search
@@ -20,17 +20,14 @@ function render(locale: "en" | "de" = "en") {
|
||||
}
|
||||
|
||||
describe("<LayoutSection>", () => {
|
||||
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("<LayoutSection>", () => {
|
||||
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.");
|
||||
});
|
||||
});
|
||||
@@ -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("<MedicationsSection> — 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
|
||||
|
||||
@@ -177,11 +177,7 @@ describe("<SettingsShell>", () => {
|
||||
// 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 =
|
||||
/<a\b[^>]*\baria-current="page"[^>]*\bhref="\/settings\/layout"|<a\b[^>]*\bhref="\/settings\/layout"[^>]*\baria-current="page"/g;
|
||||
@@ -193,8 +189,8 @@ describe("<SettingsShell>", () => {
|
||||
// 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 =
|
||||
/<a\b[^>]*\baria-current="page"[^>]*\bhref="\/settings\/layout"|<a\b[^>]*\bhref="\/settings\/layout"[^>]*\baria-current="page"/g;
|
||||
@@ -238,15 +234,14 @@ describe("<SettingsShell>", () => {
|
||||
// (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("<SettingsShell>", () => {
|
||||
// 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.
|
||||
|
||||
@@ -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() {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* 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. */}
|
||||
<InjectionSitesCard isAuthenticated={isAuthenticated} />
|
||||
{/* 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
|
||||
|
||||
@@ -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<LayoutLink> = [
|
||||
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() {
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<header className="space-y-2">
|
||||
<SettingsHubBackLink
|
||||
href="/settings/layout"
|
||||
labelKey="settings.sections.layout.backToHub"
|
||||
/>
|
||||
<header className="space-y-1">
|
||||
<h1 id="settings-section-medications-title" className="sr-only">
|
||||
{t("settings.sections.medications.title")}
|
||||
</h1>
|
||||
@@ -129,6 +131,11 @@ export function MedicationsSection() {
|
||||
<MedicationOrderEditor medications={ordered} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 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. */}
|
||||
<InjectionSitesCard isAuthenticated={isAuthenticated} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -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<string> = new Set([
|
||||
"dashboard",
|
||||
"insights",
|
||||
"medications",
|
||||
]);
|
||||
|
||||
/** Map a Layout child editor onto the Layout hub for nav highlighting. */
|
||||
|
||||
Reference in new issue
Block a user