- debug-room now returns full raw NewBook booking objects so all field names are visible for diagnosis - guest_name extraction also tries guests[0].firstname+lastname (original PHP plugin pattern), not just guests[0].guest_name Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
260 lines
9.9 KiB
JavaScript
260 lines
9.9 KiB
JavaScript
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=<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,
|
|
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),
|
|
}
|
|
})
|
|
}
|