HR-ATS-Portal/js/import.js

177 lines
9.9 KiB
JavaScript

/* ============================================================
import.js — Manual CV Import (drag&drop, parse, match, dedupe)
============================================================ */
window.Views = window.Views || {};
window.CVImport = {};
Views.import = function () {
const queue = []; // {name, size, status, atsScore, matchedJob, duplicate}
const html = `
<div class="page">
<div class="page-head">
<div><h1 class="page-title">CV Import</h1><p class="page-sub">Upload resumes — we parse, score, match, and dedupe automatically</p></div>
<div class="page-head-actions">
<span class="integration-status pending"><span class="pulse"></span>AI Resume Parser · Ready</span>
</div>
</div>
<div class="grid g-2-1">
<div>
<div class="card mb-18"><div class="card-body">
<div class="dropzone" id="dropzone">
<div class="dz-icn">${UI.icon('upload')}</div>
<h3>Drag &amp; drop resumes here</h3>
<p class="text-muted" style="margin-bottom:16px">or click to browse — PDF, DOC, DOCX and ZIP supported · up to 20 files</p>
<button class="btn btn-primary" id="browseBtn">${UI.icon('upload')} Browse Files</button>
<div class="flex items-center gap-8" style="justify-content:center;margin-top:16px">
${['PDF', 'DOC', 'DOCX', 'ZIP'].map(t => `<span class="badge b-gray badge-plain">${t}</span>`).join('')}
</div>
</div>
<div class="flex items-center gap-8" style="margin-top:16px;flex-wrap:wrap">
<button class="btn btn-secondary btn-sm" onclick="CVImport.simulate(3)">${UI.icon('sparkles')} Simulate 3 files</button>
<button class="btn btn-secondary btn-sm" onclick="CVImport.simulate(1,true)">${UI.icon('layers')} Simulate ZIP (8 CVs)</button>
<span class="text-muted text-sm" style="margin-left:auto">Files are processed locally in this demo</span>
</div>
</div></div>
<div class="card" id="queueCard" style="display:none">
<div class="card-head"><div><h3>Processing Queue</h3><span class="ch-sub" id="queueSub">0 files</span></div>
<button class="btn btn-primary btn-sm" id="importAllBtn" onclick="CVImport.importAll()">${UI.icon('check')} Import All</button></div>
<div class="card-body" id="queueList"></div>
</div>
</div>
<div class="card" style="align-self:start">
<div class="card-head"><div><h3>Auto-Processing</h3><span class="ch-sub">What happens on upload</span></div></div>
<div class="card-body"><div class="timeline">
${[
{ i: 'file', t: 'Resume parsing', d: 'Extract name, contact, experience, skills & education' },
{ i: 'target', t: 'ATS scoring', d: 'Generate a match score against the requisition' },
{ i: 'briefcase', t: 'Job matching', d: 'Suggest the best-matching open roles' },
{ i: 'users', t: 'Duplicate detection', d: 'Flag candidates already in the system' },
{ i: 'user-plus', t: 'Profile creation', d: 'Create a candidate profile in Applied stage' }
].map(s => `<div class="tl-item"><div class="tl-dot">${UI.icon(s.i)}</div><div class="tl-title">${s.t}</div><div class="tl-desc">${s.d}</div></div>`).join('')}
</div></div>
</div>
</div>
</div>`;
CVImport._queue = queue;
return {
html,
onMount() {
const dz = document.getElementById('dropzone');
const browse = document.getElementById('browseBtn');
dz.addEventListener('click', () => CVImport.simulate(DB.int(2, 4)));
browse.addEventListener('click', e => { e.stopPropagation(); CVImport.simulate(DB.int(2, 4)); });
dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('drag'); });
dz.addEventListener('dragleave', () => dz.classList.remove('drag'));
dz.addEventListener('drop', e => { e.preventDefault(); dz.classList.remove('drag'); CVImport.simulate(e.dataTransfer.files.length || DB.int(2, 4)); });
CVImport._renderQueue();
}
};
};
CVImport.simulate = function (count, isZip) {
const n = isZip ? 8 : count;
const jobs = DB.jobs.filter(j => j.status === 'Open');
for (let k = 0; k < n; k++) {
const name = DB.pick(['Olivia', 'Liam', 'Emma', 'Noah', 'Ava', 'Ethan', 'Sophia', 'Mason', 'Priya', 'Diego', 'Yuki', 'Omar']) + ' ' + DB.pick(['Chen', 'Patel', 'Kim', 'Garcia', 'Silva', 'Ahmed', 'Novak', 'Reyes', 'Khan', 'Costa']);
const item = {
id: 'UP-' + Math.random().toString(36).slice(2, 8), name, file: name.split(' ')[0] + '_Resume.' + DB.pick(['pdf', 'docx', 'doc']),
size: DB.int(120, 620) + ' KB', progress: 0, status: 'Uploading', atsScore: null,
job: DB.pick(jobs.length ? jobs : DB.jobs), duplicate: Math.random() < 0.18, imported: false
};
CVImport._queue.push(item);
CVImport._process(item);
}
document.getElementById('queueCard').style.display = '';
UI.toast(isZip ? 'ZIP extracted — 8 resumes queued' : n + ' file(s) uploaded', 'info');
CVImport._renderQueue();
};
CVImport._process = function (item) {
const tick = setInterval(() => {
item.progress += DB.int(12, 30);
if (item.progress >= 100) {
item.progress = 100; clearInterval(tick);
item.status = 'Parsing';
CVImport._renderQueue();
setTimeout(() => {
item.status = 'Ready'; item.atsScore = DB.int(52, 96);
CVImport._renderQueue();
}, 700 + DB.int(0, 500));
}
CVImport._renderQueue();
}, 220);
};
CVImport._renderQueue = function () {
const el = document.getElementById('queueList');
if (!el) return;
const q = CVImport._queue;
document.getElementById('queueSub').textContent = q.length + ' file' + (q.length === 1 ? '' : 's') + ' · ' + q.filter(i => i.imported).length + ' imported';
el.innerHTML = q.map(i => `
<div class="upload-row">
<span class="attach-icn" style="width:38px;height:38px">${UI.icon('file')}</span>
<div style="flex:1;min-width:0">
<div class="flex items-center gap-8"><span class="fw-600 text-sm">${i.name}</span>
${i.duplicate ? '<span class="badge b-red badge-plain" style="padding:1px 7px;font-size:10px">DUPLICATE</span>' : ''}</div>
<div class="cell-sub">${i.file} · ${i.size}</div>
${i.status === 'Uploading' || i.status === 'Parsing' ? `<div class="upload-progress" style="margin-top:6px"><div class="upload-progress-fill" style="width:${i.progress}%"></div></div>` :
`<div class="cell-sub" style="margin-top:4px">Best match: <b>${i.job.title}</b></div>`}
</div>
<div style="text-align:right;flex-shrink:0">
${i.status === 'Ready' ? UI.scoreChip(i.atsScore) : `<span class="badge ${i.status === 'Parsing' ? 'b-amber' : 'b-blue'}">${i.status}${i.status === 'Uploading' ? ' ' + i.progress + '%' : ''}</span>`}
</div>
<div style="flex-shrink:0">
${i.imported ? `<span class="badge b-green">Imported</span>` :
i.status === 'Ready' ? `<button class="btn btn-primary btn-sm" onclick="CVImport.importOne('${i.id}')">Import</button>` :
`<button class="act-btn" disabled>${UI.icon('clock')}</button>`}
</div>
</div>`).join('');
};
CVImport.importOne = function (id) {
const i = CVImport._queue.find(x => x.id === id);
if (!i || i.imported) return;
if (i.duplicate) {
UI.modal({
title: 'Duplicate Detected', subtitle: i.name,
body: `<div class="flex gap-16 items-center"><span class="kpi-icn i-amber" style="width:48px;height:48px;border-radius:12px;flex-shrink:0">${UI.icon('users')}</span>
<div><p class="fw-600" style="font-size:15px">A similar candidate already exists</p>
<p class="text-muted" style="margin-top:4px">${i.name} matches an existing profile (95% similarity on name + email). Importing will create a duplicate.</p></div></div>`,
footer: `<button class="btn btn-secondary" onclick="UI.closeModal()">Cancel</button>
<button class="btn btn-secondary" onclick="UI.closeModal();UI.toast('Merged into existing profile','success')">Merge</button>
<button class="btn btn-primary" onclick="UI.closeModal();CVImport._doImport('${id}')">Import Anyway</button>`
});
return;
}
CVImport._doImport(id);
};
CVImport._doImport = function (id) {
const i = CVImport._queue.find(x => x.id === id);
const job = i.job;
DB.candidates.unshift({
id: 'CAN-' + (5001 + DB.candidates.length), name: i.name, initials: DB.initials(i.name), color: DB.avatarColor(i.name),
email: i.name.toLowerCase().replace(/ /g, '.') + '@email.com', phone: '+1 (555) 000-0000', jobId: job.id, jobTitle: job.title, department: job.department,
experience: DB.int(2, 12), currentCompany: DB.pick(DB.companies), currentTitle: job.title, location: DB.pick(DB.locations),
stage: 'Applied', status: 'Applied', aiScore: i.atsScore, source: 'Manual CV Upload', recruiter: DB.pick(DB.recruiters).name, recruiterId: '',
applied: new Date('2026-07-09'), education: "Bachelor's Degree", skills: job.skills.slice(0, 4), rating: '4.0', salary: DB.int(90, 180) * 1000,
matchedSkills: job.skills.slice(0, 3), missingSkills: job.skills.slice(3), recommendation: i.atsScore >= 82 ? 'Strong Match' : 'Potential Match',
subScores: { skills: i.atsScore, experience: 80, education: 80, keywords: i.atsScore, location: 100, salary: 90 },
noticePeriod: '1 month', availability: '2 weeks', certifications: [], favorite: false, interviewStatus: 'Not Scheduled'
});
i.imported = true;
CVImport._renderQueue(); App.updateBadges();
UI.toast(`${i.name} imported → ${job.title}`, 'success');
};
CVImport.importAll = function () {
const ready = CVImport._queue.filter(i => i.status === 'Ready' && !i.imported && !i.duplicate);
if (!ready.length) { UI.toast('No files ready to import', 'warning'); return; }
ready.forEach(i => CVImport._doImport(i.id));
UI.toast(`${ready.length} candidates imported`, 'success');
};