HR-ATS-Portal/js/offers.js

140 lines
8.5 KiB
JavaScript

/* ============================================================
offers.js — Offer management
============================================================ */
window.Views = window.Views || {};
window.Offers = {};
Views.offers = function () {
const filters = { q: '', status: '' };
let table;
const stats = {
sent: DB.offers.filter(o => o.status !== 'Draft').length,
accepted: DB.offers.filter(o => o.status === 'Accepted').length,
pending: DB.offers.filter(o => ['Sent', 'Negotiating'].includes(o.status)).length,
rate: Math.round(DB.offers.filter(o => o.status === 'Accepted').length / (DB.offers.filter(o => ['Accepted', 'Declined'].includes(o.status)).length || 1) * 100)
};
function apply() {
const rows = DB.offers.filter(o => {
if (filters.status && o.status !== filters.status) return false;
if (filters.q && !(o.candidate + o.jobTitle + o.recruiter).toLowerCase().includes(filters.q.toLowerCase())) return false;
return true;
});
table.update(rows);
}
table = UI.dataTable({
pageSize: 8,
rows: DB.offers,
columns: [
{ key: 'candidate', label: 'Candidate', sortable: true, render: o => `<div class="user-cell">${UI.avatar(o.candidate, o.initials, o.color)}<div><div class="cell-primary">${o.candidate}</div><div class="cell-sub">${o.jobTitle}</div></div></div>` },
{ key: 'department', label: 'Department', sortable: true },
{ key: 'base', label: 'Base Salary', sortable: true, align: 'right', render: o => `<b>${DB.money(o.base)}</b>` },
{ key: 'equity', label: 'Equity', render: o => `<span class="text-muted">${o.equity}</span>` },
{ key: 'bonus', label: 'Bonus', align: 'center', render: o => `<span class="text-muted">${o.bonus}</span>` },
{ key: 'sent', label: 'Sent', sortable: true, sortValue: o => o.sent.getTime(), render: o => `<span class="text-muted">${DB.fmtShort(o.sent)}</span>` },
{ key: 'status', label: 'Status', sortable: true, render: o => UI.badge(o.status) },
{ key: '_a', label: 'Actions', align: 'right', render: o => `
<div class="row-actions">
<button class="act-btn" data-tip="View" onclick="Offers.view('${o.id}')">${UI.icon('eye')}</button>
<button class="act-btn" data-tip="Resend" onclick="UI.toast('Offer resent to ${o.candidate}','info')">${UI.icon('send')}</button>
</div>` }
]
});
const statusOpts = ['<option value="">All Status</option>'].concat(['Sent', 'Accepted', 'Negotiating', 'Declined', 'Draft', 'Expired'].map(s => `<option>${s}</option>`)).join('');
const statCard = (label, val, icn, cls) => `<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>`;
const html = `
<div class="page">
<div class="page-head">
<div><h1 class="page-title">Offers</h1><p class="page-sub">Track offer letters and acceptance</p></div>
<div class="page-head-actions"><button class="btn btn-primary" onclick="Offers.create()">${UI.icon('plus')} Create Offer</button></div>
</div>
<div class="grid g-kpi mb-18">
${statCard('Offers Sent', stats.sent, 'send', 'i-indigo')}
${statCard('Accepted', stats.accepted, 'check-circle', 'i-green')}
${statCard('Awaiting Response', stats.pending, 'clock', 'i-amber')}
${statCard('Acceptance Rate', stats.rate + '%', 'trending-up', 'i-teal')}
</div>
<div class="card">
<div class="card-body" style="padding-bottom:0">
<div class="toolbar">
<div class="toolbar-search">${UI.icon('search')}<input id="ofSearch" placeholder="Search candidate or role…"/></div>
<select class="select" id="ofStatus">${statusOpts}</select>
</div>
</div>
${table.html}
</div>
</div>`;
return {
html,
onMount() {
table.mount();
const s = document.getElementById('ofSearch');
s.oninput = () => { filters.q = s.value; apply(); };
document.getElementById('ofStatus').onchange = e => { filters.status = e.target.value; apply(); };
}
};
};
Offers.view = function (id) {
const o = DB.offers.find(x => x.id === id);
const total = o.base + Math.round(o.base * parseInt(o.bonus) / 100);
const body = `
<div class="flex items-center gap-12 mb-18">${UI.avatar(o.candidate, o.initials, o.color, 'avatar-lg')}
<div><div class="ph-name" style="font-size:17px">${o.candidate}</div><div class="ph-role">${o.jobTitle} · ${o.department}</div></div>
<div style="margin-left:auto">${UI.badge(o.status)}</div></div>
<div class="card" style="box-shadow:none;background:var(--bg-sunken);margin-bottom:18px"><div class="card-body">
<div class="form-section-title" style="margin-top:0">Compensation Package</div>
<div class="info-grid">
<div class="info-item"><div class="il">Base Salary</div><div class="iv" style="font-size:18px">${DB.money(o.base)}</div></div>
<div class="info-item"><div class="il">Annual Bonus</div><div class="iv" style="font-size:18px">${o.bonus}</div></div>
<div class="info-item"><div class="il">Equity</div><div class="iv" style="font-size:18px">${o.equity}</div></div>
<div class="info-item"><div class="il">Est. Total Cash</div><div class="iv" style="font-size:18px;color:var(--success)">${DB.money(total)}</div></div>
</div>
</div></div>
<div class="info-grid">
<div class="info-item"><div class="il">Sent On</div><div class="iv">${DB.fmtDate(o.sent)}</div></div>
<div class="info-item"><div class="il">Expires</div><div class="iv">${DB.fmtDate(o.expires)}</div></div>
<div class="info-item"><div class="il">Recruiter</div><div class="iv">${o.recruiter}</div></div>
<div class="info-item"><div class="il">Offer ID</div><div class="iv mono">${o.id}</div></div>
</div>`;
const footer = `<button class="btn btn-secondary" onclick="UI.closeModal()">Close</button>
<button class="btn btn-secondary" onclick="UI.toast('Offer PDF downloaded','info')">${UI.icon('download')} Download</button>
<button class="btn btn-primary" onclick="UI.closeModal();UI.toast('Offer resent','success')">${UI.icon('send')} Resend Offer</button>`;
UI.modal({ title: 'Offer Details', subtitle: o.id, body, footer, size: 'modal-lg' });
};
Offers.create = function () {
const opt = arr => arr.map(o => `<option>${o}</option>`).join('');
const body = `<form id="offerForm" novalidate><div class="form-grid">
<div class="form-field col-span-2"><label>Candidate <span class="req">*</span></label><select name="candidate">${opt(DB.candidates.filter(c => ['Interview', 'Offer'].includes(c.stage)).map(c => c.name))}</select></div>
<div class="form-field"><label>Base Salary ($) <span class="req">*</span></label><input name="base" type="number" placeholder="140000"/><span class="field-error">Required</span></div>
<div class="form-field"><label>Annual Bonus (%)</label><input name="bonus" type="number" value="10"/></div>
<div class="form-field"><label>Equity (RSU)</label><input name="equity" placeholder="20k RSU"/></div>
<div class="form-field"><label>Expiration Date</label><input name="expires" type="date"/></div>
<div class="form-field col-span-2"><label>Notes</label><textarea name="notes" placeholder="Additional details for the offer…"></textarea></div>
</div></form>`;
const footer = `<button class="btn btn-secondary" onclick="UI.closeModal()">Cancel</button>
<button class="btn btn-primary" onclick="Offers._save()">${UI.icon('send')} Send Offer</button>`;
UI.modal({ title: 'Create Offer', subtitle: 'Generate and send an offer letter', body, footer });
};
Offers._save = function () {
const form = document.getElementById('offerForm');
UI.clearErrors(form);
const f = Object.fromEntries(new FormData(form));
if (!f.base || +f.base <= 0) { UI.fieldError(form.querySelector('[name=base]'), 'Required'); UI.toast('Enter a base salary', 'error'); return; }
const cand = DB.candidates.find(c => c.name === f.candidate) || DB.candidates[0];
DB.offers.unshift({
id: 'OFR-' + (9001 + DB.offers.length), candidate: cand.name, candidateId: cand.id, initials: cand.initials, color: cand.color,
jobTitle: cand.jobTitle, department: cand.department, status: 'Sent', base: +f.base, equity: f.equity || '10k RSU',
bonus: (f.bonus || 10) + '%', sent: new Date('2026-07-09'), expires: f.expires ? new Date(f.expires) : new Date('2026-07-23'), recruiter: cand.recruiter
});
UI.closeModal();
UI.toast('Offer sent successfully', 'success');
Router.reload();
};