Initial scaffold: tablet-checkin app

Staff-push guest check-in tablet. Fastify backend + React/Vite frontend,
three-tier auth (staff JWT, terminal API key, kiosk cookie), NewBook
integration via bookings_get/guests_update/bookings_update, pdfkit-based
regcard generation, marketing opt-in with Sendy/Mailchimp adapters.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-09-01 17:35:39 +00:00
commit 3b2e25df3f
51 changed files with 10296 additions and 0 deletions

140
frontend/src/types.ts Normal file
View file

@ -0,0 +1,140 @@
export type DeviceStatus = 'idle' | 'busy'
export interface Device {
device_id: string
name: string
status: DeviceStatus
last_seen: string | null
created_at: string
}
export interface Terminal {
terminal_id: string
name: string
created_at: string
last_used_at: string | null
revoked_at: string | null
}
export interface CreatedTerminal extends Terminal {
api_key: string // present only in the create response — never again
}
export type CheckinStatus = 'pending' | 'complete' | 'failed'
export interface MarketingSyncSummary {
provider: string
status: 'pending' | 'success' | 'failed'
error: string | null
}
export interface Checkin {
id: number
booking_id: string
guest_id: string | null
hotel_id: string
device_id: string | null
device_name: string | null
guest_name: string | null
guest_email: string | null
pdf_path: string | null
opted_in: boolean
status: CheckinStatus
error: string | null
submitted_at: string
marketing_sync: MarketingSyncSummary[] | null
}
export type MarketingProviderId = 'sendy' | 'mailchimp'
export interface SendyConfig {
api_key?: string
install_url?: string
list_id?: string
}
export interface MailchimpConfig {
api_key?: string
server_prefix?: string
audience_id?: string
}
export interface MarketingProvider {
provider: MarketingProviderId
active: boolean
config: SendyConfig | MailchimpConfig
updated_at: string
implemented: boolean
}
export interface MarketingSyncRow {
id: number
checkin_id: number
provider: string
status: 'pending' | 'success' | 'failed'
attempts: number
last_attempt_at: string | null
error: string | null
created_at: string
booking_id: string
guest_name: string | null
guest_email: string | null
}
export interface AppSettings {
newbook_hotel_id: string
regcard_custom_field_id: string
online_checkin_complete_custom_field_id: string
terms_and_conditions_text: string
}
// ── Guest-facing token flow (routes/checkin.js) ─────────────────────────────
export interface CheckinBooking {
booking_id: string
arrival: string | null
departure: string | null
site_name: string | null
tariff_total: number | string | null
nights: number | null
}
export interface ExistingEmail {
content: string
is_relay: boolean
}
export interface CheckinGuest {
guest_id: string | null
name: string
address: string
phone: string
existing_emails: ExistingEmail[]
car_rego: string
}
export interface CheckinTokenData {
booking: CheckinBooking
guest: CheckinGuest
terms_text: string
}
export interface SubmitCheckinPayload {
email: string
car_rego: string
opted_in: boolean
accepted_terms: boolean
signature_png_base64: string
}
// ── Staff auth ──────────────────────────────────────────────────────────────
export interface User {
user_id: number
name: string
email: string
is_admin: boolean
caps: string[] // bare slugs — verify?app=tablet-checkin strips the prefix
}
export function can(user: User, cap: string): boolean {
return user.is_admin || user.caps.includes(cap)
}