Add a Departing state to the Rooms Accessible View

A guest checking out today but not yet gone was previously lumped in
with ordinary stayovers — fully greyed out, no way to tell "the guest is
gone in 20 minutes" apart from "the guest isn't leaving at all". Split
that out into its own bucket:

Backend: classifyBookingStatus() now also recognises 'depart'/'checkedout'
statuses (already checked out — room's free regardless of the date
window). fetchRoomOccupancy() returns occupied/departing/arriving; an
in-house booking whose departure date is today lands in 'departing'
instead of 'occupied'. The ?unoccupied=true task filter and /api/occupancy
both updated for the new three-way split.

Frontend: departing rooms get a light dim (opacity only, no greyscale)
instead of the hard grey-out, plus a purple 'Departing' badge — matching
the booked=green/occupied=blue/departing=purple colour language hvac
already uses for NewBook room states. Chip label breakdown now reads
"X free • Y arriving • Z departing • W occupied".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-09-03 09:33:57 +00:00
parent 96674c58c8
commit 8ddb99d2d7
3 changed files with 54 additions and 25 deletions

View file

@ -91,7 +91,7 @@ export function resolveTask(id: number, body: {
export function addComment(id: number, note: string, addToTemplate = false): Promise<{ ok: boolean; added_to_template: boolean }> {
return request(`/tasks/${id}/comments`, { method: 'POST', body: JSON.stringify({ note, add_to_template: addToTemplate }) })
}
export function fetchOccupancy(): Promise<{ date: string; occupied_site_ids: string[]; arriving_site_ids: string[] }> {
export function fetchOccupancy(): Promise<{ date: string; occupied_site_ids: string[]; departing_site_ids: string[]; arriving_site_ids: string[] }> {
return request('/occupancy')
}

View file

@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { Plus, RefreshCw, Camera, BedDouble, Layers, ChevronDown, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'
import { Plus, RefreshCw, Camera, BedDouble, LogOut, Layers, ChevronDown, 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'
@ -20,6 +20,7 @@ export default function Summary() {
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const [occupiedSiteIds, setOccupiedSiteIds] = useState<Set<string>>(new Set())
const [departingSiteIds, setDepartingSiteIds] = useState<Set<string>>(new Set())
const [arrivingSiteIds, setArrivingSiteIds] = useState<Set<string>>(new Set())
const [statusFilter, setStatusFilter] = useState<TaskStatus | null>(null)
@ -70,6 +71,7 @@ export default function Summary() {
.then(([t, occ]) => {
setTasks(t)
setOccupiedSiteIds(occ ? new Set(occ.occupied_site_ids) : new Set())
setDepartingSiteIds(occ ? new Set(occ.departing_site_ids) : new Set())
setArrivingSiteIds(occ ? new Set(occ.arriving_site_ids) : new Set())
setError(null)
})
@ -115,17 +117,19 @@ export default function Summary() {
return c
}, [tasks])
// Rooms chip counts: occupied = guest in room, arriving = booked today not yet in
// Rooms chip counts: occupied = guest staying, departing = guest checking out
// today (accessible later), arriving = booked today not yet in
const roomsCounts = useMemo(() => {
if (!roomsView) return null
let occupied = 0, arriving = 0
let occupied = 0, departing = 0, arriving = 0
for (const t of tasks) {
const sid = t.newbook_site_id ? String(t.newbook_site_id) : null
if (sid && occupiedSiteIds.has(sid)) occupied++
else if (sid && departingSiteIds.has(sid)) departing++
else if (sid && arrivingSiteIds.has(sid)) arriving++
}
return { total: tasks.length, occupied, arriving, free: tasks.length - occupied - arriving }
}, [tasks, roomsView, occupiedSiteIds, arrivingSiteIds])
return { total: tasks.length, occupied, departing, arriving, free: tasks.length - occupied - departing - arriving }
}, [tasks, roomsView, occupiedSiteIds, departingSiteIds, arrivingSiteIds])
const sortedTasks = useMemo(() => {
if (sortMode === 'default') return tasks
@ -226,7 +230,7 @@ export default function Summary() {
>
<BedDouble size={13} strokeWidth={1.75} />
{roomsView && roomsCounts
? `Rooms (${roomsCounts.free} free${roomsCounts.arriving ? ` • ${roomsCounts.arriving} arriving` : ''}${roomsCounts.occupied ? ` • ${roomsCounts.occupied} occupied` : ''})`
? `Rooms (${roomsCounts.free} free${roomsCounts.arriving ? ` • ${roomsCounts.arriving} arriving` : ''}${roomsCounts.departing ? ` • ${roomsCounts.departing} departing` : ''}${roomsCounts.occupied ? ` • ${roomsCounts.occupied} occupied` : ''})`
: 'Rooms Accessible View'}
</button>
)}
@ -253,18 +257,23 @@ export default function Summary() {
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)
const isDeparting = roomsView && !!sid && !isOccupied && departingSiteIds.has(sid)
const isArriving = roomsView && !!sid && !isOccupied && !isDeparting && arrivingSiteIds.has(sid)
return (
<div
key={t.id}
className={`card task-card ${t.priority === 'urgent' ? 'urgent' : t.priority === 'high' ? 'high' : ''} ${t.unusable ? 'unusable' : ''}`}
style={isOccupied ? { opacity: 0.5, filter: 'grayscale(0.3)' } : undefined}
// Occupied (stayover) rooms grey out hard — not accessible today. Departing
// rooms are lightly dimmed only: guest is still in there right now, but the
// room frees up later today so it's worth flagging rather than hiding.
style={isOccupied ? { opacity: 0.5, filter: 'grayscale(0.3)' } : isDeparting ? { opacity: 0.8 } : undefined}
onClick={() => setOpenTask(t.id)}
>
<div className="task-card-main">
<div className="task-card-title">
{t.title}
{isOccupied && <span className="badge badge-outline"><BedDouble size={10} strokeWidth={1.75} /> Occupied</span>}
{isDeparting && <span className="badge" style={{ background: '#ede9fe', color: '#6d28d9', border: '1px solid #c4b5fd' }}><LogOut size={10} strokeWidth={1.75} /> Departing</span>}
{isArriving && <span className="badge" style={{ background: '#fef3c7', color: '#92400e', border: '1px solid #fcd34d' }}><BedDouble size={10} strokeWidth={1.75} /> Arrival today</span>}
{t.unusable && <UnusableBadge />}
{t.template_id && <span className="badge badge-outline">Recurring</span>}