// Shared booking-flow classification logic (server-side). // The same logic is duplicated in frontend/src/lib/booking-flow.ts for client use. export const STATUS_COLORS = { arrived: '#3b82f6', // blue confirmed: '#10b981', // green unconfirmed: '#f59e0b', // amber departed: '#a855f7', // purple cancelled: '#94a3b8', // slate blocked: '#6b7280', // grey } // Extract YYYY-MM-DD from a NewBook datetime string export function toDateStr(dt) { if (!dt) return null return String(dt).slice(0, 10) } // Extract HH:MM from a NewBook datetime string export function toTimeStr(dt) { if (!dt) return null const t = String(dt).slice(11, 16) return t || null } // Match a booking to a site ID. NewBook sometimes uses booking_site_id instead of site_id. function siteMatches(b, siteId) { const bid = String(b.site_id || b.booking_site_id || '') return bid === String(siteId) } // Find all bookings for a given site on a specific date. // A booking occupies a site on viewDate if: arrival <= viewDate < departure // (departure date is the check-out day, so the room is vacated on that morning) export function bookingsForSiteOnDate(bookings, siteId, viewDate) { return bookings.filter(b => { if (!siteMatches(b, siteId)) return false const arrival = toDateStr(b.booking_arrival) const departure = toDateStr(b.booking_departure) if (!arrival || !departure) return false return arrival <= viewDate && departure > viewDate }) } // Find the departing booking for a site on viewDate (departure_date === viewDate) export function departingBookingForSite(bookings, siteId, viewDate) { return bookings.find(b => siteMatches(b, siteId) && toDateStr(b.booking_departure) === viewDate ) ?? null } // Classify a site's booking state for a given view date. // Returns a rich descriptor used to build the room card. export function classifyRoom(site, allBookings, viewDate, yesterday, tomorrow) { const siteId = String(site.site_id) // Primary: booking that occupies the room on viewDate (arrival <= viewDate < departure) const todayBookings = bookingsForSiteOnDate(allBookings, siteId, viewDate) const departing = departingBookingForSite(allBookings, siteId, viewDate) // Prefer bookings arriving TODAY over stopovers — handles the case where NewBook // still shows a previous stay as active when a new booking has already arrived. const todayArriving = todayBookings.filter(b => toDateStr(b.booking_arrival) === viewDate) let primaryBooking = todayArriving[0] ?? todayBookings[0] ?? null // If an arriving booking is primary AND there's also a non-arriving booking in // todayBookings, treat that as the departing context (B2B where departure date // hasn't been updated in NewBook yet). let effectiveDeparting = departing if (!effectiveDeparting && todayArriving.length > 0 && todayBookings.length > todayArriving.length) { effectiveDeparting = todayBookings.find(b => toDateStr(b.booking_arrival) !== viewDate) ?? null } // Adjacent day primary occupants (for border brackets) const prevBooking = bookingsForSiteOnDate(allBookings, siteId, yesterday)[0] ?? null const nextBooking = bookingsForSiteOnDate(allBookings, siteId, tomorrow)[0] ?? null // Determine flow type let flowType = 'vacant' if (primaryBooking) { const status = (primaryBooking.booking_status || '').toLowerCase() if (status === 'blocked') { flowType = 'blocked' } else { const arrDate = toDateStr(primaryBooking.booking_arrival) const deptDate = toDateStr(primaryBooking.booking_departure) const arrivingToday = arrDate === viewDate const departingToday = deptDate === viewDate if (arrivingToday && effectiveDeparting && String(effectiveDeparting.booking_id) !== String(primaryBooking.booking_id)) { flowType = 'back-to-back' } else if (arrivingToday) { flowType = 'arrive' } else if (departingToday) { flowType = 'depart' } else { flowType = 'stopover' } } } else if (effectiveDeparting) { primaryBooking = effectiveDeparting flowType = 'depart' } const arrivalDate = primaryBooking ? toDateStr(primaryBooking.booking_arrival) : null const departureDate = primaryBooking ? toDateStr(primaryBooking.booking_departure) : null // spans_next: booking is ALSO active tomorrow (departure strictly after tomorrow). // Using > tomorrow (not > viewDate) so a booking departing tomorrow does NOT extend // the card right — it ends tonight and its right border should be visible. const spansNext = !!primaryBooking && !!departureDate && departureDate > tomorrow // Previous-day bracket: use real NewBook status so the sliver remains 'arrived' // (wide, with checkout time) until the guest actually checks out and NewBook // flips the status to 'departed'. Do not override — let real status drive colour. const prevStatus = prevBooking ? (prevBooking.booking_status || '').toLowerCase() : null // NewBook may return category info as flat fields or a nested object. const catId = site.site_category_id ?? site.category_id ?? site.category?.id ?? null const catName = site.site_category_name ?? site.category_name ?? site.category?.name ?? '' const catOrd = site.site_category_order ?? site.category_order ?? site.category?.order ?? 0 return { site_id: siteId, site_name: site.site_name, site_status: site.site_status || 'unknown', category_id: catId != null ? String(catId) : '', category_name: catName, category_order: catOrd, site_order: site.site_order ?? 0, flow_type: flowType, booking: primaryBooking, spans_previous: !!primaryBooking && !!arrivalDate && arrivalDate < viewDate, spans_next: spansNext, previous_booking: prevBooking, next_booking: nextBooking, previous_status: prevStatus, next_status: nextBooking ? (nextBooking.booking_status || '').toLowerCase() : null, departing_booking: effectiveDeparting, departing_time: effectiveDeparting ? toTimeStr(effectiveDeparting.booking_departure) : null, } }