HR-ATS-Portal/js/inbox.js

333 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/* ============================================================
inbox.js — Central Recruitment Inbox + Outlook Email tab
============================================================ */
window.Views = window.Views || {};
window.Inbox = {};
Views.inbox = function () {
const state = { tab: 'All Applications', selected: null, emailSelected: null, q: '' };
const tabs = ['All Applications', 'Unread', 'Imported', 'Processed', 'Rejected', 'Duplicates', 'Email'];
function filtered() {
let list = DB.inbox;
if (state.tab === 'Unread') list = list.filter(i => i.processing === 'Unread');
else if (state.tab === 'Imported') list = list.filter(i => i.processing === 'Imported');
else if (state.tab === 'Processed') list = list.filter(i => i.processing === 'Processed');
else if (state.tab === 'Rejected') list = list.filter(i => i.processing === 'Rejected');
else if (state.tab === 'Duplicates') list = list.filter(i => i.duplicate);
if (state.q) list = list.filter(i => (i.name + i.position + i.source).toLowerCase().includes(state.q.toLowerCase()));
return list;
}
function counts() {
return {
'All Applications': DB.inbox.length,
'Unread': DB.inbox.filter(i => i.processing === 'Unread').length,
'Imported': DB.inbox.filter(i => i.processing === 'Imported').length,
'Processed': DB.inbox.filter(i => i.processing === 'Processed').length,
'Rejected': DB.inbox.filter(i => i.processing === 'Rejected').length,
'Duplicates': DB.inbox.filter(i => i.duplicate).length,
'Email': DB.emails.filter(e => e.unread).length
};
}
function sourceChip(item) {
const m = item.sourceMeta;
// The dot carries the partner's brand colour; the label uses theme text.
// Rendering 11px labels in the partner colour failed AA in both themes.
// `--chip` carries the source colour; CSS mixes the tint. String-concat
// alpha ("#0a66c214") breaks for the tokenised sources (var(--c1)14).
return `<span class="source-chip" style="--chip:${m.color}"><span class="source-dot"></span>${item.source}</span>`;
}
function renderList() {
const el = document.getElementById('inboxList');
if (!el) return;
const list = filtered();
if (!list.length) { el.innerHTML = `<div class="empty-state">${UI.icon('inbox')}<h3>Nothing here</h3><p>No applications in this view.</p></div>`; return; }
el.innerHTML = list.map(i => `
<div class="inbox-item ${i.unread ? 'unread' : ''} ${state.selected === i.id ? 'active' : ''}" data-id="${i.id}">
${UI.avatar(i.name, i.initials, i.color)}
<div class="ii-main">
<div class="ii-name">${i.name} ${i.duplicate ? '<span class="badge b-red badge-plain" style="padding:1px 6px;font-size:10px">DUP</span>' : ''}</div>
<div class="ii-pos">${i.position}</div>
<div class="ii-meta">${sourceChip(i)} ${UI.badge(i.processing)}</div>
</div>
<div style="text-align:right;flex-shrink:0">
<div class="ii-time">${DB.relTime(Math.round((new Date('2026-07-09T20:00') - i.received) / 60000))}</div>
<div style="margin-top:6px">${UI.scoreChip(i.atsScore)}</div>
</div>
</div>`).join('');
el.querySelectorAll('.inbox-item').forEach(row => row.onclick = () => {
state.selected = row.dataset.id;
const it = DB.inbox.find(x => x.id === state.selected); if (it) it.unread = false;
renderList(); renderDetail(); App.updateBadges();
});
}
function renderDetail() {
const el = document.getElementById('inboxDetail');
if (!el) return;
const i = DB.inbox.find(x => x.id === state.selected);
if (!i) { el.innerHTML = `<div class="empty-state" style="padding:100px 20px">${UI.icon('inbox')}<h3>Select an application</h3><p>Choose an item from the list to view details and take action.</p></div>`; return; }
const rec = DB.atsRecommendationClass;
const recLabel = i.atsScore >= 82 ? 'Strong Match' : i.atsScore >= 65 ? 'Potential Match' : 'Weak Match';
el.innerHTML = `
<div style="padding:24px">
<div class="flex items-center gap-16" style="margin-bottom:20px">
${UI.avatar(i.name, i.initials, i.color, 'avatar-lg')}
<div style="flex:1"><div class="ph-name" style="font-size:19px">${i.name}</div>
<div class="ph-role">${i.position}</div>
<div class="ph-tags" style="margin-top:8px">${sourceChip(i)} ${UI.badge(i.processing)} ${UI.badge(i.resumeStatus, i.resumeStatus === 'Parsed' ? 'b-green' : i.resumeStatus === 'Failed' ? 'b-red' : 'b-amber')}</div>
</div>
<div style="text-align:center">
<div class="ats-ring" style="width:84px;height:84px;--pct:${i.atsScore};--c:${i.atsScore >= 82 ? 'var(--success)' : i.atsScore >= 65 ? 'var(--warning)' : 'var(--danger)'}">
<div class="ats-val"><div class="ats-num" style="font-size:22px">${i.atsScore}</div></div></div>
<div class="cell-sub" style="margin-top:4px">ATS Score</div>
</div>
</div>
<div class="info-grid" style="margin-bottom:20px">
<div class="info-item"><div class="il">Email</div><div class="iv">${i.email}</div></div>
<div class="info-item"><div class="il">Phone</div><div class="iv">${i.phone}</div></div>
<div class="info-item"><div class="il">Experience</div><div class="iv">${i.experience} years</div></div>
<div class="info-item"><div class="il">Assigned Recruiter</div><div class="iv">${i.recruiter}</div></div>
<div class="info-item"><div class="il">Received</div><div class="iv">${DB.fmtDate(i.received)}</div></div>
<div class="info-item"><div class="il">Match</div><div class="iv">${UI.badge(recLabel, rec(recLabel))}</div></div>
</div>
<div class="card" style="box-shadow:none;background:var(--bg-sunken);margin-bottom:20px"><div class="card-body">
<div class="flex items-center gap-12" style="justify-content:space-between;margin-bottom:12px">
<div class="fw-600">${UI.icon('paperclip')} ${i.attachment}</div>
<button class="btn btn-secondary btn-sm" onclick="Inbox.viewResume('${i.id}')">${UI.icon('eye')} Preview</button>
</div>
<div class="resume-thumb">${Inbox._resumeText(i)}</div>
</div></div>
<div class="flex gap-8" style="flex-wrap:wrap">
<button class="btn btn-primary" onclick="Inbox.import('${i.id}')">${UI.icon('user-plus')} Import Candidate</button>
<button class="btn btn-secondary" onclick="Inbox.parse('${i.id}')">${UI.icon('sparkles')} Parse Resume</button>
<button class="btn btn-secondary" onclick="Inbox.assign('${i.id}')">${UI.icon('users')} Assign Recruiter</button>
<button class="btn btn-secondary" onclick="Inbox.moveToPipeline('${i.id}')">${UI.icon('layers')} Move to Pipeline</button>
<button class="btn btn-secondary" onclick="Inbox.note('${i.id}')">${UI.icon('edit')} Add Note</button>
<button class="btn btn-ghost" style="color:var(--danger)" onclick="Inbox.reject('${i.id}')">${UI.icon('x')} Reject</button>
</div>
</div>`;
}
function renderBody() {
const body = document.getElementById('inboxBody');
if (state.tab === 'Email') { body.innerHTML = Inbox._emailView(); Inbox._bindEmail(state); return; }
body.innerHTML = `
<div class="split">
<div class="split-list">
<div style="padding:12px 16px;border-bottom:1px solid var(--border)">
<div class="toolbar-search" style="max-width:none"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg><input id="inboxSearch" placeholder="Search applications…" value="${state.q}"/></div>
</div>
<div id="inboxList"></div>
</div>
<div class="split-detail" id="inboxDetail"></div>
</div>`;
renderList(); renderDetail();
const s = document.getElementById('inboxSearch');
s.oninput = () => { state.q = s.value; renderList(); };
}
Inbox._render = { list: renderList, detail: renderDetail, body: renderBody };
Inbox._state = state;
const c = counts();
const tabHtml = tabs.map(t => `<div class="tab ${t === state.tab ? 'active' : ''}" data-tab="${t}">${t} <span class="k-count" style="margin-left:4px">${c[t]}</span></div>`).join('');
const html = `
<div class="page">
<div class="page-head">
<div><h1 class="page-title">Recruitment Inbox</h1><p class="page-sub">Every candidate, every source — one unified queue</p></div>
<div class="page-head-actions">
<span class="integration-status"><span class="pulse"></span>Microsoft Graph API · Connected</span>
<button class="btn btn-secondary" onclick="UI.toast('Syncing all sources…','info');setTimeout(()=>UI.toast('Inbox synced','success'),900)">${UI.icon('refresh')} Sync</button>
<button class="btn btn-primary" onclick="Router.go('import')">${UI.icon('upload')} Upload CVs</button>
</div>
</div>
<div class="card">
<div class="tabs" id="inboxTabs" style="margin:0 16px;padding-top:8px">${tabHtml}</div>
<div id="inboxBody"></div>
</div>
</div>`;
return {
html,
onMount() {
renderBody();
document.querySelectorAll('#inboxTabs .tab').forEach(tab => tab.onclick = () => {
state.tab = tab.dataset.tab; state.selected = null;
document.querySelectorAll('#inboxTabs .tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
renderBody();
});
}
};
};
Inbox._resumeText = function (i) {
return `${i.name.toUpperCase()}\n${i.email} · ${i.phone}\n${'—'.repeat(30)}\nPROFESSIONAL SUMMARY\n${i.experience} years of experience. Applied for ${i.position} via ${i.source}.\n\nEXPERIENCE\n${DB.pick(DB.companies)} — Senior role (2021Present)\n${DB.pick(DB.companies)} — Associate (20182021)\n\nEDUCATION\n• Bachelor's Degree, Computer Science\n\nSKILLS\n${DB.pick(DB.skillsPool)}, ${DB.pick(DB.skillsPool)}, ${DB.pick(DB.skillsPool)}, ${DB.pick(DB.skillsPool)}`;
};
Inbox.viewResume = function (id) {
const i = DB.inbox.find(x => x.id === id);
UI.modal({
title: i.attachment, subtitle: 'Resume preview · ' + i.name,
body: `<div class="resume-thumb" style="max-height:none;font-size:12px">${Inbox._resumeText(i)}</div>`,
footer: `<button class="btn btn-secondary" onclick="UI.closeModal()">Close</button><button class="btn btn-primary" onclick="UI.closeModal();Inbox.import('${id}')">${UI.icon('user-plus')} Import Candidate</button>`,
size: 'modal-lg'
});
};
Inbox.parse = function (id) {
const i = DB.inbox.find(x => x.id === id);
i.resumeStatus = 'Parsing';
Inbox._render.detail();
UI.toast('Parsing resume with AI…', 'info');
setTimeout(() => { i.resumeStatus = 'Parsed'; i.atsScore = DB.int(60, 96); Inbox._render.list(); Inbox._render.detail(); UI.toast('Resume parsed — profile fields extracted', 'success'); }, 1100);
};
Inbox.import = function (id) {
const i = DB.inbox.find(x => x.id === id);
const job = DB.getJob(i.jobId) || DB.jobs[0];
// create candidate
const newC = {
id: 'CAN-' + (5001 + DB.candidates.length), name: i.name, initials: i.initials, color: i.color,
email: i.email, phone: i.phone, jobId: job.id, jobTitle: job.title, department: job.department,
experience: i.experience, currentCompany: DB.pick(DB.companies), currentTitle: job.title, location: DB.pick(DB.locations),
stage: 'Applied', status: 'Applied', aiScore: i.atsScore, source: i.source, recruiter: i.recruiter, 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: i.atsScore, education: 80, keywords: i.atsScore, location: 100, salary: 90 },
noticePeriod: '1 month', availability: '2 weeks', certifications: [], favorite: false, interviewStatus: 'Not Scheduled'
};
DB.candidates.unshift(newC);
i.processing = 'Imported'; i.unread = false;
Inbox._render.list(); Inbox._render.detail(); App.updateBadges();
UI.toast(`${i.name} imported → Applied stage of ${job.title}`, 'success');
};
Inbox.moveToPipeline = function (id) {
const i = DB.inbox.find(x => x.id === id);
if (i.processing !== 'Imported' && i.processing !== 'Processed') Inbox.import(id);
i.processing = 'Processed';
Inbox._render.list(); Inbox._render.detail();
UI.toast(`${i.name} moved to pipeline`, 'success');
setTimeout(() => Router.go('pipeline'), 700);
};
Inbox.assign = function (id) {
const i = DB.inbox.find(x => x.id === id);
const opts = DB.recruiters.map(r => `<option ${r.name === i.recruiter ? 'selected' : ''}>${r.name}</option>`).join('');
UI.modal({
title: 'Assign Recruiter', subtitle: i.name,
body: `<div class="form-field"><label>Recruiter</label><select id="assignRec">${opts}</select></div>
<p class="text-muted text-sm" style="margin-top:10px">Current workload is factored automatically. This recruiter has ${DB.getRecruiterByName(i.recruiter) ? DB.getRecruiterByName(i.recruiter).openReqs : 5} open reqs.</p>`,
footer: `<button class="btn btn-secondary" onclick="UI.closeModal()">Cancel</button><button class="btn btn-primary" onclick="Inbox._doAssign('${id}')">Assign</button>`
});
};
Inbox._doAssign = function (id) {
const i = DB.inbox.find(x => x.id === id);
i.recruiter = document.getElementById('assignRec').value;
UI.closeModal(); Inbox._render.detail();
UI.toast('Recruiter assigned to ' + i.name, 'success');
};
Inbox.note = function (id) {
const i = DB.inbox.find(x => x.id === id);
UI.modal({
title: 'Add Note', subtitle: i.name,
body: `<div class="form-field"><label>Note</label><textarea id="inboxNote" placeholder="Add a note about this application…"></textarea></div>`,
footer: `<button class="btn btn-secondary" onclick="UI.closeModal()">Cancel</button><button class="btn btn-primary" onclick="UI.closeModal();UI.toast('Note added','success')">${UI.icon('check')} Save Note</button>`
});
};
Inbox.reject = function (id) {
const i = DB.inbox.find(x => x.id === id);
i.processing = 'Rejected'; i.unread = false;
Inbox._render.list(); Inbox._render.detail(); App.updateBadges();
UI.toast(`${i.name} rejected`, 'warning');
};
// ---------------- Email (Outlook) tab ----------------
Inbox._emailView = function () {
const st = Inbox._state;
const listHtml = DB.emails.map(e => `
<div class="inbox-item ${e.unread ? 'unread' : ''} ${st.emailSelected === e.id ? 'active' : ''}" data-email="${e.id}">
${UI.avatar(e.from, e.initials, e.color)}
<div class="ii-main">
<div class="ii-name">${e.from}</div>
<div class="ii-pos">${e.subject}</div>
<div class="ii-meta"><span class="source-chip" style="--chip:#0078d4"><svg viewBox="0 0 24 24"><path d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>Outlook</span>${e.imported ? UI.badge('Imported', 'b-green') : ''}</div>
</div>
<div class="ii-time">${DB.fmtShort(e.when)}</div>
</div>`).join('');
return `
<div style="padding:12px 18px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px">
<span class="integration-status"><span class="pulse"></span>Outlook · Microsoft Graph API</span>
<span class="text-muted text-sm">Last sync: 2 min ago · ${DB.emails.filter(e => e.unread).length} unread</span>
<button class="btn btn-secondary btn-sm" style="margin-left:auto" onclick="UI.toast('Fetching from Outlook…','info');setTimeout(()=>UI.toast('Mailbox synced','success'),800)">${UI.icon('refresh')} Sync Mailbox</button>
</div>
<div class="split">
<div class="split-list" id="emailList">${listHtml}</div>
<div class="split-detail" id="emailDetail"></div>
</div>`;
};
Inbox._bindEmail = function (state) {
const detail = document.getElementById('emailDetail');
function renderDetail() {
const e = DB.emails.find(x => x.id === state.emailSelected);
if (!e) { detail.innerHTML = `<div class="empty-state" style="padding:100px 20px">${UI.icon('mail')}<h3>Select an email</h3><p>Preview email body and resume attachments here.</p></div>`; return; }
detail.innerHTML = `<div style="padding:24px">
<div class="flex items-center gap-12" style="margin-bottom:6px">
<h2 style="font-size:18px;flex:1">${e.subject}</h2>${e.imported ? UI.badge('Imported', 'b-green') : UI.badge('New', 'b-blue')}</div>
<div class="flex items-center gap-12" style="margin-bottom:20px">
${UI.avatar(e.from, e.initials, e.color)}
<div><div class="fw-600">${e.from}</div><div class="cell-sub">${e.fromEmail} · ${DB.fmtDate(e.when)}</div></div>
</div>
<div class="email-preview" style="margin-bottom:18px">${e.body}</div>
<div class="attach-card" style="margin-bottom:18px">
<span class="attach-icn">${UI.icon('file')}</span>
<div style="flex:1"><div class="fw-600">${e.attachment}</div><div class="cell-sub">${e.attachmentSize} · PDF</div></div>
<div class="flex items-center gap-8">${UI.scoreChip(e.atsScore)}
<button class="btn btn-secondary btn-sm" onclick="UI.toast('Opening attachment preview','info')">${UI.icon('eye')} Preview</button></div>
</div>
<div class="flex gap-8">
${e.imported ? `<button class="btn btn-secondary" disabled>${UI.icon('check')} Already Imported</button>` :
`<button class="btn btn-primary" onclick="Inbox._importEmail('${e.id}')">${UI.icon('user-plus')} Import Candidate</button>`}
<button class="btn btn-secondary" onclick="UI.toast('Reply drafted','info')">${UI.icon('mail')} Reply</button>
<button class="btn btn-ghost" style="color:var(--danger)" onclick="UI.toast('Email archived','info')">${UI.icon('trash')} Archive</button>
</div>
</div>`;
}
document.querySelectorAll('#emailList .inbox-item').forEach(row => row.onclick = () => {
state.emailSelected = row.dataset.email;
const e = DB.emails.find(x => x.id === state.emailSelected); if (e) e.unread = false;
document.querySelectorAll('#emailList .inbox-item').forEach(r => r.classList.remove('active', 'unread'));
row.classList.add('active');
renderDetail(); App.updateBadges();
});
renderDetail();
};
Inbox._importEmail = function (id) {
const e = DB.emails.find(x => x.id === id);
const job = DB.getJob(e.jobId) || DB.jobs[0];
DB.candidates.unshift({
id: 'CAN-' + (5001 + DB.candidates.length), name: e.from, initials: e.initials, color: e.color,
email: e.fromEmail, phone: '+1 (555) 000-0000', jobId: job.id, jobTitle: job.title, department: job.department,
experience: DB.int(2, 10), currentCompany: DB.pick(DB.companies), currentTitle: job.title, location: DB.pick(DB.locations),
stage: 'Applied', status: 'Applied', aiScore: e.atsScore, source: 'Microsoft Outlook', 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: 130000,
matchedSkills: job.skills.slice(0, 3), missingSkills: job.skills.slice(3), recommendation: 'Potential Match',
subScores: { skills: e.atsScore, experience: 80, education: 80, keywords: e.atsScore, location: 100, salary: 90 },
noticePeriod: '1 month', availability: '2 weeks', certifications: [], favorite: false, interviewStatus: 'Not Scheduled'
});
e.imported = true; e.unread = false;
Inbox._bindEmail(Inbox._state); App.updateBadges();
UI.toast(`${e.from} imported from Outlook → ${job.title}`, 'success');
};