nxtgauge-frontend-solid/src/components/dashboard/AdminDashboardPage.tsx
2026-04-26 23:58:43 +02:00

228 lines
7.1 KiB
TypeScript

import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
import { CARD, BTN_PRIMARY, BTN_GHOST } from '~/components/DashboardShell';
const API = '/api/gateway';
type AdminMetrics = {
totalUsers: number;
pendingVerifications: number;
activeSessions: number;
totalRoles: number;
totalPhotographers: number;
totalCustomers: number;
totalCompanies: number;
totalJobSeekers: number;
};
async function adminFetch(path: string, opts?: RequestInit) {
const token =
typeof window !== 'undefined'
? window.sessionStorage.getItem('nxtgauge_access_token') || ''
: '';
const cleanPath = path.startsWith('/api/') ? path.slice(4) : path;
return fetch(`${API}${cleanPath}`, {
...opts,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(opts?.headers ?? {}),
},
});
}
export default function AdminDashboardPage() {
const [metrics, setMetrics] = createSignal<AdminMetrics>({
totalUsers: 0,
pendingVerifications: 0,
activeSessions: 0,
totalRoles: 0,
totalPhotographers: 0,
totalCustomers: 0,
totalCompanies: 0,
totalJobSeekers: 0,
});
const [loading, setLoading] = createSignal(true);
const [error, setError] = createSignal('');
const loadAdminMetrics = async () => {
setLoading(true);
setError('');
try {
const [usersRes, rolesRes, photographersRes, customersRes, companiesRes, jobSeekersRes] =
await Promise.all([
adminFetch('/api/admin/users?page=1&limit=1'),
adminFetch('/api/admin/roles'),
adminFetch('/api/admin/users?role=photographer&page=1&limit=1'),
adminFetch('/api/admin/users?role=customer&page=1&limit=1'),
adminFetch('/api/admin/users?role=company&page=1&limit=1'),
adminFetch('/api/admin/users?role=jobseeker&page=1&limit=1'),
]);
const usersJson = await usersRes.json().catch(() => ({}));
const rolesJson = await rolesRes.json().catch(() => ({}));
const photographersJson = await photographersRes.json().catch(() => ({}));
const customersJson = await customersRes.json().catch(() => ({}));
const companiesJson = await companiesRes.json().catch(() => ({}));
const jobSeekersJson = await jobSeekersRes.json().catch(() => ({}));
setMetrics({
totalUsers: usersJson?.total ?? usersJson?.count ?? 0,
pendingVerifications: 0,
activeSessions: 0,
totalRoles: Array.isArray(rolesJson) ? rolesJson.length : 0,
totalPhotographers: photographersJson?.total ?? photographersJson?.count ?? 0,
totalCustomers: customersJson?.total ?? customersJson?.count ?? 0,
totalCompanies: companiesJson?.total ?? companiesJson?.count ?? 0,
totalJobSeekers: jobSeekersJson?.total ?? jobSeekersJson?.count ?? 0,
});
} catch (e: any) {
setError('Failed to load admin metrics: ' + e.message);
} finally {
setLoading(false);
}
};
onMount(loadAdminMetrics);
const statCards = createMemo(() => [
{ label: 'Total Users', value: metrics().totalUsers, color: '#0D0D2A' },
{ label: 'Photographers', value: metrics().totalPhotographers, color: '#FF5E13' },
{ label: 'Customers', value: metrics().totalCustomers, color: '#059669' },
{ label: 'Companies', value: metrics().totalCompanies, color: '#7C3AED' },
{ label: 'Job Seekers', value: metrics().totalJobSeekers, color: '#DC2626' },
{ label: 'Pending Verifications', value: metrics().pendingVerifications, color: '#D97706' },
]);
return (
<div style={{ display: 'grid', gap: '14px', 'max-width': '1200px' }}>
<div style={CARD}>
<p style={{ margin: '0', 'font-size': '18px', 'font-weight': '800', color: '#111827' }}>
Admin Dashboard
</p>
<p style={{ margin: '4px 0 0', 'font-size': '13px', color: '#6B7280' }}>
Platform overview and management metrics.
</p>
</div>
<Show when={error()}>
<div
style={{
...CARD,
border: '1px solid #FECACA',
background: '#FEF2F2',
padding: '12px 14px',
color: '#B91C1C',
'font-size': '13px',
'font-weight': '600',
}}
>
{error()}
</div>
</Show>
<Show when={loading()}>
<div style={{ ...CARD, 'text-align': 'center', color: '#9CA3AF' }}>
Loading admin metrics...
</div>
</Show>
<Show when={!loading()}>
<div
style={{
display: 'grid',
'grid-template-columns': 'repeat(3, minmax(0, 1fr))',
gap: '14px',
}}
>
<For each={statCards()}>
{(stat) => (
<div
style={{
border: '1px solid #E5E7EB',
background: 'white',
'border-radius': '16px',
padding: '16px',
'box-shadow': '0 1px 4px rgba(0,0,0,0.06)',
}}
>
<p
style={{
margin: '0',
'font-size': '11px',
'letter-spacing': '0.06em',
'text-transform': 'uppercase',
color: '#6B7280',
}}
>
{stat.label}
</p>
<p
style={{
margin: '8px 0 0',
'font-size': '32px',
'font-weight': '800',
color: stat.color,
}}
>
{stat.value}
</p>
</div>
)}
</For>
</div>
<div
style={{
border: '1px solid #E5E7EB',
background: 'white',
'border-radius': '16px',
padding: '16px',
'box-shadow': '0 1px 4px rgba(0,0,0,0.06)',
}}
>
<p
style={{
margin: '0 0 12px',
'font-size': '16px',
'font-weight': '700',
color: '#111827',
}}
>
Quick Actions
</p>
<div style={{ display: 'flex', gap: '10px', 'flex-wrap': 'wrap' }}>
<button
type="button"
onClick={() => (window.location.href = '/admin/users')}
style={BTN_PRIMARY}
>
Manage Users
</button>
<button
type="button"
onClick={() => (window.location.href = '/admin/verifications')}
style={BTN_GHOST}
>
Pending Verifications
</button>
<button
type="button"
onClick={() => (window.location.href = '/admin/roles')}
style={BTN_GHOST}
>
Manage Roles
</button>
<button
type="button"
onClick={loadAdminMetrics}
style={BTN_GHOST}
>
Refresh
</button>
</div>
</div>
</Show>
</div>
);
}