171 lines
8.1 KiB
JavaScript
171 lines
8.1 KiB
JavaScript
/* ============================================================
|
|
dashboard.js — Main dashboard view
|
|
============================================================ */
|
|
window.Views = window.Views || {};
|
|
|
|
Views.dashboard = function () {
|
|
const k = DB.kpis;
|
|
const kpiCards = [
|
|
{ label: 'Open Jobs', value: k.openJobs, icon: 'briefcase', cls: 'i-indigo', trend: '+8%', dir: 'up', foot: 'vs last month' },
|
|
{ label: 'Total Candidates', value: k.totalCandidates, icon: 'users', cls: 'i-blue', trend: '+12%', dir: 'up', foot: 'active in pipeline' },
|
|
{ label: 'Interviews Today', value: k.interviewsToday, icon: 'calendar', cls: 'i-purple', trend: '3 upcoming', dir: 'flat', foot: 'next at 2:00 PM' },
|
|
{ label: 'Offers Accepted', value: k.offersAccepted, icon: 'check-circle', cls: 'i-green', trend: '+5%', dir: 'up', foot: `of ${k.offersSent} sent` }
|
|
];
|
|
const kpiCards2 = [
|
|
{ label: 'Time to Hire', value: k.timeToHire + ' days', icon: 'clock', cls: 'i-teal', trend: '-3 days', dir: 'up', foot: 'faster than target' },
|
|
{ label: 'Time to Fill', value: k.timeToFill + ' days', icon: 'target', cls: 'i-amber', trend: '-2 days', dir: 'up', foot: '41 day average' },
|
|
{ label: 'Cost per Hire', value: DB.money(k.costPerHire), icon: 'dollar', cls: 'i-red', trend: '+4%', dir: 'down', foot: 'above budget' },
|
|
{ label: 'Closed Jobs', value: k.closedJobs + k.hires, icon: 'award', cls: 'i-purple', trend: '+15%', dir: 'up', foot: 'this quarter' }
|
|
];
|
|
|
|
function trendHtml(dir, txt) {
|
|
if (dir === 'flat') return `<span class="trend trend-flat">${txt}</span>`;
|
|
const cls = dir === 'up' ? 'trend-up' : 'trend-down';
|
|
const ic = dir === 'up' ? 'trending-up' : 'trending-down';
|
|
return `<span class="trend ${cls}">${UI.icon(ic)}${txt}</span>`;
|
|
}
|
|
const kpiHtml = arr => arr.map(c => `
|
|
<div class="kpi">
|
|
<div class="kpi-top">
|
|
<span class="kpi-label">${c.label}</span>
|
|
<span class="kpi-icn ${c.cls}">${UI.icon(c.icon)}</span>
|
|
</div>
|
|
<div class="kpi-value">${c.value}</div>
|
|
<div class="kpi-foot">${trendHtml(c.dir, c.trend)}<span class="kpi-foot-text">${c.foot}</span></div>
|
|
</div>`).join('');
|
|
|
|
// upcoming interviews
|
|
const upcoming = DB.interviews.filter(iv => iv.status === 'Scheduled').slice(0, 5);
|
|
const upcomingHtml = upcoming.length ? upcoming.map(iv => `
|
|
<div class="list-row" onclick="Router.go('interviews')" style="cursor:pointer">
|
|
${UI.avatar(iv.candidate, iv.candInitials, iv.color)}
|
|
<div class="lr-main">
|
|
<div class="lr-title">${iv.candidate}</div>
|
|
<div class="lr-sub">${iv.type} · ${iv.jobTitle}</div>
|
|
</div>
|
|
<div class="lr-right">
|
|
<div class="fw-600 text-sm">${DB.fmtShort(iv.when)}</div>
|
|
<div class="lr-sub">${iv.when.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}</div>
|
|
</div>
|
|
</div>`).join('') : '<div class="empty-state" style="padding:30px">No upcoming interviews</div>';
|
|
|
|
// recent applications
|
|
const recentApps = [...DB.candidates].sort((a, b) => b.applied - a.applied).slice(0, 5);
|
|
const recentHtml = recentApps.map(c => `
|
|
<div class="list-row" onclick="Candidates.openProfile('${c.id}')" style="cursor:pointer">
|
|
${UI.avatar(c.name, c.initials, c.color)}
|
|
<div class="lr-main">
|
|
<div class="lr-title">${c.name}</div>
|
|
<div class="lr-sub">${c.jobTitle}</div>
|
|
</div>
|
|
<div class="lr-right">${UI.scoreChip(c.aiScore)}</div>
|
|
</div>`).join('');
|
|
|
|
// activity feed
|
|
const activityHtml = DB.activity.slice(0, 8).map(a => `
|
|
<div class="list-row">
|
|
<span class="kpi-icn ${a.color}" style="width:36px;height:36px;border-radius:9px">${UI.icon(a.icon)}</span>
|
|
<div class="lr-main">
|
|
<div class="lr-sub" style="color:var(--text-2);font-size:13px">${a.html}</div>
|
|
<div class="lr-sub">${DB.relTime(a.time)}</div>
|
|
</div>
|
|
</div>`).join('');
|
|
|
|
// recruiter performance
|
|
const topRecs = [...DB.recruiters].sort((a, b) => b.hires - a.hires).slice(0, 5);
|
|
const recPerf = topRecs.map(r => `
|
|
<div class="list-row">
|
|
${UI.avatar(r.name, r.initials, r.color)}
|
|
<div class="lr-main">
|
|
<div class="lr-title">${r.name}</div>
|
|
<div class="lr-sub">${r.openReqs} open reqs · ${r.avgTimeToHire}d avg</div>
|
|
</div>
|
|
<div class="lr-right"><div class="fw-600">${r.hires}</div><div class="lr-sub">hires</div></div>
|
|
</div>`).join('');
|
|
|
|
const html = `
|
|
<div class="page">
|
|
<div class="page-head">
|
|
<div>
|
|
<h1 class="page-title">Good morning, Asfand 👋</h1>
|
|
<p class="page-sub">Here's what's happening with your hiring today — Thursday, July 9, 2026</p>
|
|
</div>
|
|
<div class="page-head-actions">
|
|
<button class="btn btn-secondary" onclick="Router.go('reports')">${UI.icon('download')} Export</button>
|
|
<button class="btn btn-primary" onclick="Jobs.openCreate()">${UI.icon('plus')} Create Job</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid g-kpi">${kpiHtml(kpiCards)}</div>
|
|
<div class="grid g-kpi mt-18">${kpiHtml(kpiCards2)}</div>
|
|
|
|
<div class="grid g-2-1 mt-18">
|
|
<div class="card">
|
|
<div class="card-head">
|
|
<div><h3>Hiring Trend</h3><span class="ch-sub">Hires vs applications over the last 7 months</span></div>
|
|
<div class="pill-tabs"><span class="pill-tab active">7M</span><span class="pill-tab">1Y</span></div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="chart-wrap"><canvas id="chartTrend" height="280"></canvas></div>
|
|
${Charts.legend([{ label: 'Applications', color: Charts.PALETTE[4] }, { label: 'Hires', color: Charts.PALETTE[0] }])}
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head"><div><h3>Candidate Pipeline</h3><span class="ch-sub">Active by stage</span></div></div>
|
|
<div class="card-body">
|
|
<div class="chart-wrap"><canvas id="chartPipeline" height="280"></canvas></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid g-2-1 mt-18">
|
|
<div class="card">
|
|
<div class="card-head"><div><h3>Upcoming Interviews</h3><span class="ch-sub">Next scheduled sessions</span></div>
|
|
<button class="btn btn-ghost btn-sm" onclick="Router.go('interviews')">View all</button></div>
|
|
<div class="card-body"><div class="list-tight">${upcomingHtml}</div></div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head"><div><h3>Source Analytics</h3><span class="ch-sub">Where candidates come from</span></div></div>
|
|
<div class="card-body"><div class="chart-wrap"><canvas id="chartSource" height="240"></canvas></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid g-3 mt-18">
|
|
<div class="card">
|
|
<div class="card-head"><div><h3>Recent Applications</h3></div>
|
|
<button class="btn btn-ghost btn-sm" onclick="Router.go('candidates')">View all</button></div>
|
|
<div class="card-body"><div class="list-tight">${recentHtml}</div></div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head"><div><h3>Recruiter Performance</h3></div></div>
|
|
<div class="card-body"><div class="list-tight">${recPerf}</div></div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head"><div><h3>Recent Activity</h3></div></div>
|
|
<div class="card-body" style="max-height:360px;overflow-y:auto"><div class="list-tight">${activityHtml}</div></div>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
|
|
return {
|
|
html,
|
|
onMount() {
|
|
const a = DB.analytics;
|
|
Charts.line(document.getElementById('chartTrend'), {
|
|
labels: a.hiringTrend.labels, area: true,
|
|
datasets: [
|
|
{ label: 'Applications', data: a.hiringTrend.applications, color: Charts.PALETTE[4] },
|
|
{ label: 'Hires', data: a.hiringTrend.hires, color: Charts.PALETTE[0] }
|
|
]
|
|
});
|
|
Charts.horizontalBar(document.getElementById('chartPipeline'), {
|
|
labels: a.pipeline.map(p => p.stage), data: a.pipeline.map(p => p.count),
|
|
colors: Charts.PALETTE
|
|
});
|
|
Charts.bar(document.getElementById('chartSource'), {
|
|
labels: a.sources.map(s => s.source), data: a.sources.map(s => s.count)
|
|
});
|
|
}
|
|
};
|
|
};
|