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:
parent
25d6323aa4
commit
965f28186f
1 changed files with 67 additions and 40 deletions
|
|
@ -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,55 +151,70 @@ 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 sid = t.newbook_site_id ? String(t.newbook_site_id) : null
|
const renderTask = (t: Task) => {
|
||||||
const isOccupied = roomsView && !!sid && occupiedSiteIds.has(sid)
|
const sid = t.newbook_site_id ? String(t.newbook_site_id) : null
|
||||||
const isArriving = roomsView && !!sid && !isOccupied && arrivingSiteIds.has(sid)
|
const isOccupied = roomsView && !!sid && occupiedSiteIds.has(sid)
|
||||||
return (
|
const isArriving = roomsView && !!sid && !isOccupied && arrivingSiteIds.has(sid)
|
||||||
<div
|
return (
|
||||||
key={t.id}
|
<div
|
||||||
className={`card task-card ${t.priority === 'urgent' ? 'urgent' : t.priority === 'high' ? 'high' : ''} ${t.unusable ? 'unusable' : ''}`}
|
key={t.id}
|
||||||
style={isOccupied ? { opacity: 0.5, filter: 'grayscale(0.3)' } : undefined}
|
className={`card task-card ${t.priority === 'urgent' ? 'urgent' : t.priority === 'high' ? 'high' : ''} ${t.unusable ? 'unusable' : ''}`}
|
||||||
onClick={() => setOpenTask(t.id)}
|
style={isOccupied ? { opacity: 0.5, filter: 'grayscale(0.3)' } : undefined}
|
||||||
>
|
onClick={() => setOpenTask(t.id)}
|
||||||
<div className="task-card-main">
|
>
|
||||||
<div className="task-card-title">
|
<div className="task-card-main">
|
||||||
{t.title}
|
<div className="task-card-title">
|
||||||
{isOccupied && <span className="badge badge-outline"><BedDouble size={10} strokeWidth={1.75} /> Occupied</span>}
|
{t.title}
|
||||||
{isArriving && <span className="badge" style={{ background: '#fef3c7', color: '#92400e', border: '1px solid #fcd34d' }}><BedDouble size={10} strokeWidth={1.75} /> Arrival today</span>}
|
{isOccupied && <span className="badge badge-outline"><BedDouble size={10} strokeWidth={1.75} /> Occupied</span>}
|
||||||
{t.unusable && <UnusableBadge />}
|
{isArriving && <span className="badge" style={{ background: '#fef3c7', color: '#92400e', border: '1px solid #fcd34d' }}><BedDouble size={10} strokeWidth={1.75} /> Arrival today</span>}
|
||||||
{t.template_id && <span className="badge badge-outline">Recurring</span>}
|
{t.unusable && <UnusableBadge />}
|
||||||
|
{t.template_id && <span className="badge badge-outline">Recurring</span>}
|
||||||
|
</div>
|
||||||
|
<div className="task-card-meta">
|
||||||
|
{!groupByLocation && <span>{t.location_name}</span>}
|
||||||
|
<span>{t.category_name}</span>
|
||||||
|
{t.asset_name && <span>{t.asset_name}</span>}
|
||||||
|
<span>{ageLabel(t.created_at)} old</span>
|
||||||
|
{t.due_date && (
|
||||||
|
<span className={new Date(t.due_date) < new Date() ? 'overdue' : ''}>
|
||||||
|
due {formatDate(t.due_date)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{t.hold_until && <span>held until {formatDate(t.hold_until)}</span>}
|
||||||
|
{t.photo_count > 0 && <span><Camera size={12} strokeWidth={1.75} style={{ verticalAlign: -2 }} /> {t.photo_count}</span>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="task-card-meta">
|
<div className="task-card-side">
|
||||||
<span>{t.location_name}</span>
|
<PriorityBadge priority={t.priority} />
|
||||||
<span>{t.category_name}</span>
|
<StatusBadge status={t.status} />
|
||||||
{t.asset_name && <span>{t.asset_name}</span>}
|
<span className="muted" style={{ fontSize: 11.5 }}>
|
||||||
<span>{ageLabel(t.created_at)} old</span>
|
{t.assigned_type === 'contractor' ? (t.contractor_name || '—') : (t.assigned_to_name || 'Unassigned')}
|
||||||
{t.due_date && (
|
</span>
|
||||||
<span className={new Date(t.due_date) < new Date() ? 'overdue' : ''}>
|
|
||||||
due {formatDate(t.due_date)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{t.hold_until && <span>held until {formatDate(t.hold_until)}</span>}
|
|
||||||
{t.photo_count > 0 && <span><Camera size={12} strokeWidth={1.75} style={{ verticalAlign: -2 }} /> {t.photo_count}</span>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="task-card-side">
|
)
|
||||||
<PriorityBadge priority={t.priority} />
|
}
|
||||||
<StatusBadge status={t.status} />
|
|
||||||
<span className="muted" style={{ fontSize: 11.5 }}>
|
if (taskGroups) {
|
||||||
{t.assigned_type === 'contractor' ? (t.contractor_name || '—') : (t.assigned_to_name || 'Unassigned')}
|
return taskGroups.map(([locationName, groupTasks]) => (
|
||||||
</span>
|
<div key={locationName}>
|
||||||
|
<div className="section-title">{locationName} <span style={{ fontWeight: 400, textTransform: 'none', letterSpacing: 0 }}>({groupTasks.length})</span></div>
|
||||||
|
{groupTasks.map(renderTask)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))
|
||||||
)
|
}
|
||||||
})}
|
return tasks.map(renderTask)
|
||||||
|
})()}
|
||||||
|
|
||||||
{showNew && (
|
{showNew && (
|
||||||
<NewTaskModal
|
<NewTaskModal
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue