diff --git a/src/components/pages/CemeteryStep/CemeteryStep.stories.tsx b/src/components/pages/CemeteryStep/CemeteryStep.stories.tsx new file mode 100644 index 0000000..6518eca --- /dev/null +++ b/src/components/pages/CemeteryStep/CemeteryStep.stories.tsx @@ -0,0 +1,243 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { CemeteryStep } from './CemeteryStep'; +import type { CemeteryStepValues, CemeteryStepErrors, Cemetery } from './CemeteryStep'; +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 sampleCemeteries: Cemetery[] = [ + { + id: 'rookwood', + name: 'Rookwood Cemetery', + location: 'Lidcombe, NSW', + price: 4500, + }, + { + id: 'northern-suburbs', + name: 'Northern Suburbs Memorial Gardens', + location: 'North Ryde, NSW', + price: 5200, + }, + { + id: 'macquarie-park', + name: 'Macquarie Park Cemetery', + location: 'Macquarie Park, NSW', + price: 4800, + }, +]; + +const defaultValues: CemeteryStepValues = { + burialOwn: null, + burialCustom: null, + selectedCemeteryId: null, +}; + +// ─── Meta ──────────────────────────────────────────────────────────────────── + +const meta: Meta = { + title: 'Pages/CemeteryStep', + component: CemeteryStep, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +// ─── Interactive (default) ────────────────────────────────────────────────── + +/** Fully interactive — progressive disclosure flow */ +export const Default: Story = { + render: () => { + const [values, setValues] = useState({ ...defaultValues }); + const [errors, setErrors] = useState({}); + + const handleContinue = () => { + const newErrors: CemeteryStepErrors = {}; + if (!values.burialOwn) newErrors.burialOwn = 'Please let us know about the burial plot.'; + if (values.burialOwn === 'no' && !values.burialCustom) + newErrors.burialCustom = "Please let us know if you'd like to choose a specific cemetery."; + if (values.burialOwn === 'no' && values.burialCustom === 'yes' && !values.selectedCemeteryId) + newErrors.selectedCemeteryId = 'Please choose a cemetery.'; + setErrors(newErrors); + if (Object.keys(newErrors).length === 0) alert('Continue'); + }; + + return ( + { + setValues(v); + setErrors({}); + }} + onContinue={handleContinue} + onBack={() => alert('Back')} + onSaveAndExit={() => alert('Save')} + errors={errors} + cemeteries={sampleCemeteries} + navigation={nav} + /> + ); + }, +}; + +// ─── Has existing plot ────────────────────────────────────────────────────── + +/** User already owns a burial plot — short confirmation */ +export const HasExistingPlot: Story = { + render: () => { + const [values, setValues] = useState({ + ...defaultValues, + burialOwn: 'yes', + }); + return ( + alert('Continue')} + onBack={() => alert('Back')} + cemeteries={sampleCemeteries} + navigation={nav} + /> + ); + }, +}; + +// ─── Provider arranges ────────────────────────────────────────────────────── + +/** User wants provider to arrange — no cemetery grid */ +export const ProviderArranges: Story = { + render: () => { + const [values, setValues] = useState({ + ...defaultValues, + burialOwn: 'no', + burialCustom: 'no', + }); + return ( + alert('Continue')} + onBack={() => alert('Back')} + cemeteries={sampleCemeteries} + navigation={nav} + /> + ); + }, +}; + +// ─── Cemetery grid visible ────────────────────────────────────────────────── + +/** User wants to choose — cemetery grid revealed */ +export const CemeteryGridVisible: Story = { + render: () => { + const [values, setValues] = useState({ + ...defaultValues, + burialOwn: 'no', + burialCustom: 'yes', + }); + return ( + alert('Continue')} + onBack={() => alert('Back')} + cemeteries={sampleCemeteries} + navigation={nav} + /> + ); + }, +}; + +// ─── Cemetery selected ────────────────────────────────────────────────────── + +/** Cemetery selected */ +export const CemeterySelected: Story = { + render: () => { + const [values, setValues] = useState({ + burialOwn: 'no', + burialCustom: 'yes', + selectedCemeteryId: 'rookwood', + }); + return ( + alert('Continue')} + onBack={() => alert('Back')} + cemeteries={sampleCemeteries} + navigation={nav} + /> + ); + }, +}; + +// ─── Pre-planning ─────────────────────────────────────────────────────────── + +/** Pre-planning variant */ +export const PrePlanning: Story = { + render: () => { + const [values, setValues] = useState({ ...defaultValues }); + return ( + alert('Continue')} + onBack={() => alert('Back')} + cemeteries={sampleCemeteries} + isPrePlanning + navigation={nav} + /> + ); + }, +}; + +// ─── Validation errors ────────────────────────────────────────────────────── + +/** All errors showing */ +export const WithErrors: Story = { + render: () => { + const [values, setValues] = useState({ ...defaultValues }); + return ( + {}} + errors={{ burialOwn: 'Please let us know about the burial plot.' }} + cemeteries={sampleCemeteries} + navigation={nav} + /> + ); + }, +}; diff --git a/src/components/pages/CemeteryStep/CemeteryStep.tsx b/src/components/pages/CemeteryStep/CemeteryStep.tsx new file mode 100644 index 0000000..f5d8dbd --- /dev/null +++ b/src/components/pages/CemeteryStep/CemeteryStep.tsx @@ -0,0 +1,296 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +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 cemetery available for selection */ +export interface Cemetery { + id: string; + name: string; + location: string; + price?: number; +} + +/** Form values for the cemetery step */ +export interface CemeteryStepValues { + /** Does the family already own a burial plot? */ + burialOwn: 'yes' | 'no' | null; + /** Would they like to choose a specific cemetery? (when burialOwn=no) */ + burialCustom: 'yes' | 'no' | null; + /** Selected cemetery ID */ + selectedCemeteryId: string | null; +} + +/** Field-level error messages */ +export interface CemeteryStepErrors { + burialOwn?: string; + burialCustom?: string; + selectedCemeteryId?: string; +} + +/** Props for the CemeteryStep page component */ +export interface CemeteryStepProps { + /** Current form values */ + values: CemeteryStepValues; + /** Callback when any field value changes */ + onChange: (values: CemeteryStepValues) => 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?: CemeteryStepErrors; + /** Whether the Continue button is in a loading state */ + loading?: boolean; + /** Available cemeteries */ + cemeteries: Cemetery[]; + /** Whether this is a pre-planning flow */ + isPrePlanning?: boolean; + /** Navigation bar */ + navigation?: React.ReactNode; + /** Progress stepper */ + progressStepper?: React.ReactNode; + /** Running total */ + runningTotal?: React.ReactNode; + /** Hide the help bar */ + hideHelpBar?: boolean; + /** MUI sx prop */ + sx?: SxProps; +} + +// ─── Component ─────────────────────────────────────────────────────────────── + +/** + * Step 9 — Cemetery for the FA arrangement wizard. + * + * Cemetery selection and burial plot preferences. Only shown for + * burial-type funerals (Service & Burial, Graveside, Burial Only). + * + * Progressive disclosure flow: + * 1. "Do you have a burial plot?" → Yes/No + * 2. If No: "Would you like to choose a specific cemetery?" → Yes/No + * 3. If Yes to #2: Cemetery card grid + * + * If the user already owns a plot, the cemetery grid can be shown + * for confirmation (passed via showGridForExistingPlot). + * + * Pure presentation component — props in, callbacks out. + * + * Spec: documentation/steps/steps/09_cemetery.yaml + */ +export const CemeteryStep: React.FC = ({ + values, + onChange, + onContinue, + onBack, + onSaveAndExit, + errors, + loading = false, + cemeteries, + isPrePlanning = false, + navigation, + progressStepper, + runningTotal, + hideHelpBar, + sx, +}) => { + const showCustomQuestion = values.burialOwn === 'no'; + const showCemeteryGrid = values.burialOwn === 'no' && values.burialCustom === 'yes'; + + const handleBurialOwnChange = (value: string) => { + onChange({ + ...values, + burialOwn: value as CemeteryStepValues['burialOwn'], + // Reset dependent fields when parent changes + burialCustom: null, + selectedCemeteryId: null, + }); + }; + + const handleBurialCustomChange = (value: string) => { + onChange({ + ...values, + burialCustom: value as CemeteryStepValues['burialCustom'], + selectedCemeteryId: null, + }); + }; + + const handleCemeterySelect = (id: string) => { + onChange({ ...values, selectedCemeteryId: id }); + }; + + return ( + + {/* Page heading */} + + Cemetery + + + + {isPrePlanning + ? "If you haven't decided on a cemetery yet, the funeral provider can help with this later." + : 'Choose where the burial will take place.'} + + + { + e.preventDefault(); + onContinue(); + }} + > + {/* ─── Burial plot question ─── */} + + + Do you already have a burial plot? + + handleBurialOwnChange(e.target.value)} + > + } label="Yes, we have a plot" /> + } label="No, we need to find one" /> + + {errors?.burialOwn && ( + + {errors.burialOwn} + + )} + + + {/* ─── Custom cemetery question (progressive disclosure) ─── */} + + + + Would you like to choose a specific cemetery? + + handleBurialCustomChange(e.target.value)} + > + } label="Yes, I'd like to choose" /> + } + label="No, the funeral provider can arrange this" + /> + + {errors?.burialCustom && ( + + {errors.burialCustom} + + )} + + + + {/* ─── Cemetery card grid (progressive disclosure) ─── */} + + + + Available cemeteries + + + + {cemeteries.map((cemetery, index) => ( + handleCemeterySelect(cemetery.id)} + role="radio" + aria-checked={cemetery.id === values.selectedCemeteryId} + tabIndex={ + values.selectedCemeteryId === null + ? index === 0 + ? 0 + : -1 + : cemetery.id === values.selectedCemeteryId + ? 0 + : -1 + } + sx={{ p: 3 }} + > + {cemetery.name} + + {cemetery.location} + + {cemetery.price != null && ( + + ${cemetery.price.toLocaleString('en-AU')} + + )} + + ))} + + + {errors?.selectedCemeteryId && ( + + {errors.selectedCemeteryId} + + )} + + + + + + {/* CTAs */} + + {onSaveAndExit ? ( + + ) : ( + + )} + + + + + ); +}; + +CemeteryStep.displayName = 'CemeteryStep'; +export default CemeteryStep; diff --git a/src/components/pages/CemeteryStep/index.ts b/src/components/pages/CemeteryStep/index.ts new file mode 100644 index 0000000..279b9e4 --- /dev/null +++ b/src/components/pages/CemeteryStep/index.ts @@ -0,0 +1,7 @@ +export { CemeteryStep, default } from './CemeteryStep'; +export type { + CemeteryStepProps, + CemeteryStepValues, + CemeteryStepErrors, + Cemetery, +} from './CemeteryStep';