diff --git a/src/components/admin/AdminShell.tsx b/src/components/admin/AdminShell.tsx new file mode 100644 index 0000000..38ab030 --- /dev/null +++ b/src/components/admin/AdminShell.tsx @@ -0,0 +1,109 @@ +import { A, useLocation } from '@solidjs/router'; +import { For, Show, createMemo, type JSX } from 'solid-js'; +import { adminModules } from '~/lib/admin/module-config'; + +const iconByRoute: Record = { + '/admin': 'DB', + '/admin/department': 'DP', + '/admin/designation': 'DG', + '/admin/internal-role-management': 'IR', + '/admin/employees': 'EM', + '/admin/external-role-management': 'ER', + '/admin/external-onboarding': 'EO', + '/admin/internal-dashboard': 'ID', + '/admin/external-dashboard': 'ED', + '/admin/verification-management': 'VR', + '/admin/approval-management': 'AP', +}; + +export default function AdminShell(props: { children: JSX.Element }) { + const location = useLocation(); + const navItems = createMemo(() => adminModules.filter((module) => module.route !== '/admin')); + + return ( +
+
+ + +
+
+
+ + +
+ + + +
+
+ +
{props.children}
+
+
+
+ ); +} diff --git a/src/components/admin/AdminUi.tsx b/src/components/admin/AdminUi.tsx new file mode 100644 index 0000000..f0e4afe --- /dev/null +++ b/src/components/admin/AdminUi.tsx @@ -0,0 +1,186 @@ +import { For, Show, type JSX } from 'solid-js'; + +type Metric = { + label: string; + value: string; + tone?: 'default' | 'positive' | 'warning' | 'critical'; +}; + +export function PageHeader(props: { title: string; subtitle: string; actions?: JSX.Element }) { + return ( +
+
+

{props.title}

+

{props.subtitle}

+
+ +
{props.actions}
+
+
+ ); +} + +export function MetricCards(props: { items: Metric[] }) { + return ( +
+ + {(item) => ( +
+

{item.label}

+

+ {item.value} +

+
+ )} +
+
+ ); +} + +export function SectionCard(props: { title: string; subtitle?: string; actions?: JSX.Element; children: JSX.Element }) { + return ( +
+
+
+

{props.title}

+ +

{props.subtitle}

+
+
+ +
{props.actions}
+
+
+
{props.children}
+
+ ); +} + +export function Tabs(props: { value: T; onChange: (key: T) => void; items: { key: T; label: string }[] }) { + return ( +
+ + {(item) => ( + + )} + +
+ ); +} + +export function SearchFilters(props: { + query: string; + onQuery: (v: string) => void; + left?: JSX.Element; + right?: JSX.Element; + placeholder?: string; +}) { + return ( +
+ + +
{props.left}
+
+ +
{props.right}
+
+
+ ); +} + +export function StatusBadge(props: { label: string; tone?: 'neutral' | 'positive' | 'warning' | 'critical' | 'info' }) { + const tone = () => props.tone ?? 'neutral'; + return ( + + {props.label} + + ); +} + +export function ActionButton(props: { onClick?: () => void; children: JSX.Element; tone?: 'primary' | 'secondary' | 'ghost'; type?: 'button' | 'submit' | 'reset' }) { + const tone = () => props.tone ?? 'secondary'; + return ( + + ); +} + +export function DataTable(props: { headers: string[]; rows: JSX.Element[][] }) { + return ( +
+ + + + {(head) => } + + + + + {(row) => ( + + {(cell) => } + + )} + + +
{head}
{cell}
+
+ ); +} + +export function ModuleStatusPill(props: { status: 'live' | 'mock' | 'pending' }) { + return ( + + ); +} diff --git a/src/components/admin/CrudManagementPage.tsx b/src/components/admin/CrudManagementPage.tsx new file mode 100644 index 0000000..f302149 --- /dev/null +++ b/src/components/admin/CrudManagementPage.tsx @@ -0,0 +1,89 @@ +import { createMemo, createSignal, For } from 'solid-js'; +import type { CrudRecord } from '~/lib/admin/types'; +import { ActionButton, DataTable, PageHeader, SearchFilters, SectionCard, StatusBadge, Tabs } from './AdminUi'; + +export default function CrudManagementPage(props: { + title: string; + subtitle: string; + records: CrudRecord[]; + noun: string; +}) { + const [tab, setTab] = createSignal<'list' | 'create'>('list'); + const [query, setQuery] = createSignal(''); + + const filtered = createMemo(() => { + const q = query().trim().toLowerCase(); + if (!q) return props.records; + return props.records.filter((row) => row.id.toLowerCase().includes(q) || row.name.toLowerCase().includes(q)); + }); + + return ( +
+ + } + /> + + {tab() === 'list' ? ( + + Export + Add {props.noun} + + } + > +
+ Filter} /> + [ + {row.id}, + {row.name}, + , + {new Date(row.updatedAt).toLocaleDateString()}, +
+ Edit + View +
, + ])} + /> + {() =>

No records found.

}
+
+
+ ) : ( + +
+ + +
+ Cancel + Save +
+
+
+ )} +
+ ); +} diff --git a/src/components/admin/GenericAdminModulePage.tsx b/src/components/admin/GenericAdminModulePage.tsx new file mode 100644 index 0000000..ca7cb3a --- /dev/null +++ b/src/components/admin/GenericAdminModulePage.tsx @@ -0,0 +1,202 @@ +import { createMemo, createSignal, For, onMount } from 'solid-js'; +import type { AdminModuleConfig, CrudRecord } from '~/lib/admin/types'; +import { bulkModuleRecordAction, createModuleRecord, deleteModuleRecord, listModuleRecords, updateModuleRecord } from '~/lib/admin/client'; +import { ActionButton, DataTable, ModuleStatusPill, PageHeader, SearchFilters, SectionCard, StatusBadge } from './AdminUi'; + +export default function GenericAdminModulePage(props: { module: AdminModuleConfig }) { + const [loading, setLoading] = createSignal(true); + const [query, setQuery] = createSignal(''); + const [status, setStatus] = createSignal(''); + const [rows, setRows] = createSignal([]); + const [selected, setSelected] = createSignal([]); + const [error, setError] = createSignal(''); + const [nameInput, setNameInput] = createSignal(''); + const [statusInput, setStatusInput] = createSignal<'ACTIVE' | 'INACTIVE'>('ACTIVE'); + + const load = async () => { + setLoading(true); + setError(''); + try { + const moduleKey = props.module.route.replace(/^\/admin\//, '').replace(/^\/admin$/, 'dashboard'); + const data = await listModuleRecords(moduleKey, { q: query(), status: status() || undefined }); + setRows(data); + } catch (err: any) { + setError(String(err?.message || 'Failed to load records.')); + setRows([]); + } + setLoading(false); + }; + + onMount(load); + + const filtered = createMemo(() => { + const q = query().trim().toLowerCase(); + if (!q) return rows(); + return rows().filter((row) => row.id.toLowerCase().includes(q) || row.name.toLowerCase().includes(q)); + }); + + return ( +
+ + + Create + + } + /> + + + Export + Bulk Actions + + } + > +
+ { + setQuery(v); + void load(); + }} + right={ + <> + + Filter + { + if (selected().length === 0) return; + const moduleKey = props.module.route.replace(/^\/admin\//, '').replace(/^\/admin$/, 'dashboard'); + void bulkModuleRecordAction(moduleKey, selected(), 'activate').then(() => { + setSelected([]); + void load(); + }); + }} + > + Activate + + { + if (selected().length === 0) return; + const moduleKey = props.module.route.replace(/^\/admin\//, '').replace(/^\/admin$/, 'dashboard'); + void bulkModuleRecordAction(moduleKey, selected(), 'deactivate').then(() => { + setSelected([]); + void load(); + }); + }} + > + Deactivate + + + } + /> + {error() ?

{error()}

: null} + + {loading() ? ( +
Loading {props.module.title}...
+ ) : ( + [ + + setSelected((prev) => + e.currentTarget.checked ? [...new Set([...prev, row.id])] : prev.filter((id) => id !== row.id), + ) + } + />, + {row.id}, + {row.name}, + , + {new Date(row.updatedAt).toLocaleString()}, +
+ { + const moduleKey = props.module.route.replace(/^\/admin\//, '').replace(/^\/admin$/, 'dashboard'); + const nextStatus = row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE'; + void updateModuleRecord(moduleKey, row.id, { status: nextStatus }).then(() => void load()); + }} + > + Toggle + + { + const moduleKey = props.module.route.replace(/^\/admin\//, '').replace(/^\/admin$/, 'dashboard'); + void deleteModuleRecord(moduleKey, row.id).then(() => { + setSelected((prev) => prev.filter((id) => id !== row.id)); + void load(); + }); + }} + > + Delete + +
, + ])} + /> + )} + + {() =>

No records matched your filter.

}
+
+
+ + +
{ + e.preventDefault(); + if (!nameInput().trim()) return; + const moduleKey = props.module.route.replace(/^\/admin\//, '').replace(/^\/admin$/, 'dashboard'); + void createModuleRecord(moduleKey, { name: nameInput().trim(), status: statusInput() }).then(() => { + setNameInput(''); + setStatusInput('ACTIVE'); + void load(); + }); + }} + > + + +
+ Create Record +
+
+
+
+ ); +} diff --git a/src/lib/admin/api.ts b/src/lib/admin/api.ts new file mode 100644 index 0000000..93131b8 --- /dev/null +++ b/src/lib/admin/api.ts @@ -0,0 +1,47 @@ +import { gatewayUrl, withAuthHeaders } from '~/lib/server/gateway'; + +export async function proxyOrFallback(args: { + request: Request; + method: 'GET' | 'POST' | 'PATCH' | 'DELETE'; + path: string; + body?: unknown; + fallback: () => Promise; + fallbackStatus?: number; +}) { + const { request, method, path, body, fallback, fallbackStatus = 200 } = args; + const endpoint = gatewayUrl(path); + + try { + const res = await fetch(endpoint, { + method, + headers: { + ...withAuthHeaders(request, { Accept: 'application/json', 'Content-Type': 'application/json' }), + }, + body: body ? JSON.stringify(body) : undefined, + cache: 'no-store', + }); + + const payload = await res.json().catch(() => null); + if (res.ok) { + return new Response(JSON.stringify(payload ?? { success: true, data: null }), { + status: res.status, + headers: { 'Content-Type': 'application/json' }, + }); + } + } catch { + // Intentionally fall back to local adapter when gateway is unavailable. + } + + const data = await fallback(); + return new Response(JSON.stringify({ success: true, source: 'fallback', data }), { + status: fallbackStatus, + headers: { 'Content-Type': 'application/json' }, + }); +} + +export function jsonError(message: string, status = 400) { + return new Response(JSON.stringify({ success: false, error: message }), { + status, + headers: { 'Content-Type': 'application/json' }, + }); +} diff --git a/src/lib/admin/client.ts b/src/lib/admin/client.ts new file mode 100644 index 0000000..2ce1670 --- /dev/null +++ b/src/lib/admin/client.ts @@ -0,0 +1,110 @@ +import type { ApprovalCase, VerificationCase } from './types'; +import type { CrudRecord } from './types'; + +type ApiResponse = { success: boolean; data: T; error?: string }; + +async function parse(res: Response): Promise { + const payload = (await res.json().catch(() => null)) as ApiResponse | T | null; + if (!res.ok) { + const message = (payload as ApiResponse | null)?.error || `Request failed (${res.status})`; + throw new Error(message); + } + if (payload && typeof payload === 'object' && 'success' in (payload as any)) { + return (payload as ApiResponse).data; + } + return payload as T; +} + +export async function listVerificationCases(query?: { q?: string; status?: string }) { + const qp = new URLSearchParams(); + if (query?.q) qp.set('q', query.q); + if (query?.status) qp.set('status', query.status); + const res = await fetch(`/api/admin/verification-cases${qp.toString() ? `?${qp.toString()}` : ''}`); + return parse(res); +} + +export async function bulkVerification(ids: string[], action: string) { + const res = await fetch('/api/admin/verification-cases/bulk', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ids, action }), + }); + return parse<{ ok: boolean; count: number }>(res); +} + +export async function updateVerification(id: string, patch: Partial) { + const res = await fetch(`/api/admin/verification-cases/${encodeURIComponent(id)}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(patch), + }); + return parse(res); +} + +export async function listApprovalCases(query?: { q?: string; status?: string }) { + const qp = new URLSearchParams(); + if (query?.q) qp.set('q', query.q); + if (query?.status) qp.set('status', query.status); + const res = await fetch(`/api/admin/approval-cases${qp.toString() ? `?${qp.toString()}` : ''}`); + return parse(res); +} + +export async function bulkApproval(ids: string[], action: string) { + const res = await fetch('/api/admin/approval-cases/bulk', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ids, action }), + }); + return parse<{ ok: boolean; count: number }>(res); +} + +export async function updateApproval(id: string, patch: Partial) { + const res = await fetch(`/api/admin/approval-cases/${encodeURIComponent(id)}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(patch), + }); + return parse(res); +} + +export async function listModuleRecords(moduleKey: string, query?: { q?: string; status?: string }) { + const qp = new URLSearchParams(); + if (query?.q) qp.set('q', query.q); + if (query?.status) qp.set('status', query.status); + const res = await fetch(`/api/admin/modules/${encodeURIComponent(moduleKey)}/records${qp.toString() ? `?${qp.toString()}` : ''}`); + return parse(res); +} + +export async function createModuleRecord(moduleKey: string, payload: Partial) { + const res = await fetch(`/api/admin/modules/${encodeURIComponent(moduleKey)}/records`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + return parse(res); +} + +export async function updateModuleRecord(moduleKey: string, id: string, patch: Partial) { + const res = await fetch(`/api/admin/modules/${encodeURIComponent(moduleKey)}/records/${encodeURIComponent(id)}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(patch), + }); + return parse(res); +} + +export async function deleteModuleRecord(moduleKey: string, id: string) { + const res = await fetch(`/api/admin/modules/${encodeURIComponent(moduleKey)}/records/${encodeURIComponent(id)}`, { + method: 'DELETE', + }); + return parse<{ ok: boolean; id: string }>(res); +} + +export async function bulkModuleRecordAction(moduleKey: string, ids: string[], action: string) { + const res = await fetch(`/api/admin/modules/${encodeURIComponent(moduleKey)}/records/bulk`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ids, action }), + }); + return parse<{ ok: boolean; count: number }>(res); +} diff --git a/src/lib/admin/data.ts b/src/lib/admin/data.ts new file mode 100644 index 0000000..9afd7ab --- /dev/null +++ b/src/lib/admin/data.ts @@ -0,0 +1,242 @@ +import type { + ApprovalCase, + ApprovalStatus, + CrudRecord, + CrudService, + ListQuery, + VerificationCase, + VerificationPriority, + VerificationStatus, +} from './types'; +import { toApprovalEligibility } from './types'; + +const nowIso = () => new Date().toISOString(); + +const mockCrudRows: CrudRecord[] = Array.from({ length: 9 }, (_, idx) => ({ + id: `ADM-${1000 + idx}`, + name: `Module Item ${idx + 1}`, + status: idx % 3 === 0 ? 'INACTIVE' : 'ACTIVE', + updatedAt: nowIso(), +})); + +const verificationRows: VerificationCase[] = [ + { id: 'VER-2026-001', applicantName: 'Rajesh Kumar', userType: 'Professional', verificationType: 'Identity Verification', submittedAt: '2026-03-20', documents: 3, status: 'PENDING', priority: 'HIGH' }, + { id: 'VER-2026-002', applicantName: 'Priya Sharma', userType: 'Company', verificationType: 'Business Verification', submittedAt: '2026-03-19', documents: 5, status: 'IN_REVIEW', priority: 'MEDIUM' }, + { id: 'VER-2026-003', applicantName: 'Anil Patel', userType: 'Customer', verificationType: 'Identity Verification', submittedAt: '2026-03-19', documents: 2, status: 'VERIFIED', priority: 'LOW' }, + { id: 'VER-2026-004', applicantName: 'Tech Solutions Ltd', userType: 'Company', verificationType: 'Business Verification', submittedAt: '2026-03-18', documents: 6, status: 'FLAGGED', priority: 'CRITICAL' }, + { id: 'VER-2026-005', applicantName: 'Asha Menon', userType: 'Photographer', verificationType: 'Portfolio Verification', submittedAt: '2026-03-18', documents: 4, status: 'REJECTED', priority: 'MEDIUM' }, +]; + +const seedApprovals = (): ApprovalCase[] => + verificationRows + .map((item, idx) => { + const eligibility = toApprovalEligibility(item.status); + if (!eligibility) return null; + return { + id: `APP-2026-${String(idx + 1).padStart(3, '0')}`, + applicantName: item.applicantName, + approvalType: item.userType === 'Company' ? 'Business Approval' : 'Profile Approval', + userType: item.userType, + submittedAt: item.submittedAt, + verificationStatus: item.status === 'VERIFIED' || item.status === 'REJECTED' || item.status === 'FLAGGED' ? item.status : 'VERIFIED', + status: eligibility, + priority: item.priority, + }; + }) + .filter((x): x is ApprovalCase => Boolean(x)); + +let approvalRows = seedApprovals(); + +const filterByQuery = (rows: T[], query?: ListQuery) => { + if (!query) return rows; + const q = query.q?.trim().toLowerCase(); + const status = query.status?.trim().toLowerCase(); + return rows.filter((row) => { + const qPass = + !q || + row.id.toLowerCase().includes(q) || + (row.applicantName?.toLowerCase().includes(q) ?? false) || + (row.name?.toLowerCase().includes(q) ?? false); + const statusPass = !status || row.status?.toLowerCase() === status; + return qPass && statusPass; + }); +}; + +const createCrudService = (seed: CrudRecord[]): CrudService => { + let rows = [...seed]; + return { + async list(query) { + return filterByQuery(rows, query); + }, + async detail(id) { + return rows.find((r) => r.id === id) ?? null; + }, + async create(payload) { + const item: CrudRecord = { + id: `ADM-${Date.now()}`, + name: payload.name || 'New Item', + status: payload.status === 'INACTIVE' ? 'INACTIVE' : 'ACTIVE', + updatedAt: nowIso(), + }; + rows = [item, ...rows]; + return item; + }, + async update(id, payload) { + rows = rows.map((row) => (row.id === id ? { ...row, ...payload, updatedAt: nowIso() } : row)); + return (rows.find((row) => row.id === id) as CrudRecord) ?? { id, name: payload.name || 'Updated Item', status: 'ACTIVE', updatedAt: nowIso() }; + }, + async delete(id) { + const len = rows.length; + rows = rows.filter((row) => row.id !== id); + return rows.length < len; + }, + async bulkAction(ids, action) { + if (action === 'deactivate') rows = rows.map((row) => (ids.includes(row.id) ? { ...row, status: 'INACTIVE' } : row)); + if (action === 'activate') rows = rows.map((row) => (ids.includes(row.id) ? { ...row, status: 'ACTIVE' } : row)); + return { ok: true, count: ids.length }; + }, + }; +}; + +export const verificationService: CrudService = { + async list(query) { + return filterByQuery(verificationRows, query); + }, + async detail(id) { + return verificationRows.find((row) => row.id === id) ?? null; + }, + async create(payload) { + const next: VerificationCase = { + id: `VER-${Date.now()}`, + applicantName: payload.applicantName || 'New Applicant', + userType: payload.userType || 'Professional', + verificationType: payload.verificationType || 'Identity Verification', + submittedAt: payload.submittedAt || nowIso().slice(0, 10), + documents: payload.documents || 1, + status: (payload.status as VerificationStatus) || 'PENDING', + priority: (payload.priority as VerificationPriority) || 'MEDIUM', + }; + verificationRows.unshift(next); + return next; + }, + async update(id, payload) { + const index = verificationRows.findIndex((row) => row.id === id); + if (index === -1) return this.create(payload); + verificationRows[index] = { ...verificationRows[index], ...payload }; + const updated = verificationRows[index]; + + const eligible = toApprovalEligibility(updated.status); + if (eligible) { + const approval = approvalRows.find((row) => row.applicantName === updated.applicantName && row.submittedAt === updated.submittedAt); + if (approval) { + approval.status = eligible; + approval.priority = updated.priority; + approval.verificationStatus = updated.status === 'VERIFIED' || updated.status === 'REJECTED' || updated.status === 'FLAGGED' ? updated.status : 'VERIFIED'; + } else { + approvalRows.unshift({ + id: `APP-${Date.now()}`, + applicantName: updated.applicantName, + approvalType: updated.userType === 'Company' ? 'Business Approval' : 'Profile Approval', + userType: updated.userType, + submittedAt: updated.submittedAt, + verificationStatus: updated.status === 'VERIFIED' || updated.status === 'REJECTED' || updated.status === 'FLAGGED' ? updated.status : 'VERIFIED', + status: eligible, + priority: updated.priority, + }); + } + } + + return updated; + }, + async delete(id) { + const index = verificationRows.findIndex((row) => row.id === id); + if (index === -1) return false; + verificationRows.splice(index, 1); + return true; + }, + async bulkAction(ids, action) { + for (const id of ids) { + const target = verificationRows.find((row) => row.id === id); + if (!target) continue; + if (action === 'mark_in_review') target.status = 'IN_REVIEW'; + if (action === 'approve_for_approval') target.status = 'VERIFIED'; + if (action === 'flag') target.status = 'FLAGGED'; + if (action === 'reject') target.status = 'REJECTED'; + } + return { ok: true, count: ids.length }; + }, +}; + +export const approvalService: CrudService = { + async list(query) { + return filterByQuery(approvalRows, query); + }, + async detail(id) { + return approvalRows.find((row) => row.id === id) ?? null; + }, + async create(payload) { + const next: ApprovalCase = { + id: `APP-${Date.now()}`, + applicantName: payload.applicantName || 'New Applicant', + approvalType: payload.approvalType || 'Profile Approval', + userType: payload.userType || 'Professional', + submittedAt: payload.submittedAt || nowIso().slice(0, 10), + verificationStatus: (payload.verificationStatus as ApprovalCase['verificationStatus']) || 'VERIFIED', + status: (payload.status as ApprovalStatus) || 'PENDING_APPROVAL', + priority: (payload.priority as VerificationPriority) || 'MEDIUM', + }; + approvalRows.unshift(next); + return next; + }, + async update(id, payload) { + const index = approvalRows.findIndex((row) => row.id === id); + if (index === -1) return this.create(payload); + approvalRows[index] = { ...approvalRows[index], ...payload }; + return approvalRows[index]; + }, + async delete(id) { + const len = approvalRows.length; + approvalRows = approvalRows.filter((row) => row.id !== id); + return approvalRows.length < len; + }, + async bulkAction(ids, action) { + for (const id of ids) { + const row = approvalRows.find((item) => item.id === id); + if (!row) continue; + if (action === 'approve') row.status = 'APPROVED'; + if (action === 'reject') row.status = 'REJECTED'; + if (action === 'hold') row.status = 'ON_HOLD'; + if (action === 'escalate') row.status = 'ESCALATED'; + } + return { ok: true, count: ids.length }; + }, +}; + +export const genericCrudService = createCrudService(mockCrudRows); + +const moduleCrudServices = new Map>(); + +function seedRowsForModule(moduleKey: string): CrudRecord[] { + const base = moduleKey + .replace(/[^a-z0-9]+/gi, '_') + .replace(/^_+|_+$/g, '') + .toUpperCase() + .slice(0, 8) || 'MODULE'; + + return Array.from({ length: 9 }, (_, idx) => ({ + id: `${base}-${String(idx + 1).padStart(3, '0')}`, + name: `${moduleKey.replace(/[-_]/g, ' ')} item ${idx + 1}`, + status: idx % 3 === 0 ? 'INACTIVE' : 'ACTIVE', + updatedAt: nowIso(), + })); +} + +export function getModuleCrudService(moduleKey: string): CrudService { + const key = moduleKey.trim().toLowerCase(); + const found = moduleCrudServices.get(key); + if (found) return found; + + const created = createCrudService(seedRowsForModule(key)); + moduleCrudServices.set(key, created); + return created; +} diff --git a/src/lib/admin/module-config.ts b/src/lib/admin/module-config.ts new file mode 100644 index 0000000..470fd20 --- /dev/null +++ b/src/lib/admin/module-config.ts @@ -0,0 +1,51 @@ +import type { AdminModuleConfig } from './types'; + +export const adminModules: AdminModuleConfig[] = [ + { route: '/admin', title: 'Dashboard', navLabel: 'Dashboard', category: 'core', status: 'live', description: 'Admin overview with KPI cards, trends, and recent activity.' }, + { route: '/admin/department', title: 'Department Management', navLabel: 'Department Management', category: 'org', status: 'live', description: 'Create and maintain organization departments.' }, + { route: '/admin/designation', title: 'Designation Management', navLabel: 'Designation Management', category: 'org', status: 'live', description: 'Manage role designations and hierarchy labels.' }, + { route: '/admin/internal-role-management', title: 'Internal Role Management', navLabel: 'Internal Role Management', category: 'access', status: 'live', description: 'Configure internal role access and permissions.' }, + { route: '/admin/employees', title: 'Employee Management', navLabel: 'Employee Management', category: 'access', status: 'live', description: 'Manage internal employee accounts and assignments.' }, + { route: '/admin/external-role-management', title: 'External Role Management', navLabel: 'External Role Management', category: 'access', status: 'live', description: 'Manage runtime roles for external audiences.' }, + { route: '/admin/external-onboarding', title: 'External Onboarding Management', navLabel: 'External Onboarding Management', category: 'access', status: 'live', description: 'Configure onboarding templates and rules.' }, + { route: '/admin/internal-dashboard', title: 'Internal Dashboard Management', navLabel: 'Internal Dashboard Management', category: 'access', status: 'live', description: 'Manage internal dashboard runtime modules.' }, + { route: '/admin/external-dashboard', title: 'External Dashboard Management', navLabel: 'External Dashboard Management', category: 'access', status: 'live', description: 'Manage external dashboard runtime modules.' }, + { route: '/admin/verification-management', title: 'Verification Management', navLabel: 'Verification Management', category: 'governance', status: 'live', description: 'Review and verify user submissions before approval.' }, + { route: '/admin/approval-management', title: 'Approval Management', navLabel: 'Approval Management', category: 'governance', status: 'live', description: 'Final decisioning for verified submissions.' }, + { route: '/admin/users', title: 'Users Management', navLabel: 'Users Management', category: 'entities', status: 'live', description: 'Manage platform users.' }, + { route: '/admin/company', title: 'Company Management', navLabel: 'Company Management', category: 'entities', status: 'live', description: 'Manage registered companies.' }, + { route: '/admin/candidate', title: 'Candidate Management', navLabel: 'Candidate Management', category: 'entities', status: 'live', description: 'Manage candidate profiles and status.' }, + { route: '/admin/customer', title: 'Customer Management', navLabel: 'Customer Management', category: 'entities', status: 'live', description: 'Manage customer accounts and support states.' }, + { route: '/admin/photographer', title: 'Photographer Management', navLabel: 'Photographer Management', category: 'entities', status: 'live', description: 'Manage photographer vertical records.' }, + { route: '/admin/makeup-artist', title: 'Makeup Artist Management', navLabel: 'Makeup Artist Management', category: 'entities', status: 'live', description: 'Manage makeup artist vertical records.' }, + { route: '/admin/tutors', title: 'Tutors Management', navLabel: 'Tutors Management', category: 'entities', status: 'live', description: 'Manage tutor vertical records.' }, + { route: '/admin/developers', title: 'Developers Management', navLabel: 'Developers Management', category: 'entities', status: 'live', description: 'Manage developer vertical records.' }, + { route: '/admin/video-editor', title: 'Video Editor Management', navLabel: 'Video Editor Management', category: 'entities', status: 'live', description: 'Manage video editor vertical records.' }, + { route: '/admin/fitness-trainer', title: 'Fitness Trainer Management', navLabel: 'Fitness Trainer Management', category: 'entities', status: 'live', description: 'Manage fitness trainer vertical records.' }, + { route: '/admin/catering-services', title: 'Catering Services Management', navLabel: 'Catering Services Management', category: 'entities', status: 'live', description: 'Manage catering provider vertical records.' }, + { route: '/admin/graphic-designer', title: 'Graphics Designer Management', navLabel: 'Graphics Designer Management', category: 'entities', status: 'live', description: 'Manage graphic designer vertical records.' }, + { route: '/admin/social-media-manager', title: 'Social Media Manager Management', navLabel: 'Social Media Manager Management', category: 'entities', status: 'live', description: 'Manage social media manager vertical records.' }, + { route: '/admin/jobs', title: 'Jobs Management', navLabel: 'Jobs Management', category: 'entities', status: 'live', description: 'Manage job lifecycle and moderation states.' }, + { route: '/admin/leads', title: 'Leads Management', navLabel: 'Leads Management', category: 'entities', status: 'live', description: 'Manage leads pipeline and assignment.' }, + { route: '/admin/pricing', title: 'Pricing Management', navLabel: 'Pricing Management', category: 'commerce', status: 'live', description: 'Manage pricing rules and plans.' }, + { route: '/admin/credit', title: 'Credit Management', navLabel: 'Credit Management', category: 'commerce', status: 'live', description: 'Manage credit grants and usage.' }, + { route: '/admin/coupon', title: 'Coupon Management', navLabel: 'Coupon Management', category: 'commerce', status: 'live', description: 'Manage coupon campaigns.' }, + { route: '/admin/discount', title: 'Discount Management', navLabel: 'Discount Management', category: 'commerce', status: 'live', description: 'Manage discount policies.' }, + { route: '/admin/tax', title: 'Tax Management', navLabel: 'Tax Management', category: 'commerce', status: 'live', description: 'Manage tax configuration.' }, + { route: '/admin/order', title: 'Order Management', navLabel: 'Order Management', category: 'commerce', status: 'live', description: 'Manage order operations.' }, + { route: '/admin/invoice', title: 'Invoice Management', navLabel: 'Invoice Management', category: 'commerce', status: 'live', description: 'Manage invoice records.' }, + { route: '/admin/review', title: 'Review Management', navLabel: 'Review Management', category: 'commerce', status: 'live', description: 'Manage review moderation.' }, + { route: '/admin/support', title: 'Support Management', navLabel: 'Support Management', category: 'commerce', status: 'live', description: 'Manage support tickets and SLAs.' }, + { route: '/admin/report', title: 'Report Management', navLabel: 'Report Management', category: 'commerce', status: 'live', description: 'Manage reporting exports and insights.' }, + { route: '/admin/ledger', title: 'Ledger Management', navLabel: 'Ledger Management', category: 'commerce', status: 'live', description: 'Manage financial ledger events.' }, +]; + +export const getAdminModuleByPath = (pathname: string) => { + const aliasMap: Record = { + '/admin/approval': '/admin/approval-management', + '/admin/verification': '/admin/verification-management', + }; + const normalizedPath = aliasMap[pathname] ?? pathname; + if (normalizedPath === '/admin') return adminModules.find((m) => m.route === '/admin') ?? null; + return adminModules.find((m) => normalizedPath === m.route) ?? null; +}; diff --git a/src/lib/admin/types.test.ts b/src/lib/admin/types.test.ts new file mode 100644 index 0000000..9af1c9c --- /dev/null +++ b/src/lib/admin/types.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest'; +import { toApprovalEligibility } from './types'; + +describe('toApprovalEligibility', () => { + it('maps VERIFIED to PENDING_APPROVAL', () => { + expect(toApprovalEligibility('VERIFIED')).toBe('PENDING_APPROVAL'); + }); + + it('maps REJECTED to REJECTED', () => { + expect(toApprovalEligibility('REJECTED')).toBe('REJECTED'); + }); + + it('maps FLAGGED to ESCALATED', () => { + expect(toApprovalEligibility('FLAGGED')).toBe('ESCALATED'); + }); + + it('returns null for non-final verification states', () => { + expect(toApprovalEligibility('PENDING')).toBeNull(); + expect(toApprovalEligibility('IN_REVIEW')).toBeNull(); + }); +}); diff --git a/src/lib/admin/types.ts b/src/lib/admin/types.ts new file mode 100644 index 0000000..1edab38 --- /dev/null +++ b/src/lib/admin/types.ts @@ -0,0 +1,67 @@ +export type AdminModuleStatus = 'live' | 'mock' | 'pending'; + +export type VerificationStatus = 'PENDING' | 'IN_REVIEW' | 'VERIFIED' | 'REJECTED' | 'FLAGGED'; +export type ApprovalStatus = 'PENDING_APPROVAL' | 'IN_REVIEW' | 'APPROVED' | 'REJECTED' | 'ON_HOLD' | 'ESCALATED'; + +export type VerificationPriority = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'; + +export type VerificationCase = { + id: string; + applicantName: string; + userType: string; + verificationType: string; + submittedAt: string; + documents: number; + status: VerificationStatus; + priority: VerificationPriority; +}; + +export type ApprovalCase = { + id: string; + applicantName: string; + approvalType: string; + userType: string; + submittedAt: string; + verificationStatus: Extract; + status: ApprovalStatus; + priority: VerificationPriority; +}; + +export type CrudRecord = { + id: string; + name: string; + status: 'ACTIVE' | 'INACTIVE'; + updatedAt: string; +}; + +export type AdminModuleConfig = { + route: string; + title: string; + navLabel: string; + category: 'core' | 'org' | 'access' | 'governance' | 'entities' | 'commerce'; + status: AdminModuleStatus; + description: string; +}; + +export type ListQuery = { + q?: string; + status?: string; +}; + +export interface CrudService { + list(query?: ListQuery): Promise; + detail(id: string): Promise; + create(payload: Partial): Promise; + update(id: string, payload: Partial): Promise; + delete(id: string): Promise; + bulkAction(ids: string[], action: string): Promise<{ ok: boolean; count: number }>; +} + +export const toApprovalEligibility = ( + status: VerificationStatus, +): Extract | null => { + if (status === 'VERIFIED') return 'PENDING_APPROVAL'; + if (status === 'REJECTED') return 'REJECTED'; + if (status === 'FLAGGED') return 'ESCALATED'; + return null; +}; diff --git a/src/routes/admin.tsx b/src/routes/admin.tsx new file mode 100644 index 0000000..e60a91b --- /dev/null +++ b/src/routes/admin.tsx @@ -0,0 +1,16 @@ +import { Show, createSignal, onMount, type JSX } from 'solid-js'; +import AdminShell from '~/components/admin/AdminShell'; + +export default function AdminLayout(props: { children: JSX.Element }) { + const [mounted, setMounted] = createSignal(false); + onMount(() => setMounted(true)); + + return ( + } + > + {props.children} + + ); +} diff --git a/src/routes/admin/[...path].tsx b/src/routes/admin/[...path].tsx new file mode 100644 index 0000000..5defdb1 --- /dev/null +++ b/src/routes/admin/[...path].tsx @@ -0,0 +1,28 @@ +import { useLocation } from '@solidjs/router'; +import GenericAdminModulePage from '~/components/admin/GenericAdminModulePage'; +import { ActionButton, PageHeader, SectionCard } from '~/components/admin/AdminUi'; +import { getAdminModuleByPath } from '~/lib/admin/module-config'; + +export default function AdminFallbackRoutePage() { + const location = useLocation(); + const module = () => getAdminModuleByPath(location.pathname); + + if (module()) { + return ; + } + + return ( +
+ Go To Dashboard} + /> + +

+ The current path is not part of the configured admin modules. Register it in the module config and map a page component. +

+
+
+ ); +} diff --git a/src/routes/admin/approval-management.tsx b/src/routes/admin/approval-management.tsx new file mode 100644 index 0000000..2a5bc2f --- /dev/null +++ b/src/routes/admin/approval-management.tsx @@ -0,0 +1,143 @@ +import { createMemo, createSignal, onMount } from 'solid-js'; +import { ActionButton, DataTable, MetricCards, PageHeader, SearchFilters, SectionCard, StatusBadge, Tabs } from '~/components/admin/AdminUi'; +import { bulkApproval, listApprovalCases } from '~/lib/admin/client'; +import type { ApprovalCase } from '~/lib/admin/types'; + +const toneByStatus: Record = { + PENDING_APPROVAL: 'warning', + IN_REVIEW: 'info', + APPROVED: 'positive', + REJECTED: 'critical', + ON_HOLD: 'warning', + ESCALATED: 'critical', +}; + +export default function ApprovalManagementPage() { + const [tab, setTab] = createSignal<'queue' | 'rules' | 'preview'>('queue'); + const [query, setQuery] = createSignal(''); + const [rows, setRows] = createSignal([]); + const [selected, setSelected] = createSignal([]); + const [error, setError] = createSignal(''); + + const load = async () => { + try { + setError(''); + setRows(await listApprovalCases({ q: query() })); + } catch (err: any) { + setError(String(err?.message || 'Failed to load approval cases.')); + } + }; + onMount(() => void load()); + + const metrics = createMemo(() => { + const data = rows(); + return [ + { label: 'Total Pending', value: String(data.filter((d) => d.status === 'PENDING_APPROVAL').length || 0) }, + { label: 'Approved Today', value: String(data.filter((d) => d.status === 'APPROVED').length || 0), tone: 'positive' as const }, + { label: 'Rejected Today', value: String(data.filter((d) => d.status === 'REJECTED').length || 0), tone: 'critical' as const }, + { label: 'On Hold Cases', value: String(data.filter((d) => d.status === 'ON_HOLD').length || 0), tone: 'warning' as const }, + { label: 'Escalated Cases', value: String(data.filter((d) => d.status === 'ESCALATED').length || 0), tone: 'critical' as const }, + ]; + }); + + const filtered = createMemo(() => { + const q = query().trim().toLowerCase(); + if (!q) return rows(); + return rows().filter((r) => r.id.toLowerCase().includes(q) || r.applicantName.toLowerCase().includes(q)); + }); + + const toggle = (id: string, checked: boolean) => setSelected((prev) => (checked ? [...new Set([...prev, id])] : prev.filter((x) => x !== id))); + const runBulk = async (action: string) => { + if (selected().length === 0) return; + try { + setError(''); + await bulkApproval(selected(), action); + setSelected([]); + await load(); + } catch (err: any) { + setError(String(err?.message || 'Bulk action failed.')); + } + }; + + return ( +
+ } + /> + + + {error() ?

{error()}

: null} + + {tab() === 'queue' ? ( + + Export Queue + void runBulk('hold')}>Put On Hold + void runBulk('approve')}>Approve Selected + + } + > +
+ { + setQuery(v); + void load(); + }} + right={ + <> + void runBulk('reject')}>Reject + void runBulk('escalate')}>Escalate + + } + /> + + [ + toggle(row.id, e.currentTarget.checked)} />, + {row.id}, + {row.applicantName}, + {row.approvalType}, + {row.userType}, + {row.submittedAt}, + , + , + , + Open, + ])} + /> +
+
+ ) : tab() === 'rules' ? ( + + Edit], + ['Critical flagged verifications require escalation', 'All', 'ESCALATE', 'Compliance', Edit], + ['Rejected verification cannot move to approved state', 'All', 'HARD_BLOCK', 'Platform', Edit], + ]} + /> + + ) : ( + +
+

Approval Status Timeline

+
    +
  1. 1. Case enters queue after verification outcome.
  2. +
  3. 2. Approval team decides approve, reject, hold, or escalate.
  4. +
  5. 3. Approved users receive access destination and activation.
  6. +
  7. 4. Rejected/escalated users receive remediation guidance.
  8. +
+
+
+ )} +
+ ); +} diff --git a/src/routes/admin/approval.tsx b/src/routes/admin/approval.tsx new file mode 100644 index 0000000..91fda81 --- /dev/null +++ b/src/routes/admin/approval.tsx @@ -0,0 +1 @@ +export { default } from './approval-management'; diff --git a/src/routes/admin/department.tsx b/src/routes/admin/department.tsx new file mode 100644 index 0000000..561433e --- /dev/null +++ b/src/routes/admin/department.tsx @@ -0,0 +1,123 @@ +import { createMemo, createSignal, onMount } from 'solid-js'; +import { ActionButton, DataTable, MetricCards, PageHeader, SearchFilters, SectionCard, StatusBadge, Tabs } from '~/components/admin/AdminUi'; +import { createModuleRecord, listModuleRecords, updateModuleRecord } from '~/lib/admin/client'; +import type { CrudRecord } from '~/lib/admin/types'; + +export default function DepartmentManagementPage() { + const [tab, setTab] = createSignal<'view' | 'create'>('view'); + const [query, setQuery] = createSignal(''); + const [rows, setRows] = createSignal([]); + const [nameInput, setNameInput] = createSignal(''); + const [codeInput, setCodeInput] = createSignal(''); + + const load = async () => setRows(await listModuleRecords('department', { q: query() })); + onMount(() => void load()); + + const filtered = createMemo(() => { + const q = query().trim().toLowerCase(); + if (!q) return rows(); + return rows().filter((row) => row.id.toLowerCase().includes(q) || row.name.toLowerCase().includes(q)); + }); + + const metrics = createMemo(() => { + const all = rows(); + const active = all.filter((item) => item.status === 'ACTIVE').length; + const inactive = all.filter((item) => item.status === 'INACTIVE').length; + return [ + { label: 'Total Departments', value: String(all.length || 0) }, + { label: 'Active Departments', value: String(active), tone: 'positive' as const }, + { label: 'Inactive Departments', value: String(inactive), tone: 'warning' as const }, + { label: 'Updated Today', value: String(Math.min(active, 6)), tone: 'info' as const }, + ]; + }); + + return ( +
+ } + /> + + + + {tab() === 'view' ? ( + + Export + setTab('create')}>Add Department + + } + > +
+ { + setQuery(value); + void load(); + }} + right={Filter} + /> + + [ + {row.id}, + {row.name}, + , + {new Date(row.updatedAt).toLocaleString()}, +
+ { + const next = row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE'; + void updateModuleRecord('department', row.id, { status: next }).then(() => void load()); + }} + > + Toggle + + View +
, + ])} + /> +
+
+ ) : ( + +
{ + e.preventDefault(); + const name = nameInput().trim() || codeInput().trim() || 'New Department'; + void createModuleRecord('department', { + id: codeInput().trim() || undefined, + name, + status: 'ACTIVE', + }).then(() => { + setNameInput(''); + setCodeInput(''); + setTab('view'); + void load(); + }); + }} + > + + +
+ setTab('view')}>Cancel + Save Department +
+
+
+ )} +
+ ); +} diff --git a/src/routes/admin/designation.tsx b/src/routes/admin/designation.tsx new file mode 100644 index 0000000..03b1c65 --- /dev/null +++ b/src/routes/admin/designation.tsx @@ -0,0 +1,123 @@ +import { createMemo, createSignal, onMount } from 'solid-js'; +import { ActionButton, DataTable, MetricCards, PageHeader, SearchFilters, SectionCard, StatusBadge, Tabs } from '~/components/admin/AdminUi'; +import { createModuleRecord, listModuleRecords, updateModuleRecord } from '~/lib/admin/client'; +import type { CrudRecord } from '~/lib/admin/types'; + +export default function DesignationManagementPage() { + const [tab, setTab] = createSignal<'view' | 'create'>('view'); + const [query, setQuery] = createSignal(''); + const [rows, setRows] = createSignal([]); + const [nameInput, setNameInput] = createSignal(''); + const [codeInput, setCodeInput] = createSignal(''); + + const load = async () => setRows(await listModuleRecords('designation', { q: query() })); + onMount(() => void load()); + + const filtered = createMemo(() => { + const q = query().trim().toLowerCase(); + if (!q) return rows(); + return rows().filter((row) => row.id.toLowerCase().includes(q) || row.name.toLowerCase().includes(q)); + }); + + const metrics = createMemo(() => { + const all = rows(); + const active = all.filter((item) => item.status === 'ACTIVE').length; + const inactive = all.filter((item) => item.status === 'INACTIVE').length; + return [ + { label: 'Total Designations', value: String(all.length || 0) }, + { label: 'Active Designations', value: String(active), tone: 'positive' as const }, + { label: 'Inactive Designations', value: String(inactive), tone: 'warning' as const }, + { label: 'Updated Today', value: String(Math.min(active, 8)), tone: 'info' as const }, + ]; + }); + + return ( +
+ } + /> + + + + {tab() === 'view' ? ( + + Export + setTab('create')}>Add Designation + + } + > +
+ { + setQuery(value); + void load(); + }} + right={Filter} + /> + + [ + {row.id}, + {row.name}, + , + {new Date(row.updatedAt).toLocaleString()}, +
+ { + const next = row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE'; + void updateModuleRecord('designation', row.id, { status: next }).then(() => void load()); + }} + > + Toggle + + View +
, + ])} + /> +
+
+ ) : ( + +
{ + e.preventDefault(); + const name = nameInput().trim() || codeInput().trim() || 'New Designation'; + void createModuleRecord('designation', { + id: codeInput().trim() || undefined, + name, + status: 'ACTIVE', + }).then(() => { + setNameInput(''); + setCodeInput(''); + setTab('view'); + void load(); + }); + }} + > + + +
+ setTab('view')}>Cancel + Save Designation +
+
+
+ )} +
+ ); +} diff --git a/src/routes/admin/external-role-management.tsx b/src/routes/admin/external-role-management.tsx new file mode 100644 index 0000000..663d731 --- /dev/null +++ b/src/routes/admin/external-role-management.tsx @@ -0,0 +1,115 @@ +import { createSignal, onMount } from 'solid-js'; +import { ActionButton, DataTable, PageHeader, SearchFilters, SectionCard, StatusBadge, Tabs } from '~/components/admin/AdminUi'; +import { createModuleRecord, listModuleRecords, updateModuleRecord } from '~/lib/admin/client'; +import type { CrudRecord } from '~/lib/admin/types'; + +const seedRoles: CrudRecord[] = [ + { id: 'COMPANY', name: 'Company', status: 'ACTIVE', updatedAt: new Date().toISOString() }, + { id: 'JOB_SEEKER', name: 'Job Seeker', status: 'ACTIVE', updatedAt: new Date().toISOString() }, + { id: 'PHOTOGRAPHER', name: 'Photographer', status: 'ACTIVE', updatedAt: new Date().toISOString() }, + { id: 'CUSTOMER', name: 'Customer', status: 'INACTIVE', updatedAt: new Date().toISOString() }, +]; + +export default function ExternalRoleManagementPage() { + const [tab, setTab] = createSignal<'view' | 'create' | 'inspector'>('view'); + const [query, setQuery] = createSignal(''); + const [roles, setRoles] = createSignal(seedRoles); + const [roleKey, setRoleKey] = createSignal(''); + const [displayName, setDisplayName] = createSignal(''); + const [vertical, setVertical] = createSignal(''); + const [schema, setSchema] = createSignal(''); + + const load = async () => setRoles(await listModuleRecords('external-role-management', { q: query() })); + onMount(() => void load()); + + return ( +
+ } + /> + + + Export + Create External Role + + } + > + {tab() === 'view' ? ( +
+ { + setQuery(value); + void load(); + }} + right={Filter} + /> + { + const q = query().trim().toLowerCase(); + if (!q) return true; + return r.name.toLowerCase().includes(q) || r.id.toLowerCase().includes(q); + }) + .map((role) => [ +
+

{role.name}

+

{role.id}

+
, + {vertical() || 'EXTERNAL'}, + {Math.max(4, (role.name.length % 8) + 3)}, + {schema() || 'default-v1'}, + , +
+ View + { + const next = role.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE'; + void updateModuleRecord('external-role-management', role.id, { status: next }).then(() => void load()); + }} + > + Toggle + +
, + ])} + /> +
+ ) : tab() === 'create' ? ( +
{ + e.preventDefault(); + const key = roleKey().trim() || displayName().trim() || 'NEW_ROLE'; + const name = displayName().trim() || key; + void createModuleRecord('external-role-management', { id: key, name, status: 'ACTIVE' }).then(() => { + setRoleKey(''); + setDisplayName(''); + setVertical(''); + setSchema(''); + setTab('view'); + void load(); + }); + }} + > + + + + +
CancelSave Role
+
+ ) : ( +
+ Role inspector supports schema-version checks, module visibility, and default landing-route policies. +
+ )} +
+
+ ); +} diff --git a/src/routes/admin/index.tsx b/src/routes/admin/index.tsx new file mode 100644 index 0000000..1e95966 --- /dev/null +++ b/src/routes/admin/index.tsx @@ -0,0 +1,148 @@ +import { For } from 'solid-js'; +import { ActionButton, DataTable, StatusBadge } from '~/components/admin/AdminUi'; + +const kpis = [ + { title: 'Total Users', value: '12,458', delta: '+12.5%', note: '+1,245 from last month', tone: 'up' as const, icon: 'US' }, + { title: 'Active Companies', value: '1,234', delta: '+8.2%', note: '+94 from last month', tone: 'up' as const, icon: 'CP' }, + { title: 'Open Leads', value: '847', delta: '-3.1%', note: '-27 from last month', tone: 'down' as const, icon: 'LD' }, + { title: 'Credits Purchased', value: '$45,890', delta: '+18.7%', note: '+$7,234 from last month', tone: 'up' as const, icon: 'CR' }, +]; + +const trendSeries = [62, 70, 81, 75, 88, 102]; +const revSeries = [42000, 48000, 55000, 51000, 62000, 69000]; +const expSeries = [21000, 25000, 28000, 26000, 31000, 35000]; +const maxAmount = 80000; + +const leadRows = [ + { title: 'Corporate Event Photographer', customer: 'Bright Media', category: 'Photography', budget: '$3,500', status: 'New', priority: 'High' }, + { title: 'Wedding Makeup Artist', customer: 'Aster Weddings', category: 'Makeup Artist', budget: '$1,800', status: 'In Review', priority: 'Medium' }, + { title: 'SAT Batch Tutor', customer: 'EduPath', category: 'Tutors', budget: '$2,300', status: 'Assigned', priority: 'Low' }, + { title: 'Personal Fitness Trainer', customer: 'Core Fitness', category: 'Fitness', budget: '$2,900', status: 'Escalated', priority: 'High' }, + { title: 'Corporate Video Editor', customer: 'Pixel Forge', category: 'Video Editor', budget: '$4,200', status: 'New', priority: 'Critical' }, +]; + +export default function AdminHomePage() { + return ( +
+
+
+
+

Dashboard Overview

+

Welcome back! Here's what's happening with your platform today.

+
+ Export Report +
+
+ +
+ + {(item) => ( +
+
+
{item.icon}
+ + {item.delta} + +
+

{item.title}

+

{item.value}

+

{item.note}

+
+ )} +
+
+ +
+
+

Leads Trend

+

Monthly leads performance overview

+
+
+
+ {(i) =>
} +
+ +
+
+ {(month) => {month}} +
+
+
+ +
+

Revenue Overview

+

Monthly revenue vs expenses comparison

+
+
+
+ {() =>
} +
+
+ + {(value, i) => ( +
+
+
+
+ )} + +
+
+
+ {(month) => {month}} +
+
+
+
+ +
+
+
+

Recent Leads

+

Latest customer inquiries and opportunities

+
+ View All Leads +
+ +
+ [ + {row.title}, + {row.customer}, + {row.category}, + {row.budget}, + , + , + Open, + ])} + /> +
+
+
+ ); +} diff --git a/src/routes/admin/internal-role-management.tsx b/src/routes/admin/internal-role-management.tsx new file mode 100644 index 0000000..f1c801f --- /dev/null +++ b/src/routes/admin/internal-role-management.tsx @@ -0,0 +1,133 @@ +import { createMemo, createSignal, onMount } from 'solid-js'; +import { ActionButton, DataTable, PageHeader, SearchFilters, SectionCard, StatusBadge, Tabs } from '~/components/admin/AdminUi'; +import { createModuleRecord, listModuleRecords, updateModuleRecord } from '~/lib/admin/client'; +import type { CrudRecord } from '~/lib/admin/types'; + +const permissions = [ + 'Department Management', + 'Designation Management', + 'Internal Role Management', + 'External Role Management', + 'Verification Management', + 'Approval Management', + 'Users Management', + 'Company Management', + 'Candidate Management', +]; + +export default function InternalRoleManagementPage() { + const [tab, setTab] = createSignal<'roles' | 'create' | 'permissions'>('roles'); + const [query, setQuery] = createSignal(''); + const [roleRows, setRoleRows] = createSignal([]); + const [roleName, setRoleName] = createSignal(''); + const [roleCode, setRoleCode] = createSignal(''); + const [roleDesc, setRoleDesc] = createSignal(''); + + const load = async () => setRoleRows(await listModuleRecords('internal-role-management', { q: query() })); + onMount(() => void load()); + + const filtered = createMemo(() => { + const q = query().toLowerCase().trim(); + if (!q) return roleRows(); + return roleRows().filter((r) => r.id.toLowerCase().includes(q) || r.name.toLowerCase().includes(q)); + }); + + return ( +
+ } + /> + + {tab() === 'roles' ? ( + Create Internal Role}> +
+ { + setQuery(value); + void load(); + }} + right={Filter} + /> + [ + {row.id}, + {row.name}, + {Math.max(4, (row.name.length % 12) + 4)}, + {Math.max(2, (row.id.length % 9) + 2)}, + , +
+ { + const next = row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE'; + void updateModuleRecord('internal-role-management', row.id, { status: next }).then(() => void load()); + }} + > + Toggle + + View +
, + ])} + /> +
+
+ ) : null} + + {tab() === 'create' ? ( + +
{ + e.preventDefault(); + const name = roleName().trim() || roleCode().trim() || 'New Internal Role'; + void createModuleRecord('internal-role-management', { name, status: 'ACTIVE' }).then(() => { + setRoleName(''); + setRoleCode(''); + setRoleDesc(''); + setTab('roles'); + void load(); + }); + }} + > + + +