From 1f9f7e26112b5fbda7e41abbbebbe416ac005a41 Mon Sep 17 00:00:00 2001 From: Richie Date: Sun, 29 Mar 2026 14:55:34 +1100 Subject: [PATCH] Add CrematoriumStep page (wizard step 8) - Single crematorium: confirmation card (pre-selected), most common case - Multiple crematoriums: card grid with radiogroup pattern + roving tabindex - Witness question: "Will anyone follow the hearse?" with personalised helper text - Special instructions: radio + progressive disclosure textarea via Collapse - Personalised copy with deceased name ("escort [Name] to the crematorium") - Pre-planning variant with softer helper text - Australian terminology: "crematorium" not "crematory" Co-Authored-By: Claude Opus 4.6 (1M context) --- .../CrematoriumStep.stories.tsx | 217 +++++++++++ .../pages/CrematoriumStep/CrematoriumStep.tsx | 340 ++++++++++++++++++ src/components/pages/CrematoriumStep/index.ts | 7 + 3 files changed, 564 insertions(+) create mode 100644 src/components/pages/CrematoriumStep/CrematoriumStep.stories.tsx create mode 100644 src/components/pages/CrematoriumStep/CrematoriumStep.tsx create mode 100644 src/components/pages/CrematoriumStep/index.ts diff --git a/src/components/pages/CrematoriumStep/CrematoriumStep.stories.tsx b/src/components/pages/CrematoriumStep/CrematoriumStep.stories.tsx new file mode 100644 index 0000000..ff57a58 --- /dev/null +++ b/src/components/pages/CrematoriumStep/CrematoriumStep.stories.tsx @@ -0,0 +1,217 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { CrematoriumStep } from './CrematoriumStep'; +import type { CrematoriumStepValues, CrematoriumStepErrors, Crematorium } from './CrematoriumStep'; +import { Navigation } from '../../organisms/Navigation'; +import Box from '@mui/material/Box'; + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +const FALogo = () => ( + + + + +); + +const nav = ( + } + items={[ + { label: 'FAQ', href: '/faq' }, + { label: 'Contact Us', href: '/contact' }, + ]} + /> +); + +const singleCrematorium: Crematorium[] = [ + { + id: 'warrill-park', + name: 'Warrill Park Crematorium', + location: 'Ipswich', + distance: '15 min from venue', + price: 850, + }, +]; + +const multipleCrematoriums: Crematorium[] = [ + { + id: 'warrill-park', + name: 'Warrill Park Crematorium', + location: 'Ipswich', + distance: '15 min from venue', + price: 850, + }, + { + id: 'mt-gravatt', + name: 'Mt Gravatt Crematorium', + location: 'Mt Gravatt', + distance: '25 min from venue', + price: 920, + }, + { + id: 'pinnaroo', + name: 'Pinnaroo Valley Memorial Park', + location: 'Padstow', + distance: '35 min from venue', + price: 780, + }, +]; + +const defaultValues: CrematoriumStepValues = { + selectedCrematoriumId: null, + attend: 'no', + priority: '', + hasInstructions: 'no', + specialInstructions: '', +}; + +// ─── Meta ──────────────────────────────────────────────────────────────────── + +const meta: Meta = { + title: 'Pages/CrematoriumStep', + component: CrematoriumStep, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +// ─── Single crematorium (most common) ─────────────────────────────────────── + +/** Single pre-selected crematorium — confirmation pattern */ +export const Default: Story = { + render: () => { + const [values, setValues] = useState({ + ...defaultValues, + selectedCrematoriumId: 'warrill-park', + }); + return ( + alert('Continue')} + onBack={() => alert('Back')} + onSaveAndExit={() => alert('Save')} + crematoriums={singleCrematorium} + deceasedName="Margaret" + navigation={nav} + /> + ); + }, +}; + +// ─── Multiple crematoriums ────────────────────────────────────────────────── + +/** Multiple options — card grid with selection */ +export const MultipleCrematoriums: Story = { + render: () => { + const [values, setValues] = useState({ ...defaultValues }); + const [errors, setErrors] = useState({}); + + const handleContinue = () => { + if (!values.selectedCrematoriumId) { + setErrors({ selectedCrematoriumId: 'Please confirm the crematorium.' }); + return; + } + alert(`Selected: ${values.selectedCrematoriumId}`); + }; + + return ( + { + setValues(v); + setErrors({}); + }} + onContinue={handleContinue} + onBack={() => alert('Back')} + errors={errors} + crematoriums={multipleCrematoriums} + deceasedName="Margaret" + navigation={nav} + /> + ); + }, +}; + +// ─── With special instructions ────────────────────────────────────────────── + +/** Special instructions textarea revealed */ +export const WithInstructions: Story = { + render: () => { + const [values, setValues] = useState({ + ...defaultValues, + selectedCrematoriumId: 'warrill-park', + attend: 'yes', + hasInstructions: 'yes', + specialInstructions: 'Please ensure the family has 10 minutes for a private farewell.', + }); + return ( + alert('Continue')} + onBack={() => alert('Back')} + crematoriums={singleCrematorium} + deceasedName="Margaret" + navigation={nav} + /> + ); + }, +}; + +// ─── Pre-planning ─────────────────────────────────────────────────────────── + +/** Pre-planning variant — softer helper text */ +export const PrePlanning: Story = { + render: () => { + const [values, setValues] = useState({ + ...defaultValues, + selectedCrematoriumId: 'warrill-park', + }); + return ( + alert('Continue')} + onBack={() => alert('Back')} + crematoriums={singleCrematorium} + isPrePlanning + navigation={nav} + /> + ); + }, +}; + +// ─── Validation error ─────────────────────────────────────────────────────── + +/** No crematorium selected with error */ +export const WithError: Story = { + render: () => { + const [values, setValues] = useState({ ...defaultValues }); + return ( + {}} + errors={{ selectedCrematoriumId: 'Please confirm the crematorium.' }} + crematoriums={multipleCrematoriums} + navigation={nav} + /> + ); + }, +}; diff --git a/src/components/pages/CrematoriumStep/CrematoriumStep.tsx b/src/components/pages/CrematoriumStep/CrematoriumStep.tsx new file mode 100644 index 0000000..1638104 --- /dev/null +++ b/src/components/pages/CrematoriumStep/CrematoriumStep.tsx @@ -0,0 +1,340 @@ +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 type { SxProps, Theme } from '@mui/material/styles'; +import { WizardLayout } from '../../templates/WizardLayout'; +import { Card } from '../../atoms/Card'; +import { Collapse } from '../../atoms/Collapse'; +import { Typography } from '../../atoms/Typography'; +import { Button } from '../../atoms/Button'; +import { Divider } from '../../atoms/Divider'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +/** A crematorium available for selection */ +export interface Crematorium { + id: string; + name: string; + location: string; + distance?: string; + price?: number; +} + +/** Form values for the crematorium step */ +export interface CrematoriumStepValues { + /** Selected crematorium ID */ + selectedCrematoriumId: string | null; + /** Will anyone follow the hearse? */ + attend: 'yes' | 'no'; + /** Cremation timing preference */ + priority: string; + /** Has special instructions */ + hasInstructions: 'yes' | 'no'; + /** Special instructions text */ + specialInstructions: string; +} + +/** Field-level error messages */ +export interface CrematoriumStepErrors { + selectedCrematoriumId?: string; + attend?: string; +} + +/** Props for the CrematoriumStep page component */ +export interface CrematoriumStepProps { + /** Current form values */ + values: CrematoriumStepValues; + /** Callback when any field value changes */ + onChange: (values: CrematoriumStepValues) => 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?: CrematoriumStepErrors; + /** Whether the Continue button is in a loading state */ + loading?: boolean; + /** Available crematoriums */ + crematoriums: Crematorium[]; + /** Priority/timing options (provider-specific) */ + priorityOptions?: Array<{ value: string; label: string }>; + /** Deceased first name — used to personalise copy */ + deceasedName?: string; + /** Whether this is a pre-planning flow */ + isPrePlanning?: boolean; + /** Navigation bar — passed through to WizardLayout */ + navigation?: React.ReactNode; + /** Progress stepper */ + progressStepper?: React.ReactNode; + /** Running total */ + runningTotal?: React.ReactNode; + /** Hide the help bar */ + hideHelpBar?: boolean; + /** MUI sx prop for the root */ + sx?: SxProps; +} + +// ─── Component ─────────────────────────────────────────────────────────────── + +/** + * Step 8 — Crematorium for the FA arrangement wizard. + * + * Select a crematorium and cremation witness/attendance preferences. + * Only shown for cremation-type funerals. + * + * Often a single pre-selected crematorium (provider's default). When + * multiple options exist, shows a card grid with radiogroup pattern. + * + * Personalises witness question with deceased name when available. + * + * Pure presentation component — props in, callbacks out. + * + * Spec: documentation/steps/steps/08_crematorium.yaml + */ +export const CrematoriumStep: React.FC = ({ + values, + onChange, + onContinue, + onBack, + onSaveAndExit, + errors, + loading = false, + crematoriums, + priorityOptions = [], + deceasedName, + isPrePlanning = false, + navigation, + progressStepper, + runningTotal, + hideHelpBar, + sx, +}) => { + const isSingle = crematoriums.length === 1; + + const handleFieldChange = ( + field: K, + value: CrematoriumStepValues[K], + ) => { + onChange({ ...values, [field]: value }); + }; + + const witnessHelper = deceasedName + ? `Please indicate if you wish to follow the hearse in your own vehicles to escort ${deceasedName} to the crematorium.` + : isPrePlanning + ? 'This can be decided closer to the time.' + : 'Please indicate if anyone wishes to follow the hearse to the crematorium.'; + + return ( + + {/* Page heading */} + + Crematorium + + + { + e.preventDefault(); + onContinue(); + }} + > + {/* ─── Crematorium selection ─── */} + + {isSingle ? ( + // Single crematorium — presented as confirmation + handleFieldChange('selectedCrematoriumId', crematoriums[0].id)} + role="radio" + aria-checked={values.selectedCrematoriumId === crematoriums[0].id} + tabIndex={0} + sx={{ p: 3 }} + > + {crematoriums[0].name} + + {crematoriums[0].location} + {crematoriums[0].distance && ` \u2022 ${crematoriums[0].distance}`} + + {crematoriums[0].price != null && ( + + ${crematoriums[0].price.toLocaleString('en-AU')} + + )} + + ) : ( + // Multiple crematoriums — radiogroup grid + + + {crematoriums.map((crem, index) => ( + handleFieldChange('selectedCrematoriumId', crem.id)} + role="radio" + aria-checked={crem.id === values.selectedCrematoriumId} + tabIndex={ + values.selectedCrematoriumId === null + ? index === 0 + ? 0 + : -1 + : crem.id === values.selectedCrematoriumId + ? 0 + : -1 + } + sx={{ p: 3 }} + > + {crem.name} + + {crem.location} + {crem.distance && ` \u2022 ${crem.distance}`} + + {crem.price != null && ( + + ${crem.price.toLocaleString('en-AU')} + + )} + + ))} + + + )} + + {errors?.selectedCrematoriumId && ( + + {errors.selectedCrematoriumId} + + )} + + + {/* ─── Witness / attendance question ─── */} + + + Will anyone be following the hearse to the crematorium? + + + {witnessHelper} + + + handleFieldChange('attend', e.target.value as CrematoriumStepValues['attend']) + } + > + } label="Yes" /> + } label="No" /> + + {errors?.attend && ( + + {errors.attend} + + )} + + + {/* ─── Priority / timing preference (optional, provider-specific) ─── */} + {priorityOptions.length > 0 && ( + handleFieldChange('priority', e.target.value)} + placeholder="Select timing preference (optional)" + fullWidth + sx={{ mb: 3 }} + > + {priorityOptions.map((opt) => ( + + ))} + + )} + + {/* ─── Special instructions ─── */} + + + Do you have any special requests for the cremation? + + + handleFieldChange( + 'hasInstructions', + e.target.value as CrematoriumStepValues['hasInstructions'], + ) + } + > + } label="No" /> + } label="Yes" /> + + + + + handleFieldChange('specialInstructions', e.target.value)} + placeholder="Enter any special requests or instructions..." + multiline + minRows={3} + maxRows={8} + fullWidth + sx={{ mb: 3 }} + /> + + + + + {/* CTAs */} + + {onSaveAndExit ? ( + + ) : ( + + )} + + + + + ); +}; + +CrematoriumStep.displayName = 'CrematoriumStep'; +export default CrematoriumStep; diff --git a/src/components/pages/CrematoriumStep/index.ts b/src/components/pages/CrematoriumStep/index.ts new file mode 100644 index 0000000..6a9aa85 --- /dev/null +++ b/src/components/pages/CrematoriumStep/index.ts @@ -0,0 +1,7 @@ +export { CrematoriumStep, default } from './CrematoriumStep'; +export type { + CrematoriumStepProps, + CrematoriumStepValues, + CrematoriumStepErrors, + Crematorium, +} from './CrematoriumStep';