import { useCallback, useEffect, useMemo, useState } from 'react' import { useSearchParams } from 'react-router-dom' import { Plus, RefreshCw, Camera, BedDouble, LogOut, Layers, ChevronDown, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react' import type { Task, Category, Location, AppConfig, TaskStatus, Priority } from '../types' import { STATUS_LABELS, can } from '../types' import { fetchTasks, fetchLocations, fetchConfig, fetchOccupancy } from '../api' import { useAuth } from '../components/AuthGate' import NewTaskModal from '../components/NewTaskModal' import TaskModal from '../components/TaskModal' import { PriorityBadge, StatusBadge, UnusableBadge, ageLabel, formatDate } from '../components/shared' const OPEN_STATUSES: TaskStatus[] = ['submitted', 'in_progress', 'hold_parts', 'hold_scheduled', 'temporary_fix'] export default function Summary() { const { user } = useAuth() const [tasks, setTasks] = useState([]) const [categories, setCategories] = useState([]) const [locations, setLocations] = useState([]) const [config, setConfig] = useState(null) const [error, setError] = useState(null) const [loading, setLoading] = useState(false) const [occupiedSiteIds, setOccupiedSiteIds] = useState>(new Set()) const [departingSiteIds, setDepartingSiteIds] = useState>(new Set()) const [arrivingSiteIds, setArrivingSiteIds] = useState>(new Set()) const [statusFilter, setStatusFilter] = useState(null) const [categoryFilter, setCategoryFilter] = useState(null) const [mineOnly, setMineOnly] = useState(false) const [roomsView, setRoomsView] = useState(false) const [groupByLocation, setGroupByLocation] = useState(false) const [statusOpen, setStatusOpen] = useState(false) const [filtersOpen, setFiltersOpen] = useState(false) type SortMode = 'default' | 'priority_desc' | 'priority_asc' | 'age_asc' | 'age_desc' const [sortMode, setSortMode] = useState('default') const SORT_CYCLE: SortMode[] = ['default', 'priority_desc', 'priority_asc', 'age_asc', 'age_desc'] const SORT_LABELS: Record = { default: 'Sort', priority_desc: 'Priority: High first', priority_asc: 'Priority: Low first', age_asc: 'Age: Oldest first', age_desc: 'Age: Newest first', } const PRIORITY_ORDER: Record = { urgent: 4, high: 3, medium: 2, low: 1 } function cycleSortMode() { setSortMode(prev => SORT_CYCLE[(SORT_CYCLE.indexOf(prev) + 1) % SORT_CYCLE.length]) } const [searchParams, setSearchParams] = useSearchParams() const [showNew, setShowNew] = useState(false) const [newTaskPreset, setNewTaskPreset] = useState<{ locationId?: number; assetId?: number }>({}) const [openTask, setOpenTask] = useState(null) const roomsCat = useMemo(() => categories.find(c => c.is_rooms) ?? null, [categories]) const load = useCallback(() => { setLoading(true) const taskFetch = fetchTasks({ status: statusFilter ?? OPEN_STATUSES.join(','), // In rooms-view mode filter to rooms category; otherwise use the active category chip category_id: roomsView ? (roomsCat?.id ?? undefined) : (categoryFilter ?? undefined), assigned_to: mineOnly ? user.email : undefined, }) // When rooms-view is on, also fetch live occupancy to mark occupied tasks const occFetch = roomsView ? fetchOccupancy().catch(() => null) : Promise.resolve(null) Promise.all([taskFetch, occFetch]) .then(([t, occ]) => { setTasks(t) setOccupiedSiteIds(occ ? new Set(occ.occupied_site_ids) : new Set()) setDepartingSiteIds(occ ? new Set(occ.departing_site_ids) : new Set()) setArrivingSiteIds(occ ? new Set(occ.arriving_site_ids) : new Set()) setError(null) }) .catch(err => setError(err.message)) .finally(() => setLoading(false)) }, [statusFilter, categoryFilter, mineOnly, roomsView, roomsCat, user.email]) useEffect(() => { load() }, [load]) useEffect(() => { fetchLocations().then(d => { setCategories(d.categories); setLocations(d.locations) }).catch(() => {}) fetchConfig().then(setConfig).catch(() => {}) }, []) // Deep link from the Assets page: ?new_asset=&new_location= opens a prefilled // Report fault form instead of the asset page having its own creation form. useEffect(() => { const newAsset = searchParams.get('new_asset') const newLocation = searchParams.get('new_location') if (newAsset || newLocation) { setNewTaskPreset({ locationId: newLocation ? parseInt(newLocation) : undefined, assetId: newAsset ? parseInt(newAsset) : undefined, }) setShowNew(true) setSearchParams(prev => { const next = new URLSearchParams(prev) next.delete('new_asset'); next.delete('new_location') return next }, { replace: true }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchParams]) const counts = useMemo(() => { const c: Partial> = {} for (const t of tasks) c[t.status] = (c[t.status] || 0) + 1 return c }, [tasks]) const categoryCounts = useMemo(() => { const c: Record = {} for (const t of tasks) c[t.category_id] = (c[t.category_id] || 0) + 1 return c }, [tasks]) // Rooms chip counts: occupied = guest staying, departing = guest checking out // today (accessible later), arriving = booked today not yet in const roomsCounts = useMemo(() => { if (!roomsView) return null let occupied = 0, departing = 0, arriving = 0 for (const t of tasks) { const sid = t.newbook_site_id ? String(t.newbook_site_id) : null if (sid && occupiedSiteIds.has(sid)) occupied++ else if (sid && departingSiteIds.has(sid)) departing++ else if (sid && arrivingSiteIds.has(sid)) arriving++ } return { total: tasks.length, occupied, departing, arriving, free: tasks.length - occupied - departing - arriving } }, [tasks, roomsView, occupiedSiteIds, departingSiteIds, arrivingSiteIds]) const sortedTasks = useMemo(() => { if (sortMode === 'default') return tasks return [...tasks].sort((a, b) => { if (sortMode === 'priority_desc') return (PRIORITY_ORDER[b.priority] ?? 0) - (PRIORITY_ORDER[a.priority] ?? 0) if (sortMode === 'priority_asc') return (PRIORITY_ORDER[a.priority] ?? 0) - (PRIORITY_ORDER[b.priority] ?? 0) if (sortMode === 'age_asc') return new Date(a.created_at).getTime() - new Date(b.created_at).getTime() if (sortMode === 'age_desc') return new Date(b.created_at).getTime() - new Date(a.created_at).getTime() return 0 }) }, [tasks, sortMode]) const taskGroups = useMemo(() => { if (!groupByLocation) return null const groups: Record = {} for (const t of sortedTasks) { const key = t.location_name || 'Unknown' if (!groups[key]) groups[key] = [] groups[key].push(t) } return Object.entries(groups).sort(([a], [b]) => a.localeCompare(b)) }, [sortedTasks, groupByLocation]) const roomsCategoryExists = !!roomsCat function toggleRoomsView() { setRoomsView(v => !v) setCategoryFilter(null) // clear any category chip when entering/leaving rooms-view } return (

Open maintenance

{can(user, 'report') && ( )}
{error &&
{error}
}
setStatusOpen(v => !v)}> Status {statusFilter === null ? 'All open' : `${STATUS_LABELS[statusFilter]}${counts[statusFilter] ? ` (${counts[statusFilter]})` : ''}`}
{statusOpen && (
{OPEN_STATUSES.map(s => ( ))}
)}
setFiltersOpen(v => !v)}> Filters {[ !roomsView && categoryFilter ? categories.find(c => c.id === categoryFilter)?.name : null, roomsView ? 'Rooms view' : null, mineOnly ? 'Mine only' : null, groupByLocation ? 'Grouped' : null, ].filter(Boolean).join(' · ') || 'All categories'}
{filtersOpen && (
{!roomsView && categories.map(c => ( ))} {roomsCategoryExists && ( )}
)}
{tasks.length === 0 && !loading && (
No open tasks match these filters.
)} {(() => { const renderTask = (t: Task) => { const sid = t.newbook_site_id ? String(t.newbook_site_id) : null const isOccupied = roomsView && !!sid && occupiedSiteIds.has(sid) const isDeparting = roomsView && !!sid && !isOccupied && departingSiteIds.has(sid) // Not exclusive with isDeparting — a departing room can also have a same-day // incoming booking (back-to-back turnover), and both badges should show. const isArriving = roomsView && !!sid && !isOccupied && arrivingSiteIds.has(sid) return (
setOpenTask(t.id)} >
{t.title} {isOccupied && Occupied} {isDeparting && Departing} {isArriving && Arrival today} {t.unusable && } {t.template_id && Recurring}
{!groupByLocation && {t.location_name}} {t.category_name} {t.asset_name && {t.asset_name}} {ageLabel(t.created_at)} old {t.due_date && ( due {formatDate(t.due_date)} )} {t.hold_until && held until {formatDate(t.hold_until)}} {t.photo_count > 0 && {t.photo_count}}
{t.assigned_type === 'contractor' ? (t.contractor_name || '—') : (t.assigned_to_name || 'Unassigned')}
) } if (taskGroups) { return taskGroups.map(([locationName, groupTasks]) => (
{locationName} ({groupTasks.length})
{groupTasks.map(renderTask)}
)) } return sortedTasks.map(renderTask) })()} {showNew && ( setShowNew(false)} onCreated={load} /> )} {openTask !== null && ( setOpenTask(null)} onChanged={load} /> )}
) }