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:
parent
2ec629cb5b
commit
b528624be0
3 changed files with 41 additions and 18 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
<span className={`card-site-status ${statusCls(room.site_status)}`}>
|
{isToday && (
|
||||||
{room.site_status || 'Unknown'}
|
<span className={`card-site-status ${statusCls(room.site_status)}`}>
|
||||||
</span>
|
{room.site_status || 'Unknown'}
|
||||||
|
</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 */}
|
||||||
<span className={`card-task-pill ${taskCls}`}>
|
{!isFuture && (
|
||||||
<TaskIcon size={12} {...I} />
|
<span className={`card-task-pill ${taskCls}`}>
|
||||||
{outstanding.length > 0 && <span>{outstanding.length}</span>}
|
<TaskIcon size={12} {...I} />
|
||||||
</span>
|
{outstanding.length > 0 && <span>{outstanding.length}</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]
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue