HR-ATS-Portal/js/candidates.js

442 lines
32 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/* ============================================================
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 AZ</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();
};