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 f10b541..e8efcfc 100644 --- a/backend/src/routes/rooms.js +++ b/backend/src/routes/rooms.js @@ -47,6 +47,82 @@ 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 [sites, bookings] = await Promise.all([ + fetchSites(), + fetchBookings(yesterday, tomorrow), + ]) + + 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, + 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' }) @@ -74,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) { @@ -103,7 +178,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 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