Room planner: fix spans_next, night counter, and B2B booking priority
spans_next was using departure > viewDate (today), which made any 1-night booking extend the card to the right. Fixed to departure > tomorrow so the card only extends when it's also active tomorrow. Night counter used Date.parse() on NewBook datetime strings, which browsers parse as local time and shift off by hours in BST. Replaced with utcDay() which slices the YYYY-MM-DD prefix and calls Date.UTC() — always correct. When multiple bookings overlap today, prefer the one arriving today so an arriving guest takes priority over a stale stopover. Also treat a co-active non-arriving booking as the B2B departing context. Force previous-day bracket to show as "departed" (purple) when that booking departs today, without waiting for NewBook to update its status field. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
663ef5d107
commit
b66b3a7854
2 changed files with 62 additions and 40 deletions
|
|
@ -51,80 +51,95 @@ export function classifyRoom(site, allBookings, viewDate, yesterday, tomorrow) {
|
||||||
|
|
||||||
// Primary: booking that occupies the room on viewDate (arrival <= viewDate < departure)
|
// Primary: booking that occupies the room on viewDate (arrival <= viewDate < departure)
|
||||||
const todayBookings = bookingsForSiteOnDate(allBookings, siteId, viewDate)
|
const todayBookings = bookingsForSiteOnDate(allBookings, siteId, viewDate)
|
||||||
const departing = departingBookingForSite(allBookings, siteId, viewDate)
|
const departing = departingBookingForSite(allBookings, siteId, viewDate)
|
||||||
|
|
||||||
// Adjacent day primary occupants (for border slivers)
|
// Prefer bookings arriving TODAY over stopovers — handles the case where NewBook
|
||||||
const prevBookings = bookingsForSiteOnDate(allBookings, siteId, yesterday)
|
// still shows a previous stay as active when a new booking has already arrived.
|
||||||
const nextBookings = bookingsForSiteOnDate(allBookings, siteId, tomorrow)
|
const todayArriving = todayBookings.filter(b => toDateStr(b.booking_arrival) === viewDate)
|
||||||
|
let primaryBooking = todayArriving[0] ?? todayBookings[0] ?? null
|
||||||
|
|
||||||
const prevBooking = prevBookings[0] ?? null
|
// If an arriving booking is primary AND there's also a non-arriving booking in
|
||||||
const nextBooking = nextBookings[0] ?? null
|
// 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
|
// Determine flow type
|
||||||
let flowType = 'vacant'
|
let flowType = 'vacant'
|
||||||
let primaryBooking = todayBookings[0] ?? null
|
|
||||||
|
|
||||||
if (primaryBooking) {
|
if (primaryBooking) {
|
||||||
const status = (primaryBooking.booking_status || '').toLowerCase()
|
const status = (primaryBooking.booking_status || '').toLowerCase()
|
||||||
if (status === 'blocked') {
|
if (status === 'blocked') {
|
||||||
flowType = 'blocked'
|
flowType = 'blocked'
|
||||||
} else {
|
} else {
|
||||||
const arrivalDate = toDateStr(primaryBooking.booking_arrival)
|
const arrDate = toDateStr(primaryBooking.booking_arrival)
|
||||||
const departureDate = toDateStr(primaryBooking.booking_departure)
|
const deptDate = toDateStr(primaryBooking.booking_departure)
|
||||||
const arrivingToday = arrivalDate === viewDate
|
const arrivingToday = arrDate === viewDate
|
||||||
const departingToday = departureDate === viewDate
|
const departingToday = deptDate === viewDate
|
||||||
|
|
||||||
if (arrivingToday && departing && String(departing.booking_id) !== String(primaryBooking.booking_id)) {
|
if (arrivingToday && effectiveDeparting &&
|
||||||
// A different booking departs same day this one arrives
|
String(effectiveDeparting.booking_id) !== String(primaryBooking.booking_id)) {
|
||||||
flowType = 'back-to-back'
|
flowType = 'back-to-back'
|
||||||
} else if (arrivingToday) {
|
} else if (arrivingToday) {
|
||||||
flowType = 'arrive'
|
flowType = 'arrive'
|
||||||
} else if (departingToday) {
|
} else if (departingToday) {
|
||||||
// Booking departs today but arrival <= viewDate so they were here — shouldn't normally hit
|
|
||||||
flowType = 'depart'
|
flowType = 'depart'
|
||||||
} else {
|
} else {
|
||||||
flowType = 'stopover'
|
flowType = 'stopover'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (departing) {
|
} else if (effectiveDeparting) {
|
||||||
// Room had a departing guest but no incoming booking occupies it today
|
primaryBooking = effectiveDeparting
|
||||||
primaryBooking = departing
|
|
||||||
flowType = 'depart'
|
flowType = 'depart'
|
||||||
}
|
}
|
||||||
|
|
||||||
const arrivalDate = primaryBooking ? toDateStr(primaryBooking.booking_arrival) : null
|
const arrivalDate = primaryBooking ? toDateStr(primaryBooking.booking_arrival) : null
|
||||||
const departureDate = primaryBooking ? toDateStr(primaryBooking.booking_departure) : null
|
const departureDate = primaryBooking ? toDateStr(primaryBooking.booking_departure) : null
|
||||||
|
|
||||||
// NewBook may return category info as flat fields (site_category_id, category_id)
|
// spans_next: booking is ALSO active tomorrow (departure strictly after tomorrow).
|
||||||
// or as a nested object. Resolve whichever is present.
|
// 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: if the previous booking departs TODAY, force "departed"
|
||||||
|
// colour regardless of whether NewBook has updated its status yet.
|
||||||
|
const rawPrevStatus = prevBooking ? (prevBooking.booking_status || '').toLowerCase() : null
|
||||||
|
const prevStatus = (rawPrevStatus && toDateStr(prevBooking?.booking_departure) === viewDate)
|
||||||
|
? 'departed'
|
||||||
|
: rawPrevStatus
|
||||||
|
|
||||||
|
// 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 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 catName = site.site_category_name ?? site.category_name ?? site.category?.name ?? ''
|
||||||
const catOrd = site.site_category_order ?? site.category_order ?? site.category?.order ?? 0
|
const catOrd = site.site_category_order ?? site.category_order ?? site.category?.order ?? 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
site_id: siteId,
|
site_id: siteId,
|
||||||
site_name: site.site_name,
|
site_name: site.site_name,
|
||||||
site_status: site.site_status || 'unknown',
|
site_status: site.site_status || 'unknown',
|
||||||
category_id: catId != null ? String(catId) : '',
|
category_id: catId != null ? String(catId) : '',
|
||||||
category_name: catName,
|
category_name: catName,
|
||||||
category_order: catOrd,
|
category_order: catOrd,
|
||||||
site_order: site.site_order ?? 0,
|
site_order: site.site_order ?? 0,
|
||||||
|
|
||||||
flow_type: flowType,
|
flow_type: flowType,
|
||||||
booking: primaryBooking,
|
booking: primaryBooking,
|
||||||
|
|
||||||
// Span flags — does this booking continue across the day boundary?
|
spans_previous: !!primaryBooking && !!arrivalDate && arrivalDate < viewDate,
|
||||||
spans_previous: !!primaryBooking && !!arrivalDate && arrivalDate < viewDate,
|
spans_next: spansNext,
|
||||||
spans_next: !!primaryBooking && !!departureDate && departureDate > viewDate,
|
|
||||||
|
|
||||||
// Adjacent day context for border slivers
|
|
||||||
previous_booking: prevBooking,
|
previous_booking: prevBooking,
|
||||||
next_booking: nextBooking,
|
next_booking: nextBooking,
|
||||||
previous_status: prevBooking ? (prevBooking.booking_status || '').toLowerCase() : null,
|
previous_status: prevStatus,
|
||||||
next_status: nextBooking ? (nextBooking.booking_status || '').toLowerCase() : null,
|
next_status: nextBooking ? (nextBooking.booking_status || '').toLowerCase() : null,
|
||||||
|
|
||||||
// Departure time info for the wider-border badge
|
departing_booking: effectiveDeparting,
|
||||||
departing_booking: departing,
|
departing_time: effectiveDeparting ? toTimeStr(effectiveDeparting.booking_departure) : null,
|
||||||
departing_time: departing ? toTimeStr(departing.booking_departure) : null,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,17 @@ interface Props {
|
||||||
|
|
||||||
const I = { strokeWidth: 1.75 }
|
const I = { strokeWidth: 1.75 }
|
||||||
|
|
||||||
|
// Parse any date(-time) string as a UTC midnight value, ignoring any time component.
|
||||||
|
// Date.parse on "YYYY-MM-DD HH:MM:SS" is timezone-ambiguous in browsers; this is not.
|
||||||
|
function utcDay(s: string): number {
|
||||||
|
const d = s.slice(0, 10)
|
||||||
|
return Date.UTC(+d.slice(0, 4), +d.slice(5, 7) - 1, +d.slice(8, 10))
|
||||||
|
}
|
||||||
|
|
||||||
function nightsInfo(arrival: string, departure: string, viewDate: string) {
|
function nightsInfo(arrival: string, departure: string, viewDate: string) {
|
||||||
const msDay = 86400000
|
const msDay = 86400000
|
||||||
const total = Math.round((Date.parse(departure) - Date.parse(arrival)) / msDay)
|
const total = Math.round((utcDay(departure) - utcDay(arrival)) / msDay)
|
||||||
const current = Math.round((Date.parse(viewDate) - Date.parse(arrival)) / msDay) + 1
|
const current = Math.round((utcDay(viewDate) - utcDay(arrival)) / msDay) + 1
|
||||||
return { current: Math.max(1, Math.min(current, total)), total: Math.max(1, total) }
|
return { current: Math.max(1, Math.min(current, total)), total: Math.max(1, total) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue