refactor(medications): route card actions to detail tabs, retire the advanced sheet

The medications-list card kebab no longer opens a modal editor or the
stacked advanced-settings sheet. Edit navigates to the detail page's
Zeitplan tab, History to Verlauf, Advanced to Erweitert. The list-page
wizard becomes create-only; editing an existing medication goes through
the detail hero's "Vollständig bearbeiten".

Dissolve the advanced-settings sheet entirely: its five stacked groups
(Lifecycle / External API / Data / Danger zone) now live as inset-grouped
sections in the Erweitert tab. Drop the orphaned modal component, the
standalone SettingsSection wrapper, and the lazy intake-count query the
sheet's danger zone needed (the tab fetches it on demand). Remove the two
tests that only exercised the retired surfaces.
This commit is contained in:
Marc-André Bombeck committed 2026-06-08 19:10:30 +02:00
1 parent ac39b9ca34
commit 8201cd2d8c
5 files changed
+20 -684

No files matched your search

+14 -117
View File
@@ -3,13 +3,10 @@
import { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useRouter, useSearchParams } from "next/navigation";
import { AdvancedSettingsSheet } from "@/components/medications/advanced-settings-sheet";
import { useAuth } from "@/hooks/use-auth";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { parseScheduleRecurrence } from "@/lib/medication-schedule";
import { MedicationWizardDialog } from "@/components/medications/wizard/MedicationWizardDialog";
import type { MedicationPayload } from "@/components/medications/wizard/wizard-payload";
import { MedicationCard } from "@/components/medications/medication-card";
import { Glp1MedicationCard } from "@/components/medications/glp1-medication-card";
import { LogIntakeDialog } from "@/components/medications/log-intake-dialog";
@@ -130,12 +127,6 @@ export default function MedicationsPage() {
// the param from the URL so a manual close + refresh stays closed.
const shouldOpenFromUrl = searchParams?.get("new") === "1";
const [dialogOpen, setDialogOpen] = useState(shouldOpenFromUrl);
const [editingMed, setEditingMed] = useState<Medication | null>(null);
// v1.7.1 — the medications-card History + Advanced actions. History
// navigates to the full intake-history view; Advanced opens one shared
// `<AdvancedSettingsSheet>` mounted below, keyed to the selected
// medication (reused, not duplicated per card).
const [advancedMed, setAdvancedMed] = useState<Medication | null>(null);
// v1.14.0 — the medications-page "Add" choice. The top button now offers
// two paths: log an intake (incl. a backdated one) against an existing
// medication, or create a new medication (the existing wizard).
@@ -165,61 +156,29 @@ export default function MedicationsPage() {
enabled: isAuthenticated,
});
// v1.7.1 — the advanced sheet's danger zone needs `intakeCount` to
// gate the purge button. Fetch it lazily once a medication is selected
// (the list view never pays for it otherwise), mirroring the
// detail-page query shape.
const advancedIntakeParams = {
sortBy: "takenAt",
sortDir: "desc" as const,
limit: 1,
offset: 0,
status: "completed",
};
const { data: advancedIntakeCount } = useQuery({
queryKey: advancedMed
? queryKeys.medicationIntakeList(advancedMed.id, advancedIntakeParams)
: ["medications", "intake-count", "none"],
queryFn: async () => {
if (!advancedMed) return 0;
const params = new URLSearchParams({
sortBy: advancedIntakeParams.sortBy,
sortDir: advancedIntakeParams.sortDir,
limit: String(advancedIntakeParams.limit),
offset: String(advancedIntakeParams.offset),
status: advancedIntakeParams.status,
});
const res = await fetch(
`/api/medications/${advancedMed.id}/intake?${params.toString()}`,
);
if (!res.ok) return 0;
const json = await res.json();
return (json.data?.meta?.total ?? 0) as number;
},
enabled: isAuthenticated && !!advancedMed,
});
function openCreate() {
setEditingMed(null);
setDialogOpen(true);
}
// v1.15.18 — the card kebab actions now navigate to the full-page
// tabbed detail surface on the right tab rather than opening a modal
// editor / sheet. Edit lands on Zeitplan (the everyday schedule view +
// the hero's "Vollständig bearbeiten" jump into the wizard), History on
// Verlauf, Advanced on Erweitert (the dissolved advanced-settings).
function openEdit(med: Medication) {
setEditingMed(med);
setDialogOpen(true);
router.push(`/medications/${med.id}?tab=zeitplan`);
}
function openHistory(med: Medication) {
router.push(`/medications/${med.id}/history`);
router.push(`/medications/${med.id}?tab=verlauf`);
}
function openAdvanced(med: Medication) {
setAdvancedMed(med);
router.push(`/medications/${med.id}?tab=erweitert`);
}
function closeDialog() {
setDialogOpen(false);
setEditingMed(null);
}
if (authLoading) {
@@ -412,15 +371,15 @@ export default function MedicationsPage() {
the API endpoint moves to the detail-page Settings → Externe
Integration sub-row. */}
{/* v1.5.4 — modal-wizard mount. The same component drives both
create (no initial) and edit (hydrates from the medication's
payload). The wizard owns its own ResponsiveSheet shell with
the dialog/sheet split and the sticky footer. */}
{/* v1.15.18 — create-only wizard mount. Editing an existing
medication now navigates to the detail page's tabs (the hero's
"Vollständig bearbeiten" reopens the wizard in edit mode from
there); the list page wizard only ever creates. The wizard owns
its own ResponsiveSheet shell with the sticky footer. */}
<MedicationWizardDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
mode={editingMed ? "edit" : "create"}
initial={editingMed ? medicationToPayload(editingMed) : undefined}
mode="create"
onSuccess={closeDialog}
/>
@@ -446,68 +405,6 @@ export default function MedicationsPage() {
}))}
/>
)}
{/* v1.7.1 — one shared advanced-settings sheet for every card.
Reuses the exact component the detail page mounts (Data /
Reminders / Lifecycle / Danger zone); the card's sliders button
selects the medication and opens it. Not duplicated per card. */}
{advancedMed && (
<AdvancedSettingsSheet
open={!!advancedMed}
onOpenChange={(open) => {
if (!open) setAdvancedMed(null);
}}
medicationId={advancedMed.id}
medicationName={advancedMed.name}
treatmentClass={advancedMed.treatmentClass}
active={advancedMed.active}
startsOn={advancedMed.startsOn}
endsOn={advancedMed.endsOn}
notificationsEnabled={advancedMed.notificationsEnabled}
reminderGraceMinutes={
advancedMed.schedules[0]?.reminderGraceMinutes ?? null
}
intakeCount={advancedIntakeCount ?? 0}
atcCode={advancedMed.atcCode}
rxNormCode={advancedMed.rxNormCode}
/>
)}
</div>
);
}
/**
* Map a `Medication` row from `GET /api/medications` onto the
* `MedicationPayload` shape the wizard's edit-path hydrator consumes.
* Mirrors the schedule pass-through the v1.5.3 flat form relied on so
* legacy cadences round-trip through the bridge cleanly.
*/
function medicationToPayload(med: Medication): MedicationPayload {
return {
id: med.id,
name: med.name,
dose: med.dose,
category: med.category,
treatmentClass: med.treatmentClass,
deliveryForm: med.deliveryForm,
dosesPerUnit: med.dosesPerUnit ?? null,
trackInjectionSites: med.trackInjectionSites ?? false,
allowedInjectionSites: med.allowedInjectionSites ?? [],
notificationsEnabled: med.notificationsEnabled,
startsOn: med.startsOn ? new Date(med.startsOn) : null,
endsOn: med.endsOn ? new Date(med.endsOn) : null,
oneShot: med.oneShot ?? false,
schedules: med.schedules.map((s) => ({
id: s.id,
windowStart: s.windowStart,
windowEnd: s.windowEnd,
label: s.label ?? null,
dose: s.dose ?? null,
...parseScheduleRecurrence(s.daysOfWeek),
timesOfDay: s.timesOfDay,
rrule: s.rrule ?? null,
rollingIntervalDays: s.rollingIntervalDays ?? null,
})),
};
}
@@ -1,210 +0,0 @@
/**
* v1.7.0 — AdvancedSettingsSheet composition contract.
*
* The sheet hosts five blocks — Reminders → Lifecycle → Data →
* Externe API → Danger zone — composed from bare section bodies.
* `<ResponsiveSheet>` wraps Radix portals that `renderToStaticMarkup`
* does not materialise, so we mock it to a passthrough that renders
* children only when `open` and echoes `contentWidth`. The bodies are
* mocked to ordered markers. The test pins:
* - the five blocks render in the documented order;
* - the external-API row lives in its own group, not in Data;
* - the sheet opens at the `2xl` width token;
* - `onRequestPhaseSheet` threads through to the phases row;
* - `onOpenImport` wires the import button (co-located with export);
* - the export control sits beside the import control;
* - the lifecycle + danger bodies receive an `onAfterAction` closer;
* - the sheet renders nothing when `open` is false.
*/
import { describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
vi.mock("@/lib/i18n/context", () => ({
useTranslations: () => ({ t: (key: string) => key }),
}));
vi.mock("@/components/ui/responsive-sheet", () => ({
ResponsiveSheet: ({
open,
children,
title,
contentWidth,
}: {
open: boolean;
children: React.ReactNode;
title: React.ReactNode;
contentWidth?: string;
}) =>
open ? (
<div data-slot="mock-responsive-sheet" data-content-width={contentWidth}>
<div data-slot="mock-title">{title}</div>
{children}
</div>
) : null,
}));
vi.mock("@/components/medications/sections/api-tokens-row", () => ({
ApiTokensRow: () => <div data-slot="mock-api-tokens" />,
}));
vi.mock("@/components/medications/sections/notifications-section", () => ({
NotificationsBody: () => <div data-slot="mock-notifications" />,
}));
vi.mock("@/components/medications/sections/settings-section", () => ({
GraceRow: () => <div data-slot="mock-grace" />,
DrugCodingRow: () => <div data-slot="mock-drug-coding" />,
PhasesRow: ({
onRequestPhaseSheet,
}: {
onRequestPhaseSheet?: () => void;
}) => (
<div
data-slot="mock-phases"
data-has-phase-swap={onRequestPhaseSheet ? "yes" : "no"}
/>
),
}));
vi.mock("@/components/medications/sections/destructive-zone-section", () => ({
LifecycleManageBody: ({ onAfterAction }: { onAfterAction?: () => void }) => (
<div
data-slot="mock-lifecycle"
data-has-after-action={onAfterAction ? "yes" : "no"}
/>
),
DangerZoneBody: ({ onAfterAction }: { onAfterAction?: () => void }) => (
<button
data-slot="mock-danger"
data-has-after-action={onAfterAction ? "yes" : "no"}
onClick={onAfterAction}
/>
),
}));
import { AdvancedSettingsSheet } from "@/components/medications/advanced-settings-sheet";
function baseProps(
open: boolean,
extra: Partial<{
onRequestPhaseSheet: () => void;
onOpenImport: () => void;
}> = {},
) {
return {
open,
onOpenChange: () => {},
medicationId: "med-1",
medicationName: "Test Drug",
treatmentClass: "GLP1",
active: true,
startsOn: "2026-01-01",
endsOn: "2026-06-01",
notificationsEnabled: true,
reminderGraceMinutes: 30,
intakeCount: 5,
...extra,
};
}
describe("AdvancedSettingsSheet (v1.7.0)", () => {
it("renders the five blocks in Reminders → Lifecycle → Data → Externe API → Danger order", () => {
const html = renderToStaticMarkup(
<AdvancedSettingsSheet
{...baseProps(true, { onRequestPhaseSheet: () => {} })}
/>,
);
const reminders = html.indexOf("advanced-group-reminders");
const lifecycle = html.indexOf("advanced-group-lifecycle");
const data = html.indexOf("advanced-group-data");
const externalApi = html.indexOf("advanced-group-external-api");
const danger = html.indexOf("mock-danger");
expect(reminders).toBeGreaterThan(-1);
expect(lifecycle).toBeGreaterThan(reminders);
expect(data).toBeGreaterThan(lifecycle);
expect(externalApi).toBeGreaterThan(data);
expect(danger).toBeGreaterThan(externalApi);
});
it("homes the external-API row in its own group, not in the Data group", () => {
const html = renderToStaticMarkup(
<AdvancedSettingsSheet {...baseProps(true)} />,
);
// The API row marker must sit after the external-API group opens,
// not inside the Data group it used to share.
const dataGroup = html.indexOf("advanced-group-data");
const externalApiGroup = html.indexOf("advanced-group-external-api");
const apiRow = html.indexOf("mock-api-tokens");
expect(apiRow).toBeGreaterThan(externalApiGroup);
expect(externalApiGroup).toBeGreaterThan(dataGroup);
});
it("opens at the 2xl width token", () => {
const html = renderToStaticMarkup(
<AdvancedSettingsSheet {...baseProps(true)} />,
);
expect(html).toContain('data-content-width="2xl"');
});
it("threads onRequestPhaseSheet through to the phases row", () => {
const html = renderToStaticMarkup(
<AdvancedSettingsSheet
{...baseProps(true, { onRequestPhaseSheet: () => {} })}
/>,
);
expect(html).toContain('data-has-phase-swap="yes"');
});
it("wires the import button", () => {
const html = renderToStaticMarkup(
<AdvancedSettingsSheet
{...baseProps(true, { onOpenImport: () => {} })}
/>,
);
expect(html).toContain("advanced-import-button");
expect(html).toContain(
"medications.detail.advanced.dataPortability.import.button",
);
});
it("co-locates an export control beside the import control", () => {
const html = renderToStaticMarkup(
<AdvancedSettingsSheet {...baseProps(true)} />,
);
expect(html).toContain("advanced-export-button");
expect(html).toContain(
"medications.detail.advanced.dataPortability.export.button",
);
const importIdx = html.indexOf("advanced-import-block");
const exportIdx = html.indexOf("advanced-export-block");
expect(importIdx).toBeGreaterThan(-1);
expect(exportIdx).toBeGreaterThan(importIdx);
});
it("passes an onAfterAction to the lifecycle and danger bodies so they can close the sheet", () => {
const html = renderToStaticMarkup(
<AdvancedSettingsSheet {...baseProps(true)} />,
);
const lifecycle = html.match(
/mock-lifecycle"[^>]*data-has-after-action="yes"/,
);
const danger = html.match(/mock-danger"[^>]*data-has-after-action="yes"/);
expect(lifecycle).not.toBeNull();
expect(danger).not.toBeNull();
});
it("renders nothing when closed", () => {
const html = renderToStaticMarkup(
<AdvancedSettingsSheet {...baseProps(false)} />,
);
expect(html).toBe("");
});
it("titles the sheet with the advanced-settings key", () => {
const html = renderToStaticMarkup(
<AdvancedSettingsSheet {...baseProps(true)} />,
);
expect(html).toContain("medications.detail.advanced.title");
});
});
@@ -1,208 +0,0 @@
"use client";
/**
* v1.7.0 — Advanced settings sheet.
* v1.7.2 — widened to `4xl` and re-laid into a responsive two-column
* grid.
* v1.8.6 — rebuilt as a single column. The two-column data|reminders
* grid read as overloaded; the sheet now narrows to `2xl` (~672px) and
* stacks bordered section cards in most-used-first order. One spacing
* rule throughout — `space-y-4` between cards, `divide-y` + `py-3` rows
* inside each card (the `<SettingsGroup>` card owns the dividers).
*
* v1.9.0 — the Data group split. It had carried both operator data
* portability and the developer-facing external API in one frame; the
* API endpoint now sits in its own `Externe API` group so the two
* audiences no longer share a card. Order: Reminders → Lifecycle →
* Data → Externe API → Danger.
*
* - Reminders: notifications switch + reminder window (grace).
* - Lifecycle: pause/resume + end course + phases.
* - Data: intake Import + medications CSV Export (co-located).
* - Externe API: the per-medication ingest endpoint + token, with the
* request examples collapsed by default.
* - Danger zone: purge intake history + delete medication, set off by
* its own destructive-tinted card so an irreversible action never
* sits next to a routine toggle.
*
* Button styling is consistent: reversible / neutral actions are
* `outline`, destructive actions are `destructive font-semibold`,
* toggles are switches wrapped in labels.
*
* The Phasen button is sibling-swapped, not nested: `onRequestPhaseSheet`
* closes this sheet first and lets the parent open the phase sheet so
* the modal stack never exceeds two (D-3 §10 invariant 7). Each control
* self-saves, so the footer slot stays empty.
*/
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import { SettingsGroup } from "@/components/medications/settings-group";
import { ApiTokensRow } from "@/components/medications/sections/api-tokens-row";
import { DataPortabilityRow } from "@/components/medications/sections/data-portability-row";
import { NotificationsBody } from "@/components/medications/sections/notifications-section";
import {
DrugCodingRow,
GraceRow,
PhasesRow,
} from "@/components/medications/sections/settings-section";
import {
DangerZoneBody,
LifecycleManageBody,
} from "@/components/medications/sections/destructive-zone-section";
import { useTranslations } from "@/lib/i18n/context";
export interface AdvancedSettingsSheetProps {
open: boolean;
onOpenChange: (open: boolean) => void;
medicationId: string;
medicationName: string;
treatmentClass?: string;
active: boolean;
startsOn?: string | null;
endsOn?: string | null;
notificationsEnabled: boolean;
reminderGraceMinutes: number | null;
intakeCount: number;
/** v1.9.0 — optional drug-classification codes for the FHIR export. */
atcCode?: string | null;
rxNormCode?: string | null;
/**
* Sibling-swap the phase sheet: the parent closes this sheet and
* opens `<PhaseConfigSheet>` so the two never stack (G-1 §5).
*/
onRequestPhaseSheet?: () => void;
/**
* v1.7.0 — opens the page-owned `<IntakeImportDialog>` from the Data
* group's import button.
*/
onOpenImport?: () => void;
}
export function AdvancedSettingsSheet({
open,
onOpenChange,
medicationId,
medicationName,
treatmentClass,
active,
startsOn,
endsOn,
notificationsEnabled,
reminderGraceMinutes,
intakeCount,
atcCode,
rxNormCode,
onRequestPhaseSheet,
onOpenImport,
}: AdvancedSettingsSheetProps) {
const { t } = useTranslations();
const isGlp1 = treatmentClass === "GLP1";
return (
<ResponsiveSheet
open={open}
onOpenChange={onOpenChange}
title={t("medications.detail.advanced.title")}
contentWidth="2xl"
bodyClassName="gap-4"
>
<div className="space-y-4" data-slot="advanced-settings-sheet-body">
{/* REMINDERS — notifications + reminder window. Most-used group
first; each row is a padded `<div>` so the group card's
`divide-y` draws the hairline between them. */}
<SettingsGroup
label={t("medications.detail.advanced.group.reminders")}
dataSlot="advanced-group-reminders"
>
<div className="py-3">
<NotificationsBody
medicationId={medicationId}
notificationsEnabled={notificationsEnabled}
/>
</div>
<div className="py-3">
<GraceRow
medicationId={medicationId}
reminderGraceMinutes={reminderGraceMinutes}
/>
</div>
</SettingsGroup>
{/* LIFECYCLE — pause/resume + end course + phases */}
<SettingsGroup
label={t("medications.detail.advanced.group.lifecycle")}
dataSlot="advanced-group-lifecycle"
>
<div className="py-3">
<LifecycleManageBody
medicationId={medicationId}
medicationName={medicationName}
active={active}
onAfterAction={() => onOpenChange(false)}
/>
</div>
{isGlp1 && (
<div className="py-3">
<PhasesRow
medicationId={medicationId}
treatmentClass={treatmentClass}
startsOn={startsOn}
endsOn={endsOn}
onRequestPhaseSheet={onRequestPhaseSheet}
/>
</div>
)}
</SettingsGroup>
{/* DATA — import + export. The external API lives in its own
group below; this group stays scoped to operator data
portability so the two audiences don't share one frame. */}
<SettingsGroup
label={t("medications.detail.advanced.group.data")}
dataSlot="advanced-group-data"
>
<div className="py-3">
<DataPortabilityRow
medicationId={medicationId}
onOpenImport={onOpenImport}
/>
</div>
</SettingsGroup>
{/* EXTERNAL API — the per-medication ingest endpoint + token.
Pulled out of the Data group into its own frame so the
developer/integration surface reads as a distinct, lowest-
frequency block right before the danger zone. */}
<SettingsGroup
label={t("medications.detail.advanced.group.externalApi")}
dataSlot="advanced-group-external-api"
>
<div className="py-3">
<ApiTokensRow
medicationId={medicationId}
medicationName={medicationName}
/>
</div>
<div className="py-3">
<DrugCodingRow
medicationId={medicationId}
atcCode={atcCode}
rxNormCode={rxNormCode}
/>
</div>
</SettingsGroup>
{/* DANGER ZONE — purge + delete. Set off by the destructive-
tinted card so an irreversible action never sits next to a
routine toggle. The body supplies its own matched-pair card,
so it slots in without the neutral group frame. */}
<DangerZoneBody
medicationId={medicationId}
medicationName={medicationName}
intakeCount={intakeCount}
onAfterAction={() => onOpenChange(false)}
/>
</div>
</ResponsiveSheet>
);
}
@@ -1,71 +0,0 @@
/**
* v1.5.6 G-1 §5 — phase sibling-swap wiring on `<SettingsSection>`.
*
* When the hosting `<AdvancedSettingsSheet>` passes
* `onRequestPhaseSheet`, the section must NOT mount its own
* `<PhaseConfigSheet>` — the parent orchestrates the swap (close the
* advanced sheet, then open the phase sheet) so the two never stack.
* Without the callback the section keeps owning its own phase sheet,
* preserving the standalone-surface fallback.
*
* `<PhaseConfigSheet>` is mocked to a marker so its presence /
* absence is observable in static markup regardless of open-state.
*/
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";
vi.mock("@/components/medications/sections/api-tokens-row", () => ({
ApiTokensRow: () => <div data-slot="mock-api-tokens" />,
}));
vi.mock("@/components/medications/sections/phase-config-sheet", () => ({
PhaseConfigSheet: () => <div data-slot="mock-phase-config-sheet" />,
}));
import { SettingsSection } from "@/components/medications/sections/settings-section";
function makeClient() {
return new QueryClient({
defaultOptions: {
queries: { retry: false, staleTime: Number.POSITIVE_INFINITY },
},
});
}
function render(node: React.ReactNode) {
return renderToStaticMarkup(
<QueryClientProvider client={makeClient()}>
<I18nProvider initialLocale="en">{node}</I18nProvider>
</QueryClientProvider>,
);
}
// GLP-1 + a set course window so the Phasen row + button mount.
const glp1Props = {
medicationId: "med-1",
medicationName: "Test Drug",
treatmentClass: "GLP1",
startsOn: "2026-01-01",
endsOn: "2026-06-01",
reminderGraceMinutes: 30,
};
describe("SettingsSection phase sibling-swap (G-1 §5)", () => {
it("does NOT self-mount the phase sheet when onRequestPhaseSheet is provided", () => {
const html = render(
<SettingsSection {...glp1Props} onRequestPhaseSheet={() => {}} />,
);
expect(html).toContain("medication-detail-phase-management-button");
expect(html).not.toContain("mock-phase-config-sheet");
});
it("self-mounts the phase sheet on the standalone surface (no callback)", () => {
const html = render(<SettingsSection {...glp1Props} />);
expect(html).toContain("medication-detail-phase-management-button");
expect(html).toContain("mock-phase-config-sheet");
});
});
@@ -4,12 +4,12 @@
* v1.5.5 D-3 §9.7 — Settings rows.
*
* v1.7.0 — the monolithic "Settings" card is decomposed into reusable
* rows so the redesigned `<AdvancedSettingsSheet>` can slot each one
* under the right group: API tokens → Data, Grace → Reminders,
* Phasen → Lifecycle. The standalone `<SettingsSection>` wrapper stays
* (it composes the rows under one section card) so existing callers +
* tests keep working. The CSV-import stub is gone — the sheet's Data
* group carries a real import button.
* rows so each can slot under the right group: API tokens → Data, Grace →
* Reminders, Phasen → Lifecycle.
*
* v1.15.18 — the standalone `<SettingsSection>` wrapper is retired with
* the modal advanced-settings sheet; the detail page's tabs consume the
* individual rows (`GraceRow` / `DrugCodingRow` / `PhasesRow`) directly.
*
* Per H-4-UX: the Grace row's label flags it as primary-schedule
* scoped so the user knows a multi-schedule medication does not get a
@@ -23,31 +23,10 @@ import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { MedicationDetailSection } from "@/components/medications/medication-detail-section";
import { ApiTokensRow } from "@/components/medications/sections/api-tokens-row";
import { PhaseConfigSheet } from "@/components/medications/sections/phase-config-sheet";
import { useTranslations } from "@/lib/i18n/context";
import { invalidateKeys, medicationDependentKeys } from "@/lib/query-keys";
export interface SettingsSectionProps {
medicationId: string;
medicationName: string;
treatmentClass?: string;
startsOn?: string | null;
endsOn?: string | null;
reminderGraceMinutes?: number | null;
/**
* v1.5.6 G-1 §5 — sibling-swap the phase sheet. When provided, the
* Phasen button hands control to the parent (which closes the
* hosting sheet before opening `<PhaseConfigSheet>`) instead of the
* self-managed `phaseSheetOpen` state, so the two sheets never
* stack. Absent on the standalone surface, where the section owns
* its own phase sheet.
*/
onRequestPhaseSheet?: () => void;
}
/**
* v1.7.0 — primary-schedule reminder-window (grace) row. Self-saves via
* `PUT /api/medications/[id]` with a top-level `reminderGraceMinutes`.
@@ -313,54 +292,3 @@ export function PhasesRow({
);
}
/**
* Standalone section wrapper — composes the rows under one section
* card. Retained for callers / tests that expect the bundled shape.
*/
export function SettingsSection({
medicationId,
medicationName,
treatmentClass,
startsOn,
endsOn,
reminderGraceMinutes,
onRequestPhaseSheet,
}: SettingsSectionProps) {
const { t } = useTranslations();
const isGlp1 = treatmentClass === "GLP1";
return (
<MedicationDetailSection
titleId="medication-detail-settings-heading"
title={t("medications.detail.settings.title")}
dataSlot="medication-detail-settings-section"
>
<div className="space-y-4" data-slot="medication-detail-settings-body">
<ApiTokensRow
medicationId={medicationId}
medicationName={medicationName}
/>
{isGlp1 && (
<>
<Separator />
<PhasesRow
medicationId={medicationId}
treatmentClass={treatmentClass}
startsOn={startsOn}
endsOn={endsOn}
onRequestPhaseSheet={onRequestPhaseSheet}
/>
</>
)}
<Separator />
<GraceRow
medicationId={medicationId}
reminderGraceMinutes={reminderGraceMinutes}
/>
</div>
</MedicationDetailSection>
);
}