/* ============================================================
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 => `${UI.icon('check')} ` },
{ key: 'name', label: 'Candidate', sortable: true, render: c => `
${UI.avatar(c.name, c.initials, c.color)}
${c.name} ${c.favorite ? '' + UI.icon('star') + ' ' : ''}
${c.currentTitle} · ${c.location}
` },
{ key: 'jobTitle', label: 'Applied Job', sortable: true, render: c => `${c.jobTitle}
${c.department}
` },
{ key: 'experience', label: 'Exp', sortable: true, align: 'center', render: c => `${c.experience} y` },
{ key: '_rel', label: 'Relevance', sortable: true, align: 'center', sortValue: c => relevance(c), render: c => `${relevance(c)}% ` },
{ key: 'stage', label: 'Stage', sortable: true, render: c => UI.badge(c.stage) },
{ key: 'aiScore', label: 'ATS', sortable: true, align: 'center', render: c => `${UI.scoreChip(c.aiScore)} ` },
{ key: 'availability', label: 'Availability', render: c => `${c.availability} ${c.noticePeriod} notice
` },
{ key: '_a', label: 'Actions', align: 'right', render: c => `
${UI.icon('star')}
${UI.icon('target')}
${UI.icon('eye')}
${UI.icon('check')}
` }
],
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) => [`${label} `].concat(arr.map(o => `${o} `)).join('');
const jobTitles = [...new Set(DB.candidates.map(c => c.jobTitle))];
const filterPanel = `
Job ${optList(jobTitles, 'Any Job')}
Skill ${optList(DB.skillsPool, 'Any Skill')}
Department ${optList(DB.departments, 'Any Dept')}
Location ${optList(DB.locations, 'Any Location')}
Experience ${optList(['0-2', '3-5', '6-9', '10+'], 'Any Exp')}
Education ${optList(DB.educationLevels, 'Any')}
Recruiter ${optList(DB.recruiters.map(r => r.name), 'Any Recruiter')}
Hiring Manager ${optList(DB.managers.map(m => m.name), 'Any Manager')}
Source ${optList(DB.sources, 'Any Source')}
ATS Score ${optList(['85+', '70-84', '<70'], 'Any Score')}
Pipeline Stage ${optList(DB.stages, 'Any Stage')}
Interview Status ${optList(['Not Scheduled', 'Scheduled', 'Completed'], 'Any')}
Notice Period ${optList(['Immediate', '2 weeks', '1 month', '2 months', '3 months'], 'Any')}
Availability ${optList(['Immediate', '2 weeks', '1 month', 'Passive'], 'Any')}
`;
// recently viewed strip
const rv = DB.recentlyViewed.slice(0, 6).map(id => DB.getCandidate(id)).filter(Boolean);
const rvHtml = rv.length ? `
Recently viewed:
${rv.map(c => `${UI.avatar(c.name, c.initials, c.color)} ${c.name.split(' ')[0]} `).join('')}
` : '';
const html = `
Candidates ${DB.candidates.length} candidates · ranked by AI relevance
${UI.icon('bookmark')} Save Search
${UI.icon('download')} Export
${UI.icon('plus')} Add Candidate
${rvHtml}
${UI.icon('check')}
0 selected
${UI.icon('mail')} Bulk Email
${UI.icon('users')} Assign
${UI.icon('check')} Advance
${UI.icon('x')} Reject
${UI.icon('x')} Clear
`;
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 => `${r.name} `).join('');
UI.modal({ title: 'Bulk Assign Recruiter', subtitle: ids.length + ' candidates',
body: `Assign to ${opts}
`,
footer: `Cancel Assign ` });
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) => `${label} ${UI.pbar(val)}
${val}% `;
const body = `
${UI.icon(c.recommendation === 'Weak Match' ? 'x-circle' : 'check-circle')}
${c.recommendation}
${c.name} for ${c.jobTitle}
${subRow('Skills', sub.skills)}
${subRow('Experience', sub.experience)}
${subRow('Education', sub.education)}
${subRow('Keywords', sub.keywords)}
${subRow('Location', sub.location)}
${subRow('Salary', sub.salary)}
Matched Skills (${c.matchedSkills.length})
${c.matchedSkills.length ? c.matchedSkills.map(s => `${UI.icon('check')} ${s} `).join('') : '— '}
Missing Skills (${c.missingSkills.length})
${c.missingSkills.length ? c.missingSkills.map(s => `${UI.icon('x')} ${s} `).join('') : 'None — full match '}
${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.
`;
UI.modal({
title: 'ATS Match Analysis', subtitle: c.id + ' · ' + c.jobTitle, body, size: 'modal-lg',
footer: `Close View Full Profile `
});
};
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 = `
${UI.avatar(c.name, c.initials, c.color, 'avatar-lg')}
${c.name}
${c.currentTitle} at ${c.currentCompany}
${UI.badge(c.stage)} ${UI.badge(c.source, 'b-gray')}
${c.experience} yrs exp
${UI.scoreChip(c.aiScore)}
AI Match
${tabs.map((t, i) => `
${t}
`).join('')}
${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)}
`;
const footer = `${UI.icon('star')} ${c.favorite ? 'Favorited' : 'Favorite'}
${UI.icon('target')} ATS Match
${UI.icon('mail')} Message
${UI.icon('check')} Advance Stage `;
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 = `
Current Company
${c.currentCompany}
Experience
${c.experience} years
Applied On
${DB.fmtDate(c.applied)}
Expected Salary
${DB.moneyK(c.salary)}
Rating
⭐ ${c.rating} / 5.0
Skills
${c.skills.map(s => `${s} `).join('')}
`;
} else if (name === 'Resume') {
content = `
${c.name} ${c.currentTitle} · ${c.location}
Summary
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.
Experience
${c.currentTitle} — ${c.currentCompany}
2021 – Present
Associate — ${DB.pick(DB.companies)}
2018 – 2021
Education
${c.education}
${UI.icon('download')} Download PDF `;
} 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 = `${events.map(e => `
${UI.icon(e.icon)}
${e.title}
${e.meta}
${e.desc}
`).join('')}
`;
} else if (name === 'Interview') {
const ivs = DB.interviews.filter(i => i.candidateId === c.id);
content = ivs.length ? `${ivs.map(iv => `
${UI.icon('calendar')}
${iv.type}
${DB.fmtDate(iv.when)} · ${iv.meeting}
${UI.badge(iv.status)}
`).join('')}
`
: `${UI.icon('calendar')}
No interviews scheduled Schedule an interview to get started.
Schedule Interview `;
} else if (name === 'Notes') {
content = `
Add a note
${UI.icon('plus')} Add Note
${UI.avatar(c.recruiter)}
${c.recruiter}
Strong communication skills, great culture fit. Recommend advancing.
2 days ago
${UI.avatar('Asfand Ahmed', 'AA')}
Asfand Ahmed
Reviewed portfolio — impressive work. Schedule technical round.
4 days ago
`;
} else if (name === 'Activity') {
content = `
${UI.icon('eye')} Profile viewed by ${c.recruiter}
1h ago
${UI.icon('mail')} Email sent: Interview invitation
1 day ago
${UI.icon('star')} Assessment score updated to ${c.aiScore}%
2 days ago
${UI.icon('user-plus')} Applied for ${c.jobTitle}
${DB.fmtDate(c.applied)}
`;
} 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 = `${docs.map(d => `
${UI.icon('file')}
${UI.icon('download')} `).join('')}
`;
} else if (name === 'Feedback') {
const scores = ['Strong Hire', 'Hire', 'Lean Hire'];
content = `
${[0, 1, 2].map(i => `
${UI.avatar(DB.recruiters[i].name, DB.recruiters[i].initials, DB.recruiters[i].color)}
${DB.recruiters[i].name}
${['Excellent technical depth and clear communication.', 'Good problem solving, would benefit from more system design exposure.', 'Solid candidate, positive team energy.'][i]}
${UI.badge(scores[i])}
`).join('')}
${UI.icon('plus')} Submit Scorecard `;
}
return `${content}
`;
};
Candidates.openAdd = function () {
const opt = (arr) => arr.map(o => `${o} `).join('');
const body = ``;
const footer = `Cancel
${UI.icon('check')} Add Candidate `;
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();
};