import { requireAuth, hasCap } from '../auth.js' import { pool } from '../db.js' import { fetchSites, fetchBookings, fetchTasks } from '../lib/newbook.js' import { classifyRoom, toDateStr } from '../lib/booking-flow.js' // Fetch the newbook.rooms config from the settings service (different DB). // Returns the parsed value object, or null if unavailable (settings not deployed, not synced yet, etc.). async function fetchSettingsRoomsConfig() { const url = process.env.SETTINGS_URL const secret = process.env.SETTINGS_SECRET if (!url || !secret) return null try { const res = await fetch(`${url}/settings/api/internal/global-config/newbook.rooms`, { headers: { Authorization: `Bearer ${secret}` }, }) if (!res.ok) return null const body = await res.json() return body.value ?? null } catch { return null } } function dateOffset(dateStr, days) { const d = new Date(dateStr + 'T00:00:00Z') d.setUTCDate(d.getUTCDate() + days) return d.toISOString().slice(0, 10) } function filterBookingData(booking, canSeeGuest, canSeeRate, canSeeAllNotes, visibleNoteTypes) { if (!booking) return null const out = { booking_id: booking.booking_id, booking_reference_id: booking.booking_reference_id, booking_status: booking.booking_status, booking_arrival: booking.booking_arrival, booking_departure: booking.booking_departure, booking_eta: booking.booking_eta, booking_locked: booking.booking_locked === true || booking.booking_locked === 1 || booking.booking_locked === '1', pax: booking.pax, site_id: booking.site_id, custom_fields: booking.custom_fields || [], } if (canSeeGuest) { const guests = booking.guests || [] const g0 = guests[0] const fromGuests = g0 ? (g0.guest_name || [g0.firstname, g0.lastname].filter(Boolean).join(' ') || null) : null out.guest_name = fromGuests || booking.guest_name || booking.account_for_name || null } if (canSeeRate) { out.rate_plan_name = booking.rate_plan_name || null } if (canSeeAllNotes) { out.notes = booking.notes || [] } else if (visibleNoteTypes?.length) { out.notes = (booking.notes || []).filter(n => visibleNoteTypes.includes(String(n.note_type_id))) } else { out.notes = [] } return out } 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, 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' }) const viewDate = req.query.date || new Date().toISOString().slice(0, 10) const yesterday = dateOffset(viewDate, -1) const tomorrow = dateOffset(viewDate, +1) const canSeeGuest = hasCap(req, 'guest_details') const canSeeRate = hasCap(req, 'rate_details') const canSeeAllNotes = hasCap(req, 'view_all_notes') // Load local config + settings category order in parallel const [cfgRow, excRow, settingsRooms] = await Promise.all([ pool.query(`SELECT value FROM config WHERE key = 'visible_note_types'`), pool.query(`SELECT key, value FROM config WHERE key IN ('excluded_categories','hide_excluded_categories')`), fetchSettingsRoomsConfig(), ]) const visibleNoteTypes = cfgRow.rows[0]?.value || [] const cfgMap = Object.fromEntries(excRow.rows.map(r => [r.key, r.value])) const excludedCategories = cfgMap.excluded_categories || [] const hideExcluded = cfgMap.hide_excluded_categories ?? false // Build category sort-order map from settings (admin-configured) — keyed by category id const settingsCatOrder = {} for (const cat of settingsRooms?.categories ?? []) { settingsCatOrder[String(cat.id)] = cat.sort_order ?? 999 } // Fetch from NewBook in parallel let sites, bookings, tasks try { // 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, tomorrow), fetchTasks(yesterday, tomorrow), ]) } catch (err) { return reply.status(502).send({ error: `NewBook fetch failed: ${err.message}` }) } const categoryMap = {} for (const site of sites) { const catId = String( site.site_category_id ?? site.category_id ?? site.category?.id ?? '' ) if (catId && !categoryMap[catId]) { const nbOrder = site.site_category_order ?? site.category_order ?? site.category?.order ?? 999 categoryMap[catId] = { id: catId, name: site.site_category_name ?? site.category_name ?? site.category?.name ?? catId, order: settingsCatOrder[catId] ?? nbOrder, } } } // Classify each site const rooms = [] for (const site of sites) { const catId = String(site.site_category_id ?? site.category_id ?? site.category?.id ?? '') const isExcluded = excludedCategories.includes(catId) if (isExcluded && hideExcluded) continue const classified = classifyRoom(site, bookings, viewDate, yesterday, tomorrow) // Override category_order with settings value if available const catOrderOverride = settingsCatOrder[catId] if (catOrderOverride !== undefined) classified.category_order = catOrderOverride // Filter booking data per capabilities if (classified.booking) { classified.booking = filterBookingData( classified.booking, canSeeGuest, canSeeRate, canSeeAllNotes, visibleNoteTypes ) } if (classified.departing_booking && classified.departing_booking !== classified.booking) { classified.departing_booking = filterBookingData( classified.departing_booking, canSeeGuest, canSeeRate, canSeeAllNotes, visibleNoteTypes ) } classified.previous_booking = classified.previous_booking ? filterBookingData(classified.previous_booking, false, false, false, []) : null classified.next_booking = classified.next_booking ? filterBookingData(classified.next_booking, false, false, false, []) : null classified.filter_excluded = isExcluded // Attach tasks for this site across the 3-day window, keyed by date const siteId = String(site.site_id) classified.tasks = tasks .filter(t => String(t.booking_site_id || t.site_id) === siteId) .map(t => ({ task_id: t.task_id, task_description: t.task_description, task_type_id: String(t.task_type_id), task_when_date: t.task_when_date ? toDateStr(t.task_when_date) : null, task_period_from: t.task_period_from ? toDateStr(t.task_period_from) : null, task_period_to: t.task_period_to ? toDateStr(t.task_period_to) : null, site_id: siteId, booking_id: t.booking_id, completed_on: t.task_completed_on || null, })) rooms.push(classified) } return { view_date: viewDate, yesterday, tomorrow, rooms, categories: Object.values(categoryMap).sort((a, b) => a.order - b.order), } }) }