Compare commits

..

3 commits

Author SHA1 Message Date
89ad3811da Room planner: fix bookings_list to use list_type=staying
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 <noreply@anthropic.com>
2026-07-04 12:18:26 +00:00
3c4bdd9a1c 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 <noreply@anthropic.com>
2026-07-04 12:09:16 +00:00
0d14473daf Room planner: add debug-room endpoint, fix next_status null check
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-04 11:54:32 +00:00
3 changed files with 86 additions and 10 deletions

View file

@ -58,13 +58,14 @@ export async function fetchSites() {
return res?.data ?? [] return res?.data ?? []
} }
// Fetch bookings spanning a date range. // Fetch bookings whose stay overlaps the date window.
// list_type 'all' includes arrived, confirmed, unconfirmed, departed, blocked. // 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) { export async function fetchBookings(fromDate, toDate) {
const res = await callApi('bookings_list', { const res = await callApi('bookings_list', {
period_from: `${fromDate} 00:00:00`, period_from: `${fromDate} 00:00:00`,
period_to: `${toDate} 23:59:59`, period_to: `${toDate} 23:59:59`,
list_type: 'all', list_type: 'staying',
}) })
return res?.data ?? [] return res?.data ?? []
} }

View file

@ -47,6 +47,82 @@ function filterBookingData(booking, canSeeGuest, canSeeRate, canSeeAllNotes, vis
export async function roomRoutes(app) { export async function roomRoutes(app) {
app.addHook('preHandler', requireAuth) 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=<site_name> 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 // GET /api/rooms?date=YYYY-MM-DD
app.get('/api/rooms', async (req, reply) => { app.get('/api/rooms', async (req, reply) => {
if (!hasCap(req, 'view')) return reply.status(403).send({ error: 'Missing capability: view' }) 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 // Fetch from NewBook in parallel
let sites, bookings, tasks let sites, bookings, tasks
try { try {
// Fetch bookings one day beyond tomorrow so tomorrow-arriving bookings appear // list_type 'staying' includes bookings with arrival <= period_to,
// in nextBooking lookups (some NewBook regions exclude period_to-date arrivals). // so fetching yesterday→tomorrow captures all adjacent-day brackets.
const dayAfterTomorrow = dateOffset(viewDate, +2)
;[sites, bookings, tasks] = await Promise.all([ ;[sites, bookings, tasks] = await Promise.all([
fetchSites(), fetchSites(),
fetchBookings(yesterday, dayAfterTomorrow), fetchBookings(yesterday, tomorrow),
fetchTasks(yesterday, tomorrow), fetchTasks(yesterday, tomorrow),
]) ])
} catch (err) { } catch (err) {
@ -103,7 +178,7 @@ export async function roomRoutes(app) {
// Classify each site // Classify each site
const rooms = [] const rooms = []
for (const site of sites) { 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) const isExcluded = excludedCategories.includes(catId)
if (isExcluded && hideExcluded) continue if (isExcluded && hideExcluded) continue

View file

@ -104,8 +104,8 @@ function RoomCard({ room, viewDate, config, onClick }: Props) {
} }
if (room.spans_next) { if (room.spans_next) {
dataAttrs['data-spans-next'] = 'true' dataAttrs['data-spans-next'] = 'true'
} else if (room.next_status) { } else if (room.next_status != null) {
dataAttrs['data-next-status'] = room.next_status.toLowerCase() dataAttrs['data-next-status'] = room.next_status || 'unknown'
} }
// Task status pill // Task status pill