/* ============================================================ 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 => `
` } ], 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) => [``].concat(arr.map(o => ``)).join(''); const jobTitles = [...new Set(DB.candidates.map(c => c.jobTitle))]; const filterPanel = ` `; // 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 => ``).join('')}
` : ''; const html = `

Candidates

${DB.candidates.length} candidates · ranked by AI relevance

${rvHtml}
${filterPanel}
${table.html}
`; 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 => ``).join(''); UI.modal({ title: 'Bulk Assign Recruiter', subtitle: ids.length + ' candidates', body: `
`, footer: `` }); 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}
${c.aiScore}
ATS MATCH
${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: `` }); }; 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.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 = `
Email
${c.email}
Phone
${c.phone}
Location
${c.location}
Applied For
${c.jobTitle}
Current Company
${c.currentCompany}
Experience
${c.experience} years
Education
${c.education}
Source
${c.source}
Recruiter
${c.recruiter}
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}
`; } 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.

`; } else if (name === 'Notes') { content = `
${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')}
${d.n}
${d.s}
`).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('')}
`; } return `
${content}
`; }; Candidates.openAdd = function () { const opt = (arr) => arr.map(o => ``).join(''); const body = `
Required
Valid email required
`; const footer = ` `; 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(); };