Refine external preview flows and simplify verification/approval management
This commit is contained in:
parent
8a24700e73
commit
ad8a17a766
5 changed files with 2316 additions and 452 deletions
|
|
@ -1,24 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -u
|
||||
|
||||
ROOT_DIR="/Users/ashwin/workspace/nxtgauge-admin-solid"
|
||||
APP_LOG="/tmp/nxtgauge-admin-3000.log"
|
||||
|
||||
cd "$ROOT_DIR" || exit 1
|
||||
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] admin-3000 daemon started" >> "$APP_LOG"
|
||||
|
||||
while true; do
|
||||
if [[ ! -f ".output/server/index.mjs" ]]; then
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] build output missing, running build..." >> "$APP_LOG"
|
||||
npm run build >> "$APP_LOG" 2>&1
|
||||
fi
|
||||
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] launching admin on 0.0.0.0:3000" >> "$APP_LOG"
|
||||
HOST=0.0.0.0 PORT=3000 node .output/server/index.mjs >> "$APP_LOG" 2>&1
|
||||
code=$?
|
||||
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] admin exited with code ${code}, restarting in 2s" >> "$APP_LOG"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -8,6 +8,9 @@ type ApprovalRecord = CrudRecord & {
|
|||
applicantName?: string;
|
||||
approvalType: 'PROFILE' | 'BUSINESS' | 'JOB' | 'ORDER' | 'INVOICE' | 'COUPON' | 'DISCOUNT' | 'TAX' | 'ROLE' | 'REQUIREMENT';
|
||||
userType: 'CUSTOMER' | 'PROFESSIONAL' | 'COMPANY' | 'JOBSEEKER';
|
||||
roleTags?: string[];
|
||||
primaryService?: string;
|
||||
area?: string;
|
||||
submittedDate?: string;
|
||||
verificationStatus: 'PENDING' | 'VERIFIED' | 'FLAGGED';
|
||||
assignedApprover?: string;
|
||||
|
|
@ -15,6 +18,36 @@ type ApprovalRecord = CrudRecord & {
|
|||
status: 'PENDING' | 'IN_REVIEW' | 'APPROVED' | 'REJECTED' | 'ON_HOLD' | 'ESCALATED';
|
||||
};
|
||||
|
||||
const toTitle = (value: string) => String(value || '')
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
|
||||
function extractRoleTags(source: any): string[] {
|
||||
const values: string[] = [];
|
||||
const pushValue = (value: unknown) => {
|
||||
if (!value) return;
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((v) => pushValue(v));
|
||||
return;
|
||||
}
|
||||
const text = String(value || '').trim();
|
||||
if (!text) return;
|
||||
values.push(text);
|
||||
};
|
||||
pushValue(source?.role_key);
|
||||
pushValue(source?.roleKey);
|
||||
pushValue(source?.role_keys);
|
||||
pushValue(source?.roleKeys);
|
||||
pushValue(source?.roles);
|
||||
pushValue(source?.categories);
|
||||
pushValue(source?.category);
|
||||
pushValue(source?.service_category);
|
||||
pushValue(source?.serviceCategory);
|
||||
pushValue(source?.profession);
|
||||
pushValue(source?.service_type);
|
||||
return Array.from(new Set(values.map((v) => toTitle(v)))).slice(0, 4);
|
||||
}
|
||||
|
||||
function StatusBadge(props: { status: string }) {
|
||||
const getColors = () => {
|
||||
switch (props.status) {
|
||||
|
|
@ -64,8 +97,7 @@ function VerificationBadge(props: { status: ApprovalRecord['verificationStatus']
|
|||
}
|
||||
|
||||
export default function ApprovalManagementPage() {
|
||||
const [view, setView] = createSignal<'list' | 'form'>('list');
|
||||
const [listTab, setListTab] = createSignal<'all' | 'create' | 'view'>('all');
|
||||
const [listTab, setListTab] = createSignal<'all' | 'view' | 'escalated'>('all');
|
||||
const [detailTab, setDetailTab] = createSignal<'overview' | 'verification' | 'checklist' | 'logs'>('overview');
|
||||
|
||||
const [search, setSearch] = createSignal('');
|
||||
|
|
@ -80,25 +112,6 @@ export default function ApprovalManagementPage() {
|
|||
const [error, setError] = createSignal('');
|
||||
const [isActing, setIsActing] = createSignal(false);
|
||||
|
||||
type ApprovalRule = {
|
||||
id: string;
|
||||
name: string;
|
||||
approvalType: ApprovalRecord['approvalType'];
|
||||
status: 'ACTIVE' | 'INACTIVE';
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
const [rules, setRules] = createSignal<ApprovalRule[]>([
|
||||
{ id: 'ar1', name: 'Professional Profile Approval', approvalType: 'PROFILE', status: 'ACTIVE', updatedAt: '2026-03-20' },
|
||||
{ id: 'ar2', name: 'High Value Order Approval', approvalType: 'ORDER', status: 'ACTIVE', updatedAt: '2026-03-18' },
|
||||
{ id: 'ar3', name: 'Job Post Manual Approval', approvalType: 'JOB', status: 'INACTIVE', updatedAt: '2026-03-12' },
|
||||
]);
|
||||
|
||||
const [ruleName, setRuleName] = createSignal('');
|
||||
const [ruleType, setRuleType] = createSignal<ApprovalRecord['approvalType']>('PROFILE');
|
||||
const [ruleActive, setRuleActive] = createSignal(true);
|
||||
const [formError, setFormError] = createSignal('');
|
||||
|
||||
const load = async () => {
|
||||
setError('');
|
||||
try {
|
||||
|
|
@ -123,6 +136,9 @@ export default function ApprovalManagementPage() {
|
|||
applicantName: String(job.title || 'Untitled Job'),
|
||||
approvalType: 'JOB',
|
||||
userType: 'COMPANY',
|
||||
roleTags: extractRoleTags(job),
|
||||
primaryService: String(job.category || job.department || job.role || 'Job Posting'),
|
||||
area: String(job.location || job.city || job.work_mode || '—'),
|
||||
submittedDate: String(job.created_at || ''),
|
||||
verificationStatus: 'VERIFIED',
|
||||
assignedApprover: 'Unassigned',
|
||||
|
|
@ -137,6 +153,9 @@ export default function ApprovalManagementPage() {
|
|||
applicantName: String(req.title || 'Untitled Requirement'),
|
||||
approvalType: 'REQUIREMENT',
|
||||
userType: 'CUSTOMER',
|
||||
roleTags: extractRoleTags(req),
|
||||
primaryService: String(req.category || req.profession || req.service_type || 'Requirement'),
|
||||
area: String(req.location || req.city || req.area || '—'),
|
||||
submittedDate: String(req.created_at || ''),
|
||||
verificationStatus: 'VERIFIED',
|
||||
assignedApprover: 'Unassigned',
|
||||
|
|
@ -189,6 +208,8 @@ export default function ApprovalManagementPage() {
|
|||
return sorted;
|
||||
});
|
||||
|
||||
const escalatedCount = createMemo(() => rows().filter((r) => r.status === 'ESCALATED').length);
|
||||
|
||||
const exportCsv = () => {
|
||||
const headers = ['Approval ID', 'Applicant', 'Type', 'Verification', 'Priority', 'Status', 'Submitted Date'];
|
||||
const rowsData = filteredRows().map((row) => [
|
||||
|
|
@ -221,35 +242,6 @@ export default function ApprovalManagementPage() {
|
|||
setOpenMenuId(null);
|
||||
};
|
||||
|
||||
const resetRuleForm = () => {
|
||||
setRuleName('');
|
||||
setRuleType('PROFILE');
|
||||
setRuleActive(true);
|
||||
setFormError('');
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
resetRuleForm();
|
||||
setListTab('create');
|
||||
setView('form');
|
||||
};
|
||||
|
||||
const saveRule = () => {
|
||||
if (!ruleName().trim()) {
|
||||
setFormError('Rule name is required.');
|
||||
return;
|
||||
}
|
||||
const now = new Date().toISOString().slice(0, 10);
|
||||
const id = `ar_${Math.random().toString(16).slice(2)}`;
|
||||
setRules((prev) => [
|
||||
{ id, name: ruleName().trim(), approvalType: ruleType(), status: ruleActive() ? 'ACTIVE' : 'INACTIVE', updatedAt: now },
|
||||
...prev,
|
||||
]);
|
||||
setView('list');
|
||||
setListTab('all');
|
||||
resetRuleForm();
|
||||
};
|
||||
|
||||
const runApprovalAction = async (row: ApprovalRecord, action: 'approve' | 'reject') => {
|
||||
const type = row.approvalType;
|
||||
if (type !== 'JOB' && type !== 'REQUIREMENT') {
|
||||
|
|
@ -307,11 +299,11 @@ export default function ApprovalManagementPage() {
|
|||
</Show>
|
||||
|
||||
{/* ── LIST VIEW ── */}
|
||||
<Show when={view() === 'list'}>
|
||||
<Show when={true}>
|
||||
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
|
||||
{([
|
||||
{ key: 'all', label: 'All Approvals', action: () => { setListTab('all'); setStatusFilter('all'); } },
|
||||
{ key: 'create', label: 'Create Rule', action: () => openCreate() },
|
||||
{ key: 'escalated', label: `Escalated (${escalatedCount()})`, action: () => { setListTab('escalated'); setStatusFilter('escalated'); } },
|
||||
{ key: 'view', label: 'View Approval', action: () => { setListTab('view'); } },
|
||||
] as const).map((tab) => (
|
||||
<button
|
||||
|
|
@ -370,9 +362,23 @@ export default function ApprovalManagementPage() {
|
|||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
|
||||
<div><p style="font-size:11px;color:#9CA3AF">Entity Name</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.applicantName}</p></div>
|
||||
<div><p style="font-size:11px;color:#9CA3AF">Approval Type</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.approvalType}</p></div>
|
||||
<div><p style="font-size:11px;color:#9CA3AF">Primary Service</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.primaryService || '—'}</p></div>
|
||||
<div><p style="font-size:11px;color:#9CA3AF">Area / Place</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.area || '—'}</p></div>
|
||||
<div><p style="font-size:11px;color:#9CA3AF">Verification Status</p><VerificationBadge status={viewingCase()!.verificationStatus} /></div>
|
||||
<div><p style="font-size:11px;color:#9CA3AF">Assigned Approver</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.assignedApprover}</p></div>
|
||||
</div>
|
||||
<Show when={viewingCase()!.roleTags?.length}>
|
||||
<div style="margin-top:12px">
|
||||
<p style="margin:0 0 8px;font-size:11px;color:#9CA3AF">Registered Roles / Services</p>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:6px">
|
||||
<For each={viewingCase()!.roleTags || []}>
|
||||
{(tag) => (
|
||||
<span style="height:24px;padding:0 10px;border-radius:999px;border:1px solid #E5E7EB;background:#F9FAFB;display:inline-flex;align-items:center;font-size:11px;font-weight:600;color:#374151">{tag}</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px">
|
||||
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:16px">Approval Decision Path</h3>
|
||||
|
|
@ -539,7 +545,10 @@ export default function ApprovalManagementPage() {
|
|||
<td style="padding:12px 20px;font-size:12px;font-family:monospace;color:#6B7280">{row.id}</td>
|
||||
<td style="padding:12px 20px">
|
||||
<p style="font-size:14px;font-weight:600;color:#111827">{row.applicantName}</p>
|
||||
<p style="font-size:11px;color:#6B7280">{row.userType}</p>
|
||||
<p style="font-size:11px;color:#6B7280">{row.userType}{row.area ? ` • ${row.area}` : ''}</p>
|
||||
<Show when={row.roleTags?.length}>
|
||||
<p style="margin-top:2px;font-size:11px;color:#9CA3AF">{(row.roleTags || []).join(', ')}</p>
|
||||
</Show>
|
||||
</td>
|
||||
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{row.approvalType}</td>
|
||||
<td style="padding:12px 20px"><VerificationBadge status={row.verificationStatus} /></td>
|
||||
|
|
@ -568,78 +577,6 @@ export default function ApprovalManagementPage() {
|
|||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={view() === 'form'}>
|
||||
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
|
||||
<button type="button" onClick={() => { setView('list'); setListTab('all'); }} style="padding-bottom:12px;font-size:14px;font-weight:500;color:#6B7280;background:none;border:none;cursor:pointer">
|
||||
All Approvals
|
||||
</button>
|
||||
<button type="button" style="padding-bottom:12px;font-size:14px;font-weight:500;color:#FF5E13;border:none;border-bottom:2px solid #FF5E13;background:none;cursor:pointer;margin-bottom:-1px">
|
||||
Create Rule
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
||||
<div style="padding:24px">
|
||||
<Show when={formError()}>
|
||||
<div style="margin-bottom:20px;border-radius:10px;border:1px solid #FECACA;background:#FEF2F2;padding:12px 16px;font-size:13px;color:#B91C1C">
|
||||
{formError()}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
|
||||
<label style="display:block">
|
||||
<span style="font-size:13px;font-weight:600;color:#374151">Rule Name<span style="margin-left:2px;color:#FF5E13">*</span></span>
|
||||
<input value={ruleName()} onInput={(e) => setRuleName(e.currentTarget.value)} placeholder="e.g. High Value Order Approval" style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:13px;color:#111827;outline:none;box-sizing:border-box" />
|
||||
</label>
|
||||
<label style="display:block">
|
||||
<span style="font-size:13px;font-weight:600;color:#374151">Approval Type</span>
|
||||
<select value={ruleType()} onChange={(e) => setRuleType(e.currentTarget.value as ApprovalRecord['approvalType'])} style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none;box-sizing:border-box">
|
||||
{(['PROFILE', 'BUSINESS', 'JOB', 'ORDER', 'INVOICE', 'COUPON', 'DISCOUNT', 'TAX', 'ROLE'] as const).map((t) => <option value={t}>{t}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:20px">
|
||||
<p style="font-size:13px;font-weight:600;color:#374151">Rule Status</p>
|
||||
<div style="margin-top:8px;display:flex;gap:10px">
|
||||
<button type="button" onClick={() => setRuleActive(true)} style={`height:38px;border-radius:10px;padding:0 20px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid ${ruleActive() ? '#FF5E13' : '#E5E7EB'};background:${ruleActive() ? '#FFF3EE' : 'white'};color:${ruleActive() ? '#FF5E13' : '#6B7280'}`}>Active</button>
|
||||
<button type="button" onClick={() => setRuleActive(false)} style={`height:38px;border-radius:10px;padding:0 20px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid ${!ruleActive() ? '#FF5E13' : '#E5E7EB'};background:${!ruleActive() ? '#FFF3EE' : 'white'};color:${!ruleActive() ? '#FF5E13' : '#6B7280'}`}>Inactive</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:24px;display:flex;justify-content:flex-end;gap:10px">
|
||||
<button type="button" onClick={() => { setView('list'); setListTab('all'); resetRuleForm(); }} style="height:40px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer">Cancel</button>
|
||||
<button type="button" onClick={saveRule} style="height:40px;border-radius:10px;background:#0D0D2A;padding:0 18px;font-size:13px;font-weight:700;color:white;border:none;cursor:pointer">Save Rule</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:24px;border:1px solid #E5E7EB;border-radius:16px;background:white;overflow:hidden">
|
||||
<table style="width:100%;border-collapse:collapse">
|
||||
<thead style="background:#0D0D2A">
|
||||
<tr style="text-align:left">
|
||||
<th style="padding:12px 20px;font-size:11px;font-weight:600;color:white;text-transform:uppercase">Rule Name</th>
|
||||
<th style="padding:12px 20px;font-size:11px;font-weight:600;color:white;text-transform:uppercase">Approval Type</th>
|
||||
<th style="padding:12px 20px;font-size:11px;font-weight:600;color:white;text-transform:uppercase">Status</th>
|
||||
<th style="padding:12px 20px;font-size:11px;font-weight:600;color:white;text-transform:uppercase">Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<For each={rules()}>
|
||||
{(rule) => (
|
||||
<tr style="border-top:1px solid #E5E7EB">
|
||||
<td style="padding:14px 20px;font-size:14px;font-weight:600;color:#111827">{rule.name}</td>
|
||||
<td style="padding:14px 20px;font-size:13px;color:#6B7280">{rule.approvalType}</td>
|
||||
<td style="padding:14px 20px"><StatusBadge status={rule.status} /></td>
|
||||
<td style="padding:14px 20px;font-size:13px;color:#6B7280">{formatDate(rule.updatedAt)}</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
</div>
|
||||
</AdminShell>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ const AVAILABLE_FIELDS = [
|
|||
'approval_status',
|
||||
'documents_submitted',
|
||||
];
|
||||
const PORTFOLIO_SIDEBAR_ITEM = 'My Portfolio';
|
||||
|
||||
function normalizeToken(value: string): string {
|
||||
return String(value || '').trim().toLowerCase();
|
||||
|
|
@ -104,6 +105,34 @@ function humanizeLabel(value: string): string {
|
|||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
function hasSidebarItem(items: string[], value: string): boolean {
|
||||
const needle = normalizeToken(value);
|
||||
return items.some((item) => normalizeToken(item) === needle);
|
||||
}
|
||||
|
||||
function applyPortfolioRule(items: string[], persona: 'PROFESSIONAL' | 'COMPANY' | 'JOB_SEEKER' | 'CUSTOMER' | null): string[] {
|
||||
if (!items.length) return items;
|
||||
|
||||
if (persona === 'PROFESSIONAL') {
|
||||
if (hasSidebarItem(items, PORTFOLIO_SIDEBAR_ITEM)) return items;
|
||||
const insertAfter = items.findIndex((item) => normalizeToken(item) === 'my profile');
|
||||
if (insertAfter >= 0) {
|
||||
const next = [...items];
|
||||
next.splice(insertAfter + 1, 0, PORTFOLIO_SIDEBAR_ITEM);
|
||||
return next;
|
||||
}
|
||||
return [...items, PORTFOLIO_SIDEBAR_ITEM];
|
||||
}
|
||||
|
||||
if (!hasSidebarItem(items, PORTFOLIO_SIDEBAR_ITEM)) return items;
|
||||
return items.filter((item) => normalizeToken(item) !== normalizeToken(PORTFOLIO_SIDEBAR_ITEM));
|
||||
}
|
||||
|
||||
function sameSidebarOrder(a: string[], b: string[]): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
return a.every((item, idx) => normalizeToken(item) === normalizeToken(b[idx] || ''));
|
||||
}
|
||||
|
||||
function rolePreviewPath(roleKey: string): string {
|
||||
const key = String(roleKey || '').toUpperCase();
|
||||
if (key.includes('COMPANY')) return '/employers/dashboard';
|
||||
|
|
@ -234,10 +263,14 @@ export default function ExternalDashboardManagementPage() {
|
|||
// Fallback: derive persona directly from the stored role key (works when roles API is empty or roleId unmatched)
|
||||
const personaByKey = personaFromKey(selectedRoleKey() || formRoleKey());
|
||||
const persona = personaById ?? personaByKey;
|
||||
if (persona && ROLE_BASED_SIDEBAR[persona]?.length) return ROLE_BASED_SIDEBAR[persona];
|
||||
if (sidebarLooksCustomer()) return ROLE_BASED_SIDEBAR.CUSTOMER;
|
||||
if (sidebarItems().length) return sidebarItems();
|
||||
return ['My Dashboard', 'My Profile', 'Switch Services', 'Logout'];
|
||||
const baseItems = sidebarItems().length
|
||||
? sidebarItems()
|
||||
: (persona && ROLE_BASED_SIDEBAR[persona]?.length)
|
||||
? ROLE_BASED_SIDEBAR[persona]
|
||||
: sidebarLooksCustomer()
|
||||
? ROLE_BASED_SIDEBAR.CUSTOMER
|
||||
: ['My Dashboard', 'My Profile', 'Switch Services', 'Logout'];
|
||||
return applyPortfolioRule(baseItems, persona ?? (sidebarLooksCustomer() ? 'CUSTOMER' : null));
|
||||
});
|
||||
const previewTabs = createMemo(() => (tabs().length ? tabs() : ['overview']));
|
||||
|
||||
|
|
@ -386,6 +419,16 @@ export default function ExternalDashboardManagementPage() {
|
|||
applyPreviewPathForRole(selected, false);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (view() !== 'form') return;
|
||||
const persona = rolePersonaById()[roleId()] ?? personaFromKey(selectedRoleKey() || formRoleKey());
|
||||
if (!persona || !sidebarItems().length) return;
|
||||
setSidebarItems((prev) => {
|
||||
const next = applyPortfolioRule(prev, persona);
|
||||
return sameSidebarOrder(prev, next) ? prev : next;
|
||||
});
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const items = previewSidebarItems();
|
||||
if (!items.includes(activePreviewSidebar())) setActivePreviewSidebar(items[0] || '');
|
||||
|
|
@ -478,7 +521,7 @@ export default function ExternalDashboardManagementPage() {
|
|||
|
||||
<div style="display:flex;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 16px;background:#FAFAFA">
|
||||
{(['general', 'tabs', 'sidebar', 'fields', 'preview'] as const).map((tab) => (
|
||||
<button type="button" onClick={() => setFormTab(tab)} style={`position:relative;padding:12px 10px;font-size:12px;font-weight:700;border:none;background:none;cursor:pointer;color:${formTab() === tab ? '#FF5E13' : '#6B7280'}`}>
|
||||
<button type="button" onClick={() => setFormTab(tab)} style={`position:relative;padding:12px 10px;font-size:12px;font-weight:500;border:none;background:none;cursor:pointer;color:${formTab() === tab ? '#FF5E13' : '#6B7280'}`}>
|
||||
{humanizeLabel(tab)}
|
||||
<Show when={formTab() === tab}><span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13" /></Show>
|
||||
</button>
|
||||
|
|
@ -570,27 +613,6 @@ export default function ExternalDashboardManagementPage() {
|
|||
|
||||
<Show when={formTab() === 'preview'}>
|
||||
<div style="display:flex;flex-direction:column;gap:10px">
|
||||
<div style="border:1px solid #E5E7EB;border-radius:12px;background:#F9FAFB;padding:10px 12px">
|
||||
<p style="margin:0;font-size:12px;font-weight:800;color:#374151">Config Snapshot (Instant)</p>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:8px">
|
||||
<div>
|
||||
<p style="margin:0;font-size:11px;font-weight:800;color:#6B7280;text-transform:uppercase">Widgets</p>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:6px">
|
||||
<For each={widgets().length ? widgets() : ['(none)']}>
|
||||
{(w) => <span style="padding:2px 8px;border-radius:999px;border:1px solid #E5E7EB;background:white;font-size:11px;color:#374151">{w}</span>}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p style="margin:0;font-size:11px;font-weight:800;color:#6B7280;text-transform:uppercase">Tabs</p>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:6px">
|
||||
<For each={tabs().length ? tabs() : ['(none)']}>
|
||||
{(t) => <span style="padding:2px 8px;border-radius:999px;border:1px solid #E5E7EB;background:white;font-size:11px;color:#374151">{t}</span>}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DashboardDesignPreview
|
||||
status={isActive() ? 'ACTIVE' : 'INACTIVE'}
|
||||
sidebarItems={previewSidebarItems()}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,17 @@ type VerificationRecord = CrudRecord & {
|
|||
verificationType: 'IDENTITY' | 'BUSINESS' | 'PROFILE' | 'DOCUMENT' | 'MIXED';
|
||||
submittedDate?: string;
|
||||
documentsCount?: number;
|
||||
documents?: Array<{
|
||||
name: string;
|
||||
fileName: string;
|
||||
state: 'APPROVED' | 'RECEIVED' | 'MISSING' | 'REQUESTED';
|
||||
}>;
|
||||
requestedDocuments?: string[];
|
||||
roleTags?: string[];
|
||||
primaryService?: string;
|
||||
area?: string;
|
||||
userId?: string;
|
||||
roleKey?: string;
|
||||
assignedVerifier?: string;
|
||||
priority: 'LOW' | 'MEDIUM' | 'HIGH';
|
||||
status: 'PENDING' | 'IN_REVIEW' | 'PARTIALLY_VERIFIED' | 'VERIFIED' | 'FLAGGED' | 'RE_UPLOAD_REQUESTED' | 'REJECTED';
|
||||
|
|
@ -48,9 +59,83 @@ function PriorityBadge(props: { priority: string }) {
|
|||
);
|
||||
}
|
||||
|
||||
const DEFAULT_DOCS_BY_USER_TYPE: Record<VerificationRecord['userType'], string[]> = {
|
||||
COMPANY: ['GST Certificate', 'PAN Card', 'Incorporation Certificate'],
|
||||
CUSTOMER: ['Identity Proof', 'Address Proof'],
|
||||
PROFESSIONAL: ['Identity Proof', 'Address Proof', 'Portfolio Ownership'],
|
||||
JOBSEEKER: ['Identity Proof', 'Address Proof', 'Resume'],
|
||||
};
|
||||
|
||||
const toTitle = (value: string) => String(value || '')
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
|
||||
type SubmissionData = {
|
||||
role_key?: string;
|
||||
onboarding?: {
|
||||
status?: string;
|
||||
progress_json?: Record<string, unknown>;
|
||||
} | null;
|
||||
};
|
||||
|
||||
function extractRoleTags(source: any): string[] {
|
||||
const values: string[] = [];
|
||||
const pushValue = (value: unknown) => {
|
||||
if (!value) return;
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((v) => pushValue(v));
|
||||
return;
|
||||
}
|
||||
const text = String(value || '').trim();
|
||||
if (!text) return;
|
||||
values.push(text);
|
||||
};
|
||||
|
||||
pushValue(source?.role_key);
|
||||
pushValue(source?.roleKey);
|
||||
pushValue(source?.role_keys);
|
||||
pushValue(source?.roleKeys);
|
||||
pushValue(source?.roles);
|
||||
pushValue(source?.categories);
|
||||
pushValue(source?.category);
|
||||
pushValue(source?.service_category);
|
||||
pushValue(source?.serviceCategory);
|
||||
pushValue(source?.profession);
|
||||
pushValue(source?.service_type);
|
||||
|
||||
const unique = Array.from(new Set(values.map((v) => toTitle(v))));
|
||||
return unique.slice(0, 4);
|
||||
}
|
||||
|
||||
function flattenFields(obj: Record<string, unknown>, prefix = ''): Array<{ key: string; value: string }> {
|
||||
const result: Array<{ key: string; value: string }> = [];
|
||||
for (const [k, v] of Object.entries(obj || {})) {
|
||||
const label = prefix ? `${prefix}.${k}` : k;
|
||||
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
|
||||
result.push(...flattenFields(v as Record<string, unknown>, label));
|
||||
} else if (Array.isArray(v)) {
|
||||
result.push({ key: label, value: v.map((item) => String(item ?? '')).join(', ') });
|
||||
} else {
|
||||
result.push({ key: label, value: String(v ?? '') });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function loadSubmissionDetails(userId: string, roleKey: string): Promise<SubmissionData | null> {
|
||||
if (!userId) return null;
|
||||
const qs = roleKey ? `?roleKey=${encodeURIComponent(roleKey)}` : '';
|
||||
try {
|
||||
const res = await fetch(`${API}/api/admin/approvals/submission/${userId}${qs}`);
|
||||
if (!res.ok) return null;
|
||||
return res.json();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default function VerificationManagementPage() {
|
||||
const [view, setView] = createSignal<'list' | 'form'>('list');
|
||||
const [listTab, setListTab] = createSignal<'all' | 'create' | 'view'>('all');
|
||||
const [listTab, setListTab] = createSignal<'all' | 'view' | 'flagged'>('all');
|
||||
const [detailTab, setDetailTab] = createSignal<'overview' | 'documents' | 'checklist' | 'logs'>('overview');
|
||||
|
||||
const [search, setSearch] = createSignal('');
|
||||
|
|
@ -62,28 +147,11 @@ export default function VerificationManagementPage() {
|
|||
const [sortBy, setSortBy] = createSignal<'submitted_desc' | 'submitted_asc' | 'priority_desc' | 'priority_asc'>('submitted_desc');
|
||||
const [sortMenuOpen, setSortMenuOpen] = createSignal(false);
|
||||
const [filterMenuOpen, setFilterMenuOpen] = createSignal(false);
|
||||
|
||||
const [ruleName, setRuleName] = createSignal('');
|
||||
const [ruleUserType, setRuleUserType] = createSignal<VerificationRecord['userType']>('PROFESSIONAL');
|
||||
const [ruleVerificationType, setRuleVerificationType] = createSignal<VerificationRecord['verificationType']>('IDENTITY');
|
||||
const [ruleActive, setRuleActive] = createSignal(true);
|
||||
const [formError, setFormError] = createSignal('');
|
||||
const [requestNote, setRequestNote] = createSignal('');
|
||||
const [requestingDocs, setRequestingDocs] = createSignal<string[]>([]);
|
||||
const [error, setError] = createSignal('');
|
||||
|
||||
type VerificationRule = {
|
||||
id: string;
|
||||
name: string;
|
||||
userType: VerificationRecord['userType'];
|
||||
verificationType: VerificationRecord['verificationType'];
|
||||
status: 'ACTIVE' | 'INACTIVE';
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
const [rules, setRules] = createSignal<VerificationRule[]>([
|
||||
{ id: 'vr1', name: 'Professional Identity Rule', userType: 'PROFESSIONAL', verificationType: 'IDENTITY', status: 'ACTIVE', updatedAt: '2026-03-20' },
|
||||
{ id: 'vr2', name: 'Company Business Verification', userType: 'COMPANY', verificationType: 'BUSINESS', status: 'ACTIVE', updatedAt: '2026-03-18' },
|
||||
{ id: 'vr3', name: 'Jobseeker Profile Check', userType: 'JOBSEEKER', verificationType: 'PROFILE', status: 'INACTIVE', updatedAt: '2026-03-12' },
|
||||
]);
|
||||
const [loadingLinkedDetails, setLoadingLinkedDetails] = createSignal(false);
|
||||
const [actionNote, setActionNote] = createSignal('');
|
||||
|
||||
const load = async () => {
|
||||
setError('');
|
||||
|
|
@ -103,33 +171,81 @@ export default function VerificationManagementPage() {
|
|||
const jobs = Array.isArray(payload?.jobs) ? payload.jobs : [];
|
||||
const requirements = Array.isArray(payload?.requirements) ? payload.requirements : [];
|
||||
|
||||
const jobCases: VerificationRecord[] = jobs.map((job: any) => ({
|
||||
id: `job-${String(job.id)}`,
|
||||
name: `Job Verification - ${String(job.title || 'Untitled Job')}`,
|
||||
applicantName: String(job.title || 'Untitled Job'),
|
||||
userType: 'COMPANY',
|
||||
verificationType: 'BUSINESS',
|
||||
submittedDate: String(job.created_at || ''),
|
||||
documentsCount: 1,
|
||||
assignedVerifier: 'Unassigned',
|
||||
priority: 'HIGH',
|
||||
status: 'IN_REVIEW',
|
||||
updatedAt: String(job.updated_at || job.created_at || ''),
|
||||
}));
|
||||
const buildDocuments = (userType: VerificationRecord['userType'], source: any) => {
|
||||
const names = DEFAULT_DOCS_BY_USER_TYPE[userType];
|
||||
const provided = Array.isArray(source?.documents) ? source.documents : [];
|
||||
const mapped = names.map((name, index) => {
|
||||
const fromPayload = provided[index];
|
||||
const fileName = String(
|
||||
fromPayload?.file_name
|
||||
|| fromPayload?.fileName
|
||||
|| fromPayload?.name
|
||||
|| `${name.toLowerCase().replace(/\s+/g, '_')}.pdf`,
|
||||
);
|
||||
const stateRaw = String(fromPayload?.state || fromPayload?.status || '').toUpperCase();
|
||||
const state: 'APPROVED' | 'RECEIVED' | 'MISSING' | 'REQUESTED' =
|
||||
stateRaw === 'APPROVED' || stateRaw === 'VERIFIED'
|
||||
? 'APPROVED'
|
||||
: stateRaw === 'REQUESTED' || stateRaw === 'RE_UPLOAD_REQUESTED'
|
||||
? 'REQUESTED'
|
||||
: stateRaw === 'RECEIVED'
|
||||
? 'RECEIVED'
|
||||
: index === names.length - 1
|
||||
? 'MISSING'
|
||||
: 'APPROVED';
|
||||
return { name, fileName, state };
|
||||
});
|
||||
const requested = mapped.filter((d) => d.state === 'REQUESTED' || d.state === 'MISSING').map((d) => d.name);
|
||||
return { mapped, requested };
|
||||
};
|
||||
|
||||
const requirementCases: VerificationRecord[] = requirements.map((req: any) => ({
|
||||
id: `requirement-${String(req.id)}`,
|
||||
name: `Requirement Verification - ${String(req.title || 'Untitled Requirement')}`,
|
||||
applicantName: String(req.title || 'Untitled Requirement'),
|
||||
userType: 'CUSTOMER',
|
||||
verificationType: 'PROFILE',
|
||||
submittedDate: String(req.created_at || ''),
|
||||
documentsCount: 1,
|
||||
assignedVerifier: 'Unassigned',
|
||||
priority: 'MEDIUM',
|
||||
status: 'PENDING',
|
||||
updatedAt: String(req.updated_at || req.created_at || ''),
|
||||
}));
|
||||
const jobCases: VerificationRecord[] = jobs.map((job: any) => {
|
||||
const docs = buildDocuments('COMPANY', job);
|
||||
return {
|
||||
id: `job-${String(job.id)}`,
|
||||
name: `Job Verification - ${String(job.title || 'Untitled Job')}`,
|
||||
applicantName: String(job.title || 'Untitled Job'),
|
||||
userType: 'COMPANY',
|
||||
verificationType: 'BUSINESS',
|
||||
submittedDate: String(job.created_at || ''),
|
||||
documentsCount: docs.mapped.length,
|
||||
documents: docs.mapped,
|
||||
requestedDocuments: docs.requested,
|
||||
roleTags: extractRoleTags(job),
|
||||
primaryService: String(job.category || job.department || job.role || 'Job Posting'),
|
||||
area: String(job.location || job.city || job.work_mode || '—'),
|
||||
userId: String(job.user_id || job.created_by || ''),
|
||||
roleKey: String(job.role_key || job.roleKey || ''),
|
||||
assignedVerifier: 'Unassigned',
|
||||
priority: 'HIGH',
|
||||
status: docs.requested.length ? 'RE_UPLOAD_REQUESTED' : 'IN_REVIEW',
|
||||
updatedAt: String(job.updated_at || job.created_at || ''),
|
||||
};
|
||||
});
|
||||
|
||||
const requirementCases: VerificationRecord[] = requirements.map((req: any) => {
|
||||
const docs = buildDocuments('CUSTOMER', req);
|
||||
return {
|
||||
id: `requirement-${String(req.id)}`,
|
||||
name: `Requirement Verification - ${String(req.title || 'Untitled Requirement')}`,
|
||||
applicantName: String(req.title || 'Untitled Requirement'),
|
||||
userType: 'CUSTOMER',
|
||||
verificationType: 'PROFILE',
|
||||
submittedDate: String(req.created_at || ''),
|
||||
documentsCount: docs.mapped.length,
|
||||
documents: docs.mapped,
|
||||
requestedDocuments: docs.requested,
|
||||
roleTags: extractRoleTags(req),
|
||||
primaryService: String(req.category || req.profession || req.service_type || 'Requirement'),
|
||||
area: String(req.location || req.city || req.area || '—'),
|
||||
userId: String(req.user_id || req.created_by || ''),
|
||||
roleKey: String(req.role_key || req.roleKey || ''),
|
||||
assignedVerifier: 'Unassigned',
|
||||
priority: 'MEDIUM',
|
||||
status: docs.requested.length ? 'RE_UPLOAD_REQUESTED' : 'PENDING',
|
||||
updatedAt: String(req.updated_at || req.created_at || ''),
|
||||
};
|
||||
});
|
||||
|
||||
setRows([...jobCases, ...requirementCases]);
|
||||
} catch (e: any) {
|
||||
|
|
@ -176,41 +292,240 @@ export default function VerificationManagementPage() {
|
|||
return sorted;
|
||||
});
|
||||
|
||||
const flaggedCount = createMemo(() => rows().filter((r) => r.status === 'FLAGGED').length);
|
||||
const reviewCount = createMemo(() => rows().filter((r) => r.status === 'PENDING' || r.status === 'IN_REVIEW').length);
|
||||
|
||||
const setCaseStatus = (id: string, status: VerificationRecord['status']) => {
|
||||
const now = new Date().toISOString();
|
||||
setRows((prev) => prev.map((r) => (r.id === id ? { ...r, status, updatedAt: now } : r)));
|
||||
setViewingCase((prev) => (prev && prev.id === id ? { ...prev, status, updatedAt: now } : prev));
|
||||
};
|
||||
|
||||
const markVerifiedCase = (row: VerificationRecord) => {
|
||||
const now = new Date().toISOString();
|
||||
setRows((prev) => prev.map((r) => {
|
||||
if (r.id !== row.id) return r;
|
||||
return {
|
||||
...r,
|
||||
status: 'VERIFIED',
|
||||
requestedDocuments: [],
|
||||
documents: (r.documents || []).map((d) => (d.state === 'MISSING' || d.state === 'REQUESTED' ? { ...d, state: 'RECEIVED' } : d)),
|
||||
updatedAt: now,
|
||||
};
|
||||
}));
|
||||
setViewingCase((prev) => {
|
||||
if (!prev || prev.id !== row.id) return prev;
|
||||
return {
|
||||
...prev,
|
||||
status: 'VERIFIED',
|
||||
requestedDocuments: [],
|
||||
documents: (prev.documents || []).map((d) => (d.state === 'MISSING' || d.state === 'REQUESTED' ? { ...d, state: 'RECEIVED' } : d)),
|
||||
updatedAt: now,
|
||||
};
|
||||
});
|
||||
setOpenMenuId(null);
|
||||
};
|
||||
|
||||
const requestReuploadCase = (row: VerificationRecord, requested: string[]) => {
|
||||
const fallback = (row.documents || []).filter((d) => d.state === 'MISSING' || d.state === 'REQUESTED').map((d) => d.name);
|
||||
const nextRequested = requested.length ? requested : fallback;
|
||||
if (!nextRequested.length) return;
|
||||
const now = new Date().toISOString();
|
||||
const requestedSet = new Set(nextRequested);
|
||||
setRows((prev) => prev.map((r) => {
|
||||
if (r.id !== row.id) return r;
|
||||
return {
|
||||
...r,
|
||||
status: 'RE_UPLOAD_REQUESTED',
|
||||
requestedDocuments: nextRequested,
|
||||
documents: (r.documents || []).map((d) => (requestedSet.has(d.name) ? { ...d, state: 'REQUESTED' } : d)),
|
||||
updatedAt: now,
|
||||
};
|
||||
}));
|
||||
setViewingCase((prev) => {
|
||||
if (!prev || prev.id !== row.id) return prev;
|
||||
return {
|
||||
...prev,
|
||||
status: 'RE_UPLOAD_REQUESTED',
|
||||
requestedDocuments: nextRequested,
|
||||
documents: (prev.documents || []).map((d) => (requestedSet.has(d.name) ? { ...d, state: 'REQUESTED' } : d)),
|
||||
updatedAt: now,
|
||||
};
|
||||
});
|
||||
setOpenMenuId(null);
|
||||
};
|
||||
|
||||
const enrichCaseFromSubmission = async (row: VerificationRecord) => {
|
||||
if (!row.userId) return;
|
||||
setLoadingLinkedDetails(true);
|
||||
try {
|
||||
const submission = await loadSubmissionDetails(row.userId || '', row.roleKey || '');
|
||||
const progress = (submission?.onboarding?.progress_json && typeof submission.onboarding.progress_json === 'object')
|
||||
? submission.onboarding.progress_json
|
||||
: {};
|
||||
const flattened = flattenFields(progress as Record<string, unknown>);
|
||||
const getField = (...hints: string[]) => {
|
||||
const hit = flattened.find((field) => hints.some((hint) => field.key.toLowerCase().includes(hint)));
|
||||
return String(hit?.value || '').trim();
|
||||
};
|
||||
|
||||
const linkedDocs = flattened
|
||||
.filter((field) => {
|
||||
const key = field.key.toLowerCase();
|
||||
const value = String(field.value || '').toLowerCase();
|
||||
const isDocLike = /(document|doc|file|upload|resume|cv|certificate|license|id|proof|portfolio)/.test(key);
|
||||
const hasPath = value.startsWith('http') || value.startsWith('/');
|
||||
return isDocLike && (hasPath || value.endsWith('.pdf') || value.endsWith('.jpg') || value.endsWith('.jpeg') || value.endsWith('.png'));
|
||||
})
|
||||
.slice(0, 8)
|
||||
.map((field) => {
|
||||
const nameParts = field.key.split('.');
|
||||
const baseName = nameParts[nameParts.length - 1] || field.key;
|
||||
const humanName = toTitle(baseName.replace(/_/g, ' ').replace(/file|upload|url/gi, '').trim() || baseName);
|
||||
const fileName = String(field.value || '').split('/').pop() || `${humanName.toLowerCase().replace(/\s+/g, '_')}.pdf`;
|
||||
const requestedSet = new Set(row.requestedDocuments || []);
|
||||
return {
|
||||
name: humanName,
|
||||
fileName,
|
||||
state: (requestedSet.has(humanName) ? 'REQUESTED' : 'RECEIVED') as 'REQUESTED' | 'RECEIVED',
|
||||
};
|
||||
});
|
||||
|
||||
const nextRoleTags = Array.from(new Set([
|
||||
...extractRoleTags(progress),
|
||||
...extractRoleTags({ role_key: submission?.role_key }),
|
||||
...(row.roleTags || []),
|
||||
])).slice(0, 5);
|
||||
const nextPrimary = getField('category', 'profession', 'service', 'role', 'job_title') || row.primaryService || '—';
|
||||
const nextArea = getField('location', 'city', 'area', 'place', 'venue') || row.area || '—';
|
||||
const nextDocs = linkedDocs.length ? linkedDocs : (row.documents || []);
|
||||
|
||||
setRows((prev) => prev.map((item) => {
|
||||
if (item.id !== row.id) return item;
|
||||
return {
|
||||
...item,
|
||||
roleTags: nextRoleTags.length ? nextRoleTags : item.roleTags,
|
||||
primaryService: nextPrimary,
|
||||
area: nextArea,
|
||||
documents: nextDocs,
|
||||
documentsCount: nextDocs.length || item.documentsCount,
|
||||
roleKey: item.roleKey || String(submission?.role_key || ''),
|
||||
};
|
||||
}));
|
||||
setViewingCase((prev) => {
|
||||
if (!prev || prev.id !== row.id) return prev;
|
||||
return {
|
||||
...prev,
|
||||
roleTags: nextRoleTags.length ? nextRoleTags : prev.roleTags,
|
||||
primaryService: nextPrimary,
|
||||
area: nextArea,
|
||||
documents: nextDocs,
|
||||
documentsCount: nextDocs.length || prev.documentsCount,
|
||||
roleKey: prev.roleKey || String(submission?.role_key || ''),
|
||||
};
|
||||
});
|
||||
} finally {
|
||||
setLoadingLinkedDetails(false);
|
||||
}
|
||||
};
|
||||
|
||||
const requestDocumentsPersist = async (row: VerificationRecord, requested: string[], note: string) => {
|
||||
if (!row.userId) return false;
|
||||
const roleKey = String(row.roleKey || '').toUpperCase();
|
||||
const payload = { documents: requested, note: note || 'Please upload the missing documents.' };
|
||||
const endpoints: string[] = [];
|
||||
if (roleKey && roleKey !== 'COMPANY' && roleKey !== 'CUSTOMER') {
|
||||
endpoints.push(`${API}/api/admin/approvals/profiles/professional/${roleKey}/${row.userId}/request-documents`);
|
||||
}
|
||||
if (roleKey === 'COMPANY') endpoints.push(`${API}/api/admin/approvals/profiles/company/${row.userId}/request-documents`);
|
||||
if (roleKey === 'CUSTOMER') endpoints.push(`${API}/api/admin/approvals/profiles/customer/${row.userId}/request-documents`);
|
||||
endpoints.push(`${API}/api/admin/approvals/submission/${row.userId}/request-documents`);
|
||||
for (const endpoint of endpoints) {
|
||||
try {
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (res.ok) return true;
|
||||
} catch {
|
||||
// Try the next known endpoint shape.
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const flagCase = (row: VerificationRecord) => {
|
||||
setCaseStatus(row.id, 'FLAGGED');
|
||||
setOpenMenuId(null);
|
||||
};
|
||||
|
||||
const toggleRequestedDocument = (name: string) => {
|
||||
setRequestingDocs((prev) => (
|
||||
prev.includes(name) ? prev.filter((v) => v !== name) : [...prev, name]
|
||||
));
|
||||
};
|
||||
|
||||
const requestMissingDocuments = async (row: VerificationRecord) => {
|
||||
const selected = requestingDocs();
|
||||
if (!selected.length) return;
|
||||
const persisted = await requestDocumentsPersist(row, selected, requestNote().trim());
|
||||
requestReuploadCase(row, selected);
|
||||
setActionNote(
|
||||
persisted
|
||||
? 'Document request sent successfully.'
|
||||
: 'Document request updated in dashboard state. API endpoint for persistence was not available.',
|
||||
);
|
||||
setRequestNote('');
|
||||
};
|
||||
|
||||
const quickRequestDocuments = async (row: VerificationRecord, selected: string[]) => {
|
||||
if (!selected.length) return;
|
||||
const persisted = await requestDocumentsPersist(row, selected, '');
|
||||
requestReuploadCase(row, selected);
|
||||
setActionNote(
|
||||
persisted
|
||||
? 'Document request sent successfully.'
|
||||
: 'Document request updated in dashboard state. API endpoint for persistence was not available.',
|
||||
);
|
||||
};
|
||||
|
||||
const exportRows = () => {
|
||||
const data = filteredRows();
|
||||
const headers = ['Verification ID', 'Applicant', 'User Type', 'Verification Type', 'Documents', 'Priority', 'Status', 'Submitted Date'];
|
||||
const lines = data.map((row) => [
|
||||
row.id,
|
||||
row.applicantName || '',
|
||||
row.userType,
|
||||
row.verificationType,
|
||||
String(row.documentsCount || 0),
|
||||
row.priority,
|
||||
row.status,
|
||||
formatDate(row.submittedDate || row.updatedAt),
|
||||
]);
|
||||
const csv = [headers, ...lines]
|
||||
.map((line) => line.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(','))
|
||||
.join('\n');
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `verification-cases-${new Date().toISOString().slice(0, 10)}.csv`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const openView = (row: VerificationRecord) => {
|
||||
setActionNote('');
|
||||
setViewingCase(row);
|
||||
setDetailTab('overview');
|
||||
setListTab('view');
|
||||
setOpenMenuId(null);
|
||||
};
|
||||
|
||||
const resetRuleForm = () => {
|
||||
setRuleName('');
|
||||
setRuleUserType('PROFESSIONAL');
|
||||
setRuleVerificationType('IDENTITY');
|
||||
setRuleActive(true);
|
||||
setFormError('');
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
resetRuleForm();
|
||||
setListTab('create');
|
||||
setView('form');
|
||||
};
|
||||
|
||||
const saveRule = () => {
|
||||
if (!ruleName().trim()) {
|
||||
setFormError('Rule name is required.');
|
||||
return;
|
||||
}
|
||||
const now = new Date().toISOString().slice(0, 10);
|
||||
const id = `vr_${Math.random().toString(16).slice(2)}`;
|
||||
setRules((prev) => [
|
||||
{ id, name: ruleName().trim(), userType: ruleUserType(), verificationType: ruleVerificationType(), status: ruleActive() ? 'ACTIVE' : 'INACTIVE', updatedAt: now },
|
||||
...prev,
|
||||
]);
|
||||
setView('list');
|
||||
setListTab('all');
|
||||
resetRuleForm();
|
||||
setRequestNote('');
|
||||
setRequestingDocs(row.requestedDocuments?.length ? row.requestedDocuments : (row.documents || []).filter((d) => d.state === 'MISSING').map((d) => d.name));
|
||||
void enrichCaseFromSubmission(row);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -227,24 +542,45 @@ export default function VerificationManagementPage() {
|
|||
{error()}
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={actionNote()}>
|
||||
<div style="margin-bottom:10px;border-radius:10px;border:1px solid #E5E7EB;background:#F9FAFB;padding:12px 16px;font-size:13px;color:#374151">
|
||||
{actionNote()}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* ── LIST VIEW ── */}
|
||||
<Show when={view() === 'list'}>
|
||||
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
|
||||
{([
|
||||
{ key: 'all', label: 'All Verifications', action: () => { setListTab('all'); setStatusFilter('all'); } },
|
||||
{ key: 'create', label: 'Create Rule', action: () => openCreate() },
|
||||
{ key: 'view', label: 'View Verification', action: () => { setListTab('view'); } },
|
||||
] as const).map((tab) => (
|
||||
<Show when={true}>
|
||||
<div style="display:grid;grid-template-columns:220px minmax(0,1fr);gap:20px;align-items:start">
|
||||
<div style="position:sticky;top:12px;border:1px solid #E5E7EB;border-radius:12px;background:white;padding:8px;display:flex;flex-direction:column;gap:6px">
|
||||
<button
|
||||
type="button"
|
||||
onClick={tab.action}
|
||||
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${listTab() === tab.key ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280'}`}
|
||||
onClick={() => { setListTab('all'); setStatusFilter('all'); setViewingCase(null); }}
|
||||
style={`display:flex;align-items:center;justify-content:space-between;height:38px;border-radius:8px;padding:0 12px;font-size:12.5px;font-weight:500;border:none;cursor:pointer;${listTab() === 'all' ? 'background:#FFF3EE;color:#FF5E13' : 'background:white;color:#6B7280'}`}
|
||||
>
|
||||
{tab.label}
|
||||
<span>Verification Queue</span>
|
||||
<span style={`font-size:11px;font-weight:700;${listTab() === 'all' ? 'color:#FF5E13' : 'color:#9CA3AF'}`}>{rows().length}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setListTab('view'); }}
|
||||
style={`display:flex;align-items:center;justify-content:space-between;height:38px;border-radius:8px;padding:0 12px;font-size:12.5px;font-weight:500;border:none;cursor:pointer;${listTab() === 'view' ? 'background:#FFF3EE;color:#FF5E13' : 'background:white;color:#6B7280'}`}
|
||||
>
|
||||
<span>View Verification</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setListTab('flagged'); setStatusFilter('flagged'); setViewingCase(null); }}
|
||||
style={`display:flex;align-items:center;justify-content:space-between;height:38px;border-radius:8px;padding:0 12px;font-size:12.5px;font-weight:500;border:none;cursor:pointer;${listTab() === 'flagged' ? 'background:#FFF3EE;color:#FF5E13' : 'background:white;color:#6B7280'}`}
|
||||
>
|
||||
<span>Flagged Cases</span>
|
||||
<span style={`font-size:11px;font-weight:700;${listTab() === 'flagged' ? 'color:#FF5E13' : 'color:#9CA3AF'}`}>{flaggedCount()}</span>
|
||||
</button>
|
||||
<div style="margin-top:4px;border-top:1px solid #F3F4F6;padding:8px 6px 2px 6px;font-size:11px;color:#9CA3AF">
|
||||
Pending Review: <span style="font-weight:700;color:#6B7280">{reviewCount()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
<Show when={listTab() === 'view'}>
|
||||
<Show when={!viewingCase()}>
|
||||
|
|
@ -265,8 +601,15 @@ export default function VerificationManagementPage() {
|
|||
<p style="margin-top:2px;font-size:13px;color:#6B7280">ID: {viewingCase()!.id} • {viewingCase()!.verificationType} • Submitted: {formatDate(viewingCase()!.submittedDate)}</p>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px">
|
||||
<button type="button" style="height:36px;border-radius:8px;background:#0D0D2A;padding:0 16px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer">Mark Verified</button>
|
||||
<button type="button" style="height:36px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer">Request Re-upload</button>
|
||||
<button type="button" onClick={() => markVerifiedCase(viewingCase()!)} style="height:36px;border-radius:8px;background:#0D0D2A;padding:0 16px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer">Mark Verified</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!requestingDocs().length}
|
||||
onClick={() => requestMissingDocuments(viewingCase()!)}
|
||||
style={`height:36px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:${requestingDocs().length ? 'pointer' : 'not-allowed'};opacity:${requestingDocs().length ? 1 : 0.55}`}
|
||||
>
|
||||
Request Documents
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -289,12 +632,59 @@ export default function VerificationManagementPage() {
|
|||
<div style="display:flex;flex-direction:column;gap:24px">
|
||||
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px">
|
||||
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:16px">Case Summary</h3>
|
||||
<Show when={loadingLinkedDetails()}>
|
||||
<p style="margin:0 0 12px;font-size:12px;color:#6B7280">Syncing linked submission details...</p>
|
||||
</Show>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
|
||||
<div><p style="font-size:11px;color:#9CA3AF">Applicant Name</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.applicantName}</p></div>
|
||||
<div><p style="font-size:11px;color:#9CA3AF">User Type</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.userType}</p></div>
|
||||
<div><p style="font-size:11px;color:#9CA3AF">Primary Service</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.primaryService || '—'}</p></div>
|
||||
<div><p style="font-size:11px;color:#9CA3AF">Area / Place</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.area || '—'}</p></div>
|
||||
<div><p style="font-size:11px;color:#9CA3AF">Assigned Verifier</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.assignedVerifier}</p></div>
|
||||
<div><p style="font-size:11px;color:#9CA3AF">Documents</p><p style="font-size:14px;font-weight:600;color:#111827">{Number(viewingCase()!.documentsCount || 0)}</p></div>
|
||||
</div>
|
||||
<Show when={viewingCase()!.roleTags?.length}>
|
||||
<div style="margin-top:12px">
|
||||
<p style="margin:0 0 8px;font-size:11px;color:#9CA3AF">Registered Roles / Services</p>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:6px">
|
||||
<For each={viewingCase()!.roleTags || []}>
|
||||
{(tag) => (
|
||||
<span style="height:24px;padding:0 10px;border-radius:999px;border:1px solid #E5E7EB;background:#F9FAFB;display:inline-flex;align-items:center;font-size:11px;font-weight:600;color:#374151">{tag}</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div style="border:1px solid #FFE2D3;border-radius:12px;padding:20px;background:#FFF8F4">
|
||||
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:12px">Request Missing Documents</h3>
|
||||
<p style="margin:0 0 10px;font-size:12px;color:#6B7280">Select missing documents and send one clear request to the user.</p>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
||||
<For each={viewingCase()!.documents || []}>
|
||||
{(doc) => (
|
||||
<label style="display:flex;align-items:center;gap:8px;border:1px solid #E5E7EB;background:white;border-radius:10px;padding:8px 10px;cursor:pointer">
|
||||
<input type="checkbox" checked={requestingDocs().includes(doc.name)} onChange={() => toggleRequestedDocument(doc.name)} style="width:14px;height:14px;accent-color:#FF5E13" />
|
||||
<span style="font-size:12px;color:#111827;font-weight:600">{doc.name}</span>
|
||||
</label>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<textarea
|
||||
value={requestNote()}
|
||||
onInput={(e) => setRequestNote(e.currentTarget.value)}
|
||||
placeholder="Optional note for user (e.g. upload clear PDF/JPG, full document visible)..."
|
||||
style="margin-top:10px;width:100%;height:72px;border-radius:8px;border:1px solid #E5E7EB;padding:10px;font-size:12px;resize:none"
|
||||
/>
|
||||
<div style="margin-top:10px;display:flex;justify-content:flex-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => requestMissingDocuments(viewingCase()!)}
|
||||
disabled={!requestingDocs().length}
|
||||
style={`height:34px;border-radius:8px;border:none;background:#0D0D2A;color:white;padding:0 14px;font-size:12px;font-weight:700;cursor:${requestingDocs().length ? 'pointer' : 'not-allowed'};opacity:${requestingDocs().length ? 1 : 0.55}`}
|
||||
>
|
||||
Send Document Request
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px">
|
||||
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:16px">Verification Tracker</h3>
|
||||
|
|
@ -333,17 +723,21 @@ export default function VerificationManagementPage() {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<For each={[
|
||||
{ n: 'Passport / National ID', s: 'VERIFIED' },
|
||||
{ n: 'Address Proof (Utility Bill)', s: 'PENDING' },
|
||||
]}>
|
||||
<For each={viewingCase()!.documents || []}>
|
||||
{(doc) => (
|
||||
<tr style="border-top:1px solid #E5E7EB">
|
||||
<td style="padding:12px 16px;font-size:13px;font-weight:600;color:#111827">{doc.n}</td>
|
||||
<td style="padding:12px 16px"><StatusBadge status={doc.s} /></td>
|
||||
<td style="padding:12px 16px">
|
||||
<p style="margin:0;font-size:13px;font-weight:600;color:#111827">{doc.name}</p>
|
||||
<p style="margin:2px 0 0;font-size:11px;color:#6B7280">{doc.fileName}</p>
|
||||
</td>
|
||||
<td style="padding:12px 16px"><StatusBadge status={doc.state === 'APPROVED' ? 'VERIFIED' : doc.state === 'REQUESTED' ? 'RE_UPLOAD_REQUESTED' : doc.state === 'MISSING' ? 'PENDING' : 'IN_REVIEW'} /></td>
|
||||
<td style="padding:12px 16px;display:flex;gap:8px">
|
||||
<button type="button" style="font-size:12px;color:#FF5E13;background:none;border:none;cursor:pointer;font-weight:600">Preview</button>
|
||||
<button type="button" style="font-size:12px;color:#0D0D2A;background:none;border:none;cursor:pointer;font-weight:600">Approve</button>
|
||||
<Show when={doc.state === 'MISSING' || doc.state === 'REQUESTED'}>
|
||||
<button type="button" onClick={() => toggleRequestedDocument(doc.name)} style="font-size:12px;color:#0D0D2A;background:none;border:none;cursor:pointer;font-weight:600">
|
||||
{requestingDocs().includes(doc.name) ? 'Unselect' : 'Request'}
|
||||
</button>
|
||||
</Show>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
|
@ -443,7 +837,7 @@ export default function VerificationManagementPage() {
|
|||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<button type="button" style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;background:#0D0D2A;padding:0 12px;font-size:12px;font-weight:600;color:white;border:none;cursor:pointer">
|
||||
<button type="button" onClick={exportRows} style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;background:#0D0D2A;padding:0 12px;font-size:12px;font-weight:600;color:white;border:none;cursor:pointer">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
Export
|
||||
</button>
|
||||
|
|
@ -470,7 +864,10 @@ export default function VerificationManagementPage() {
|
|||
<td style="padding:12px 20px;font-size:12px;font-family:monospace;color:#6B7280">{row.id}</td>
|
||||
<td style="padding:12px 20px">
|
||||
<p style="font-size:14px;font-weight:600;color:#111827">{row.applicantName}</p>
|
||||
<p style="font-size:11px;color:#6B7280">{row.userType}</p>
|
||||
<p style="font-size:11px;color:#6B7280">{row.userType}{row.area ? ` • ${row.area}` : ''}</p>
|
||||
<Show when={row.roleTags?.length}>
|
||||
<p style="margin-top:2px;font-size:11px;color:#9CA3AF">{(row.roleTags || []).join(', ')}</p>
|
||||
</Show>
|
||||
</td>
|
||||
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{row.verificationType}</td>
|
||||
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{Number(row.documentsCount || 0)} docs</td>
|
||||
|
|
@ -484,8 +881,9 @@ export default function VerificationManagementPage() {
|
|||
<Show when={openMenuId() === row.id}>
|
||||
<div style="position:absolute;right:20px;top:44px;z-index:20;width:190px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 20px rgba(0,0,0,0.12)">
|
||||
<button type="button" onClick={() => openView(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">View Verification</button>
|
||||
<button type="button" style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">Mark Verified</button>
|
||||
<button type="button" style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;color:#DC2626;background:none;border:none;cursor:pointer;text-align:left">Flag Case</button>
|
||||
<button type="button" onClick={() => void quickRequestDocuments(row, (row.documents || []).filter((d) => d.state === 'MISSING' || d.state === 'REQUESTED').map((d) => d.name))} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">Request Documents</button>
|
||||
<button type="button" onClick={() => markVerifiedCase(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">Mark Verified</button>
|
||||
<button type="button" onClick={() => flagCase(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:8px 12px;font-size:13px;color:#DC2626;background:none;border:none;cursor:pointer;text-align:left">Flag Case</button>
|
||||
</div>
|
||||
</Show>
|
||||
</td>
|
||||
|
|
@ -497,87 +895,7 @@ export default function VerificationManagementPage() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={view() === 'form'}>
|
||||
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
|
||||
<button type="button" onClick={() => { setView('list'); setListTab('all'); }} style="padding-bottom:12px;font-size:14px;font-weight:500;color:#6B7280;background:none;border:none;cursor:pointer">
|
||||
All Verifications
|
||||
</button>
|
||||
<button type="button" style="padding-bottom:12px;font-size:14px;font-weight:500;color:#FF5E13;border:none;border-bottom:2px solid #FF5E13;background:none;cursor:pointer;margin-bottom:-1px">
|
||||
Create Rule
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
||||
<div style="padding:24px">
|
||||
<Show when={formError()}>
|
||||
<div style="margin-bottom:20px;border-radius:10px;border:1px solid #FECACA;background:#FEF2F2;padding:12px 16px;font-size:13px;color:#B91C1C">
|
||||
{formError()}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
|
||||
<label style="display:block">
|
||||
<span style="font-size:13px;font-weight:600;color:#374151">Rule Name<span style="margin-left:2px;color:#FF5E13">*</span></span>
|
||||
<input value={ruleName()} onInput={(e) => setRuleName(e.currentTarget.value)} placeholder="e.g. Professional Identity Review" style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:13px;color:#111827;outline:none;box-sizing:border-box" />
|
||||
</label>
|
||||
<label style="display:block">
|
||||
<span style="font-size:13px;font-weight:600;color:#374151">User Type</span>
|
||||
<select value={ruleUserType()} onChange={(e) => setRuleUserType(e.currentTarget.value as VerificationRecord['userType'])} style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none;box-sizing:border-box">
|
||||
{(['CUSTOMER', 'PROFESSIONAL', 'COMPANY', 'JOBSEEKER'] as const).map((t) => <option value={t}>{t}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:20px">
|
||||
<label style="display:block">
|
||||
<span style="font-size:13px;font-weight:600;color:#374151">Verification Type</span>
|
||||
<select value={ruleVerificationType()} onChange={(e) => setRuleVerificationType(e.currentTarget.value as VerificationRecord['verificationType'])} style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none;box-sizing:border-box">
|
||||
{(['IDENTITY', 'BUSINESS', 'PROFILE', 'DOCUMENT', 'MIXED'] as const).map((t) => <option value={t}>{t}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<div>
|
||||
<p style="font-size:13px;font-weight:600;color:#374151">Rule Status</p>
|
||||
<div style="margin-top:8px;display:flex;gap:10px">
|
||||
<button type="button" onClick={() => setRuleActive(true)} style={`height:38px;border-radius:10px;padding:0 20px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid ${ruleActive() ? '#FF5E13' : '#E5E7EB'};background:${ruleActive() ? '#FFF3EE' : 'white'};color:${ruleActive() ? '#FF5E13' : '#6B7280'}`}>Active</button>
|
||||
<button type="button" onClick={() => setRuleActive(false)} style={`height:38px;border-radius:10px;padding:0 20px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid ${!ruleActive() ? '#FF5E13' : '#E5E7EB'};background:${!ruleActive() ? '#FFF3EE' : 'white'};color:${!ruleActive() ? '#FF5E13' : '#6B7280'}`}>Inactive</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:24px;display:flex;justify-content:flex-end;gap:10px">
|
||||
<button type="button" onClick={() => { setView('list'); setListTab('all'); resetRuleForm(); }} style="height:40px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer">Cancel</button>
|
||||
<button type="button" onClick={saveRule} style="height:40px;border-radius:10px;background:#0D0D2A;padding:0 18px;font-size:13px;font-weight:700;color:white;border:none;cursor:pointer">Save Rule</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:24px;border:1px solid #E5E7EB;border-radius:16px;background:white;overflow:hidden">
|
||||
<table style="width:100%;border-collapse:collapse">
|
||||
<thead style="background:#0D0D2A">
|
||||
<tr style="text-align:left">
|
||||
<th style="padding:12px 20px;font-size:11px;font-weight:600;color:white;text-transform:uppercase">Rule Name</th>
|
||||
<th style="padding:12px 20px;font-size:11px;font-weight:600;color:white;text-transform:uppercase">User Type</th>
|
||||
<th style="padding:12px 20px;font-size:11px;font-weight:600;color:white;text-transform:uppercase">Verification Type</th>
|
||||
<th style="padding:12px 20px;font-size:11px;font-weight:600;color:white;text-transform:uppercase">Status</th>
|
||||
<th style="padding:12px 20px;font-size:11px;font-weight:600;color:white;text-transform:uppercase">Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<For each={rules()}>
|
||||
{(rule) => (
|
||||
<tr style="border-top:1px solid #E5E7EB">
|
||||
<td style="padding:14px 20px;font-size:14px;font-weight:600;color:#111827">{rule.name}</td>
|
||||
<td style="padding:14px 20px;font-size:13px;color:#6B7280">{rule.userType}</td>
|
||||
<td style="padding:14px 20px;font-size:13px;color:#6B7280">{rule.verificationType}</td>
|
||||
<td style="padding:14px 20px"><StatusBadge status={rule.status} /></td>
|
||||
<td style="padding:14px 20px;font-size:13px;color:#6B7280">{formatDate(rule.updatedAt)}</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue