From 965f28186fd6974b603065be2e9d97c34de26f1e Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Fri, 10 Jul 2026 11:19:53 +0000 Subject: [PATCH] Add group-by-location toggle to maintenance summary filters Layers chip in the second chip bar groups tasks by location alphabetically with section headers. Location name is suppressed from task card meta when grouping is active to avoid redundancy. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/pages/Summary.tsx | 107 +++++++++++++++++++++------------ 1 file changed, 67 insertions(+), 40 deletions(-) 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 && (