diff --git a/src/components/medications/wizard/steps/Step1Name.tsx b/src/components/medications/wizard/steps/Step1Name.tsx index 8900c2da1..d406e81c0 100644 --- a/src/components/medications/wizard/steps/Step1Name.tsx +++ b/src/components/medications/wizard/steps/Step1Name.tsx @@ -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) => void; } -export function Step1Name(_props: StepProps) { - return null; +export function Step1Name({ payload, applyPartial }: StepProps) { + const { t } = useTranslations(); + return ( +
+ + applyPartial({ name: e.target.value })} + placeholder={t("medications.wizard.steps.step1.placeholder")} + maxLength={100} + autoCapitalize="words" + autoComplete="off" + enterKeyHint="next" + className="h-11" + /> +
+ ); } diff --git a/src/components/medications/wizard/steps/Step2Class.tsx b/src/components/medications/wizard/steps/Step2Class.tsx index 154bbd3bc..49a9c9903 100644 --- a/src/components/medications/wizard/steps/Step2Class.tsx +++ b/src/components/medications/wizard/steps/Step2Class.tsx @@ -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 +> = { + 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 ( +
+ {WIZARD_TREATMENT_ROWS.map((row) => { + const Icon = ROW_ICONS[row]; + const selected = payload.treatmentRow === row; + const label = t(`medications.wizard.classRow.${row}`); + return ( + + ); + })} +
+ ); } diff --git a/src/components/medications/wizard/steps/Step3Dose.tsx b/src/components/medications/wizard/steps/Step3Dose.tsx index 4a315ef5d..06740eb93 100644 --- a/src/components/medications/wizard/steps/Step3Dose.tsx +++ b/src/components/medications/wizard/steps/Step3Dose.tsx @@ -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 ( +
+
+ + applyPartial({ doseAmount: e.target.value })} + placeholder={t("medications.wizard.steps.step3.amountPlaceholder")} + maxLength={20} + autoComplete="off" + enterKeyHint="next" + className="h-11" + /> +
+
+ + +
+
+ ); } diff --git a/src/components/medications/wizard/steps/Step4Window.tsx b/src/components/medications/wizard/steps/Step4Window.tsx index b3d7aaaf2..26dcf69b5 100644 --- a/src/components/medications/wizard/steps/Step4Window.tsx +++ b/src/components/medications/wizard/steps/Step4Window.tsx @@ -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 ( +
+ + applyPartial({ startsOn, endsOn }) + } + /> +
+ ); } diff --git a/src/components/medications/wizard/steps/Step5Cadence.tsx b/src/components/medications/wizard/steps/Step5Cadence.tsx index 88ed62b97..3fcbb7235 100644 --- a/src/components/medications/wizard/steps/Step5Cadence.tsx +++ b/src/components/medications/wizard/steps/Step5Cadence.tsx @@ -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 ( +
+ {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 ( + + ); + })} +
+ ); +} + +function applyRowPick( + row: Step5Row, + payload: WizardPayload, + applyPartial: (partial: Partial) => 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), + }); +} diff --git a/src/components/medications/wizard/steps/Step6SubCadence.tsx b/src/components/medications/wizard/steps/Step6SubCadence.tsx index 8546fe062..aa4831fe4 100644 --- a/src/components/medications/wizard/steps/Step6SubCadence.tsx +++ b/src/components/medications/wizard/steps/Step6SubCadence.tsx @@ -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 `` 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) { + const nextSub = { ...payload.subControls, ...patch }; + applyPartial({ + subControls: nextSub, + cadence: encodeCadence(kind, nextSub), + }); + } + + if (kind === "weekdays" || kind === "everyNWeeks") { + return ( +
+ {kind === "everyNWeeks" && ( +
+ + { + const n = Number(e.target.value); + if (Number.isFinite(n) && n >= 1 && n <= 12) { + patchSub({ intervalWeeks: Math.trunc(n) }); + } + }} + className="h-11 w-20" + /> + + {t("medications.wizard.steps.step6.intervalWeeks.suffix")} + +
+ )} + patchSub({ weekdays })} + /> +
+ ); + } + + if (kind === "monthly") { + return ( +
+ + { + const n = Number(e.target.value); + if (Number.isFinite(n) && n >= 1 && n <= 31) { + patchSub({ dayOfMonth: Math.trunc(n) }); + } + }} + className="h-11 w-20" + /> + + {t("medications.wizard.steps.step6.dayOfMonth.suffix")} + +
+ ); + } + + if (kind === "rolling") { + return ( +
+ + { + const n = Number(e.target.value); + if (Number.isFinite(n) && n >= 1 && n <= 365) { + patchSub({ rollingDays: Math.trunc(n) }); + } + }} + className="h-11 w-20" + /> + + {t("medications.wizard.steps.step6.rollingDays.suffix")} + +
+ ); + } + 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 ( +
+ {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 ( + + ); + })} +
+ ); +} diff --git a/src/components/medications/wizard/steps/Step7Times.tsx b/src/components/medications/wizard/steps/Step7Times.tsx index 2d8916f05..54784c074 100644 --- a/src/components/medications/wizard/steps/Step7Times.tsx +++ b/src/components/medications/wizard/steps/Step7Times.tsx @@ -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 ( +
+
+ {PRESETS.map(({ time, icon: Icon, key }) => { + const isOn = payload.timesOfDay.includes(time); + const label = t( + `medications.wizard.steps.step7.presets.${key}`, + ); + return ( + + ); + })} +
+ applyPartial({ timesOfDay })} + maxChips={8} + /> +
+ ); } diff --git a/src/components/medications/wizard/steps/Step8Summary.tsx b/src/components/medications/wizard/steps/Step8Summary.tsx index e750f5bab..0a8e2fd4b 100644 --- a/src/components/medications/wizard/steps/Step8Summary.tsx +++ b/src/components/medications/wizard/steps/Step8Summary.tsx @@ -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 ( +
+
+

+ {t("medications.wizard.summary.title")} +

+

{summary}

+
+ +
+
+ +

+ {t("medications.wizard.steps.step8.remindersDescription")} +

+
+ + applyPartial({ notificationsEnabled: checked }) + } + data-slot="wizard-reminders-toggle" + aria-label={t("medications.wizard.steps.step8.remindersLabel")} + /> +
+ + {isMultiSchedule && ( +

+ {t("medications.wizard.steps.step8.multiScheduleNote")} +

+ )} + + {submitError && ( +

+ {submitError} +

+ )} +
+ ); }