import { createMemo, createResource, createSignal, For, Show } from 'solid-js'; import AdminShell from '~/components/AdminShell'; const API = '/api/gateway'; interface Approval { id: string; requestType?: string; type?: string; requestStatus?: string; status?: string; priority?: number; createdAt?: string; created_at?: string; requester?: { name?: string; email?: string }; requesterName?: string; requesterEmail?: string; requester_name?: string; requester_email?: string; } interface ApprovalRule { id: string; entityType: string; entity_type?: string; approverType: string; approver_type?: string; priority?: number; } const ENTITY_TYPE_OPTIONS = ['JOB_POST', 'COMPANY', 'LEAD', 'INVOICE']; const APPROVER_TYPE_OPTIONS = ['USER', 'ROLE']; type StatusTab = 'PENDING' | 'APPROVED' | 'REJECTED' | 'CHANGES_REQUESTED' | 'CANCELLED' | 'rules'; const STATUS_TABS: { key: StatusTab; label: string }[] = [ { key: 'PENDING', label: 'Pending' }, { key: 'APPROVED', label: 'Approved' }, { key: 'REJECTED', label: 'Rejected' }, { key: 'CHANGES_REQUESTED', label: 'Changes Requested' }, { key: 'CANCELLED', label: 'Cancelled' }, { key: 'rules', label: 'Rules' }, ]; async function fetchApprovals(): Promise { try { const res = await fetch(`${API}/api/admin/approvals`); if (!res.ok) throw new Error('Failed to load approvals'); const data = await res.json(); return Array.isArray(data) ? data : (data.approvals || []); } catch { return []; } } async function fetchRules(): Promise { try { const res = await fetch(`${API}/api/admin/approval-rules`); if (!res.ok) throw new Error('Failed to load rules'); const data = await res.json(); return Array.isArray(data) ? data : (data.rules || []); } catch { return []; } } function StatusBadge(props: { status: string }) { const s = (props.status || '').toUpperCase(); if (s === 'APPROVED') return APPROVED; if (s === 'REJECTED') return REJECTED; if (s === 'CHANGES_REQUESTED') return CHANGES REQUESTED; if (s === 'CANCELLED') return CANCELLED; return {props.status || 'PENDING'}; } export default function ApprovalPage() { const [activeTab, setActiveTab] = createSignal('PENDING'); const [approvals, { refetch: refetchApprovals }] = createResource(fetchApprovals); const [acting, setActing] = createSignal(''); const [approvalError, setApprovalError] = createSignal(''); const [rules, { refetch: refetchRules }] = createResource(fetchRules); const [showAddRule, setShowAddRule] = createSignal(false); const [newEntityType, setNewEntityType] = createSignal('JOB_POST'); const [newApproverType, setNewApproverType] = createSignal('USER'); const [newPriority, setNewPriority] = createSignal(1); const [ruleError, setRuleError] = createSignal(''); const [deletingRule, setDeletingRule] = createSignal(''); const [submittingRule, setSubmittingRule] = createSignal(false); const tabApprovals = createMemo(() => { const tab = activeTab(); if (tab === 'rules') return []; const list = approvals() ?? []; return list.filter((a) => { const s = (a.requestStatus || a.status || 'PENDING').toUpperCase(); return s === tab; }); }); // Count per status for badges const countFor = (status: string) => { const list = approvals() ?? []; if (status === 'rules') return (rules() ?? []).length; return list.filter((a) => (a.requestStatus || a.status || 'PENDING').toUpperCase() === status).length; }; const handleAction = async (id: string, newStatus: 'APPROVED' | 'REJECTED' | 'CHANGES_REQUESTED' | 'CANCELLED') => { try { setActing(`${id}-${newStatus}`); setApprovalError(''); const res = await fetch(`${API}/api/admin/approvals/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: newStatus }), }); if (!res.ok) throw new Error(`Failed to ${newStatus.toLowerCase()} approval`); refetchApprovals(); } catch (err: any) { setApprovalError(err.message || 'Action failed'); } finally { setActing(''); } }; const handleAddRule = async () => { try { setSubmittingRule(true); setRuleError(''); const res = await fetch(`${API}/api/admin/approval-rules`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ entityType: newEntityType(), approverType: newApproverType(), priority: newPriority(), }), }); if (!res.ok) throw new Error('Failed to create rule'); setShowAddRule(false); setNewEntityType('JOB_POST'); setNewApproverType('USER'); setNewPriority(1); refetchRules(); } catch (err: any) { setRuleError(err.message || 'Failed to create rule'); } finally { setSubmittingRule(false); } }; const handleDeleteRule = async (id: string) => { if (!confirm('Delete this approval rule?')) return; try { setDeletingRule(id); setRuleError(''); const res = await fetch(`${API}/api/admin/approval-rules/${id}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Failed to delete rule'); refetchRules(); } catch (err: any) { setRuleError(err.message || 'Failed to delete rule'); } finally { setDeletingRule(''); } }; return (

Approval Management

Review and manage approvals and approval rules.

{/* Status tabs */}
{STATUS_TABS.map((t) => { const count = countFor(t.key); return ( ); })}
{/* Approvals content */}
{approvalError()}
0}> {(item) => { const requesterName = item.requester?.name || item.requesterName || item.requester_name || '—'; const requesterEmail = item.requester?.email || item.requesterEmail || item.requester_email || ''; const status = (item.requestStatus || item.status || 'PENDING').toUpperCase(); const requestType = item.requestType || item.type || '—'; const submittedAt = item.createdAt || item.created_at; return ( ); }}
Requester Request Type Status Priority Submitted At Actions
Loading...
Failed to load approvals.
No {activeTab().toLowerCase().replace('_', ' ')} approvals.
{requesterName}
{requesterEmail}
{requestType} {item.priority ?? '—'} {submittedAt ? new Date(submittedAt).toLocaleString() : '—'}
{/* Rules tab */}
{ruleError()}

New Approval Rule

setNewPriority(parseInt(e.currentTarget.value) || 1)} />
0}> {(rule) => ( )}
Entity Type Approver Type Priority Actions
Loading...
Failed to load rules.
No approval rules found.
{rule.entityType || rule.entity_type || '—'} {rule.approverType || rule.approver_type || '—'} {rule.priority ?? '—'}
); }