- Uploadable reception logo (Settings, PNG/JPEG, public GET so the kiosk can show it without a staff login) — shown top-center on the kiosk login and idle screens, Welcome copy centered underneath. - Tablet-sized styling pass on all kiosk views for a ~10" tablet (bigger touch targets, base text, spacing) rather than the general app's desktop-first sizing. - New booking-confirm step (name/arrive/nights/depart/rate name(s)/rate total) replaces the old partial review step. - Contact details, car registration and marketing merged into one step (was 3 separate steps) — 5 steps down to 3 overall. - Email handling now matches the legacy system's confirmed behaviour: excluded-domain (OTA/agent relay) addresses are filtered out of what's shown as 'on file' entirely, not just flagged, and a new excluded_email_domains Settings field (CSV) replaces the old hardcoded domain list so any OTA/agent domain can be added without a code change. - Single email field design: only pre-filled when there's exactly one valid existing email, left blank (not guessed) when there are several — marketing opt-in only ever touches whatever's in that one field, never blanket-subscribes multiple pre-existing emails (project_checkin memory, 2026-09-01 'stay in touch' note). - Car registration now allows multiple additions — add/remove list UI, buildContactDetails appends each new one not already on file rather than the old single-slot overwrite. - Signature now required to actually complete (tracked live via SignatureCanvas onEnd, not just caught on submit) alongside accepted terms — Complete Check-In stays disabled until both are true. - T&C block is height-capped with its own internal scroll (keeps the step on one non-scrolling screen) and now renders basic HTML (<b>, <br>, <p>, <ul> etc) instead of plain text — content is admin-authored (settings capability required), same trust boundary as any other setting. PDF generation strips tags back to plain text since pdfkit can't render HTML. - PDF and booking-confirm screen both surface tariff name(s) (deduped across tariffs_quoted in case the rate changed mid-stay) and nights, not just the total. - Also fixed: injected extension buttons could hang forever on 'Loading tablets…' with no feedback if the extension was reloaded without also refreshing the NewBook tab (stale content script) — now surfaces a clear 'Extension was reloaded — refresh this page' message instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
145 lines
3.5 KiB
TypeScript
145 lines
3.5 KiB
TypeScript
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 {
|
|
regcard_custom_field_id: string
|
|
online_checkin_complete_custom_field_id: string
|
|
terms_and_conditions_text: string
|
|
excluded_email_domains: 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
|
|
tariff_names: string[]
|
|
nights: number | null
|
|
}
|
|
|
|
// No is_relay flag any more — excluded-domain emails are filtered out
|
|
// server-side entirely (matches the legacy system's confirmed behaviour,
|
|
// see project_checkin_legacy_plugin), not just flagged. had_excluded_email
|
|
// on CheckinGuest below covers "was something hidden" without exposing it.
|
|
export interface ExistingEmail {
|
|
content: string
|
|
}
|
|
|
|
export interface CheckinGuest {
|
|
guest_id: string | null
|
|
name: string
|
|
address: string
|
|
phone: string
|
|
existing_emails: ExistingEmail[]
|
|
had_excluded_email: boolean
|
|
car_regos: string[]
|
|
}
|
|
|
|
export interface CheckinTokenData {
|
|
booking: CheckinBooking
|
|
guest: CheckinGuest
|
|
terms_text: string
|
|
}
|
|
|
|
export interface SubmitCheckinPayload {
|
|
email: string
|
|
car_regos: 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)
|
|
}
|