import { useCallback, useEffect, useMemo, useState } from 'react' import { Plus, RefreshCw, Camera, BedDouble, Layers, ChevronDown } from 'lucide-react' import type { Task, Category, Location, AppConfig, TaskStatus } 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 [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) const [showNew, setShowNew] = useState(false) 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()) 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(() => {}) }, []) 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 in room, arriving = booked today not yet in const roomsCounts = useMemo(() => { if (!roomsView) return null let occupied = 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 && arrivingSiteIds.has(sid)) arriving++ } return { total: tasks.length, occupied, arriving, free: tasks.length - occupied - arriving } }, [tasks, roomsView, occupiedSiteIds, arrivingSiteIds]) const taskGroups = useMemo(() => { if (!groupByLocation) return null const groups: Record = {} for (const t of tasks) { 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)) }, [tasks, 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 isArriving = roomsView && !!sid && !isOccupied && arrivingSiteIds.has(sid) return (
setOpenTask(t.id)} >
{t.title} {isOccupied && Occupied} {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 tasks.map(renderTask) })()} {showNew && ( setShowNew(false)} onCreated={load} /> )} {openTask !== null && ( setOpenTask(null)} onChanged={load} /> )}
) }