- Heading: display3 for centered-form consistency with IntroStep
- Name fields: swap MUI TextField for Input atom (external label, no clipping)
- Date preferences: single date → up to 3 preferred dates with progressive
disclosure ("+ Add another date" link, × remove on 2nd/3rd)
- Remove service style/religion field — tradition flows from provider/package
selection and is confirmed on summary step
- Add dividers between question sections for visual separation
- Fix spacing between sections (mb: 5, mb: 3 on headings)
- FormLabels styled with fontWeight 600 for readability
- Updated types: preferredDates: string[] replaces funeralDateSpecific,
removed religion field
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
397 lines
14 KiB
TypeScript
397 lines
14 KiB
TypeScript
import React from 'react';
|
|
import Box from '@mui/material/Box';
|
|
import TextField from '@mui/material/TextField';
|
|
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 IconButton from '@mui/material/IconButton';
|
|
import CloseIcon from '@mui/icons-material/Close';
|
|
import type { SxProps, Theme } from '@mui/material/styles';
|
|
import { WizardLayout } from '../../templates/WizardLayout';
|
|
import { Input } from '../../atoms/Input';
|
|
import { Collapse } from '../../atoms/Collapse';
|
|
import { Typography } from '../../atoms/Typography';
|
|
import { Button } from '../../atoms/Button';
|
|
import { Link } from '../../atoms/Link';
|
|
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;
|
|
/** Preferred dates (up to 3) when funeralDate is "specific" */
|
|
preferredDates: string[];
|
|
/** Time-of-day preference */
|
|
funeralTime: FuneralTimePref;
|
|
}
|
|
|
|
/** Field-level error messages */
|
|
export interface DateTimeStepErrors {
|
|
firstName?: string;
|
|
surname?: string;
|
|
funeralDate?: string;
|
|
preferredDates?: 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;
|
|
/** 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 MAX_PREFERRED_DATES = 3;
|
|
|
|
const DATE_LABELS = ['First preference', 'Second preference', 'Third preference'];
|
|
|
|
// ─── 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.
|
|
*
|
|
* Date preferences support up to 3 preferred dates via progressive
|
|
* disclosure ("+ Add another date" link).
|
|
*
|
|
* Service tradition (religion) is NOT captured here — it flows through
|
|
* from provider/package selection and is confirmed on the summary step.
|
|
*
|
|
* Grief-sensitive labels: "Their first name" not "Deceased First Name".
|
|
*
|
|
* 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,
|
|
navigation,
|
|
hideHelpBar,
|
|
sx,
|
|
}) => {
|
|
const handleFieldChange = <K extends keyof DateTimeStepValues>(
|
|
field: K,
|
|
value: DateTimeStepValues[K],
|
|
) => {
|
|
onChange({ ...values, [field]: value });
|
|
};
|
|
|
|
const handleDateChange = (index: number, value: string) => {
|
|
const next = [...values.preferredDates];
|
|
next[index] = value;
|
|
onChange({ ...values, preferredDates: next });
|
|
};
|
|
|
|
const handleAddDate = () => {
|
|
if (values.preferredDates.length < MAX_PREFERRED_DATES) {
|
|
onChange({ ...values, preferredDates: [...values.preferredDates, ''] });
|
|
}
|
|
};
|
|
|
|
const handleRemoveDate = (index: number) => {
|
|
const next = values.preferredDates.filter((_, i) => i !== index);
|
|
onChange({ ...values, preferredDates: next.length === 0 ? [''] : next });
|
|
};
|
|
|
|
const personSectionHeading = isAtNeed ? 'About the person who has passed' : 'About the person';
|
|
|
|
const schedulingHeading = isAtNeed
|
|
? 'When are you hoping to have the service?'
|
|
: 'When would you like the service?';
|
|
|
|
const minDate = new Date().toISOString().split('T')[0];
|
|
|
|
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>
|
|
|
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 5 }}>
|
|
{isAtNeed
|
|
? 'We just need a few details to help arrange the service.'
|
|
: "If you're not sure about dates yet, that's fine. You can update this later."}
|
|
</Typography>
|
|
|
|
<Box
|
|
component="form"
|
|
noValidate
|
|
aria-busy={loading}
|
|
onSubmit={(e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!loading) onContinue();
|
|
}}
|
|
>
|
|
{/* ─── Section 1: About the person ─── */}
|
|
{showNameFields && (
|
|
<Box component="fieldset" sx={{ border: 0, m: 0, p: 0, mb: 2 }}>
|
|
<Typography component="legend" variant="h5" sx={{ mb: 3 }}>
|
|
{personSectionHeading}
|
|
</Typography>
|
|
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: { xs: 'column', sm: 'row' },
|
|
gap: 2.5,
|
|
}}
|
|
>
|
|
<Input
|
|
label="Their first name"
|
|
value={values.firstName}
|
|
onChange={(e) => handleFieldChange('firstName', e.target.value)}
|
|
error={!!errors?.firstName}
|
|
helperText={errors?.firstName}
|
|
autoComplete="off"
|
|
fullWidth
|
|
required
|
|
/>
|
|
<Input
|
|
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>
|
|
)}
|
|
|
|
{showNameFields && showScheduling && <Divider sx={{ my: 4 }} />}
|
|
|
|
{/* ─── Section 2: Scheduling ─── */}
|
|
{showScheduling && (
|
|
<Box component="fieldset" sx={{ border: 0, m: 0, p: 0, mb: 5 }}>
|
|
<Typography component="legend" variant="h5" sx={{ mb: 3 }}>
|
|
{schedulingHeading}
|
|
</Typography>
|
|
|
|
{/* Date preference */}
|
|
<FormControl component="fieldset" sx={{ mb: 3, display: 'block' }}>
|
|
<FormLabel
|
|
component="legend"
|
|
sx={{
|
|
mb: 1.5,
|
|
color: 'text.primary',
|
|
fontWeight: 600,
|
|
'&.Mui-focused': { color: 'text.primary' },
|
|
}}
|
|
>
|
|
Preferred timing
|
|
</FormLabel>
|
|
<RadioGroup
|
|
value={values.funeralDate ?? ''}
|
|
onChange={(e) => {
|
|
const pref = e.target.value as FuneralDatePref;
|
|
onChange({
|
|
...values,
|
|
funeralDate: pref,
|
|
// Initialise with one empty date slot when switching to specific
|
|
preferredDates:
|
|
pref === 'specific' && values.preferredDates.length === 0
|
|
? ['']
|
|
: values.preferredDates,
|
|
});
|
|
}}
|
|
>
|
|
<FormControlLabel
|
|
value="asap"
|
|
control={<Radio />}
|
|
label="As soon as possible"
|
|
sx={{ mb: 0.5 }}
|
|
/>
|
|
<FormControlLabel
|
|
value="specific"
|
|
control={<Radio />}
|
|
label="I have a preferred date"
|
|
/>
|
|
</RadioGroup>
|
|
</FormControl>
|
|
|
|
{/* Preferred dates — progressive disclosure, up to 3 */}
|
|
<Collapse in={values.funeralDate === 'specific'}>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3, pl: 4 }}>
|
|
{values.preferredDates.map((date, index) => (
|
|
<Box key={index} sx={{ display: 'flex', alignItems: 'flex-start', gap: 1 }}>
|
|
<TextField
|
|
label={DATE_LABELS[index] ?? `Preference ${index + 1}`}
|
|
type="date"
|
|
value={date}
|
|
onChange={(e) => handleDateChange(index, e.target.value)}
|
|
error={index === 0 && !!errors?.preferredDates}
|
|
helperText={index === 0 ? errors?.preferredDates : undefined}
|
|
InputLabelProps={{ shrink: true }}
|
|
inputProps={{ min: minDate }}
|
|
fullWidth
|
|
required={index === 0}
|
|
/>
|
|
{index > 0 && (
|
|
<IconButton
|
|
onClick={() => handleRemoveDate(index)}
|
|
aria-label={`Remove ${DATE_LABELS[index]?.toLowerCase() ?? 'date'}`}
|
|
sx={{ mt: 1, color: 'text.secondary' }}
|
|
>
|
|
<CloseIcon fontSize="small" />
|
|
</IconButton>
|
|
)}
|
|
</Box>
|
|
))}
|
|
|
|
{values.preferredDates.length < MAX_PREFERRED_DATES && (
|
|
<Link
|
|
component="button"
|
|
type="button"
|
|
onClick={handleAddDate}
|
|
underline="hover"
|
|
sx={{ alignSelf: 'flex-start', fontSize: '0.875rem' }}
|
|
>
|
|
+ Add another date
|
|
</Link>
|
|
)}
|
|
</Box>
|
|
</Collapse>
|
|
|
|
<Divider sx={{ my: 4 }} />
|
|
|
|
{/* Time-of-day preference */}
|
|
<FormControl component="fieldset" sx={{ display: 'block' }}>
|
|
<FormLabel
|
|
component="legend"
|
|
sx={{
|
|
mb: 1.5,
|
|
color: 'text.primary',
|
|
fontWeight: 600,
|
|
'&.Mui-focused': { color: 'text.primary' },
|
|
}}
|
|
>
|
|
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"
|
|
sx={{ mb: 0.5 }}
|
|
/>
|
|
<FormControlLabel
|
|
value="morning"
|
|
control={<Radio />}
|
|
label="Morning"
|
|
sx={{ mb: 0.5 }}
|
|
/>
|
|
<FormControlLabel
|
|
value="midday"
|
|
control={<Radio />}
|
|
label="Midday"
|
|
sx={{ mb: 0.5 }}
|
|
/>
|
|
<FormControlLabel
|
|
value="afternoon"
|
|
control={<Radio />}
|
|
label="Afternoon"
|
|
sx={{ mb: 0.5 }}
|
|
/>
|
|
<FormControlLabel value="evening" control={<Radio />} label="Evening" />
|
|
</RadioGroup>
|
|
</FormControl>
|
|
</Box>
|
|
)}
|
|
|
|
<Divider sx={{ my: 4 }} />
|
|
|
|
{/* 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;
|