Files
Parsons/src/components/pages/UnverifiedPackageT3/UnverifiedPackageT3.tsx
Richie 68889af9c2 Add UnverifiedPackageT2/T3 pages, FuneralFinder pre-planning timeframe, PackageDetail variants
- UnverifiedPackageT3: package step for unverified providers (no image,
  estimated pricing disclaimer, "Make an enquiry" CTA, nearby verified
  providers section)
- UnverifiedPackageT2: same but with "Itemised Pricing Unavailable" notice
  replacing the line-item breakdown
- PackageDetail: new props — arrangeLabel, priceDisclaimer, itemizedUnavailable
- FuneralFinderV3: pre-planning follow-up question ("How soon might you
  need this?"), responsive sizing fixes, compulsory validation
- HomePage: fix finder container width (flex stretch + 500px cap)
- .gitignore: exclude Claude/Playwright artifacts, working docs, screenshots

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 10:35:28 +11:00

334 lines
11 KiB
TypeScript

import React from 'react';
import Box from '@mui/material/Box';
import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
import StarRoundedIcon from '@mui/icons-material/StarRounded';
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
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 { Card } from '../../atoms/Card';
import { Divider } from '../../atoms/Divider';
// ─── Types ───────────────────────────────────────────────────────────────────
/** Provider summary for the compact card */
export interface UnverifiedPackageT3Provider {
/** 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 UnverifiedPackageT3Data {
/** Unique package ID */
id: string;
/** Package display name */
name: string;
/** Package price in dollars */
price: number;
/** Short description */
description?: string;
/** 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;
}
/** A similar package from a nearby verified provider */
export interface NearbyVerifiedPackage {
/** Unique ID */
id: string;
/** Package name */
packageName: string;
/** Package price in dollars */
price: number;
/** Provider name */
providerName: string;
/** Provider location */
location: string;
/** Provider rating */
rating?: number;
/** Number of reviews */
reviewCount?: number;
}
/** Props for the UnverifiedPackageT3 page component */
export interface UnverifiedPackageT3Props {
/** Provider summary shown at top of the list panel (no image — unverified provider) */
provider: UnverifiedPackageT3Provider;
/** Packages matching the user's filters from the previous step */
packages: UnverifiedPackageT3Data[];
/** Similar packages from nearby verified providers */
nearbyPackages?: NearbyVerifiedPackage[];
/** Currently selected package ID */
selectedPackageId: string | null;
/** Callback when a package is selected */
onSelectPackage: (id: string) => void;
/** Callback when "Make Arrangement" is clicked (opens ArrangementDialog) */
onArrange: () => void;
/** Callback when a nearby verified package is clicked */
onNearbyPackageClick?: (id: string) => void;
/** Callback when the provider card is clicked (opens provider profile popup) */
onProviderClick?: () => void;
/** Callback for the Back button */
onBack: () => void;
/** Validation error */
error?: string;
/** Whether the arrange action is loading */
loading?: boolean;
/** Navigation bar */
navigation?: React.ReactNode;
/** Whether this is a pre-planning flow */
isPrePlanning?: boolean;
/** MUI sx prop */
sx?: SxProps<Theme>;
}
// ─── Component ───────────────────────────────────────────────────────────────
/**
* UnverifiedPackageT3 — Package selection page for unverified (Tier 3) providers.
*
* List + Detail split layout. Left panel shows the selected provider
* (compact) and selectable package cards. Right panel shows the full
* detail breakdown of the selected package with "Make Arrangement" CTA.
*
* Two sections:
* - **This provider's packages**: estimated pricing from publicly available info
* - **Similar packages from verified providers nearby**: promoted alternatives
* with verified pricing, ratings, and location
*
* Selecting a package reveals its detail. Clicking "Make an enquiry"
* on the detail panel initiates contact with the unverified provider.
*
* Pure presentation component — props in, callbacks out.
*/
export const UnverifiedPackageT3: React.FC<UnverifiedPackageT3Props> = ({
provider,
packages,
nearbyPackages = [],
selectedPackageId,
onSelectPackage,
onArrange,
onNearbyPackageClick,
onProviderClick,
onBack,
error,
loading = false,
navigation,
isPrePlanning = false,
sx,
}) => {
const selectedPackage = packages.find((p) => p.id === selectedPackageId);
const hasNearbyPackages = nearbyPackages.length > 0;
const subheading = isPrePlanning
? 'Browse estimated packages to get a sense of what this provider offers. Nothing is committed.'
: 'These packages are based on publicly available information. Make an enquiry to confirm details and pricing.';
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={onArrange}
arrangeDisabled={loading}
arrangeLabel="Make an enquiry"
priceDisclaimer="Prices are estimates based on publicly available information and may not reflect the provider's current pricing."
/>
) : (
<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 — clickable to open provider profile */}
<Box sx={{ mb: 3 }}>
<ProviderCardCompact
name={provider.name}
location={provider.location}
rating={provider.rating}
reviewCount={provider.reviewCount}
onClick={onProviderClick}
/>
</Box>
{/* Heading */}
<Typography variant="h4" component="h1" sx={{ mb: 0.5 }} tabIndex={-1}>
Explore available packages
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{subheading}
</Typography>
{/* Error message */}
{error && (
<Typography
variant="body2"
sx={{ mb: 2, color: 'var(--fa-color-text-brand)' }}
role="alert"
>
{error}
</Typography>
)}
{/* ─── Packages ─── */}
<Box
role="radiogroup"
aria-label="Funeral packages"
sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}
>
{packages.map((pkg) => (
<ServiceOption
key={pkg.id}
name={pkg.name}
description={pkg.description}
price={pkg.price}
selected={selectedPackageId === pkg.id}
onClick={() => onSelectPackage(pkg.id)}
/>
))}
{packages.length === 0 && (
<Box sx={{ py: 4, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
No packages match your current preferences.
</Typography>
</Box>
)}
</Box>
{/* ─── Similar packages from nearby verified providers ─── */}
{hasNearbyPackages && (
<>
<Divider sx={{ mb: 2.5 }} />
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
mb: 2,
}}
>
<VerifiedOutlinedIcon sx={{ fontSize: 16, color: 'primary.main' }} aria-hidden />
<Typography variant="body2" sx={{ fontWeight: 600, color: 'text.primary' }}>
Similar packages from verified providers nearby
</Typography>
</Box>
<Box
aria-label="Similar packages from nearby verified providers"
sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}
>
{nearbyPackages.map((pkg) => (
<Card
key={pkg.id}
variant="outlined"
interactive={!!onNearbyPackageClick}
padding="none"
onClick={onNearbyPackageClick ? () => onNearbyPackageClick(pkg.id) : undefined}
sx={{ p: 'var(--fa-card-padding-compact)' }}
>
{/* Package name + price */}
<Box
sx={{
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'space-between',
gap: 2,
mb: 1,
}}
>
<Typography variant="h6" component="span">
{pkg.packageName}
</Typography>
<Typography
variant="labelLg"
component="span"
color="primary"
sx={{ whiteSpace: 'nowrap' }}
>
${pkg.price.toLocaleString('en-AU')}
</Typography>
</Box>
{/* Provider info */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexWrap: 'wrap' }}>
<Typography variant="body2" color="text.secondary">
{pkg.providerName}
</Typography>
{pkg.rating != null && (
<>
<Typography variant="body2" color="text.secondary">
&middot;
</Typography>
<StarRoundedIcon sx={{ fontSize: 14, color: 'warning.main' }} aria-hidden />
<Typography variant="caption" color="text.secondary">
{pkg.rating}
{pkg.reviewCount != null ? ` (${pkg.reviewCount})` : ''}
</Typography>
</>
)}
<Typography variant="body2" color="text.secondary">
&middot;
</Typography>
<LocationOnOutlinedIcon
sx={{ fontSize: 14, color: 'text.secondary' }}
aria-hidden
/>
<Typography variant="caption" color="text.secondary">
{pkg.location}
</Typography>
</Box>
</Card>
))}
</Box>
</>
)}
</WizardLayout>
);
};
UnverifiedPackageT3.displayName = 'UnverifiedPackageT3';
export default UnverifiedPackageT3;