From 25d6323aa437b007355faca95881f54a9d598083 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Thu, 9 Jul 2026 12:29:26 +0000 Subject: [PATCH] Differentiate arrived vs arriving-today in Rooms Accessible View MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A confirmed booking for today that hasn't checked in yet is accessible — staff can get in before the guest arrives. Previously it was lumped with in-room guests and greyed out. Backend: classifyBookingStatus() splits NewBook 'staying' bookings by status string (normalised). Statuses containing 'arriv', 'inhouse' or 'checkedin' → occupied; everything else → arriving. /api/occupancy now returns both occupied_site_ids and arriving_site_ids. Frontend: - Occupied rooms: greyed out + 'Occupied' badge (guest physically in room) - Arriving today: normal display + amber 'Arrival today' badge (access now, prioritise before guest checks in) - Chip label shows 'Rooms (X free • Y arriving • Z occupied)' breakdown Co-Authored-By: Claude Sonnet 4.6 --- backend/src/routes/tasks.js | 34 +++++++++++++++++++++++++++------- frontend/src/api.ts | 2 +- frontend/src/index.css | 15 +++++++++++++++ frontend/src/pages/Summary.tsx | 25 ++++++++++++++++++------- 4 files changed, 61 insertions(+), 15 deletions(-) diff --git a/backend/src/routes/tasks.js b/backend/src/routes/tasks.js index b36f09f..361d7c4 100644 --- a/backend/src/routes/tasks.js +++ b/backend/src/routes/tasks.js @@ -25,16 +25,31 @@ function stripCosts(task) { return rest } -async function fetchOccupiedSiteIds() { +// Returns guests physically in the room right now vs those booked to arrive today. +// NewBook status strings vary by account; we normalise and check for 'arriv' / 'inhouse' +// / 'checkedin' to identify in-room guests. Everything else in the 'staying' list is +// treated as "arriving today" — confirmed but not yet checked in. +function classifyBookingStatus(rawStatus) { + const s = String(rawStatus ?? '').toLowerCase().replace(/[\s_\-]/g, '') + if (s.includes('arriv') || s.includes('inhouse') || s.includes('checkedin')) return 'occupied' + return 'arriving' +} + +async function fetchRoomOccupancy() { const today = new Date().toISOString().slice(0, 10) const bookings = await fetchBookings(today, today) const occupied = new Set() + const arriving = new Set() for (const b of bookings) { - // NewBook returns booking_site_id on some account configurations, site_id on others const siteId = b.booking_site_id ?? b.site_id - if (siteId != null) occupied.add(String(siteId)) + if (siteId == null) continue + const id = String(siteId) + if (classifyBookingStatus(b.booking_status) === 'occupied') occupied.add(id) + else arriving.add(id) } - return occupied + // A site that is both (e.g. multi-booking day) should be treated as occupied + for (const id of occupied) arriving.delete(id) + return { occupied, arriving } } export async function taskRoutes(app) { @@ -266,11 +281,16 @@ export async function taskRoutes(app) { return { ok: true, added_to_template: addedToTemplate } }) - // GET /api/occupancy — today's in-house NewBook site ids (for the unoccupied filter UI) + // GET /api/occupancy — room state from NewBook for the Rooms Accessible View + // Returns two lists: occupied (guest in room) and arriving (booked today, not yet checked in) app.get('/api/occupancy', { preHandler: requireCap('view') }, async (req, reply) => { try { - const occupied = await fetchOccupiedSiteIds() - return { date: new Date().toISOString().slice(0, 10), occupied_site_ids: [...occupied] } + const { occupied, arriving } = await fetchRoomOccupancy() + return { + date: new Date().toISOString().slice(0, 10), + occupied_site_ids: [...occupied], + arriving_site_ids: [...arriving], + } } catch (err) { return reply.status(502).send({ error: `NewBook error: ${err.message}` }) } diff --git a/frontend/src/api.ts b/frontend/src/api.ts index d264c47..65a6840 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -84,7 +84,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[] }> { +export function fetchOccupancy(): Promise<{ date: string; occupied_site_ids: string[]; arriving_site_ids: string[] }> { return request('/occupancy') } diff --git a/frontend/src/index.css b/frontend/src/index.css index 21b75d6..388a5ff 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -375,3 +375,18 @@ table.data tr.clickable:hover td { background: var(--body-bg); } .section-title { font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--text-mid); margin: 18px 0 8px; } .muted { color: var(--text-mid); } .overdue { color: var(--danger); font-weight: 600; } + +/* Sidebar scrollbar */ +.nav-scroll::-webkit-scrollbar, +.sidebar::-webkit-scrollbar, +.sidebar-nav::-webkit-scrollbar { width: 4px; } +.nav-scroll::-webkit-scrollbar-track, +.sidebar::-webkit-scrollbar-track, +.sidebar-nav::-webkit-scrollbar-track { background: transparent; } +.nav-scroll::-webkit-scrollbar-thumb, +.sidebar::-webkit-scrollbar-thumb, +.sidebar-nav::-webkit-scrollbar-thumb { background: rgba(201,168,76,0.35); border-radius: 2px; } +.nav-scroll::-webkit-scrollbar-thumb:hover, +.sidebar::-webkit-scrollbar-thumb:hover, +.sidebar-nav::-webkit-scrollbar-thumb:hover { background: rgba(201,168,76,0.65); } +.nav-scroll, .sidebar, .sidebar-nav { scrollbar-width: thin; scrollbar-color: rgba(201,168,76,0.35) transparent; } diff --git a/frontend/src/pages/Summary.tsx b/frontend/src/pages/Summary.tsx index a35a8e5..ef74e77 100644 --- a/frontend/src/pages/Summary.tsx +++ b/frontend/src/pages/Summary.tsx @@ -19,6 +19,7 @@ export default function Summary() { const [error, setError] = useState(null) const [loading, setLoading] = useState(false) const [occupiedSiteIds, setOccupiedSiteIds] = useState>(new Set()) + const [arrivingSiteIds, setArrivingSiteIds] = useState>(new Set()) const [statusFilter, setStatusFilter] = useState(null) const [categoryFilter, setCategoryFilter] = useState(null) @@ -45,6 +46,7 @@ export default function Summary() { .then(([t, occ]) => { setTasks(t) setOccupiedSiteIds(occ ? new Set(occ.occupied_site_ids) : new Set()) + setArrivingSiteIds(occ ? new Set(occ.arriving_site_ids) : new Set()) setError(null) }) .catch(err => setError(err.message)) @@ -69,11 +71,17 @@ export default function Summary() { return c }, [tasks]) - // Count of rooms tasks with no in-house guest right now - const freeRoomsCount = useMemo(() => { + // Rooms chip counts: occupied = guest in room, arriving = booked today not yet in + const roomsCounts = useMemo(() => { if (!roomsView) return null - return tasks.filter(t => !t.newbook_site_id || !occupiedSiteIds.has(String(t.newbook_site_id))).length - }, [tasks, roomsView, occupiedSiteIds]) + let occupied = 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 && arrivingSiteIds.has(sid)) arriving++ + } + return { total: tasks.length, occupied, arriving, free: tasks.length - occupied - arriving } + }, [tasks, roomsView, occupiedSiteIds, arrivingSiteIds]) const roomsCategoryExists = !!roomsCat @@ -126,8 +134,8 @@ export default function Summary() { onClick={toggleRoomsView} > - {roomsView - ? `Rooms (${tasks.length}${freeRoomsCount !== null ? ` • ${freeRoomsCount} free` : ''})` + {roomsView && roomsCounts + ? `Rooms (${roomsCounts.free} free${roomsCounts.arriving ? ` • ${roomsCounts.arriving} arriving` : ''}${roomsCounts.occupied ? ` • ${roomsCounts.occupied} occupied` : ''})` : 'Rooms Accessible View'} )} @@ -138,7 +146,9 @@ export default function Summary() { )} {tasks.map(t => { - const isOccupied = roomsView && !!t.newbook_site_id && occupiedSiteIds.has(String(t.newbook_site_id)) + 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 (
{t.title} {isOccupied && Occupied} + {isArriving && Arrival today} {t.unusable && } {t.template_id && Recurring}