import { useEffect, useState } from 'react' import { MonitorCog, Image as ImageIcon } from 'lucide-react' import { fetchSettings, updateSettings, testNewbookConnection, fetchKioskPasswordStatus, setKioskPassword, uploadLogo, logoUrl, } from '../api' import type { AppSettings } from '../types' export default function Settings() { const [settings, setSettings] = useState({ regcard_custom_field_id: '', online_checkin_complete_custom_field_id: '', terms_and_conditions_text: '', excluded_email_domains: '', checkin_inactivity_timeout_minutes: '5', }) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [error, setError] = useState('') const [msg, setMsg] = useState('') const [testing, setTesting] = useState(false) const [kioskConfigured, setKioskConfigured] = useState(null) const [kioskPassword, setKioskPasswordInput] = useState('') const [kioskSaving, setKioskSaving] = useState(false) const [kioskMsg, setKioskMsg] = useState('') const [kioskError, setKioskError] = useState('') const [logoVersion, setLogoVersion] = useState(0) const [logoExists, setLogoExists] = useState(null) const [logoUploading, setLogoUploading] = useState(false) const [logoError, setLogoError] = useState('') useEffect(() => { fetchSettings().then(setSettings).catch(() => {}).finally(() => setLoading(false)) fetchKioskPasswordStatus().then(s => setKioskConfigured(s.configured)).catch(() => {}) fetch(logoUrl()).then(r => setLogoExists(r.ok)).catch(() => setLogoExists(false)) }, []) async function handleLogoFile(e: React.ChangeEvent) { const file = e.target.files?.[0] e.target.value = '' // allow re-selecting the same file later if (!file) return if (!['image/png', 'image/jpeg'].includes(file.type)) { setLogoError('Must be a PNG or JPEG image'); return } if (file.size > 2 * 1024 * 1024) { setLogoError('Must be under 2MB'); return } setLogoUploading(true); setLogoError('') try { const dataUrl = await new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = () => resolve(reader.result as string) reader.onerror = reject reader.readAsDataURL(file) }) await uploadLogo(dataUrl) setLogoExists(true) setLogoVersion(v => v + 1) // cache-bust the so the new logo shows immediately } catch (e) { setLogoError(e instanceof Error ? e.message : 'Upload failed') } finally { setLogoUploading(false) } } async function handleSave() { setSaving(true); setError(''); setMsg('') try { await updateSettings(settings) setMsg('Settings saved'); setTimeout(() => setMsg(''), 2500) } catch (e) { setError(e instanceof Error ? e.message : 'Save failed') } finally { setSaving(false) } } async function handleTest() { setTesting(true); setError(''); setMsg('') try { const res = await testNewbookConnection() setMsg(res.ok ? `Connection OK: ${res.message || ''}` : `Failed: ${res.error || 'unknown'}`) } catch (e) { setError(e instanceof Error ? e.message : 'Test failed') } finally { setTesting(false) } } async function handleSaveKioskPassword() { setKioskSaving(true); setKioskError(''); setKioskMsg('') try { await setKioskPassword(kioskPassword) setKioskPasswordInput('') setKioskConfigured(true) setKioskMsg('Kiosk password saved — enter it once on each tablet at /tablet-checkin/kiosk.') } catch (e) { setKioskError(e instanceof Error ? e.message : 'Save failed') } finally { setKioskSaving(false) } } if (loading) return

Loading…

return (

Settings

{error &&
{error}
} {msg &&
{msg}
}
NewBook Integration
setSettings(s => ({ ...s, regcard_custom_field_id: e.target.value }))} placeholder="varies per hotel — confirm in NewBook config" />
setSettings(s => ({ ...s, online_checkin_complete_custom_field_id: e.target.value }))} placeholder="varies per hotel — confirm in NewBook config" />

Neither custom field ID is known for this hotel yet — the legacy plugin's fields were named regcard_link and online_checkin_complete, but their numeric IDs varied per install. Confirm both in NewBook (Settings → Custom Fields → Booking) before these write-backs will work; until they're set, check-ins still complete and generate a PDF, they just skip writing back into NewBook.

Guest Email Handling
setSettings(s => ({ ...s, excluded_email_domains: e.target.value }))} placeholder="guest.booking.com, m.expediapartnercentral.com" />

Comma-separated. An email at one of these domains never reaches the guest directly, so it's never shown as "on file" and the guest is prompted to add their real one instead — matches the legacy system's original behaviour. Add any other OTA/agent domain here as they turn up.

Terms & Conditions