room-planner/frontend/src/components/ActivityPanel.tsx
jtricerolph 1e658b6a48 feat: add room-planner app — 3-day HK room view with NewBook integration
NewBook-connected daily housekeeping planner. Replaces the hotelhubmodule-housekeeping-dailylist
WordPress plugin. LXC 120 · 10.10.10.120:3080 · slug: room-planner.

- 3-day booking window (yesterday/today/tomorrow) fetched live from NewBook
- Task completion ticks back to NewBook; room status patches NewBook directly
- 23px border sliver CSS system for adjacent-day booking status
- 3-state filter cycling (off→inclusive→exclusive) for categories and flow types
- Stat filters for outstanding tasks and clean/dirty status
- Rolling 48h activity log with checkout/checkin/status/tasks events
- newbook_pings event bus for future NewBook poller integration
- Room modal with permission-gated guest/rate/notes, task checkboxes, status buttons
- Placeholder sections for future linen-count and routine-tasks modules
- Settings page: task type colours, twin/extra-bed detection, category exclusions
- Mobile-first layout (sidebar desktop, compact top bar mobile)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 13:07:00 +00:00

59 lines
1.9 KiB
TypeScript

import type { ActivityEntry } from '../types'
interface Props {
entries: ActivityEntry[]
}
const EVENT_LABELS: Record<ActivityEntry['event_type'], string> = {
checkout: 'Checked out',
checkin: 'Checked in',
status_clean: 'Marked clean',
status_dirty: 'Marked dirty',
tasks_complete: 'All tasks done',
}
const EVENT_CLASS: Record<ActivityEntry['event_type'], string> = {
checkout: 'event-checkout',
checkin: 'event-checkin',
status_clean: 'event-clean',
status_dirty: 'event-dirty',
tasks_complete: 'event-tasks',
}
function relativeTime(iso: string) {
const diff = Date.now() - new Date(iso).getTime()
const mins = Math.floor(diff / 60000)
if (mins < 1) return 'Just now'
if (mins < 60) return `${mins}m ago`
const hrs = Math.floor(mins / 60)
if (hrs < 24) return `${hrs}h ago`
return new Date(iso).toLocaleDateString()
}
export default function ActivityPanel({ entries }: Props) {
return (
<aside className="activity-panel">
<div className="activity-panel-header">Recent Changes</div>
<div className="activity-list">
{entries.length === 0 ? (
<div style={{ padding: '16px 12px', color: 'var(--text-muted)', fontSize: 12 }}>
No activity yet today
</div>
) : (
entries.map(entry => (
<div key={entry.id} className="activity-item">
<div className="activity-room">{entry.room_id}</div>
<div className={`activity-event ${EVENT_CLASS[entry.event_type]}`}>
{EVENT_LABELS[entry.event_type]}
</div>
{entry.user_name && (
<div className="activity-time">{entry.user_name}</div>
)}
<div className="activity-time">{relativeTime(entry.occurred_at)}</div>
</div>
))
)}
</div>
</aside>
)
}