163 lines
8 KiB
TypeScript
163 lines
8 KiB
TypeScript
|
|
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
||
|
|
import { BTN_GHOST, CARD } from '~/components/DashboardShell';
|
||
|
|
import { PROFESSIONAL_ROLE_SET, ROLE_PREFIXES, type RoleKey } from './RoleDashboardShared';
|
||
|
|
|
||
|
|
const API = '/api/gateway';
|
||
|
|
|
||
|
|
type Props = {
|
||
|
|
roleKey: RoleKey;
|
||
|
|
userName?: string;
|
||
|
|
};
|
||
|
|
|
||
|
|
type Metric = {
|
||
|
|
title: string;
|
||
|
|
value: string;
|
||
|
|
hint: string;
|
||
|
|
};
|
||
|
|
|
||
|
|
async function apiFetch(path: string, opts?: RequestInit) {
|
||
|
|
return fetch(`${API}${path}`, {
|
||
|
|
...opts,
|
||
|
|
credentials: 'include',
|
||
|
|
headers: { 'Content-Type': 'application/json', ...(opts?.headers ?? {}) },
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function MyDashboardPage(props: Props) {
|
||
|
|
const [metrics, setMetrics] = createSignal<Metric[]>([]);
|
||
|
|
const [loading, setLoading] = createSignal(true);
|
||
|
|
const [err, setErr] = createSignal('');
|
||
|
|
|
||
|
|
const roleLabel = createMemo(() => String(props.roleKey || '').replace(/_/g, ' '));
|
||
|
|
|
||
|
|
const loadData = async () => {
|
||
|
|
setLoading(true);
|
||
|
|
setErr('');
|
||
|
|
const next: Metric[] = [];
|
||
|
|
|
||
|
|
try {
|
||
|
|
if (props.roleKey === 'COMPANY') {
|
||
|
|
const [jobsRes, appsRes] = await Promise.all([
|
||
|
|
apiFetch('/api/companies/jobs?page=1&limit=100'),
|
||
|
|
apiFetch('/api/companies/jobs?page=1&limit=1'),
|
||
|
|
]);
|
||
|
|
const jobsJson = await jobsRes.json().catch(() => ({}));
|
||
|
|
const appsJson = await appsRes.json().catch(() => ({}));
|
||
|
|
const jobs = Array.isArray(jobsJson?.data) ? jobsJson.data : [];
|
||
|
|
next.push(
|
||
|
|
{ title: 'Total Jobs', value: String(jobs.length), hint: 'All job posts created' },
|
||
|
|
{ title: 'Active Jobs', value: String(jobs.filter((j: any) => String(j.status || '').toUpperCase() === 'OPEN').length), hint: 'Open to applications' },
|
||
|
|
{ title: 'Jobs In Verification', value: String(jobs.filter((j: any) => String(j.status || '').toUpperCase().includes('PENDING')).length), hint: 'Pending verification/approval' },
|
||
|
|
{ title: 'Latest Sync', value: appsRes.ok ? 'Live' : 'Partial', hint: 'Dashboard data status' },
|
||
|
|
);
|
||
|
|
if (!jobsRes.ok && !appsRes.ok) setErr('Some company metrics could not be loaded.');
|
||
|
|
} else if (props.roleKey === 'CUSTOMER') {
|
||
|
|
const reqRes = await apiFetch('/api/customers/requirements?page=1&limit=100');
|
||
|
|
const reqJson = await reqRes.json().catch(() => ({}));
|
||
|
|
const reqs = Array.isArray(reqJson?.data) ? reqJson.data : [];
|
||
|
|
next.push(
|
||
|
|
{ title: 'My Requirements', value: String(reqs.length), hint: 'Total posted requirements' },
|
||
|
|
{ title: 'Open Requirements', value: String(reqs.filter((r: any) => String(r.status || '').toUpperCase() === 'OPEN').length), hint: 'Visible to professionals' },
|
||
|
|
{ title: 'In Verification', value: String(reqs.filter((r: any) => String(r.status || '').toUpperCase().includes('PENDING')).length), hint: 'Verification/approval stage' },
|
||
|
|
{ title: 'Drafts', value: String(reqs.filter((r: any) => String(r.status || '').toUpperCase() === 'DRAFT').length), hint: 'Not yet submitted' },
|
||
|
|
);
|
||
|
|
if (!reqRes.ok) setErr('Some customer metrics could not be loaded.');
|
||
|
|
} else if (props.roleKey === 'JOB_SEEKER') {
|
||
|
|
const [jobsRes, appsRes] = await Promise.all([
|
||
|
|
apiFetch('/api/jobseeker/jobs?page=1&limit=100'),
|
||
|
|
apiFetch('/api/jobseeker/applications?page=1&limit=100'),
|
||
|
|
]);
|
||
|
|
const jobsJson = await jobsRes.json().catch(() => ({}));
|
||
|
|
const appsJson = await appsRes.json().catch(() => ({}));
|
||
|
|
const jobs = Array.isArray(jobsJson?.data) ? jobsJson.data : [];
|
||
|
|
const apps = Array.isArray(appsJson?.data) ? appsJson.data : [];
|
||
|
|
next.push(
|
||
|
|
{ title: 'Available Jobs', value: String(jobs.length), hint: 'Open approved jobs' },
|
||
|
|
{ title: 'My Applications', value: String(apps.length), hint: 'Total applications submitted' },
|
||
|
|
{ title: 'Shortlisted', value: String(apps.filter((a: any) => String(a.status || '').toUpperCase() === 'SHORTLISTED').length), hint: 'Moved ahead in process' },
|
||
|
|
{ title: 'Under Review', value: String(apps.filter((a: any) => String(a.status || '').toUpperCase().includes('REVIEW')).length), hint: 'Awaiting decision' },
|
||
|
|
);
|
||
|
|
if (!jobsRes.ok && !appsRes.ok) setErr('Some job seeker metrics could not be loaded.');
|
||
|
|
} else if (PROFESSIONAL_ROLE_SET.has(props.roleKey)) {
|
||
|
|
const prefix = ROLE_PREFIXES[props.roleKey];
|
||
|
|
const [marketRes, reqRes, walletRes] = await Promise.all([
|
||
|
|
apiFetch(`/api/${prefix}/marketplace?page=1&limit=100`),
|
||
|
|
apiFetch(`/api/${prefix}/leads/requests/me?page=1&limit=100`),
|
||
|
|
apiFetch(`/api/${prefix}/wallet/me`),
|
||
|
|
]);
|
||
|
|
const marketJson = await marketRes.json().catch(() => ({}));
|
||
|
|
const reqJson = await reqRes.json().catch(() => ({}));
|
||
|
|
const walletJson = await walletRes.json().catch(() => ({}));
|
||
|
|
const market = Array.isArray(marketJson?.data) ? marketJson.data : [];
|
||
|
|
const requests = Array.isArray(reqJson?.data) ? reqJson.data : [];
|
||
|
|
next.push(
|
||
|
|
{ title: 'Open Leads', value: String(market.length), hint: 'Available opportunities' },
|
||
|
|
{ title: 'My Requests', value: String(requests.length), hint: 'Lead requests sent' },
|
||
|
|
{ title: 'Accepted Requests', value: String(requests.filter((r: any) => ['APPROVED', 'CONTACT_UNLOCKED'].includes(String(r.status || '').toUpperCase())).length), hint: 'Approved responses' },
|
||
|
|
{ title: 'Tracecoins', value: String(walletJson?.balance ?? 0), hint: 'Current wallet balance' },
|
||
|
|
);
|
||
|
|
if (!marketRes.ok && !reqRes.ok && !walletRes.ok) setErr('Some professional metrics could not be loaded.');
|
||
|
|
} else {
|
||
|
|
next.push(
|
||
|
|
{ title: 'Welcome', value: 'Ready', hint: 'Dashboard initialized' },
|
||
|
|
{ title: 'Role', value: roleLabel(), hint: 'Current active role' },
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
setMetrics(next);
|
||
|
|
} catch {
|
||
|
|
setErr('Network error while loading dashboard metrics.');
|
||
|
|
setMetrics([
|
||
|
|
{ title: 'Status', value: 'Unavailable', hint: 'Please retry' },
|
||
|
|
]);
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
onMount(loadData);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div style={{ display: 'grid', gap: '14px', 'max-width': '980px' }}>
|
||
|
|
<div style={CARD}>
|
||
|
|
<p style={{ margin: '0', 'font-size': '22px', 'font-weight': '800', color: '#0D0D2A' }}>My Dashboard</p>
|
||
|
|
<p style={{ margin: '4px 0 0', 'font-size': '13px', color: '#6B7280' }}>
|
||
|
|
Welcome {props.userName || 'User'}. Role: {roleLabel()}.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Show when={err()}>
|
||
|
|
<div style={{ ...CARD, border: '1px solid #FECACA', background: '#FEF2F2', padding: '12px 14px', color: '#B91C1C', 'font-size': '13px', 'font-weight': '600' }}>
|
||
|
|
{err()}
|
||
|
|
</div>
|
||
|
|
</Show>
|
||
|
|
|
||
|
|
<div style={CARD}>
|
||
|
|
<div style={{ display: 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-bottom': '10px' }}>
|
||
|
|
<p style={{ margin: '0', 'font-size': '16px', 'font-weight': '700', color: '#111827' }}>Quick Summary</p>
|
||
|
|
<button type="button" onClick={loadData} style={BTN_GHOST}>Refresh</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Show when={loading()}>
|
||
|
|
<p style={{ margin: '0', color: '#9CA3AF', 'font-size': '13px' }}>Loading dashboard...</p>
|
||
|
|
</Show>
|
||
|
|
|
||
|
|
<Show when={!loading()}>
|
||
|
|
<div style={{ display: 'grid', 'grid-template-columns': 'repeat(4,minmax(0,1fr))', gap: '10px' }}>
|
||
|
|
<For each={metrics()}>
|
||
|
|
{(m) => (
|
||
|
|
<div style={{ border: '1px solid #E5E7EB', 'border-radius': '12px', padding: '12px', background: '#FCFCFD' }}>
|
||
|
|
<p style={{ margin: '0', 'font-size': '11px', 'letter-spacing': '0.05em', 'text-transform': 'uppercase', color: '#6B7280' }}>{m.title}</p>
|
||
|
|
<p style={{ margin: '8px 0 0', 'font-size': '28px', 'line-height': '1', 'font-weight': '800', color: '#111827' }}>{m.value}</p>
|
||
|
|
<p style={{ margin: '6px 0 0', 'font-size': '12px', color: '#6B7280' }}>{m.hint}</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</For>
|
||
|
|
</div>
|
||
|
|
</Show>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|