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 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-10 11:19:53 +00:00
parent 25d6323aa4
commit 965f28186f

View file

@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react' 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 type { Task, Category, Location, AppConfig, TaskStatus } from '../types'
import { STATUS_LABELS, can } from '../types' import { STATUS_LABELS, can } from '../types'
import { fetchTasks, fetchLocations, fetchConfig, fetchOccupancy } from '../api' import { fetchTasks, fetchLocations, fetchConfig, fetchOccupancy } from '../api'
@ -25,6 +25,7 @@ export default function Summary() {
const [categoryFilter, setCategoryFilter] = useState<number | null>(null) const [categoryFilter, setCategoryFilter] = useState<number | null>(null)
const [mineOnly, setMineOnly] = useState(false) const [mineOnly, setMineOnly] = useState(false)
const [roomsView, setRoomsView] = useState(false) const [roomsView, setRoomsView] = useState(false)
const [groupByLocation, setGroupByLocation] = useState(false)
const [showNew, setShowNew] = useState(false) const [showNew, setShowNew] = useState(false)
const [openTask, setOpenTask] = useState<number | null>(null) const [openTask, setOpenTask] = useState<number | null>(null)
@ -83,6 +84,17 @@ export default function Summary() {
return { total: tasks.length, occupied, arriving, free: tasks.length - occupied - arriving } return { total: tasks.length, occupied, arriving, free: tasks.length - occupied - arriving }
}, [tasks, roomsView, occupiedSiteIds, arrivingSiteIds]) }, [tasks, roomsView, occupiedSiteIds, arrivingSiteIds])
const taskGroups = useMemo(() => {
if (!groupByLocation) return null
const groups: Record<string, Task[]> = {}
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 const roomsCategoryExists = !!roomsCat
function toggleRoomsView() { function toggleRoomsView() {
@ -139,13 +151,17 @@ export default function Summary() {
: 'Rooms Accessible View'} : 'Rooms Accessible View'}
</button> </button>
)} )}
<button className={`chip ${groupByLocation ? 'active' : ''}`} onClick={() => setGroupByLocation(v => !v)}>
<Layers size={13} strokeWidth={1.75} /> Group by location
</button>
</div> </div>
{tasks.length === 0 && !loading && ( {tasks.length === 0 && !loading && (
<div className="empty-state">No open tasks match these filters.</div> <div className="empty-state">No open tasks match these filters.</div>
)} )}
{tasks.map(t => { {(() => {
const renderTask = (t: Task) => {
const sid = t.newbook_site_id ? String(t.newbook_site_id) : null const sid = t.newbook_site_id ? String(t.newbook_site_id) : null
const isOccupied = roomsView && !!sid && occupiedSiteIds.has(sid) const isOccupied = roomsView && !!sid && occupiedSiteIds.has(sid)
const isArriving = roomsView && !!sid && !isOccupied && arrivingSiteIds.has(sid) const isArriving = roomsView && !!sid && !isOccupied && arrivingSiteIds.has(sid)
@ -165,7 +181,7 @@ export default function Summary() {
{t.template_id && <span className="badge badge-outline">Recurring</span>} {t.template_id && <span className="badge badge-outline">Recurring</span>}
</div> </div>
<div className="task-card-meta"> <div className="task-card-meta">
<span>{t.location_name}</span> {!groupByLocation && <span>{t.location_name}</span>}
<span>{t.category_name}</span> <span>{t.category_name}</span>
{t.asset_name && <span>{t.asset_name}</span>} {t.asset_name && <span>{t.asset_name}</span>}
<span>{ageLabel(t.created_at)} old</span> <span>{ageLabel(t.created_at)} old</span>
@ -187,7 +203,18 @@ export default function Summary() {
</div> </div>
</div> </div>
) )
})} }
if (taskGroups) {
return taskGroups.map(([locationName, groupTasks]) => (
<div key={locationName}>
<div className="section-title">{locationName} <span style={{ fontWeight: 400, textTransform: 'none', letterSpacing: 0 }}>({groupTasks.length})</span></div>
{groupTasks.map(renderTask)}
</div>
))
}
return tasks.map(renderTask)
})()}
{showNew && ( {showNew && (
<NewTaskModal <NewTaskModal