From 513565ecc87342aa1ae8325b82f0c165f06d8204 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Wed, 22 Jul 2026 09:02:55 +0000 Subject: [PATCH] Replace collapsible filter accordions with flat horizontal chip bar + add sort chip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Status and filter sections now always visible in one scrollable chip-bar. Sort chip cycles through: Priority high→low, Priority low→high, Age oldest→newest, Age newest→oldest. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/index.css | 24 +----- frontend/src/pages/Summary.tsx | 139 +++++++++++++++++---------------- 2 files changed, 73 insertions(+), 90 deletions(-) diff --git a/frontend/src/index.css b/frontend/src/index.css index 7c704de..0f30c1f 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -261,28 +261,8 @@ html, body, #root { height: 100%; margin: 0; font-size: 14px; } .badge-unusable { background: var(--danger); } /* ── Filter chips ──────────────────────────────────────────── */ -.filter-section { margin-bottom: 10px; } -.filter-section-header { - display: flex; - align-items: center; - gap: 8px; - padding: 6px 10px; - border-radius: 8px; - background: var(--card-bg); - border: 1px solid var(--card-border); - cursor: pointer; - user-select: none; - font-size: 12px; - color: var(--text-mid); - transition: border-color .12s; -} -.filter-section-header:hover { border-color: var(--gold); } -.filter-section-label { font-weight: 600; color: var(--text-dark); white-space: nowrap; } -.filter-section-summary { flex: 1; color: var(--gold); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.filter-section-summary.muted { color: var(--text-mid); font-weight: 400; } -.filter-section-chevron { flex-shrink: 0; transition: transform .18s; } -.filter-section-chevron.open { transform: rotate(180deg); } -.chip-bar { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0 4px; align-items: center; } +.chip-bar { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; } +.chip-divider { width: 1px; height: 20px; background: var(--card-border); flex-shrink: 0; margin: 0 2px; } .chip { display: inline-flex; align-items: center; diff --git a/frontend/src/pages/Summary.tsx b/frontend/src/pages/Summary.tsx index 8b25261..466510e 100644 --- a/frontend/src/pages/Summary.tsx +++ b/frontend/src/pages/Summary.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react' -import { Plus, RefreshCw, Camera, BedDouble, Layers, ChevronDown } from 'lucide-react' -import type { Task, Category, Location, AppConfig, TaskStatus } from '../types' +import { Plus, RefreshCw, Camera, BedDouble, Layers, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react' +import type { Task, Category, Location, AppConfig, TaskStatus, Priority } from '../types' import { STATUS_LABELS, can } from '../types' import { fetchTasks, fetchLocations, fetchConfig, fetchOccupancy } from '../api' import { useAuth } from '../components/AuthGate' @@ -27,8 +27,22 @@ export default function Summary() { const [roomsView, setRoomsView] = useState(false) const [groupByLocation, setGroupByLocation] = useState(false) - const [statusOpen, setStatusOpen] = useState(false) - const [filtersOpen, setFiltersOpen] = useState(false) + type SortMode = 'default' | 'priority_desc' | 'priority_asc' | 'age_asc' | 'age_desc' + const [sortMode, setSortMode] = useState('default') + + const SORT_CYCLE: SortMode[] = ['default', 'priority_desc', 'priority_asc', 'age_asc', 'age_desc'] + const SORT_LABELS: Record = { + default: 'Sort', + priority_desc: 'Priority: High first', + priority_asc: 'Priority: Low first', + age_asc: 'Age: Oldest first', + age_desc: 'Age: Newest first', + } + const PRIORITY_ORDER: Record = { urgent: 4, high: 3, medium: 2, low: 1 } + + function cycleSortMode() { + setSortMode(prev => SORT_CYCLE[(SORT_CYCLE.indexOf(prev) + 1) % SORT_CYCLE.length]) + } const [showNew, setShowNew] = useState(false) const [openTask, setOpenTask] = useState(null) @@ -87,16 +101,27 @@ export default function Summary() { return { total: tasks.length, occupied, arriving, free: tasks.length - occupied - arriving } }, [tasks, roomsView, occupiedSiteIds, arrivingSiteIds]) + const sortedTasks = useMemo(() => { + if (sortMode === 'default') return tasks + return [...tasks].sort((a, b) => { + if (sortMode === 'priority_desc') return (PRIORITY_ORDER[b.priority] ?? 0) - (PRIORITY_ORDER[a.priority] ?? 0) + if (sortMode === 'priority_asc') return (PRIORITY_ORDER[a.priority] ?? 0) - (PRIORITY_ORDER[b.priority] ?? 0) + if (sortMode === 'age_asc') return new Date(a.created_at).getTime() - new Date(b.created_at).getTime() + if (sortMode === 'age_desc') return new Date(b.created_at).getTime() - new Date(a.created_at).getTime() + return 0 + }) + }, [tasks, sortMode]) + const taskGroups = useMemo(() => { if (!groupByLocation) return null const groups: Record = {} - for (const t of tasks) { + for (const t of sortedTasks) { 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]) + }, [sortedTasks, groupByLocation]) const roomsCategoryExists = !!roomsCat @@ -121,68 +146,46 @@ export default function Summary() { {error &&
{error}
} -
-
setStatusOpen(v => !v)}> - Status - - {statusFilter === null ? 'All open' : `${STATUS_LABELS[statusFilter]}${counts[statusFilter] ? ` (${counts[statusFilter]})` : ''}`} - - -
- {statusOpen && ( -
- - {OPEN_STATUSES.map(s => ( - - ))} -
- )} -
- -
-
setFiltersOpen(v => !v)}> - Filters - - {[ - !roomsView && categoryFilter ? categories.find(c => c.id === categoryFilter)?.name : null, - roomsView ? 'Rooms view' : null, - mineOnly ? 'Mine only' : null, - groupByLocation ? 'Grouped' : null, - ].filter(Boolean).join(' · ') || 'All categories'} - - -
- {filtersOpen && ( -
- {!roomsView && categories.map(c => ( - - ))} - - {roomsCategoryExists && ( - - )} - -
+
+ + {OPEN_STATUSES.map(s => ( + + ))} + + {!roomsView && categories.map(c => ( + + ))} + + {roomsCategoryExists && ( + )} + + +
{tasks.length === 0 && !loading && ( @@ -242,7 +245,7 @@ export default function Summary() {
)) } - return tasks.map(renderTask) + return sortedTasks.map(renderTask) })()} {showNew && (