Widen booking fetch window, resolve governing booking per-night, add turnover icons/nights to dashboard

Backend:
- fetchBookings() window widened from today-only to yesterday..+7 days, so a
  departing booking (arrived any number of nights ago) and upcoming bookings are
  both in view — the previous today-only fetch couldn't see a departing guest's
  arrival date at all.
- New resolveGoverningBooking(): replaces the single-booking-per-site pick with a
  two-tier resolution — (1) any non-cancelled booking whose stay covers today,
  preferring one not yet marked 'departed' (so a departing guest keeps governing
  blue->purple right up until checkout is confirmed, even with a same-day arrival
  also active); (2) otherwise the soonest upcoming booking, purely for colour
  ('booked' ahead of its own prep window). Also computes booking_kind
  (arrival/departure/stayover), current/total night count, and a has_next_booking
  turnover hint (for departure days with another booking already lined up).
- determineRoomState() no longer forces 'vacant' for a future-dated arrival — that
  case is now handled by resolveGoverningBooking() deciding whether a booking is
  relevant to today at all; once picked as governing it always resolves to at
  least 'booked'.
- zone_state table: new booking_kind/current_night/total_nights/has_next_booking
  columns, persisted every poll alongside room_state and exposed via GET /api/status.

Frontend:
- ZoneCard: room name is now followed by a takeoff (departure) / landing (arrival)
  / bed (stayover) icon plus a current/total nights figure, sourced from the new
  status fields. A departure additionally shows a landing icon when has_next_booking,
  hinting at a same-day turnover before the room's actually marked departed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-08-25 10:21:16 +00:00
parent 99ec8b9747
commit 48ffebc08d
5 changed files with 138 additions and 37 deletions

View file

@ -9,7 +9,8 @@ export async function statusRoutes(app) {
app.get('/api/status', { preHandler: requireCap('view') }, async () => {
const hvacDefaults = await getConfig('hvac_defaults', DEFAULT_HVAC_TEMPLATE)
const { rows: zones } = await pool.query(`
SELECT z.*, zs.room_state, zs.last_transition_at, zs.last_booking_status
SELECT z.*, zs.room_state, zs.last_transition_at, zs.last_booking_status,
zs.booking_kind, zs.current_night, zs.total_nights, zs.has_next_booking
FROM zones z
LEFT JOIN zone_state zs ON zs.zone_id = z.id
WHERE z.active = TRUE
@ -34,6 +35,7 @@ export async function statusRoutes(app) {
zones: zones.map(z => ({
...z,
room_state: z.room_state || 'vacant',
has_next_booking: z.has_next_booking ?? false,
target_temp: ['heating_up', 'occupied'].includes(z.room_state)
? (z.occupied_temp ?? hvacDefaults.trv_occupied_temp)
: (z.vacant_temp ?? hvacDefaults.trv_vacant_temp),