"use client"; import React, { useEffect, useRef, useState } from "react"; import { useAuth } from "@/hooks/useAuth"; import { useRouter } from "next/navigation"; import { apiFetch, API_BASE, resolveToApiOrigin } from "@/lib/api"; import { useSiteSettings } from "@/contexts/SiteSettingsContext"; import { useDismissingState } from "@/hooks/useDismissingState"; type TabId = "organisation" | "branding" | "notifications" | "email" | "legal"; const TABS: { id: TabId; label: string }[] = [ { id: "organisation", label: "Organisation" }, { id: "branding", label: "Branding" }, { id: "notifications", label: "Notifications" }, { id: "email", label: "Email" }, { id: "legal", label: "Legal" }, ]; const inputCls = "w-full border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400"; function Field({ label, hint, required, children, }: { label: string; hint?: string; required?: boolean; children: React.ReactNode; }) { return (
{children} {hint &&

{hint}

}
); } function SaveBar({ saving, onSave, result, }: { saving: boolean; onSave: () => void; result: { ok: boolean; message: string } | null; }) { return (
{result ? ( {result.ok ? "✓" : "✗"} {result.message} ) : ( )}
); } export default function SiteSettingsPage() { const { token } = useAuth(); const router = useRouter(); const { reload: reloadSettings } = useSiteSettings(); const [activeTab, setActiveTab] = useState("organisation"); const [loadingInitial, setLoadingInitial] = useState(true); // Per-tab save state const [saving, setSaving] = useState(false); const [result, setResult] = useDismissingState<{ ok: boolean; message: string } | null>(null); // ── Organisation ── const [orgName, setOrgName] = useState(""); const [orgTagline, setOrgTagline] = useState(""); const [orgEmail, setOrgEmail] = useState(""); const [orgPhone, setOrgPhone] = useState(""); const [orgAddress, setOrgAddress] = useState(""); const [appBaseUrl, setAppBaseUrl] = useState(""); // ── Branding ── const [accentColor, setAccentColor] = useState("#2563eb"); const [logoUrl, setLogoUrl] = useState(""); const [logoFile, setLogoFile] = useState(null); const [logoPreview, setLogoPreview] = useState(null); const fileRef = useRef(null); // ── Notifications ── const [notifEmails, setNotifEmails] = useState(""); // ── SMTP ── const [smtpHost, setSmtpHost] = useState(""); const [smtpPort, setSmtpPort] = useState("587"); const [smtpSecure, setSmtpSecure] = useState(false); const [smtpFrom, setSmtpFrom] = useState(""); const [smtpUser, setSmtpUser] = useState(""); const [smtpPass, setSmtpPass] = useState(""); const [smtpPassSet, setSmtpPassSet] = useState(false); const [smtpTesting, setSmtpTesting] = useState(false); const [smtpTestResult, setSmtpTestResult] = useDismissingState<{ ok: boolean; message: string; raw?: string } | null>(null); // ── Legal ── const [legalOperatorName, setLegalOperatorName] = useState(""); const [legalIoName, setLegalIoName] = useState(""); const [legalIoEmail, setLegalIoEmail] = useState(""); const [legalWebsiteUrl, setLegalWebsiteUrl] = useState(""); const [legalEffectiveDate, setLegalEffectiveDate] = useState(""); // ── Load all settings once ──────────────────────────────────────────────── useEffect(() => { if (!token) return; apiFetch>("/api/settings/all", { authToken: token }) .then((s) => { setOrgName(s.org_name || ""); setOrgTagline(s.org_tagline || ""); setOrgEmail(s.org_email || ""); setOrgPhone(s.org_phone || ""); setOrgAddress(s.org_address || ""); setAppBaseUrl(s.app_base_url || ""); setAccentColor(s.accent_color || "#2563eb"); setLogoUrl(s.logo_url || ""); setNotifEmails(s.reg_notification_emails || ""); setSmtpHost(s.smtp_host || ""); setSmtpPort(s.smtp_port || "587"); setSmtpSecure((s.smtp_secure || "").toLowerCase() === "true"); setSmtpFrom(s.smtp_from || ""); setSmtpUser(s.smtp_user || ""); setSmtpPassSet(!!s.smtp_pass && s.smtp_pass !== ""); setSmtpPass(""); setLegalOperatorName(s.legal_operator_name || ""); setLegalIoName(s.legal_io_name || ""); setLegalIoEmail(s.legal_io_email || ""); setLegalWebsiteUrl(s.legal_website_url || ""); setLegalEffectiveDate(s.legal_effective_date || ""); }) .catch((e: any) => setResult({ ok: false, message: e?.message || "Failed to load settings" })) .finally(() => setLoadingInitial(false)); }, [token]); // Clear result when switching tabs const switchTab = (id: TabId) => { setActiveTab(id); setResult(null); }; // ── Save helpers ────────────────────────────────────────────────────────── const save = async (updates: Record) => { if (!token) return; setSaving(true); setResult(null); try { await apiFetch("/api/settings", { method: "PUT", authToken: token, body: updates }); setResult({ ok: true, message: "Saved." }); reloadSettings(); } catch (e: any) { setResult({ ok: false, message: e?.message || "Failed to save" }); } finally { setSaving(false); } }; const saveOrganisation = () => save({ org_name: orgName.trim(), org_tagline: orgTagline.trim(), org_email: orgEmail.trim(), org_phone: orgPhone.trim(), org_address: orgAddress.trim(), app_base_url: appBaseUrl.trim(), }); const saveBranding = async () => { if (!token) return; setSaving(true); setResult(null); try { let finalLogoUrl = logoUrl; if (logoFile) { const fd = new FormData(); fd.append("image", logoFile); const res = await fetch(`${API_BASE}/api/uploads/logo`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, body: fd, }); if (!res.ok) throw new Error("Logo upload failed"); const data = await res.json(); finalLogoUrl = data.url || logoUrl; } await apiFetch("/api/settings", { method: "PUT", authToken: token, body: { accent_color: accentColor, logo_url: finalLogoUrl }, }); setLogoUrl(finalLogoUrl); setLogoFile(null); setLogoPreview(null); setResult({ ok: true, message: "Saved." }); reloadSettings(); } catch (e: any) { setResult({ ok: false, message: e?.message || "Failed to save" }); } finally { setSaving(false); } }; const saveNotifications = () => save({ reg_notification_emails: notifEmails.trim() }); const saveSmtp = async () => { if (!token) return; setSaving(true); setResult(null); try { const updates: Record = { smtp_host: smtpHost.trim(), smtp_port: smtpPort.trim(), smtp_secure: smtpSecure ? "true" : "false", smtp_from: smtpFrom.trim(), smtp_user: smtpUser.trim(), }; if (smtpPass.trim() !== "") updates.smtp_pass = smtpPass.trim(); await apiFetch("/api/settings", { method: "PUT", authToken: token, body: updates }); if (smtpPass.trim() !== "") { setSmtpPassSet(true); setSmtpPass(""); } setResult({ ok: true, message: "Saved." }); reloadSettings(); } catch (e: any) { setResult({ ok: false, message: e?.message || "Failed to save" }); } finally { setSaving(false); } }; const saveLegal = () => save({ legal_operator_name: legalOperatorName.trim(), legal_io_name: legalIoName.trim(), legal_io_email: legalIoEmail.trim(), legal_website_url: legalWebsiteUrl.trim(), legal_effective_date: legalEffectiveDate.trim(), }); const handleTestSmtp = async () => { if (!token) return; setSmtpTesting(true); setSmtpTestResult(null); try { await apiFetch("/api/settings/test-smtp", { method: "POST", authToken: token, body: { host: smtpHost.trim(), port: smtpPort.trim(), secure: smtpSecure, from: smtpFrom.trim(), user: smtpUser.trim(), pass: smtpPass.trim() !== "" ? smtpPass.trim() : "••••••••", }, }); setSmtpTestResult({ ok: true, message: "Connection successful — check your inbox for a test email." }); } catch (e: any) { setSmtpTestResult({ ok: false, message: e?.message || "SMTP test failed", raw: e?.data?.raw }); } finally { setSmtpTesting(false); } }; if (loadingInitial) return
Loading settings…
; const currentLogoSrc = logoPreview || (logoUrl ? resolveToApiOrigin(logoUrl) : null); return (
{/* Header */}

Site Settings

Configure your organisation, branding, email, and legal pages.

{/* Tab bar */}
{TABS.map((tab) => ( ))}
{/* ── Organisation ─────────────────────────────────────────────────── */} {activeTab === "organisation" && (
setOrgName(e.target.value)} /> setOrgTagline(e.target.value)} />
setOrgEmail(e.target.value)} /> setOrgPhone(e.target.value)} />
setOrgAddress(e.target.value)} /> setAppBaseUrl(e.target.value)} />
)} {/* ── Branding ─────────────────────────────────────────────────────── */} {activeTab === "branding" && (
setAccentColor(e.target.value)} /> setAccentColor(e.target.value)} />
{currentLogoSrc && (
{/* eslint-disable-next-line @next/next/no-img-element */} Current logo
)} { const file = e.target.files?.[0]; if (!file) return; setLogoFile(file); setLogoPreview(URL.createObjectURL(file)); }} className="text-sm" />
)} {/* ── Notifications ─────────────────────────────────────────────────── */} {activeTab === "notifications" && (
setNotifEmails(e.target.value)} />
)} {/* ── Email / SMTP ──────────────────────────────────────────────────── */} {activeTab === "email" && (

Outgoing email for tickets, payment confirmations, and account notifications. Leave blank to use server environment variables. The password is stored encrypted.

setSmtpHost(e.target.value)} /> setSmtpPort(e.target.value)} />
setSmtpSecure(e.target.checked)} className="rounded" />
setSmtpFrom(e.target.value)} />
setSmtpUser(e.target.value)} autoComplete="username" />
setSmtpPass(e.target.value)} autoComplete="new-password" /> {smtpPassSet && smtpPass === "" && ( ● saved )}
{/* Test connection */}
{smtpTestResult && ( {smtpTestResult.ok ? "✓" : "✗"} {smtpTestResult.message} )}
{smtpTestResult && !smtpTestResult.ok && smtpTestResult.raw && (
Show technical details
{smtpTestResult.raw}
)}
)} {/* ── Legal ─────────────────────────────────────────────────────────── */} {activeTab === "legal" && (

These values populate the Terms of Use and Privacy Policy pages automatically.

setLegalOperatorName(e.target.value)} /> setLegalWebsiteUrl(e.target.value)} /> setLegalEffectiveDate(e.target.value)} />

Information Officer (POPIA)

setLegalIoName(e.target.value)} /> setLegalIoEmail(e.target.value)} />
)}
); }