diff --git a/src/components/pages/PackagesStep/PackagesStep.stories.tsx b/src/components/pages/PackagesStep/PackagesStep.stories.tsx new file mode 100644 index 0000000..be4f085 --- /dev/null +++ b/src/components/pages/PackagesStep/PackagesStep.stories.tsx @@ -0,0 +1,303 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { PackagesStep } from './PackagesStep'; +import type { PackageData, PackagesStepProvider } from './PackagesStep'; +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' }, + { label: 'Log in', href: '/login' }, + ]} + /> +); + +const mockProvider: PackagesStepProvider = { + name: 'H.Parsons Funeral Directors', + location: 'Wentworth, NSW', + imageUrl: 'https://placehold.co/120x80/E8E0D6/8B6F47?text=H.Parsons', + rating: 4.6, + reviewCount: 7, +}; + +const mockPackages: PackageData[] = [ + { + id: 'everyday', + name: 'Everyday Funeral Package', + price: 2700, + description: + 'This package includes a funeral service at a chapel or a church with a funeral procession. It includes many of the most commonly selected funeral options.', + popular: true, + sections: [ + { + heading: 'Essentials', + items: [ + { name: 'Accommodation', price: 500 }, + { name: 'Death registration certificate', price: 150 }, + { name: 'Doctor fee for Cremation', price: 150 }, + { name: 'NSW Government Levy - Cremation', price: 83 }, + { name: 'Professional Mortuary Care', price: 1200 }, + { name: 'Professional Service Fee', price: 1120 }, + ], + }, + { + heading: 'Complimentary Items', + items: [ + { name: 'Dressing Fee', price: 0 }, + { name: 'Viewing Fee', price: 0 }, + ], + }, + ], + total: 2700, + extras: { + heading: 'Extras', + items: [ + { name: 'Allowance for Flowers', price: 150, isAllowance: true }, + { name: 'Allowance for Master of Ceremonies', price: 500, isAllowance: true }, + { name: 'After Business Hours Service Surcharge', price: 150 }, + { name: 'After Hours Prayers', price: 1920 }, + { name: 'Coffin Bearing by Funeral Directors', price: 1500 }, + { name: 'Digital Recording', price: 500 }, + ], + }, + terms: + 'This package includes a funeral service at a chapel or a church with a funeral procession. Pricing may vary based on additional selections.', + }, + { + id: 'deluxe', + name: 'Deluxe Funeral Package', + price: 4900, + description: + 'A comprehensive package with premium inclusions, higher-quality coffin selection, and expanded service options for families wanting a more personalised farewell.', + sections: [ + { + heading: 'Essentials', + items: [ + { name: 'Accommodation', price: 750 }, + { name: 'Death registration certificate', price: 150 }, + { name: 'Professional Mortuary Care', price: 1500 }, + { name: 'Professional Service Fee', price: 1500 }, + { name: 'Premium Coffin', price: 1000 }, + ], + }, + ], + total: 4900, + }, + { + id: 'essential', + name: 'Essential Funeral Package', + price: 1800, + description: + 'A simple, dignified option covering the essential requirements for a cremation service.', + sections: [ + { + heading: 'Essentials', + items: [ + { name: 'Death registration certificate', price: 150 }, + { name: 'Professional Mortuary Care', price: 800 }, + { name: 'Professional Service Fee', price: 850 }, + ], + }, + ], + total: 1800, + }, + { + id: 'catholic', + name: 'Catholic Service', + price: 3200, + description: + 'Tailored for Catholic funeral traditions including a Requiem Mass, graveside prayers, and coordination with parish requirements.', + sections: [ + { + heading: 'Essentials', + items: [ + { name: 'Accommodation', price: 500 }, + { name: 'Professional Mortuary Care', price: 1200 }, + { name: 'Professional Service Fee', price: 1200 }, + { name: 'Church coordination', price: 300 }, + ], + }, + ], + total: 3200, + }, +]; + +// ─── Meta ──────────────────────────────────────────────────────────────────── + +const meta: Meta = { + title: 'Pages/PackagesStep', + component: PackagesStep, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +// ─── Interactive (default) ────────────────────────────────────────────────── + +/** Fully interactive — browse, filter, select a package, see detail */ +export const Default: Story = { + render: () => { + const [selectedId, setSelectedId] = useState(null); + const [budget, setBudget] = useState('all'); + const [error, setError] = useState(); + + const filtered = + budget === 'all' + ? mockPackages + : mockPackages.filter((p) => { + const [min, max] = budget.split('-').map(Number); + return p.price >= min && p.price <= (max || Infinity); + }); + + const handleContinue = () => { + if (!selectedId) { + setError('Please choose a package to continue.'); + return; + } + setError(undefined); + alert(`Continue with package: ${selectedId}`); + }; + + return ( + { + setSelectedId(id); + setError(undefined); + }} + budgetFilter={budget} + onBudgetFilterChange={setBudget} + onContinue={handleContinue} + onBack={() => alert('Back')} + error={error} + navigation={nav} + /> + ); + }, +}; + +// ─── With selection ───────────────────────────────────────────────────────── + +/** Package already selected — detail panel visible */ +export const WithSelection: Story = { + render: () => { + const [selectedId, setSelectedId] = useState('everyday'); + const [budget, setBudget] = useState('all'); + + return ( + alert('Continue')} + onBack={() => alert('Back')} + navigation={nav} + /> + ); + }, +}; + +// ─── Pre-planning ─────────────────────────────────────────────────────────── + +/** Pre-planning flow — softer helper text */ +export const PrePlanning: Story = { + render: () => { + const [selectedId, setSelectedId] = useState(null); + const [budget, setBudget] = useState('all'); + + return ( + alert('Continue')} + onBack={() => alert('Back')} + navigation={nav} + isPrePlanning + /> + ); + }, +}; + +// ─── Filtered empty ───────────────────────────────────────────────────────── + +/** Budget filter yielding no results */ +export const FilteredEmpty: Story = { + render: () => { + const [budget, setBudget] = useState('7000-10000'); + + return ( + {}} + budgetFilter={budget} + onBudgetFilterChange={setBudget} + onContinue={() => {}} + onBack={() => alert('Back')} + navigation={nav} + /> + ); + }, +}; + +// ─── Validation error ─────────────────────────────────────────────────────── + +/** Error shown when no package selected */ +export const WithError: Story = { + render: () => { + const [selectedId, setSelectedId] = useState(null); + const [budget, setBudget] = useState('all'); + + return ( + {}} + onBack={() => alert('Back')} + error="Please choose a package to continue." + navigation={nav} + /> + ); + }, +}; diff --git a/src/components/pages/PackagesStep/PackagesStep.tsx b/src/components/pages/PackagesStep/PackagesStep.tsx new file mode 100644 index 0000000..ab68528 --- /dev/null +++ b/src/components/pages/PackagesStep/PackagesStep.tsx @@ -0,0 +1,289 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import TextField from '@mui/material/TextField'; +import MenuItem from '@mui/material/MenuItem'; +import type { SxProps, Theme } from '@mui/material/styles'; +import { WizardLayout } from '../../templates/WizardLayout'; +import { ProviderCardCompact } from '../../molecules/ProviderCardCompact'; +import { ServiceOption } from '../../molecules/ServiceOption'; +import { PackageDetail } from '../../organisms/PackageDetail'; +import type { PackageSection } from '../../organisms/PackageDetail'; +import { Typography } from '../../atoms/Typography'; +import { Badge } from '../../atoms/Badge'; +import { Button } from '../../atoms/Button'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +/** Provider summary for the compact card */ +export interface PackagesStepProvider { + /** Provider name */ + name: string; + /** Location */ + location: string; + /** Image URL */ + imageUrl?: string; + /** Rating */ + rating?: number; + /** Review count */ + reviewCount?: number; +} + +/** Package data for the selection list */ +export interface PackageData { + /** Unique package ID */ + id: string; + /** Package display name */ + name: string; + /** Package price in dollars */ + price: number; + /** Short description */ + description?: string; + /** Whether this is a "Most Popular" package */ + popular?: boolean; + /** Line item sections for the detail panel */ + sections: PackageSection[]; + /** Total price (may differ from base price with extras) */ + total?: number; + /** Extra items section (after total) */ + extras?: PackageSection; + /** Terms and conditions */ + terms?: string; +} + +/** Budget filter option */ +export interface BudgetOption { + /** Option value */ + value: string; + /** Display label */ + label: string; +} + +/** Props for the PackagesStep page component */ +export interface PackagesStepProps { + /** Provider summary shown at top of the list panel */ + provider: PackagesStepProvider; + /** Available packages */ + packages: PackageData[]; + /** Currently selected package ID */ + selectedPackageId: string | null; + /** Callback when a package is selected */ + onSelectPackage: (id: string) => void; + /** Current budget filter value */ + budgetFilter: string; + /** Callback when budget filter changes */ + onBudgetFilterChange: (value: string) => void; + /** Budget filter options */ + budgetOptions?: BudgetOption[]; + /** Callback for the Continue button */ + onContinue: () => void; + /** Callback for the Back button */ + onBack: () => void; + /** Validation error */ + error?: string; + /** Whether Continue is loading */ + loading?: boolean; + /** Navigation bar */ + navigation?: React.ReactNode; + /** Whether this is a pre-planning flow */ + isPrePlanning?: boolean; + /** MUI sx prop */ + sx?: SxProps; +} + +// ─── Constants ─────────────────────────────────────────────────────────────── + +const DEFAULT_BUDGET_OPTIONS: BudgetOption[] = [ + { value: 'all', label: 'All packages' }, + { value: '2000-4000', label: '$2,000 \u2013 $4,000' }, + { value: '4000-7000', label: '$4,000 \u2013 $7,000' }, + { value: '7000-10000', label: '$7,000 \u2013 $10,000+' }, +]; + +// ─── Component ─────────────────────────────────────────────────────────────── + +/** + * Step 3 — Package selection page for the FA arrangement wizard. + * + * List + Detail split layout. Left panel shows the selected provider + * (compact), a budget filter, and selectable package cards. Right panel + * shows the full detail breakdown of the selected package. + * + * Packages are displayed as ServiceOption cards in a radiogroup pattern. + * "Most Popular" badge on qualifying packages reduces decision paralysis. + * + * Pure presentation component — props in, callbacks out. + * + * Spec: documentation/steps/steps/03_packages.yaml + */ +export const PackagesStep: React.FC = ({ + provider, + packages, + selectedPackageId, + onSelectPackage, + budgetFilter, + onBudgetFilterChange, + budgetOptions = DEFAULT_BUDGET_OPTIONS, + onContinue, + onBack, + error, + loading = false, + navigation, + isPrePlanning = false, + sx, +}) => { + const selectedPackage = packages.find((p) => p.id === selectedPackageId); + + const subheading = + 'Each package includes a set of services. You can customise your selections in the next steps.'; + const helperText = isPrePlanning + ? 'Compare packages to find what suits your wishes. Nothing is committed until you confirm.' + : 'Prices shown include the base services listed. Additional options may change the total.'; + + return ( + + ) : ( + + + Select a package to see what's included. + + + ) + } + > + {/* Provider compact card */} + + + + + {/* Heading */} + + Choose a funeral package + + + {subheading} + + + {helperText} + + + {/* Budget filter */} + + onBudgetFilterChange(e.target.value)} + label="Budget range" + sx={{ width: { xs: '100%', sm: 240 } }} + > + {budgetOptions.map((opt) => ( + + {opt.label} + + ))} + + + + {/* Error message */} + {error && ( + + {error} + + )} + + {/* Package list — radiogroup pattern */} + + {packages.map((pkg) => ( + + {pkg.popular && ( + + Most Popular + + )} + onSelectPackage(pkg.id)} + /> + + ))} + + {packages.length === 0 && ( + + + No packages match the selected budget range. + + + )} + + + {/* Mobile: Continue button (desktop uses PackageDetail's CTA) */} + + + + + ); +}; + +PackagesStep.displayName = 'PackagesStep'; +export default PackagesStep; diff --git a/src/components/pages/PackagesStep/index.ts b/src/components/pages/PackagesStep/index.ts new file mode 100644 index 0000000..6805537 --- /dev/null +++ b/src/components/pages/PackagesStep/index.ts @@ -0,0 +1,2 @@ +export { default } from './PackagesStep'; +export * from './PackagesStep';