nxtgauge-frontend-solid/src/components/dashboard/MyDashboardPage.tsx

180 lines
9.1 KiB
TypeScript
Raw Normal View History

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';
import { readJobSeekerProfile } from '~/lib/job-seeker-custom-data';
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, profile] = await Promise.all([
apiFetch('/api/jobseeker/jobs?page=1&limit=100'),
apiFetch('/api/jobseeker/applications?page=1&limit=100'),
readJobSeekerProfile(),
]);
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 : [];
const customData = (profile?.custom_data && typeof profile.custom_data === 'object')
? (profile.custom_data as Record<string, unknown>)
: {};
const savedJobs = Array.isArray(customData.saved_jobs) ? customData.saved_jobs : [];
const portfolio = (customData.job_seeker_portfolio && typeof customData.job_seeker_portfolio === 'object')
? (customData.job_seeker_portfolio as Record<string, unknown>)
: {};
const profileStatus = String(profile?.status || 'NOT_SUBMITTED').replace(/_/g, ' ');
const portfolioDone = Boolean(
String(portfolio.headline || '').trim()
&& String(portfolio.education || '').trim()
&& String(portfolio.workExperience || '').trim()
&& String(portfolio.skills || '').trim(),
);
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: 'Saved Jobs', value: String(savedJobs.length), hint: 'Bookmarked for later' },
{ title: 'Profile Status', value: profileStatus, hint: 'Verification state' },
{ title: 'Portfolio', value: portfolioDone ? 'Complete' : 'Incomplete', hint: 'Education/work/skills sections' },
);
if (!jobsRes.ok && !appsRes.ok && !profile) 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>
);
}