mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
feat(meds): step content for the modal wizard
Each of the eight steps lands with its Lucide icon, title, subline and a single focused field. The cadence radio rows carry the plain-language description directly under each label (not in a tooltip), and the rolling-cadence row gets the literal mental-model line so the patient understands the counter resets on every log. Step 2 surfaces ten taxonomy rows that each map onto a single (treatmentClass, category) pair through a hard-coded table; Step 6 swaps its body on the cadence kind picked in Step 5; Step 7 wraps the existing times-of-day chips primitive with a sunrise / sun / sunset / moon preset row above it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
0a0af4b772
commit
b1f9a4bf8f
8 files changed
+657
-17
No files matched your search
@@ -1,5 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
import type { WizardPayload } from "../wizard-payload";
|
||||
|
||||
export interface StepProps {
|
||||
@@ -7,6 +11,24 @@ export interface StepProps {
|
||||
applyPartial: (partial: Partial<WizardPayload>) => void;
|
||||
}
|
||||
|
||||
export function Step1Name(_props: StepProps) {
|
||||
return null;
|
||||
export function Step1Name({ payload, applyPartial }: StepProps) {
|
||||
const { t } = useTranslations();
|
||||
return (
|
||||
<div className="space-y-1.5" data-slot="wizard-step1">
|
||||
<Label htmlFor="wizard-name" className="sr-only">
|
||||
{t("medications.wizard.steps.step1.label")}
|
||||
</Label>
|
||||
<Input
|
||||
id="wizard-name"
|
||||
value={payload.name}
|
||||
onChange={(e) => applyPartial({ name: e.target.value })}
|
||||
placeholder={t("medications.wizard.steps.step1.placeholder")}
|
||||
maxLength={100}
|
||||
autoCapitalize="words"
|
||||
autoComplete="off"
|
||||
enterKeyHint="next"
|
||||
className="h-11"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Activity,
|
||||
Apple,
|
||||
Droplet,
|
||||
Flame,
|
||||
Leaf,
|
||||
type LucideIcon,
|
||||
type LucideProps,
|
||||
ShieldCheck,
|
||||
Stethoscope,
|
||||
Syringe,
|
||||
Tag,
|
||||
Wind,
|
||||
} from "lucide-react";
|
||||
import type { ComponentType } from "react";
|
||||
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
import {
|
||||
type WizardTreatmentRow,
|
||||
WIZARD_TREATMENT_ROWS,
|
||||
} from "../wizard-payload";
|
||||
import type { StepProps } from "./Step1Name";
|
||||
|
||||
export function Step2Class(_props: StepProps) {
|
||||
return null;
|
||||
// Each row's Lucide glyph. Marc-confirmed assignment in D-1 §3 Step 2.
|
||||
const ROW_ICONS: Record<
|
||||
WizardTreatmentRow,
|
||||
ComponentType<LucideProps>
|
||||
> = {
|
||||
bloodPressure: Stethoscope,
|
||||
diabetes: Droplet,
|
||||
hormone: Activity,
|
||||
glp1: Syringe,
|
||||
painRelief: Flame,
|
||||
allergy: Wind,
|
||||
vitamin: Apple,
|
||||
supplement: Leaf,
|
||||
antibiotic: ShieldCheck,
|
||||
other: Tag as unknown as LucideIcon,
|
||||
};
|
||||
|
||||
export function Step2Class({ payload, applyPartial }: StepProps) {
|
||||
const { t } = useTranslations();
|
||||
return (
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label={t("medications.wizard.steps.step2.title")}
|
||||
className="space-y-2"
|
||||
data-slot="wizard-step2"
|
||||
>
|
||||
{WIZARD_TREATMENT_ROWS.map((row) => {
|
||||
const Icon = ROW_ICONS[row];
|
||||
const selected = payload.treatmentRow === row;
|
||||
const label = t(`medications.wizard.classRow.${row}`);
|
||||
return (
|
||||
<label
|
||||
key={row}
|
||||
className={[
|
||||
"flex min-h-11 cursor-pointer items-center gap-3 rounded-md border p-3 transition-colors",
|
||||
selected
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:bg-muted/40",
|
||||
].join(" ")}
|
||||
data-slot="wizard-class-row"
|
||||
data-row={row}
|
||||
data-selected={selected ? "true" : "false"}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="wizard-class"
|
||||
value={row}
|
||||
checked={selected}
|
||||
onChange={() => applyPartial({ treatmentRow: row })}
|
||||
className="sr-only"
|
||||
aria-label={label}
|
||||
/>
|
||||
<Icon
|
||||
className="text-primary h-5 w-5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="text-sm font-medium">{label}</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
import type { StepProps } from "./Step1Name";
|
||||
|
||||
export function Step3Dose(_props: StepProps) {
|
||||
return null;
|
||||
/**
|
||||
* Dose unit choices, ordered so mg / ml / IE / µg lead and the
|
||||
* device-form units (Tablette, Hub, Sprühstoß, …) follow. Mirrors the
|
||||
* v1.5.3 list with the Marc-requested order tightening.
|
||||
*/
|
||||
const DOSE_UNIT_KEYS = [
|
||||
"mg",
|
||||
"ml",
|
||||
"iu",
|
||||
"mcg",
|
||||
"g",
|
||||
"tablets",
|
||||
"capsules",
|
||||
"drops",
|
||||
"puffs",
|
||||
"sprays",
|
||||
"pieces",
|
||||
"other",
|
||||
] as const;
|
||||
|
||||
export function Step3Dose({ payload, applyPartial }: StepProps) {
|
||||
const { t } = useTranslations();
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-3" data-slot="wizard-step3">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="wizard-dose-amount" className="text-sm">
|
||||
{t("medications.wizard.steps.step3.amountLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
id="wizard-dose-amount"
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
value={payload.doseAmount}
|
||||
onChange={(e) => applyPartial({ doseAmount: e.target.value })}
|
||||
placeholder={t("medications.wizard.steps.step3.amountPlaceholder")}
|
||||
maxLength={20}
|
||||
autoComplete="off"
|
||||
enterKeyHint="next"
|
||||
className="h-11"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="wizard-dose-unit" className="text-sm">
|
||||
{t("medications.wizard.steps.step3.unitLabel")}
|
||||
</Label>
|
||||
<Select
|
||||
value={payload.doseUnit}
|
||||
onValueChange={(v) => applyPartial({ doseUnit: v })}
|
||||
>
|
||||
<SelectTrigger id="wizard-dose-unit" className="h-11 w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DOSE_UNIT_KEYS.map((u) => (
|
||||
<SelectItem key={u} value={u}>
|
||||
{t(`medications.wizard.steps.step3.unit.${u}`)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { CourseWindowRow } from "@/components/medications/scheduling/CourseWindowRow";
|
||||
|
||||
import type { StepProps } from "./Step1Name";
|
||||
|
||||
export function Step4Window(_props: StepProps) {
|
||||
return null;
|
||||
/**
|
||||
* Step 4 — Behandlungszeitraum (course window).
|
||||
*
|
||||
* On the one-shot path (Step 5 set `mode = "oneShot"`) the endsOn
|
||||
* stays pinned to the startsOn via `lockEndsToStart`. Before Step 5
|
||||
* runs, the mode is still `null` so the unlocked form renders by
|
||||
* default — this matches the wizard's "you can change anything later"
|
||||
* stance.
|
||||
*/
|
||||
export function Step4Window({ payload, applyPartial }: StepProps) {
|
||||
const isOneShot = payload.mode === "oneShot";
|
||||
return (
|
||||
<div data-slot="wizard-step4">
|
||||
<CourseWindowRow
|
||||
startsOn={payload.startsOn}
|
||||
endsOn={payload.endsOn}
|
||||
lockEndsToStart={isOneShot}
|
||||
onChange={({ startsOn, endsOn }) =>
|
||||
applyPartial({ startsOn, endsOn })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import { encodeCadence } from "@/components/medications/scheduling/CadencePicker";
|
||||
import type { CadenceKind } from "@/components/medications/scheduling/types";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
import type { WizardPayload } from "../wizard-payload";
|
||||
import type { StepProps } from "./Step1Name";
|
||||
|
||||
export function Step5Cadence(_props: StepProps) {
|
||||
/**
|
||||
* The six cadence rows surfaced in Step 5. Each maps to a (mode,
|
||||
* cadenceKind) decision. "Einmalig" flips the wizard onto the
|
||||
* one-shot path; every other row stays recurring.
|
||||
*/
|
||||
type Step5Row =
|
||||
| "daily"
|
||||
| "weekdays"
|
||||
| "everyNWeeks"
|
||||
| "monthly"
|
||||
| "rolling"
|
||||
| "oneShot";
|
||||
|
||||
const STEP5_ROWS: readonly Step5Row[] = [
|
||||
"daily",
|
||||
"weekdays",
|
||||
"everyNWeeks",
|
||||
"monthly",
|
||||
"rolling",
|
||||
"oneShot",
|
||||
];
|
||||
|
||||
function rowFromPayload(payload: WizardPayload): Step5Row | null {
|
||||
if (payload.mode === "oneShot") return "oneShot";
|
||||
if (payload.mode === "recurring") {
|
||||
switch (payload.cadence.kind) {
|
||||
case "daily":
|
||||
case "weekdays":
|
||||
case "everyNWeeks":
|
||||
case "monthly":
|
||||
case "rolling":
|
||||
return payload.cadence.kind;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function Step5Cadence({ payload, applyPartial }: StepProps) {
|
||||
const { t } = useTranslations();
|
||||
const current = rowFromPayload(payload);
|
||||
return (
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label={t("medications.wizard.steps.step5.title")}
|
||||
className="space-y-2"
|
||||
data-slot="wizard-step5"
|
||||
>
|
||||
{STEP5_ROWS.map((row) => {
|
||||
const selected = current === row;
|
||||
const label = t(`medications.wizard.cadence.${row}.label`);
|
||||
const description = t(`medications.wizard.cadence.${row}.description`);
|
||||
return (
|
||||
<label
|
||||
key={row}
|
||||
className={[
|
||||
"block min-h-11 cursor-pointer rounded-md border p-3 transition-colors",
|
||||
selected
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:bg-muted/40",
|
||||
].join(" ")}
|
||||
data-slot="wizard-cadence-row"
|
||||
data-row={row}
|
||||
data-selected={selected ? "true" : "false"}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="wizard-cadence"
|
||||
value={row}
|
||||
checked={selected}
|
||||
onChange={() => applyRowPick(row, payload, applyPartial)}
|
||||
className="sr-only"
|
||||
aria-label={label}
|
||||
/>
|
||||
<div className="text-sm font-medium">{label}</div>
|
||||
<p className="text-muted-foreground mt-0.5 text-xs">
|
||||
{description}
|
||||
</p>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function applyRowPick(
|
||||
row: Step5Row,
|
||||
payload: WizardPayload,
|
||||
applyPartial: (partial: Partial<WizardPayload>) => void,
|
||||
) {
|
||||
if (row === "oneShot") {
|
||||
applyPartial({
|
||||
mode: "oneShot",
|
||||
cadence: encodeCadence("oneShot", payload.subControls),
|
||||
// One-shot pins endsOn to startsOn; the route enforces the
|
||||
// same invariant on the server side.
|
||||
endsOn: payload.startsOn,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const cadenceKind: CadenceKind = row;
|
||||
applyPartial({
|
||||
mode: "recurring",
|
||||
cadence: encodeCadence(cadenceKind, payload.subControls),
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,188 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { encodeCadence } from "@/components/medications/scheduling/CadencePicker";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
type CadenceSubControls,
|
||||
type WeekdayToken,
|
||||
WEEKDAY_TOKENS,
|
||||
} from "@/components/medications/scheduling/types";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
import type { StepProps } from "./Step1Name";
|
||||
|
||||
export function Step6SubCadence(_props: StepProps) {
|
||||
/**
|
||||
* Step 6 — sub-cadence detail. The body swaps on the cadence kind the
|
||||
* user picked in Step 5. Daily and one-shot skip Step 6 entirely (the
|
||||
* path table omits the index), so this component only branches on the
|
||||
* four kinds that have additional detail: weekdays, everyNWeeks,
|
||||
* monthly, rolling.
|
||||
*
|
||||
* Each branch keeps the `<CadenceSubControls>` shape the picker
|
||||
* primitive uses, so the encoded RRULE / rollingIntervalDays the
|
||||
* wizard emits matches what the existing edit form would produce for
|
||||
* the same inputs.
|
||||
*/
|
||||
export function Step6SubCadence({ payload, applyPartial }: StepProps) {
|
||||
const { t } = useTranslations();
|
||||
const kind = payload.cadence.kind;
|
||||
|
||||
function patchSub(patch: Partial<CadenceSubControls>) {
|
||||
const nextSub = { ...payload.subControls, ...patch };
|
||||
applyPartial({
|
||||
subControls: nextSub,
|
||||
cadence: encodeCadence(kind, nextSub),
|
||||
});
|
||||
}
|
||||
|
||||
if (kind === "weekdays" || kind === "everyNWeeks") {
|
||||
return (
|
||||
<div className="space-y-3" data-slot="wizard-step6">
|
||||
{kind === "everyNWeeks" && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="wizard-interval-weeks" className="text-sm">
|
||||
{t("medications.wizard.steps.step6.intervalWeeks.label")}
|
||||
</Label>
|
||||
<Input
|
||||
id="wizard-interval-weeks"
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
min={1}
|
||||
max={12}
|
||||
value={payload.subControls.intervalWeeks}
|
||||
onChange={(e) => {
|
||||
const n = Number(e.target.value);
|
||||
if (Number.isFinite(n) && n >= 1 && n <= 12) {
|
||||
patchSub({ intervalWeeks: Math.trunc(n) });
|
||||
}
|
||||
}}
|
||||
className="h-11 w-20"
|
||||
/>
|
||||
<span className="text-sm">
|
||||
{t("medications.wizard.steps.step6.intervalWeeks.suffix")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<WeekdayChips
|
||||
selected={payload.subControls.weekdays}
|
||||
onChange={(weekdays) => patchSub({ weekdays })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (kind === "monthly") {
|
||||
return (
|
||||
<div className="flex items-center gap-2" data-slot="wizard-step6">
|
||||
<Label htmlFor="wizard-day-of-month" className="text-sm">
|
||||
{t("medications.wizard.steps.step6.dayOfMonth.label")}
|
||||
</Label>
|
||||
<Input
|
||||
id="wizard-day-of-month"
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
min={1}
|
||||
max={31}
|
||||
value={payload.subControls.dayOfMonth}
|
||||
onChange={(e) => {
|
||||
const n = Number(e.target.value);
|
||||
if (Number.isFinite(n) && n >= 1 && n <= 31) {
|
||||
patchSub({ dayOfMonth: Math.trunc(n) });
|
||||
}
|
||||
}}
|
||||
className="h-11 w-20"
|
||||
/>
|
||||
<span className="text-sm">
|
||||
{t("medications.wizard.steps.step6.dayOfMonth.suffix")}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (kind === "rolling") {
|
||||
return (
|
||||
<div className="flex items-center gap-2" data-slot="wizard-step6">
|
||||
<Label htmlFor="wizard-rolling-days" className="text-sm">
|
||||
{t("medications.wizard.steps.step6.rollingDays.label")}
|
||||
</Label>
|
||||
<Input
|
||||
id="wizard-rolling-days"
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
min={1}
|
||||
max={365}
|
||||
value={payload.subControls.rollingDays}
|
||||
onChange={(e) => {
|
||||
const n = Number(e.target.value);
|
||||
if (Number.isFinite(n) && n >= 1 && n <= 365) {
|
||||
patchSub({ rollingDays: Math.trunc(n) });
|
||||
}
|
||||
}}
|
||||
className="h-11 w-20"
|
||||
/>
|
||||
<span className="text-sm">
|
||||
{t("medications.wizard.steps.step6.rollingDays.suffix")}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
interface WeekdayChipsProps {
|
||||
selected: readonly WeekdayToken[];
|
||||
onChange: (next: WeekdayToken[]) => void;
|
||||
}
|
||||
|
||||
function WeekdayChips({ selected, onChange }: WeekdayChipsProps) {
|
||||
const { t } = useTranslations();
|
||||
const set = useMemo(() => new Set(selected), [selected]);
|
||||
function toggle(tok: WeekdayToken) {
|
||||
const next = new Set(set);
|
||||
if (next.has(tok)) next.delete(tok);
|
||||
else next.add(tok);
|
||||
onChange(WEEKDAY_TOKENS.filter((w) => next.has(w)));
|
||||
}
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-label={t("medications.scheduling.cadence.weekdays.label")}
|
||||
className="flex flex-wrap gap-1.5"
|
||||
data-slot="wizard-weekday-chips"
|
||||
>
|
||||
{WEEKDAY_TOKENS.map((tok) => {
|
||||
const isOn = set.has(tok);
|
||||
const short = t(
|
||||
`medications.scheduling.cadence.weekdays.short.${tok.toLowerCase()}`,
|
||||
);
|
||||
const long = t(
|
||||
`medications.scheduling.cadence.weekdays.long.${tok.toLowerCase()}`,
|
||||
);
|
||||
return (
|
||||
<button
|
||||
key={tok}
|
||||
type="button"
|
||||
aria-pressed={isOn}
|
||||
aria-label={long}
|
||||
data-slot="wizard-weekday-chip"
|
||||
data-token={tok}
|
||||
data-active={isOn ? "true" : "false"}
|
||||
onClick={() => toggle(tok)}
|
||||
className={[
|
||||
"focus-visible:ring-ring inline-flex h-11 min-w-11 items-center justify-center rounded-md border px-2 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2",
|
||||
isOn
|
||||
? "bg-primary text-primary-foreground border-primary"
|
||||
: "border-border bg-background text-foreground hover:bg-muted",
|
||||
].join(" ")}
|
||||
>
|
||||
{short}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { Moon, Sun, Sunrise, Sunset } from "lucide-react";
|
||||
|
||||
import { TimesOfDayChips } from "@/components/medications/scheduling/TimesOfDayChips";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
import type { StepProps } from "./Step1Name";
|
||||
|
||||
export function Step7Times(_props: StepProps) {
|
||||
return null;
|
||||
/**
|
||||
* Step 7 — Einnahmezeit(en). Reuses the v1.5 `TimesOfDayChips`
|
||||
* primitive. The preset row of sunrise / sun / sunset / moon icons
|
||||
* toggles 08:00 / 12:00 / 18:00 / 22:00 — same semantics as the
|
||||
* chips' own preset row, surfaced here as a visual continuation of
|
||||
* the wizard's icon language.
|
||||
*/
|
||||
const PRESETS = [
|
||||
{ time: "08:00", icon: Sunrise, key: "morning" },
|
||||
{ time: "12:00", icon: Sun, key: "noon" },
|
||||
{ time: "18:00", icon: Sunset, key: "evening" },
|
||||
{ time: "22:00", icon: Moon, key: "night" },
|
||||
] as const;
|
||||
|
||||
export function Step7Times({ payload, applyPartial }: StepProps) {
|
||||
const { t } = useTranslations();
|
||||
function togglePreset(time: string) {
|
||||
const has = payload.timesOfDay.includes(time);
|
||||
if (has) {
|
||||
applyPartial({
|
||||
timesOfDay: payload.timesOfDay.filter((x) => x !== time),
|
||||
});
|
||||
} else {
|
||||
applyPartial({
|
||||
timesOfDay: [...payload.timesOfDay, time].sort((a, b) =>
|
||||
a.localeCompare(b),
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="space-y-3" data-slot="wizard-step7">
|
||||
<div
|
||||
role="group"
|
||||
aria-label={t("medications.wizard.steps.step7.presetsLabel")}
|
||||
className="flex flex-wrap gap-1.5"
|
||||
data-slot="wizard-preset-row"
|
||||
>
|
||||
{PRESETS.map(({ time, icon: Icon, key }) => {
|
||||
const isOn = payload.timesOfDay.includes(time);
|
||||
const label = t(
|
||||
`medications.wizard.steps.step7.presets.${key}`,
|
||||
);
|
||||
return (
|
||||
<button
|
||||
key={time}
|
||||
type="button"
|
||||
aria-pressed={isOn}
|
||||
aria-label={`${label} ${time}`}
|
||||
data-slot="wizard-preset-chip"
|
||||
data-preset={key}
|
||||
data-active={isOn ? "true" : "false"}
|
||||
onClick={() => togglePreset(time)}
|
||||
className={[
|
||||
"focus-visible:ring-ring inline-flex h-11 items-center gap-1.5 rounded-md border px-3 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2",
|
||||
isOn
|
||||
? "bg-primary text-primary-foreground border-primary"
|
||||
: "border-border bg-background text-foreground hover:bg-muted",
|
||||
].join(" ")}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
<span>{time}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<TimesOfDayChips
|
||||
value={payload.timesOfDay}
|
||||
onChange={(timesOfDay) => applyPartial({ timesOfDay })}
|
||||
maxChips={8}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import type {
|
||||
MedicationPayload,
|
||||
WizardPayload,
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
import {
|
||||
type MedicationPayload,
|
||||
summariseCadence,
|
||||
type WizardPayload,
|
||||
} from "../wizard-payload";
|
||||
|
||||
export interface Step8SummaryProps {
|
||||
@@ -13,6 +18,76 @@ export interface Step8SummaryProps {
|
||||
submitError: string | null;
|
||||
}
|
||||
|
||||
export function Step8Summary(_props: Step8SummaryProps) {
|
||||
return null;
|
||||
export function Step8Summary({
|
||||
payload,
|
||||
applyPartial,
|
||||
mode,
|
||||
initial,
|
||||
submitError,
|
||||
}: Step8SummaryProps) {
|
||||
const { t } = useTranslations();
|
||||
const summary = summariseCadence(payload, t);
|
||||
|
||||
// v1.5.4 ships single-schedule editing only. A medication that
|
||||
// already carries multiple schedules surfaces a note pointing at
|
||||
// the legacy detail form; the multi-schedule compose-mode lands in
|
||||
// v1.5.5 per the design hand-off (D-1 §10 deferred bullet 2).
|
||||
const isMultiSchedule =
|
||||
mode === "edit" && (initial?.schedules.length ?? 0) > 1;
|
||||
|
||||
return (
|
||||
<div className="space-y-4" data-slot="wizard-step8">
|
||||
<div
|
||||
className="bg-muted/40 rounded-md border p-3 text-sm"
|
||||
data-slot="wizard-summary"
|
||||
>
|
||||
<p className="mb-1 font-medium">
|
||||
{t("medications.wizard.summary.title")}
|
||||
</p>
|
||||
<p>{summary}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label
|
||||
htmlFor="wizard-reminders"
|
||||
className="text-sm font-medium"
|
||||
>
|
||||
{t("medications.wizard.steps.step8.remindersLabel")}
|
||||
</Label>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("medications.wizard.steps.step8.remindersDescription")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="wizard-reminders"
|
||||
checked={payload.notificationsEnabled}
|
||||
onCheckedChange={(checked) =>
|
||||
applyPartial({ notificationsEnabled: checked })
|
||||
}
|
||||
data-slot="wizard-reminders-toggle"
|
||||
aria-label={t("medications.wizard.steps.step8.remindersLabel")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isMultiSchedule && (
|
||||
<p
|
||||
className="text-muted-foreground border-border/70 rounded-md border border-dashed p-3 text-xs"
|
||||
data-slot="wizard-multi-schedule-note"
|
||||
>
|
||||
{t("medications.wizard.steps.step8.multiScheduleNote")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{submitError && (
|
||||
<p
|
||||
className="text-destructive text-sm"
|
||||
role="alert"
|
||||
data-slot="wizard-submit-error"
|
||||
>
|
||||
{submitError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user