210 lines
12 KiB
JavaScript
210 lines
12 KiB
JavaScript
/* ============================================================
|
|
interviews.js — Interviews list, upcoming, mini calendar
|
|
============================================================ */
|
|
window.Views = window.Views || {};
|
|
window.Interviews = {};
|
|
|
|
Views.interviews = function () {
|
|
const filters = { q: '', status: '', type: '' };
|
|
let table;
|
|
|
|
const upcoming = DB.interviews.filter(iv => iv.status === 'Scheduled').slice(0, 4);
|
|
const stats = {
|
|
scheduled: DB.interviews.filter(i => i.status === 'Scheduled').length,
|
|
completed: DB.interviews.filter(i => i.status === 'Completed').length,
|
|
today: 5,
|
|
cancelled: DB.interviews.filter(i => ['Cancelled', 'No Show'].includes(i.status)).length
|
|
};
|
|
|
|
function apply() {
|
|
const rows = DB.interviews.filter(iv => {
|
|
if (filters.status && iv.status !== filters.status) return false;
|
|
if (filters.type && iv.type !== filters.type) return false;
|
|
if (filters.q && !(iv.candidate + iv.jobTitle + iv.interviewers.join(' ')).toLowerCase().includes(filters.q.toLowerCase())) return false;
|
|
return true;
|
|
});
|
|
table.update(rows);
|
|
}
|
|
|
|
table = UI.dataTable({
|
|
pageSize: 8,
|
|
rows: DB.interviews,
|
|
columns: [
|
|
{ key: 'candidate', label: 'Candidate', sortable: true, render: iv => `<div class="user-cell">${UI.avatar(iv.candidate, iv.candInitials, iv.color)}<div><div class="cell-primary">${iv.candidate}</div><div class="cell-sub">${iv.jobTitle}</div></div></div>` },
|
|
{ key: 'type', label: 'Round', sortable: true, render: iv => UI.badge(iv.type, 'b-indigo') },
|
|
{ key: 'when', label: 'Date & Time', sortable: true, sortValue: iv => iv.when.getTime(), render: iv => `<div class="text-sm fw-600">${DB.fmtShort(iv.when)}</div><div class="cell-sub">${iv.when.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })} · ${iv.duration}m</div>` },
|
|
{ key: 'meeting', label: 'Type', render: iv => `<span class="flex items-center gap-8">${UI.icon(iv.meeting === 'Video Call' ? 'video' : iv.meeting === 'Phone' ? 'phone' : 'map')} ${iv.meeting}</span>` },
|
|
{ key: 'interviewers', label: 'Interviewers', render: iv => UI.avatarStack(iv.interviewers) },
|
|
{ key: 'status', label: 'Status', sortable: true, render: iv => UI.badge(iv.status) },
|
|
{ key: 'feedback', label: 'Feedback', render: iv => iv.feedback ? UI.badge(iv.feedback) : '<span class="text-muted">—</span>' },
|
|
{ key: '_a', label: 'Actions', align: 'right', render: iv => `
|
|
<div class="row-actions">
|
|
<button class="act-btn" data-tip="View candidate" onclick="Candidates.openProfile('${iv.candidateId}')">${UI.icon('eye')}</button>
|
|
<button class="act-btn" data-tip="Feedback" onclick="Interviews.feedback('${iv.id}')">${UI.icon('star')}</button>
|
|
</div>` }
|
|
]
|
|
});
|
|
|
|
const statusOpts = ['<option value="">All Status</option>'].concat(['Scheduled', 'Completed', 'Cancelled', 'No Show'].map(s => `<option>${s}</option>`)).join('');
|
|
const typeOpts = ['<option value="">All Rounds</option>'].concat(DB.interviewTypes.map(t => `<option>${t}</option>`)).join('');
|
|
|
|
const statCard = (label, val, icn, cls) => `<div class="kpi"><div class="kpi-top"><span class="kpi-label">${label}</span><span class="kpi-icn ${cls}">${UI.icon(icn)}</span></div><div class="kpi-value">${val}</div></div>`;
|
|
|
|
const upcomingHtml = upcoming.map(iv => `
|
|
<div class="list-row">
|
|
${UI.avatar(iv.candidate, iv.candInitials, iv.color)}
|
|
<div class="lr-main"><div class="lr-title">${iv.candidate}</div><div class="lr-sub">${iv.type} · ${iv.meeting}</div></div>
|
|
<div class="lr-right"><div class="fw-600 text-sm">${DB.fmtShort(iv.when)}</div><div class="lr-sub">${iv.when.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}</div></div>
|
|
</div>`).join('');
|
|
|
|
const html = `
|
|
<div class="page">
|
|
<div class="page-head">
|
|
<div><h1 class="page-title">Interviews</h1><p class="page-sub">Manage and track all interview activity</p></div>
|
|
<div class="page-head-actions">
|
|
<button class="btn btn-secondary" onclick="Router.go('calendar')">${UI.icon('calendar')} Calendar View</button>
|
|
<button class="btn btn-primary" onclick="Interviews.schedule()">${UI.icon('plus')} Schedule Interview</button>
|
|
</div>
|
|
</div>
|
|
<div class="grid g-kpi mb-18">
|
|
${statCard('Scheduled', stats.scheduled, 'calendar', 'i-blue')}
|
|
${statCard('Completed', stats.completed, 'check-circle', 'i-green')}
|
|
${statCard('Today', stats.today, 'clock', 'i-purple')}
|
|
${statCard('Cancelled / No-show', stats.cancelled, 'x-circle', 'i-red')}
|
|
</div>
|
|
<div class="grid g-2-1">
|
|
<div class="card">
|
|
<div class="card-head"><div><h3>All Interviews</h3></div></div>
|
|
<div class="card-body" style="padding-bottom:0">
|
|
<div class="toolbar">
|
|
<div class="toolbar-search">${UI.icon('search')}<input id="ivSearch" placeholder="Search candidate or interviewer…"/></div>
|
|
<select class="select" id="ivStatus">${statusOpts}</select>
|
|
<select class="select" id="ivType">${typeOpts}</select>
|
|
</div>
|
|
</div>
|
|
${table.html}
|
|
</div>
|
|
<div class="card" style="align-self:start">
|
|
<div class="card-head"><div><h3>Up Next</h3><span class="ch-sub">Scheduled sessions</span></div></div>
|
|
<div class="card-body"><div class="list-tight">${upcomingHtml}</div></div>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
|
|
return {
|
|
html,
|
|
onMount() {
|
|
table.mount();
|
|
const s = document.getElementById('ivSearch');
|
|
s.oninput = () => { filters.q = s.value; apply(); };
|
|
document.getElementById('ivStatus').onchange = e => { filters.status = e.target.value; apply(); };
|
|
document.getElementById('ivType').onchange = e => { filters.type = e.target.value; apply(); };
|
|
}
|
|
};
|
|
};
|
|
|
|
Interviews.feedback = function (id) {
|
|
const iv = DB.interviews.find(i => i.id === id);
|
|
// pick evaluation template by department
|
|
const job = DB.jobs.find(j => j.title === iv.jobTitle);
|
|
const dept = job ? job.department : 'All';
|
|
const tmpl = DB.evalTemplates.find(t => t.dept === dept) || DB.evalTemplates.find(t => t.dept === 'All');
|
|
const tmplOpts = DB.evalTemplates.map(t => `<option ${t === tmpl ? 'selected' : ''}>${t.name}</option>`).join('');
|
|
|
|
const ratingRow = (crit) => `
|
|
<div class="setting-row" style="padding:12px 0">
|
|
<div class="setting-info"><h4>${crit}</h4></div>
|
|
<div class="rating-stars" data-crit="${crit}">
|
|
${[1, 2, 3, 4, 5].map(n => `<span class="rs" data-val="${n}">${UI.icon('star')}</span>`).join('')}
|
|
</div>
|
|
</div>`;
|
|
|
|
const body = `
|
|
<div class="flex items-center gap-12 mb-18">${UI.avatar(iv.candidate, iv.candInitials, iv.color, 'avatar-lg')}
|
|
<div style="flex:1"><div class="ph-name" style="font-size:17px">${iv.candidate}</div><div class="ph-role">${iv.type} · ${iv.jobTitle}</div></div>
|
|
${UI.badge(iv.status)}</div>
|
|
|
|
<div class="tabs" id="evalTabs" style="margin-bottom:18px">
|
|
<div class="tab active" data-etab="0">Dynamic Form</div>
|
|
<div class="tab" data-etab="1">Upload Sheet</div>
|
|
<div class="tab" data-etab="2">Both</div>
|
|
</div>
|
|
|
|
<div id="evalPanes">
|
|
<div class="tab-pane active" data-epane="0">
|
|
<div class="form-field" style="margin-bottom:8px"><label>Evaluation Template</label><select id="evalTmpl">${tmplOpts}</select></div>
|
|
<div id="critList">${tmpl.criteria.map(ratingRow).join('')}</div>
|
|
<div class="form-field" style="margin-top:8px"><label>Comments</label><textarea placeholder="Strengths, concerns, and areas explored…"></textarea></div>
|
|
<div class="form-field" style="margin-top:14px"><label>Overall Recommendation</label>
|
|
<div class="seg" style="margin-top:4px"><button type="button" class="active" data-rec="Hire">Hire</button><button type="button" data-rec="Hold">Hold</button><button type="button" data-rec="Reject">Reject</button></div></div>
|
|
</div>
|
|
|
|
<div class="tab-pane" data-epane="1">
|
|
<div class="dropzone" onclick="UI.toast('File picker (demo)','info')" style="padding:32px">
|
|
<div class="dz-icn">${UI.icon('upload')}</div>
|
|
<h3 style="font-size:15px">Upload evaluation sheet</h3>
|
|
<p class="text-muted">PDF, DOC, or DOCX · scanned scorecards supported</p>
|
|
<div class="flex items-center gap-8" style="justify-content:center;margin-top:12px">${['PDF', 'DOC', 'DOCX'].map(t => `<span class="badge b-gray badge-plain">${t}</span>`).join('')}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tab-pane" data-epane="2">
|
|
<p class="text-muted" style="margin-bottom:14px">Capture structured ratings <b>and</b> attach a signed sheet — both are stored on the scorecard.</p>
|
|
<div id="critList2">${tmpl.criteria.slice(0, 3).map(ratingRow).join('')}</div>
|
|
<div class="upload-row" style="margin-top:12px"><span class="attach-icn" style="width:38px;height:38px">${UI.icon('file')}</span>
|
|
<div style="flex:1"><div class="fw-600 text-sm">Interviewer_Scorecard.pdf</div><div class="cell-sub">Attached · 214 KB</div></div>${UI.badge('Uploaded', 'b-green')}</div>
|
|
</div>
|
|
</div>`;
|
|
|
|
const footer = `<button class="btn btn-secondary" onclick="UI.closeModal()">Cancel</button>
|
|
<button class="btn btn-primary" onclick="UI.closeModal();UI.toast('Scorecard submitted','success')">${UI.icon('check')} Submit Scorecard</button>`;
|
|
UI.modal({ title: 'Interview Evaluation', subtitle: iv.id + ' · ' + iv.type, body, footer, size: 'modal-lg' });
|
|
|
|
// wire tabs
|
|
const panes = document.querySelectorAll('#evalPanes .tab-pane');
|
|
document.querySelectorAll('#evalTabs .tab').forEach(tab => tab.onclick = () => {
|
|
document.querySelectorAll('#evalTabs .tab').forEach(t => t.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
panes.forEach(p => p.classList.remove('active'));
|
|
panes[+tab.dataset.etab].classList.add('active');
|
|
});
|
|
// wire star ratings
|
|
Interviews._bindStars();
|
|
// template switch rebuilds criteria
|
|
const tmplSel = document.getElementById('evalTmpl');
|
|
if (tmplSel) tmplSel.onchange = () => {
|
|
const t = DB.evalTemplates.find(x => x.name === tmplSel.value);
|
|
document.getElementById('critList').innerHTML = t.criteria.map(ratingRow).join('');
|
|
Interviews._bindStars();
|
|
};
|
|
// recommendation seg
|
|
document.querySelectorAll('[data-rec]').forEach(b => b.onclick = () => {
|
|
b.parentElement.querySelectorAll('button').forEach(x => x.classList.remove('active'));
|
|
b.classList.add('active');
|
|
});
|
|
};
|
|
Interviews._bindStars = function () {
|
|
document.querySelectorAll('.rating-stars').forEach(group => {
|
|
group.querySelectorAll('.rs').forEach(star => star.onclick = () => {
|
|
const val = +star.dataset.val;
|
|
group.querySelectorAll('.rs').forEach(s => s.classList.toggle('on', +s.dataset.val <= val));
|
|
});
|
|
});
|
|
};
|
|
|
|
Interviews.schedule = function () {
|
|
const opt = arr => arr.map(o => `<option>${o}</option>`).join('');
|
|
const body = `<form id="schForm"><div class="form-grid">
|
|
<div class="form-field col-span-2"><label>Candidate <span class="req">*</span></label><select name="candidate">${opt(DB.candidates.slice(0, 40).map(c => c.name))}</select></div>
|
|
<div class="form-field"><label>Interview Round</label><select name="type">${opt(DB.interviewTypes)}</select></div>
|
|
<div class="form-field"><label>Meeting Type</label><select name="meeting">${opt(DB.meetingTypes)}</select></div>
|
|
<div class="form-field"><label>Date</label><input type="date" name="date"/></div>
|
|
<div class="form-field"><label>Time</label><input type="time" name="time" value="14:00"/></div>
|
|
<div class="form-field"><label>Duration</label><select name="dur"><option>30 min</option><option>45 min</option><option selected>60 min</option><option>90 min</option></select></div>
|
|
<div class="form-field"><label>Interviewer</label><select name="interviewer">${opt(DB.recruiters.concat(DB.managers).map(r => r.name))}</select></div>
|
|
</div></form>`;
|
|
const footer = `<button class="btn btn-secondary" onclick="UI.closeModal()">Cancel</button>
|
|
<button class="btn btn-primary" onclick="UI.closeModal();UI.toast('Interview scheduled & invite sent','success')">${UI.icon('calendar')} Schedule</button>`;
|
|
UI.modal({ title: 'Schedule Interview', subtitle: 'Set up a new interview session', body, footer });
|
|
};
|