From 77bac1478f4556488d8704d6c2575c912fe84eef Mon Sep 17 00:00:00 2001 From: Richie Date: Sun, 29 Mar 2026 15:02:18 +1100 Subject: [PATCH] Add AdditionalServicesStep page (wizard step 12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Merged from baseline steps 14 (optionals) + 15 (extras) per Rec #2 - Section 1: Complimentary inclusions (dressing, viewing, prayers, announcement) - Section 2: Paid extras (catering, music, bearing, newspaper notice) - Progressive disclosure: viewing → same venue radio, music → live musician → type - Dependent field resets when parent toggled off - AddOnOption reuse for all toggle rows - Bearing as RadioGroup (family/professional/both) - No upsell language — toggle design is inherently low-pressure Co-Authored-By: Claude Opus 4.6 (1M context) --- .../AdditionalServicesStep.stories.tsx | 157 ++++++++ .../AdditionalServicesStep.tsx | 352 ++++++++++++++++++ .../pages/AdditionalServicesStep/index.ts | 5 + 3 files changed, 514 insertions(+) create mode 100644 src/components/pages/AdditionalServicesStep/AdditionalServicesStep.stories.tsx create mode 100644 src/components/pages/AdditionalServicesStep/AdditionalServicesStep.tsx create mode 100644 src/components/pages/AdditionalServicesStep/index.ts diff --git a/src/components/pages/AdditionalServicesStep/AdditionalServicesStep.stories.tsx b/src/components/pages/AdditionalServicesStep/AdditionalServicesStep.stories.tsx new file mode 100644 index 0000000..090a26c --- /dev/null +++ b/src/components/pages/AdditionalServicesStep/AdditionalServicesStep.stories.tsx @@ -0,0 +1,157 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { AdditionalServicesStep } from './AdditionalServicesStep'; +import type { AdditionalServicesStepValues } from './AdditionalServicesStep'; +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 defaultValues: AdditionalServicesStepValues = { + dressing: false, + viewing: false, + viewingSameVenue: null, + prayers: false, + funeralAnnouncement: true, + catering: false, + music: false, + liveMusician: false, + musicianType: null, + bearing: null, + newspaperNotice: false, +}; + +// ─── Meta ──────────────────────────────────────────────────────────────────── + +const meta: Meta = { + title: 'Pages/AdditionalServicesStep', + component: AdditionalServicesStep, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +// ─── Interactive (default) ────────────────────────────────────────────────── + +/** Full interactive flow with both sections */ +export const Default: Story = { + render: () => { + const [values, setValues] = useState({ ...defaultValues }); + return ( + alert(JSON.stringify(values, null, 2))} + onBack={() => alert('Back')} + onSaveAndExit={() => alert('Save')} + newspaperPrice={250} + musicianPrice={450} + navigation={nav} + /> + ); + }, +}; + +// ─── Many options enabled ─────────────────────────────────────────────────── + +/** Multiple services toggled on with sub-options visible */ +export const ManyOptionsEnabled: Story = { + render: () => { + const [values, setValues] = useState({ + dressing: true, + viewing: true, + viewingSameVenue: 'yes', + prayers: false, + funeralAnnouncement: true, + catering: true, + music: true, + liveMusician: true, + musicianType: 'vocalist', + bearing: 'both', + newspaperNotice: true, + }); + return ( + alert('Continue')} + onBack={() => alert('Back')} + cateringPrice={850} + newspaperPrice={250} + musicianPrice={450} + navigation={nav} + /> + ); + }, +}; + +// ─── Pre-planning ─────────────────────────────────────────────────────────── + +/** Pre-planning variant */ +export const PrePlanning: Story = { + render: () => { + const [values, setValues] = useState({ ...defaultValues }); + return ( + alert('Continue')} + onBack={() => alert('Back')} + isPrePlanning + navigation={nav} + /> + ); + }, +}; + +// ─── Minimal (provider shows few options) ─────────────────────────────────── + +/** Announcement only — minimal provider offerings */ +export const Minimal: Story = { + render: () => { + const [values, setValues] = useState({ + ...defaultValues, + funeralAnnouncement: true, + }); + return ( + alert('Continue')} + onBack={() => alert('Back')} + navigation={nav} + /> + ); + }, +}; diff --git a/src/components/pages/AdditionalServicesStep/AdditionalServicesStep.tsx b/src/components/pages/AdditionalServicesStep/AdditionalServicesStep.tsx new file mode 100644 index 0000000..2813f41 --- /dev/null +++ b/src/components/pages/AdditionalServicesStep/AdditionalServicesStep.tsx @@ -0,0 +1,352 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Paper from '@mui/material/Paper'; +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 { AddOnOption } from '../../molecules/AddOnOption'; +import { Collapse } from '../../atoms/Collapse'; +import { Typography } from '../../atoms/Typography'; +import { Button } from '../../atoms/Button'; +import { Divider } from '../../atoms/Divider'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +/** Form values for the additional services step */ +export interface AdditionalServicesStepValues { + // Section 1: Complimentary inclusions + dressing: boolean; + viewing: boolean; + viewingSameVenue: 'yes' | 'no' | null; + prayers: boolean; + funeralAnnouncement: boolean; + + // Section 2: Paid extras + catering: boolean; + music: boolean; + liveMusician: boolean; + musicianType: 'vocalist' | 'cellist' | 'other' | null; + bearing: 'family' | 'funeralHouse' | 'both' | null; + newspaperNotice: boolean; +} + +/** Props for the AdditionalServicesStep page component */ +export interface AdditionalServicesStepProps { + /** Current form values */ + values: AdditionalServicesStepValues; + /** Callback when any field value changes */ + onChange: (values: AdditionalServicesStepValues) => void; + /** Callback when the Continue button is clicked */ + onContinue: () => void; + /** Callback for back navigation */ + onBack?: () => void; + /** Callback for save-and-exit */ + onSaveAndExit?: () => void; + /** Whether the Continue button is in a loading state */ + loading?: boolean; + /** Price for catering (undefined = POA) */ + cateringPrice?: number; + /** Price for newspaper notice (undefined = POA) */ + newspaperPrice?: number; + /** Price for live musician (undefined = POA) */ + musicianPrice?: number; + /** 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 12 — Additional Services for the FA arrangement wizard. + * + * Merged from baseline steps 14 (optionals) and 15 (extras) per Rec #2. + * Two sections preserving the semantic distinction: + * 1. Complimentary inclusions (toggle on/off at no cost) + * 2. Paid extras (toggle with pricing or POA) + * + * Progressive disclosure: sub-options revealed when parent toggle is on. + * Toggle design is inherently low-pressure — no upsell language. + * + * Pure presentation component — props in, callbacks out. + * + * Spec: documentation/steps/steps/12_additional_services.yaml + */ +export const AdditionalServicesStep: React.FC = ({ + values, + onChange, + onContinue, + onBack, + onSaveAndExit, + loading = false, + cateringPrice, + newspaperPrice, + musicianPrice, + isPrePlanning = false, + navigation, + progressStepper, + runningTotal, + hideHelpBar, + sx, +}) => { + const handleToggle = (field: keyof AdditionalServicesStepValues, checked: boolean) => { + const next = { ...values, [field]: checked }; + // Reset dependent fields when parent toggled off + if (field === 'viewing' && !checked) { + next.viewingSameVenue = null; + } + if (field === 'music' && !checked) { + next.liveMusician = false; + next.musicianType = null; + } + if (field === 'liveMusician' && !checked) { + next.musicianType = null; + } + onChange(next); + }; + + const handleFieldChange = ( + field: K, + value: AdditionalServicesStepValues[K], + ) => { + onChange({ ...values, [field]: value }); + }; + + return ( + + {/* Page heading */} + + Additional services + + + + {isPrePlanning + ? "These options can be finalised later. Toggle on the ones you're interested in." + : 'Choose which services to include in your plan.'} + + + { + e.preventDefault(); + onContinue(); + }} + > + {/* ─── Section 1: Complimentary inclusions ─── */} + + + Complimentary inclusions + + + These items are included at no additional cost. You can choose to include or remove + them. + + + + handleToggle('dressing', c)} + /> + + handleToggle('viewing', c)} + /> + + + + + + Same venue as the service? + + + handleFieldChange( + 'viewingSameVenue', + e.target.value as AdditionalServicesStepValues['viewingSameVenue'], + ) + } + > + } label="Yes, same venue" /> + } label="No, different venue" /> + + + + + + handleToggle('prayers', c)} + /> + + handleToggle('funeralAnnouncement', c)} + /> + + + + {/* ─── Section 2: Paid extras ─── */} + + + Additional extras + + + These items are available but may incur additional costs. Prices shown where available. + + + + handleToggle('catering', c)} + /> + + handleToggle('music', c)} + /> + + + + handleToggle('liveMusician', c)} + /> + + + + + + Musician type + + + handleFieldChange( + 'musicianType', + e.target.value as AdditionalServicesStepValues['musicianType'], + ) + } + > + } label="Vocalist" /> + } label="Cellist" /> + } label="Other" /> + + + + + + + + {/* Coffin bearing */} + + + + Coffin bearing + + + handleFieldChange( + 'bearing', + e.target.value as AdditionalServicesStepValues['bearing'], + ) + } + > + } label="Family and friends" /> + } + label="Professional bearers" + /> + } + label="Both family and professional" + /> + + + + + + handleToggle('newspaperNotice', c)} + /> + + + + + + {/* CTAs */} + + {onSaveAndExit ? ( + + ) : ( + + )} + + + + + ); +}; + +AdditionalServicesStep.displayName = 'AdditionalServicesStep'; +export default AdditionalServicesStep; diff --git a/src/components/pages/AdditionalServicesStep/index.ts b/src/components/pages/AdditionalServicesStep/index.ts new file mode 100644 index 0000000..23af28d --- /dev/null +++ b/src/components/pages/AdditionalServicesStep/index.ts @@ -0,0 +1,5 @@ +export { AdditionalServicesStep, default } from './AdditionalServicesStep'; +export type { + AdditionalServicesStepProps, + AdditionalServicesStepValues, +} from './AdditionalServicesStep';