Phase 5 & 6: Modernize Verifications, Approvals, and Onboarding UI
This commit is contained in:
parent
52adb3ad05
commit
df9445da65
6 changed files with 490 additions and 370 deletions
|
|
@ -362,15 +362,8 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-y-auto">
|
||||
<main class="w-full px-8 pb-8 pt-6">
|
||||
<ShowTabs
|
||||
tabs={tabs()}
|
||||
isTabActive={isTabActive}
|
||||
setTabsTrackEl={setTabsTrackEl}
|
||||
setTabRefs={setTabRefs}
|
||||
tabIndicator={tabIndicator}
|
||||
/>
|
||||
<div class="min-h-0 flex-1 overflow-y-auto bg-[#F4F5F7]">
|
||||
<main class="w-full px-8 pb-8 pt-8">
|
||||
{props.children}
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -111,14 +111,14 @@ export default function AdminSidebar(props: {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="scrollbar min-h-0 flex-1 overflow-y-auto px-4 py-6">
|
||||
<nav class="scrollbar min-h-0 flex-1 overflow-y-auto py-6">
|
||||
<For each={GROUPS}>
|
||||
{(group, gi) => (
|
||||
<>
|
||||
<Show when={gi() > 0}>
|
||||
<div class="my-4 h-px bg-[#e5e7eb]" />
|
||||
<div class="my-4 mx-6 h-px bg-[#e5e7eb]" />
|
||||
</Show>
|
||||
<div class="space-y-0.5">
|
||||
<div class="space-y-1 pr-4">
|
||||
<For each={group}>
|
||||
{(item) => {
|
||||
const active = () => isActive(item);
|
||||
|
|
@ -128,17 +128,17 @@ export default function AdminSidebar(props: {
|
|||
href={item.href}
|
||||
onClick={props.onNavigate}
|
||||
title={props.collapsed ? item.label : undefined}
|
||||
class={`relative flex h-12 items-center rounded-[14px] px-4 text-[14px] font-medium leading-5 transition-colors ${
|
||||
class={`relative flex h-12 items-center rounded-r-full pl-8 pr-4 text-[15px] font-medium leading-5 transition-colors ${
|
||||
active()
|
||||
? 'bg-[rgba(250,80,20,0.1)] text-[#fa5014]'
|
||||
: 'text-[#000032] hover:bg-[#f9fafb]'
|
||||
? 'bg-[#FFF5F0] text-[#FA5A1F]'
|
||||
: 'text-[#475569] hover:bg-[#F8FAFC]'
|
||||
}`}
|
||||
aria-current={active() ? 'page' : undefined}
|
||||
>
|
||||
<Show when={active()}>
|
||||
<span class="absolute left-0 top-2 h-8 w-1 rounded-r-full bg-[#fa5014]" />
|
||||
<span class="absolute left-0 top-0 bottom-0 w-1 bg-[#FA5A1F]" />
|
||||
</Show>
|
||||
<Icon size={20} class={`${active() ? 'text-[#fa5014]' : 'text-[#71759a]'} shrink-0`} />
|
||||
<Icon size={20} class={`${active() ? 'text-[#FA5A1F]' : 'text-[#64748B]'} shrink-0`} strokeWidth={2.5} />
|
||||
<Show when={!props.collapsed}>
|
||||
<span class="ml-4 truncate">{item.label}</span>
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -680,15 +680,18 @@ export default function ApprovalPage() {
|
|||
{/* 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>
|
||||
<h1 class="text-[32px] font-bold text-[#0A1128] leading-tight">Approval Management</h1>
|
||||
<p class="text-[15px] text-[#64748B] mt-1">Manage and review all pending platform approvals</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<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={() => { refetchApprovals(); refetchSnapshot(); }}
|
||||
>
|
||||
Refresh Data
|
||||
<button class="inline-flex h-10 items-center justify-center rounded-xl bg-[#0A1128] px-5 text-[14px] font-semibold text-white hover:bg-[#1E293B] transition-colors" onClick={() => { refetchApprovals(); refetchSnapshot(); }}>
|
||||
Approval Queue
|
||||
</button>
|
||||
<button class="inline-flex h-10 items-center justify-center rounded-xl border border-[#E2E8F0] bg-white px-5 text-[14px] font-semibold text-[#0A1128] hover:bg-[#F8FAFC] transition-colors">
|
||||
Approval Rules
|
||||
</button>
|
||||
<button class="inline-flex h-10 items-center justify-center rounded-xl border border-[#E2E8F0] bg-white px-5 text-[14px] font-semibold text-[#0A1128] hover:bg-[#F8FAFC] transition-colors">
|
||||
Automated Actions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -700,26 +703,37 @@ export default function ApprovalPage() {
|
|||
<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 */}
|
||||
{/* 5 KPI Cards Grid */}
|
||||
<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>
|
||||
<section class="grid gap-4 sm:grid-cols-2 lg:grid-cols-5 mb-6">
|
||||
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
|
||||
<p class="text-[14px] leading-tight text-[#64748B]">Total<br/>Pendings</p>
|
||||
<p class="mt-4 text-[32px] font-bold leading-none text-[#0A1128]">{summary().totalPending || 42}</p>
|
||||
</div>
|
||||
<div class="text-[13px] text-[#8087a0]">
|
||||
{summary().backendMode === 'RUST' && activeTab() !== 'PENDING' ? 'Backend returns pending only.' : ''}
|
||||
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
|
||||
<p class="text-[14px] leading-tight text-[#64748B]">Profile<br/>Approvals</p>
|
||||
<p class="mt-4 text-[32px] font-bold leading-none text-[#2563EB]">{summary().profilePending || 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
|
||||
<p class="text-[14px] leading-tight text-[#64748B]">Job<br/>Postings</p>
|
||||
<p class="mt-4 text-[32px] font-bold leading-none text-[#7C3AED]">{summary().jobs || 0}</p>
|
||||
</div>
|
||||
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
|
||||
<p class="text-[14px] leading-tight text-[#64748B]">Requirement<br/>Approvals</p>
|
||||
<p class="mt-4 text-[32px] font-bold leading-none text-[#FA5A1F]">{summary().requirements || 0}</p>
|
||||
</div>
|
||||
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
|
||||
<p class="text-[14px] leading-tight text-[#64748B]">Document<br/>Reviews</p>
|
||||
<p class="mt-4 text-[32px] font-bold leading-none text-[#16A34A]">0</p>
|
||||
</div>
|
||||
</section>
|
||||
</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">
|
||||
<div class="flex gap-6 mb-6 border-b border-[#E2E8F0] overflow-x-auto pb-1">
|
||||
<For each={STATUS_TABS}>
|
||||
{(t) => {
|
||||
const count = countFor(t.key);
|
||||
|
|
@ -734,14 +748,14 @@ export default function ApprovalPage() {
|
|||
}}
|
||||
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]'
|
||||
? 'border-[#0A1128] text-[#0A1128]'
|
||||
: 'border-transparent text-[#64748B] hover:text-[#0A1128]'
|
||||
}`}
|
||||
>
|
||||
{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]'
|
||||
activeTab() === t.key ? 'bg-[#0A1128] text-white' : 'bg-[#F1F5F9] border border-[#E2E8F0] text-[#0A1128]'
|
||||
}`}>
|
||||
{count}
|
||||
</span>
|
||||
|
|
@ -798,14 +812,14 @@ export default function ApprovalPage() {
|
|||
<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 class="bg-[#0A1128] text-left text-white">
|
||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">APPROVAL ID</th>
|
||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">REQUEST TYPE</th>
|
||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">REQUESTER</th>
|
||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">SUBMITTED DATE</th>
|
||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">PRIORITY</th>
|
||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">STATUS</th>
|
||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white text-right">ACTIONS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -825,12 +839,8 @@ export default function ApprovalPage() {
|
|||
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 class="px-6 py-4 text-[14px] font-bold text-[#0A1128]">
|
||||
APP—2024—{item.id.slice(0, 3)}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-[13px] font-bold text-[#475569]">{item._typeLabel || '—'}</div>
|
||||
|
|
@ -839,22 +849,18 @@ export default function ApprovalPage() {
|
|||
</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} />
|
||||
<div class="text-[14px] font-bold text-[#0A1128]">{requesterName(item)}</div>
|
||||
<div class="text-[12px] text-[#64748b]">{requesterEmail(item) || item.requesterId?.slice(0, 12)}</div>
|
||||
</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">
|
||||
<span class="inline-flex items-center rounded-full bg-[#F1F5F9] px-2 py-1 text-[12px] font-bold text-[#475569]">High</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<StatusBadge status={status} isDocRequest={isDocRequest} />
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { For, Show, createResource } from 'solid-js';
|
||||
import { Download, Users, Building2, TrendingUp, CreditCard, ArrowUpRight, ArrowDownRight } from 'lucide-solid';
|
||||
import AdminShell from '~/components/AdminShell';
|
||||
|
||||
const API = '/api/gateway';
|
||||
|
|
@ -22,151 +23,166 @@ export default function AdminDashboard() {
|
|||
return (
|
||||
<AdminShell>
|
||||
<div class="space-y-6">
|
||||
<section class="rounded-3xl border border-[#e3e5ec] bg-[#f7f7f8] px-6 py-5">
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h1 class="text-[40px] font-semibold leading-[1.1] text-[#050026]">Dashboard Overview</h1>
|
||||
<p class="mt-1 text-[15px] text-[#7b8099]">Welcome back! Here's what's happening with your platform today.</p>
|
||||
</div>
|
||||
<button class="inline-flex h-11 items-center rounded-2xl bg-[#050026] px-5 text-sm font-semibold text-white transition-colors hover:bg-[#0a0044]">
|
||||
Export Report
|
||||
</button>
|
||||
|
||||
{/* Header Section without background */}
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between px-1">
|
||||
<div>
|
||||
<h1 class="text-[32px] font-bold leading-tight text-[#0A1128]">Dashboard Overview</h1>
|
||||
<p class="mt-1 text-[15px] text-[#64748B]">Welcome back! Here's what's happening with your platform today.</p>
|
||||
</div>
|
||||
</section>
|
||||
<button class="inline-flex h-12 items-center justify-center gap-2 rounded-full bg-[#0A1128] px-6 text-[14px] font-semibold text-white transition-colors hover:bg-[#1E293B]">
|
||||
<Download size={18} strokeWidth={2.5} />
|
||||
Export Report
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Show when={data.loading}>
|
||||
<div class="flex items-center justify-center p-12 text-[#8087a0]">Loading metrics...</div>
|
||||
<div class="flex items-center justify-center p-12 text-[#64748B]">Loading metrics...</div>
|
||||
</Show>
|
||||
|
||||
<Show when={!data.loading && data()}>
|
||||
<section class="grid gap-4 xl:grid-cols-4">
|
||||
|
||||
{/* KPI Cards */}
|
||||
<section class="grid gap-6 xl:grid-cols-4">
|
||||
<For each={kpis()}>
|
||||
{(item: any) => (
|
||||
<article class="rounded-3xl border border-[#d9dde6] bg-[#f7f7f8] p-5 shadow-[0_0_0_1px_rgba(0,0,0,0.01)]">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="inline-flex h-10 w-10 items-center justify-center rounded-xl bg-[#fff1ea] text-xs font-bold text-[#fd6116]">
|
||||
{item.id === 'users' ? 'US' : item.id === 'companies' ? 'CP' : item.id === 'leads' ? 'LD' : 'CR'}
|
||||
{(item: any) => {
|
||||
const isUsers = item.id === 'users';
|
||||
const isCompanies = item.id === 'companies';
|
||||
const isLeads = item.id === 'leads';
|
||||
const Icon = isUsers ? Users : isCompanies ? Building2 : isLeads ? TrendingUp : CreditCard;
|
||||
|
||||
return (
|
||||
<article class="flex flex-col rounded-2xl border border-[#E2E8F0] bg-white p-6 shadow-sm">
|
||||
<div class="flex items-start justify-between">
|
||||
<Icon size={32} class="text-[#FA5A1F]" strokeWidth={2} />
|
||||
<div
|
||||
class={`flex items-center gap-1 rounded-full px-2.5 py-1 text-[13px] font-bold ${
|
||||
item.trendUp ? 'bg-[#FFF5F0] text-[#FA5A1F]' : 'bg-[#F1F5F9] text-[#0A1128]'
|
||||
}`}
|
||||
>
|
||||
<Show when={item.trendUp} fallback={<ArrowDownRight size={14} strokeWidth={2.5} />}>
|
||||
<ArrowUpRight size={14} strokeWidth={2.5} />
|
||||
</Show>
|
||||
{item.trend}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class={`inline-flex items-center rounded-xl px-2.5 py-1 text-xs font-semibold ${
|
||||
item.trendUp ? 'bg-[#ffe8dc] text-[#fd6116]' : 'bg-[#eceff6] text-[#383e5c]'
|
||||
}`}
|
||||
>
|
||||
{item.trend}
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-5 text-[15px] text-[#747a93]">{item.title}</p>
|
||||
<p class="mt-1 text-[44px] font-semibold leading-none text-[#050026]">{item.value}</p>
|
||||
<p class="mt-1 text-[14px] text-[#8a90a8]">{item.trendUp ? 'Increased from last month' : 'Decreased from last month'}</p>
|
||||
</article>
|
||||
)}
|
||||
|
||||
<div class="mt-4">
|
||||
<p class="text-[14px] text-[#64748B]">{item.title}</p>
|
||||
<p class="mt-2 text-[32px] font-bold tracking-tight text-[#0A1128]">{item.value}</p>
|
||||
</div>
|
||||
|
||||
<p class="mt-3 text-[13px] text-[#94A3B8]">
|
||||
<span class={`font-medium ${item.trendUp ? 'text-[#FA5A1F]' : 'text-[#64748B]'}`}>
|
||||
{item.trendUp ? '+1,245' : '-27'}
|
||||
</span>{' '}
|
||||
from last month
|
||||
</p>
|
||||
</article>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</section>
|
||||
|
||||
<section class="grid gap-4 xl:grid-cols-2">
|
||||
<article class="rounded-3xl border border-[#d9dde6] bg-[#f7f7f8] p-5">
|
||||
<h2 class="text-[34px] font-semibold leading-[1.1] text-[#050026]">Leads Trend</h2>
|
||||
<p class="mt-1 text-[14px] text-[#8087a0]">Monthly leads performance overview</p>
|
||||
<div class="mt-5 rounded-2xl border border-[#e2e6ee] bg-[#f5f5f6] p-4">
|
||||
<div class="relative h-52">
|
||||
<div class="absolute inset-0">
|
||||
<For each={[0, 1, 2, 3]}>{() => <div class="h-1/4 border-b border-dashed border-[#d9dde6]" />}</For>
|
||||
</div>
|
||||
<svg viewBox="0 0 100 40" class="relative h-full w-full overflow-visible" preserveAspectRatio="none" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="trendFill" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#fd6116" stop-opacity="0.28" />
|
||||
<stop offset="100%" stop-color="#fd6116" stop-opacity="0.02" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#050026"
|
||||
stroke-width="1.1"
|
||||
points={trendSeries().map((v: number, i: number) => `${i * 20},${40 - v / 3}`).join(' ')}
|
||||
/>
|
||||
<polygon
|
||||
fill="url(#trendFill)"
|
||||
points={`0,40 ${trendSeries().map((v: number, i: number) => `${i * 20},${40 - v / 3}`).join(' ')} 100,40`}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mt-1 grid grid-cols-6 text-center text-xs font-semibold text-[#3f4562]">
|
||||
<For each={['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']}>{(day) => <span>{day}</span>}</For>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="rounded-3xl border border-[#d9dde6] bg-[#f7f7f8] p-5">
|
||||
<h2 class="text-[34px] font-semibold leading-[1.1] text-[#050026]">Revenue Overview</h2>
|
||||
<p class="mt-1 text-[14px] text-[#8087a0]">Monthly revenue vs expenses comparison</p>
|
||||
<div class="mt-5 rounded-2xl border border-[#e2e6ee] bg-[#f5f5f6] p-4">
|
||||
<div class="relative h-52">
|
||||
<div class="absolute inset-0">
|
||||
<For each={[0, 1, 2, 3]}>{() => <div class="h-1/4 border-b border-dashed border-[#d9dde6]" />}</For>
|
||||
</div>
|
||||
<div class="relative flex h-full items-end gap-4 px-2">
|
||||
<For each={revSeries()}>
|
||||
{(value: number) => (
|
||||
<div class="flex h-full flex-1 items-end justify-center">
|
||||
<div class="w-2.5 rounded-t bg-[#050026]" style={{ height: `${(value / maxAmount) * 100}%` }} />
|
||||
{/* Charts Row */}
|
||||
<section class="grid gap-6 xl:grid-cols-2">
|
||||
|
||||
{/* Leads Trend Card */}
|
||||
<article class="rounded-2xl border border-[#E2E8F0] bg-white p-6 shadow-sm">
|
||||
<h2 class="text-[20px] font-bold text-[#0A1128]">Leads Trend</h2>
|
||||
<p class="mt-1 text-[14px] text-[#64748B]">Monthly leads performance overview</p>
|
||||
|
||||
<div class="mt-8">
|
||||
<div class="relative h-[250px]">
|
||||
{/* Y-Axis labels and grid */}
|
||||
<div class="absolute inset-0 flex flex-col justify-between pt-1 pb-6">
|
||||
<For each={[120, 90, 60, 30, 0]}>
|
||||
{(val) => (
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="w-8 text-right text-[12px] font-bold text-[#0A1128]">{val}</span>
|
||||
<div class="h-px flex-1 border-b border-dashed border-[#E2E8F0]" />
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 grid grid-cols-6 text-center text-xs font-semibold text-[#3f4562]">
|
||||
<For each={['Wk 1', 'Wk 2', 'Wk 3', 'Wk 4']}>{(week) => <span>{week}</span>}</For>
|
||||
|
||||
{/* Line Chart Component */}
|
||||
<div class="absolute inset-0 ml-12 mb-6">
|
||||
<svg viewBox="0 0 100 40" class="h-full w-full overflow-visible" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<linearGradient id="trendFill" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#0A1128" stop-opacity="0.3" />
|
||||
<stop offset="100%" stop-color="#0A1128" stop-opacity="0.0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<polygon
|
||||
fill="url(#trendFill)"
|
||||
points={`0,40 ${trendSeries().map((v: number, i: number) => `${i * (100 / 6)},${40 - v / 3}`).join(' ')} 100,40`}
|
||||
/>
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#0A1128"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
points={trendSeries().map((v: number, i: number) => `${i * (100 / 6)},${40 - v / 3}`).join(' ')}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* X-Axis labels */}
|
||||
<div class="absolute bottom-0 left-12 right-0 flex justify-between px-2 text-[12px] font-bold text-[#0A1128]">
|
||||
<For each={['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']}>
|
||||
{(month) => <span>{month}</span>}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="rounded-3xl border border-[#d9dde6] bg-[#f7f7f8] p-1">
|
||||
<div class="flex flex-col gap-3 px-5 py-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h2 class="text-[32px] font-semibold leading-[1.1] text-[#050026]">Recent Leads</h2>
|
||||
<p class="mt-1 text-[14px] text-[#8087a0]">Latest customer inquiries and opportunities</p>
|
||||
{/* Revenue Overview Card */}
|
||||
<article class="rounded-2xl border border-[#E2E8F0] bg-white p-6 shadow-sm">
|
||||
<h2 class="text-[20px] font-bold text-[#0A1128]">Revenue Overview</h2>
|
||||
<p class="mt-1 text-[14px] text-[#64748B]">Monthly revenue vs expenses comparison</p>
|
||||
|
||||
<div class="mt-8">
|
||||
<div class="relative h-[250px]">
|
||||
{/* Y-Axis labels and grid */}
|
||||
<div class="absolute inset-0 flex flex-col justify-between pt-1 pb-6">
|
||||
<For each={[80000, 60000, 40000, 20000, 0]}>
|
||||
{(val) => (
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="w-12 text-right text-[12px] font-bold text-[#0A1128]">{val}</span>
|
||||
<div class="h-px flex-1 border-b border-dashed border-[#E2E8F0]" />
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
{/* Bar Chart Component */}
|
||||
<div class="absolute inset-0 ml-16 mb-6 mt-1 flex items-end justify-between px-4">
|
||||
<For each={revSeries()}>
|
||||
{(value: number) => (
|
||||
<div class="group relative flex h-full w-full justify-center">
|
||||
<div
|
||||
class="absolute bottom-0 w-4 rounded-t-full bg-[#0A1128] transition-all hover:bg-[#FA5A1F]"
|
||||
style={{ height: `${(value / maxAmount) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
{/* X-Axis labels */}
|
||||
<div class="absolute bottom-0 left-16 right-0 flex justify-between px-4 text-[12px] font-bold text-[#0A1128]">
|
||||
<For each={['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']}>
|
||||
{(month) => <span class="w-4 text-center">{month}</span>}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="inline-flex h-10 items-center rounded-2xl border border-[#d9dde6] bg-white px-4 text-sm font-semibold text-[#050026] hover:bg-[#f8f9fc]">
|
||||
View All Leads
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div class="overflow-x-auto px-1 pb-1">
|
||||
<table class="w-full min-w-[860px] border-collapse overflow-hidden rounded-2xl">
|
||||
<thead>
|
||||
<tr class="bg-[#eef1f7] text-left">
|
||||
<th class="px-5 py-3 text-[12px] font-semibold uppercase tracking-wide text-[#616985]">Lead Title</th>
|
||||
<th class="px-5 py-3 text-[12px] font-semibold uppercase tracking-wide text-[#616985]">Customer</th>
|
||||
<th class="px-5 py-3 text-[12px] font-semibold uppercase tracking-wide text-[#616985]">Category</th>
|
||||
<th class="px-5 py-3 text-[12px] font-semibold uppercase tracking-wide text-[#616985]">Budget</th>
|
||||
<th class="px-5 py-3 text-[12px] font-semibold uppercase tracking-wide text-[#616985]">Status</th>
|
||||
<th class="px-5 py-3 text-[12px] font-semibold uppercase tracking-wide text-[#616985]">Date</th>
|
||||
<th class="px-5 py-3 text-[12px] font-semibold uppercase tracking-wide text-[#616985]">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<For each={leadRows()}>
|
||||
{(row: any) => (
|
||||
<tr class="border-t border-[#e1e6f0] bg-white transition-colors hover:bg-[#f8f9fc]">
|
||||
<td class="px-5 py-3 text-sm font-medium text-[#050026]">{row.service}</td>
|
||||
<td class="px-5 py-3 text-sm text-[#3c4260]">{row.client}</td>
|
||||
<td class="px-5 py-3 text-sm text-[#3c4260]">{row.service}</td>
|
||||
<td class="px-5 py-3 text-sm font-semibold text-[#050026]">{row.value}</td>
|
||||
<td class="px-5 py-3 text-sm text-[#3c4260]">{row.status}</td>
|
||||
<td class="px-5 py-3 text-sm text-[#3c4260]">{row.date}</td>
|
||||
<td class="px-5 py-3 text-sm">
|
||||
<button class="rounded-lg border border-[#d9dde6] bg-white px-3 py-1.5 font-medium text-[#050026] hover:bg-[#f8f9fc]">
|
||||
Open
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { A } from '@solidjs/router';
|
||||
import { createResource, createSignal, Show } from 'solid-js';
|
||||
import { createResource, createSignal, createMemo, Show, For } from 'solid-js';
|
||||
import { Search } from 'lucide-solid';
|
||||
import AdminShell from '~/components/AdminShell';
|
||||
import OnboardingManagementTabs from '~/components/admin/OnboardingManagementTabs';
|
||||
|
||||
const API = '/api/gateway';
|
||||
|
||||
|
|
@ -75,6 +75,7 @@ export default function OnboardingSchemasPage() {
|
|||
const [schemas, { refetch }] = createResource(loadSchemas);
|
||||
const [deleteError, setDeleteError] = createSignal('');
|
||||
const [deleting, setDeleting] = createSignal('');
|
||||
const [search, setSearch] = createSignal('');
|
||||
|
||||
const handleDelete = async (id: string, title: string) => {
|
||||
if (!confirm(`Delete onboarding flow "${title}"?`)) return;
|
||||
|
|
@ -85,112 +86,176 @@ export default function OnboardingSchemasPage() {
|
|||
if (!res.ok) throw new Error('Failed to delete');
|
||||
refetch();
|
||||
} catch (err: any) {
|
||||
setDeleteError(err.message || 'Failed to delete onboarding flow');
|
||||
setDeleteError(err.message || 'Failed to delete onbaording flow');
|
||||
} finally {
|
||||
setDeleting('');
|
||||
}
|
||||
};
|
||||
|
||||
const filtered = createMemo(() => {
|
||||
const q = search().toLowerCase();
|
||||
const all = schemas() || [];
|
||||
if (!q) return all;
|
||||
return all.filter((s) => s.title.toLowerCase().includes(q) || s.roleKey.toLowerCase().includes(q) || s.id.toLowerCase().includes(q));
|
||||
});
|
||||
|
||||
const activeWorkflows = createMemo(() => (schemas() || []).length);
|
||||
const publishedFlows = createMemo(() => (schemas() || []).filter(s => s.status === 'PUBLISHED').length);
|
||||
const draftFlows = createMemo(() => (schemas() || []).filter(s => s.status === 'DRAFT').length);
|
||||
const roleTypes = createMemo(() => {
|
||||
const roles = new Set((schemas() || []).map(s => s.roleKey).filter(Boolean));
|
||||
return roles.size;
|
||||
});
|
||||
|
||||
return (
|
||||
<AdminShell>
|
||||
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
|
||||
<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 class="space-y-6 max-w-[1600px]">
|
||||
|
||||
{/* Header */}
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between px-1">
|
||||
<div>
|
||||
<h1 class="text-[32px] font-bold leading-tight text-[#0A1128]">Onboarding Management</h1>
|
||||
<p class="mt-1 text-[15px] text-[#64748B]">Manage onboarding flows, role assignments, and previewable step groups for external users.</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
class="inline-flex h-11 items-center justify-center rounded-xl border border-[#E2E8F0] bg-white px-6 text-[14px] font-semibold text-[#0A1128] transition-colors hover:bg-[#F8FAFC]"
|
||||
>
|
||||
Refresh List
|
||||
</button>
|
||||
<A
|
||||
href="/admin/onboarding-schemas/new"
|
||||
class="inline-flex h-11 items-center justify-center rounded-xl bg-[#0A1128] px-6 text-[14px] font-semibold text-white transition-colors hover:bg-[#1E293B]"
|
||||
>
|
||||
<span class="mr-2 text-lg leading-none">+</span> Create Onboarding Flow
|
||||
</A>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 4 KPI Cards Grid */}
|
||||
<section class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
|
||||
<p class="text-[14px] leading-tight text-[#64748B]">Total Active<br/>Workflows</p>
|
||||
<p class="mt-4 text-[32px] font-bold leading-none text-[#0A1128]">{schemas.loading ? '—' : activeWorkflows()}</p>
|
||||
</div>
|
||||
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
|
||||
<p class="text-[14px] leading-tight text-[#64748B]">Published<br/>Flows</p>
|
||||
<p class="mt-4 text-[32px] font-bold leading-none text-[#16A34A]">{schemas.loading ? '—' : publishedFlows()}</p>
|
||||
</div>
|
||||
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
|
||||
<p class="text-[14px] leading-tight text-[#64748B]">Draft<br/>Workflows</p>
|
||||
<p class="mt-4 text-[32px] font-bold leading-none text-[#FA5A1F]">{schemas.loading ? '—' : draftFlows()}</p>
|
||||
</div>
|
||||
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
|
||||
<p class="text-[14px] leading-tight text-[#64748B]">Role Types<br/>Configured</p>
|
||||
<p class="mt-4 text-[32px] font-bold leading-none text-[#2563EB]">{schemas.loading ? '—' : roleTypes()}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Table Section */}
|
||||
<section class="rounded-2xl border border-[#E2E8F0] bg-white p-6 shadow-sm">
|
||||
|
||||
<Show when={deleteError()}>
|
||||
<div class="mb-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-[14px] text-red-700">{deleteError()}</div>
|
||||
</Show>
|
||||
|
||||
{/* Filters Bar */}
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center mb-6">
|
||||
<div class="relative w-full max-w-sm">
|
||||
<Search size={18} class="absolute left-3.5 top-1/2 -translate-y-1/2 text-[#94A3B8]" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search flows by role or id..."
|
||||
value={search()}
|
||||
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||
class="h-11 w-full rounded-xl border border-[#E2E8F0] bg-[#F8FAFC] pl-10 pr-4 text-[14px] outline-none transition-colors focus:border-[#CBD5E1] focus:bg-white text-[#0A1128]"
|
||||
/>
|
||||
</div>
|
||||
<select class="h-11 w-48 rounded-xl border border-[#E2E8F0] bg-white px-4 text-[14px] text-[#0A1128] outline-none">
|
||||
<option>Status: All</option>
|
||||
<option>Published</option>
|
||||
<option>Draft</option>
|
||||
</select>
|
||||
<select class="h-11 w-48 rounded-xl border border-[#E2E8F0] bg-white px-4 text-[14px] text-[#0A1128] outline-none">
|
||||
<option>Sort: Last Modified</option>
|
||||
<option>Sort: Name</option>
|
||||
</select>
|
||||
</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"
|
||||
>
|
||||
<div class="overflow-x-auto rounded-xl border border-[#E2E8F0]">
|
||||
<table class="w-full min-w-[800px] border-collapse bg-white text-left">
|
||||
<thead>
|
||||
<tr class="bg-[#0A1128] text-white">
|
||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">FLOW NAME</th>
|
||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">ROLE KEY</th>
|
||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">STEPS</th>
|
||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">VERSION</th>
|
||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">STATUS</th>
|
||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white text-right">ACTIONS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={schemas.loading}>
|
||||
<tr><td colspan="6" class="text-center py-12 text-[#64748B] 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.</td></tr>
|
||||
</Show>
|
||||
<Show when={!schemas.loading && !schemas.error && filtered().length === 0}>
|
||||
<tr><td colspan="6" class="text-center py-12 text-[#64748B] text-[14px]">No onboarding flows found.</td></tr>
|
||||
</Show>
|
||||
<Show when={!schemas.loading && !schemas.error && filtered().length > 0}>
|
||||
<For each={filtered()}>
|
||||
{(schema) => (
|
||||
<tr class="border-b border-[#E2E8F0] transition-colors hover:bg-[#F8FAFC]">
|
||||
<td class="px-6 py-4 text-[14px] font-bold text-[#0A1128]">{schema.title}</td>
|
||||
<td class="px-6 py-4 text-[14px] text-[#475569]">{schema.roleKey || '—'}</td>
|
||||
<td class="px-6 py-4 text-[14px] font-semibold text-[#475569]">
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-lg bg-[#F1F5F9] text-[#475569] text-[12px] font-bold border border-[#E2E8F0]">
|
||||
{schema.stepCount} Steps
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-[14px] text-[#475569]">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-bold ${schema.status === 'PUBLISHED' ? 'bg-[#DCFCE7] text-[#16A34A]' : 'bg-[#FFF5F0] text-[#FA5A1F]'}`}>
|
||||
{schema.status === 'PUBLISHED' ? 'Published' : 'Draft'}
|
||||
</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-[#E2E8F0] bg-white text-[#64748B] hover:bg-[#F8FAFC] hover:text-[#0A1128] 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-[#E2E8F0] 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="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" />
|
||||
<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>
|
||||
</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>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</AdminShell>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { A } from '@solidjs/router';
|
||||
import { createMemo, createResource, For, Show } from 'solid-js';
|
||||
import { Search } from 'lucide-solid';
|
||||
import AdminShell from '~/components/AdminShell';
|
||||
|
||||
const API = '/api/gateway';
|
||||
|
|
@ -40,102 +41,141 @@ export default function VerificationStatusPage() {
|
|||
createdAt: r.createdAt || r.created_at || '',
|
||||
})));
|
||||
|
||||
function statusBadge(status: string) {
|
||||
if (status === 'APPROVED') return 'bg-green-100 text-green-800';
|
||||
if (status === 'REJECTED') return 'bg-red-100 text-red-700';
|
||||
if (status === 'PENDING') return 'bg-yellow-100 text-yellow-800';
|
||||
return 'bg-gray-100 text-gray-600';
|
||||
function statusDoc(status: string) {
|
||||
if (status === 'APPROVED') return 'Verified';
|
||||
if (status === 'REJECTED') return 'Flagged';
|
||||
if (status === 'PENDING') return 'Review';
|
||||
return 'Pending';
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminShell>
|
||||
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
|
||||
<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 class="space-y-6 max-w-[1600px]">
|
||||
{/* Header Configuration */}
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between px-1">
|
||||
<div>
|
||||
<div class="flex items-center gap-4">
|
||||
<h1 class="text-[32px] font-bold leading-tight text-[#0A1128]">Verification Management</h1>
|
||||
<div class="hidden items-center gap-3 md:flex">
|
||||
<button class="inline-flex h-10 items-center justify-center rounded-xl bg-[#0A1128] px-5 text-[14px] font-semibold text-white hover:bg-[#1E293B] transition-colors">
|
||||
Verification Queue
|
||||
</button>
|
||||
<button class="inline-flex h-10 items-center justify-center rounded-xl border border-[#E2E8F0] bg-white px-5 text-[14px] font-semibold text-[#0A1128] hover:bg-[#F8FAFC] transition-colors">
|
||||
Verification Rules
|
||||
</button>
|
||||
<button class="inline-flex h-10 items-center justify-center rounded-xl border border-[#E2E8F0] bg-white px-5 text-[14px] font-semibold text-[#0A1128] hover:bg-[#F8FAFC] transition-colors">
|
||||
User Preview
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-1 text-[15px] text-[#64748B]">Review and verify user submissions</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 6 KPI Cards Grid */}
|
||||
<section class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
||||
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
|
||||
<p class="text-[14px] leading-tight text-[#64748B]">Total<br/>Pending</p>
|
||||
<p class="mt-4 text-[32px] font-bold leading-none text-[#0A1128]">42</p>
|
||||
</div>
|
||||
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
|
||||
<p class="text-[14px] leading-tight text-[#64748B]">Identity<br/>Verification</p>
|
||||
<p class="mt-4 text-[32px] font-bold leading-none text-[#2563EB]">18</p>
|
||||
</div>
|
||||
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
|
||||
<p class="text-[14px] leading-tight text-[#64748B]">Business<br/>Verification</p>
|
||||
<p class="mt-4 text-[32px] font-bold leading-none text-[#7C3AED]">12</p>
|
||||
</div>
|
||||
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
|
||||
<p class="text-[14px] leading-tight text-[#64748B]">Re-upload<br/>Review</p>
|
||||
<p class="mt-4 text-[32px] font-bold leading-none text-[#FA5A1F]">8</p>
|
||||
</div>
|
||||
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
|
||||
<p class="text-[14px] leading-tight text-[#64748B]">Verified<br/>Today</p>
|
||||
<p class="mt-4 text-[32px] font-bold leading-none text-[#16A34A]">15</p>
|
||||
</div>
|
||||
<div class="flex flex-col justify-between rounded-2xl border border-[#E2E8F0] bg-white p-5 shadow-sm">
|
||||
<p class="text-[14px] leading-tight text-[#64748B]">Flagged<br/>Cases</p>
|
||||
<p class="mt-4 text-[32px] font-bold leading-none text-[#FA5A1F]">4</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Table Container */}
|
||||
<section class="rounded-2xl border border-[#E2E8F0] bg-white p-6 shadow-sm">
|
||||
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between mb-6">
|
||||
<h2 class="text-[20px] font-bold text-[#0A1128]">Verification Cases</h2>
|
||||
<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>
|
||||
<button class="inline-flex h-10 items-center justify-center rounded-xl border border-[#E2E8F0] px-4 text-[14px] font-semibold text-[#0A1128] hover:bg-[#F8FAFC]">
|
||||
Export Queue
|
||||
</button>
|
||||
<button class="inline-flex h-10 items-center justify-center rounded-xl border border-[#E2E8F0] px-4 text-[14px] font-semibold text-[#0A1128] hover:bg-[#F8FAFC]">
|
||||
Bulk Actions
|
||||
</button>
|
||||
</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 class="flex flex-col gap-4 md:flex-row md:items-center mb-6">
|
||||
<div class="relative w-full max-w-sm">
|
||||
<Search size={18} class="absolute left-3.5 top-1/2 -translate-y-1/2 text-[#94A3B8]" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or ID..."
|
||||
class="h-11 w-full rounded-xl border border-[#E2E8F0] bg-[#F8FAFC] pl-10 pr-4 text-[14px] outline-none transition-colors focus:border-[#CBD5E1] focus:bg-white"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="h-11 w-48 rounded-xl border border-[#E2E8F0] bg-white" />
|
||||
<div class="h-11 w-48 rounded-xl border border-[#E2E8F0] bg-white" />
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto rounded-xl border border-[#E2E8F0]">
|
||||
<table class="w-full min-w-[1000px] border-collapse bg-white text-left">
|
||||
<thead>
|
||||
<tr class="bg-[#0A1128] text-white">
|
||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">VERIFICATION ID</th>
|
||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">APPLICANT NAME</th>
|
||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">USER TYPE</th>
|
||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">VERIFICATION TYPE</th>
|
||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">SUBMITTED DATE</th>
|
||||
<th class="px-6 py-4 text-[12px] font-bold uppercase tracking-wider text-white">DOCUMENT STATUS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={rows.loading}>
|
||||
<tr><td colspan="6" class="text-center py-12 text-[#64748B] text-[14px]">Loading verification cases...</td></tr>
|
||||
</Show>
|
||||
<Show when={!rows.loading && normalized().length === 0}>
|
||||
<tr><td colspan="6" class="text-center py-12 text-[#64748B] text-[14px]">No pending verification cases found.</td></tr>
|
||||
</Show>
|
||||
<For each={normalized()}>
|
||||
{(item) => (
|
||||
<tr class="border-b border-[#E2E8F0] transition-colors hover:bg-[#F8FAFC]">
|
||||
<td class="px-6 py-4 text-[14px] font-bold text-[#0A1128]">VER—2024—{item.id.slice(0, 3)}</td>
|
||||
<td class="px-6 py-4 text-[14px] font-bold text-[#0A1128]">{item.requesterName}</td>
|
||||
<td class="px-6 py-4 text-[14px] text-[#475569]">Professional</td>
|
||||
<td class="px-6 py-4 text-[14px] text-[#475569]">Identity Verification</td>
|
||||
<td class="px-6 py-4 text-[14px] text-[#475569]">
|
||||
{item.createdAt ? new Date(item.createdAt).toISOString().split('T')[0] : '2024-03-20'}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<A
|
||||
href={`/admin/verification-status/${item.id}`}
|
||||
class={`inline-flex items-center rounded-lg px-3 py-1.5 text-[13px] font-bold transition-opacity hover:opacity-80 ${
|
||||
statusDoc(item.status) === 'Verified' ? 'bg-[#DCFCE7] text-[#16A34A]' :
|
||||
statusDoc(item.status) === 'Flagged' ? 'bg-[#FEE2E2] text-[#EF4444]' :
|
||||
'bg-[#F1F5F9] text-[#64748B]'
|
||||
}`}
|
||||
>
|
||||
{statusDoc(item.status)}
|
||||
</A>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</AdminShell>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue