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 (
(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 && (