From 965f28186fd6974b603065be2e9d97c34de26f1e Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Fri, 10 Jul 2026 11:19:53 +0000 Subject: [PATCH 1/2] 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 && ( Date: Fri, 10 Jul 2026 11:44:00 +0000 Subject: [PATCH 2/2] Fix expired session showing EmbeddedFallback instead of login page Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/api.ts | 4 ++++ frontend/src/components/AuthGate.tsx | 16 +++------------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 65a6840..9ac50d8 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -11,6 +11,10 @@ async function request(path: string, opts: RequestInit = {}): Promise { headers: { 'Content-Type': 'application/json', ...opts.headers }, ...opts, }) + if (res.status === 401) { + ;(window.top ?? window).location.href = '/login' + throw new Error('Unauthenticated') + } if (!res.ok) { const err = await res.json().catch(() => ({ error: res.statusText })) throw new Error(err.error || `Request failed: ${res.status}`) diff --git a/frontend/src/components/AuthGate.tsx b/frontend/src/components/AuthGate.tsx index b25c7ea..175e009 100644 --- a/frontend/src/components/AuthGate.tsx +++ b/frontend/src/components/AuthGate.tsx @@ -12,30 +12,20 @@ export function useAuth() { export default function AuthGate({ children }: { children: React.ReactNode }) { const [user, setUser] = useState(null) - const [error, setError] = useState(null) useEffect(() => { fetch('/api/auth/verify?app=maintenance', { credentials: 'include' }) .then(r => { - if (r.status === 401 || r.status === 403) { - window.location.href = `/portal?redirect=${encodeURIComponent(window.location.href)}` + if (!r.ok) { + ;(window.top ?? window).location.href = '/login' return null } - if (!r.ok) throw new Error(`Auth check failed: ${r.status}`) return r.json() }) .then(data => { if (data) setUser(data) }) - .catch(err => setError(err.message)) + .catch(() => { ;(window.top ?? window).location.href = '/login' }) }, []) - if (error) { - return ( -
- Authentication error: {error} -
- ) - } - if (!user) { return (