Cards: date-aware display — late checkouts only on future days, hide current-state info off-today

- Future dates: departure time sliver only shows for late checkouts (after default_checkout_time, default 11:00) — on future days every in-house booking is still 'arrived' so the time was showing on every card
- Sliver bracket widens to 19px when a time is shown and the time renders vertically (writing-mode) to fit the narrow space
- Clean/dirty site status pill only shows when viewing today — it reflects current state, meaningless on other dates
- NewBook task pill, task dots, and category header task/dirty badges hidden on future dates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-04 18:33:07 +00:00
parent 2ec629cb5b
commit b528624be0
3 changed files with 41 additions and 18 deletions

View file

@ -15,6 +15,10 @@ interface Props {
export default function CategoryGroup({ categoryId, categoryName, rooms, viewDate, config, onRoomClick }: Props) { export default function CategoryGroup({ categoryId, categoryName, rooms, viewDate, config, onRoomClick }: Props) {
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const today = new Date().toISOString().slice(0, 10)
const isToday = viewDate === today
const isFuture = viewDate > today
const outstandingTasks = rooms.reduce((n, r) => { const outstandingTasks = rooms.reduce((n, r) => {
return n + r.tasks.filter(t => { return n + r.tasks.filter(t => {
const d = t.task_when_date || t.task_period_from const d = t.task_when_date || t.task_period_from
@ -30,10 +34,10 @@ export default function CategoryGroup({ categoryId, categoryName, rooms, viewDat
<div className="category-header" onClick={() => setOpen(o => !o)}> <div className="category-header" onClick={() => setOpen(o => !o)}>
<span className="category-name">{categoryName}</span> <span className="category-name">{categoryName}</span>
<span style={{ fontSize: 11, color: 'var(--text-muted)' }}>{rooms.length}</span> <span style={{ fontSize: 11, color: 'var(--text-muted)' }}>{rooms.length}</span>
{outstandingTasks > 0 && ( {!isFuture && outstandingTasks > 0 && (
<span className="category-badge badge-tasks">{outstandingTasks} tasks</span> <span className="category-badge badge-tasks">{outstandingTasks} tasks</span>
)} )}
{dirtyCount > 0 && ( {isToday && dirtyCount > 0 && (
<span className="category-badge badge-dirty">{dirtyCount} dirty</span> <span className="category-badge badge-dirty">{dirtyCount} dirty</span>
)} )}
<ChevronDown <ChevronDown

View file

@ -67,6 +67,10 @@ function RoomCard({ room, viewDate, config, onClick }: Props) {
const isBlocked = room.flow_type === 'blocked' const isBlocked = room.flow_type === 'blocked'
const isVacant = room.flow_type === 'vacant' const isVacant = room.flow_type === 'vacant'
const today = new Date().toISOString().slice(0, 10)
const isToday = viewDate === today
const isFuture = viewDate > today
// Booking status (lowercased) drives the card border via CSS attribute selector // Booking status (lowercased) drives the card border via CSS attribute selector
const bookingStatus = booking?.booking_status?.toLowerCase() ?? null const bookingStatus = booking?.booking_status?.toLowerCase() ?? null
@ -94,9 +98,13 @@ function RoomCard({ room, viewDate, config, onClick }: Props) {
// Departure time shown in the left-bracket sliver (not as a card pill). // Departure time shown in the left-bracket sliver (not as a card pill).
// Show only when there's a departing booking whose status is still 'arrived' // Show only when there's a departing booking whose status is still 'arrived'
// (not yet checked out). Matches original: collapses once status flips to 'departed'. // (not yet checked out). Matches original: collapses once status flips to 'departed'.
// On future dates every in-house booking is still 'arrived', so only show
// late checkouts (time after the default checkout time).
const defaultCheckout = (config?.default_checkout_time || '11:00').slice(0, 5)
const sliverDepartTime = !room.spans_previous && const sliverDepartTime = !room.spans_previous &&
room.departing_time && room.departing_time &&
room.departing_booking?.booking_status?.toLowerCase() === 'arrived' room.departing_booking?.booking_status?.toLowerCase() === 'arrived' &&
(!isFuture || room.departing_time.slice(0, 5) > defaultCheckout)
? room.departing_time.slice(0, 5) ? room.departing_time.slice(0, 5)
: null : null
@ -131,7 +139,7 @@ function RoomCard({ room, viewDate, config, onClick }: Props) {
return ( return (
<div <div
className={`room-card${isBlocked ? ' room-blocked' : ''}`} className={`room-card${isBlocked ? ' room-blocked' : ''}${sliverDepartTime ? ' has-sliver-time' : ''}`}
{...(dataAttrs as React.HTMLAttributes<HTMLDivElement>)} {...(dataAttrs as React.HTMLAttributes<HTMLDivElement>)}
onClick={() => onClick(room)} onClick={() => onClick(room)}
> >
@ -139,7 +147,7 @@ function RoomCard({ room, viewDate, config, onClick }: Props) {
{sliverDepartTime && ( {sliverDepartTime && (
<div className="card-sliver-depart"> <div className="card-sliver-depart">
<Clock size={9} {...I} /> <Clock size={9} {...I} />
{sliverDepartTime} <span className="sliver-time">{sliverDepartTime}</span>
</div> </div>
)} )}
@ -171,9 +179,11 @@ function RoomCard({ room, viewDate, config, onClick }: Props) {
{nights.current}/{nights.total} {nights.current}/{nights.total}
</span> </span>
)} )}
{isToday && (
<span className={`card-site-status ${statusCls(room.site_status)}`}> <span className={`card-site-status ${statusCls(room.site_status)}`}>
{room.site_status || 'Unknown'} {room.site_status || 'Unknown'}
</span> </span>
)}
</div> </div>
</div> </div>
@ -222,16 +232,18 @@ function RoomCard({ room, viewDate, config, onClick }: Props) {
</span> </span>
)} )}
{/* Task status */} {/* Task status — housekeeping tasks only apply up to today */}
{!isFuture && (
<span className={`card-task-pill ${taskCls}`}> <span className={`card-task-pill ${taskCls}`}>
<TaskIcon size={12} {...I} /> <TaskIcon size={12} {...I} />
{outstanding.length > 0 && <span>{outstanding.length}</span>} {outstanding.length > 0 && <span>{outstanding.length}</span>}
</span> </span>
)}
</div> </div>
)} )}
{/* ── show_on_card task dots ──────────────────── */} {/* ── show_on_card task dots ──────────────────── */}
{todayTasks.some(t => taskDisp[t.task_type_id]?.show_on_card) && ( {!isFuture && todayTasks.some(t => taskDisp[t.task_type_id]?.show_on_card) && (
<div className="card-task-dots"> <div className="card-task-dots">
{todayTasks.map(t => { {todayTasks.map(t => {
const d = taskDisp[t.task_type_id] const d = taskDisp[t.task_type_id]

View file

@ -460,23 +460,30 @@ html, body, #root {
.room-card[data-next-status="unconfirmed"]::after { border-color: #f59e0b; } .room-card[data-next-status="unconfirmed"]::after { border-color: #f59e0b; }
.room-card[data-next-status="departed"]::after { border-color: #a855f7; } .room-card[data-next-status="departed"]::after { border-color: #a855f7; }
/* Departure time shown in left-bracket sliver (while prev booking still 'arrived') */ /* Departure time shown in left-bracket sliver (while prev booking still 'arrived').
The bracket widens to 19px when a time is shown; the time reads vertically. */
.room-card.has-sliver-time[data-previous-status]::before { width: 19px; }
.card-sliver-depart { .card-sliver-depart {
position: absolute; position: absolute;
left: -21px; left: -23px;
width: 19px;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 1px; gap: 2px;
font-size: 8px; font-size: 9px;
font-weight: 600; font-weight: 600;
color: var(--col-arrived); color: var(--col-arrived);
line-height: 1; line-height: 1;
pointer-events: none; pointer-events: none;
white-space: nowrap; white-space: nowrap;
} }
.card-sliver-depart .sliver-time {
writing-mode: vertical-rl;
letter-spacing: .04em;
}
/* Blocked room */ /* Blocked room */
.room-card.room-blocked { .room-card.room-blocked {