Fix: a booking arriving in a few days showed 'booked' instead of 'vacant'

resolveGoverningBooking() was letting an upcoming (not-today) booking govern colour,
so any room with something booked within the 7-day lookahead showed green 'Booked'
regardless of how far off the arrival was. 'booked' should only apply when someone's
actually arriving tonight.

Now a booking only governs state/colour when it covers today (arriving today,
mid-stay, or departing today) — otherwise the room is genuinely vacant. The soonest
upcoming booking (if any) is surfaced separately as vacant_nights, a countdown to
the next arrival, purely informational and no longer tied to colour. Dashboard shows
'Vacant N nights' when something's booked within the window, or 'Vacant 7+ nights'
when nothing was found in the fetched window at all.

New zone_state.vacant_nights column, threaded through saveZoneState/pollOnce/status.js.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-08-25 11:10:35 +00:00
parent 915fc0039c
commit dc8c9df719
5 changed files with 60 additions and 31 deletions

View file

@ -7,18 +7,34 @@ const AC_DEVICE_TYPES = ['mhi_modbus', 'mhi_mqtt', 'midea', 'daikin', 'home_assi
// Departure = takeoff, arrival = landing, stayover = bed (lucide has no "person in
// bed" icon, so BedDouble is the closest stand-in). A departure also shows a second
// landing icon when has_next_booking — same-day-turnover hint (someone's due in later).
// A genuinely vacant room (no icon, since nothing's arriving today) instead gets a
// plain night-count-until-next-arrival, or "7+ nights" when nothing's booked at all
// within the scheduler's lookahead window.
function BookingIcons({ zone }: { zone: ZoneStatus }) {
if (!zone.booking_kind) return null
const iconProps = { size: 13, strokeWidth: 1.75 }
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 2, marginLeft: 6, fontSize: 11.5, fontWeight: 600, color: 'var(--text-mid)' }}>
{zone.booking_kind === 'departure' && <PlaneTakeoff {...iconProps} />}
{zone.booking_kind === 'arrival' && <PlaneLanding {...iconProps} />}
{zone.booking_kind === 'stayover' && <BedDouble {...iconProps} />}
{zone.booking_kind === 'departure' && zone.has_next_booking && <PlaneLanding {...iconProps} />}
{zone.total_nights != null && <span>{zone.current_night}/{zone.total_nights}</span>}
</span>
)
const textStyle = { display: 'inline-flex', alignItems: 'center', gap: 2, marginLeft: 6, fontSize: 11.5, fontWeight: 600, color: 'var(--text-mid)' } as const
if (zone.booking_kind) {
const iconProps = { size: 13, strokeWidth: 1.75 }
return (
<span style={textStyle}>
{zone.booking_kind === 'departure' && <PlaneTakeoff {...iconProps} />}
{zone.booking_kind === 'arrival' && <PlaneLanding {...iconProps} />}
{zone.booking_kind === 'stayover' && <BedDouble {...iconProps} />}
{zone.booking_kind === 'departure' && zone.has_next_booking && <PlaneLanding {...iconProps} />}
{zone.total_nights != null && <span>{zone.current_night}/{zone.total_nights}</span>}
</span>
)
}
if (zone.room_state === 'vacant' && zone.zone_type === 'room') {
return (
<span style={textStyle}>
Vacant {zone.vacant_nights != null ? `${zone.vacant_nights} night${zone.vacant_nights === 1 ? '' : 's'}` : '7+ nights'}
</span>
)
}
return null
}
const MODE_ICONS: Record<string, typeof Snowflake> = {