diff --git a/frontend/src/pages/Summary.tsx b/frontend/src/pages/Summary.tsx index ef74e77..9dee753 100644 --- a/frontend/src/pages/Summary.tsx +++ b/frontend/src/pages/Summary.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from 'react' -import { Plus, RefreshCw, Camera, BedDouble } from 'lucide-react' +import { Plus, RefreshCw, Camera, BedDouble, Layers } 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' @@ -25,6 +25,7 @@ export default function Summary() { const [categoryFilter, setCategoryFilter] = useState(null) const [mineOnly, setMineOnly] = useState(false) const [roomsView, setRoomsView] = useState(false) + const [groupByLocation, setGroupByLocation] = useState(false) const [showNew, setShowNew] = useState(false) const [openTask, setOpenTask] = useState(null) @@ -83,6 +84,17 @@ export default function Summary() { 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() { @@ -139,55 +151,70 @@ export default function Summary() { : 'Rooms Accessible View'} )} + {tasks.length === 0 && !loading && (
No open tasks match these filters.
)} - {tasks.map(t => { - 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} + {(() => { + 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.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')} +
-
- - - - {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 && (