HR-ATS-Portal/js/pipeline.js

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(); };
}
};
};