import { useState, useEffect } from 'react' import { api } from '../api' import { PageHeader, Card, Btn } from '../components/Layout' import type { User } from '../types' interface SettingsData { default_report_days: string petty_cash_float: string sales_breakdown_columns: string change_tin_breakdown: string } interface GlColumn { gl_code: string; display_name: string; enabled: boolean; sort_order: number } export function SettingsPage({ user }: { user: User }) { const [settings, setSettings] = useState>({}) const [columns, setColumns] = useState([]) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [testing, setTesting] = useState(false) const [refreshing, setRefreshing] = useState(false) const [msg, setMsg] = useState<{ text: string; ok: boolean } | null>(null) function flash(text: string, ok = true) { setMsg({ text, ok }) setTimeout(() => setMsg(null), 5000) } useEffect(() => { api.get('/settings').then(s => { setSettings(s) try { setColumns(JSON.parse(s.sales_breakdown_columns || '[]')) } catch { setColumns([]) } }).finally(() => setLoading(false)) }, []) function set(key: keyof SettingsData, value: string) { setSettings(prev => ({ ...prev, [key]: value })) } async function save() { setSaving(true) try { await api.put('/settings', { ...settings, sales_breakdown_columns: JSON.stringify(columns) }) flash('Settings saved.') } catch (e: unknown) { flash(e instanceof Error ? e.message : 'Save failed.', false) } finally { setSaving(false) } } async function testConnection() { setTesting(true) try { const r = await api.post<{ success: boolean; message: string }>('/settings/test-connection', {}) flash(r.message, r.success) } catch (e: unknown) { flash(e instanceof Error ? e.message : 'Test failed.', false) } finally { setTesting(false) } } async function refreshGl() { setRefreshing(true) try { const r = await api.post<{ message: string; columns: GlColumn[] }>('/settings/refresh-gl-accounts', {}) setColumns(r.columns) flash(r.message) } catch (e: unknown) { flash(e instanceof Error ? e.message : 'Refresh failed.', false) } finally { setRefreshing(false) } } function moveColumn(idx: number, dir: -1 | 1) { const next = [...columns] const target = idx + dir if (target < 0 || target >= next.length) return ;[next[idx], next[target]] = [next[target], next[idx]] next.forEach((c, i) => (c.sort_order = i + 1)) setColumns(next) } if (loading) return
Loading…
return (
{msg && (
{msg.text}
)} {/* Newbook — managed centrally */}

Newbook PMS

Credentials are managed in the{' '} Settings service.

{user.is_admin && ( {testing ? 'Testing…' : 'Test Connection'} )}
{/* Report defaults */}

Report Defaults

set('default_report_days', v)} />
{/* Float settings */}

Float Settings

set('petty_cash_float', v)} />
{/* Sales breakdown GL columns — admin only */} {user.is_admin && (

Sales Breakdown Columns

{refreshing ? 'Refreshing…' : 'Sync from Newbook'}
{columns.length === 0 ? (

No GL columns configured. Click "Sync from Newbook" to import GL account groups.

) : (
{columns.map((col, i) => (
setColumns(cols => cols.map((c, j) => j === i ? { ...c, enabled: e.target.checked } : c))} />
{col.display_name} {col.gl_code}
setColumns(cols => cols.map((c, j) => j === i ? { ...c, display_name: e.target.value } : c))} style={{ ...inpSt, fontSize: '0.8rem', padding: '0.25rem 0.5rem' }} />
))}
)}
)} {saving ? 'Saving…' : 'Save Settings'}
) } function FieldRow({ label, value, onChange, type = 'text' }: { label: string; value: string; onChange: (v: string) => void; type?: string }) { return (
onChange(e.target.value)} style={inpSt} />
) } const labelSt: React.CSSProperties = { fontSize: '0.75rem', color: 'var(--text-mid)', display: 'block', marginBottom: '0.25rem', fontWeight: 600 } const inpSt: React.CSSProperties = { border: '1px solid var(--card-border)', borderRadius: '6px', padding: '0.45rem 0.6rem', fontSize: '0.875rem', width: '100%' } const arrowBtn: React.CSSProperties = { background: 'none', border: '1px solid var(--card-border)', borderRadius: '4px', padding: '0.15rem 0.4rem', cursor: 'pointer', fontSize: '0.75rem' }