Files
Parsons/src/components/pages/PackagesStep/PackagesStep.tsx
Richie 87249b6d9b Groom wizard steps 1-15: critique/harden/polish pass
- [P0] CrematoriumStep: Fix <option> → <MenuItem> in priority select
- [P1] All form steps: Add aria-busy={loading} + loading guard on submit
- [P1] Error messages: Replace color="error" (red) with copper
  (var(--fa-color-text-brand)) across ProvidersStep, PackagesStep,
  VenueStep, CrematoriumStep, CemeteryStep, CoffinsStep, PaymentStep
- [P2] IntroStep: "Has the person died?" → "Has this person passed away?"
- [P2] DateTimeStep: "About the person who died" → "who has passed"
- [P2] ProvidersStep: "Showing results from X" → "X providers found"
- [P2] Empty states: Add guidance text for ProvidersStep, PackagesStep,
  VenueStep, CoffinsStep empty results

Steps 4, 13, 15 passed with no issues.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 15:33:01 +11:00

297 lines
9.3 KiB
TypeScript

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<Theme>;
}
// ─── 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<PackagesStepProps> = ({
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 (
<WizardLayout
variant="list-detail"
navigation={navigation}
showBackLink
backLabel="Back"
onBack={onBack}
sx={sx}
secondaryPanel={
selectedPackage ? (
<PackageDetail
name={selectedPackage.name}
price={selectedPackage.price}
sections={selectedPackage.sections}
total={selectedPackage.total}
extras={selectedPackage.extras}
terms={selectedPackage.terms}
onArrange={onContinue}
arrangeDisabled={loading}
/>
) : (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
minHeight: 300,
bgcolor: 'var(--fa-color-brand-50)',
borderRadius: 2,
p: 4,
}}
>
<Typography variant="body1" color="text.secondary" sx={{ textAlign: 'center' }}>
Select a package to see what&apos;s included.
</Typography>
</Box>
)
}
>
{/* Provider compact card */}
<Box sx={{ mb: 3 }}>
<ProviderCardCompact
name={provider.name}
location={provider.location}
imageUrl={provider.imageUrl}
rating={provider.rating}
reviewCount={provider.reviewCount}
/>
</Box>
{/* Heading */}
<Typography variant="h4" component="h1" sx={{ mb: 0.5 }} tabIndex={-1}>
Choose a funeral package
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{subheading}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ mb: 3, display: 'block' }}>
{helperText}
</Typography>
{/* Budget filter */}
<Box sx={{ mb: 3 }}>
<TextField
select
size="small"
value={budgetFilter}
onChange={(e) => onBudgetFilterChange(e.target.value)}
label="Budget range"
sx={{ width: { xs: '100%', sm: 240 } }}
>
{budgetOptions.map((opt) => (
<MenuItem key={opt.value} value={opt.value}>
{opt.label}
</MenuItem>
))}
</TextField>
</Box>
{/* Error message */}
{error && (
<Typography
variant="body2"
sx={{ mb: 2, color: 'var(--fa-color-text-brand)' }}
role="alert"
>
{error}
</Typography>
)}
{/* Package list — radiogroup pattern */}
<Box
role="radiogroup"
aria-label="Funeral packages"
sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}
>
{packages.map((pkg) => (
<Box key={pkg.id} sx={{ position: 'relative' }}>
{pkg.popular && (
<Badge
variant="filled"
color="brand"
size="small"
aria-label="Most popular choice"
sx={{
position: 'absolute',
top: -8,
right: 12,
zIndex: 1,
}}
>
Most Popular
</Badge>
)}
<ServiceOption
name={pkg.name}
description={pkg.description}
price={pkg.price}
selected={selectedPackageId === pkg.id}
onClick={() => onSelectPackage(pkg.id)}
/>
</Box>
))}
{packages.length === 0 && (
<Box sx={{ py: 6, textAlign: 'center' }}>
<Typography variant="body1" color="text.secondary" sx={{ mb: 1 }}>
No packages match the selected budget range.
</Typography>
<Typography variant="body2" color="text.secondary">
Try selecting &quot;All packages&quot; to see the full range.
</Typography>
</Box>
)}
</Box>
{/* Mobile: Continue button (desktop uses PackageDetail's CTA) */}
<Box sx={{ display: { xs: 'flex', md: 'none' }, justifyContent: 'flex-end', pb: 2 }}>
<Button
variant="contained"
size="large"
onClick={onContinue}
disabled={!selectedPackageId}
loading={loading}
>
Continue
</Button>
</Box>
</WizardLayout>
);
};
PackagesStep.displayName = 'PackagesStep';
export default PackagesStep;