// Platform-wide admin console. Only rendered for users whose /me payload has // is_super_admin — the API enforces the same check, this is just the UI half. // Every table sits in an overflow-x wrapper and every header row wraps, so the // screen works at 390px without a horizontal page scroll. function SAStat({ label, value, sub, tone }) { return (
{label}
{value}
{sub &&
{sub}
}
); } function SATh({ children, w }) { return ( {children} ); } function SATd({ children, strong }) { return ( {children} ); } function SuperAdmin() { const [tab, setTab] = React.useState('overview'); const [overview, setOverview] = React.useState(null); const [workspaces, setWorkspaces] = React.useState([]); const [users, setUsers] = React.useState([]); const [activity, setActivity] = React.useState([]); const [errorLogs, setErrorLogs] = React.useState([]); const [errSummary, setErrSummary] = React.useState(null); const [errFilter, setErrFilter] = React.useState(''); // type filter const [errQuery, setErrQuery] = React.useState(''); // search text const [errShowResolved, setErrShowResolved] = React.useState(false); const [errBusy, setErrBusy] = React.useState(null); // id being resolved const [explaining, setExplaining] = React.useState(null); // id being explained const [explanations, setExplanations] = React.useState({}); // id -> text const [loading, setLoading] = React.useState(true); const [err, setErr] = React.useState(''); const [busyUser, setBusyUser] = React.useState(null); const [expandedErr, setExpandedErr] = React.useState(null); const [members, setMembers] = React.useState([]); const [expandedMember, setExpandedMember] = React.useState(null); const [busyMember, setBusyMember] = React.useState(null); const [memberSearch, setMemberSearch] = React.useState(''); const [health, setHealth] = React.useState(null); const [healthLoading, setHealthLoading] = React.useState(false); const [analytics, setAnalytics] = React.useState(null); const [security, setSecurity] = React.useState(null); const [bcast, setBcast] = React.useState({ title: '', body: '' }); const [bcastMsg, setBcastMsg] = React.useState(''); React.useEffect(() => { if (tab === 'analytics' && !analytics && window.API?.superAdminAnalytics) window.API.superAdminAnalytics().then(r => setAnalytics(r?.data||null)).catch(()=>{}); if (tab === 'security' && !security && window.API?.superAdminSecurity) window.API.superAdminSecurity().then(r => setSecurity(r?.data||null)).catch(()=>{}); }, [tab]); async function sendBroadcast() { if (!bcast.title.trim() || !window.API?.superAdminBroadcast) return; setBcastMsg('Sending…'); try { const r = await window.API.superAdminBroadcast(bcast); setBcastMsg('Sent to ' + (r?.data?.sent ?? 0) + ' members.'); setBcast({ title: '', body: '' }); } catch { setBcastMsg('Failed to send.'); } } async function impersonate(u) { if (!window.confirm('Log in as ' + u.name + '? You will be signed in as this user.')) return; try { const r = await window.API.superAdminImpersonate(u.id); if (r?.data?.token) { localStorage.setItem('tf_auth_token', r.data.token); localStorage.setItem('tf_auth_user', JSON.stringify(r.data.user)); window.location.reload(); } } catch { alert('Could not impersonate.'); } } async function toggleUserStatus(u, status) { try { await window.API.superAdminSetUserStatus(u.id, status); alert('User ' + (status==='inactive'?'suspended':'reactivated') + '.'); } catch { alert('Failed.'); } } async function runHealthCheck() { if (!window.API || !window.API.superAdminHealth) return; setHealthLoading(true); try { const r = await window.API.superAdminHealth(); setHealth(r?.data || r || null); } catch (e) { setHealth({ ok: false, checks: [{ name: 'request', status: 'fail', detail: e?.message || 'Could not run health check' }] }); } finally { setHealthLoading(false); } } // Reload the error list applying the current filter/search/resolved toggle, // plus the summary counts, without reloading the whole platform. const loadErrors = React.useCallback(async () => { if (!window.API || !window.API.getErrorLogs) return; try { const q = {}; if (errFilter) q.type = errFilter; if (errQuery) q.q = errQuery; if (errShowResolved) q.resolved = 1; const [el, s] = await Promise.all([ window.API.getErrorLogs(Object.keys(q).length ? q : undefined), window.API.getErrorLogSummary ? window.API.getErrorLogSummary() : Promise.resolve({ data: null }), ]); setErrorLogs(el?.data || []); setErrSummary(s?.data || null); } catch {} }, [errFilter, errQuery, errShowResolved]); // Re-fetch errors when a filter changes — but only while the Errors tab is // open, and debounced, so typing in search doesn't fire a request per // keystroke (that burst was exhausting the DB connection limit). The initial // load is handled by the main platform fetch below, so we skip first mount. const errFirst = React.useRef(true); React.useEffect(() => { if (tab !== 'errors') return; if (errFirst.current) { errFirst.current = false; return; } const t = setTimeout(() => { loadErrors(); }, 400); return () => clearTimeout(t); }, [loadErrors, tab]); async function resolveError(id, status) { if (!window.API || !window.API.setErrorStatus) return; setErrBusy(id); try { await window.API.setErrorStatus(id, status); await loadErrors(); } catch {} finally { setErrBusy(null); } } async function explainError(id) { if (!window.API || !window.API.explainError) return; setExplaining(id); try { const r = await window.API.explainError(id); setExplanations(prev => ({ ...prev, [id]: (r && (r.explanation || (r.data && r.data.explanation))) || 'No explanation returned.' })); } catch { setExplanations(prev => ({ ...prev, [id]: 'Could not get an explanation.' })); } finally { setExplaining(null); } } React.useEffect(() => { let alive = true; (async () => { setLoading(true); setErr(''); try { // Load SEQUENTIALLY, not all-at-once. The shared MySQL has a low // connection limit; firing 7 heavy super-admin queries in parallel // exhausted it and every one returned a 2002 "Operation not permitted" // 500. One-at-a-time is a hair slower but reliable. Each is guarded so // one failure doesn't abort the rest. const safe = async (fn) => { try { return await fn(); } catch { return null; } }; const o = await safe(() => window.API.superAdminOverview()); if (!alive) return; setOverview(o?.data || null); const w = await safe(() => window.API.superAdminWorkspaces()); if (!alive) return; setWorkspaces(w?.data || []); const u = await safe(() => window.API.superAdminUsers()); if (!alive) return; setUsers(u?.data || []); const a = await safe(() => window.API.superAdminActivity()); if (!alive) return; setActivity(a?.data || []); const el = await safe(() => window.API.getErrorLogs ? window.API.getErrorLogs() : { data: [] }); if (!alive) return; setErrorLogs(el?.data || []); const mem = await safe(() => window.API.superAdminMembers ? window.API.superAdminMembers() : { data: [] }); if (!alive) return; setMembers(mem?.data || []); const s = await safe(() => window.API.getErrorLogSummary ? window.API.getErrorLogSummary() : { data: null }); if (!alive) return; setErrSummary(s?.data || null); } catch (e) { if (alive) setErr(e?.message || 'Could not load platform data.'); } finally { if (alive) setLoading(false); } })(); return () => { alive = false; }; }, []); async function toggleSuperAdmin(u) { const next = !u.is_super_admin; if (next && !window.confirm(`Make ${u.email} a super admin? They will see every workspace on the platform.`)) return; if (!next && !window.confirm(`Remove super admin from ${u.email}?`)) return; setBusyUser(u.id); try { await window.API.setSuperAdmin(u.id, next); setUsers(prev => prev.map(x => x.id === u.id ? { ...x, is_super_admin: next ? 1 : 0 } : x)); } catch (e) { alert(e?.message || 'Could not change super admin.'); } finally { setBusyUser(null); } } const money = n => '₨' + Number(n || 0).toLocaleString('en-PK', { maximumFractionDigits: 0 }); const when = d => d ? new Date(d.replace(' ', 'T')).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) : '—'; const whenTime = d => d ? new Date(d.replace(' ', 'T')).toLocaleString('en-GB', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }) : '—'; // Same controllable tab set as the per-workspace editor in Settings.jsx. const SA_TABS = [ ['dashboard', 'Dashboard'], ['tasks', 'Tasks'], ['projects', 'Projects'], ['calendar', 'Calendar'], ['crm', 'Client CRM'], ['pipeline', 'Sales pipeline'], ['portal', 'Client portal'], ['content', 'Content'], ['ads', 'Meta Ads'], ['email', 'Email'], ['finance', 'Finance'], ['reports', 'Reports'], ['docs', 'Docs & Files'], ['files', 'Files'], ['team', 'Team'], ['chat', 'Team Chat'], ['automations', 'Automations'], ['activity', 'Activity Log'], ['integrations', 'Integrations'], ['workspace', 'Workspace'], ]; const SA_MEMBER_DEFAULT = ['dashboard', 'tasks', 'projects', 'calendar', 'chat']; // Which tabs a member currently sees: explicit hidden_tabs wins, else role default. function visibleTabsFor(m) { const hidden = Array.isArray(m.hidden_tabs) ? m.hidden_tabs : null; if (hidden) return SA_TABS.map(t => t[0]).filter(id => hidden.indexOf(id) === -1); if ((m.role || 'member') === 'member') return SA_MEMBER_DEFAULT.slice(); return SA_TABS.map(t => t[0]); } async function toggleMemberTab(m, tabId) { if (!window.API || !window.API.setMemberTabs) return; const current = members.find(x => x.id === m.id) || m; const visible = new Set(visibleTabsFor(current)); if (visible.has(tabId)) visible.delete(tabId); else visible.add(tabId); const hidden = SA_TABS.map(t => t[0]).filter(id => !visible.has(id)); const prev = members; setMembers(members.map(x => x.id === m.id ? { ...x, hidden_tabs: hidden } : x)); setBusyMember(m.id); try { await window.API.setMemberTabs(m.id, hidden); } catch (e) { setMembers(prev); alert(e?.message || 'Could not update tab access.'); } finally { setBusyMember(null); } } // Colour-code the status pill so 500s jump out. const statusTone = s => s >= 500 ? 'var(--red-600)' : s === 403 || s === 401 ? 'var(--amber-600)' : 'var(--text-muted)'; const typeLabel = t => ({ server_500: 'Server error', permission_denied: 'Permission', api_failed: 'API failed', client_error: 'Client', user_reported: '🙋 User reported' }[t] || t); const TABS = [ { id: 'overview', label: 'Overview', icon: 'layout-dashboard' }, { id: 'analytics', label: 'Analytics', icon: 'chart-line' }, { id: 'workspaces', label: 'Workspaces', icon: 'briefcase' }, { id: 'users', label: 'Users', icon: 'users' }, { id: 'broadcast', label: 'Broadcast', icon: 'megaphone' }, { id: 'security', label: 'Security', icon: 'shield-alert' }, { id: 'activity', label: 'Activity', icon: 'activity' }, { id: 'tabaccess', label: 'Tab Access', icon: 'sliders-horizontal' }, { id: 'health', label: 'Health', icon: 'heart-pulse' }, { id: 'errors', label: 'Errors', icon: 'triangle-alert' }, ]; // Auto-run the health check the first time the Health tab is opened. React.useEffect(() => { if (tab === 'health' && !health && !healthLoading) runHealthCheck(); }, [tab]); return (
{/* Header — wraps on narrow screens */}

Super Admin

Platform-wide view across every workspace

{/* Tabs — scroll sideways on mobile instead of pushing the page */}
{TABS.map(t => ( ))}
{loading &&

Loading platform data…

} {err && !loading && (
{err}
)} {/* ---------- OVERVIEW ---------- */} {!loading && !err && tab === 'overview' && overview && ( <>
)} {/* ---------- ANALYTICS ---------- */} {!loading && !err && tab === 'analytics' && ( analytics ? (
Signups — last 7 days
{analytics.signups_7d.map((d,i) => { const max = Math.max(1, ...analytics.signups_7d.map(x=>x.count)); return (
{d.date.slice(5)}
); })}
) :

Loading analytics…

)} {/* ---------- BROADCAST ---------- */} {!loading && !err && tab === 'broadcast' && (
Broadcast to all workspaces

Sends an in-app notification to every active member across every workspace.

setBcast(b => ({ ...b, title: e.target.value }))} placeholder="Title (e.g. Scheduled maintenance tonight)" style={{ width: '100%', height: 38, padding: '0 12px', border: '1px solid var(--border-default)', borderRadius: 'var(--radius-md)', fontSize: 'var(--text-sm)', fontFamily: 'var(--font-sans)', outline: 'none', boxSizing: 'border-box', marginBottom: 10 }} />