import { A, useParams, useSearchParams } from '@solidjs/router'; import { createMemo, createResource, createSignal, For, Show, createEffect } from 'solid-js'; const API = ''; // ── Types ────────────────────────────────────────────────────────── type RoleType = | 'COMPANY' | 'CANDIDATE' | 'CUSTOMER' | 'PHOTOGRAPHER' | 'MAKEUP_ARTIST' | 'TUTOR' | 'DEVELOPER' | 'VIDEO_EDITOR' | 'GRAPHIC_DESIGNER' | 'SOCIAL_MEDIA_MANAGER' | 'FITNESS_TRAINER' | 'CATERING_SERVICE' | 'ADMIN' | 'UNKNOWN'; interface SubmissionData { user: { id: string; name?: string; email: string; phone?: string; status: string; email_verified: boolean; created_at: string; }; role_key?: string; onboarding?: { status: string; progress_json: Record; completed_at?: string; updated_at: string; } | null; } interface AdminRemark { type: 'INFO' | 'CHANGES_REQUESTED' | 'MORE_DOCUMENTS_REQUESTED' | 'REJECTED'; comment: string; fields?: string[]; } // ── Helpers ────────────────────────────────────────────────────────── function inferRoleType(roleKey?: string, name?: string): RoleType { const raw = [roleKey, name].filter(Boolean).join(' ').toLowerCase(); if (raw.includes('photographer')) return 'PHOTOGRAPHER'; if (raw.includes('makeup')) return 'MAKEUP_ARTIST'; if (raw.includes('tutor')) return 'TUTOR'; if (raw.includes('developer')) return 'DEVELOPER'; if (raw.includes('video') || raw.includes('video_editor')) return 'VIDEO_EDITOR'; if (raw.includes('graphic') || raw.includes('graphic_designer')) return 'GRAPHIC_DESIGNER'; if (raw.includes('social') || raw.includes('social_media')) return 'SOCIAL_MEDIA_MANAGER'; if (raw.includes('fitness') || raw.includes('fitness_trainer')) return 'FITNESS_TRAINER'; if (raw.includes('catering')) return 'CATERING_SERVICE'; if (raw.includes('customer')) return 'CUSTOMER'; if (raw.includes('job_seeker') || raw.includes('candidate')) return 'CANDIDATE'; if (raw.includes('company')) return 'COMPANY'; if (raw.includes('admin') || raw.includes('employee')) return 'ADMIN'; return 'UNKNOWN'; } function managementDest(roleType: RoleType): { label: string; href: string } { const map: Record = { COMPANY: { label: 'Company Management', href: '/admin/company' }, CANDIDATE: { label: 'Candidate Management', href: '/admin/candidate' }, CUSTOMER: { label: 'Customer Management', href: '/admin/customer' }, PHOTOGRAPHER: { label: 'Photographer Management', href: '/admin/photographer' }, MAKEUP_ARTIST: { label: 'Makeup Artist Management', href: '/admin/makeup-artist' }, TUTOR: { label: 'Tutors Management', href: '/admin/tutors' }, DEVELOPER: { label: 'Developers Management', href: '/admin/developers' }, VIDEO_EDITOR: { label: 'Video Editor Management', href: '/admin/video-editors' }, GRAPHIC_DESIGNER: { label: 'Graphics Designer Management', href: '/admin/graphic-designers' }, SOCIAL_MEDIA_MANAGER: { label: 'Social Media Manager Management', href: '/admin/social-media-managers' }, FITNESS_TRAINER: { label: 'Fitness Trainer Management', href: '/admin/fitness-trainers' }, CATERING_SERVICE: { label: 'Catering Services Management', href: '/admin/catering-services' }, ADMIN: { label: 'Employee Management', href: '/admin/employees' }, UNKNOWN: { label: 'Users Management', href: '/admin/users' }, }; return map[roleType] ?? map.UNKNOWN; } /** Flatten nested JSON into key→value pairs for display */ function flattenFields(obj: Record, 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, label)); } else if (Array.isArray(v)) { result.push({ key: label, value: v.join(', ') }); } else { result.push({ key: label, value: String(v ?? '—') }); } } return result; } /** Skip internal tracking fields — only show what the user actually submitted */ const SKIP_KEYS = new Set(['step', 'total', 'currentStep', '__version', '__schema']); function isSubmittedField(key: string): boolean { return !SKIP_KEYS.has(key) && !key.startsWith('_'); } const ROLE_COLORS: Record = { COMPANY: 'background:#dbeafe;color:#1d4ed8', CANDIDATE: 'background:#e0e7ff;color:#4338ca', CUSTOMER: 'background:#cffafe;color:#0e7490', PHOTOGRAPHER: 'background:#ede9fe;color:#6d28d9', MAKEUP_ARTIST: 'background:#fce7f3;color:#9d174d', TUTOR: 'background:#d1fae5;color:#065f46', DEVELOPER: 'background:#e0f2fe;color:#0369a1', VIDEO_EDITOR: 'background:#fef3c7;color:#92400e', GRAPHIC_DESIGNER: 'background:#f0fdf4;color:#166534', SOCIAL_MEDIA_MANAGER: 'background:#fdf2f8;color:#86198f', FITNESS_TRAINER: 'background:#fff7ed;color:#9a3412', CATERING_SERVICE: 'background:#fefce8;color:#854d0e', ADMIN: 'background:#f1f5f9;color:#334155', UNKNOWN: 'background:#f8fafc;color:#64748b', }; // ── Field type detection ────────────────────────────────────────────────────────── type FieldKind = 'image' | 'pdf' | 'document' | 'url' | 'text'; function detectKind(key: string, value: string): FieldKind { const k = key.toLowerCase(); const v = (value || '').toLowerCase(); // Image: key hints OR common image extensions if ( /photo|image|picture|avatar|selfie|headshot|thumbnail|profile_pic/i.test(k) || /\.(jpg|jpeg|png|gif|webp|bmp|svg)(\?|$)/i.test(v) ) return 'image'; // PDF if (/\.(pdf)(\?|$)/i.test(v) || /pdf|resume|cv\b/i.test(k)) return 'pdf'; // Generic document/upload (not image/pdf) if ( /upload|document|file|attachment|certificate|license|govt_id|aadhaar|pan|passport|degree|transcript|portfolio|id_proof/i.test(k) || /\.(doc|docx|xls|xlsx|ppt|pptx|zip|rar)(\?|$)/i.test(v) ) return 'document'; // URL that isn't a file if (value.startsWith('http') || value.startsWith('/')) return 'url'; return 'text'; } // ── Data loaders ────────────────────────────────────────────────────────── async function loadSubmission(args: { userId: string; roleKey: string }): Promise { if (!args.userId) return null; try { const qs = args.roleKey ? `?roleKey=${encodeURIComponent(args.roleKey)}` : ''; const res = await fetch(`${API}/api/admin/approvals/submission/${args.userId}${qs}`); if (!res.ok) return null; return res.json(); } catch { return null; } } // ── Page ────────────────────────────────────────────────────────── export default function ApprovalDetailPage() { const params = useParams(); const [searchParams] = useSearchParams(); // params.id can be either: // - a user UUID → we load the submission directly // - an old approval request ID → shown as legacy fallback const userId = () => params.id; const roleKey = () => (searchParams.roleKey as string) || ''; const [data] = createResource( () => ({ userId: userId(), roleKey: roleKey() }), loadSubmission, ); const [acting, setActing] = createSignal(''); const [actionError, setActionError] = createSignal(''); const [actionDone, setActionDone] = createSignal(''); const roleType = createMemo(() => inferRoleType(data()?.role_key, undefined)); const dest = createMemo(() => managementDest(roleType())); // Flatten progress_json into displayable rows const submittedRows = createMemo(() => { const pj = data()?.onboarding?.progress_json; if (!pj || typeof pj !== 'object') return []; return flattenFields(pj as Record) .filter((f) => isSubmittedField(f.key)); }); // ── Approve / Reject ── // Routes: POST /api/admin/approvals/profiles/professional/{role_key}/{user_id}/approve // POST /api/admin/approvals/profiles/company/{user_id}/approve // POST /api/admin/approvals/profiles/customer/{user_id}/approve const getApprovalPath = (action: 'approve' | 'reject') => { const rk = (roleKey() || '').toUpperCase(); const uid = userId(); if (rk === 'COMPANY') return `/api/admin/approvals/profiles/company/${uid}/${action}`; if (rk === 'CUSTOMER') return `/api/admin/approvals/profiles/customer/${uid}/${action}`; if (rk) return `/api/admin/approvals/profiles/professional/${rk}/${uid}/${action}`; return null; }; const handleApprove = async () => { if (!confirm('Approve this profile submission?')) return; const path = getApprovalPath('approve'); if (!path) { setActionError('Cannot resolve approval endpoint — roleKey missing in URL'); return; } try { setActing('APPROVE'); setActionError(''); const res = await fetch(`${API}${path}`, { method: 'POST' }); if (!res.ok) throw new Error('Failed to approve'); setActionDone('APPROVED'); } catch (err: any) { setActionError(err.message || 'Failed to approve'); } finally { setActing(''); } }; const handleReject = async () => { const reason = prompt('Rejection reason (required):'); if (!reason?.trim()) return; const path = getApprovalPath('reject'); if (!path) { setActionError('Cannot resolve rejection endpoint — roleKey missing in URL'); return; } try { setActing('REJECT'); setActionError(''); const res = await fetch(`${API}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reason: reason.trim() }), }); if (!res.ok) throw new Error('Failed to reject'); setActionDone('REJECTED'); } catch (err: any) { setActionError(err.message || 'Failed to reject'); } finally { setActing(''); } }; return (

Submission Review

Review a user's onboarding form submission and take action.

← Back to Approvals
{actionError()}

{actionDone() === 'APPROVED' ? '✓ Profile approved successfully.' : '✕ Profile rejected.'}

Open {dest().label} →

Loading submission...

Submission not found or user does not have an onboarding record for this role.

Make sure the URL includes ?roleKey=PHOTOGRAPHER (or the relevant role key).

{/* ── Action bar ── */}
{(roleKey() || 'UNKNOWN').replace(/_/g, ' ')} Onboarding: {data()!.onboarding?.status ?? 'NO DATA'}
{/* User info */}

User Info

Name{data()!.user.name || '—'}
Email{data()!.user.email}
Phone{data()!.user.phone || '—'}
Account Status {data()!.user.status}
Email Verified {data()!.user.email_verified ? '✓ Yes' : '✕ No'}
Registered{new Date(data()!.user.created_at).toLocaleDateString()}
User ID{data()!.user.id}
{/* Submission status */}

Submission Info

No onboarding data found

This user has not started or submitted the onboarding form for role: {roleKey() || 'unknown'}

}>
Role{(roleKey() || '—').replace(/_/g, ' ')}
Status {data()!.onboarding!.status}
Submitted{data()!.onboarding!.completed_at ? new Date(data()!.onboarding!.completed_at!).toLocaleString() : '—'}
Last Updated{new Date(data()!.onboarding!.updated_at).toLocaleString()}
Fields{submittedRows().length} fields submitted
{/* ── Submitted form answers + media viewer ── */} 0}> {/* ── No form data fallback ── */}

Submitted Form Answers

Onboarding state is present but contains no displayable field data.

); } // ────────────────────────────── SubmissionViewer ────────────────────────────── function SubmissionViewer(props: { rows: Array<{ key: string; value: string }> }) { const [lightbox, setLightbox] = createSignal<{ src: string; label: string } | null>(null); const [pdfViewer, setPdfViewer] = createSignal<{ src: string; label: string } | null>(null); // Split rows into text fields vs media const textFields = createMemo(() => props.rows.filter((r) => { const kind = detectKind(r.key, r.value); return kind === 'text' || kind === 'url'; }) ); const mediaFields = createMemo(() => props.rows.filter((r) => { const kind = detectKind(r.key, r.value); return kind === 'image' || kind === 'pdf' || kind === 'document'; }) ); return ( <> {/* ── Text / data fields ── */} 0}>

Submitted Form Data {textFields().length} fields

{(field) => { const kind = detectKind(field.key, field.value); return (
{field.key.replace(/_/g, ' ').replace(/\./g, ' › ').toUpperCase()}
{field.value || '—'}
}> 🔗 {field.value}
); }}
{/* ── Documents & Images ── */} 0}>

Documents & Media {mediaFields().length} file{mediaFields().length !== 1 ? 's' : ''}

{(field) => { const kind = detectKind(field.key, field.value); const label = field.key.replace(/_/g, ' ').replace(/\./g, ' › '); return (
{/* Preview area */}
setLightbox({ src: field.value, label })} > {label} { (e.target as HTMLImageElement).style.display = 'none'; ((e.target as HTMLImageElement).nextElementSibling as HTMLElement)!.style.display = 'flex'; }} />
🖼 Preview unavailable
🔍 Click to enlarge
setPdfViewer({ src: field.value, label })} > 📄 PDF Document Click to view
{/* Label + open link */}
{label}
↗ Download
); }}
{/* ── Image Lightbox ── */}
setLightbox(null)} >
{lightbox()!.label}
{lightbox()!.label} e.stopPropagation()} />
{/* ── PDF Viewer Modal ── */}
{/* Header */}
📄 {pdfViewer()!.label}
↗ Open in new tab
{/* PDF embed */}