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>
This commit is contained in:
jtricerolph 2026-07-03 13:07:00 +00:00
commit 1e658b6a48
39 changed files with 3765 additions and 0 deletions

77
frontend/src/api.ts Normal file
View file

@ -0,0 +1,77 @@
import type { RoomsResponse, ActivityEntry, AppConfig, TaskData } from './types'
const BASE = '/room-planner/api'
async function request<T>(path: string, opts: RequestInit = {}): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
credentials: 'include',
headers: { 'Content-Type': 'application/json', ...opts.headers },
...opts,
})
if (!res.ok) {
const err = await res.json().catch(() => ({ error: res.statusText }))
throw new Error(err.error || `Request failed: ${res.status}`)
}
return res.json()
}
export function fetchRooms(date: string): Promise<RoomsResponse> {
return request(`/rooms?date=${date}`)
}
export function completeTask(taskId: string, roomId: string, serviceDate: string, bookingRef?: string) {
return request<{ ok: boolean; site_status: string | null }>('/tasks/complete', {
method: 'POST',
body: JSON.stringify({ task_id: taskId, room_id: roomId, service_date: serviceDate, booking_ref: bookingRef }),
})
}
export function uncompleteTask(taskId: string) {
return request<{ ok: boolean }>('/tasks/uncomplete', {
method: 'POST',
body: JSON.stringify({ task_id: taskId }),
})
}
export function updateStatus(roomId: string, status: string, serviceDate: string, bookingRef?: string) {
return request<{ ok: boolean }>('/status', {
method: 'POST',
body: JSON.stringify({ room_id: roomId, status, service_date: serviceDate, booking_ref: bookingRef }),
})
}
export function fetchConfig(): Promise<AppConfig> {
return request('/config')
}
export function updateConfig(key: string, value: unknown): Promise<{ ok: boolean }> {
return request(`/config/${key}`, {
method: 'PUT',
body: JSON.stringify({ value }),
})
}
export function fetchTaskTypes(): Promise<Array<{ id: string; name: string }>> {
return request('/config/task-types')
}
export function fetchActivity(date: string): Promise<ActivityEntry[]> {
return request(`/activity?date=${date}`)
}
export function logActivity(entry: {
room_id: string
event_type: ActivityEntry['event_type']
event_data?: Record<string, unknown>
service_date?: string
booking_ref?: string
}): Promise<{ ok: boolean }> {
return request('/activity', { method: 'POST', body: JSON.stringify(entry) })
}
export function fetchEvents(since: string): Promise<{
pings: Array<{ id: number; booking_ids: string[]; event_types: string[]; detected_at: string }>
server_time: string
}> {
return request(`/events?since=${encodeURIComponent(since)}`)
}