Room planner: semantic flow filters, forward arrow, padlock + guest name fixes

- Flow filter chips are now semantic: Departing = depart|B2B,
  Arriving = arrive|B2B, so B2B rooms appear in both chips. Adds
  missing Arriving filter.
- Forward → button added to date bar (right of datepicker)
- booking_locked normalised to real boolean — NewBook returns '0'/'1'
  strings which were all truthy, causing padlock on every room
- guest_name fallback: also try booking.guest_name (flat field) before
  account_for_name in case NewBook omits the guests[] array

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-04 13:07:55 +00:00
parent 1ab864f4e3
commit fbe913c01c
4 changed files with 45 additions and 24 deletions

View file

@ -18,7 +18,7 @@ function filterBookingData(booking, canSeeGuest, canSeeRate, canSeeAllNotes, vis
booking_arrival: booking.booking_arrival, booking_arrival: booking.booking_arrival,
booking_departure: booking.booking_departure, booking_departure: booking.booking_departure,
booking_eta: booking.booking_eta, booking_eta: booking.booking_eta,
booking_locked: booking.booking_locked, booking_locked: booking.booking_locked === true || booking.booking_locked === 1 || booking.booking_locked === '1',
pax: booking.pax, pax: booking.pax,
site_id: booking.site_id, site_id: booking.site_id,
custom_fields: booking.custom_fields || [], custom_fields: booking.custom_fields || [],
@ -26,7 +26,7 @@ function filterBookingData(booking, canSeeGuest, canSeeRate, canSeeAllNotes, vis
if (canSeeGuest) { if (canSeeGuest) {
const guests = booking.guests || [] const guests = booking.guests || []
out.guest_name = guests[0]?.guest_name || booking.account_for_name || null out.guest_name = guests[0]?.guest_name || booking.guest_name || booking.account_for_name || null
} }
if (canSeeRate) { if (canSeeRate) {

View file

@ -1,6 +1,7 @@
import { useRef } from 'react' import { useRef } from 'react'
import { ChevronLeft, ChevronRight } from 'lucide-react' import { ChevronLeft, ChevronRight } from 'lucide-react'
import type { FlowType, FilterMode, FilterState, StatFilters, StatFilterMode } from '../types' import type { FilterMode, FilterState, StatFilters, StatFilterMode, SemanticFlowKey } from '../types'
import { SEMANTIC_FLOW_MAP } from '../types'
import type { RoomData } from '../types' import type { RoomData } from '../types'
interface FilterBarProps { interface FilterBarProps {
@ -9,13 +10,13 @@ interface FilterBarProps {
rooms: RoomData[] rooms: RoomData[]
viewDate: string viewDate: string
onToggleCategory: (id: string) => void onToggleCategory: (id: string) => void
onToggleFlow: (flow: FlowType) => void onToggleFlow: (flow: SemanticFlowKey) => void
} }
const FLOW_TYPES: FlowType[] = ['arrive', 'depart', 'stopover', 'back-to-back', 'vacant', 'blocked'] const SEMANTIC_KEYS: SemanticFlowKey[] = ['arriving', 'departing', 'back-to-back', 'staying', 'vacant', 'blocked']
const FLOW_LABELS: Record<FlowType, string> = { const SEMANTIC_LABELS: Record<SemanticFlowKey, string> = {
arrive: 'Arriving', depart: 'Departing', stopover: 'Staying', arriving: 'Arriving', departing: 'Departing', 'back-to-back': 'B2B',
'back-to-back': 'B2B', vacant: 'Vacant', blocked: 'Blocked', staying: 'Staying', vacant: 'Vacant', blocked: 'Blocked',
} }
function nextMode(mode: FilterMode): FilterMode { function nextMode(mode: FilterMode): FilterMode {
@ -28,8 +29,9 @@ function countByCategory(rooms: RoomData[], catId: string) {
return rooms.filter(r => r.category_id === catId).length return rooms.filter(r => r.category_id === catId).length
} }
function countByFlow(rooms: RoomData[], flow: FlowType) { function countBySemantic(rooms: RoomData[], key: SemanticFlowKey) {
return rooms.filter(r => r.flow_type === flow).length const flowTypes = SEMANTIC_FLOW_MAP[key]
return rooms.filter(r => flowTypes.includes(r.flow_type)).length
} }
export function FilterBar({ filters, categories, rooms, viewDate, onToggleCategory, onToggleFlow }: FilterBarProps) { export function FilterBar({ filters, categories, rooms, viewDate, onToggleCategory, onToggleFlow }: FilterBarProps) {
@ -70,17 +72,17 @@ export function FilterBar({ filters, categories, rooms, viewDate, onToggleCatego
{/* Flow type filter row */} {/* Flow type filter row */}
<div className="filter-bar" style={{ borderTop: '1px solid var(--border)' }}> <div className="filter-bar" style={{ borderTop: '1px solid var(--border)' }}>
<div className="filter-bar-scroll"> <div className="filter-bar-scroll">
{FLOW_TYPES.map(flow => { {SEMANTIC_KEYS.map(key => {
const mode = filters.flowTypes[flow] ?? 'off' const mode = filters.flowTypes[key] ?? 'off'
const count = countByFlow(rooms, flow) const count = countBySemantic(rooms, key)
if (count === 0 && mode === 'off') return null if (count === 0 && mode === 'off') return null
return ( return (
<button <button
key={flow} key={key}
className={`filter-chip ${mode}`} className={`filter-chip ${mode}`}
onClick={() => onToggleFlow(flow)} onClick={() => onToggleFlow(key)}
> >
{FLOW_LABELS[flow]} {SEMANTIC_LABELS[key]}
<span className="chip-count">{count}</span> <span className="chip-count">{count}</span>
</button> </button>
) )

View file

@ -1,6 +1,7 @@
import { useState, useEffect, useCallback, useRef, useMemo } from 'react' import { useState, useEffect, useCallback, useRef, useMemo } from 'react'
import { RefreshCw } from 'lucide-react' import { RefreshCw } from 'lucide-react'
import type { RoomData, AppConfig, Category, FilterState, StatFilters, FlowType, ActivityEntry } from '../types' import type { RoomData, AppConfig, Category, FilterState, StatFilters, SemanticFlowKey, ActivityEntry } from '../types'
import { SEMANTIC_FLOW_MAP } from '../types'
import { useAuth } from '../components/AuthGate' import { useAuth } from '../components/AuthGate'
import CategoryGroup from '../components/CategoryGroup' import CategoryGroup from '../components/CategoryGroup'
import { FilterBar, StatFilterBar } from '../components/FilterBar' import { FilterBar, StatFilterBar } from '../components/FilterBar'
@ -138,7 +139,7 @@ export default function Planner() {
}) })
}, []) }, [])
const toggleFlow = useCallback((flow: FlowType) => { const toggleFlow = useCallback((flow: SemanticFlowKey) => {
setFilters(f => { setFilters(f => {
const cur = f.flowTypes[flow] ?? 'off' const cur = f.flowTypes[flow] ?? 'off'
const next = cur === 'off' ? 'inclusive' : cur === 'inclusive' ? 'exclusive' : 'off' const next = cur === 'off' ? 'inclusive' : cur === 'inclusive' ? 'exclusive' : 'off'
@ -158,17 +159,21 @@ export default function Planner() {
const visibleRooms = useMemo(() => { const visibleRooms = useMemo(() => {
const inclCats = Object.entries(filters.categories).filter(([, m]) => m === 'inclusive').map(([k]) => k) const inclCats = Object.entries(filters.categories).filter(([, m]) => m === 'inclusive').map(([k]) => k)
const exclCats = Object.entries(filters.categories).filter(([, m]) => m === 'exclusive').map(([k]) => k) const exclCats = Object.entries(filters.categories).filter(([, m]) => m === 'exclusive').map(([k]) => k)
const inclFlows = Object.entries(filters.flowTypes).filter(([, m]) => m === 'inclusive').map(([k]) => k) as FlowType[] const inclSemantic = Object.entries(filters.flowTypes).filter(([, m]) => m === 'inclusive').map(([k]) => k as SemanticFlowKey)
const exclFlows = Object.entries(filters.flowTypes).filter(([, m]) => m === 'exclusive').map(([k]) => k) as FlowType[] const exclSemantic = Object.entries(filters.flowTypes).filter(([, m]) => m === 'exclusive').map(([k]) => k as SemanticFlowKey)
// Expand semantic keys to the union of their real flow types
const inclFlowTypes = new Set(inclSemantic.flatMap(s => SEMANTIC_FLOW_MAP[s]))
const exclFlowTypes = new Set(exclSemantic.flatMap(s => SEMANTIC_FLOW_MAP[s]))
return rooms.filter(room => { return rooms.filter(room => {
// Category filters: if any inclusive → must be in inclusive set; exclusive → must not be in exclusive set // Category filters: if any inclusive → must be in inclusive set; exclusive → must not be in exclusive set
if (inclCats.length && !inclCats.includes(room.category_id)) return false if (inclCats.length && !inclCats.includes(room.category_id)) return false
if (exclCats.includes(room.category_id)) return false if (exclCats.includes(room.category_id)) return false
// Flow type filters // Flow filters: semantic — e.g. "Departing" matches both 'depart' and 'back-to-back'
if (inclFlows.length && !inclFlows.includes(room.flow_type)) return false if (inclFlowTypes.size && !inclFlowTypes.has(room.flow_type)) return false
if (exclFlows.includes(room.flow_type)) return false if (exclFlowTypes.has(room.flow_type)) return false
// Stat filters // Stat filters
const tasks = statFilters.newbookTasks const tasks = statFilters.newbookTasks
@ -240,6 +245,7 @@ export default function Planner() {
value={viewDate} value={viewDate}
onChange={e => setViewDate(e.target.value)} onChange={e => setViewDate(e.target.value)}
/> />
<button className="date-nav-btn" onClick={() => navigate(+1)}></button>
<span className="date-label">{formatDateLabel(viewDate)}</span> <span className="date-label">{formatDateLabel(viewDate)}</span>
{viewDate !== todayStr() && ( {viewDate !== todayStr() && (
<button className="date-today-btn" onClick={() => setViewDate(todayStr())}>Today</button> <button className="date-today-btn" onClick={() => setViewDate(todayStr())}>Today</button>

View file

@ -86,9 +86,22 @@ export interface ActivityEntry {
export type FilterMode = 'off' | 'inclusive' | 'exclusive' export type FilterMode = 'off' | 'inclusive' | 'exclusive'
// Semantic filter keys — each maps to one or more FlowTypes so that e.g.
// "Departing" catches both 'depart' and 'back-to-back' rooms.
export type SemanticFlowKey = 'arriving' | 'departing' | 'back-to-back' | 'staying' | 'vacant' | 'blocked'
export const SEMANTIC_FLOW_MAP: Record<SemanticFlowKey, FlowType[]> = {
arriving: ['arrive', 'back-to-back'],
departing: ['depart', 'back-to-back'],
'back-to-back': ['back-to-back'],
staying: ['stopover'],
vacant: ['vacant'],
blocked: ['blocked'],
}
export interface FilterState { export interface FilterState {
categories: Record<string, FilterMode> categories: Record<string, FilterMode>
flowTypes: Partial<Record<FlowType, FilterMode>> flowTypes: Partial<Record<SemanticFlowKey, FilterMode>>
} }
export type StatFilterMode = 'off' | 'show-only' | 'hide' export type StatFilterMode = 'off' | 'show-only' | 'hide'