import React from 'react'; import Box from '@mui/material/Box'; import Tooltip from '@mui/material/Tooltip'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; import StarRoundedIcon from '@mui/icons-material/StarRounded'; import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined'; import type { SxProps, Theme } from '@mui/material/styles'; import { Typography } from '../../atoms/Typography'; import { Button } from '../../atoms/Button'; import { Badge } from '../../atoms/Badge'; import { Card } from '../../atoms/Card'; import { Link } from '../../atoms/Link'; import { Divider } from '../../atoms/Divider'; // ─── Types ─────────────────────────────────────────────────────────────────── /** Cell value types for the comparison table */ export type ComparisonCellValue = | { type: 'price'; amount: number } | { type: 'allowance'; amount: number } | { type: 'complimentary' } | { type: 'included' } | { type: 'poa' } | { type: 'unknown' } | { type: 'unavailable' }; export interface ComparisonLineItem { name: string; info?: string; value: ComparisonCellValue; } export interface ComparisonSection { heading: string; items: ComparisonLineItem[]; } export interface ComparisonProvider { name: string; location: string; logoUrl?: string; rating?: number; reviewCount?: number; verified: boolean; } export interface ComparisonPackage { id: string; name: string; price: number; provider: ComparisonProvider; sections: ComparisonSection[]; isRecommended?: boolean; itemizedAvailable?: boolean; } export interface ComparisonTableProps { packages: ComparisonPackage[]; onArrange: (packageId: string) => void; onRemove: (packageId: string) => void; sx?: SxProps; } // ─── Helpers ──────────────────────────────────────────────────────────────── function formatPrice(amount: number): string { return `$${amount.toLocaleString('en-AU', { minimumFractionDigits: amount % 1 !== 0 ? 2 : 0 })}`; } function CellValue({ value }: { value: ComparisonCellValue }) { switch (value.type) { case 'price': return ( {formatPrice(value.amount)} ); case 'allowance': return ( {formatPrice(value.amount)}* ); case 'complimentary': return ( Complimentary ); case 'included': return ( Included ); case 'poa': return ( Price On Application ); case 'unknown': return ( Unknown ); case 'unavailable': return ( ); } } function buildMergedSections( packages: ComparisonPackage[], ): { heading: string; items: { name: string; info?: string }[] }[] { const sectionMap = new Map(); const sectionOrder: string[] = []; for (const pkg of packages) { if (pkg.itemizedAvailable === false) continue; for (const section of pkg.sections) { if (!sectionMap.has(section.heading)) { sectionMap.set(section.heading, []); sectionOrder.push(section.heading); } const existing = sectionMap.get(section.heading)!; for (const item of section.items) { if (!existing.some((e) => e.name === item.name)) { existing.push({ name: item.name, info: item.info }); } } } } return sectionOrder.map((heading) => ({ heading, items: sectionMap.get(heading) ?? [], })); } function lookupValue( pkg: ComparisonPackage, sectionHeading: string, itemName: string, ): ComparisonCellValue { if (pkg.itemizedAvailable === false) return { type: 'unavailable' }; const section = pkg.sections.find((s) => s.heading === sectionHeading); if (!section) return { type: 'unavailable' }; const item = section.items.find((i) => i.name === itemName); if (!item) return { type: 'unavailable' }; return item.value; } /** Section heading with left accent border */ function SectionHeading({ children }: { children: React.ReactNode }) { return ( {children} ); } /** Reusable bordered table wrapper */ const tableSx = { display: 'grid', border: '1px solid', borderColor: 'divider', borderRadius: 'var(--fa-card-border-radius-default)', overflow: 'hidden', bgcolor: 'background.paper', }; // ─── Component ────────────────────────────────────────────────────────────── /** * Side-by-side package comparison table for the FA design system. * * Info card in top-left column, floating verified badges above cards, * section tables with left accent borders, no reviews table (rating in cards). * * Desktop only — ComparisonPage handles the mobile card view. */ export const ComparisonTable = React.forwardRef( ({ packages, onArrange, onRemove, sx }, ref) => { const colCount = packages.length + 1; const mergedSections = buildMergedSections(packages); const gridCols = `minmax(220px, 280px) repeat(${packages.length}, minmax(200px, 1fr))`; const minW = packages.length > 3 ? 960 : packages.length > 2 ? 800 : 600; return ( {/* ── Package header cards ── */} {/* Info card — stretches to match package card height, text at top */} Package Comparison Review and compare features side-by-side to find the right fit. {/* Package cards */} {packages.map((pkg) => ( {/* Floating verified badge — overlaps card top edge */} {pkg.provider.verified && ( } sx={{ position: 'absolute', top: -12, left: '50%', transform: 'translateX(-50%)', zIndex: 1, boxShadow: '0 1px 3px rgba(0,0,0,0.1)', }} > Verified )} {pkg.isRecommended && ( Recommended )} {/* Provider name (truncated with tooltip) */} {pkg.provider.name} {/* Location */} {pkg.provider.location} {/* Rating */} {pkg.provider.rating != null && ( {pkg.provider.rating} {pkg.provider.reviewCount != null && ` (${pkg.provider.reviewCount})`} )} {pkg.name} Total package price {formatPrice(pkg.price)} {/* Spacer pushes CTA to bottom across all cards */} {!pkg.isRecommended && ( onRemove(pkg.id)} sx={{ mt: 0.5 }} > Remove )} ))} {/* ── Section tables (each separate with left accent headings) ── */} {mergedSections.map((section) => ( {section.heading} {section.items.map((item) => ( {item.name} {item.info && ( )} {packages.map((pkg) => ( ))} ))} ))} {packages.some((p) => p.itemizedAvailable === false) && mergedSections.length > 0 && ( * Some providers have not provided an itemised pricing breakdown. Their items are shown as "—" above. )} ); }, ); ComparisonTable.displayName = 'ComparisonTable'; export default ComparisonTable;