import React from 'react'; import Box from '@mui/material/Box'; import TextField from '@mui/material/TextField'; import InputAdornment from '@mui/material/InputAdornment'; import Autocomplete from '@mui/material/Autocomplete'; import FormControlLabel from '@mui/material/FormControlLabel'; import MenuItem from '@mui/material/MenuItem'; import Menu from '@mui/material/Menu'; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import ToggleButton from '@mui/material/ToggleButton'; import SwapVertIcon from '@mui/icons-material/SwapVert'; import ViewListOutlinedIcon from '@mui/icons-material/ViewListOutlined'; import MapOutlinedIcon from '@mui/icons-material/MapOutlined'; import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined'; import type { SxProps, Theme } from '@mui/material/styles'; import { WizardLayout } from '../../templates/WizardLayout'; import { VenueCard } from '../../molecules/VenueCard'; import { FilterPanel } from '../../molecules/FilterPanel'; import { Button } from '../../atoms/Button'; import { Chip } from '../../atoms/Chip'; import { Switch } from '../../atoms/Switch'; import { Typography } from '../../atoms/Typography'; import { Divider } from '../../atoms/Divider'; // ─── Types ─────────────────────────────────────────────────────────────────── /** A venue available for selection */ export interface Venue { id: string; name: string; imageUrl: string; location: string; capacity?: number; price?: number; } /** A venue type option for the filter */ export interface VenueTypeOption { /** Machine-readable value */ value: string; /** Human-readable label */ label: string; } /** Structured filter state for the venue list */ export interface VenueFilterValues { /** Selected venue type values (empty = all) */ venueTypes: string[]; /** Show only venues with video streaming */ videoStreaming: boolean; /** Show only venues with photo display */ photoDisplay: boolean; /** Selected service tradition (null = any) */ tradition: string | null; } /** Sort options for the venue list */ export type VenueSortBy = 'recommended' | 'nearest' | 'price_low' | 'price_high'; /** View mode for the listing */ export type ListViewMode = 'list' | 'map'; /** Props for the VenueStep page component */ export interface VenueStepProps { /** List of venues to display */ venues: Venue[]; /** Callback when a venue card is clicked — triggers navigation to VenueDetailStep */ onSelectVenue: (id: string) => void; /** Search query value */ searchQuery: string; /** Callback when search query changes */ onSearchChange: (query: string) => void; /** Callback when search is submitted */ onSearch?: (query: string) => void; /** Current filter state */ filterValues: VenueFilterValues; /** Callback when any filter changes */ onFilterChange: (values: VenueFilterValues) => void; /** Available venue type options */ venueTypeOptions?: VenueTypeOption[]; /** Available service tradition options for the autocomplete */ traditionOptions?: string[]; /** Current sort order */ sortBy?: VenueSortBy; /** Callback when sort order changes */ onSortChange?: (sort: VenueSortBy) => void; /** Current view mode */ viewMode?: ListViewMode; /** Callback when view mode changes */ onViewModeChange?: (mode: ListViewMode) => void; /** Callback for back navigation */ onBack: () => void; /** Location name for the results count */ locationName?: string; /** Whether this is a pre-planning flow */ isPrePlanning?: boolean; /** Map panel content — slot for map integration */ mapPanel?: React.ReactNode; /** Navigation bar — passed through to WizardLayout */ navigation?: React.ReactNode; /** Progress stepper */ progressStepper?: React.ReactNode; /** Running total widget (e.g. CartButton) */ runningTotal?: React.ReactNode; /** MUI sx prop for the root */ sx?: SxProps; } // ─── Defaults ──────────────────────────────────────────────────────────────── const DEFAULT_VENUE_TYPES: VenueTypeOption[] = [ { value: 'chapel', label: 'Chapel' }, { value: 'church', label: 'Church' }, { value: 'cathedral', label: 'Cathedral' }, { value: 'outdoor', label: 'Outdoor Venue' }, { value: 'hall', label: 'Hall / Room' }, { value: 'mosque', label: 'Mosque' }, { value: 'temple', label: 'Temple' }, ]; const DEFAULT_TRADITIONS = [ 'None', 'Anglican', "Bahá'í", 'Baptist', 'Buddhist', 'Catholic', 'Eastern Orthodox', 'Hindu', 'Humanist', 'Indigenous Australian', 'Jewish', 'Lutheran', 'Methodist', 'Muslim', 'Non-religious', 'Pentecostal', 'Presbyterian', 'Salvation Army', 'Secular', 'Sikh', 'Uniting Church', ]; const SORT_OPTIONS: { value: VenueSortBy; label: string }[] = [ { value: 'recommended', label: 'Recommended' }, { value: 'nearest', label: 'Nearest' }, { value: 'price_low', label: 'Price: Low to High' }, { value: 'price_high', label: 'Price: High to Low' }, ]; export const EMPTY_VENUE_FILTERS: VenueFilterValues = { venueTypes: [], videoStreaming: false, photoDisplay: false, tradition: null, }; // ─── Shared styles ─────────────────────────────────────────────────────────── const sectionHeadingSx = { mb: 1.5, display: 'block', fontWeight: 600, color: 'text.primary', } as const; const chipWrapSx = { display: 'flex', flexWrap: 'wrap', gap: 1, } as const; // ─── Component ─────────────────────────────────────────────────────────────── /** * Step 7 — Service Venue selection for the FA arrangement wizard. * * List + Map split layout. Left panel shows a scrollable list of * venue cards with search and filter button. Right panel is a * slot for map integration. * * Filters: location (chip-in-input), venue type (wrapping chips), * additional services (switches), service tradition (autocomplete). * * Click-to-navigate: clicking a venue card triggers navigation * to VenueDetailStep — no selection state or Continue button. * * Pure presentation component — props in, callbacks out. * * Spec: documentation/steps/steps/07_venue_consolidated.yaml */ export const VenueStep: React.FC = ({ venues, onSelectVenue, searchQuery, onSearchChange, onSearch, filterValues, onFilterChange, venueTypeOptions = DEFAULT_VENUE_TYPES, traditionOptions = DEFAULT_TRADITIONS, sortBy = 'recommended', onSortChange, viewMode = 'list', onViewModeChange, onBack, locationName, isPrePlanning = false, mapPanel, navigation, progressStepper, runningTotal, sx, }) => { const [sortAnchor, setSortAnchor] = React.useState(null); const subheading = isPrePlanning ? 'Browse available venues. Your choice can be changed later.' : 'Choose a venue for the funeral service. You can filter by type, services, and tradition.'; // ─── Active filter count ─── const activeCount = (searchQuery.trim() ? 1 : 0) + filterValues.venueTypes.length + (filterValues.videoStreaming ? 1 : 0) + (filterValues.photoDisplay ? 1 : 0) + (filterValues.tradition ? 1 : 0); const handleClear = () => { onSearchChange(''); onFilterChange(EMPTY_VENUE_FILTERS); }; const handleVenueTypeToggle = (value: string) => { const current = filterValues.venueTypes; const next = current.includes(value) ? current.filter((v) => v !== value) : [...current, value]; onFilterChange({ ...filterValues, venueTypes: next }); }; return ( {/* Floating view toggle */} val && onViewModeChange?.(val as ListViewMode)} size="small" aria-label="View mode" sx={{ position: 'absolute', top: 12, left: 12, zIndex: 1, bgcolor: 'background.paper', boxShadow: 'var(--fa-shadow-md)', borderRadius: 1, '& .MuiToggleButton-root': { px: 1.5, py: 0.5, fontSize: '0.75rem', fontWeight: 500, gap: 0.5, border: '1px solid', borderColor: 'divider', textTransform: 'none', '&.Mui-selected': { bgcolor: 'var(--fa-color-brand-100)', color: 'primary.main', borderColor: 'primary.main', '&:hover': { bgcolor: 'var(--fa-color-brand-200)' }, }, }, }} > List Map {/* Map content */} {mapPanel || ( Map coming soon )} } > {/* Heading — scrolls with listings */} Where would you like the service? {subheading} {/* Sticky controls — search + filters pinned while listings scroll */} {/* Location search */} onSearchChange(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && onSearch) { e.preventDefault(); onSearch(searchQuery); } }} fullWidth size="small" InputProps={{ startAdornment: ( ), }} sx={{ mb: 1.5 }} /> {/* Control bar — filters + sort */} {/* Filters */} {/* ── Location ── */} Location { const last = newValue[newValue.length - 1] ?? ''; onSearchChange(typeof last === 'string' ? last : ''); }} options={[]} renderInput={(params) => ( {params.InputProps.startAdornment} ), }} /> )} size="small" /> {/* ── Venue type ── */} Venue type {venueTypeOptions.map((option) => ( handleVenueTypeToggle(option.value)} variant="outlined" size="small" /> ))} {/* ── Additional services ── */} Additional services onFilterChange({ ...filterValues, videoStreaming: checked }) } /> } label="Video streaming available" sx={{ mx: 0 }} /> onFilterChange({ ...filterValues, photoDisplay: checked }) } /> } label="Photo display available" sx={{ mx: 0 }} /> {/* ── Service tradition ── */} Service tradition onFilterChange({ ...filterValues, tradition: newValue })} options={traditionOptions} renderInput={(params) => ( )} clearOnEscape size="small" /> {/* Sort — compact menu button, pushed right */} setSortAnchor(null)} anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} transformOrigin={{ vertical: 'top', horizontal: 'right' }} > {SORT_OPTIONS.map((opt) => ( { onSortChange?.(opt.value); setSortAnchor(null); }} sx={{ fontSize: '0.813rem' }} > {opt.label} ))} {/* Results count — below controls */} {venues.length} venue{venues.length !== 1 ? 's' : ''} {locationName ? ` near ${locationName}` : ''} found {/* Venue list — click-to-navigate */} {venues.map((venue) => ( onSelectVenue(venue.id)} aria-label={`${venue.name}, ${venue.location}${venue.price ? `, $${venue.price}` : ''}`} /> ))} {venues.length === 0 && ( No venues found in this area. Try adjusting your search or clearing filters. )} ); }; VenueStep.displayName = 'VenueStep'; export default VenueStep;