import { useEffect, useState } from 'react' import { CheckCircle, XCircle, Loader } from 'lucide-react' import { getSettings, postSettings, testNewbook } from '../api' import type { AppSettings } from '../types' type TestState = 'idle' | 'testing' | 'ok' | 'error' export function Settings() { const [form, setForm] = useState(null) const [saving, setSaving] = useState(false) const [saved, setSaved] = useState(false) const [error, setError] = useState(null) const [testState, setTest] = useState('idle') const [testMsg, setTestMsg] = useState('') useEffect(() => { getSettings() .then(s => setForm(s)) .catch(e => setError(e.message)) }, []) function patch(key: keyof AppSettings, value: string) { setForm(prev => prev ? { ...prev, [key]: value } : prev) } async function save(e: React.FormEvent) { e.preventDefault() if (!form) return setSaving(true) setError(null) setSaved(false) try { const updated = await postSettings(form) setForm(updated) setSaved(true) setTimeout(() => setSaved(false), 3000) } catch (e) { setError(e instanceof Error ? e.message : 'Save failed') } finally { setSaving(false) } } async function runTest() { setTest('testing') setTestMsg('') try { const r = await testNewbook() setTest(r.ok ? 'ok' : 'error') setTestMsg(r.message || r.error || '') } catch (e) { setTest('error') setTestMsg(e instanceof Error ? e.message : 'Connection failed') } } if (!form) { return (
{error ? `Error: ${error}` : 'Loading…'}
) } const inputStyle: React.CSSProperties = { width: '100%', border: '1px solid var(--card-border)', borderRadius: '6px', padding: '0.45rem 0.6rem', fontSize: '0.85rem', color: 'var(--text-dark)', background: 'var(--body-bg)', } return (

Settings

{/* Twin detection */}

Twin Detection

patch('custom_field_names', e.target.value)} placeholder="Bed Type, Room Configuration" />

NewBook booking custom field names to check for twin indicators.

patch('custom_field_values', e.target.value)} placeholder="twin, 2 x single, 2x single" />

Case-insensitive partial match against the field values above. These trigger a confirmed twin.

patch('notes_search_terms', e.target.value)} placeholder="twin bed, two singles, separate beds" />

If no custom field match is found, these are searched in booking notes. Triggers a potential twin (amber).

patch('excluded_terms', e.target.value)} placeholder="Double or Twin:, Suite or Twin:" />

Removed from notes before searching — use to strip ambiguous boilerplate like "Double or Twin:" so it doesn't trigger a false positive.

{/* Colors */}

Display colours

patch('normal_color', e.target.value)} /> Normal booking
patch('twin_color', e.target.value)} /> Confirmed twin
patch('potential_twin_color', e.target.value)} /> Potential twin
{/* Save */}
{saved && ( Saved )} {error && {error}}
{/* Newbook test */}

Newbook connection

Credentials are configured in the Settings service. This tests the connection.

{testState === 'ok' && {testMsg}} {testState === 'error' && {testMsg}}
{/* Detection legend */}

How detection works

  1. Confirmed twin — custom field names matched against field values above (case-insensitive, partial match).
  2. Legacy fallback — "Bed Type" field containing "twin" or "2 x single" (always active).
  3. Potential twin — notes search terms found in booking notes, after excluded terms are stripped out.
) }