From 0d14473dafa219a089c9edac4c5c6717438fa946 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Sat, 4 Jul 2026 11:54:32 +0000 Subject: [PATCH 1/3] Room planner: add debug-room endpoint, fix next_status null check Co-Authored-By: Claude Sonnet 4.6 --- backend/src/routes/rooms.js | 77 ++++++++++++++++++++++++++++ frontend/src/components/RoomCard.tsx | 4 +- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/backend/src/routes/rooms.js b/backend/src/routes/rooms.js index f10b541..1477329 100644 --- a/backend/src/routes/rooms.js +++ b/backend/src/routes/rooms.js @@ -47,6 +47,83 @@ function filterBookingData(booking, canSeeGuest, canSeeRate, canSeeAllNotes, vis export async function roomRoutes(app) { app.addHook('preHandler', requireAuth) + // GET /api/rooms/debug-room?room=101&date=YYYY-MM-DD + // Returns raw NewBook data for a single room — use to diagnose bracket/booking issues. + app.get('/api/rooms/debug-room', async (req, reply) => { + if (!hasCap(req, 'settings')) return reply.status(403).send({ error: 'Forbidden' }) + + const viewDate = req.query.date || new Date().toISOString().slice(0, 10) + const roomName = req.query.room + const yesterday = dateOffset(viewDate, -1) + const tomorrow = dateOffset(viewDate, +1) + const dayAfterTomorrow = dateOffset(viewDate, +2) + + const [sites, bookings] = await Promise.all([ + fetchSites(), + fetchBookings(yesterday, dayAfterTomorrow), + ]) + + if (!roomName) { + const sample = bookings[0] || {} + return { + hint: 'Pass ?room= to inspect a specific room', + view_date: viewDate, + total_sites: sites.length, + total_bookings: bookings.length, + booking_field_names: Object.keys(sample), + site_field_names: sites.length ? Object.keys(sites[0]) : [], + site_list: sites.map(s => ({ id: s.site_id, name: s.site_name })), + } + } + + const site = sites.find(s => s.site_name === roomName) + if (!site) return { error: `Room "${roomName}" not found`, available: sites.map(s => s.site_name) } + + const siteId = String(site.site_id) + const idFields = bookings.length + ? Object.keys(bookings[0]).filter(k => + k.toLowerCase().includes('site') || k.toLowerCase().includes('room') || + k.toLowerCase().includes('unit') || k.toLowerCase().includes('location') + ) + : [] + + const matchFn = b => + idFields.some(f => String(b[f] || '') === siteId) || + String(b.site_id || '') === siteId || + String(b.booking_site_id || '') === siteId || + (b.site_name && b.site_name === roomName) + + const siteBookings = bookings.filter(matchFn) + + const classified = classifyRoom(site, bookings, viewDate, yesterday, tomorrow) + + return { + view_date: viewDate, yesterday, tomorrow, day_after_tomorrow: dayAfterTomorrow, + site: { id: siteId, name: site.site_name, fields: Object.keys(site) }, + booking_id_fields: idFields, + total_bookings_fetched: bookings.length, + matching_bookings: siteBookings.map(b => { + const out = {} + for (const f of ['booking_id', 'booking_status', 'booking_arrival', 'booking_departure', ...idFields]) { + out[f] = b[f] + } + return out + }), + classified: { + flow_type: classified.flow_type, + spans_previous: classified.spans_previous, + spans_next: classified.spans_next, + previous_status: classified.previous_status, + next_status: classified.next_status, + booking_id: classified.booking?.booking_id, + booking_arrival: classified.booking?.booking_arrival, + booking_departure: classified.booking?.booking_departure, + next_booking_id: classified.next_booking?.booking_id, + next_booking_arrival: classified.next_booking?.booking_arrival, + }, + } + }) + // GET /api/rooms?date=YYYY-MM-DD app.get('/api/rooms', async (req, reply) => { if (!hasCap(req, 'view')) return reply.status(403).send({ error: 'Missing capability: view' }) diff --git a/frontend/src/components/RoomCard.tsx b/frontend/src/components/RoomCard.tsx index f5a6008..74acb71 100644 --- a/frontend/src/components/RoomCard.tsx +++ b/frontend/src/components/RoomCard.tsx @@ -104,8 +104,8 @@ function RoomCard({ room, viewDate, config, onClick }: Props) { } if (room.spans_next) { dataAttrs['data-spans-next'] = 'true' - } else if (room.next_status) { - dataAttrs['data-next-status'] = room.next_status.toLowerCase() + } else if (room.next_status != null) { + dataAttrs['data-next-status'] = room.next_status || 'unknown' } // Task status pill From 3c4bdd9a1c246222b0a9c6cfa4299bbdd4942381 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Sat, 4 Jul 2026 12:09:16 +0000 Subject: [PATCH 2/3] Room planner: fix category exclusion field name (category_id not site_category_id) NewBook API returns category_id on site objects; exclusion check was using the non-existent site_category_id field so excluded categories config never worked. Co-Authored-By: Claude Sonnet 4.6 --- backend/src/routes/rooms.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/routes/rooms.js b/backend/src/routes/rooms.js index 1477329..a2db666 100644 --- a/backend/src/routes/rooms.js +++ b/backend/src/routes/rooms.js @@ -180,7 +180,7 @@ export async function roomRoutes(app) { // Classify each site const rooms = [] for (const site of sites) { - const catId = String(site.site_category_id || '') + const catId = String(site.site_category_id ?? site.category_id ?? site.category?.id ?? '') const isExcluded = excludedCategories.includes(catId) if (isExcluded && hideExcluded) continue From 89ad3811da36b5ce9a85aa8dbc63f6faa414b471 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Sat, 4 Jul 2026 12:18:26 +0000 Subject: [PATCH 3/3] Room planner: fix bookings_list to use list_type=staying MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original WordPress plugin (class-hhdl-ajax.php line 409) uses list_type='staying', which returns bookings whose stay *overlaps* the date window (arrival <= period_to AND departure >= period_from). Using list_type='all' was returning bookings filtered by booking-placed date (or a flat dump), completely missing tomorrow-arriving bookings that are needed for right-bracket display. Switching to 'staying' with the original yesterday→tomorrow window gives 50 bookings vs 89 before, and correctly includes adjacent-day arrivals. Verified on dev: room 101 now shows confirmed right-bracket for July 5 arrival, room 102 now correctly classifies as back-to-back. Co-Authored-By: Claude Sonnet 4.6 --- backend/src/lib/newbook.js | 7 ++++--- backend/src/routes/rooms.js | 16 +++++++--------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/backend/src/lib/newbook.js b/backend/src/lib/newbook.js index 7f7cb06..7dabbeb 100644 --- a/backend/src/lib/newbook.js +++ b/backend/src/lib/newbook.js @@ -58,13 +58,14 @@ export async function fetchSites() { return res?.data ?? [] } -// Fetch bookings spanning a date range. -// list_type 'all' includes arrived, confirmed, unconfirmed, departed, blocked. +// Fetch bookings whose stay overlaps the date window. +// list_type 'staying' returns bookings where arrival <= period_to AND departure >= period_from, +// so a booking arriving on period_to is included. This matches the original plugin behaviour. export async function fetchBookings(fromDate, toDate) { const res = await callApi('bookings_list', { period_from: `${fromDate} 00:00:00`, period_to: `${toDate} 23:59:59`, - list_type: 'all', + list_type: 'staying', }) return res?.data ?? [] } diff --git a/backend/src/routes/rooms.js b/backend/src/routes/rooms.js index a2db666..e8efcfc 100644 --- a/backend/src/routes/rooms.js +++ b/backend/src/routes/rooms.js @@ -54,13 +54,12 @@ export async function roomRoutes(app) { const viewDate = req.query.date || new Date().toISOString().slice(0, 10) const roomName = req.query.room - const yesterday = dateOffset(viewDate, -1) - const tomorrow = dateOffset(viewDate, +1) - const dayAfterTomorrow = dateOffset(viewDate, +2) + const yesterday = dateOffset(viewDate, -1) + const tomorrow = dateOffset(viewDate, +1) const [sites, bookings] = await Promise.all([ fetchSites(), - fetchBookings(yesterday, dayAfterTomorrow), + fetchBookings(yesterday, tomorrow), ]) if (!roomName) { @@ -98,7 +97,7 @@ export async function roomRoutes(app) { const classified = classifyRoom(site, bookings, viewDate, yesterday, tomorrow) return { - view_date: viewDate, yesterday, tomorrow, day_after_tomorrow: dayAfterTomorrow, + view_date: viewDate, yesterday, tomorrow, site: { id: siteId, name: site.site_name, fields: Object.keys(site) }, booking_id_fields: idFields, total_bookings_fetched: bookings.length, @@ -151,12 +150,11 @@ export async function roomRoutes(app) { // Fetch from NewBook in parallel let sites, bookings, tasks try { - // Fetch bookings one day beyond tomorrow so tomorrow-arriving bookings appear - // in nextBooking lookups (some NewBook regions exclude period_to-date arrivals). - const dayAfterTomorrow = dateOffset(viewDate, +2) + // list_type 'staying' includes bookings with arrival <= period_to, + // so fetching yesterday→tomorrow captures all adjacent-day brackets. ;[sites, bookings, tasks] = await Promise.all([ fetchSites(), - fetchBookings(yesterday, dayAfterTomorrow), + fetchBookings(yesterday, tomorrow), fetchTasks(yesterday, tomorrow), ]) } catch (err) {