room-planner/backend/src/lib/booking-flow.js
jtricerolph 9ef047900b Room planner: fix settings scroll, checkout time to left-bracket sliver
- Settings page: add overflow-y:auto + flex:1 so content scrolls within
  page-content (which has overflow:hidden)

- Checkout time: move departure time out of B2B card stats row and into
  the left-bracket sliver, matching original plugin behaviour:
  - Only shown while prev booking status is still 'arrived' (not yet
    checked out); collapses to normal bracket once NewBook flips to
    'departed'
  - Positioned absolutely over the ::before left bracket area
  - Remove the forced 'departed' override on prev_status — let NewBook's
    real status drive bracket colour so the sliver shows correctly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-04 12:30:27 +00:00

149 lines
6.1 KiB
JavaScript

// 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,
}
}