// ============================================================================ // public/screens/admin.jsx — Super-Admin Panel (Phase 8) // ============================================================================ // ═══════════════════ Admin Overview ══════════════════════════════════════ function AdminOverviewScreen() { const [data, setData] = React.useState(null); React.useEffect(() => { VoaisAPI.get("/api/admin/overview").then(r => { if (r.ok) setData(r.data.overview); }); }, []); if (!data) return ; return (
{data.health?.length > 0 && (
System health
{data.health.map((h, i) => (
{h.name}
{h.latency_ms && {h.latency_ms}ms}
))}
)}
); } // ═══════════════════ Clients ═════════════════════════════════════════════ function AdminClientsScreen() { const [clients, setClients] = React.useState(null); const [pag, setPag] = React.useState({}); const [search, setSearch] = React.useState(""); const [detail, setDetail] = React.useState(null); const [showCreate, setShow] = React.useState(false); const load = async (page = 1) => { const p = new URLSearchParams({ page, limit: 25 }); if (search) p.set("search", search); const r = await VoaisAPI.get("/api/admin/clients?" + p.toString()); if (r.ok) { setClients(r.data.clients); setPag(r.data.pagination); } }; React.useEffect(() => { load(); }, []); const openDetail = async (id) => { const r = await VoaisAPI.get("/api/admin/clients/" + id); if (r.ok) setDetail(r.data); }; const updateClient = async (id, fields) => { await VoaisAPI.patch("/api/admin/clients/" + id, fields); load(); if (detail?.tenant?.id === id) openDetail(id); }; if (!clients) return ; return (
setSearch(e.target.value)} onKeyDown={e => { if (e.key === "Enter") load(); }}/> load()}>Search
} onClick={() => setShow(true)}>Add client
{clients.map(c => ( openDetail(c.id)} style={{ cursor: "pointer" }}> ))}
ClientPlanStatusCallsUsersMRRJoined
{c.name}
{c.slug} · {c.email || "—"}
{c.plan_name || "—"} {c.status} {+c.total_calls} {+c.user_count} {c.mrr_inr ? "₹"+Number(c.mrr_inr).toLocaleString("en-IN") : "—"} {new Date(c.created_at).toLocaleDateString("en-IN",{day:"numeric",month:"short",year:"numeric"})}
{pag.pages > 1 &&
load(pag.page-1)}>PrevPage {pag.page}/{pag.pages}=pag.pages} onClick={()=>load(pag.page+1)}>Next
}
{detail && setDetail(null)} width={620}>
Slug: {detail.tenant.slug}
Industry: {detail.tenant.industry || "—"}
Status: {detail.tenant.status}
{detail.subscription &&
Plan: {detail.subscription.plan_name} · {detail.subscription.status} · MRR ₹{Number(detail.subscription.mrr_inr||0).toLocaleString("en-IN")}
}
{+detail.stats.total_calls}
Calls
{+detail.stats.agents}
Agents
{+detail.stats.contacts}
Contacts
Team ({detail.users?.length})
{detail.users?.map(u =>
{u.name || u.email}{u.role}
)}
{detail.tenant.status !== "suspended" && updateClient(detail.tenant.id, {status:"suspended"})}>Suspend} {detail.tenant.status === "suspended" && updateClient(detail.tenant.id, {status:"active"})}>Unsuspend}
} {showCreate && setShow(false)} width={460}> { setShow(false); load(); }}/> }
); } function AdminCreateClient({ onCreated }) { const [f, setF] = React.useState({ name: "", slug: "", email: "", plan_code: "starter" }); const [sub, setSub] = React.useState(false); const submit = async () => { if (!f.name.trim()||!f.slug.trim()) return; setSub(true); const r = await VoaisAPI.post("/api/admin/clients", f); setSub(false); if (r.ok) onCreated(); else alert(r.data?.msg); }; return (
setF({...f, name: e.target.value})}/> setF({...f, slug: e.target.value.toLowerCase().replace(/[^a-z0-9-]/g,"")})}/> setF({...f, email: e.target.value})}/> {sub ? "Creating..." : "Create"}
); } // ═══════════════════ SIP Trunks ══════════════════════════════════════════ function AdminSipScreen() { const [trunks, setTrunks] = React.useState(null); const load = () => VoaisAPI.get("/api/admin/sip").then(r => { if (r.ok) setTrunks(r.data.trunks); }); React.useEffect(() => { load(); }, []); if (!trunks) return ; return ({trunks.map(t => ( ))}{trunks.length===0&&}
TrunkProviderServerChannelsStatusTenant
{t.name}{t.provider} {t.server}:{t.port} {t.channel_capacity} {t.status} {t.tenant_name || "Platform"}
No SIP trunks configured.
); } // ═══════════════════ Agent Library ═══════════════════════════════════════ function AdminAgentLibScreen() { const [agents, setAgents] = React.useState(null); React.useEffect(() => { VoaisAPI.get("/api/admin/agents").then(r => { if (r.ok) setAgents(r.data.agents); }); }, []); if (!agents) return ; return ({agents.map(a => ( ))}{agents.length===0&&}
AgentRoleLanguageLLMStatusCalls
{a.name}
{a.code}
{a.role||"—"}{a.language}{a.llm_provider}/{a.llm_model} {a.status}{a.total_calls}
No global agent templates.
); } // ═══════════════════ Billing Overview ════════════════════════════════════ function AdminBillingScreen() { const [data, setData] = React.useState(null); React.useEffect(() => { VoaisAPI.get("/api/admin/billing").then(r => { if (r.ok) setData(r.data); }); }, []); if (!data) return ; return (
Subscribers by plan
{data.byPlan?.map(p =>
{p.name}{p.count} clients₹{Number(p.revenue).toLocaleString("en-IN")}/mo
)}
Recent invoices
{(data.recentInvoices||[]).map((inv,i) => ( ))}
InvoiceTenantAmountStatus
{inv.invoice_number}{inv.tenant_name} ₹{Number(inv.total_inr).toLocaleString("en-IN")} {inv.status}
); } // ═══════════════════ Compliance ══════════════════════════════════════════ function AdminComplianceScreen() { const [data, setData] = React.useState(null); React.useEffect(() => { VoaisAPI.get("/api/admin/compliance").then(r => { if (r.ok) setData(r.data); }); }, []); if (!data) return ; return (
{data.recentViolations?.length > 0 &&
Recent compliance events
{data.recentViolations.map((v,i) =>
{v.tenant_name || "Platform"}{v.action} {new Date(v.created_at).toLocaleString("en-IN")}
)}
}
); } // ═══════════════════ System Health ═══════════════════════════════════════ function AdminHealthScreen() { const [data, setData] = React.useState(null); React.useEffect(() => { VoaisAPI.get("/api/admin/health").then(r => { if (r.ok) setData(r.data); }); }, []); if (!data) return ; return (
{data.checks?.map((c,i) => (
{c.name}
{c.category} · {c.latency_ms ? c.latency_ms + "ms" : "—"} {c.success_rate != null && · {Number(c.success_rate).toFixed(1)}%}
{c.last_error &&
{c.last_error}
}
))}
AI providers
{data.aiProviders?.map(p => (
{p.display_name}
{p.category} · {p.default_model}
{p.enabled ? "Enabled" : "Disabled"}
))}
Jobs queue
Total: {data.jobs.total} Pending: {data.jobs.pending} Running: {data.jobs.running} Failed: {data.jobs.failed}
); } // ═══════════════════ Audit Logs ══════════════════════════════════════════ function AdminAuditScreen() { const [entries, setEntries] = React.useState(null); const [pag, setPag] = React.useState({}); const [actionF, setActionF] = React.useState(""); const load = async (page = 1) => { const p = new URLSearchParams({ page, limit: 50 }); if (actionF) p.set("action", actionF); const r = await VoaisAPI.get("/api/admin/audit?" + p.toString()); if (r.ok) { setEntries(r.data.entries); setPag(r.data.pagination); } }; React.useEffect(() => { load(); }, []); if (!entries) return ; return (
setActionF(e.target.value)} onKeyDown={e => {if(e.key==="Enter")load();}}/> load()}>Filter
{entries.map(e => ( ))}
ActionActorTenantEntityWhen
{e.action}
{e.actor_email || e.actor_type}
{e.tenant_name || "—"} {e.entity_type ? e.entity_type + " #" + e.entity_id : "—"} {new Date(e.created_at).toLocaleString("en-IN")}
{pag.pages>1&&
load(pag.page-1)}>PrevPage {pag.page}/{pag.pages} ({pag.total} entries)=pag.pages} onClick={()=>load(pag.page+1)}>Next
}
); } // ── Shared ─────────────────────────────────────────────────────────────── function AKpiCard({ label, value, color }) { return (
{label}
{value}
); } // ═══════════════════ Platform Telephony (BYOI defaults) ══════════════════ // Super-admin sets the shared Asterisk / Dinstar config that every tenant on // "platform" mode falls back to. Secrets are masked on read; sending "" on // save means "leave as-is", a non-empty string replaces, null clears. function AdminTelephonyScreen() { const [cfg, setCfg] = React.useState(null); const [saving, setSaving] = React.useState(false); const [testing, setTesting] = React.useState(false); const [status, setStatus] = React.useState(null); const [testResult, setTestResult] = React.useState(null); const [touched, setTouched] = React.useState({ ami_secret: false, ari_password: false }); const [secrets, setSecrets] = React.useState({ ami_secret: "", ari_password: "" }); const load = React.useCallback(() => { VoaisAPI.get("/api/admin/telephony-defaults").then(r => { if (r.ok && r.data?.ok) setCfg(r.data.config); }); }, []); React.useEffect(() => { load(); }, [load]); if (!cfg) return ; const setField = (k, v) => setCfg({ ...cfg, [k]: v }); const save = async () => { setSaving(true); setStatus(null); const payload = { ami_host: cfg.ami_host, ami_port: cfg.ami_port, ami_user: cfg.ami_user, ari_url: cfg.ari_url, ari_user: cfg.ari_user, ari_app: cfg.ari_app, external_media_host: cfg.external_media_host, external_media_port: cfg.external_media_port, trunk: cfg.trunk, ai_context: cfg.ai_context, dial_context: cfg.dial_context, dinstar_ip: cfg.dinstar_ip, gsm_ports: cfg.gsm_ports, gsm_prefix_template: cfg.gsm_prefix_template, }; if (touched.ami_secret) payload.ami_secret = secrets.ami_secret; if (touched.ari_password) payload.ari_password = secrets.ari_password; const r = await VoaisAPI.patch("/api/admin/telephony-defaults", payload); setSaving(false); if (r.ok) { setStatus({ ok: true, msg: "Saved" }); setSecrets({ ami_secret: "", ari_password: "" }); setTouched({ ami_secret: false, ari_password: false }); load(); } else { setStatus({ ok: false, msg: r.data?.msg || "Save failed" }); } setTimeout(() => setStatus(null), 3000); }; const runTest = async () => { setTesting(true); setTestResult(null); const r = await VoaisAPI.post("/api/admin/telephony-defaults/test"); setTesting(false); setTestResult({ ok: r.ok && r.data?.ok, msg: r.data?.msg || (r.ok ? "OK" : "Test failed"), ms: r.data?.ms }); }; return (
Platform Asterisk defaults
Tenants on "Use Platform Asterisk" mode fall back to these values. Secrets are AES-256-GCM encrypted at rest.
{status && {status.msg}}
Asterisk AMI
setField("ami_host", e.target.value)}/> setField("ami_port", Number(e.target.value))}/>
setField("ami_user", e.target.value)}/> { setSecrets({ ...secrets, ami_secret: e.target.value }); setTouched({ ...touched, ami_secret: true }); }}/>
Asterisk ARI
setField("ari_url", e.target.value)}/> setField("ari_app", e.target.value)}/>
setField("ari_user", e.target.value)}/> { setSecrets({ ...secrets, ari_password: e.target.value }); setTouched({ ...touched, ari_password: true }); }}/>
External media
setField("external_media_host", e.target.value)}/> setField("external_media_port", Number(e.target.value))}/>
Dialplan contexts
setField("trunk", e.target.value)}/> setField("ai_context", e.target.value)}/> setField("dial_context", e.target.value)}/>
Dinstar GSM gateway (platform default)
setField("dinstar_ip", e.target.value)}/> setField("gsm_ports", e.target.value)}/> setField("gsm_prefix_template", e.target.value)}/>
{saving ? "Saving…" : "Save platform defaults"} {testing ? "Testing…" : "Test platform AMI"} {testResult && ( {testResult.ok ? `OK (${testResult.ms || 0}ms)` : testResult.msg} )}
); } // Expose all admin screens globally window.AdminOverviewScreen = AdminOverviewScreen; window.AdminClientsScreen = AdminClientsScreen; window.AdminSipScreen = AdminSipScreen; window.AdminAgentLibScreen = AdminAgentLibScreen; window.AdminBillingScreen = AdminBillingScreen; window.AdminComplianceScreen = AdminComplianceScreen; window.AdminHealthScreen = AdminHealthScreen; window.AdminAuditScreen = AdminAuditScreen; window.AdminTelephonyScreen = AdminTelephonyScreen;