nxtgauge-admin-solid/src/routes/admin/verification.tsx

988 lines
54 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { A } from '@solidjs/router';
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
type VerificationStatus = 'PENDING' | 'UNDER_REVIEW' | 'DOCUMENTS_REQUESTED' | 'REVISION_REQUESTED' | 'APPROVED' | 'REJECTED';
type VerificationPriority = 'HIGH' | 'MEDIUM' | 'LOW';
type VerificationRow = {
id: string;
applicantName: string;
requestType:
| 'Profile Approval'
| 'Portfolio Approval'
| 'Company Approval'
| 'Job Seeker Approval'
| 'Service Seeker Profile Approval'
| 'Service Seeker Requirement'
| 'Job Approval';
roleLabel: string;
submittedOn: string;
status: VerificationStatus;
priority: VerificationPriority;
userType: 'PROFESSIONAL' | 'COMPANY' | 'JOBSEEKER' | 'CUSTOMER';
area: string;
userId: string;
roleKey: string;
payload?: any;
};
type SubmittedDocument = {
id: string;
title: string;
type: 'IMAGE' | 'PDF';
url: string;
status: 'SUBMITTED' | 'MISSING' | 'INVALID';
};
type PortfolioAsset = {
id: string;
title: string;
url: string;
};
type ApprovalQueueItem = {
id: string;
requestType: VerificationRow['requestType'];
applicantName: string;
roleLabel: string;
userType: VerificationRow['userType'];
roleKey: string;
area: string;
submittedOn: string;
documents: SubmittedDocument[];
submittedFields: Array<{ label: string; value: string }>;
};
const ROLE_PROFILE_FIELDS: Record<string, string[]> = {
CUSTOMER: ['First Name', 'Last Name', 'Email Address', 'Mobile Number', 'Area', 'Place', 'PIN Code', 'Service Category'],
COMPANY: ['Company Name', 'Company Email', 'Company Phone', 'City', 'Area', 'Place', 'PIN Code', 'Website URL'],
JOB_SEEKER: ['First Name', 'Last Name', 'Email Address', 'Mobile Number', 'Current Role', 'Total Experience', 'City', 'Area', 'Place'],
PROFESSIONAL: ['First Name', 'Last Name', 'Email Address', 'Mobile Number', 'Gender', 'Address Line 1', 'Address Line 2 (Optional)', 'City', 'Area', 'State', 'PIN Code'],
};
const ROLE_DOCUMENTS: Record<string, string[]> = {
CUSTOMER: ['Identity Proof', 'Address Proof'],
COMPANY: ['GST Certificate', 'PAN Card', 'Incorporation Certificate'],
JOB_SEEKER: ['Identity Proof', 'Address Proof', 'Education Proof'],
PHOTOGRAPHER: ['Identity Proof', 'Address Proof', 'Portfolio Ownership Proof'],
MAKEUP_ARTIST: ['Identity Proof', 'Address Proof', 'Professional Certifications'],
DEVELOPER: ['Identity Proof', 'Address Proof', 'Tax Document'],
VIDEO_EDITOR: ['Identity Proof', 'Address Proof', 'Tax Document'],
UGC_CONTENT_CREATOR: ['Identity Proof', 'Address Proof', 'Tax Document'],
GRAPHIC_DESIGNER: ['Identity Proof', 'Address Proof', 'Tax Document'],
SOCIAL_MEDIA_MANAGER: ['Identity Proof', 'Address Proof', 'Tax Document'],
FITNESS_TRAINER: ['Identity Proof', 'Address Proof', 'Certification Proof'],
TUTOR: ['Identity Proof', 'Address Proof', 'Educational Proof'],
CATERING_SERVICES: ['Identity Proof', 'Address Proof', 'Food License'],
PROFESSIONAL: ['Identity Proof', 'Address Proof', 'Tax Document'],
};
const REQUIREMENT_ROLE_FIELDS: Record<string, string[]> = {
PHOTOGRAPHER: ['Event Type', 'Shoot Type', 'Event Date & Time', 'Event Duration (Hours)', 'Venue / Location', 'Number of People', 'Delivery Deadline'],
MAKEUP_ARTIST: ['Event Type', 'Makeup Category', 'Event Date & Time', 'Artists Required', 'Venue / Location', 'Skin Tone Preference', 'Ready By Time'],
TUTOR: ['Subject', 'Class / Grade', 'Mode (Online / Offline)', 'Sessions Per Week', 'Preferred Start Date', 'Student Location', 'Exam Goal'],
DEVELOPER: ['Project Type', 'Platform', 'Preferred Stack', 'Project Duration', 'Launch Deadline', 'Team Size Needed', 'Support Duration'],
VIDEO_EDITOR: ['Video Category', 'Final Duration', 'Footage Volume', 'Delivery Date', 'Editing Style', 'Platform', 'Revision Rounds'],
UGC_CONTENT_CREATOR: ['Campaign Goal', 'Platform', 'Deliverables Needed', 'Brand Category', 'Delivery Deadline', 'Target Audience', 'Usage Rights Duration'],
FITNESS_TRAINER: ['Primary Goal', 'Current Activity Level', 'Preferred Mode', 'Training Days Per Week', 'Preferred Timings', 'Health Conditions', 'Goal Timeline'],
CATERING_SERVICES: ['Event Type', 'Cuisine Preference', 'Guest Count', 'Service Date', 'Venue / Location', 'Meal Slot', 'Serving Style'],
GRAPHIC_DESIGNER: ['Project Type', 'Brand Industry', 'Deliverables Needed', 'Deadline', 'Target Audience', 'Reference Links', 'Output Formats'],
SOCIAL_MEDIA_MANAGER: ['Primary Goal', 'Platforms', 'Posting Frequency', 'Campaign Duration', 'Start Date', 'Brand Category', 'Monthly Budget'],
};
const JOB_POSTING_FIELDS = [
'Job Title',
'Department',
'Job Category',
'Employment Type',
'Seniority',
'Openings',
'Role & Requirements',
'Compensation',
'Location',
'Description',
];
const APPROVAL_QUEUE_STORAGE_KEY = 'nxtgauge_admin_approval_queue';
const API = '/api/gateway';
const toTitle = (value: string) => String(value || '').replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
const statusUi = (status: VerificationStatus) => {
if (status === 'APPROVED') return { bg: '#ECFDF3', border: '#BBF7D0', text: '#166534', label: 'Approved' };
if (status === 'UNDER_REVIEW') return { bg: '#EEF2FF', border: '#C7D2FE', text: '#3730A3', label: 'Under Review' };
if (status === 'DOCUMENTS_REQUESTED') return { bg: '#FFF7ED', border: '#FED7AA', text: '#C2410C', label: 'Document Requested' };
if (status === 'REVISION_REQUESTED') return { bg: '#FFF7ED', border: '#FED7AA', text: '#C2410C', label: 'Revision Requested' };
if (status === 'REJECTED') return { bg: '#FEF2F2', border: '#FECACA', text: '#B91C1C', label: 'Rejected' };
return { bg: '#FFFBEB', border: '#FDE68A', text: '#92400E', label: 'Pending' };
};
const priorityUi = (priority: VerificationPriority) => {
if (priority === 'HIGH') return { color: '#DC2626', label: 'High' };
if (priority === 'MEDIUM') return { color: '#D97706', label: 'Medium' };
return { color: '#64748B', label: 'Low' };
};
const parseDate = (value: string) => {
const ts = Date.parse(String(value || ''));
return Number.isNaN(ts) ? 0 : ts;
};
const normalizeRoleSpecKey = (value: string) => {
const key = String(value || '').toUpperCase();
if (key.includes('COMPANY')) return 'COMPANY';
if (key.includes('CUSTOMER') || key.includes('SERVICE_SEEKER')) return 'CUSTOMER';
if (key.includes('JOB_SEEKER') || key.includes('JOBSEEKER')) return 'JOB_SEEKER';
if (key.includes('PHOTOGRAPHER') || key.includes('PHOTO')) return 'PHOTOGRAPHER';
if (key.includes('MAKEUP')) return 'MAKEUP_ARTIST';
if (key.includes('DEVELOPER')) return 'DEVELOPER';
if (key.includes('VIDEO')) return 'VIDEO_EDITOR';
if (key.includes('UGC') || (key.includes('CONTENT') && key.includes('CREATOR'))) return 'UGC_CONTENT_CREATOR';
if (key.includes('GRAPHIC')) return 'GRAPHIC_DESIGNER';
if (key.includes('SOCIAL')) return 'SOCIAL_MEDIA_MANAGER';
if (key.includes('FITNESS')) return 'FITNESS_TRAINER';
if (key.includes('TUTOR')) return 'TUTOR';
if (key.includes('CATER')) return 'CATERING_SERVICES';
return 'PROFESSIONAL';
};
export default function VerificationManagementPage() {
const [rows, setRows] = createSignal<VerificationRow[]>([]);
const [search, setSearch] = createSignal('');
const [statusFilter, setStatusFilter] = createSignal<'ALL' | VerificationStatus>('ALL');
const [sortBy, setSortBy] = createSignal<'latest' | 'oldest' | 'priority'>('latest');
const [sortOpen, setSortOpen] = createSignal(false);
const [filterOpen, setFilterOpen] = createSignal(false);
const [error, setError] = createSignal('');
const [categoryTab, setCategoryTab] = createSignal<'all' | 'profile' | 'portfolio' | 'company' | 'job_seeker' | 'service_profile' | 'service_requirement' | 'job'>('all');
const [listTab, setListTab] = createSignal<'all' | 'view'>('all');
const [selectedRow, setSelectedRow] = createSignal<VerificationRow | null>(null);
const [viewer, setViewer] = createSignal<{ open: boolean; title: string; type: 'IMAGE' | 'PDF'; url: string }>({
open: false,
title: '',
type: 'IMAGE',
url: '',
});
const [docSelection, setDocSelection] = createSignal<Record<string, boolean>>({});
const [requestNote, setRequestNote] = createSignal('');
const [actionMessage, setActionMessage] = createSignal('');
const load = async () => {
try {
setError('');
const res = await fetch(`${API}/api/admin/verifications?page=1&limit=200`, {
headers: { Accept: 'application/json' },
credentials: 'include',
});
if (!res.ok) throw new Error(`Failed to load verification queue (${res.status})`);
const data = await res.json().catch(() => ({} as any));
const items = Array.isArray(data?.items) ? data.items : [];
const mergedRows: VerificationRow[] = items.map((v: any) => {
const payload = v.payload || {};
const userType = (v.type === 'job_approval' ? 'COMPANY' : (v.type === 'requirement_approval' ? 'CUSTOMER' : 'PROFESSIONAL')) as VerificationRow['userType'];
return {
id: v.id,
applicantName: v.user_name || 'Applicant',
requestType: (v.type === 'job_approval' ? 'Job Approval' : (v.type === 'requirement_approval' ? 'Service Seeker Requirement' : 'Profile Approval')) as VerificationRow['requestType'],
roleLabel: toTitle(v.role_key || 'User'),
submittedOn: v.created_at,
status: v.status as VerificationStatus,
priority: 'MEDIUM',
userType,
area: payload.city || payload.area || 'Unknown',
userId: v.user_id,
roleKey: v.role_key,
payload,
};
});
setRows(mergedRows);
} catch (e: any) {
setRows([]);
setError(e?.message ? e.message : 'Could not load verification queue.');
}
};
onMount(() => {
void load();
});
const tabCounts = createMemo(() => {
const all = rows();
return {
all: all.length,
profile: all.filter((r) => r.requestType === 'Profile Approval').length,
portfolio: all.filter((r) => r.requestType === 'Portfolio Approval').length,
company: all.filter((r) => r.requestType === 'Company Approval').length,
jobSeeker: all.filter((r) => r.requestType === 'Job Seeker Approval').length,
serviceProfile: all.filter((r) => r.requestType === 'Service Seeker Profile Approval').length,
serviceRequirement: all.filter((r) => r.requestType === 'Service Seeker Requirement').length,
job: all.filter((r) => r.requestType === 'Job Approval').length,
};
});
const filteredRows = createMemo(() => {
const query = search().trim().toLowerCase();
const activeTab = categoryTab();
const status = statusFilter();
const sort = sortBy();
const scoped = rows().filter((row) => {
if (activeTab === 'profile' && row.requestType !== 'Profile Approval') return false;
if (activeTab === 'portfolio' && row.requestType !== 'Portfolio Approval') return false;
if (activeTab === 'company' && row.requestType !== 'Company Approval') return false;
if (activeTab === 'job_seeker' && row.requestType !== 'Job Seeker Approval') return false;
if (activeTab === 'service_profile' && row.requestType !== 'Service Seeker Profile Approval') return false;
if (activeTab === 'service_requirement' && row.requestType !== 'Service Seeker Requirement') return false;
if (activeTab === 'job' && row.requestType !== 'Job Approval') return false;
if (status !== 'ALL' && row.status !== status) return false;
if (!query) return true;
return [row.id, row.applicantName, row.requestType, row.roleLabel, row.area]
.some((value) => String(value || '').toLowerCase().includes(query));
});
const next = [...scoped];
if (sort === 'priority') {
const rank = (p: VerificationPriority) => (p === 'HIGH' ? 3 : p === 'MEDIUM' ? 2 : 1);
next.sort((a, b) => rank(b.priority) - rank(a.priority));
return next;
}
next.sort((a, b) => (sort === 'oldest' ? 1 : -1) * (parseDate(a.submittedOn) - parseDate(b.submittedOn)));
return next;
});
const displayRows = createMemo(() => {
return filteredRows();
});
const metrics = createMemo(() => {
const all = rows();
const today = new Date().toISOString().slice(0, 10);
const submittedToday = all.filter((r) => String(r.submittedOn || '').slice(0, 10) === today);
return {
totalPending: all.filter((r) => r.status === 'PENDING' || r.status === 'UNDER_REVIEW').length,
approvedToday: submittedToday.filter((r) => r.status === 'APPROVED').length,
rejectedToday: submittedToday.filter((r) => r.status === 'REJECTED').length,
needsRevision: all.filter((r) => r.status === 'DOCUMENTS_REQUESTED' || r.status === 'REVISION_REQUESTED').length,
};
});
const exportCsv = () => {
const headers = ['Submission ID', 'Type', 'Applicant Name', 'Role', 'Submitted On', 'Status', 'Priority', 'Area'];
const lines = filteredRows().map((row) => [
row.id,
row.requestType,
row.applicantName,
row.roleLabel,
String(row.submittedOn || '').slice(0, 10),
row.status,
row.priority,
row.area,
]);
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 link = document.createElement('a');
link.href = url;
link.download = `verification-queue-${new Date().toISOString().slice(0, 10)}.csv`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const selectedDocuments = createMemo<SubmittedDocument[]>(() => {
const row = selectedRow();
if (!row) return [];
if (row.requestType === 'Job Approval') {
return [
{ id: 'job_desc', title: 'Job Description File', type: 'PDF', url: '/nxtgauge-logo.png', status: 'SUBMITTED' },
{ id: 'company_proof', title: 'Company Verification Snapshot', type: 'PDF', url: '/nxtgauge-icon.png', status: 'SUBMITTED' },
];
}
if (row.requestType === 'Service Seeker Requirement') {
return [
{ id: 'requirement_brief', title: 'Requirement Brief', type: 'PDF', url: '/nxtgauge-logo.png', status: 'SUBMITTED' },
{ id: 'reference', title: 'Reference Attachment', type: 'IMAGE', url: '/nxtgauge-icon.png', status: 'SUBMITTED' },
];
}
const roleSpecKey = normalizeRoleSpecKey(row.roleKey || row.userType);
const fromPayload = Array.isArray(row.payload?.documents) ? row.payload.documents : [];
if (fromPayload.length) {
return fromPayload.slice(0, 8).map((doc: any, idx: number) => ({
id: String(doc.id || `doc-${idx + 1}`),
title: String(doc.title || doc.name || `Document ${idx + 1}`),
type: String(doc.type || '').toUpperCase().includes('PDF') ? 'PDF' : 'IMAGE',
url: String(doc.url || '/nxtgauge-logo.png'),
status: String(doc.status || '').toUpperCase() === 'MISSING'
? 'MISSING'
: String(doc.status || '').toUpperCase() === 'INVALID'
? 'INVALID'
: 'SUBMITTED',
}));
}
const docs = ROLE_DOCUMENTS[roleSpecKey] || ROLE_DOCUMENTS.PROFESSIONAL;
return docs.map((title, idx) => ({
id: `${roleSpecKey.toLowerCase()}-doc-${idx + 1}`,
title,
type: title.toLowerCase().includes('proof') ? 'IMAGE' : 'PDF',
url: idx % 2 === 0 ? '/nxtgauge-logo.png' : '/nxtgauge-icon.png',
status: idx === docs.length - 1 ? 'MISSING' : 'SUBMITTED',
}));
});
const selectedPortfolio = createMemo<PortfolioAsset[]>(() => {
const row = selectedRow();
if (!row || !(row.userType === 'PROFESSIONAL' || row.requestType === 'Portfolio Approval')) return [];
const fromPayload = Array.isArray(row.payload?.portfolio_images)
? row.payload.portfolio_images
: Array.isArray(row.payload?.images)
? row.payload.images
: Array.isArray(row.payload?.gallery)
? row.payload.gallery
: [];
if (fromPayload.length) {
return fromPayload.slice(0, 6).map((asset: any, idx: number) => ({
id: String(asset.id || `pf-${idx + 1}`),
title: String(asset.title || asset.name || `Portfolio ${idx + 1}`),
url: String(asset.url || '/nxtgauge-logo.png'),
}));
}
return Array.from({ length: 6 }).map((_, idx) => ({
id: `pf-${idx + 1}`,
title: `Portfolio ${idx + 1}`,
url: idx % 2 === 0 ? '/nxtgauge-logo.png' : '/nxtgauge-icon.png',
}));
});
const selectedFieldValues = createMemo<Array<{ label: string; value: string }>>(() => {
const row = selectedRow();
if (!row) return [];
const roleSpecKey = normalizeRoleSpecKey(row.roleKey || row.userType);
const payload = row.payload || {};
const fullName = String(
payload.full_name
|| payload.fullName
|| [payload.first_name || payload.firstName, payload.last_name || payload.lastName].filter(Boolean).join(' ')
|| row.applicantName
|| '',
).trim();
const email = String(payload.email || payload.email_address || payload.emailAddress || 'applicant@nxtgauge.com');
const phone = String(payload.phone || payload.mobile || payload.mobile_number || payload.mobileNumber || '+91 90000 00000');
const area = String(payload.area || row.area || 'T. Nagar');
const place = String(payload.place || payload.locality || 'Chennai');
const city = String(payload.city || 'Chennai');
const state = String(payload.state || 'Tamil Nadu');
const pin = String(payload.pin_code || payload.pinCode || '600001');
const gender = String(payload.gender || 'Not specified');
const byLabel: Record<string, string> = {
'First Name': fullName.split(' ')[0] || fullName,
'Last Name': fullName.split(' ').slice(1).join(' ') || '—',
'Full Name': fullName || '—',
'Email Address': email,
'Mobile Number': phone,
Area: area,
Place: place,
City: city,
State: state,
'PIN Code': pin,
Gender: gender,
'Address Line 1': String(payload.address_line1 || payload.addressLine1 || payload.address || 'No. 12, Main Road'),
'Address Line 2 (Optional)': String(payload.address_line2 || payload.addressLine2 || '—'),
'Service Category': String(payload.service_category || payload.serviceCategory || row.roleLabel || 'General'),
'Company Name': String(payload.company_name || payload.companyName || row.applicantName || '—'),
'Company Email': email,
'Company Phone': phone,
'Website URL': String(payload.website || payload.website_url || payload.websiteUrl || '—'),
'Contact Person Name': String(payload.contact_person_name || payload.contactPersonName || fullName || '—'),
'Current Role': String(payload.current_role || payload.currentRole || row.roleLabel || '—'),
'Total Experience': String(payload.total_experience || payload.totalExperience || payload.experience || '—'),
};
if (row.requestType === 'Job Approval') {
return JOB_POSTING_FIELDS.map((label) => ({
label,
value: ({
'Job Title': String(payload.title || payload.job_title || '—'),
Department: String(payload.department || payload.company_department || '—'),
'Job Category': String(payload.category || payload.job_category || '—'),
'Employment Type': String(payload.employment_type || payload.type || '—'),
Seniority: String(payload.seniority || payload.level || '—'),
Openings: String(payload.openings || payload.positions || '—'),
'Role & Requirements': String(payload.requirements || payload.skills || '—'),
Compensation: String(payload.salary_range || payload.compensation || '—'),
Location: String(payload.location || payload.city || 'Chennai'),
Description: String(payload.description || payload.summary || '—'),
} as Record<string, string>)[label] || '—',
}));
}
if (row.requestType === 'Service Seeker Requirement') {
const reqRoleKey = normalizeRoleSpecKey(String(payload.role_key || payload.profession || payload.category || row.roleKey));
const dynamicReqFields = REQUIREMENT_ROLE_FIELDS[reqRoleKey] || REQUIREMENT_ROLE_FIELDS.PHOTOGRAPHER;
const baseReqFields = ['Requirement Title', 'Priority', 'Requirement Description', 'Expected Start Date', 'Service City', 'Contact Number'];
const fields = [...baseReqFields, ...dynamicReqFields];
return fields.map((label) => ({
label,
value: ({
'Requirement Title': String(payload.title || payload.requirement_title || '—'),
Priority: String(payload.priority || '—'),
'Requirement Description': String(payload.description || payload.details || '—'),
'Expected Start Date': String(payload.start_date || payload.expected_start_date || '—'),
'Service City': String(payload.city || 'Chennai'),
'Contact Number': String(payload.phone || payload.contact_number || '+91 90000 00000'),
} as Record<string, string>)[label]
|| String(
payload[label]
|| payload[label.toLowerCase().replace(/[^a-z0-9]+/g, '_')]
|| '—',
),
}));
}
if (row.requestType === 'Portfolio Approval') {
const portfolioFields = [
'About',
'Services & Pricing',
'Portfolio Photos',
'Experience & Tools',
'Specialties',
'Languages',
'Service Areas',
];
return portfolioFields.map((label) => ({
label,
value: ({
About: String(payload.about || payload.bio || '—'),
'Services & Pricing': Array.isArray(payload.services) ? `${payload.services.length} service(s) added` : String(payload.pricing || '—'),
'Portfolio Photos': Array.isArray(payload.images || payload.portfolio_images || payload.gallery) ? `${(payload.images || payload.portfolio_images || payload.gallery).length} image(s)` : '0 image(s)',
'Experience & Tools': String(payload.experience || payload.tools || '—'),
Specialties: Array.isArray(payload.specialties) ? payload.specialties.join(', ') : String(payload.specialties || '—'),
Languages: Array.isArray(payload.languages) ? payload.languages.join(', ') : String(payload.languages || '—'),
'Service Areas': Array.isArray(payload.service_areas || payload.travel_areas) ? (payload.service_areas || payload.travel_areas).join(', ') : String(payload.service_areas || '—'),
} as Record<string, string>)[label] || '—',
}));
}
const fields = ROLE_PROFILE_FIELDS[roleSpecKey] || ROLE_PROFILE_FIELDS.PROFESSIONAL;
return fields.map((label) => ({ label, value: byLabel[label] || '—' }));
});
const pushToApprovalQueue = (row: VerificationRow) => {
if (typeof window === 'undefined') return;
const item: ApprovalQueueItem = {
id: row.id,
requestType: row.requestType,
applicantName: row.applicantName,
roleLabel: row.roleLabel,
userType: row.userType,
roleKey: row.roleKey,
area: row.area,
submittedOn: row.submittedOn,
documents: selectedDocuments(),
submittedFields: selectedFieldValues(),
};
const raw = window.localStorage.getItem(APPROVAL_QUEUE_STORAGE_KEY);
const parsed = raw ? JSON.parse(raw) : [];
const current = Array.isArray(parsed) ? parsed as ApprovalQueueItem[] : [];
const filtered = current.filter((entry) => entry.id !== item.id);
window.localStorage.setItem(APPROVAL_QUEUE_STORAGE_KEY, JSON.stringify([item, ...filtered]));
};
const applySelectedStatus = async (nextStatus: VerificationStatus) => {
const current = selectedRow();
if (!current) return;
const isApprove = nextStatus === 'APPROVED';
const isReject = nextStatus === 'REJECTED';
if (!isApprove && !isReject) {
// local update only for intermediate states if needed, but usually we skip backend call here
setRows((prev) => prev.map((item) => (item.id === current.id ? { ...item, status: nextStatus } : item)));
setSelectedRow({ ...current, status: nextStatus });
return;
}
try {
const accessToken = typeof sessionStorage !== 'undefined'
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
: '';
const common = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
},
credentials: 'include' as const,
body: isReject ? JSON.stringify({ reason: requestNote() }) : undefined,
};
const endpoint = `/api/admin/verifications/${current.id}/${isApprove ? 'approve' : 'reject'}`;
const res = await fetch(`${API}${endpoint}`, common);
if (!res.ok) {
const txt = await res.text();
throw new Error(`Failed to update status (${res.status}): ${txt}`);
}
setRows((prev) => prev.map((item) => (item.id === current.id ? { ...item, status: nextStatus } : item)));
setSelectedRow({ ...current, status: nextStatus });
if (isApprove) {
pushToApprovalQueue({ ...current, status: nextStatus });
setActionMessage('Successfully verified and sent to Approval Management.');
} else {
setActionMessage('Successfully rejected submission.');
}
} catch (e: any) {
setError(e.message || 'Failed to update backend status');
}
};
const requestSelectedDocuments = () => {
const selectedIds = Object.entries(docSelection()).filter(([, checked]) => checked).map(([id]) => id);
if (selectedIds.length === 0) {
setActionMessage('Select at least one document before sending request.');
return;
}
applySelectedStatus('DOCUMENTS_REQUESTED');
setActionMessage(`Document request sent for ${selectedIds.length} item(s).`);
};
const requestProfileChanges = () => {
applySelectedStatus('REVISION_REQUESTED');
setActionMessage('Revision request sent to applicant.');
};
return (
<>
<div class="w-full space-y-6 pb-8">
<div style="margin-bottom:1.5rem">
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Verification Management</h1>
<p class="mt-1 text-[14px] text-[#6B7280]">Review and verify all platform submissions before they move to approval management</p>
</div>
<Show when={error()}>
<div style="border-radius:10px;border:1px solid #FECACA;background:#FEF2F2;padding:12px 16px;font-size:13px;color:#B91C1C">{error()}</div>
</Show>
<div style="display:flex;align-items:center;gap:24px;min-height:44px;border-bottom:1px solid #E5E7EB;overflow:auto">
{([
{ key: 'all', label: 'All Verifications' },
{ key: 'view', label: 'View Verification' },
] as const).map((tab) => (
<button
type="button"
onClick={() => setListTab(tab.key)}
style={`height:44px;padding:0 2px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;white-space:nowrap;${listTab() === tab.key ? 'color:#FF5E13;box-shadow:inset 0 -2px 0 #FF5E13' : 'color:#6B7280'}`}
>
{tab.label}
</button>
))}
</div>
<Show when={listTab() === 'all'}>
<div style="display:flex;align-items:center;gap:24px;min-height:44px;border-bottom:1px solid #E5E7EB;overflow:auto">
{([
{ key: 'all', label: `All Verifications (${tabCounts().all})` },
{ key: 'profile', label: `Profile Approvals (${tabCounts().profile})` },
{ key: 'portfolio', label: `Portfolio Approvals (${tabCounts().portfolio})` },
{ key: 'company', label: `Company Approvals (${tabCounts().company})` },
{ key: 'job_seeker', label: `Job Seeker Approvals (${tabCounts().jobSeeker})` },
{ key: 'service_profile', label: `Service Seeker Profile (${tabCounts().serviceProfile})` },
{ key: 'service_requirement', label: `Service Seeker Requirements (${tabCounts().serviceRequirement})` },
{ key: 'job', label: `Job Approvals (${tabCounts().job})` },
] as const).map((tab) => (
<button
type="button"
onClick={() => setCategoryTab(tab.key)}
style={`height:44px;padding:0 2px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;white-space:nowrap;${categoryTab() === tab.key ? 'color:#FF5E13;box-shadow:inset 0 -2px 0 #FF5E13' : 'color:#6B7280'}`}
>
{tab.label}
</button>
))}
</div>
</Show>
<Show when={listTab() === 'all'}>
<div style="position:relative;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none;overflow:visible;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)">
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
<input
value={search()}
onInput={(e) => setSearch(e.currentTarget.value)}
placeholder="Search by ID, name or type..."
style="height:34px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none"
/>
<div style="position:relative">
<button
type="button"
onClick={() => { setSortOpen((v) => !v); setFilterOpen(false); }}
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 4v13"/><path d="m3 13 4 4 4-4"/><path d="M17 20V7"/><path d="m21 11-4-4-4 4"/></svg>
Sort
</button>
<Show when={sortOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{([
{ key: 'latest', label: 'Latest Submitted' },
{ key: 'oldest', label: 'Oldest Submitted' },
{ key: 'priority', label: 'Priority (High-Low)' },
] as const).map((item) => (
<button
type="button"
onClick={() => { setSortBy(item.key); setSortOpen(false); }}
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === item.key ? '#FF5E13' : '#374151'};background:${sortBy() === item.key ? '#FFF1EB' : 'transparent'}`}
>
{item.label}
</button>
))}
</div>
</Show>
</div>
<div style="position:relative">
<button
type="button"
onClick={() => { setFilterOpen((v) => !v); setSortOpen(false); }}
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
Filters
</button>
<Show when={filterOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{([
{ key: 'ALL', label: 'All Status' },
{ key: 'PENDING', label: 'Pending' },
{ key: 'UNDER_REVIEW', label: 'Under Review' },
{ key: 'DOCUMENTS_REQUESTED', label: 'Documents Requested' },
{ key: 'REVISION_REQUESTED', label: 'Revision Requested' },
{ key: 'APPROVED', label: 'Approved' },
{ key: 'REJECTED', label: 'Rejected' },
] as const).map((item) => (
<button
type="button"
onClick={() => { setStatusFilter(item.key); setFilterOpen(false); }}
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === item.key ? '#FF5E13' : '#374151'};background:${statusFilter() === item.key ? '#FFF1EB' : 'transparent'}`}
>
{item.label}
</button>
))}
</div>
</Show>
</div>
<button
type="button"
onClick={exportCsv}
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>
</div>
<div class="overflow-x-auto overflow-y-visible">
<table class="min-w-full">
<thead>
<tr style="background:#0D0D2A;text-align:left">
{['Submission ID', 'Type', 'Applicant Name', 'Role', 'Submitted On', 'Status', 'Priority', 'Actions'].map((header) => (
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">{header}</th>
))}
</tr>
</thead>
<tbody>
<Show
when={displayRows().length > 0}
fallback={
<tr>
<td colSpan={8} style="padding:32px;text-align:center">
<p style="font-size:15px;font-weight:600;color:#111827">No verification requests found</p>
<p style="margin-top:6px;font-size:13px;color:#6B7280">Try changing filters or search.</p>
</td>
</tr>
}
>
<For each={displayRows()}>
{(row) => {
const s = statusUi(row.status);
const p = priorityUi(row.priority);
return (
<tr style="border-bottom:1px solid #F3F4F6" class="hover:bg-[#FAFAFA] transition-colors">
<td style="padding:12px 20px;font-size:12px;font-family:monospace;color:#6B7280">#{row.id}</td>
<td style="padding:12px 20px;font-size:14px;color:#111827">{row.requestType}</td>
<td style="padding:12px 20px">
<div style="display:flex;flex-direction:column;gap:2px">
<span style="font-size:14px;font-weight:600;color:#111827">{row.applicantName}</span>
<span style="font-size:12px;color:#6B7280">{row.area || 'Chennai'}</span>
</div>
</td>
<td style="padding:12px 20px;font-size:14px;color:#111827">{row.roleLabel}</td>
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{String(row.submittedOn || '').slice(0, 10) || '—'}</td>
<td style="padding:12px 20px">
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${s.border};background:${s.bg};color:${s.text};padding:2px 10px;font-size:12px;font-weight:500`}>
{s.label}
</span>
</td>
<td style="padding:12px 20px">
<span style={`display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:700;color:${p.color}`}>
<span style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${p.color}`} />
{p.label}
</span>
</td>
<td style="padding:12px 20px">
<button
type="button"
onClick={() => {
setSelectedRow(row);
setDocSelection({});
setRequestNote('');
setActionMessage('');
setListTab('view');
}}
style="height:30px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;font-weight:700;color:#374151;display:inline-flex;align-items:center;cursor:pointer"
>
View
</button>
</td>
</tr>
);
}}
</For>
</Show>
</tbody>
</table>
</div>
<Show when={displayRows().length > 0}>
<div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #F3F4F6;padding:12px 20px">
<p style="font-size:13px;color:#6B7280">
Showing <strong style="font-weight:600;color:#111827">1{displayRows().length}</strong> of <strong style="font-weight:600;color:#111827">{displayRows().length}</strong> verifications
</p>
<div style="display:flex;align-items:center;gap:4px">
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px"></button>
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;background:#FF5E13;color:white;font-size:13px;font-weight:600;border:none;cursor:pointer">1</button>
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer">2</button>
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer">3</button>
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px"></button>
</div>
</div>
</Show>
</div>
</Show>
<Show when={listTab() === 'view'}>
<Show
when={selectedRow()}
fallback={
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;padding:48px 24px;text-align:center">
<p style="font-size:15px;font-weight:600;color:#111827">No verification selected</p>
<p style="margin-top:6px;font-size:13px;color:#6B7280">Click <strong>View</strong> on a row from <strong>All Verifications</strong>.</p>
</div>
}
>
<div style="margin-top:24px;display:grid;gap:12px">
<div style="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:20px 24px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between">
<div>
<h2 style="font-size:18px;font-weight:700;color:#111827">#{selectedRow()!.id}</h2>
<p style="margin-top:2px;font-size:13px;color:#6B7280">{selectedRow()!.requestType}</p>
</div>
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${statusUi(selectedRow()!.status).border};background:${statusUi(selectedRow()!.status).bg};color:${statusUi(selectedRow()!.status).text};padding:2px 10px;font-size:12px;font-weight:500`}>
{statusUi(selectedRow()!.status).label}
</span>
</div>
<div>
<div style="display:flex;border-bottom:1px solid #F3F4F6">
<div style="flex:1;padding:16px 24px;border-right:1px solid #F3F4F6">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">Applicant Name</p>
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">{selectedRow()!.applicantName}</p>
</div>
<div style="flex:1;padding:16px 24px;border-right:1px solid #F3F4F6">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">Role</p>
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">{selectedRow()!.roleLabel}</p>
</div>
<div style="flex:1;padding:16px 24px">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">Area</p>
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">{selectedRow()!.area || 'Chennai'}</p>
</div>
</div>
</div>
</div>
<div style="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:16px 20px;border-bottom:1px solid #F3F4F6;display:flex;align-items:center;justify-content:space-between">
<h3 style="margin:0;font-size:16px;font-weight:700;color:#111827">Submitted Form Details</h3>
<span style="font-size:12px;color:#6B7280">{selectedFieldValues().length} fields</span>
</div>
<div style="padding:14px;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px">
<For each={selectedFieldValues()}>
{(entry) => (
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px">
<p style="margin:0;font-size:10px;letter-spacing:0.05em;text-transform:uppercase;color:#9CA3AF">{entry.label}</p>
<p style="margin:6px 0 0;font-size:13px;font-weight:700;color:#111827;line-height:1.35">{entry.value}</p>
</div>
)}
</For>
</div>
</div>
<div style="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:16px 20px;border-bottom:1px solid #F3F4F6;display:flex;align-items:center;justify-content:space-between">
<h3 style="margin:0;font-size:16px;font-weight:700;color:#111827">Submitted Documents</h3>
<span style="font-size:12px;color:#6B7280">{selectedDocuments().length} files</span>
</div>
<div class="overflow-x-auto">
<table class="min-w-full">
<thead>
<tr style="background:#0D0D2A;text-align:left">
{['Document', 'State', 'View', 'Request'].map((header) => (
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">{header}</th>
))}
</tr>
</thead>
<tbody>
<For each={selectedDocuments()}>
{(doc) => (
<tr style="border-bottom:1px solid #F3F4F6">
<td style="padding:12px 16px;font-size:14px;font-weight:600;color:#111827">{doc.title}</td>
<td style="padding:12px 16px;font-size:13px;color:#6B7280">{toTitle(doc.status)}</td>
<td style="padding:12px 16px">
<button
type="button"
onClick={() => setViewer({ open: true, title: doc.title, type: doc.type, url: doc.url })}
style="height:30px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;font-weight:700;color:#374151;cursor:pointer"
>
View
</button>
</td>
<td style="padding:12px 16px">
<label style="display:inline-flex;align-items:center;gap:8px;font-size:13px;color:#374151;cursor:pointer">
<input
type="checkbox"
checked={Boolean(docSelection()[doc.id])}
onChange={(e) => setDocSelection((prev) => ({ ...prev, [doc.id]: e.currentTarget.checked }))}
style="accent-color:#FF5E13"
/>
Request
</label>
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</div>
<div style="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:16px 20px;border-bottom:1px solid #F3F4F6;display:flex;align-items:center;justify-content:space-between;background:#FFFBF0">
<h3 style="margin:0;font-size:16px;font-weight:700;color:#854D0E">Verification Requirements Checklist</h3>
<span style="font-size:12px;font-weight:600;color:#A16207">System Audit</span>
</div>
<div style="padding:20px;display:grid;gap:12px">
<For each={selectedDocuments()}>
{(doc) => (
<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-radius:8px;background:#F9FAFB;border:1px solid #E5E7EB">
<div style="display:flex;align-items:center;gap:10px">
<div style={`width:18px;height:18px;border-radius:50%;display:flex;align-items:center;justify-content:center;${doc.status === 'SUBMITTED' ? 'background:#22C55E' : 'background:#EF4444'}`}>
<Show when={doc.status === 'SUBMITTED'} fallback={<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>}>
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="4"><polyline points="20 6 9 17 4 12"/></svg>
</Show>
</div>
<span style="font-size:13px;font-weight:600;color:#374151">{doc.title}</span>
</div>
<span style={`font-size:11px;font-weight:700;padding:2px 8px;border-radius:4px;${doc.status === 'SUBMITTED' ? 'background:#DCFCE7;color:#166534' : 'background:#FEE2E2;color:#991B1B'}`}>
{doc.status === 'SUBMITTED' ? 'RECEIVED' : 'MISSING'}
</span>
</div>
)}
</For>
<div style="margin-top:8px;padding-top:16px;border-top:1px dashed #E5E7EB">
<p style="font-size:12px;font-weight:700;color:#6B7280;margin-bottom:8px;text-transform:uppercase">Profile Completeness</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
<For each={selectedFieldValues().slice(0, 6)}>
{(field) => (
<div style="display:flex;align-items:center;gap:8px;font-size:12px;color:#4B5563">
<div style={`width:6px;height:6px;border-radius:50%;${field.value && field.value !== '—' ? 'background:#22C55E' : 'background:#EF4444'}`} />
<span style="opacity:0.8">{field.label}:</span>
<span style="font-weight:600">{field.value && field.value !== '—' ? 'Filled' : 'Empty'}</span>
</div>
)}
</For>
</div>
</div>
</div>
</div>
<div style="border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);padding:16px">
<h3 style="margin:0 0 8px;font-size:16px;font-weight:700;color:#111827">Reviewer Actions</h3>
<textarea
value={requestNote()}
onInput={(e) => setRequestNote(e.currentTarget.value)}
placeholder="Reviewer note for applicant"
style="width:100%;min-height:74px;border:1px solid #E5E7EB;border-radius:10px;padding:10px;font-size:13px;color:#374151;resize:vertical"
/>
<div style="margin-top:10px;display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<button type="button" onClick={requestSelectedDocuments} style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151;cursor:pointer">Request Selected Documents</button>
<button type="button" onClick={requestProfileChanges} style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151;cursor:pointer">Request Changes</button>
<button type="button" onClick={() => applySelectedStatus('APPROVED')} style="height:34px;border-radius:8px;border:none;background:#0D0D2A;padding:0 12px;font-size:12px;font-weight:700;color:white;cursor:pointer">Approve</button>
<button type="button" onClick={() => applySelectedStatus('REJECTED')} style="height:34px;border-radius:8px;border:1px solid #FECACA;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#B91C1C;cursor:pointer">Reject</button>
</div>
<Show when={actionMessage()}>
<div style="margin-top:10px;border-radius:8px;border:1px solid #E5E7EB;background:#F9FAFB;padding:8px 10px;font-size:12px;color:#374151">{actionMessage()}</div>
</Show>
<Show when={selectedRow()!.status === 'APPROVED'}>
<A href="/admin/approval" style="margin-top:8px;height:32px;border-radius:8px;border:none;background:#0D0D2A;color:white;padding:0 12px;font-size:12px;font-weight:700;display:inline-flex;align-items:center;text-decoration:none">
Open Approval Management
</A>
</Show>
</div>
<div style="display:flex;align-items:center;gap:10px">
<A
href={`/admin/verification/${encodeURIComponent(selectedRow()!.id)}?roleKey=${encodeURIComponent(selectedRow()!.roleKey || '')}&userId=${encodeURIComponent(selectedRow()!.userId || '')}&type=${encodeURIComponent(selectedRow()!.requestType)}`}
style="height:36px;border-radius:8px;background:#0D0D2A;padding:0 16px;font-size:13px;font-weight:600;color:white;display:inline-flex;align-items:center;text-decoration:none"
>
Open Full Review
</A>
<button type="button" onClick={() => setListTab('all')} 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">Back to List</button>
</div>
</div>
</Show>
</Show>
</div>
<Show when={viewer().open}>
<div style="position:fixed;inset:0;z-index:9999;background:rgba(17,24,39,0.72);display:flex;align-items:center;justify-content:center;padding:20px">
<div style="width:min(90vw,900px);max-height:88vh;border-radius:14px;overflow:hidden;border:1px solid #E5E7EB;background:white;display:flex;flex-direction:column">
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between">
<p style="margin:0;font-size:14px;font-weight:700;color:#111827">{viewer().title}</p>
<button type="button" onClick={() => setViewer({ open: false, title: '', type: 'IMAGE', url: '' })} style="height:30px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;font-weight:700;color:#374151;cursor:pointer">Close</button>
</div>
<div style="padding:12px;overflow:auto;display:flex;justify-content:center;align-items:center;background:#F8FAFC">
<Show
when={viewer().type === 'IMAGE'}
fallback={<iframe src={viewer().url} style="width:100%;height:70vh;border:none;background:white" title={viewer().title} />}
>
<img src={viewer().url} alt={viewer().title} style="max-width:100%;max-height:70vh;object-fit:contain" />
</Show>
</div>
</div>
</div>
</Show>
</>
);
}