Add DateTimeStep page (wizard step 6)

- Centered-form layout with two fieldset sections: name + scheduling
- Grief-sensitive labels: "Their first name", "About the person who died"
- Pre-planning variant: softer copy, "About the person" (no "who died")
- Date preference: ASAP / specific with progressive disclosure date picker
- Time preference: 5-option radio (no preference, morning, midday, afternoon, evening)
- Religion/service style: Autocomplete with 22 options
- Save-and-exit tertiary CTA
- showNameFields + showScheduling props for conditional visibility per funeral type

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 14:51:09 +11:00
parent 9738e6e893
commit 2004fe10c0
3 changed files with 550 additions and 0 deletions

View File

@@ -0,0 +1,208 @@
import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { DateTimeStep } from './DateTimeStep';
import type { DateTimeStepValues, DateTimeStepErrors } from './DateTimeStep';
import { Navigation } from '../../organisms/Navigation';
import Box from '@mui/material/Box';
// ─── Helpers ─────────────────────────────────────────────────────────────────
const FALogo = () => (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box
component="img"
src="/brandlogo/logo-full.svg"
alt="Funeral Arranger"
sx={{ height: 28, display: { xs: 'none', md: 'block' } }}
/>
<Box
component="img"
src="/brandlogo/logo-short.svg"
alt="Funeral Arranger"
sx={{ height: 28, display: { xs: 'block', md: 'none' } }}
/>
</Box>
);
const nav = (
<Navigation
logo={<FALogo />}
items={[
{ label: 'FAQ', href: '/faq' },
{ label: 'Contact Us', href: '/contact' },
]}
/>
);
const defaultValues: DateTimeStepValues = {
firstName: '',
surname: '',
funeralDate: 'asap',
funeralDateSpecific: '',
funeralTime: 'no_preference',
religion: null,
};
// ─── Meta ────────────────────────────────────────────────────────────────────
const meta: Meta<typeof DateTimeStep> = {
title: 'Pages/DateTimeStep',
component: DateTimeStep,
tags: ['autodocs'],
parameters: {
layout: 'fullscreen',
},
};
export default meta;
type Story = StoryObj<typeof DateTimeStep>;
// ─── Interactive (default) — at-need ────────────────────────────────────────
/** At-need flow — all fields visible, grief-sensitive copy */
export const Default: Story = {
render: () => {
const [values, setValues] = useState<DateTimeStepValues>({ ...defaultValues });
const [errors, setErrors] = useState<DateTimeStepErrors>({});
const handleContinue = () => {
const newErrors: DateTimeStepErrors = {};
if (!values.firstName)
newErrors.firstName = 'We need their first name for the funeral arrangements.';
if (!values.surname)
newErrors.surname = 'We need their surname for the funeral arrangements.';
if (values.funeralDate === 'specific' && !values.funeralDateSpecific)
newErrors.funeralDateSpecific = 'Please select a preferred date.';
setErrors(newErrors);
if (Object.keys(newErrors).length === 0)
alert(`Continue: ${values.firstName} ${values.surname}`);
};
return (
<DateTimeStep
values={values}
onChange={(v) => {
setValues(v);
setErrors({});
}}
onContinue={handleContinue}
onBack={() => alert('Back')}
onSaveAndExit={() => alert('Save and exit')}
errors={errors}
isAtNeed
navigation={nav}
/>
);
},
};
// ─── Pre-planning ───────────────────────────────────────────────────────────
/** Pre-planning flow — softer copy, helper text about updating later */
export const PrePlanning: Story = {
render: () => {
const [values, setValues] = useState<DateTimeStepValues>({ ...defaultValues });
return (
<DateTimeStep
values={values}
onChange={setValues}
onContinue={() => alert('Continue')}
onBack={() => alert('Back')}
onSaveAndExit={() => alert('Save')}
isAtNeed={false}
navigation={nav}
/>
);
},
};
// ─── Specific date selected ─────────────────────────────────────────────────
/** Date picker revealed when "specific date" is selected */
export const SpecificDate: Story = {
render: () => {
const [values, setValues] = useState<DateTimeStepValues>({
...defaultValues,
firstName: 'Margaret',
surname: 'Wilson',
funeralDate: 'specific',
funeralTime: 'morning',
});
return (
<DateTimeStep
values={values}
onChange={setValues}
onContinue={() => alert('Continue')}
onBack={() => alert('Back')}
isAtNeed
navigation={nav}
/>
);
},
};
// ─── Name fields only (cremation/burial only) ──────────────────────────────
/** Scheduling hidden — only name fields shown (e.g. cremation-only at-need) */
export const NameFieldsOnly: Story = {
render: () => {
const [values, setValues] = useState<DateTimeStepValues>({ ...defaultValues });
return (
<DateTimeStep
values={values}
onChange={setValues}
onContinue={() => alert('Continue')}
onBack={() => alert('Back')}
showScheduling={false}
navigation={nav}
/>
);
},
};
// ─── Validation errors ──────────────────────────────────────────────────────
/** All validation errors showing */
export const WithErrors: Story = {
render: () => {
const [values, setValues] = useState<DateTimeStepValues>({ ...defaultValues });
return (
<DateTimeStep
values={values}
onChange={setValues}
onContinue={() => {}}
errors={{
firstName: 'We need their first name for the funeral arrangements.',
surname: 'We need their surname for the funeral arrangements.',
}}
onBack={() => alert('Back')}
navigation={nav}
/>
);
},
};
// ─── Loading ────────────────────────────────────────────────────────────────
/** Continue button in loading state */
export const Loading: Story = {
render: () => {
const [values, setValues] = useState<DateTimeStepValues>({
...defaultValues,
firstName: 'Margaret',
surname: 'Wilson',
funeralDate: 'asap',
funeralTime: 'morning',
religion: 'Anglican',
});
return (
<DateTimeStep
values={values}
onChange={setValues}
onContinue={() => {}}
loading
navigation={nav}
/>
);
},
};

View File

@@ -0,0 +1,334 @@
import React from 'react';
import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import FormControl from '@mui/material/FormControl';
import FormLabel from '@mui/material/FormLabel';
import FormControlLabel from '@mui/material/FormControlLabel';
import RadioGroup from '@mui/material/RadioGroup';
import Radio from '@mui/material/Radio';
import type { SxProps, Theme } from '@mui/material/styles';
import { WizardLayout } from '../../templates/WizardLayout';
import { Collapse } from '../../atoms/Collapse';
import { Typography } from '../../atoms/Typography';
import { Button } from '../../atoms/Button';
import { Divider } from '../../atoms/Divider';
// ─── Types ───────────────────────────────────────────────────────────────────
/** Funeral date preference */
export type FuneralDatePref = 'asap' | 'specific' | null;
/** Time-of-day preference */
export type FuneralTimePref = 'no_preference' | 'morning' | 'midday' | 'afternoon' | 'evening';
/** Form values for the date/time step */
export interface DateTimeStepValues {
/** Deceased first name */
firstName: string;
/** Deceased surname */
surname: string;
/** Date preference (ASAP or specific) */
funeralDate: FuneralDatePref;
/** Specific date string (ISO format) when funeralDate is "specific" */
funeralDateSpecific: string;
/** Time-of-day preference */
funeralTime: FuneralTimePref;
/** Service style / religion preference */
religion: string | null;
}
/** Field-level error messages */
export interface DateTimeStepErrors {
firstName?: string;
surname?: string;
funeralDate?: string;
funeralDateSpecific?: string;
funeralTime?: string;
}
/** Props for the DateTimeStep page component */
export interface DateTimeStepProps {
/** Current form values */
values: DateTimeStepValues;
/** Callback when any field value changes */
onChange: (values: DateTimeStepValues) => void;
/** Callback when the Continue button is clicked */
onContinue: () => void;
/** Callback for back navigation */
onBack?: () => void;
/** Callback for save-and-exit */
onSaveAndExit?: () => void;
/** Field-level validation errors */
errors?: DateTimeStepErrors;
/** Whether the Continue button is in a loading state */
loading?: boolean;
/** Whether the person has passed (at-need) or is alive (pre-planning) */
isAtNeed?: boolean;
/** Whether deceased name fields should be shown */
showNameFields?: boolean;
/** Whether scheduling fields should be shown */
showScheduling?: boolean;
/** Available religion/service style options */
religionOptions?: string[];
/** Navigation bar — passed through to WizardLayout */
navigation?: React.ReactNode;
/** Hide the help bar */
hideHelpBar?: boolean;
/** MUI sx prop for the root */
sx?: SxProps<Theme>;
}
// ─── Constants ───────────────────────────────────────────────────────────────
const DEFAULT_RELIGIONS = [
'No Religion',
'Civil Celebrant',
'Aboriginal',
'Anglican',
'Baptist',
'Buddhism',
'Catholic',
'Christian',
'Hinduism',
'Indigenous',
'Islam',
'Judaism',
'Lutheran',
'Oriental Orthodox',
'Eastern Orthodox',
'Greek Orthodox',
'Russian Orthodox',
'Serbian Orthodox',
'Pentecostal',
'Presbyterian',
'Sikhism',
'Uniting Church',
];
// ─── Component ───────────────────────────────────────────────────────────────
/**
* Step 6 — Details & Scheduling for the FA arrangement wizard.
*
* Captures deceased details (name) and funeral date/time preferences.
* Two logical sections: "About the person" and "Service timing".
* Each wrapped in fieldset/legend for screen reader structure.
*
* Field visibility is controlled by props (showNameFields, showScheduling)
* since it depends on funeral type and pre-planning status.
*
* Grief-sensitive labels: "Their first name" not "Deceased First Name".
* Pre-planning copy: "About the person" (no "who died").
*
* Pure presentation component — props in, callbacks out.
*
* Spec: documentation/steps/steps/06_date_time.yaml
*/
export const DateTimeStep: React.FC<DateTimeStepProps> = ({
values,
onChange,
onContinue,
onBack,
onSaveAndExit,
errors,
loading = false,
isAtNeed = true,
showNameFields = true,
showScheduling = true,
religionOptions = DEFAULT_RELIGIONS,
navigation,
hideHelpBar,
sx,
}) => {
const handleFieldChange = <K extends keyof DateTimeStepValues>(
field: K,
value: DateTimeStepValues[K],
) => {
onChange({ ...values, [field]: value });
};
const personSectionHeading = isAtNeed ? 'About the person who died' : 'About the person';
const schedulingHeading = isAtNeed
? 'When are you hoping to have the service?'
: 'When would you like the service?';
return (
<WizardLayout
variant="centered-form"
navigation={navigation}
showBackLink={!!onBack}
backLabel="Back"
onBack={onBack}
hideHelpBar={hideHelpBar}
sx={sx}
>
{/* Page heading */}
<Typography variant="display3" component="h1" sx={{ mb: 1 }} tabIndex={-1}>
A few important details
</Typography>
{!isAtNeed && (
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
If you&apos;re not sure about dates yet, that&apos;s fine. You can update this later.
</Typography>
)}
<Box
component="form"
noValidate
onSubmit={(e: React.FormEvent) => {
e.preventDefault();
onContinue();
}}
>
{/* ─── Section 1: About the person ─── */}
{showNameFields && (
<Box component="fieldset" sx={{ border: 0, m: 0, p: 0, mb: 4 }}>
<Typography component="legend" variant="h5" sx={{ mb: 2.5 }}>
{personSectionHeading}
</Typography>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
gap: 2,
mb: 2,
}}
>
<TextField
label="Their first name"
value={values.firstName}
onChange={(e) => handleFieldChange('firstName', e.target.value)}
error={!!errors?.firstName}
helperText={errors?.firstName}
autoComplete="off"
fullWidth
required
/>
<TextField
label="Their surname"
value={values.surname}
onChange={(e) => handleFieldChange('surname', e.target.value)}
error={!!errors?.surname}
helperText={errors?.surname}
autoComplete="off"
fullWidth
required
/>
</Box>
</Box>
)}
{/* ─── Section 2: Scheduling ─── */}
{showScheduling && (
<Box component="fieldset" sx={{ border: 0, m: 0, p: 0, mb: 3 }}>
<Typography component="legend" variant="h5" sx={{ mb: 2.5 }}>
{schedulingHeading}
</Typography>
{/* Date preference */}
<FormControl component="fieldset" sx={{ mb: 3, display: 'block' }}>
<FormLabel component="legend" sx={{ mb: 1 }}>
Preferred timing
</FormLabel>
<RadioGroup
value={values.funeralDate ?? ''}
onChange={(e) =>
handleFieldChange('funeralDate', e.target.value as FuneralDatePref)
}
>
<FormControlLabel value="asap" control={<Radio />} label="As soon as possible" />
<FormControlLabel
value="specific"
control={<Radio />}
label="I have a specific date in mind"
/>
</RadioGroup>
</FormControl>
{/* Specific date — progressive disclosure */}
<Collapse in={values.funeralDate === 'specific'}>
<TextField
label="Preferred date"
type="date"
value={values.funeralDateSpecific}
onChange={(e) => handleFieldChange('funeralDateSpecific', e.target.value)}
error={!!errors?.funeralDateSpecific}
helperText={errors?.funeralDateSpecific}
InputLabelProps={{ shrink: true }}
inputProps={{ min: new Date().toISOString().split('T')[0] }}
fullWidth
required
sx={{ mb: 3 }}
/>
</Collapse>
{/* Time-of-day preference */}
<FormControl component="fieldset" sx={{ mb: 3, display: 'block' }}>
<FormLabel component="legend" sx={{ mb: 1 }}>
Do you have a preferred time of day?
</FormLabel>
<RadioGroup
value={values.funeralTime}
onChange={(e) =>
handleFieldChange('funeralTime', e.target.value as FuneralTimePref)
}
>
<FormControlLabel value="no_preference" control={<Radio />} label="No preference" />
<FormControlLabel value="morning" control={<Radio />} label="Morning" />
<FormControlLabel value="midday" control={<Radio />} label="Midday" />
<FormControlLabel value="afternoon" control={<Radio />} label="Afternoon" />
<FormControlLabel value="evening" control={<Radio />} label="Evening" />
</RadioGroup>
</FormControl>
</Box>
)}
{/* ─── Service style (religion) ─── */}
<Autocomplete
options={religionOptions}
value={values.religion}
onChange={(_, newValue) => handleFieldChange('religion', newValue)}
renderInput={(params) => (
<TextField
{...params}
label="Service style preference"
placeholder="Select a service style (optional)"
/>
)}
sx={{ mb: 4 }}
/>
<Divider sx={{ my: 3 }} />
{/* CTAs */}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexDirection: { xs: 'column-reverse', sm: 'row' },
gap: 2,
}}
>
{onSaveAndExit ? (
<Button variant="text" color="secondary" onClick={onSaveAndExit} type="button">
Save and continue later
</Button>
) : (
<Box />
)}
<Button type="submit" variant="contained" size="large" loading={loading}>
Continue
</Button>
</Box>
</Box>
</WizardLayout>
);
};
DateTimeStep.displayName = 'DateTimeStep';
export default DateTimeStep;

View File

@@ -0,0 +1,8 @@
export { DateTimeStep, default } from './DateTimeStep';
export type {
DateTimeStepProps,
DateTimeStepValues,
DateTimeStepErrors,
FuneralDatePref,
FuneralTimePref,
} from './DateTimeStep';