442 lines
32 KiB
JavaScript
442 lines
32 KiB
JavaScript
/* ============================================================
|
||
candidates.js — Candidate list, filters, profile modal w/ tabs
|
||
============================================================ */
|
||
window.Views = window.Views || {};
|
||
window.Candidates = {};
|
||
|
||
Views.candidates = function () {
|
||
const f = { q: '', job: '', skill: '', dept: '', location: '', exp: '', edu: '', recruiter: '', manager: '', source: '', ats: '', stage: '', interview: '', notice: '', availability: '' };
|
||
const selected = new Set();
|
||
let sortMode = 'relevance';
|
||
let table;
|
||
Candidates._selected = selected;
|
||
|
||
function relevance(c) {
|
||
// composite relevance: ATS + matched-skill ratio + recency
|
||
const req = (DB.getJob(c.jobId) || {}).skills || [];
|
||
const skillRatio = req.length ? c.matchedSkills.length / req.length : 0.5;
|
||
const recency = 1 - Math.min(1, (new Date('2026-07-09') - c.applied) / (90 * 864e5));
|
||
return Math.round(c.aiScore * 0.7 + skillRatio * 20 + recency * 10);
|
||
}
|
||
|
||
function apply() {
|
||
let rows = DB.candidates.filter(c => {
|
||
if (f.job && c.jobTitle !== f.job) return false;
|
||
if (f.skill && !c.skills.includes(f.skill)) return false;
|
||
if (f.dept && c.department !== f.dept) return false;
|
||
if (f.location && c.location !== f.location) return false;
|
||
if (f.exp === '0-2' && c.experience > 2) return false;
|
||
if (f.exp === '3-5' && (c.experience < 3 || c.experience > 5)) return false;
|
||
if (f.exp === '6-9' && (c.experience < 6 || c.experience > 9)) return false;
|
||
if (f.exp === '10+' && c.experience < 10) return false;
|
||
if (f.edu && c.education !== f.edu) return false;
|
||
if (f.recruiter && c.recruiter !== f.recruiter) return false;
|
||
if (f.manager) { const job = DB.getJob(c.jobId); if (!job || job.manager !== f.manager) return false; }
|
||
if (f.source && c.source !== f.source) return false;
|
||
if (f.ats === '85+' && c.aiScore < 85) return false;
|
||
if (f.ats === '70-84' && (c.aiScore < 70 || c.aiScore > 84)) return false;
|
||
if (f.ats === '<70' && c.aiScore >= 70) return false;
|
||
if (f.stage && c.stage !== f.stage) return false;
|
||
if (f.interview && c.interviewStatus !== f.interview) return false;
|
||
if (f.notice && c.noticePeriod !== f.notice) return false;
|
||
if (f.availability && c.availability !== f.availability) return false;
|
||
if (f.q) { const q = f.q.toLowerCase(); if (!(c.name + c.email + c.jobTitle + c.currentCompany + c.recruiter + c.skills.join(' ')).toLowerCase().includes(q)) return false; }
|
||
return true;
|
||
});
|
||
if (sortMode === 'relevance') rows = [...rows].sort((a, b) => relevance(b) - relevance(a));
|
||
else if (sortMode === 'ats') rows = [...rows].sort((a, b) => b.aiScore - a.aiScore);
|
||
else if (sortMode === 'recent') rows = [...rows].sort((a, b) => b.applied - a.applied);
|
||
else if (sortMode === 'name') rows = [...rows].sort((a, b) => a.name.localeCompare(b.name));
|
||
table.update(rows);
|
||
updateBulkBar();
|
||
const cnt = document.getElementById('canResultCount');
|
||
if (cnt) cnt.textContent = rows.length + ' candidate' + (rows.length === 1 ? '' : 's');
|
||
}
|
||
|
||
function updateBulkBar() {
|
||
const bar = document.getElementById('bulkBar');
|
||
if (!bar) return;
|
||
if (selected.size) { bar.style.display = 'flex'; document.getElementById('bulkCount').textContent = selected.size + ' selected'; }
|
||
else bar.style.display = 'none';
|
||
}
|
||
|
||
table = UI.dataTable({
|
||
pageSize: 10,
|
||
rows: DB.candidates,
|
||
columns: [
|
||
{ key: '_sel', label: '', render: c => `<span class="checkbox ${selected.has(c.id) ? 'on' : ''}" data-sel="${c.id}">${UI.icon('check')}</span>` },
|
||
{ key: 'name', label: 'Candidate', sortable: true, render: c => `<div class="user-cell">${UI.avatar(c.name, c.initials, c.color)}<div><div class="cell-primary">${c.name} ${c.favorite ? '<span class="star-btn on" style="display:inline">' + UI.icon('star') + '</span>' : ''}</div><div class="cell-sub">${c.currentTitle} · ${c.location}</div></div></div>` },
|
||
{ key: 'jobTitle', label: 'Applied Job', sortable: true, render: c => `<div class="text-sm">${c.jobTitle}</div><div class="cell-sub">${c.department}</div>` },
|
||
{ key: 'experience', label: 'Exp', sortable: true, align: 'center', render: c => `<b>${c.experience}</b>y` },
|
||
{ key: '_rel', label: 'Relevance', sortable: true, align: 'center', sortValue: c => relevance(c), render: c => `<span class="badge ${DB.atsRecommendationClass(c.recommendation)} badge-plain">${relevance(c)}%</span>` },
|
||
{ key: 'stage', label: 'Stage', sortable: true, render: c => UI.badge(c.stage) },
|
||
{ key: 'aiScore', label: 'ATS', sortable: true, align: 'center', render: c => `<span style="cursor:pointer" onclick="Candidates.atsMatch('${c.id}')">${UI.scoreChip(c.aiScore)}</span>` },
|
||
{ key: 'availability', label: 'Availability', render: c => `<span class="text-sm">${c.availability}</span><div class="cell-sub">${c.noticePeriod} notice</div>` },
|
||
{ key: '_a', label: 'Actions', align: 'right', render: c => `
|
||
<div class="row-actions">
|
||
<button class="act-btn star-btn ${c.favorite ? 'on' : ''}" data-tip="Favorite" data-fav="${c.id}">${UI.icon('star')}</button>
|
||
<button class="act-btn" data-tip="ATS Match" onclick="Candidates.atsMatch('${c.id}')">${UI.icon('target')}</button>
|
||
<button class="act-btn" data-tip="Profile" onclick="Candidates.openProfile('${c.id}')">${UI.icon('eye')}</button>
|
||
<button class="act-btn" data-tip="Advance" onclick="Candidates.advance('${c.id}')">${UI.icon('check')}</button>
|
||
</div>` }
|
||
],
|
||
onRender(el) {
|
||
el.querySelectorAll('[data-sel]').forEach(chk => chk.onclick = () => {
|
||
const id = chk.dataset.sel;
|
||
if (selected.has(id)) selected.delete(id); else selected.add(id);
|
||
chk.classList.toggle('on'); updateBulkBar();
|
||
});
|
||
el.querySelectorAll('[data-fav]').forEach(st => st.onclick = () => {
|
||
const c = DB.getCandidate(st.dataset.fav); c.favorite = !c.favorite;
|
||
st.classList.toggle('on'); UI.toast(c.favorite ? c.name + ' added to favorites' : 'Removed from favorites', 'success');
|
||
});
|
||
}
|
||
});
|
||
|
||
const optList = (arr, label) => [`<option value="">${label}</option>`].concat(arr.map(o => `<option>${o}</option>`)).join('');
|
||
const jobTitles = [...new Set(DB.candidates.map(c => c.jobTitle))];
|
||
|
||
const filterPanel = `
|
||
<div class="filter-panel" id="filterPanel" style="display:none;padding:16px 0;border-top:1px solid var(--border);margin-top:12px">
|
||
<div class="form-field"><label>Job</label><select data-f="job">${optList(jobTitles, 'Any Job')}</select></div>
|
||
<div class="form-field"><label>Skill</label><select data-f="skill">${optList(DB.skillsPool, 'Any Skill')}</select></div>
|
||
<div class="form-field"><label>Department</label><select data-f="dept">${optList(DB.departments, 'Any Dept')}</select></div>
|
||
<div class="form-field"><label>Location</label><select data-f="location">${optList(DB.locations, 'Any Location')}</select></div>
|
||
<div class="form-field"><label>Experience</label><select data-f="exp">${optList(['0-2', '3-5', '6-9', '10+'], 'Any Exp')}</select></div>
|
||
<div class="form-field"><label>Education</label><select data-f="edu">${optList(DB.educationLevels, 'Any')}</select></div>
|
||
<div class="form-field"><label>Recruiter</label><select data-f="recruiter">${optList(DB.recruiters.map(r => r.name), 'Any Recruiter')}</select></div>
|
||
<div class="form-field"><label>Hiring Manager</label><select data-f="manager">${optList(DB.managers.map(m => m.name), 'Any Manager')}</select></div>
|
||
<div class="form-field"><label>Source</label><select data-f="source">${optList(DB.sources, 'Any Source')}</select></div>
|
||
<div class="form-field"><label>ATS Score</label><select data-f="ats">${optList(['85+', '70-84', '<70'], 'Any Score')}</select></div>
|
||
<div class="form-field"><label>Pipeline Stage</label><select data-f="stage">${optList(DB.stages, 'Any Stage')}</select></div>
|
||
<div class="form-field"><label>Interview Status</label><select data-f="interview">${optList(['Not Scheduled', 'Scheduled', 'Completed'], 'Any')}</select></div>
|
||
<div class="form-field"><label>Notice Period</label><select data-f="notice">${optList(['Immediate', '2 weeks', '1 month', '2 months', '3 months'], 'Any')}</select></div>
|
||
<div class="form-field"><label>Availability</label><select data-f="availability">${optList(['Immediate', '2 weeks', '1 month', 'Passive'], 'Any')}</select></div>
|
||
</div>`;
|
||
|
||
// recently viewed strip
|
||
const rv = DB.recentlyViewed.slice(0, 6).map(id => DB.getCandidate(id)).filter(Boolean);
|
||
const rvHtml = rv.length ? `<div class="flex items-center gap-8" style="margin-bottom:14px;flex-wrap:wrap">
|
||
<span class="text-muted text-sm fw-600">Recently viewed:</span>
|
||
${rv.map(c => `<button class="prompt-chip" style="padding:5px 10px" onclick="Candidates.openProfile('${c.id}')">${UI.avatar(c.name, c.initials, c.color)} ${c.name.split(' ')[0]}</button>`).join('')}
|
||
</div>` : '';
|
||
|
||
const html = `
|
||
<div class="page">
|
||
<div class="page-head">
|
||
<div><h1 class="page-title">Candidates</h1><p class="page-sub"><span id="canResultCount">${DB.candidates.length} candidates</span> · ranked by AI relevance</p></div>
|
||
<div class="page-head-actions">
|
||
<button class="btn btn-secondary" onclick="UI.toast('Search saved','success')">${UI.icon('bookmark')} Save Search</button>
|
||
<button class="btn btn-secondary" onclick="UI.toast('Candidates exported','success')">${UI.icon('download')} Export</button>
|
||
<button class="btn btn-primary" onclick="Candidates.openAdd()">${UI.icon('plus')} Add Candidate</button>
|
||
</div>
|
||
</div>
|
||
${rvHtml}
|
||
<div class="bulk-bar" id="bulkBar" style="display:none">
|
||
<span class="checkbox on">${UI.icon('check')}</span>
|
||
<span class="fw-600" id="bulkCount">0 selected</span>
|
||
<div style="flex:1"></div>
|
||
<button class="btn btn-sm" onclick="Candidates.bulk('email')">${UI.icon('mail')} Bulk Email</button>
|
||
<button class="btn btn-sm" onclick="Candidates.bulk('assign')">${UI.icon('users')} Assign</button>
|
||
<button class="btn btn-sm" onclick="Candidates.bulk('advance')">${UI.icon('check')} Advance</button>
|
||
<button class="btn btn-sm" onclick="Candidates.bulk('reject')">${UI.icon('x')} Reject</button>
|
||
<button class="btn btn-sm" onclick="Candidates.bulkClear()">${UI.icon('x')} Clear</button>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-body" style="padding-bottom:0">
|
||
<div class="toolbar">
|
||
<div class="toolbar-search">${UI.icon('search')}<input id="canSearch" placeholder="Search name, skill, company…"/></div>
|
||
<button class="btn btn-secondary" id="filterToggle">${UI.icon('filter')} Filters</button>
|
||
<div class="spacer"></div>
|
||
<label class="text-muted text-sm">Sort:</label>
|
||
<select class="select" id="canSort">
|
||
<option value="relevance">AI Relevance</option>
|
||
<option value="ats">ATS Score</option>
|
||
<option value="recent">Most Recent</option>
|
||
<option value="name">Name A–Z</option>
|
||
</select>
|
||
</div>
|
||
${filterPanel}
|
||
</div>
|
||
${table.html}
|
||
</div>
|
||
</div>`;
|
||
|
||
return {
|
||
html,
|
||
onMount() {
|
||
table.mount();
|
||
apply();
|
||
const s = document.getElementById('canSearch');
|
||
s.oninput = () => { f.q = s.value; apply(); };
|
||
document.getElementById('canSort').onchange = e => { sortMode = e.target.value; apply(); };
|
||
document.getElementById('filterToggle').onclick = () => {
|
||
const p = document.getElementById('filterPanel');
|
||
p.style.display = p.style.display === 'none' ? 'grid' : 'none';
|
||
};
|
||
document.querySelectorAll('#filterPanel [data-f]').forEach(sel => sel.onchange = e => { f[e.target.dataset.f] = e.target.value; apply(); });
|
||
}
|
||
};
|
||
};
|
||
|
||
// ---------------- Bulk actions ----------------
|
||
Candidates.bulk = function (action) {
|
||
const ids = [...Candidates._selected];
|
||
if (!ids.length) return;
|
||
if (action === 'email') UI.toast(`Bulk email drafted to ${ids.length} candidates`, 'success');
|
||
else if (action === 'assign') {
|
||
const opts = DB.recruiters.map(r => `<option>${r.name}</option>`).join('');
|
||
UI.modal({ title: 'Bulk Assign Recruiter', subtitle: ids.length + ' candidates',
|
||
body: `<div class="form-field"><label>Assign to</label><select id="bulkRec">${opts}</select></div>`,
|
||
footer: `<button class="btn btn-secondary" onclick="UI.closeModal()">Cancel</button><button class="btn btn-primary" onclick="Candidates._bulkAssign()">Assign</button>` });
|
||
return;
|
||
}
|
||
else if (action === 'advance') { ids.forEach(id => Candidates._advanceSilent(id)); UI.toast(`${ids.length} candidates advanced`, 'success'); Router.reload(); return; }
|
||
else if (action === 'reject') { ids.forEach(id => { const c = DB.getCandidate(id); c.stage = 'Rejected'; c.status = 'Rejected'; }); UI.toast(`${ids.length} candidates rejected`, 'warning'); Router.reload(); return; }
|
||
Candidates.bulkClear();
|
||
};
|
||
Candidates._bulkAssign = function () {
|
||
const rec = document.getElementById('bulkRec').value;
|
||
[...Candidates._selected].forEach(id => { DB.getCandidate(id).recruiter = rec; });
|
||
UI.closeModal(); UI.toast('Recruiter assigned to selected candidates', 'success');
|
||
Candidates.bulkClear(); Router.reload();
|
||
};
|
||
Candidates.bulkClear = function () { Candidates._selected.clear(); Router.reload(); };
|
||
Candidates._advanceSilent = function (id) {
|
||
const c = DB.getCandidate(id);
|
||
const order = ['Applied', 'Screening', 'Assessment', 'Interview', 'Offer', 'Hired'];
|
||
const i = order.indexOf(c.stage);
|
||
if (i > -1 && i < order.length - 1) { c.stage = order[i + 1]; c.status = c.stage; }
|
||
};
|
||
|
||
// ---------------- ATS Match detail ----------------
|
||
Candidates.atsMatch = function (id) {
|
||
const c = DB.getCandidate(id);
|
||
const job = DB.getJob(c.jobId) || {};
|
||
const recCls = c.recommendation === 'Strong Match' ? 'recc-strong' : c.recommendation === 'Potential Match' ? 'recc-potential' : 'recc-weak';
|
||
const ringColor = c.aiScore >= 82 ? 'var(--success)' : c.aiScore >= 65 ? 'var(--warning)' : 'var(--danger)';
|
||
const sub = c.subScores;
|
||
const subRow = (label, val) => `<div class="flex items-center gap-12" style="margin-bottom:12px"><span style="width:110px;font-size:13px">${label}</span><div style="flex:1">${UI.pbar(val)}</div><b style="width:42px;text-align:right">${val}%</b></div>`;
|
||
|
||
const body = `
|
||
<div class="recc-banner ${recCls}">
|
||
<span class="recc-icn">${UI.icon(c.recommendation === 'Weak Match' ? 'x-circle' : 'check-circle')}</span>
|
||
<div style="flex:1"><div class="fw-600" style="font-size:15px">${c.recommendation}</div>
|
||
<div style="opacity:.85;font-size:13px">${c.name} for ${c.jobTitle}</div></div>
|
||
</div>
|
||
<div class="grid g-2" style="align-items:center;margin-bottom:20px">
|
||
<div style="text-align:center">
|
||
<div class="ats-ring" style="--pct:${c.aiScore};--c:${ringColor}"><div class="ats-val"><div class="ats-num">${c.aiScore}</div><div class="ats-lbl">ATS MATCH</div></div></div>
|
||
</div>
|
||
<div>
|
||
${subRow('Skills', sub.skills)}
|
||
${subRow('Experience', sub.experience)}
|
||
${subRow('Education', sub.education)}
|
||
${subRow('Keywords', sub.keywords)}
|
||
${subRow('Location', sub.location)}
|
||
${subRow('Salary', sub.salary)}
|
||
</div>
|
||
</div>
|
||
<div class="form-section-title" style="margin-top:0">Matched Skills (${c.matchedSkills.length})</div>
|
||
<div class="k-tags" style="margin-bottom:16px">${c.matchedSkills.length ? c.matchedSkills.map(s => `<span class="skill-pill skill-matched">${UI.icon('check')} ${s}</span>`).join('') : '<span class="text-muted">—</span>'}</div>
|
||
<div class="form-section-title" style="margin-top:0">Missing Skills (${c.missingSkills.length})</div>
|
||
<div class="k-tags">${c.missingSkills.length ? c.missingSkills.map(s => `<span class="skill-pill skill-missing">${UI.icon('x')} ${s}</span>`).join('') : '<span class="text-muted">None — full match</span>'}</div>
|
||
<div class="divider"></div>
|
||
<p class="text-muted text-sm">${UI.icon('sparkles')} Score computed from JD keywords, resume parsing, experience, education, location and salary alignment. Connect an AI model to refine with semantic matching.</p>`;
|
||
UI.modal({
|
||
title: 'ATS Match Analysis', subtitle: c.id + ' · ' + c.jobTitle, body, size: 'modal-lg',
|
||
footer: `<button class="btn btn-secondary" onclick="UI.closeModal()">Close</button><button class="btn btn-primary" onclick="UI.closeModal();Candidates.openProfile('${c.id}')">View Full Profile</button>`
|
||
});
|
||
};
|
||
|
||
Candidates.toggleFav = function (id, btn) {
|
||
const c = DB.getCandidate(id); c.favorite = !c.favorite;
|
||
if (btn) { btn.classList.toggle('on'); btn.innerHTML = UI.icon('star') + (c.favorite ? ' Favorited' : ' Favorite'); }
|
||
UI.toast(c.favorite ? c.name + ' added to favorites' : 'Removed from favorites', 'success');
|
||
};
|
||
|
||
Candidates.advance = function (id) {
|
||
const c = DB.getCandidate(id);
|
||
const order = ['Applied', 'Screening', 'Assessment', 'Interview', 'Offer', 'Hired'];
|
||
const i = order.indexOf(c.stage);
|
||
if (i === -1 || i >= order.length - 1) { UI.toast(c.name + ' cannot be advanced further', 'warning'); return; }
|
||
c.stage = order[i + 1]; c.status = c.stage;
|
||
UI.toast(`${c.name} moved to ${c.stage}`, 'success');
|
||
Router.reload();
|
||
};
|
||
|
||
// ---------------- Candidate profile w/ tabs ----------------
|
||
Candidates.openProfile = function (id) {
|
||
const c = DB.getCandidate(id);
|
||
// track recently viewed
|
||
const rvIdx = DB.recentlyViewed.indexOf(id);
|
||
if (rvIdx > -1) DB.recentlyViewed.splice(rvIdx, 1);
|
||
DB.recentlyViewed.unshift(id);
|
||
if (DB.recentlyViewed.length > 12) DB.recentlyViewed.pop();
|
||
const tabs = ['Overview', 'Resume', 'Timeline', 'Interview', 'Notes', 'Activity', 'Documents', 'Feedback'];
|
||
const body = `
|
||
<div class="profile-hero">
|
||
${UI.avatar(c.name, c.initials, c.color, 'avatar-lg')}
|
||
<div style="flex:1">
|
||
<div class="ph-name">${c.name}</div>
|
||
<div class="ph-role">${c.currentTitle} at ${c.currentCompany}</div>
|
||
<div class="ph-tags">${UI.badge(c.stage)} ${UI.badge(c.source, 'b-gray')}
|
||
<span class="badge b-plain b-indigo badge-plain">${c.experience} yrs exp</span></div>
|
||
</div>
|
||
<div style="text-align:center">${UI.scoreChip(c.aiScore)}<div class="cell-sub" style="margin-top:4px">AI Match</div></div>
|
||
</div>
|
||
<div class="tabs" id="canTabs" style="margin-top:22px">
|
||
${tabs.map((t, i) => `<div class="tab ${i === 0 ? 'active' : ''}" data-tab="${i}">${t}</div>`).join('')}
|
||
</div>
|
||
<div id="canPanes">
|
||
${Candidates._pane('Overview', c)}
|
||
${Candidates._pane('Resume', c)}
|
||
${Candidates._pane('Timeline', c)}
|
||
${Candidates._pane('Interview', c)}
|
||
${Candidates._pane('Notes', c)}
|
||
${Candidates._pane('Activity', c)}
|
||
${Candidates._pane('Documents', c)}
|
||
${Candidates._pane('Feedback', c)}
|
||
</div>`;
|
||
const footer = `<button class="btn btn-ghost star-btn ${c.favorite ? 'on' : ''}" style="margin-right:auto" onclick="Candidates.toggleFav('${c.id}',this)">${UI.icon('star')} ${c.favorite ? 'Favorited' : 'Favorite'}</button>
|
||
<button class="btn btn-secondary" onclick="Candidates.atsMatch('${c.id}')">${UI.icon('target')} ATS Match</button>
|
||
<button class="btn btn-secondary" onclick="UI.toast('Email drafted','info')">${UI.icon('mail')} Message</button>
|
||
<button class="btn btn-primary" onclick="Candidates.advance('${c.id}');UI.closeModal()">${UI.icon('check')} Advance Stage</button>`;
|
||
UI.modal({ title: 'Candidate Profile', subtitle: c.id, body, footer, size: 'modal-lg' });
|
||
|
||
const paneEls = document.querySelectorAll('#canPanes .tab-pane');
|
||
document.querySelectorAll('#canTabs .tab').forEach(tab => tab.onclick = () => {
|
||
document.querySelectorAll('#canTabs .tab').forEach(t => t.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
paneEls.forEach(p => p.classList.remove('active'));
|
||
paneEls[+tab.dataset.tab].classList.add('active');
|
||
});
|
||
};
|
||
|
||
Candidates._pane = function (name, c) {
|
||
const active = name === 'Overview' ? 'active' : '';
|
||
let content = '';
|
||
if (name === 'Overview') {
|
||
content = `
|
||
<div class="info-grid" style="margin-bottom:20px">
|
||
<div class="info-item"><div class="il">Email</div><div class="iv">${c.email}</div></div>
|
||
<div class="info-item"><div class="il">Phone</div><div class="iv">${c.phone}</div></div>
|
||
<div class="info-item"><div class="il">Location</div><div class="iv">${c.location}</div></div>
|
||
<div class="info-item"><div class="il">Applied For</div><div class="iv">${c.jobTitle}</div></div>
|
||
<div class="info-item"><div class="il">Current Company</div><div class="iv">${c.currentCompany}</div></div>
|
||
<div class="info-item"><div class="il">Experience</div><div class="iv">${c.experience} years</div></div>
|
||
<div class="info-item"><div class="il">Education</div><div class="iv">${c.education}</div></div>
|
||
<div class="info-item"><div class="il">Source</div><div class="iv">${c.source}</div></div>
|
||
<div class="info-item"><div class="il">Recruiter</div><div class="iv">${c.recruiter}</div></div>
|
||
<div class="info-item"><div class="il">Applied On</div><div class="iv">${DB.fmtDate(c.applied)}</div></div>
|
||
<div class="info-item"><div class="il">Expected Salary</div><div class="iv">${DB.moneyK(c.salary)}</div></div>
|
||
<div class="info-item"><div class="il">Rating</div><div class="iv">⭐ ${c.rating} / 5.0</div></div>
|
||
</div>
|
||
<div class="il" style="font-size:12px;color:var(--text-3);font-weight:600;text-transform:uppercase;margin-bottom:8px">Skills</div>
|
||
<div class="k-tags">${c.skills.map(s => `<span class="tag">${s}</span>`).join('')}</div>`;
|
||
} else if (name === 'Resume') {
|
||
content = `<div class="card" style="box-shadow:none;background:var(--bg-sunken)"><div class="card-body">
|
||
<h3 style="margin-bottom:4px">${c.name}</h3><p class="text-muted">${c.currentTitle} · ${c.location}</p>
|
||
<div class="divider"></div>
|
||
<div class="form-section-title" style="margin-top:0">Summary</div>
|
||
<p class="text-muted">Results-driven ${c.currentTitle.toLowerCase()} with ${c.experience} years of experience across ${c.department.toLowerCase()}. Passionate about building high-quality products and collaborating with cross-functional teams.</p>
|
||
<div class="form-section-title">Experience</div>
|
||
<div class="info-item"><div class="iv">${c.currentTitle} — ${c.currentCompany}</div><div class="il" style="text-transform:none">2021 – Present</div></div>
|
||
<div class="info-item" style="margin-top:10px"><div class="iv">Associate — ${DB.pick(DB.companies)}</div><div class="il" style="text-transform:none">2018 – 2021</div></div>
|
||
<div class="form-section-title">Education</div>
|
||
<div class="iv">${c.education}</div>
|
||
</div></div>
|
||
<button class="btn btn-secondary" style="margin-top:14px" onclick="UI.toast('Downloading resume.pdf','info')">${UI.icon('download')} Download PDF</button>`;
|
||
} else if (name === 'Timeline') {
|
||
const events = [
|
||
{ icon: 'user-plus', title: 'Application received', meta: DB.fmtDate(c.applied), desc: `Applied via ${c.source}` },
|
||
{ icon: 'star', title: 'AI screening completed', meta: '1 day later', desc: `Match score: ${c.aiScore}%` },
|
||
{ icon: 'phone', title: 'Recruiter screen', meta: '3 days later', desc: `Call with ${c.recruiter}` },
|
||
{ icon: 'calendar', title: 'Technical interview', meta: '1 week later', desc: 'Panel of 3 interviewers' },
|
||
{ icon: 'check', title: `Moved to ${c.stage}`, meta: 'Recently', desc: 'Current stage in pipeline' }
|
||
];
|
||
content = `<div class="timeline">${events.map(e => `
|
||
<div class="tl-item"><div class="tl-dot">${UI.icon(e.icon)}</div>
|
||
<div class="tl-title">${e.title}</div><div class="tl-meta">${e.meta}</div><div class="tl-desc">${e.desc}</div></div>`).join('')}</div>`;
|
||
} else if (name === 'Interview') {
|
||
const ivs = DB.interviews.filter(i => i.candidateId === c.id);
|
||
content = ivs.length ? `<div class="list-tight">${ivs.map(iv => `
|
||
<div class="list-row"><span class="kpi-icn i-blue" style="width:38px;height:38px;border-radius:10px">${UI.icon('calendar')}</span>
|
||
<div class="lr-main"><div class="lr-title">${iv.type}</div><div class="lr-sub">${DB.fmtDate(iv.when)} · ${iv.meeting}</div></div>
|
||
<div class="lr-right">${UI.badge(iv.status)}</div></div>`).join('')}</div>`
|
||
: `<div class="empty-state">${UI.icon('calendar')}<h3>No interviews scheduled</h3><p>Schedule an interview to get started.</p>
|
||
<button class="btn btn-primary btn-sm" style="margin-top:10px" onclick="UI.toast('Interview scheduler opened','info')">Schedule Interview</button></div>`;
|
||
} else if (name === 'Notes') {
|
||
content = `
|
||
<div class="form-field"><label>Add a note</label><textarea id="noteInput" placeholder="Write a private note about this candidate…"></textarea></div>
|
||
<button class="btn btn-primary btn-sm" style="margin:10px 0 18px" onclick="UI.toast('Note saved','success')">${UI.icon('plus')} Add Note</button>
|
||
<div class="list-tight">
|
||
<div class="list-row">${UI.avatar(c.recruiter)}<div class="lr-main"><div class="lr-title">${c.recruiter}</div><div class="lr-sub" style="color:var(--text-2)">Strong communication skills, great culture fit. Recommend advancing.</div><div class="lr-sub">2 days ago</div></div></div>
|
||
<div class="list-row">${UI.avatar('Asfand Ahmed', 'AA')}<div class="lr-main"><div class="lr-title">Asfand Ahmed</div><div class="lr-sub" style="color:var(--text-2)">Reviewed portfolio — impressive work. Schedule technical round.</div><div class="lr-sub">4 days ago</div></div></div>
|
||
</div>`;
|
||
} else if (name === 'Activity') {
|
||
content = `<div class="list-tight">
|
||
<div class="list-row"><span class="kpi-icn i-green" style="width:34px;height:34px;border-radius:9px">${UI.icon('eye')}</span><div class="lr-main"><div class="lr-sub" style="color:var(--text-2);font-size:13px">Profile viewed by ${c.recruiter}</div><div class="lr-sub">1h ago</div></div></div>
|
||
<div class="list-row"><span class="kpi-icn i-blue" style="width:34px;height:34px;border-radius:9px">${UI.icon('mail')}</span><div class="lr-main"><div class="lr-sub" style="color:var(--text-2);font-size:13px">Email sent: Interview invitation</div><div class="lr-sub">1 day ago</div></div></div>
|
||
<div class="list-row"><span class="kpi-icn i-amber" style="width:34px;height:34px;border-radius:9px">${UI.icon('star')}</span><div class="lr-main"><div class="lr-sub" style="color:var(--text-2);font-size:13px">Assessment score updated to ${c.aiScore}%</div><div class="lr-sub">2 days ago</div></div></div>
|
||
<div class="list-row"><span class="kpi-icn i-purple" style="width:34px;height:34px;border-radius:9px">${UI.icon('user-plus')}</span><div class="lr-main"><div class="lr-sub" style="color:var(--text-2);font-size:13px">Applied for ${c.jobTitle}</div><div class="lr-sub">${DB.fmtDate(c.applied)}</div></div></div>
|
||
</div>`;
|
||
} else if (name === 'Documents') {
|
||
const docs = [{ n: 'Resume.pdf', s: '284 KB' }, { n: 'Cover_Letter.pdf', s: '112 KB' }, { n: 'Portfolio.pdf', s: '4.2 MB' }, { n: 'References.docx', s: '48 KB' }];
|
||
content = `<div class="list-tight">${docs.map(d => `
|
||
<div class="list-row"><span class="kpi-icn i-red" style="width:38px;height:38px;border-radius:10px">${UI.icon('file')}</span>
|
||
<div class="lr-main"><div class="lr-title">${d.n}</div><div class="lr-sub">${d.s}</div></div>
|
||
<button class="act-btn" onclick="UI.toast('Downloading ${d.n}','info')">${UI.icon('download')}</button></div>`).join('')}</div>`;
|
||
} else if (name === 'Feedback') {
|
||
const scores = ['Strong Hire', 'Hire', 'Lean Hire'];
|
||
content = `<div class="list-tight">
|
||
${[0, 1, 2].map(i => `<div class="list-row">${UI.avatar(DB.recruiters[i].name, DB.recruiters[i].initials, DB.recruiters[i].color)}
|
||
<div class="lr-main"><div class="lr-title">${DB.recruiters[i].name}</div><div class="lr-sub" style="color:var(--text-2)">${['Excellent technical depth and clear communication.', 'Good problem solving, would benefit from more system design exposure.', 'Solid candidate, positive team energy.'][i]}</div></div>
|
||
<div class="lr-right">${UI.badge(scores[i])}</div></div>`).join('')}
|
||
</div>
|
||
<button class="btn btn-primary btn-sm" style="margin-top:14px" onclick="UI.toast('Scorecard form opened','info')">${UI.icon('plus')} Submit Scorecard</button>`;
|
||
}
|
||
return `<div class="tab-pane ${active}">${content}</div>`;
|
||
};
|
||
|
||
Candidates.openAdd = function () {
|
||
const opt = (arr) => arr.map(o => `<option>${o}</option>`).join('');
|
||
const body = `<form id="canForm" novalidate><div class="form-grid">
|
||
<div class="form-field"><label>Full Name <span class="req">*</span></label><input name="name" placeholder="Jane Doe"/><span class="field-error">Required</span></div>
|
||
<div class="form-field"><label>Email <span class="req">*</span></label><input name="email" type="email" placeholder="jane@email.com"/><span class="field-error">Valid email required</span></div>
|
||
<div class="form-field"><label>Phone</label><input name="phone" placeholder="+1 (555) 000-0000"/></div>
|
||
<div class="form-field"><label>Applied Job <span class="req">*</span></label><select name="job">${opt(DB.jobs.filter(j => j.status === 'Open').map(j => j.title))}</select></div>
|
||
<div class="form-field"><label>Experience (years)</label><input name="experience" type="number" value="3"/></div>
|
||
<div class="form-field"><label>Current Company</label><input name="company" placeholder="Acme Inc."/></div>
|
||
<div class="form-field"><label>Source</label><select name="source">${opt(DB.sources)}</select></div>
|
||
<div class="form-field"><label>Stage</label><select name="stage">${opt(DB.stages)}</select></div>
|
||
</div></form>`;
|
||
const footer = `<button class="btn btn-secondary" onclick="UI.closeModal()">Cancel</button>
|
||
<button class="btn btn-primary" onclick="Candidates._add()">${UI.icon('check')} Add Candidate</button>`;
|
||
UI.modal({ title: 'Add Candidate', subtitle: 'Manually add a candidate to the pipeline', body, footer });
|
||
};
|
||
Candidates._add = function () {
|
||
const form = document.getElementById('canForm');
|
||
UI.clearErrors(form);
|
||
const f = Object.fromEntries(new FormData(form));
|
||
let ok = true;
|
||
if (!f.name.trim()) { UI.fieldError(form.querySelector('[name=name]'), 'Required'); ok = false; }
|
||
if (!/^\S+@\S+\.\S+$/.test(f.email)) { UI.fieldError(form.querySelector('[name=email]'), 'Valid email required'); ok = false; }
|
||
if (!ok) { UI.toast('Please fix the highlighted fields', 'error'); return; }
|
||
const job = DB.jobs.find(j => j.title === f.job) || DB.jobs[0];
|
||
const score = DB.int(55, 95);
|
||
DB.candidates.unshift({
|
||
id: 'CAN-' + (5001 + DB.candidates.length), name: f.name, initials: DB.initials(f.name), color: DB.avatarColor(f.name),
|
||
email: f.email, phone: f.phone || '+1 (555) 000-0000', jobId: job.id, jobTitle: job.title, department: job.department,
|
||
experience: +f.experience || 1, currentCompany: f.company || '—', currentTitle: job.title, location: job.location,
|
||
stage: f.stage, status: f.stage, aiScore: score, source: f.source, recruiter: job.recruiter, recruiterId: job.recruiterId,
|
||
applied: new Date('2026-07-09'), education: "Bachelor's Degree", skills: job.skills.slice(0, 4), rating: '4.0', salary: 120000,
|
||
matchedSkills: job.skills.slice(0, 3), missingSkills: job.skills.slice(3), recommendation: score >= 82 ? 'Strong Match' : score >= 65 ? 'Potential Match' : 'Weak Match',
|
||
subScores: { skills: score, experience: 80, education: 80, keywords: score, location: 100, salary: 90 },
|
||
noticePeriod: '1 month', availability: '2 weeks', certifications: [], favorite: false, interviewStatus: 'Not Scheduled'
|
||
});
|
||
UI.closeModal();
|
||
UI.toast('Candidate added to pipeline', 'success');
|
||
Router.reload();
|
||
};
|