167 lines
7.4 KiB
JavaScript
167 lines
7.4 KiB
JavaScript
/* ============================================================
|
|
pipeline.js — Kanban board (drag & drop) + Talent Pool
|
|
============================================================ */
|
|
window.Views = window.Views || {};
|
|
window.Pipeline = {};
|
|
|
|
// Stage colours reference CSS tokens so the board re-tints with the theme.
|
|
const KANBAN_STAGES = [
|
|
{ name: 'Applied', color: 'var(--stage-1)' },
|
|
{ name: 'Screening', color: 'var(--stage-2)' },
|
|
{ name: 'Assessment', color: 'var(--stage-3)' },
|
|
{ name: 'Interview', color: 'var(--stage-4)' },
|
|
{ name: 'Offer', color: 'var(--stage-5)' },
|
|
{ name: 'Hired', color: 'var(--stage-6)' },
|
|
{ name: 'Rejected', color: 'var(--stage-7)' }
|
|
];
|
|
|
|
Views.pipeline = function () {
|
|
const jobFilter = { id: '' };
|
|
|
|
function columns() {
|
|
const list = jobFilter.id ? DB.candidates.filter(c => c.jobId === jobFilter.id) : DB.candidates;
|
|
return KANBAN_STAGES.map(st => {
|
|
const cards = list.filter(c => c.stage === st.name);
|
|
return `<div class="kanban-col" data-stage="${st.name}">
|
|
<div class="kanban-col-head"><span class="k-dot" style="background:${st.color}"></span><h4>${st.name}</h4><span class="k-count">${cards.length}</span></div>
|
|
<div class="kanban-cards" data-stage="${st.name}">
|
|
${cards.map(c => Pipeline._card(c)).join('')}
|
|
</div></div>`;
|
|
}).join('');
|
|
}
|
|
|
|
const jobOpts = ['<option value="">All Jobs</option>'].concat(DB.jobs.filter(j => j.status === 'Open').map(j => `<option value="${j.id}">${j.title}</option>`)).join('');
|
|
|
|
const html = `
|
|
<div class="page">
|
|
<div class="page-head">
|
|
<div><h1 class="page-title">Pipeline</h1><p class="page-sub">Drag candidates between stages to update their status</p></div>
|
|
<div class="page-head-actions">
|
|
<select class="select" id="pipeJob">${jobOpts}</select>
|
|
<button class="btn btn-primary" onclick="Candidates.openAdd()">${UI.icon('plus')} Add Candidate</button>
|
|
</div>
|
|
</div>
|
|
<div class="kanban" id="kanban">${columns()}</div>
|
|
</div>`;
|
|
|
|
return {
|
|
html,
|
|
onMount() {
|
|
Pipeline._bindDnd();
|
|
document.getElementById('pipeJob').onchange = e => {
|
|
jobFilter.id = e.target.value;
|
|
document.getElementById('kanban').innerHTML = columns();
|
|
Pipeline._bindDnd();
|
|
};
|
|
}
|
|
};
|
|
};
|
|
|
|
Pipeline._card = function (c) {
|
|
return `<div class="k-card" draggable="true" data-id="${c.id}" onclick="Candidates.openProfile('${c.id}')">
|
|
<div class="k-card-top">${UI.avatar(c.name, c.initials, c.color)}
|
|
<div><div class="kc-name">${c.name}</div><div class="kc-role">${c.currentTitle}</div></div></div>
|
|
<div class="kc-role">${c.jobTitle}</div>
|
|
<div class="k-tags">${c.skills.slice(0, 3).map(s => `<span class="tag">${s}</span>`).join('')}</div>
|
|
<div class="k-card-meta"><span class="cell-sub">${c.currentCompany}</span>${UI.scoreChip(c.aiScore)}</div>
|
|
</div>`;
|
|
};
|
|
|
|
Pipeline._bindDnd = function () {
|
|
let dragged = null;
|
|
document.querySelectorAll('.k-card').forEach(card => {
|
|
card.addEventListener('dragstart', e => {
|
|
dragged = card; card.classList.add('dragging');
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
e.dataTransfer.setData('text/plain', card.dataset.id);
|
|
});
|
|
card.addEventListener('dragend', () => { card.classList.remove('dragging'); dragged = null; });
|
|
// prevent click-through opening profile right after drag
|
|
card.addEventListener('click', e => { if (card._justDropped) { e.stopPropagation(); card._justDropped = false; } });
|
|
});
|
|
document.querySelectorAll('.kanban-cards').forEach(zone => {
|
|
zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('drag-over'); });
|
|
zone.addEventListener('dragleave', () => zone.classList.remove('drag-over'));
|
|
zone.addEventListener('drop', e => {
|
|
e.preventDefault();
|
|
zone.classList.remove('drag-over');
|
|
if (!dragged) return;
|
|
const id = dragged.dataset.id;
|
|
const cand = DB.getCandidate(id);
|
|
const newStage = zone.dataset.stage;
|
|
if (cand.stage === newStage) return;
|
|
cand.stage = newStage; cand.status = newStage;
|
|
zone.appendChild(dragged);
|
|
// update counts
|
|
document.querySelectorAll('.kanban-col').forEach(col => {
|
|
col.querySelector('.k-count').textContent = col.querySelectorAll('.k-card').length;
|
|
});
|
|
UI.toast(`${cand.name} moved to ${newStage}`, 'success');
|
|
});
|
|
});
|
|
};
|
|
|
|
// ---------------- Talent Pool ----------------
|
|
Views.talentpool = function () {
|
|
const filters = { q: '', dept: '' };
|
|
// Talent pool = candidates not currently in active loop (silver medalists / passive talent)
|
|
const pool = DB.candidates.filter(c => ['Rejected', 'Applied', 'Hired'].includes(c.stage));
|
|
|
|
function render(list) {
|
|
const grid = document.getElementById('poolGrid');
|
|
if (!grid) return;
|
|
if (!list.length) { grid.innerHTML = `<div class="empty-state" style="grid-column:1/-1">${UI.icon('search')}<h3>No talent found</h3></div>`; return; }
|
|
grid.innerHTML = list.map(c => `
|
|
<div class="card" style="cursor:pointer" onclick="Candidates.openProfile('${c.id}')">
|
|
<div class="card-body">
|
|
<div class="flex items-center gap-12" style="margin-bottom:12px">
|
|
${UI.avatar(c.name, c.initials, c.color, 'avatar-lg')}
|
|
<div style="flex:1;min-width:0"><div class="lr-title">${c.name}</div><div class="lr-sub">${c.currentTitle}</div></div>
|
|
${UI.scoreChip(c.aiScore)}
|
|
</div>
|
|
<div class="k-tags" style="margin-bottom:12px">${c.skills.slice(0, 4).map(s => `<span class="tag">${s}</span>`).join('')}</div>
|
|
<div class="divider" style="margin:12px 0"></div>
|
|
<div class="flex items-center" style="justify-content:space-between">
|
|
<span class="cell-sub">${UI.icon('briefcase')} ${c.experience} yrs</span>
|
|
<span class="cell-sub">${c.currentCompany}</span>
|
|
${UI.badge(c.source, 'b-gray')}
|
|
</div>
|
|
</div>
|
|
</div>`).join('');
|
|
}
|
|
function apply() {
|
|
let list = pool.filter(c => {
|
|
if (filters.dept && c.department !== filters.dept) return false;
|
|
if (filters.q && !(c.name + c.currentCompany + c.skills.join(' ')).toLowerCase().includes(filters.q.toLowerCase())) return false;
|
|
return true;
|
|
});
|
|
render(list);
|
|
}
|
|
const deptOpts = ['<option value="">All Departments</option>'].concat(DB.departments.map(d => `<option>${d}</option>`)).join('');
|
|
|
|
const html = `
|
|
<div class="page">
|
|
<div class="page-head">
|
|
<div><h1 class="page-title">Talent Pool</h1><p class="page-sub">${pool.length} silver-medalists & passive candidates to re-engage</p></div>
|
|
<div class="page-head-actions"><button class="btn btn-primary" onclick="UI.toast('Talent campaign created','success')">${UI.icon('send')} Start Campaign</button></div>
|
|
</div>
|
|
<div class="card mb-18"><div class="card-body" style="padding:16px">
|
|
<div class="toolbar" style="margin-bottom:0">
|
|
<div class="toolbar-search">${UI.icon('search')}<input id="poolSearch" placeholder="Search by name, skill, company…"/></div>
|
|
<select class="select" id="poolDept">${deptOpts}</select>
|
|
</div>
|
|
</div></div>
|
|
<div class="grid g-3" id="poolGrid"></div>
|
|
</div>`;
|
|
|
|
return {
|
|
html,
|
|
onMount() {
|
|
apply();
|
|
const s = document.getElementById('poolSearch');
|
|
s.oninput = () => { filters.q = s.value; apply(); };
|
|
document.getElementById('poolDept').onchange = e => { filters.dept = e.target.value; apply(); };
|
|
}
|
|
};
|
|
};
|