diff --git a/src/app.css b/src/app.css index 2a3d9f2..07158ac 100644 --- a/src/app.css +++ b/src/app.css @@ -176,22 +176,31 @@ body { } .admin-header { - position: sticky; + position: fixed; top: 0; + left: 0; + right: 0; z-index: 40; border-bottom: 1px solid #e2e8f0; - background: rgba(255, 255, 255, 0.94); - backdrop-filter: blur(12px); + background: #fff; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08); } .admin-header-inner { - width: min(1440px, calc(100% - 36px)); - margin: 0 auto; - min-height: 64px; + width: calc(100% - 48px); + margin: 0 24px; + height: 64px; display: flex; align-items: center; justify-content: space-between; - gap: 14px; + gap: 16px; +} + +.admin-header-left { + display: flex; + align-items: center; + gap: 28px; + min-width: 0; } .admin-brand { @@ -201,30 +210,24 @@ body { } .admin-brand img { - height: 44px; + height: 40px; width: auto; } -.admin-brand-kicker { +.admin-page-heading { margin: 0; - color: #64748b; - font-size: 11px; - font-weight: 700; - letter-spacing: 0.1em; - text-transform: uppercase; -} - -.admin-brand h1 { - margin: 2px 0 0; - font-size: 17px; - font-weight: 800; - color: var(--brand-navy); + font-size: 16px; + font-weight: 600; + color: #1f2937; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .admin-header-actions { display: flex; align-items: center; - gap: 10px; + gap: 8px; } .admin-role-chip { @@ -240,12 +243,84 @@ body { text-transform: uppercase; } +.admin-avatar-btn { + border: 0; + border-radius: 10px; + background: transparent; + display: flex; + align-items: center; + gap: 10px; + padding: 4px 8px; + cursor: pointer; + color: #475569; +} + +.admin-avatar-btn:hover { + background: #f3f4f6; +} + +.admin-avatar { + width: 32px; + height: 32px; + border-radius: 999px; + border: 1px solid #fed7aa; + background: #ffedd5; + color: #c2410c; + font-size: 13px; + font-weight: 700; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.admin-avatar-meta { + display: flex; + flex-direction: column; + align-items: flex-start; + line-height: 1.1; +} + +.admin-avatar-name { + font-size: 12px; + font-weight: 600; + color: #334155; +} + +.admin-avatar-role { + font-size: 10px; + color: #64748b; +} + +.admin-logout-btn { + border: 0; + border-radius: 8px; + background: transparent; + color: #64748b; + padding: 6px; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.admin-logout-btn svg { + width: 18px; + height: 18px; +} + +.admin-logout-btn:hover { + background: #fef2f2; + color: #dc2626; +} + .shell { display: grid; grid-template-columns: 264px 1fr; - height: calc(100vh - 64px); + height: calc(100vh - 80px); overflow: hidden; transition: grid-template-columns 300ms ease; + padding-top: 80px; + background: #f9fafb; } .shell.sidebar-collapsed { @@ -337,10 +412,10 @@ body { text-decoration: none; color: #475569; border: 1px solid transparent; - border-radius: 10px; - padding: 10px 12px; + border-radius: 12px; + padding: 11px 12px; margin-bottom: 2px; - font-size: 13.5px; + font-size: 14px; line-height: 1.35; font-weight: 500; transition: background 180ms ease, border-color 180ms ease, color 180ms ease, box-shadow 180ms ease; @@ -412,11 +487,12 @@ body { min-width: 0; overflow-y: auto; height: 100%; + background: #f9fafb; } .main-inner { - max-width: 1200px; - padding: 20px 24px 32px; + max-width: none; + padding: 24px; } .admin-tab-wrap { @@ -471,6 +547,78 @@ body { color: #64748b; } +.page-hero-card { + margin-bottom: 16px; + border: 1px solid #fed7aa; + border-radius: 16px; + background: #fff; + box-shadow: 0 10px 28px -24px rgba(15, 23, 42, 0.42); + padding: 18px; +} + +.admin-link-tabs { + display: inline-flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 14px; +} + +.admin-link-tab { + text-decoration: none; + border: 1px solid #cbd5e1; + border-radius: 10px; + background: #fff; + color: #475569; + padding: 8px 12px; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.01em; + transition: all 150ms ease; +} + +.admin-link-tab:hover { + border-color: #94a3b8; + color: #0f172a; +} + +.admin-link-tab.active { + border-color: #ffc9ac; + background: #fff1e8; + color: #c2410c; +} + +.admin-segmented { + margin: 14px 0 12px; + display: inline-flex; + gap: 6px; + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 10px; + padding: 4px; +} + +.admin-segment { + border: 1px solid transparent; + background: transparent; + color: #475569; + border-radius: 8px; + padding: 7px 12px; + font-size: 12px; + font-weight: 700; + cursor: pointer; +} + +.admin-segment.active { + background: #fff1e8; + border-color: #ffc9ac; + color: #c2410c; +} + +.admin-segment:disabled { + opacity: 0.45; + cursor: not-allowed; +} + .grid { display: grid; grid-template-columns: 1.2fr 1fr; @@ -642,6 +790,10 @@ body { background: #fff7ed; } +.list-table tbody tr.row-selected td { + background: #fff7ed; +} + .align-right { text-align: right !important; } @@ -652,6 +804,42 @@ body { gap: 8px; } +.action-icon-btn { + border: 1px solid #d1d5db; + background: #fff; + color: #334155; + border-radius: 8px; + width: 30px; + height: 30px; + display: inline-flex; + align-items: center; + justify-content: center; + text-decoration: none; + cursor: pointer; + font-size: 13px; +} + +.action-icon-btn:hover { + background: #f8fafc; +} + +.action-icon-btn.danger { + color: #b91c1c; + border-color: #fca5a5; +} + +.admin-pagination { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + padding: 12px; + border-top: 1px solid #e2e8f0; + background: #fff; + font-size: 12px; + color: #475569; +} + .status-chip { display: inline-flex; align-items: center; @@ -714,6 +902,8 @@ body { @media (max-width: 1000px) { .shell { grid-template-columns: 1fr; + padding-top: 72px; + height: calc(100vh - 72px); } .sidebar { @@ -727,11 +917,23 @@ body { } .admin-header-inner { - min-height: 62px; + height: 56px; + width: calc(100% - 24px); + margin: 0 12px; + gap: 10px; } .admin-brand img { - height: 36px; + height: 32px; + } + + .admin-page-heading { + font-size: 14px; + } + + .admin-role-chip, + .admin-avatar-meta { + display: none; } } diff --git a/src/components/AdminShell.tsx b/src/components/AdminShell.tsx index d1d2f42..3822f48 100644 --- a/src/components/AdminShell.tsx +++ b/src/components/AdminShell.tsx @@ -49,6 +49,29 @@ const TAB_SETS: Array<{ prefixes: string[]; tabs: Tab[] }> = [ }, ]; +const PAGE_TITLES: Array<{ prefix: string; title: string }> = [ + { prefix: '/admin/approval', title: 'Approval Management' }, + { prefix: '/admin/users', title: 'External User Management' }, + { prefix: '/admin/company', title: 'Company Management' }, + { prefix: '/admin/customer', title: 'Customer Management' }, + { prefix: '/admin/candidate', title: 'Candidate Management' }, + { prefix: '/admin/photographer', title: 'Photographer Management' }, + { prefix: '/admin/makeup-artist', title: 'Makeup Artist Management' }, + { prefix: '/admin/tutors', title: 'Tutor Management' }, + { prefix: '/admin/developers', title: 'Developer Management' }, + { prefix: '/admin/jobs', title: 'Jobs Management' }, + { prefix: '/admin/leads', title: 'Leads Management' }, + { prefix: '/admin/pricing', title: 'Pricing Management' }, + { prefix: '/admin/invoice', title: 'Invoice Management' }, + { prefix: '/admin/credit', title: 'Credit Management' }, + { prefix: '/admin/ledger', title: 'Ledger Management' }, + { prefix: '/admin/report', title: 'Report Management' }, + { prefix: '/admin/roles', title: 'Internal Role Management' }, + { prefix: '/admin/runtime-roles', title: 'External Role Management' }, + { prefix: '/admin/onboarding-schemas', title: 'Onboarding Management' }, + { prefix: '/admin', title: 'Dashboard' }, +]; + export default function AdminShell(props: { children: JSX.Element }) { const location = useLocation(); const navigate = useNavigate(); @@ -69,6 +92,14 @@ export default function AdminShell(props: { children: JSX.Element }) { return location.pathname === tab.href || location.pathname.startsWith(`${tab.href}/`); }; + const pageTitle = createMemo(() => { + const path = location.pathname; + for (const item of PAGE_TITLES) { + if (path === item.prefix || path.startsWith(`${item.prefix}/`)) return item.title; + } + return 'Dashboard'; + }); + onMount(() => { if (!hasAdminSession()) { const from = encodeURIComponent(location.pathname + location.search); @@ -87,16 +118,26 @@ export default function AdminShell(props: { children: JSX.Element }) {
-
- NXTGAUGE -
-

Administration

-

NXTGAUGE Admin Panel

+
+
+ NXTGAUGE
+

{pageTitle()}

Super Admin

- + +
diff --git a/src/routes/admin/approval.tsx b/src/routes/admin/approval.tsx index 738472c..9dcacd6 100644 --- a/src/routes/admin/approval.tsx +++ b/src/routes/admin/approval.tsx @@ -30,8 +30,10 @@ interface ApprovalRule { const ENTITY_TYPE_OPTIONS = ['JOB_POST', 'COMPANY', 'LEAD', 'INVOICE']; const APPROVER_TYPE_OPTIONS = ['USER', 'ROLE']; +const REQUEST_FILTERS = ['ALL', 'PROFILE', 'JOB', 'REQUIREMENT']; type StatusTab = 'PENDING' | 'APPROVED' | 'REJECTED' | 'CHANGES_REQUESTED' | 'CANCELLED' | 'rules'; +type PanelTab = 'list' | 'view'; const STATUS_TABS: { key: StatusTab; label: string }[] = [ { key: 'PENDING', label: 'Pending' }, @@ -64,17 +66,40 @@ async function fetchRules(): Promise { } } +function statusValue(item: Approval) { + return (item.requestStatus || item.status || 'PENDING').toUpperCase(); +} + +function requestTypeValue(item: Approval) { + return (item.requestType || item.type || 'OTHER').toUpperCase(); +} + +function requestClass(item: Approval): 'PROFILE' | 'JOB' | 'REQUIREMENT' | 'OTHER' { + const t = requestTypeValue(item); + if (t.includes('JOB')) return 'JOB'; + if (t.includes('LEAD') || t.includes('REQUIREMENT')) return 'REQUIREMENT'; + if (t.includes('PROFILE') || t.includes('USER') || t.includes('COMPANY') || t.includes('PROFESSIONAL') || t.includes('CUSTOMER')) return 'PROFILE'; + return 'OTHER'; +} + function StatusBadge(props: { status: string }) { - const s = (props.status || '').toUpperCase(); + const s = props.status.toUpperCase(); if (s === 'APPROVED') return APPROVED; if (s === 'REJECTED') return REJECTED; if (s === 'CHANGES_REQUESTED') return CHANGES REQUESTED; if (s === 'CANCELLED') return CANCELLED; - return {props.status || 'PENDING'}; + return {s}; } export default function ApprovalPage() { const [activeTab, setActiveTab] = createSignal('PENDING'); + const [panelTab, setPanelTab] = createSignal('list'); + const [selectedApproval, setSelectedApproval] = createSignal(null); + + const [search, setSearch] = createSignal(''); + const [requestFilter, setRequestFilter] = createSignal('ALL'); + const [currentPage, setCurrentPage] = createSignal(1); + const perPage = 10; const [approvals, { refetch: refetchApprovals }] = createResource(fetchApprovals); const [acting, setActing] = createSignal(''); @@ -89,21 +114,48 @@ export default function ApprovalPage() { const [deletingRule, setDeletingRule] = createSignal(''); const [submittingRule, setSubmittingRule] = createSignal(false); - const tabApprovals = createMemo(() => { + const filteredApprovals = createMemo(() => { const tab = activeTab(); - if (tab === 'rules') return []; + const q = search().trim().toLowerCase(); + const rf = requestFilter(); const list = approvals() ?? []; + + if (tab === 'rules') return [] as Approval[]; + return list.filter((a) => { - const s = (a.requestStatus || a.status || 'PENDING').toUpperCase(); - return s === tab; + const matchesStatus = statusValue(a) === tab; + if (!matchesStatus) return false; + + const cls = requestClass(a); + const matchesType = rf === 'ALL' || cls === rf; + if (!matchesType) return false; + + if (!q) return true; + + const requesterName = (a.requester?.name || a.requesterName || a.requester_name || '').toLowerCase(); + const requesterEmail = (a.requester?.email || a.requesterEmail || a.requester_email || '').toLowerCase(); + const requestType = requestTypeValue(a).toLowerCase(); + + return requesterName.includes(q) || requesterEmail.includes(q) || requestType.includes(q) || a.id.toLowerCase().includes(q); }); }); - // Count per status for badges + const totalPages = createMemo(() => Math.max(1, Math.ceil(filteredApprovals().length / perPage))); + + const paginatedApprovals = createMemo(() => { + const start = (currentPage() - 1) * perPage; + return filteredApprovals().slice(start, start + perPage); + }); + const countFor = (status: string) => { const list = approvals() ?? []; if (status === 'rules') return (rules() ?? []).length; - return list.filter((a) => (a.requestStatus || a.status || 'PENDING').toUpperCase() === status).length; + return list.filter((a) => statusValue(a) === status).length; + }; + + const selectApproval = (approval: Approval) => { + setSelectedApproval(approval); + setPanelTab('view'); }; const handleAction = async (id: string, newStatus: 'APPROVED' | 'REJECTED' | 'CHANGES_REQUESTED' | 'CANCELLED') => { @@ -117,6 +169,10 @@ export default function ApprovalPage() { }); if (!res.ok) throw new Error(`Failed to ${newStatus.toLowerCase()} approval`); refetchApprovals(); + + if (selectedApproval()?.id === id) { + setSelectedApproval((prev) => (prev ? { ...prev, status: newStatus, requestStatus: newStatus } : prev)); + } } catch (err: any) { setApprovalError(err.message || 'Action failed'); } finally { @@ -174,15 +230,18 @@ export default function ApprovalPage() {
- {/* Status tabs */} -
+
{STATUS_TABS.map((t) => { const count = countFor(t.key); return (
- {/* Approvals content */} +
+ + +
+
{approvalError()}
-
-
- - - - - - - - - - - - - - - - - - - - - - 0}> - - {(item) => { - const requesterName = item.requester?.name || item.requesterName || item.requester_name || '—'; - const requesterEmail = item.requester?.email || item.requesterEmail || item.requester_email || ''; - const status = (item.requestStatus || item.status || 'PENDING').toUpperCase(); - const requestType = item.requestType || item.type || '—'; - const submittedAt = item.createdAt || item.created_at; - return ( - - - - - - - - - ); - }} - - - -
RequesterRequest TypeStatusPrioritySubmitted AtActions
Loading...
Failed to load approvals.
No {activeTab().toLowerCase().replace('_', ' ')} approvals.
-
{requesterName}
- -
{requesterEmail}
-
-
{requestType}{item.priority ?? '—'}{submittedAt ? new Date(submittedAt).toLocaleString() : '—'} -
- - - - - - - - - -
-
+ +
+ { + setSearch(e.currentTarget.value); + setCurrentPage(1); + }} + style="border:1px solid #cbd5e1;border-radius:6px;padding:7px 12px;font-size:14px;width:300px;outline:none;" + /> + + {filteredApprovals().length} records
-
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + 0}> + + {(item) => { + const requesterName = item.requester?.name || item.requesterName || item.requester_name || '—'; + const requesterEmail = item.requester?.email || item.requesterEmail || item.requester_email || ''; + const status = statusValue(item); + const requestType = requestTypeValue(item); + const submittedAt = item.createdAt || item.created_at; + return ( + + + + + + + + + + ); + }} + + + +
Approval IDRequesterRequest TypeStatusPrioritySubmitted AtActions
Loading...
Failed to load approvals.
No matching approvals.
{item.id.slice(0, 8)}... +
{requesterName}
+ +
{requesterEmail}
+
+
{requestType}{item.priority ?? '—'}{submittedAt ? new Date(submittedAt).toLocaleString() : '—'} +
+ + + + + + + + + + +
+
+
+
+ + Page {currentPage()} of {totalPages()} + +
+
+
+ + +
+ Select an approval from list to view details.

}> +
+

Approval Details

+ +
+
+
+

Request Summary

+

Approval ID: {selectedApproval()!.id}

+

Type: {requestTypeValue(selectedApproval()!)}

+

Status: {statusValue(selectedApproval()!)}

+

Priority: {selectedApproval()!.priority ?? '—'}

+

Submitted: {(selectedApproval()!.createdAt || selectedApproval()!.created_at) ? new Date((selectedApproval()!.createdAt || selectedApproval()!.created_at)!).toLocaleString() : '—'}

+
+
+

Requester

+

Name: {selectedApproval()!.requester?.name || selectedApproval()!.requesterName || selectedApproval()!.requester_name || '—'}

+

Email: {selectedApproval()!.requester?.email || selectedApproval()!.requesterEmail || selectedApproval()!.requester_email || '—'}

+

Class: {requestClass(selectedApproval()!)}

+
+ + + +
+
+
+
+
+
- {/* Rules tab */}
{ruleError()}
@@ -321,7 +432,7 @@ export default function ApprovalPage() {
- setNewPriority(parseInt(e.currentTarget.value) || 1)} /> + setNewPriority(parseInt(e.currentTarget.value, 10) || 1)} />
@@ -363,11 +474,7 @@ export default function ApprovalPage() { {rule.priority ?? '—'}
-
diff --git a/src/routes/admin/onboarding-schemas/index.tsx b/src/routes/admin/onboarding-schemas/index.tsx index bc51280..5cace5f 100644 --- a/src/routes/admin/onboarding-schemas/index.tsx +++ b/src/routes/admin/onboarding-schemas/index.tsx @@ -1,4 +1,4 @@ -import { A, useNavigate } from '@solidjs/router'; +import { A } from '@solidjs/router'; import { createResource, createSignal, Show } from 'solid-js'; import AdminShell from '~/components/AdminShell'; @@ -33,7 +33,6 @@ async function loadSchemas(): Promise { } export default function OnboardingSchemasPage() { - const navigate = useNavigate(); const [schemas, { refetch }] = createResource(loadSchemas); const [deleteError, setDeleteError] = createSignal(''); const [deleting, setDeleting] = createSignal(''); @@ -63,6 +62,12 @@ export default function OnboardingSchemasPage() { Create Onboarding Flow
+ +
{deleteError()}
@@ -108,13 +113,14 @@ export default function OnboardingSchemasPage() {
- + 👁
diff --git a/src/routes/admin/roles/index.tsx b/src/routes/admin/roles/index.tsx index d8fc3c3..b964e20 100644 --- a/src/routes/admin/roles/index.tsx +++ b/src/routes/admin/roles/index.tsx @@ -1,4 +1,4 @@ -import { A, useNavigate } from '@solidjs/router'; +import { A } from '@solidjs/router'; import { createResource, createSignal, Show } from 'solid-js'; import AdminShell from '~/components/AdminShell'; @@ -29,7 +29,6 @@ async function loadInternalRoles(): Promise { } export default function InternalRolesPage() { - const navigate = useNavigate(); const [roles, { refetch }] = createResource(loadInternalRoles); const [deleting, setDeleting] = createSignal(''); const [deleteError, setDeleteError] = createSignal(''); @@ -51,7 +50,7 @@ export default function InternalRolesPage() { return ( -
+

Internal Role Management

Manage internal employee roles and permissions from one clean list.

@@ -59,6 +58,12 @@ export default function InternalRolesPage() { Create Internal Role
+ +
{deleteError()}
@@ -101,19 +106,15 @@ export default function InternalRolesPage() { {role.description || 'No description added yet.'}
- View + 👁 + -
diff --git a/src/routes/admin/runtime-roles/index.tsx b/src/routes/admin/runtime-roles/index.tsx index cf27c0f..ee8b5e8 100644 --- a/src/routes/admin/runtime-roles/index.tsx +++ b/src/routes/admin/runtime-roles/index.tsx @@ -1,5 +1,5 @@ -import { A, useNavigate } from '@solidjs/router'; -import { createResource, createSignal, Show } from 'solid-js'; +import { A, useSearchParams } from '@solidjs/router'; +import { createMemo, createResource, createSignal, Show } from 'solid-js'; import AdminShell from '~/components/AdminShell'; const API = '/api/gateway'; @@ -35,10 +35,11 @@ async function loadExternalRoles(): Promise { } export default function RuntimeRolesPage() { - const navigate = useNavigate(); + const [searchParams] = useSearchParams(); const [roles, { refetch }] = createResource(loadExternalRoles); const [deleting, setDeleting] = createSignal(''); const [deleteError, setDeleteError] = createSignal(''); + const selectedRoleKey = createMemo(() => (searchParams.roleKey || '').toLowerCase()); const handleDelete = async (id: string, name: string) => { if (!confirm(`Delete external role "${name}"?`)) return; @@ -61,9 +62,18 @@ export default function RuntimeRolesPage() {

External Role Management

Manage canonical external runtime roles, enabled modules, onboarding assignment, and approval gates from one place.

- Create External Role +
+ +
{deleteError()}
@@ -102,7 +112,7 @@ export default function RuntimeRolesPage() { 0}> {roles()!.map((role) => ( - +

{role.displayName}

@@ -119,13 +129,15 @@ export default function RuntimeRolesPage() {
- Edit + 👁 +
diff --git a/src/routes/admin/users.tsx b/src/routes/admin/users.tsx index c28f16a..c65ddc0 100644 --- a/src/routes/admin/users.tsx +++ b/src/routes/admin/users.tsx @@ -14,6 +14,9 @@ interface User { status: 'ACTIVE' | 'INACTIVE' | 'PENDING'; created_at?: string; createdAt?: string; + roleId?: string; + role_name?: string; + userType?: string | number; } const ROLE_OPTIONS = [ @@ -60,9 +63,13 @@ function StatusBadge(props: { status: string }) { export default function UsersPage() { const [users, { refetch }] = createResource(fetchUsers); + const [activeTab, setActiveTab] = createSignal<'list' | 'view'>('list'); + const [selectedUser, setSelectedUser] = createSignal(null); const [search, setSearch] = createSignal(''); const [filterRole, setFilterRole] = createSignal(''); const [filterStatus, setFilterStatus] = createSignal(''); + const [currentPage, setCurrentPage] = createSignal(1); + const usersPerPage = 10; const filtered = createMemo(() => { const list = users() ?? []; @@ -79,6 +86,25 @@ export default function UsersPage() { }); }); + const totalPages = createMemo(() => { + const count = filtered().length; + return Math.max(1, Math.ceil(count / usersPerPage)); + }); + + const paginated = createMemo(() => { + const page = currentPage(); + const start = (page - 1) * usersPerPage; + return filtered().slice(start, start + usersPerPage); + }); + + const shortId = (id: string) => `${id.slice(0, 8)}...`; + const registrationRole = (u: User) => (u.role_name || u.role || 'UNKNOWN').toUpperCase(); + + const onView = (user: User) => { + setSelectedUser(user); + setActiveTab('view'); + }; + return (
@@ -88,93 +114,167 @@ export default function UsersPage() {
- {/* Filters */} -
- setSearch(e.currentTarget.value)} - style="border:1px solid #cbd5e1;border-radius:6px;padding:7px 12px;font-size:14px;width:260px;outline:none;" - /> - - - - - Showing {filtered().length} of {users()?.length ?? 0} users - - + View Details +
-
-
- - - - - - - - - - - - - - - - - - - - - - 0}> - - {(item) => ( - - - - - - - - - )} - - - -
NameEmailRoleStatusCreated AtActions
Loading...
Failed to load. Is the backend running?
No users found.
{item.name || item.full_name || '—'}{item.email}{item.role_name || item.role || '—'} - - - {(item.created_at || item.createdAt) - ? new Date((item.created_at || item.createdAt)!).toLocaleDateString() - : '—'} - -
- View -
-
+ {/* Filters */} + +
+ { + setSearch(e.currentTarget.value); + setCurrentPage(1); + }} + style="border:1px solid #cbd5e1;border-radius:6px;padding:7px 12px;font-size:14px;width:260px;outline:none;" + /> + + + + + Showing {paginated().length} of {filtered().length} users + +
-
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + 0}> + + {(item) => ( + + + + + + + + + + )} + + + +
User IDNameEmailRegistration RoleStatusCreatedActions
Loading...
Failed to load. Is the backend running?
No users found.
{shortId(item.id)}{item.name || item.full_name || '—'}{item.email}{registrationRole(item)} + + + {(item.created_at || item.createdAt) + ? new Date((item.created_at || item.createdAt)!).toLocaleDateString() + : '—'} + +
+ + + +
+
+
+
+ + Page {currentPage()} of {totalPages()} + +
+
+
+ + +
+ Select a user from list to view details.

}> +
+

User Details

+ +
+
+
+

Profile

+

User ID: {selectedUser()!.id}

+

Name: {selectedUser()!.name || selectedUser()!.full_name || '—'}

+

Email: {selectedUser()!.email}

+

Role: {registrationRole(selectedUser()!)}

+

Status: {selectedUser()!.status}

+
+
+

Account

+

Created: {(selectedUser()!.created_at || selectedUser()!.createdAt) ? new Date((selectedUser()!.created_at || selectedUser()!.createdAt)!).toLocaleString() : '—'}

+

Role ID: {selectedUser()!.roleId || '—'}

+
+ Edit User + +
+
+
+
+
+
); }