Replaces the 2-row (vs Booked / vs with Pickup) footer with a progressive 5-row breakdown: - Total Available (unchanged) - vs Booked — plain diff, room cleaning only - with Recurring Tasks — plain diff, adds recurring tasks - with Pickup — plain diff, adds pickup hours - with Adjustments — bold coloured final row with configurable thresholds New 5-tier icon system: red ⚠ / amber ⚠ / green ✓ / amber ✗ / red ✗, with defaults of 4h / 1h / 1h / 2h spare or short. Thresholds are editable in Settings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
147 lines
4.5 KiB
TypeScript
147 lines
4.5 KiB
TypeScript
import type { BookingsData, StaffMember, GeneralTask, PickupData, TimeReqs, WorkforceRota, Adjustment } from './types'
|
|
|
|
const BASE = '/hk-planner/api'
|
|
|
|
export interface ConfigData {
|
|
time_requirements: TimeReqs
|
|
staff_data: StaffMember[]
|
|
pickup_data: PickupData
|
|
general_tasks: GeneralTask[]
|
|
last_reviewed: string
|
|
workforce_rota: WorkforceRota | null
|
|
workforce_departments: string[]
|
|
adjustments: Adjustment[]
|
|
warn_over_red_hrs: number
|
|
warn_over_amber_hrs: number
|
|
warn_under_amber_hrs: number
|
|
warn_under_red_hrs: number
|
|
}
|
|
|
|
export interface CategoryConfig {
|
|
id: string
|
|
name: string
|
|
room_count: number
|
|
excluded: boolean
|
|
}
|
|
|
|
async function request<T>(path: string, opts?: RequestInit): Promise<T> {
|
|
const res = await fetch(BASE + path, { credentials: 'include', ...opts })
|
|
if (res.status === 401) {
|
|
;(window.top ?? window).location.href = '/login'
|
|
throw new Error('Unauthenticated')
|
|
}
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({})) as { error?: string }
|
|
throw new Error(body.error || `HTTP ${res.status}`)
|
|
}
|
|
return res.json() as Promise<T>
|
|
}
|
|
|
|
export function getBookings(weekStart?: string, lastViewed?: string, forceRefresh = false): Promise<BookingsData> {
|
|
const p = new URLSearchParams()
|
|
if (weekStart) p.set('week_start', weekStart)
|
|
if (lastViewed) p.set('last_viewed', lastViewed)
|
|
if (forceRefresh) p.set('force_refresh', '1')
|
|
return request<BookingsData>(`/bookings?${p}`)
|
|
}
|
|
|
|
export function getConfig(): Promise<ConfigData> {
|
|
return request<ConfigData>('/config')
|
|
}
|
|
|
|
export function putTimeReq(cat: string, action: string, value: number): Promise<{ ok: boolean }> {
|
|
return request('/config/time-requirements', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ cat, action, value }),
|
|
})
|
|
}
|
|
|
|
export function putStaff(staff_data: StaffMember[]): Promise<{ ok: boolean }> {
|
|
return request('/config/staff', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ staff_data }),
|
|
})
|
|
}
|
|
|
|
export function putPickup(pickup_data: PickupData): Promise<{ ok: boolean }> {
|
|
return request('/config/pickup', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ pickup_data }),
|
|
})
|
|
}
|
|
|
|
export function putGeneralTasks(general_tasks: GeneralTask[]): Promise<{ ok: boolean }> {
|
|
return request('/config/general-tasks', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ general_tasks }),
|
|
})
|
|
}
|
|
|
|
export function putLastReviewed(date: string): Promise<{ ok: boolean }> {
|
|
return request('/config/last-reviewed', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ date }),
|
|
})
|
|
}
|
|
|
|
export function getCategories(): Promise<{ categories: CategoryConfig[] }> {
|
|
return request('/categories')
|
|
}
|
|
|
|
export function putCategories(order: string[], excluded: string[]): Promise<{ ok: boolean }> {
|
|
return request('/categories', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ order, excluded }),
|
|
})
|
|
}
|
|
|
|
export function testNewbook(): Promise<{ ok: boolean; message?: string; error?: string }> {
|
|
return request('/newbook/test', { method: 'POST' })
|
|
}
|
|
|
|
export function getWorkforceDepartments(): Promise<{ id: string; name: string }[]> {
|
|
return request('/workforce/departments')
|
|
}
|
|
|
|
export function putWorkforceDepartments(dept_ids: string[]): Promise<{ ok: boolean }> {
|
|
return request('/config/workforce-departments', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ dept_ids }),
|
|
})
|
|
}
|
|
|
|
export function syncWorkforceRota(start: string, end: string): Promise<WorkforceRota> {
|
|
return request(`/workforce/sync?start=${start}&end=${end}`, { method: 'POST' })
|
|
}
|
|
|
|
export function getWorkforceStaff(): Promise<{ id: string; name: string }[]> {
|
|
return request('/workforce/staff')
|
|
}
|
|
|
|
export function putAdjustments(adjustments: Adjustment[]): Promise<{ ok: boolean }> {
|
|
return request('/config/adjustments', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ adjustments }),
|
|
})
|
|
}
|
|
|
|
export function putWarningThresholds(thresholds: {
|
|
warn_over_red_hrs: number
|
|
warn_over_amber_hrs: number
|
|
warn_under_amber_hrs: number
|
|
warn_under_red_hrs: number
|
|
}): Promise<{ ok: boolean }> {
|
|
return request('/config/warning-thresholds', {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(thresholds),
|
|
})
|
|
}
|