HR-ATS-Portal/js/jobboard.js

173 lines
11 KiB
JavaScript

/* ============================================================
jobboard.js — Job Posting Center: publish + track performance
============================================================ */
window.Views = window.Views || {};
window.JobBoard = {};
Views.jobboard = function () {
const totals = DB.publishings.reduce((a, p) => ({ views: a.views + p.views, clicks: a.clicks + p.clicks, apps: a.apps + p.apps }), { views: 0, clicks: 0, apps: 0 });
const conv = totals.views ? ((totals.apps / totals.views) * 100).toFixed(1) : 0;
const statCard = (label, val, icn, cls, sub) => `<div class="kpi"><div class="kpi-top"><span class="kpi-label">${label}</span><span class="kpi-icn ${cls}">${UI.icon(icn)}</span></div><div class="kpi-value">${val}</div><div class="kpi-foot"><span class="kpi-foot-text">${sub}</span></div></div>`;
// per-platform aggregate
const platAgg = {};
DB.publishings.forEach(p => {
if (!platAgg[p.platform]) platAgg[p.platform] = { views: 0, clicks: 0, apps: 0, jobs: 0 };
platAgg[p.platform].views += p.views; platAgg[p.platform].clicks += p.clicks; platAgg[p.platform].apps += p.apps; platAgg[p.platform].jobs++;
});
const platRows = Object.entries(platAgg).sort((a, b) => b[1].apps - a[1].apps);
// publishing table
const table = UI.dataTable({
pageSize: 8,
rows: DB.publishings,
columns: [
{ key: 'jobTitle', label: 'Job', sortable: true, render: p => `<div class="cell-primary">${p.jobTitle}</div><div class="cell-sub">${p.jobId}</div>` },
{ key: 'platform', label: 'Platform', sortable: true, render: p => { const pl = DB.publishPlatforms.find(x => x.name === p.platform) || {}; return `<span class="flex items-center gap-8"><span class="platform-logo" style="width:26px;height:26px;background:${pl.color || '#888'}">${UI.icon(pl.icon || 'briefcase')}</span>${p.platform}</span>`; } },
{ key: 'status', label: 'Status', sortable: true, render: p => UI.badge(p.status, p.status === 'Live' ? 'b-green' : p.status === 'Paused' ? 'b-amber' : 'b-blue') },
{ key: 'views', label: 'Views', sortable: true, align: 'right', render: p => p.views.toLocaleString() },
{ key: 'clicks', label: 'Clicks', sortable: true, align: 'right', render: p => p.clicks.toLocaleString() },
{ key: 'apps', label: 'Applications', sortable: true, align: 'right', render: p => `<b>${p.apps}</b>` },
{ key: '_conv', label: 'Conversion', sortable: true, sortValue: p => p.apps / p.views, render: p => `<span class="badge b-indigo badge-plain">${((p.apps / p.views) * 100).toFixed(1)}%</span>` },
{ key: '_a', label: '', align: 'right', render: p => `<button class="act-btn" data-tip="Manage" onclick="UI.toast('Managing ${p.platform} posting','info')">${UI.icon('external')}</button>` }
]
});
const html = `
<div class="page">
<div class="page-head">
<div><h1 class="page-title">Job Board</h1><p class="page-sub">Publish requisitions across channels and track performance</p></div>
<div class="page-head-actions">
<button class="btn btn-secondary" onclick="Router.go('analytics')">${UI.icon('trending-up')} Analytics</button>
<button class="btn btn-primary" onclick="JobBoard.publishFlow()">${UI.icon('send')} Publish a Job</button>
</div>
</div>
<div class="grid g-kpi mb-18">
${statCard('Total Views', totals.views.toLocaleString(), 'eye', 'i-blue', 'across all platforms')}
${statCard('Total Clicks', totals.clicks.toLocaleString(), 'target', 'i-purple', ((totals.clicks / totals.views) * 100).toFixed(1) + '% CTR')}
${statCard('Applications', totals.apps.toLocaleString(), 'users', 'i-green', 'from job boards')}
${statCard('Conversion Rate', conv + '%', 'trending-up', 'i-teal', 'view → application')}
</div>
<div class="grid g-2-1 mb-18">
<div class="card">
<div class="card-head"><div><h3>Platform Performance</h3><span class="ch-sub">Applications by channel</span></div></div>
<div class="card-body"><div class="chart-wrap"><canvas id="jbChart" height="300"></canvas></div></div>
</div>
<div class="card">
<div class="card-head"><div><h3>Connected Platforms</h3></div></div>
<div class="card-body"><div class="list-tight">
${DB.publishPlatforms.map(p => `<div class="list-row">
<span class="platform-logo" style="background:${p.color}">${UI.icon(p.icon)}</span>
<div class="lr-main"><div class="lr-title">${p.name}</div><div class="lr-sub">${p.cost === 'Free' ? 'Free posting' : 'Paid · ' + p.cost}</div></div>
${p.connected ? UI.badge('Connected', 'b-green') : `<button class="btn btn-secondary btn-sm" onclick="UI.toast('Connecting ${p.name}…','info')">Connect</button>`}
</div>`).join('')}
</div></div>
</div>
</div>
<div class="card">
<div class="card-head"><div><h3>Active Postings</h3><span class="ch-sub">${DB.publishings.length} live postings across ${platRows.length} platforms</span></div>
<button class="btn btn-secondary btn-sm" onclick="UI.toast('Performance report exported','success')">${UI.icon('download')} Export</button></div>
${table.html}
</div>
</div>`;
return {
html,
onMount() {
table.mount();
Charts.horizontalBar(document.getElementById('jbChart'), {
labels: platRows.map(p => p[0]), data: platRows.map(p => p[1].apps)
});
}
};
};
// ---------------- Publish flow (stepper modal) ----------------
JobBoard.publishFlow = function (jobId) {
const state = { step: 1, jobId: jobId || DB.jobs.filter(j => j.status === 'Open')[0].id, platforms: ['Career Portal'] };
JobBoard._state = state;
JobBoard._renderFlow();
};
JobBoard._renderFlow = function () {
const state = JobBoard._state;
const steps = ['Select Job', 'Approval', 'Platforms', 'Publish'];
const stepper = `<div class="stepper">${steps.map((s, i) => {
const n = i + 1;
const cls = n < state.step ? 'done' : n === state.step ? 'active' : '';
return `<div class="step ${cls}"><div class="step-num">${n < state.step ? '✓' : n}</div><div class="step-label">${s}</div></div>${i < steps.length - 1 ? `<div class="step-line ${n < state.step ? 'done' : ''}"></div>` : ''}`;
}).join('')}</div>`;
let body = stepper;
if (state.step === 1) {
const opts = DB.jobs.filter(j => j.status !== 'Draft').map(j => `<option value="${j.id}" ${j.id === state.jobId ? 'selected' : ''}>${j.title} · ${j.id}</option>`).join('');
const job = DB.getJob(state.jobId);
body += `<div class="form-field"><label>Select requisition to publish</label><select id="pubJob">${opts}</select></div>
<div class="card" style="box-shadow:none;background:var(--bg-sunken);margin-top:16px"><div class="card-body">
<div class="flex items-center gap-12"><span class="kpi-icn i-indigo" style="width:44px;height:44px;border-radius:12px">${UI.icon('briefcase')}</span>
<div><div class="fw-600">${job.title}</div><div class="cell-sub">${job.department} · ${job.location} · ${job.type}</div></div></div>
</div></div>`;
} else if (state.step === 2) {
body += `<div class="card" style="box-shadow:none;background:var(--bg-sunken)"><div class="card-body">
<div class="flex items-center gap-12" style="margin-bottom:14px"><span class="kpi-icn i-green" style="width:44px;height:44px;border-radius:12px">${UI.icon('check-circle')}</span>
<div><div class="fw-600">Approval granted</div><div class="cell-sub">Approved by Department Head · Budget confirmed</div></div></div>
<div class="setting-row" style="padding:10px 0"><div class="setting-info"><h4>Hiring Manager sign-off</h4></div>${UI.badge('Approved', 'b-green')}</div>
<div class="setting-row" style="padding:10px 0"><div class="setting-info"><h4>Finance budget approval</h4></div>${UI.badge('Approved', 'b-green')}</div>
<div class="setting-row" style="padding:10px 0;border:none"><div class="setting-info"><h4>Compliance review</h4></div>${UI.badge('Approved', 'b-green')}</div>
</div></div>`;
} else if (state.step === 3) {
body += `<p class="text-muted" style="margin-bottom:14px">Select the platforms to publish this role to</p>
<div class="grid g-2" id="platGrid">
${DB.publishPlatforms.map(p => `<div class="platform-card ${state.platforms.includes(p.name) ? 'selected' : ''} ${!p.connected ? 'disabled' : ''}" data-plat="${p.name}" ${!p.connected ? 'style="opacity:.5;pointer-events:none"' : ''}>
<span class="platform-logo" style="background:${p.color}">${UI.icon(p.icon)}</span>
<div style="flex:1"><div class="fw-600">${p.name}</div><div class="cell-sub">${p.cost === 'Free' ? 'Free' : 'Paid · ' + p.cost}</div></div>
<span class="platform-check">${UI.icon('check')}</span>
</div>`).join('')}
</div>`;
} else if (state.step === 4) {
body += `<div style="text-align:center;padding:20px 0">
<div class="kpi-icn i-green" style="width:64px;height:64px;border-radius:18px;margin:0 auto 16px">${UI.icon('check-circle')}</div>
<h2 style="font-size:20px;margin-bottom:6px">Published Successfully</h2>
<p class="text-muted" style="margin-bottom:20px">${DB.getJob(state.jobId).title} is now live on ${state.platforms.length} platform${state.platforms.length > 1 ? 's' : ''}</p>
<div class="flex gap-8" style="justify-content:center;flex-wrap:wrap">
${state.platforms.map(p => { const pl = DB.publishPlatforms.find(x => x.name === p); return `<span class="badge b-green">${pl.name}</span>`; }).join('')}
</div>
</div>`;
}
let footer;
if (state.step === 4) footer = `<button class="btn btn-primary" onclick="UI.closeModal();Router.reload()">${UI.icon('check')} Done</button>`;
else footer = `<button class="btn btn-secondary" onclick="${state.step === 1 ? 'UI.closeModal()' : 'JobBoard._back()'}">${state.step === 1 ? 'Cancel' : 'Back'}</button>
<button class="btn btn-primary" onclick="JobBoard._next()">${state.step === 3 ? UI.icon('send') + ' Publish' : 'Continue'} </button>`;
UI.modal({ title: 'Publish Job', subtitle: 'Distribute this requisition to job boards', body, footer, size: 'modal-lg' });
if (state.step === 3) {
document.querySelectorAll('#platGrid .platform-card').forEach(card => card.onclick = () => {
const name = card.dataset.plat;
const idx = state.platforms.indexOf(name);
if (idx > -1) state.platforms.splice(idx, 1); else state.platforms.push(name);
card.classList.toggle('selected');
});
}
};
JobBoard._next = function () {
const state = JobBoard._state;
if (state.step === 1) { const sel = document.getElementById('pubJob'); if (sel) state.jobId = sel.value; }
if (state.step === 3 && !state.platforms.length) { UI.toast('Select at least one platform', 'warning'); return; }
state.step++;
if (state.step === 4) {
// create publishing records
const job = DB.getJob(state.jobId);
state.platforms.forEach(p => {
DB.publishings.unshift({ jobId: job.id, jobTitle: job.title, platform: p, status: 'Live', views: DB.int(0, 30), clicks: 0, apps: 0, published: new Date('2026-07-09') });
});
}
JobBoard._renderFlow();
};
JobBoard._back = function () { JobBoard._state.step--; JobBoard._renderFlow(); };