feat: add room-planner app — 3-day HK room view with NewBook integration
NewBook-connected daily housekeeping planner. Replaces the hotelhubmodule-housekeeping-dailylist WordPress plugin. LXC 120 · 10.10.10.120:3080 · slug: room-planner. - 3-day booking window (yesterday/today/tomorrow) fetched live from NewBook - Task completion ticks back to NewBook; room status patches NewBook directly - 23px border sliver CSS system for adjacent-day booking status - 3-state filter cycling (off→inclusive→exclusive) for categories and flow types - Stat filters for outstanding tasks and clean/dirty status - Rolling 48h activity log with checkout/checkin/status/tasks events - newbook_pings event bus for future NewBook poller integration - Room modal with permission-gated guest/rate/notes, task checkboxes, status buttons - Placeholder sections for future linen-count and routine-tasks modules - Settings page: task type colours, twin/extra-bed detection, category exclusions - Mobile-first layout (sidebar desktop, compact top bar mobile) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
1e658b6a48
39 changed files with 3765 additions and 0 deletions
124
backend/src/lib/booking-flow.js
Normal file
124
backend/src/lib/booking-flow.js
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
// 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
|
||||
}
|
||||
|
||||
// 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 (String(b.site_id) !== String(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 =>
|
||||
String(b.site_id) === String(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)
|
||||
|
||||
// Adjacent day primary occupants (for border slivers)
|
||||
const prevBookings = bookingsForSiteOnDate(allBookings, siteId, yesterday)
|
||||
const nextBookings = bookingsForSiteOnDate(allBookings, siteId, tomorrow)
|
||||
|
||||
const prevBooking = prevBookings[0] ?? null
|
||||
const nextBooking = nextBookings[0] ?? null
|
||||
|
||||
// Determine flow type
|
||||
let flowType = 'vacant'
|
||||
let primaryBooking = todayBookings[0] ?? null
|
||||
|
||||
if (primaryBooking) {
|
||||
const status = (primaryBooking.booking_status || '').toLowerCase()
|
||||
if (status === 'blocked') {
|
||||
flowType = 'blocked'
|
||||
} else {
|
||||
const arrivalDate = toDateStr(primaryBooking.booking_arrival)
|
||||
const departureDate = toDateStr(primaryBooking.booking_departure)
|
||||
const arrivingToday = arrivalDate === viewDate
|
||||
const departingToday = departureDate === viewDate
|
||||
|
||||
if (arrivingToday && departing && String(departing.booking_id) !== String(primaryBooking.booking_id)) {
|
||||
// A different booking departs same day this one arrives
|
||||
flowType = 'back-to-back'
|
||||
} else if (arrivingToday) {
|
||||
flowType = 'arrive'
|
||||
} else if (departingToday) {
|
||||
// Booking departs today but arrival <= viewDate so they were here — shouldn't normally hit
|
||||
flowType = 'depart'
|
||||
} else {
|
||||
flowType = 'stopover'
|
||||
}
|
||||
}
|
||||
} else if (departing) {
|
||||
// Room had a departing guest but no incoming booking occupies it today
|
||||
primaryBooking = departing
|
||||
flowType = 'depart'
|
||||
}
|
||||
|
||||
const arrivalDate = primaryBooking ? toDateStr(primaryBooking.booking_arrival) : null
|
||||
const departureDate = primaryBooking ? toDateStr(primaryBooking.booking_departure) : null
|
||||
|
||||
return {
|
||||
site_id: siteId,
|
||||
site_name: site.site_name,
|
||||
site_status: site.site_status || 'unknown',
|
||||
category_id: String(site.site_category_id || ''),
|
||||
category_name: site.site_category_name || '',
|
||||
category_order: site.site_category_order ?? 0,
|
||||
site_order: site.site_order ?? 0,
|
||||
|
||||
flow_type: flowType,
|
||||
booking: primaryBooking,
|
||||
|
||||
// Span flags — does this booking continue across the day boundary?
|
||||
spans_previous: !!primaryBooking && !!arrivalDate && arrivalDate < viewDate,
|
||||
spans_next: !!primaryBooking && !!departureDate && departureDate > viewDate,
|
||||
|
||||
// Adjacent day context for border slivers
|
||||
previous_booking: prevBooking,
|
||||
next_booking: nextBooking,
|
||||
previous_status: prevBooking ? (prevBooking.booking_status || '').toLowerCase() : null,
|
||||
next_status: nextBooking ? (nextBooking.booking_status || '').toLowerCase() : null,
|
||||
|
||||
// Departure time info for the wider-border badge
|
||||
departing_booking: departing,
|
||||
departing_time: departing ? toTimeStr(departing.booking_departure) : null,
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue