feat(admin): modernize verification, approvals, and onboarding ui

This commit is contained in:
Ashwin Kumar 2026-03-26 00:16:52 +01:00
parent 1b70f40e40
commit 52adb3ad05
3 changed files with 594 additions and 577 deletions

View file

@ -676,396 +676,375 @@ export default function ApprovalPage() {
return (
<AdminShell>
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
{/* ── Page header ── */}
<div class="bg-white border-b border-gray-200 px-6 py-4">
<h1 class="text-xl font-semibold text-gray-900">Approval Management</h1>
<p class="text-sm text-gray-500 mt-0.5">Review, approve, reject and configure approval workflows.</p>
</div>
{/* ── Status tabs ── */}
<div class="bg-white border-b border-gray-200 px-6 flex items-center gap-6 sticky top-0 z-10 overflow-x-auto">
<For each={STATUS_TABS}>
{(t) => {
const count = countFor(t.key);
return (
<div class="p-6 flex-1 max-w-[1600px] mx-auto w-full">
{/* Header */}
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between mb-8">
<div>
<h1 class="text-[32px] font-bold text-[#050026] leading-tight">Approval Management</h1>
<p class="text-[14px] text-[#8087a0] mt-1">Review, approve, reject and configure approval workflows.</p>
</div>
<div class="flex items-center gap-3">
<button
type="button"
onClick={() => {
setActiveTab(t.key);
setShowDetail(false);
setCurrentPage(1);
setDocRequestedOnly(false);
}}
class={`flex shrink-0 items-center gap-1.5 py-3 border-b-2 text-sm font-medium transition-colors ${
activeTab() === t.key
? 'border-orange-500 text-orange-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
class="inline-flex h-11 items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-6 text-[14px] font-semibold text-[#050026] transition-colors hover:bg-[#f8f9fc]"
onClick={() => { refetchApprovals(); refetchSnapshot(); }}
>
{t.label}
<Show when={!approvals.loading && count > 0}>
<span class={`inline-flex items-center justify-center min-w-[18px] h-[18px] rounded-full text-[10px] font-bold px-1 ${
activeTab() === t.key ? 'bg-orange-500 text-white' : 'bg-slate-200 text-slate-600'
}`}>
{count}
</span>
</Show>
Refresh Data
</button>
);
}}
</For>
</div>
<div class="flex-1 p-6">
<Show when={actionError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{actionError()}</div>
</Show>
<Show when={approvals.error}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{String((approvals.error as any)?.message || approvals.error)}</div>
</Show>
<Show when={!approvals.loading && !snapshot.loading}>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:12px;background:#f8fafc;border-color:#e2e8f0">
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap">
<div style="display:flex;gap:8px;flex-wrap:wrap">
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#e0f2fe;color:#075985;border-color:#bae6fd">Pending Jobs: {summary().jobs}</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#ecfeff;color:#155e75;border-color:#a5f3fc">Pending Requirements: {summary().requirements}</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#eef2ff;color:#3730a3;border-color:#c7d2fe">Pending Profiles: {summary().profilePending}</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fef3c7;color:#92400e;border-color:#fde68a">Total Pending: {summary().totalPending}</span>
</div>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" type="button" onClick={() => { refetchApprovals(); refetchSnapshot(); }}>Refresh</button>
</div>
<Show when={summary().totalPending === 0}>
<p style="margin:10px 0 0;color:#64748b;font-size:13px">
No pending approval requests are available right now. New submissions will appear here automatically.
</p>
</Show>
<Show when={summary().backendMode === 'RUST' && activeTab() !== 'PENDING'}>
<p style="margin:8px 0 0;color:#64748b;font-size:12px">
Current backend feed returns pending requests only for Approval Management.
</p>
</Show>
</div>
</Show>
{/* ── Approval List / Detail ── */}
<Show when={activeTab() !== 'rules'}>
<Show when={!showDetail()}>
{/* Filter bar */}
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:12px;display:flex;gap:10px;flex-wrap:wrap;align-items:center">
<input
type="text"
placeholder="Search requester, email, type or ID..."
value={search()}
onInput={(e) => { setSearch(e.currentTarget.value); setCurrentPage(1); }}
style="border:1px solid #cbd5e1;border-radius:6px;padding:7px 12px;font-size:14px;width:280px;outline:none"
/>
<select
value={requestFilter()}
onChange={(e) => { setRequestFilter(e.currentTarget.value); setCurrentPage(1); }}
style="border:1px solid #cbd5e1;border-radius:6px;padding:7px 12px;font-size:14px;background:#fff;outline:none"
>
<For each={REQUEST_FILTERS}>{(r) => <option value={r}>{r}</option>}</For>
</select>
<label style="display:inline-flex;align-items:center;gap:6px;font-size:13px;color:#334155;border:1px solid #cbd5e1;border-radius:8px;padding:7px 10px;background:#fff">
<input
type="checkbox"
checked={docRequestedOnly()}
onChange={(e) => { setDocRequestedOnly(e.currentTarget.checked); setCurrentPage(1); }}
/>
Docs Requested Only
</label>
<span style="font-size:12px;color:#64748b;margin-left:auto">{filteredApprovals().length} record{filteredApprovals().length !== 1 ? 's' : ''}</span>
</div>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Requester</th>
<th>Type</th>
<th>Request Category</th>
<th>Document Request</th>
<th>Status</th>
<th>Submitted</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={approvals.loading}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#64748b">Loading approvals...</td></tr>
</Show>
<Show when={!approvals.loading && paginatedApprovals().length === 0}>
<tr><td colspan="7" style="text-align:center;padding:32px;color:#94a3b8">{activeTab() === 'PENDING' ? 'No pending approvals right now.' : `No ${activeTab().toLowerCase().replace('_', ' ')} approvals available in this feed.`}</td></tr>
</Show>
<Show when={!approvals.loading && paginatedApprovals().length > 0}>
<For each={paginatedApprovals()}>
{(item) => {
const status = statusValue(item);
const docRemark = latestDocumentRequest(item);
const isDocRequest = !!docRemark && status === 'CHANGES_REQUESTED';
const dest = managementDestination(item._roleType || 'UNKNOWN');
const isActing = acting().startsWith(item.id);
return (
<tr>
<td>
<div style="font-weight:600;color:#0f172a">{requesterName(item)}</div>
<div style="font-size:11px;color:#64748b">{requesterEmail(item) || item.requesterId?.slice(0, 12)}</div>
</td>
<td>
<RoleTypeBadge type={item._roleType} />
</td>
<td>
<div style="font-size:13px;color:#475569;font-weight:500">{item._typeLabel || '—'}</div>
<Show when={item._parsedReason?.templateId}>
<div style="font-size:11px;color:#94a3b8">{item._parsedReason?.templateId}</div>
</Show>
</td>
<td>
<Show when={docRemark} fallback={<span style="font-size:12px;color:#94a3b8"></span>}>
<div style="display:flex;flex-direction:column;gap:4px;max-width:280px">
<span style="display:inline-flex;align-self:flex-start;font-size:10px;font-weight:700;color:#1d4ed8;background:#eff6ff;border:1px solid #bfdbfe;border-radius:999px;padding:2px 8px">
Docs Requested
</span>
<span style="font-size:12px;color:#0f172a;line-height:1.3">{docRemark!.comment}</span>
<Show when={(docRemark!.fields || []).length > 0}>
<span style="font-size:11px;color:#475569">Needed: {(docRemark!.fields || []).join(', ')}</span>
</Show>
<Show when={actionError()}>
<div class="mb-6 rounded-xl border border-red-200 bg-red-50 p-4 text-[14px] font-medium text-red-700">{actionError()}</div>
</Show>
<Show when={approvals.error}>
<div class="mb-6 rounded-xl border border-red-200 bg-red-50 p-4 text-[14px] font-medium text-red-700">{String((approvals.error as any)?.message || approvals.error)}</div>
</Show>
{/* Metric Summary */}
<Show when={!approvals.loading && !snapshot.loading}>
<div class="mb-6 rounded-[20px] bg-white p-5 border border-[#e2e6ee] shadow-sm flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<div class="flex items-center flex-wrap gap-3">
<span class="inline-flex items-center rounded-lg bg-[#e0f2fe] px-3 py-1 text-[13px] font-bold text-[#0369a1] border border-[#bae6fd]">Pending Jobs: {summary().jobs}</span>
<span class="inline-flex items-center rounded-lg bg-[#ecfeff] px-3 py-1 text-[13px] font-bold text-[#0e7490] border border-[#a5f3fc]">Pending Requirements: {summary().requirements}</span>
<span class="inline-flex items-center rounded-lg bg-[#eef2ff] px-3 py-1 text-[13px] font-bold text-[#4338ca] border border-[#c7d2fe]">Pending Profiles: {summary().profilePending}</span>
<span class="inline-flex items-center rounded-lg bg-[#fef3c7] px-3 py-1 text-[13px] font-bold text-[#b45309] border border-[#fde68a]">Total Pending: {summary().totalPending}</span>
</div>
<div class="text-[13px] text-[#8087a0]">
{summary().backendMode === 'RUST' && activeTab() !== 'PENDING' ? 'Backend returns pending only.' : ''}
</div>
</div>
</Show>
<section class="rounded-[24px] border border-[#e2e6ee] bg-[#f7f7f8] p-1.5 h-full">
<div class="rounded-[20px] bg-white p-5">
{/* Tabs */}
<div class="flex gap-6 mb-6 border-b border-[#e2e6ee] overflow-x-auto pb-1">
<For each={STATUS_TABS}>
{(t) => {
const count = countFor(t.key);
return (
<button
type="button"
onClick={() => {
setActiveTab(t.key);
setShowDetail(false);
setCurrentPage(1);
setDocRequestedOnly(false);
}}
class={`flex shrink-0 items-center justify-center gap-2 pb-3 text-[14px] font-bold transition-colors border-b-2 whitespace-nowrap ${
activeTab() === t.key
? 'border-[#050026] text-[#050026]'
: 'border-transparent text-[#8087a0] hover:text-[#050026]'
}`}
>
{t.label}
<Show when={!approvals.loading && count > 0}>
<span class={`inline-flex items-center justify-center min-w-[20px] h-[20px] rounded-full text-[11px] font-bold px-1.5 ${
activeTab() === t.key ? 'bg-[#050026] text-white' : 'bg-[#f7f7f8] border border-[#e2e6ee] text-[#050026]'
}`}>
{count}
</span>
</Show>
</button>
);
}}
</For>
</div>
{/* Detail view vs List View */}
<Show when={activeTab() !== 'rules'}>
<Show when={!showDetail()}>
{/* Filters Row */}
<div class="flex flex-col gap-4 md:flex-row items-center justify-between mb-6">
<div class="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
<div class="relative w-full md:w-[320px]">
<div class="absolute inset-y-0 left-4 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-[#a0aabf]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<input
type="text"
placeholder="Search requester, email..."
value={search()}
onInput={(e) => { setSearch(e.currentTarget.value); setCurrentPage(1); }}
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-[#f9fafb] pl-11 pr-4 text-[14px] text-[#050026] outline-none transition-colors focus:border-[#050026] focus:bg-white"
/>
</div>
<select
value={requestFilter()}
onChange={(e) => { setRequestFilter(e.currentTarget.value); setCurrentPage(1); }}
class="h-11 w-full md:w-[200px] rounded-xl border border-[#d9dde6] bg-[#f9fafb] px-4 text-[14px] text-[#050026] outline-none transition-colors focus:border-[#050026] focus:bg-white"
>
<For each={REQUEST_FILTERS}>{(r) => <option value={r}>{r}</option>}</For>
</select>
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={docRequestedOnly()}
onChange={(e) => { setDocRequestedOnly(e.currentTarget.checked); setCurrentPage(1); }}
class="h-4 w-4 rounded border-[#d9dde6] text-[#050026]"
/>
<span class="text-[14px] font-semibold text-[#050026]">Docs Requested Only</span>
</label>
</div>
<div class="text-[13px] font-medium text-[#8087a0]">
Showing {Math.min((currentPage() - 1) * perPage + 1, filteredApprovals().length)}{Math.min(currentPage() * perPage, filteredApprovals().length)} of {filteredApprovals().length}
</div>
</div>
{/* Table */}
<div class="overflow-x-auto">
<table class="w-full min-w-[1000px] border-collapse">
<thead>
<tr class="bg-[#050026] text-left text-white">
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider rounded-tl-xl whitespace-nowrap">REQUESTER</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">TYPE</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">CATEGORY</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">DOCUMENT REQUEST</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">STATUS</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">SUBMITTED</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider text-right rounded-tr-xl whitespace-nowrap">ACTIONS</th>
</tr>
</thead>
<tbody>
<Show when={approvals.loading}>
<tr><td colspan="7" class="text-center py-12 text-[#8087a0] text-[14px]">Loading approvals...</td></tr>
</Show>
<Show when={!approvals.loading && paginatedApprovals().length === 0}>
<tr><td colspan="7" class="text-center py-12 text-[#8087a0] text-[14px]">{activeTab() === 'PENDING' ? 'No pending approvals right now.' : `No ${activeTab().toLowerCase().replace('_', ' ')} approvals available.`}</td></tr>
</Show>
<Show when={!approvals.loading && paginatedApprovals().length > 0}>
<For each={paginatedApprovals()}>
{(item) => {
const status = statusValue(item);
const docRemark = latestDocumentRequest(item);
const isDocRequest = !!docRemark && status === 'CHANGES_REQUESTED';
const dest = managementDestination(item._roleType || 'UNKNOWN');
const isActing = acting().startsWith(item.id);
return (
<tr class="border-b border-[#e2e6ee] bg-white transition-colors hover:bg-[#f8f9fc]">
<td class="px-6 py-4">
<div class="text-[14px] font-bold text-[#050026]">{requesterName(item)}</div>
<div class="text-[12px] text-[#64748b]">{requesterEmail(item) || item.requesterId?.slice(0, 12)}</div>
</td>
<td class="px-6 py-4">
<RoleTypeBadge type={item._roleType} />
</td>
<td class="px-6 py-4">
<div class="text-[13px] font-bold text-[#475569]">{item._typeLabel || '—'}</div>
<Show when={item._parsedReason?.templateId}>
<div class="text-[11px] text-[#94a3b8]">{item._parsedReason?.templateId}</div>
</Show>
</td>
<td class="px-6 py-4">
<Show when={docRemark} fallback={<span class="text-[12px] text-[#94a3b8]"></span>}>
<div class="flex flex-col gap-1 max-w-[250px]">
<span class="inline-flex self-start text-[10px] font-bold text-[#1d4ed8] bg-[#eff6ff] border border-[#bfdbfe] rounded-full px-2 py-0.5">Docs Requested</span>
<span class="text-[12px] text-[#050026] leading-tight truncate" title={docRemark!.comment}>{docRemark!.comment}</span>
<Show when={(docRemark!.fields || []).length > 0}>
<span class="text-[11px] text-[#64748b] truncate" title={(docRemark!.fields || []).join(', ')}>Needed: {(docRemark!.fields || []).join(', ')}</span>
</Show>
</div>
</Show>
</td>
<td class="px-6 py-4">
<StatusBadge status={status} isDocRequest={isDocRequest} />
</td>
<td class="px-6 py-4 text-[13px] text-[#475569] whitespace-nowrap">
{(item.createdAt || item.created_at) ? new Date((item.createdAt || item.created_at)!).toLocaleDateString() : '—'}
</td>
<td class="px-6 py-4">
<div class="flex items-center justify-end gap-2">
<button
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors"
type="button"
title="View Request"
onClick={() => { setSelectedApproval(item); setShowDetail(true); }}
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
</button>
<Show when={status === 'PENDING'}>
<button class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#bfdbfe] bg-[#eff6ff] text-[#1d4ed8] hover:bg-[#dbeafe] transition-colors" type="button" title="Request Documents" disabled={isActing} onClick={() => handleRequestMoreDocuments(item.id)}>📄</button>
<button class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#fed7aa] bg-[#fff7ed] text-[#c2410c] hover:bg-[#ffedd5] transition-colors" type="button" title="Request Changes" disabled={isActing} onClick={() => handleRequestChanges(item.id)}></button>
<button class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#bbf7d0] bg-[#f0fdf4] text-[#15803d] hover:bg-[#dcfce7] transition-colors" type="button" title="Approve" disabled={isActing} onClick={() => handleApprove(item.id)}></button>
<button class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#fecaca] bg-[#fef2f2] text-[#b91c1c] hover:bg-[#fee2e2] transition-colors" type="button" title="Reject" disabled={isActing} onClick={() => handleReject(item.id)}></button>
</Show>
<Show when={status === 'CHANGES_REQUESTED'}>
<button class="inline-flex h-8 items-center justify-center rounded-lg border border-[#bfdbfe] bg-[#eff6ff] text-[12px] font-bold text-[#1d4ed8] px-3 hover:bg-[#dbeafe] transition-colors whitespace-nowrap" type="button" title="Update requested documents" disabled={isActing} onClick={() => handleRequestMoreDocuments(item.id)}>
Update Docs Request
</button>
</Show>
<Show when={status === 'APPROVED'}>
<A href={dest.href} class="flex h-8 items-center justify-center rounded-lg border border-[#bbf7d0] bg-[#f0fdf4] px-3 font-semibold text-[12px] text-[#15803d] hover:bg-[#dcfce7] transition-colors whitespace-nowrap" title={`Open ${dest.label}`}>
{dest.label.replace(' Management', '')}
</A>
</Show>
</div>
</td>
</tr>
);
}}
</For>
</Show>
</tbody>
</table>
</div>
{/* Pagination */}
<div class="mt-6 flex items-center justify-between border-t border-[#e2e6ee] pt-4">
<span class="text-[13px] font-medium text-[#8087a0]">Page {currentPage()} of {totalPages()}</span>
<div class="flex items-center gap-2">
<button
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
disabled={currentPage() <= 1}
class="flex h-9 w-9 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg>
</button>
<button
onClick={() => setCurrentPage((p) => Math.min(totalPages(), p + 1))}
disabled={currentPage() >= totalPages()}
class="flex h-9 w-9 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
</button>
</div>
</div>
</Show>
{/* ── Inline Detail Panel ── */}
<Show when={showDetail()}>
<ApprovalDetailPanel
approval={selectedApproval()}
acting={acting()}
onBack={() => setShowDetail(false)}
onApprove={handleApprove}
onReject={handleReject}
onRequestChanges={handleRequestChanges}
onRequestMoreDocuments={handleRequestMoreDocuments}
/>
</Show>
</Show>
{/* ── Rules Tab ── */}
<Show when={activeTab() === 'rules'}>
<Show when={ruleError()}>
<div class="mb-4 rounded-xl border border-red-200 bg-red-50 p-4 text-[14px] font-medium text-red-700">{ruleError()}</div>
</Show>
<div class="mb-6 flex justify-end">
<button class="inline-flex h-11 items-center justify-center rounded-xl bg-[#050026] px-6 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]" onClick={() => setShowAddRule((v) => !v)}>
{showAddRule() ? 'Cancel' : '+ Add Rule'}
</button>
</div>
<Show when={showAddRule()}>
<div class="rounded-xl border border-[#e2e6ee] bg-[#f8f9fc] p-6 mb-6">
<h3 class="text-[16px] font-bold text-[#050026] mb-4">New Approval Rule</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="flex flex-col gap-1.5">
<label class="text-[13px] font-bold text-[#050026]">Rule Name <span class="text-red-500">*</span></label>
<input
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-white px-4 text-[14px] text-[#050026] outline-none transition-colors focus:border-[#050026]"
value={newRuleName()}
onInput={(e) => setNewRuleName(e.currentTarget.value)}
placeholder="e.g. Profile Approval by Admin"
/>
</div>
<div class="flex flex-col gap-1.5">
<label class="text-[13px] font-bold text-[#050026]">Entity Type</label>
<select
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-white px-4 text-[14px] text-[#050026] outline-none transition-colors focus:border-[#050026]"
value={newEntityType()}
onChange={(e) => setNewEntityType(e.currentTarget.value)}
>
<For each={ENTITY_TYPE_OPTIONS}>{(et) => <option value={et}>{et}</option>}</For>
</select>
</div>
<div class="flex flex-col gap-1.5">
<label class="text-[13px] font-bold text-[#050026]">Approver Type</label>
<select
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-white px-4 text-[14px] text-[#050026] outline-none transition-colors focus:border-[#050026]"
value={newApproverType()}
onChange={(e) => setNewApproverType(e.currentTarget.value)}
>
<For each={APPROVER_TYPE_OPTIONS}>{(at) => <option value={at}>{at}</option>}</For>
</select>
</div>
<div class="flex flex-col gap-1.5">
<label class="text-[13px] font-bold text-[#050026]">Priority</label>
<input
class="h-11 w-full rounded-xl border border-[#d9dde6] bg-white px-4 text-[14px] text-[#050026] outline-none transition-colors focus:border-[#050026]"
type="number"
min="1"
value={newPriority()}
onInput={(e) => setNewPriority(parseInt(e.currentTarget.value, 10) || 1)}
/>
</div>
</div>
<div class="mt-6 flex gap-3">
<button class="inline-flex h-11 items-center justify-center rounded-xl bg-[#050026] px-6 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]" disabled={submittingRule()} onClick={handleAddRule}>
{submittingRule() ? 'Saving...' : 'Save Rule'}
</button>
<button class="inline-flex h-11 items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-6 text-[14px] font-semibold text-[#050026] transition-colors hover:bg-[#f8f9fc]" onClick={() => setShowAddRule(false)}>
Cancel
</button>
</div>
</div>
</Show>
<div class="overflow-x-auto border border-[#e2e6ee] rounded-xl">
<table class="w-full text-sm border-collapse">
<thead>
<tr class="bg-[#f8f9fc] text-left text-[#64748b]">
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider">Rule Name</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider">Entity Type</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider">Approver Type</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider">Priority</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={rules.loading}>
<tr><td colspan="5" class="py-12 text-center text-[#8087a0]">Loading...</td></tr>
</Show>
<Show when={!rules.loading && (rules()?.length ?? 0) === 0}>
<tr><td colspan="5" class="py-12 text-center text-[#8087a0]">No approval rules configured yet.</td></tr>
</Show>
<Show when={!rules.loading && (rules()?.length ?? 0) > 0}>
<For each={rules()!}>
{(rule) => (
<tr class="border-t border-[#e2e6ee] bg-white">
<td class="px-6 py-4 font-bold text-[#050026]">{rule.name || '—'}</td>
<td class="px-6 py-4 text-[#475569]">{rule.entityType || rule.entity_type || '—'}</td>
<td class="px-6 py-4 text-[#475569]">{rule.approverType || rule.approver_type || '—'}</td>
<td class="px-6 py-4 font-bold text-[#475569]">{rule.priority ?? '—'}</td>
<td class="px-6 py-4">
<div class="flex items-center justify-end gap-1">
<button
class="inline-flex h-8 items-center justify-center rounded-lg bg-red-50 text-[12px] font-bold text-red-600 px-3 hover:bg-red-100 transition-colors"
disabled={deletingRule() === rule.id}
onClick={() => handleDeleteRule(rule.id)}
>
{deletingRule() === rule.id ? '...' : 'Delete'}
</button>
</div>
</Show>
</td>
<td>
<StatusBadge status={status} isDocRequest={isDocRequest} />
</td>
<td style="color:#475569;white-space:nowrap">
{(item.createdAt || item.created_at) ? new Date((item.createdAt || item.created_at)!).toLocaleDateString() : '—'}
</td>
<td>
<div class="flex items-center justify-end gap-1">
{/* View detail */}
<button
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
type="button"
title="View Request"
style="font-size:12px;padding:4px 10px"
onClick={() => { setSelectedApproval(item); setShowDetail(true); }}
>
View
</button>
<Show when={item._viewHref}>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={item._viewHref!} style="font-size:12px;padding:4px 10px" title="Open full request page">
Full Request
</A>
</Show>
<Show when={item._supportsSubmissionView}>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/approval/${item.id}`} style="font-size:12px;padding:4px 10px" title="Open full profile review">
Profile Review
</A>
</Show>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
</Show>
<Show when={status === 'PENDING'}>
{/* Request More Documents */}
<button
class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm"
type="button"
title="Request More Documents"
disabled={isActing}
style="background:#eff6ff;color:#1d4ed8;border-color:#bfdbfe"
onClick={() => handleRequestMoreDocuments(item.id)}
>
📄
</button>
{/* Request Changes */}
<button
class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm"
type="button"
title="Request Changes"
disabled={isActing}
style="background:#fff7ed;color:#c2410c;border-color:#fed7aa"
onClick={() => handleRequestChanges(item.id)}
>
</button>
{/* Approve */}
<button
class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm"
type="button"
title="Approve"
disabled={isActing}
style="background:#f0fdf4;color:#15803d;border-color:#bbf7d0"
onClick={() => handleApprove(item.id)}
>
</button>
{/* Reject */}
<button
class="rounded p-1.5 text-red-500 hover:bg-red-50 hover:text-red-700 text-sm"
type="button"
title="Reject"
disabled={isActing}
onClick={() => handleReject(item.id)}
>
</button>
</Show>
<Show when={status === 'CHANGES_REQUESTED'}>
<button
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
type="button"
title="Update requested documents"
style="font-size:12px;padding:4px 10px;background:#eff6ff;color:#1d4ed8;border-color:#bfdbfe"
disabled={isActing}
onClick={() => handleRequestMoreDocuments(item.id)}
>
Update Docs Request
</button>
</Show>
{/* Approved → link to management page */}
<Show when={status === 'APPROVED'}>
<A
href={dest.href}
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
style="font-size:11px;padding:3px 8px;background:#f0fdf4;color:#15803d;border-color:#bbf7d0;white-space:nowrap"
title={`Open ${dest.label}`}
>
Open {dest.label.replace(' Management', '')}
</A>
</Show>
</div>
</td>
</tr>
);
}}
</For>
</Show>
</tbody>
</table>
</div>
<div class="admin-pagination">
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" type="button" disabled={currentPage() <= 1} onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}> Prev</button>
<span>Page {currentPage()} of {totalPages()}</span>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" type="button" disabled={currentPage() >= totalPages()} onClick={() => setCurrentPage((p) => Math.min(totalPages(), p + 1))}>Next </button>
</div>
</section>
</Show>
{/* ── Inline Detail Panel ── */}
<Show when={showDetail()}>
<ApprovalDetailPanel
approval={selectedApproval()}
acting={acting()}
onBack={() => setShowDetail(false)}
onApprove={handleApprove}
onReject={handleReject}
onRequestChanges={handleRequestChanges}
onRequestMoreDocuments={handleRequestMoreDocuments}
/>
</Show>
</Show>
{/* ── Rules Tab ── */}
<Show when={activeTab() === 'rules'}>
<Show when={ruleError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{ruleError()}</div>
</Show>
<div class="mb-6 flex items-start justify-between gap-4" style="margin-bottom:16px">
<div />
<button class="btn-primary" onClick={() => setShowAddRule((v) => !v)}>
{showAddRule() ? 'Cancel' : '+ Add Rule'}
</button>
</div>
<Show when={showAddRule()}>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:16px">
<h3 style="font-size:15px;font-weight:600;color:#0f172a;margin:0 0 14px">New Approval Rule</h3>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Rule Name <span style="color:#ef4444">*</span></label>
<input
value={newRuleName()}
onInput={(e) => setNewRuleName(e.currentTarget.value)}
placeholder="e.g. Profile Approval by Admin"
/>
</div>
<div class="field">
<label>Entity Type</label>
<select value={newEntityType()} onChange={(e) => setNewEntityType(e.currentTarget.value)}>
<For each={ENTITY_TYPE_OPTIONS}>{(et) => <option value={et}>{et}</option>}</For>
</select>
</div>
<div class="field">
<label>Approver Type</label>
<select value={newApproverType()} onChange={(e) => setNewApproverType(e.currentTarget.value)}>
<For each={APPROVER_TYPE_OPTIONS}>{(at) => <option value={at}>{at}</option>}</For>
</select>
</div>
<div class="field">
<label>Priority</label>
<input type="number" min="1" value={newPriority()} onInput={(e) => setNewPriority(parseInt(e.currentTarget.value, 10) || 1)} />
</div>
</div>
<div class="actions" style="margin-top:14px">
<button class="btn-primary" disabled={submittingRule()} onClick={handleAddRule}>
{submittingRule() ? 'Saving...' : 'Save Rule'}
</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={() => setShowAddRule(false)}>Cancel</button>
</div>
</div>
</Show>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Rule Name</th>
<th>Entity Type</th>
<th>Approver Type</th>
<th>Priority</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={rules.loading}>
<tr><td colspan="5" style="text-align:center;padding:32px;color:#64748b">Loading...</td></tr>
</Show>
<Show when={!rules.loading && (rules()?.length ?? 0) === 0}>
<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">No approval rules configured yet.</td></tr>
</Show>
<Show when={!rules.loading && (rules()?.length ?? 0) > 0}>
<For each={rules()!}>
{(rule) => (
<tr>
<td style="font-weight:600;color:#0f172a">{rule.name || '—'}</td>
<td style="color:#475569">{rule.entityType || rule.entity_type || '—'}</td>
<td style="color:#475569">{rule.approverType || rule.approver_type || '—'}</td>
<td style="color:#475569">{rule.priority ?? '—'}</td>
<td>
<div class="flex items-center justify-end gap-1">
<button
class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors"
disabled={deletingRule() === rule.id}
onClick={() => handleDeleteRule(rule.id)}
>
{deletingRule() === rule.id ? '...' : 'Delete'}
</button>
</div>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>
</div>
</section>
</Show>
</div>
</div>
</AdminShell>
);
@ -1092,62 +1071,67 @@ function ApprovalDetailPanel(props: {
return (
<Show when={a()} fallback={
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Select an approval from the list.</p></div>
<div class="rounded-xl border border-[#e2e6ee] bg-[#f8f9fc] p-6 text-center text-[#8087a0]"><p>Select an approval from the list.</p></div>
}>
<div class="mb-6 flex items-start justify-between gap-4" style="margin-bottom:12px">
<div class="mb-6 flex items-center justify-between gap-4">
<div>
<h2 class="text-lg font-semibold text-gray-900">Approval Detail</h2>
<p class="mt-1 text-sm text-gray-500">{a()!._typeLabel || 'Request'}</p>
<h2 class="text-[20px] font-bold text-[#050026]">Approval Detail</h2>
<p class="text-[13px] text-[#64748b] mt-1">{a()!._typeLabel || 'Request'}</p>
</div>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" type="button" onClick={props.onBack}> Back to List</button>
<button
class="inline-flex h-10 items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-5 text-[14px] font-semibold text-[#050026] transition-colors hover:bg-[#f8f9fc]"
onClick={props.onBack}
>
Back to List
</button>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
{/* Request info */}
<div class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h3 style="margin:0 0 12px;font-size:15px;font-weight:600;color:#0f172a">Request Summary</h3>
<table style="width:100%;border-collapse:collapse;font-size:13px">
<div class="rounded-[16px] border border-[#e2e6ee] bg-white p-5 shadow-sm">
<h3 class="text-[15px] font-bold text-[#050026] mb-4">Request Summary</h3>
<table class="w-full border-collapse text-[13px]">
<tbody>
<tr><td style="color:#64748b;padding:4px 8px 4px 0;white-space:nowrap">ID</td><td style="font-family:ui-monospace,monospace;font-size:11px;color:#475569">{a()!.id}</td></tr>
<tr><td style="color:#64748b;padding:4px 8px 4px 0">Category</td><td style="font-weight:500">{a()!._typeLabel || '—'}</td></tr>
<tr><td style="color:#64748b;padding:4px 8px 4px 0">Status</td><td><StatusBadge status={status()} /></td></tr>
<tr><td style="color:#64748b;padding:4px 8px 4px 0">Priority</td><td>{a()!.priority ?? '—'}</td></tr>
<tr><td style="color:#64748b;padding:4px 8px 4px 0">Template</td><td style="color:#475569">{a()!._parsedReason?.templateId || '—'}</td></tr>
<tr><td style="color:#64748b;padding:4px 8px 4px 0">Submitted</td><td style="color:#475569">{(a()!.createdAt || a()!.created_at) ? new Date((a()!.createdAt || a()!.created_at)!).toLocaleString() : '—'}</td></tr>
<tr class="border-b border-[#f1f5f9]"><td class="py-2 text-[#64748b] whitespace-nowrap pr-4">ID</td><td class="py-2 font-mono text-[#475569]">{a()!.id}</td></tr>
<tr class="border-b border-[#f1f5f9]"><td class="py-2 text-[#64748b] pr-4">Category</td><td class="py-2 font-bold text-[#050026]">{a()!._typeLabel || '—'}</td></tr>
<tr class="border-b border-[#f1f5f9]"><td class="py-2 text-[#64748b] pr-4">Status</td><td class="py-2"><StatusBadge status={status()} /></td></tr>
<tr class="border-b border-[#f1f5f9]"><td class="py-2 text-[#64748b] pr-4">Priority</td><td class="py-2 font-bold">{a()!.priority ?? '—'}</td></tr>
<tr class="border-b border-[#f1f5f9]"><td class="py-2 text-[#64748b] pr-4">Template</td><td class="py-2 text-[#475569]">{a()!._parsedReason?.templateId || '—'}</td></tr>
<tr><td class="py-2 text-[#64748b] pr-4">Submitted</td><td class="py-2 text-[#475569]">{(a()!.createdAt || a()!.created_at) ? new Date((a()!.createdAt || a()!.created_at)!).toLocaleString() : '—'}</td></tr>
</tbody>
</table>
</div>
{/* Requester info */}
<div class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h3 style="margin:0 0 12px;font-size:15px;font-weight:600;color:#0f172a">Requester</h3>
<table style="width:100%;border-collapse:collapse;font-size:13px">
<div class="rounded-[16px] border border-[#e2e6ee] bg-white p-5 shadow-sm">
<h3 class="text-[15px] font-bold text-[#050026] mb-4">Requester</h3>
<table class="w-full border-collapse text-[13px] mb-4">
<tbody>
<tr><td style="color:#64748b;padding:4px 8px 4px 0;white-space:nowrap">Name</td><td style="font-weight:500">{requesterName(a()!)}</td></tr>
<tr><td style="color:#64748b;padding:4px 8px 4px 0">Email</td><td style="color:#475569">{requesterEmail(a()!) || '—'}</td></tr>
<tr><td style="color:#64748b;padding:4px 8px 4px 0">Role</td><td><RoleTypeBadge type={a()!._roleType} /></td></tr>
<tr><td style="color:#64748b;padding:4px 8px 4px 0">Profession</td><td style="color:#475569">{a()!._parsedReason?.profession || '—'}</td></tr>
<tr class="border-b border-[#f1f5f9]"><td class="py-2 text-[#64748b] whitespace-nowrap pr-4">Name</td><td class="py-2 font-bold text-[#050026]">{requesterName(a()!)}</td></tr>
<tr class="border-b border-[#f1f5f9]"><td class="py-2 text-[#64748b] pr-4">Email</td><td class="py-2 font-medium text-[#475569]">{requesterEmail(a()!) || '—'}</td></tr>
<tr class="border-b border-[#f1f5f9]"><td class="py-2 text-[#64748b] pr-4">Role</td><td class="py-2"><RoleTypeBadge type={a()!._roleType} /></td></tr>
<tr><td class="py-2 text-[#64748b] pr-4">Profession</td><td class="py-2 font-medium text-[#475569]">{a()!._parsedReason?.profession || '—'}</td></tr>
</tbody>
</table>
{/* Actions */}
<div style="margin-top:16px;display:flex;flex-wrap:wrap;gap:8px">
<div class="flex flex-wrap gap-2">
<Show when={isPending()}>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" style="background:#eff6ff;color:#1d4ed8;border-color:#bfdbfe;font-size:13px" disabled={isActing()} onClick={() => props.onRequestMoreDocuments(a()!.id)}>📄 Request Documents</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;font-size:13px" disabled={isActing()} onClick={() => props.onRequestChanges(a()!.id)}> Request Changes</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" style="background:#f0fdf4;color:#15803d;border-color:#bbf7d0;font-size:13px" disabled={isActing()} onClick={() => props.onApprove(a()!.id)}> Approve</button>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" style="font-size:13px" disabled={isActing()} onClick={() => props.onReject(a()!.id)}> Reject</button>
<button class="inline-flex h-9 items-center justify-center rounded-lg bg-[#eff6ff] border border-[#bfdbfe] px-4 text-[13px] font-bold text-[#1d4ed8] hover:bg-[#dbeafe] transition-colors" disabled={isActing()} onClick={() => props.onRequestMoreDocuments(a()!.id)}>📄 Request Documents</button>
<button class="inline-flex h-9 items-center justify-center rounded-lg bg-[#fff7ed] border border-[#fed7aa] px-4 text-[13px] font-bold text-[#c2410c] hover:bg-[#ffedd5] transition-colors" disabled={isActing()} onClick={() => props.onRequestChanges(a()!.id)}> Request Changes</button>
<button class="inline-flex h-9 items-center justify-center rounded-lg bg-[#f0fdf4] border border-[#bbf7d0] px-4 text-[13px] font-bold text-[#15803d] hover:bg-[#dcfce7] transition-colors" disabled={isActing()} onClick={() => props.onApprove(a()!.id)}> Approve</button>
<button class="inline-flex h-9 items-center justify-center rounded-lg bg-[#fef2f2] border border-[#fecaca] px-4 text-[13px] font-bold text-[#b91c1c] hover:bg-[#fee2e2] transition-colors" disabled={isActing()} onClick={() => props.onReject(a()!.id)}> Reject</button>
</Show>
<Show when={status() === 'APPROVED'}>
<A href={dest().href} class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" style="background:#f0fdf4;color:#15803d;border-color:#bbf7d0;font-size:13px">
<A href={dest().href} class="inline-flex h-9 items-center justify-center rounded-lg bg-[#f0fdf4] border border-[#bbf7d0] px-4 text-[13px] font-bold text-[#15803d] hover:bg-[#dcfce7] transition-colors">
Open {dest().label}
</A>
</Show>
<Show when={a()!._viewHref}>
<A href={a()!._viewHref!} class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" style="font-size:13px">Open Full Request</A>
<A href={a()!._viewHref!} class="inline-flex h-9 items-center justify-center rounded-lg bg-white border border-[#e2e6ee] px-4 text-[13px] font-bold text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors">Open Full Request</A>
</Show>
<Show when={a()!._supportsSubmissionView}>
<A href={`/admin/approval/${a()!.id}`} class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" style="font-size:13px">Open Profile Review</A>
<A href={`/admin/approval/${a()!.id}`} class="inline-flex h-9 items-center justify-center rounded-lg bg-white border border-[#e2e6ee] px-4 text-[13px] font-bold text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors">Open Profile Review</A>
</Show>
</div>
</div>
@ -1155,13 +1139,13 @@ function ApprovalDetailPanel(props: {
{/* Submitted fields */}
<Show when={a()!._parsedReason?.values && Object.keys(a()!._parsedReason!.values!).length > 0}>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:16px">
<h3 style="margin:0 0 12px;font-size:15px;font-weight:600;color:#0f172a">Submitted Data</h3>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px">
<div class="rounded-[16px] border border-[#e2e6ee] bg-white p-5 shadow-sm mb-6">
<h3 class="text-[15px] font-bold text-[#050026] mb-4">Submitted Data</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-3">
{Object.entries(a()!._parsedReason!.values!).map(([k, v]) => (
<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;padding:8px 10px">
<div style="font-size:11px;color:#94a3b8;margin-bottom:2px">{k.replace(/_/g, ' ').toUpperCase()}</div>
<div style="font-size:13px;color:#0f172a;word-break:break-all">{String(v) || '—'}</div>
<div class="bg-[#f8f9fc] border border-[#e2e6ee] rounded-xl p-3">
<div class="text-[11px] font-bold text-[#8087a0] mb-1">{k.replace(/_/g, ' ').toUpperCase()}</div>
<div class="text-[13px] font-medium text-[#050026] break-all">{String(v) || '—'}</div>
</div>
))}
</div>
@ -1170,11 +1154,11 @@ function ApprovalDetailPanel(props: {
{/* Document request spotlight (USP) */}
<Show when={docRemark()}>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:16px;border:1px solid #bfdbfe;background:#eff6ff">
<h3 style="margin:0 0 10px;font-size:15px;font-weight:700;color:#1d4ed8">Requested Documents</h3>
<p style="margin:0;font-size:13px;color:#0f172a">{docRemark()!.comment}</p>
<div class="rounded-[16px] border border-[#bfdbfe] bg-[#eff6ff] p-5 shadow-sm mb-6">
<h3 class="text-[15px] font-bold text-[#1d4ed8] mb-2">Requested Documents</h3>
<p class="text-[14px] text-[#050026] font-medium m-0">{docRemark()!.comment}</p>
<Show when={(docRemark()!.fields || []).length > 0}>
<p style="margin:8px 0 0;font-size:12px;color:#334155">
<p class="text-[12px] font-bold text-[#3b82f6] m-0 mt-2">
Required: {(docRemark()!.fields || []).join(', ')}
</p>
</Show>
@ -1183,26 +1167,26 @@ function ApprovalDetailPanel(props: {
{/* Admin remarks history */}
<Show when={remarks().length > 0}>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h3 style="margin:0 0 12px;font-size:15px;font-weight:600;color:#0f172a">Admin Remarks History</h3>
<div style="display:flex;flex-direction:column;gap:8px">
<div class="rounded-[16px] border border-[#e2e6ee] bg-white p-5 shadow-sm">
<h3 class="text-[15px] font-bold text-[#050026] mb-4">Admin Remarks History</h3>
<div class="flex flex-col gap-3">
<For each={remarks()}>
{(remark, i) => {
const typeColor: Record<string, string> = {
INFO: '#e0f2fe',
CHANGES_REQUESTED: '#fff7ed',
MORE_DOCUMENTS_REQUESTED: '#eff6ff',
REJECTED: '#fef2f2',
INFO: 'bg-[#e0f2fe] border-[#bae6fd]',
CHANGES_REQUESTED: 'bg-[#fff7ed] border-[#fed7aa]',
MORE_DOCUMENTS_REQUESTED: 'bg-[#eff6ff] border-[#bfdbfe]',
REJECTED: 'bg-[#fef2f2] border-[#fecaca]',
};
return (
<div style={`background:${typeColor[remark.type] || '#f8fafc'};border:1px solid #e2e8f0;border-radius:6px;padding:10px 12px`}>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
<span style="font-size:11px;font-weight:700;color:#64748b;background:#e2e8f0;padding:2px 6px;border-radius:4px">{remark.type.replace(/_/g, ' ')}</span>
<span style="font-size:11px;color:#94a3b8">Remark #{i() + 1}</span>
<div class={`${typeColor[remark.type] || 'bg-[#f8f9fc] border-[#e2e6ee]'} border rounded-xl p-4`}>
<div class="flex items-center gap-2 mb-2">
<span class="text-[10px] font-bold bg-white px-2 py-0.5 rounded text-[#475569] uppercase tracking-wide">{remark.type.replace(/_/g, ' ')}</span>
<span class="text-[11px] font-bold text-[#94a3b8]">Remark #{i() + 1}</span>
</div>
<p style="margin:0;font-size:13px;color:#0f172a">{remark.comment}</p>
<p class="text-[13px] font-medium text-[#050026] m-0">{remark.comment}</p>
<Show when={remark.fields && remark.fields.length > 0}>
<p style="margin:6px 0 0;font-size:12px;color:#64748b">Fields: {remark.fields!.join(', ')}</p>
<p class="text-[12px] font-bold text-[#64748b] mt-2 mb-0">Fields: {remark.fields!.join(', ')}</p>
</Show>
</div>
);

View file

@ -30,7 +30,7 @@ async function loadSchemas(): Promise<OnboardingSchema[]> {
}));
const hydrated = await Promise.all(
baseRows.map(async (row) => {
baseRows.map(async (row: { id: string; roleId: string; roleKey: string; version: number; status: string }) => {
if (!row.roleId) {
return {
...row,
@ -94,77 +94,102 @@ export default function OnboardingSchemasPage() {
return (
<AdminShell>
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
{/* ── Page header ── */}
<div class="bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between">
<div>
<h1 class="text-xl font-semibold text-gray-900">Onboarding Management</h1>
<p class="text-sm text-gray-500 mt-0.5">Manage onboarding flows, role assignments, and previewable step groups for external users.</p>
</div>
<A class="btn-primary" href="/admin/onboarding-schemas/new">Create Onboarding Flow</A>
</div>
<OnboardingManagementTabs />
{/* ── Content ── */}
<div class="p-6">
<Show when={deleteError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{deleteError()}</div>
</Show>
<div class="table-card">
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>Flow</th>
<th>Role</th>
<th>Steps</th>
<th>Version</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={schemas.loading}>
<tr><td colspan="6" class="py-10 text-center text-sm text-slate-400">Loading onboarding flows</td></tr>
</Show>
<Show when={!schemas.loading && schemas.error}>
<tr><td colspan="6" class="py-10 text-center text-sm text-red-500">Failed to load onboarding schemas. Is the backend running?</td></tr>
</Show>
<Show when={!schemas.loading && !schemas.error && schemas()?.length === 0}>
<tr><td colspan="6" class="py-10 text-center text-sm text-slate-400">No onboarding flows created yet.</td></tr>
</Show>
<Show when={!schemas.loading && !schemas.error && (schemas()?.length ?? 0) > 0}>
{schemas()!.map((schema) => (
<tr class="hover:bg-slate-50">
<td class="font-medium text-gray-900">{schema.title}</td>
<td class="text-slate-500">{schema.roleKey || '—'}</td>
<td class="text-slate-500">{schema.stepCount}</td>
<td class="text-slate-500">v{schema.version}</td>
<td>
<span class={`inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium ${schema.status === 'PUBLISHED' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-600'}`}>{schema.status}</span>
</td>
<td>
<div class="flex items-center justify-end gap-1">
<A class="action-btn flex items-center justify-center hover:bg-gray-50 transition-colors text-sm" href={`/admin/onboarding-schemas/${schema.roleId || schema.id}`} title="Open Flow">👁</A>
<button
class="action-btn flex items-center justify-center border-red-100 bg-red-50 hover:bg-red-100 transition-colors text-sm"
disabled={deleting() === schema.id}
onClick={() => handleDelete(schema.id, schema.title)}
title="Delete Flow"
>
{deleting() === schema.id ? '…' : '🗑'}
</button>
</div>
</td>
</tr>
))}
</Show>
</tbody>
</table>
<div class="p-6 flex-1 max-w-[1600px] mx-auto w-full">
{/* Header */}
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between mb-8">
<div>
<h1 class="text-[32px] font-bold text-[#050026] leading-tight">Onboarding Management</h1>
<p class="text-[14px] text-[#8087a0] mt-1">Manage onboarding flows, role assignments, and previewable step groups for external users.</p>
</div>
<div class="flex items-center gap-3">
<A
href="/admin/onboarding-schemas/new"
class="inline-flex h-11 items-center justify-center rounded-xl bg-[#050026] px-6 text-[14px] font-semibold text-white transition-colors hover:bg-[#0a0044]"
>
<span class="mr-2 text-lg leading-none">+</span> Create Onboarding Flow
</A>
</div>
</div>
<OnboardingManagementTabs />
{/* Content */}
<section class="rounded-[24px] border border-[#e2e6ee] bg-[#f7f7f8] p-1.5 h-full mt-6">
<div class="rounded-[20px] bg-white p-5">
<Show when={deleteError()}>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-[14px] text-red-700">{deleteError()}</div>
</Show>
<div class="overflow-x-auto">
<table class="w-full min-w-[800px] border-collapse">
<thead>
<tr class="bg-[#050026] text-left text-white">
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider rounded-tl-xl whitespace-nowrap">FLOW</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">ROLE</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">STEPS</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">VERSION</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">STATUS</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider text-right rounded-tr-xl whitespace-nowrap">ACTIONS</th>
</tr>
</thead>
<tbody>
<Show when={schemas.loading}>
<tr><td colspan="6" class="text-center py-12 text-[#8087a0] text-[14px]">Loading onboarding flows</td></tr>
</Show>
<Show when={!schemas.loading && schemas.error}>
<tr><td colspan="6" class="text-center py-12 text-red-500 text-[14px]">Failed to load onboarding schemas. Is the backend running?</td></tr>
</Show>
<Show when={!schemas.loading && !schemas.error && schemas()?.length === 0}>
<tr><td colspan="6" class="text-center py-12 text-[#8087a0] text-[14px]">No onboarding flows created yet.</td></tr>
</Show>
<Show when={!schemas.loading && !schemas.error && (schemas()?.length ?? 0) > 0}>
{schemas()!.map((schema) => (
<tr class="border-b border-[#e2e6ee] bg-white transition-colors hover:bg-[#f8f9fc]">
<td class="px-6 py-4 text-[14px] font-bold text-[#050026]">{schema.title}</td>
<td class="px-6 py-4 text-[14px] text-[#64748b]">{schema.roleKey || '—'}</td>
<td class="px-6 py-4 text-[14px] font-semibold text-[#64748b]">
<span class="inline-flex items-center px-3 py-1 rounded-lg bg-[#f8f9fc] text-[#050026] text-[12px] font-bold border border-[#e2e6ee]">
{schema.stepCount} Steps
</span>
</td>
<td class="px-6 py-4 text-[14px] text-[#64748b]">v{schema.version}</td>
<td class="px-6 py-4">
<span class={`inline-flex rounded-full px-2.5 py-0.5 text-[12px] font-medium ${schema.status === 'PUBLISHED' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-[#64748b]'}`}>{schema.status}</span>
</td>
<td class="px-6 py-4">
<div class="flex items-center justify-end gap-2">
<A
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors"
href={`/admin/onboarding-schemas/${schema.roleId || schema.id}`}
title="Open Flow"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</A>
<button
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-red-50 hover:text-red-500 hover:border-red-200 transition-colors disabled:opacity-50"
disabled={deleting() === schema.id}
onClick={() => handleDelete(schema.id, schema.title)}
title="Delete Flow"
>
{deleting() === schema.id ? '…' : (
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
)}
</button>
</div>
</td>
</tr>
))}
</Show>
</tbody>
</table>
</div>
</div>
</section>
</div>
</div>
</AdminShell>

View file

@ -50,83 +50,91 @@ export default function VerificationStatusPage() {
return (
<AdminShell>
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
{/* ── Page header ── */}
<div class="bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between">
<div>
<h1 class="text-xl font-semibold text-gray-900">Verification Status</h1>
<p class="text-sm text-gray-500 mt-0.5">Track request status states and open a specific record for follow-up.</p>
</div>
<A
class="rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
href="/admin/approval"
>
Open Approval Center
</A>
</div>
{/* ── Content ── */}
<div class="p-6">
<div class="table-card">
<div class="overflow-x-auto">
<table class="data-table w-full text-sm">
<thead>
<tr>
<th>ID</th>
<th>Type</th>
<th>Requester</th>
<th>Status</th>
<th>Submitted</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<Show when={rows.loading}>
<tr><td colspan="6" class="py-10 text-center text-sm text-slate-400">Loading verification statuses</td></tr>
</Show>
<Show when={!rows.loading && normalized().length === 0}>
<tr><td colspan="6" class="py-10 text-center text-sm text-slate-400">No verification status records found.</td></tr>
</Show>
<For each={normalized()}>
{(item) => (
<tr class="hover:bg-slate-50">
<td class="font-mono text-xs text-slate-500">{item.id.slice(0, 8)}</td>
<td class="text-slate-600">{item.type}</td>
<td>
<p class="font-medium text-gray-900">{item.requesterName}</p>
<p class="text-xs text-slate-500">{item.requesterEmail}</p>
</td>
<td>
<span class={`inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium ${statusBadge(item.status)}`}>
{item.status}
</span>
</td>
<td class="text-slate-500">{item.createdAt ? new Date(item.createdAt).toLocaleString() : '—'}</td>
<td>
<div class="flex items-center justify-end gap-2">
<A
class="action-btn flex items-center justify-center text-gray-500 hover:bg-gray-50 transition-colors text-sm"
href={`/admin/verification-status/${item.id}`}
title="Open Status Detail"
>
</A>
<A
class="action-btn flex items-center justify-center text-gray-500 hover:bg-gray-50 transition-colors text-sm"
href={`/admin/approval/${item.id}`}
title="Open Approval Detail"
>
👁
</A>
</div>
</td>
</tr>
)}
</For>
</tbody>
</table>
<div class="p-6 flex-1 max-w-[1600px] mx-auto w-full">
{/* Header */}
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between mb-8">
<div>
<h1 class="text-[32px] font-bold text-[#050026] leading-tight">Verification Status</h1>
<p class="text-[14px] text-[#8087a0] mt-1">Track request status states and open a specific record for follow-up.</p>
</div>
<div class="flex items-center gap-3">
<A
href="/admin/approval"
class="inline-flex h-11 items-center justify-center rounded-xl border border-[#d9dde6] bg-white px-6 text-[14px] font-semibold text-[#050026] transition-colors hover:bg-[#f8f9fc]"
>
Open Approval Center
</A>
</div>
</div>
{/* Content */}
<section class="rounded-[24px] border border-[#e2e6ee] bg-[#f7f7f8] p-1.5 h-full">
<div class="rounded-[20px] bg-white p-5">
<div class="overflow-x-auto">
<table class="w-full min-w-[800px] border-collapse">
<thead>
<tr class="bg-[#050026] text-left text-white">
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider rounded-tl-xl whitespace-nowrap">ID</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">TYPE</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">REQUESTER</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">STATUS</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider whitespace-nowrap">SUBMITTED</th>
<th class="px-6 py-4 text-[11px] font-bold uppercase tracking-wider text-right rounded-tr-xl whitespace-nowrap">ACTIONS</th>
</tr>
</thead>
<tbody>
<Show when={rows.loading}>
<tr><td colspan="6" class="text-center py-12 text-[#8087a0] text-[14px]">Loading verification statuses</td></tr>
</Show>
<Show when={!rows.loading && normalized().length === 0}>
<tr><td colspan="6" class="text-center py-12 text-[#8087a0] text-[14px]">No verification status records found.</td></tr>
</Show>
<For each={normalized()}>
{(item) => (
<tr class="border-b border-[#e2e6ee] bg-white transition-colors hover:bg-[#f8f9fc]">
<td class="px-6 py-4 text-[14px] font-mono text-[#64748b]">{item.id.slice(0, 8)}</td>
<td class="px-6 py-4 text-[14px] font-bold text-[#050026]">{item.type}</td>
<td class="px-6 py-4">
<p class="text-[14px] font-bold text-[#050026]">{item.requesterName}</p>
<p class="text-[12px] text-[#64748b]">{item.requesterEmail}</p>
</td>
<td class="px-6 py-4 text-[14px]">
<span class={`inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium ${statusBadge(item.status)}`}>
{item.status}
</span>
</td>
<td class="px-6 py-4 text-[14px] text-[#64748b]">{item.createdAt ? new Date(item.createdAt).toLocaleString() : '—'}</td>
<td class="px-6 py-4">
<div class="flex items-center justify-end gap-2">
<A
href={`/admin/verification-status/${item.id}`}
title="Open Status Detail"
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</A>
<A
href={`/admin/approval/${item.id}`}
title="Open Approval Detail"
class="flex h-8 w-8 items-center justify-center rounded-lg border border-[#e2e6ee] bg-white text-[#64748b] hover:bg-[#f8f9fc] hover:text-[#050026] transition-colors"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</A>
</div>
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</div>
</section>
</div>
</div>
</AdminShell>