2026-04-08 22:40:43 +02:00
|
|
|
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
|
|
|
|
import { BTN_GHOST, CARD } from '~/components/DashboardShell';
|
2026-04-22 01:13:56 +02:00
|
|
|
import { normalizeRole, PROFESSIONAL_ROLE_SET, ROLE_PREFIXES, type RoleKey } from './RoleDashboardShared';
|
2026-04-10 01:21:36 +02:00
|
|
|
import { readJobSeekerProfile } from '~/lib/job-seeker-custom-data';
|
2026-04-08 22:40:43 +02:00
|
|
|
|
|
|
|
|
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, ' '));
|
|
|
|
|
|
2026-04-22 01:13:56 +02:00
|
|
|
const getEffectiveRole = (): RoleKey => {
|
|
|
|
|
if (typeof window === 'undefined') return props.roleKey;
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
|
const urlRole = urlParams.get('role');
|
|
|
|
|
if (urlRole) {
|
|
|
|
|
const normalized = normalizeRole(urlRole);
|
|
|
|
|
if (normalized) return normalized;
|
|
|
|
|
}
|
|
|
|
|
return props.roleKey;
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-08 22:40:43 +02:00
|
|
|
const loadData = async () => {
|
2026-04-22 01:13:56 +02:00
|
|
|
const effectiveRole = getEffectiveRole();
|
|
|
|
|
console.log('[MyDashboardPage] loadData called, effectiveRole:', effectiveRole, 'props.roleKey:', props.roleKey);
|
|
|
|
|
|
2026-04-08 22:40:43 +02:00
|
|
|
setLoading(true);
|
|
|
|
|
setErr('');
|
|
|
|
|
const next: Metric[] = [];
|
|
|
|
|
|
2026-04-22 01:13:56 +02:00
|
|
|
const roleKey = effectiveRole;
|
|
|
|
|
|
2026-04-08 22:40:43 +02:00
|
|
|
try {
|
2026-04-22 01:13:56 +02:00
|
|
|
if (roleKey === 'COMPANY') {
|
2026-04-08 22:40:43 +02:00
|
|
|
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.');
|
2026-04-22 01:13:56 +02:00
|
|
|
} else if (roleKey === 'CUSTOMER') {
|
2026-04-08 22:40:43 +02:00
|
|
|
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.');
|
2026-04-22 01:13:56 +02:00
|
|
|
} else if (roleKey === 'JOB_SEEKER') {
|
2026-04-10 01:21:36 +02:00
|
|
|
const [jobsRes, appsRes, profile] = await Promise.all([
|
2026-04-08 22:40:43 +02:00
|
|
|
apiFetch('/api/jobseeker/jobs?page=1&limit=100'),
|
|
|
|
|
apiFetch('/api/jobseeker/applications?page=1&limit=100'),
|
2026-04-10 01:21:36 +02:00
|
|
|
readJobSeekerProfile(),
|
2026-04-08 22:40:43 +02:00
|
|
|
]);
|
|
|
|
|
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 : [];
|
2026-04-10 01:21:36 +02:00
|
|
|
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 : [];
|
2026-04-22 01:13:56 +02:00
|
|
|
const portfolio = (customData.job_seeker_portfolio && typeof profile.job_seeker_portfolio === 'object')
|
|
|
|
|
? (profile.job_seeker_portfolio as Record<string, unknown>)
|
2026-04-10 01:21:36 +02:00
|
|
|
: {};
|
|
|
|
|
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(),
|
|
|
|
|
);
|
2026-04-08 22:40:43 +02:00
|
|
|
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' },
|
2026-04-10 01:21:36 +02:00
|
|
|
{ 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' },
|
2026-04-08 22:40:43 +02:00
|
|
|
);
|
2026-04-10 01:21:36 +02:00
|
|
|
if (!jobsRes.ok && !appsRes.ok && !profile) setErr('Some job seeker metrics could not be loaded.');
|
2026-04-22 01:13:56 +02:00
|
|
|
} else if (PROFESSIONAL_ROLE_SET.has(roleKey)) {
|
|
|
|
|
const prefix = ROLE_PREFIXES[roleKey];
|
2026-04-08 22:40:43 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-22 01:13:56 +02:00
|
|
|
onMount(() => {
|
|
|
|
|
setTimeout(() => loadData(), 0);
|
|
|
|
|
});
|
2026-04-08 22:40:43 +02:00
|
|
|
|
|
|
|
|
return (
|
2026-04-22 01:26:36 +02:00
|
|
|
<div style={{ display: "grid", gap: "14px", "max-width": "980px" }}>
|
2026-04-08 22:40:43 +02:00
|
|
|
<div style={CARD}>
|
2026-04-22 01:26:36 +02:00
|
|
|
<p style={{ margin: "0", "font-size": "18px", "font-weight": "800", color: "#111827" }}>My Dashboard</p>
|
|
|
|
|
<p style={{ margin: "4px 0 0", "font-size": "13px", color: "#6B7280" }}>
|
2026-04-08 22:40:43 +02:00
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
}
|