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:
parent
1ab864f4e3
commit
fbe913c01c
4 changed files with 45 additions and 24 deletions
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue