This is a UI preview. Connect an LLM endpoint to generate live rankings from resume + JD embeddings.
`;
- }
- if (p.includes('compare')) {
- const two = DB.candidates.slice(0, 2);
- return `We're looking for a Senior Product Designer to craft intuitive, delightful experiences across our platform. You'll own end-to-end design, from research to polished UI, and partner closely with product and engineering.
${c.experience} years of experience, currently ${c.currentTitle} at ${c.currentCompany}. Strong in ${c.skills.slice(0, 3).join(', ')}. ATS match ${c.aiScore}% for ${c.jobTitle}. ${c.recommendation}.
`;
- }
- if (p.includes('email')) {
- return `Thank you for applying. We were impressed by your background and would love to invite you to an interview. Please share your availability for this week.
Dear [Candidate], We are pleased to offer you the position of Product Manager at a base salary of $160,000, plus equity and benefits. This offer is contingent on standard background checks.
We're excited about the possibility of you joining the team.
`;
- }
- if (p.includes('skill gap')) {
- return `Consider sourcing candidates with cloud-native infra experience.
`;
- }
- if (p.includes('pipeline')) {
- const total = DB.candidates.length;
- return `Recommendation: accelerate assessment turnaround to improve velocity.
`;
- }
- if (p.includes('productivity') || p.includes('recruiter')) {
- return `I can help with ranking candidates, comparing profiles, drafting JDs, interview questions, emails, offer letters, skill-gap and pipeline analysis, and more.
This is a fully-designed interface. Wire an AI endpoint (Claude / OpenAI) into AI.send() to make responses live.
`;
-};
-
-AI._chatHtml = function (compact) {
- const promptsHtml = DB.aiPrompts.slice(0, compact ? 6 : 12).map(p =>
- `${UI.icon('users')}
-
`);
- input.value = ''; input.style.height = 'auto';
- scroll.scrollTop = scroll.scrollHeight;
-
- // typing indicator
- const typingId = 'typing_' + Math.random().toString(36).slice(2, 7);
- scroll.insertAdjacentHTML('beforeend', `
-
`);
- scroll.scrollTop = scroll.scrollHeight;
-
- setTimeout(() => {
- const t = document.getElementById(typingId);
- if (t) t.querySelector('.chat-bubble').innerHTML = `
AI Assistant
${AI._reply(text)}
`;
- scroll.scrollTop = scroll.scrollHeight;
- }, 850 + Math.random() * 500);
-};
-
-// ---------------- Full page ----------------
-Views.aiassistant = function () {
- const html = `
-
-
-
AI Assistant Your recruiting copilot — powered by AI (interface preview)
-
- Model endpoint · Not connected
- ${UI.icon('plus')} New Chat
-
-
-
-
`;
- return { html, onMount() { AI._bindChat(); } };
-};
-AI.newChat = function () {
- const mount = document.getElementById('aiChatMount');
- if (mount) { mount.innerHTML = AI._chatHtml(false); AI._bindChat(); }
- else { AI._dockOpen(true); }
-};
-
-// ---------------- Floating dock ----------------
-AI._dockOpen = function (force) {
- const dock = document.getElementById('aiDock');
- const inner = document.getElementById('aiDockInner');
- const willOpen = force || !dock.classList.contains('open');
- if (willOpen) {
- inner.innerHTML = `
-
${UI.icon('sparkles')} AI Assistant
-
- Expand
- ${UI.icon('x')}
-
${AI._chatHtml(true)}
`;
- dock.classList.add('open');
- AI._bindChat();
- } else { AI._dockClose(); }
-};
-AI._dockClose = function () { document.getElementById('aiDock').classList.remove('open'); };
-
-// ---------------- AI Studio (future modules) ----------------
-Views.aistudio = function () {
- const cards = DB.aiModules.map(m => `
-
-
-
- ${UI.icon(m.icon)}
- ${UI.badge(m.status, m.status === 'Beta' ? 'b-indigo' : 'b-gray')}
-
-
${m.name}
-
${m.desc}
-
${m.status === 'Beta' ? 'Try it' : 'Join waitlist'} ${UI.icon('arrow-right')}
-
-
`).join('');
-
- const html = `
-
-
-
AI Studio Next-generation AI modules — designed and API-ready for backend integration
-
${DB.aiModules.filter(m => m.status === 'Beta').length} in Beta
- ${UI.icon('sparkles')} Open Assistant
-
-
-
-
${UI.icon('sparkles')}
-
Everything is API-ready
-
Each module below ships with a complete, production-grade interface. Connect your model endpoint to activate them — no UI work required.
-
${UI.icon('external')} Integration Guide
-
-
-
${cards}
-
`;
- return { html };
-};
-AI.moduleDetail = function (name) {
- const m = DB.aiModules.find(x => x.name === name);
- UI.modal({
- title: m.name, subtitle: m.status + ' · AI Module',
- body: `
-
-
API Contract (preview)
-
POST /api/ai/${m.name.toLowerCase().replace(/ /g, '-')}
-{
- "context": { "jobId": "JOB-1001", "candidateIds": [...] },
- "options": { "model": "claude-opus", "stream": true }
-}
-
-→ 200 OK
-{
- "result": { ... },
- "usage": { "tokens": 1240 }
-}
-
-
${UI.icon('lock')} This feature's UI is complete. Backend wiring is the only remaining step.
`,
- footer: `
Close
-
${UI.icon('sparkles')} Try in Assistant `,
- size: 'modal-lg'
- });
-};
diff --git a/js/analytics.js b/js/analytics.js
deleted file mode 100644
index 6af8a81..0000000
--- a/js/analytics.js
+++ /dev/null
@@ -1,114 +0,0 @@
-/* ============================================================
- analytics.js — Analytics dashboard (many charts)
- ============================================================ */
-window.Views = window.Views || {};
-
-Views.analytics = function () {
- const a = DB.analytics;
-
- const html = `
-
-
-
Analytics Deep-dive metrics across your recruitment funnel
-
-
Week Month Quarter
-
${UI.icon('download')} Export
-
-
-
-
-
-
Hiring Trend Hires vs applications
-
- ${Charts.legend([{ label: 'Applications', color: Charts.PALETTE[4] }, { label: 'Hires', color: Charts.PALETTE[0] }])}
-
-
-
Applications Received Monthly volume
-
-
-
-
-
-
-
-
-
-
-
- Accepted
- Pending
- Declined
-
-
-
-
-
-
-
-
-
Applications by Department Volume per team
-
-
-
-
Recruiter Performance Hires by recruiter (top 8)
-
-
-
-
-
-
-
Time to Hire Days, monthly average
-
-
-
-
Time to Fill Days, monthly average
-
-
-
-
`;
-
- return {
- html,
- onMount() {
- Charts.line(document.getElementById('anTrend'), {
- labels: a.hiringTrend.labels, area: true,
- datasets: [
- { label: 'Applications', data: a.hiringTrend.applications, color: Charts.PALETTE[4] },
- { label: 'Hires', data: a.hiringTrend.hires, color: Charts.PALETTE[0] }
- ]
- });
- Charts.bar(document.getElementById('anApps'), { labels: a.hiringTrend.labels, data: a.hiringTrend.applications });
- Charts.doughnut(document.getElementById('anSource'), {
- labels: a.sources.map(s => s.source), data: a.sources.map(s => s.count),
- centerValue: DB.candidates.length, centerLabel: 'Total'
- });
- document.getElementById('anSourceLegend').innerHTML = a.sources.map((s, i) =>
- `
${s.source}`).join('');
- Charts.doughnut(document.getElementById('anOffer'), {
- labels: ['Accepted', 'Pending', 'Declined'],
- data: [a.offerAcceptance.accepted, a.offerAcceptance.pending, a.offerAcceptance.declined],
- colors: [Charts.token('--success'), Charts.token('--warning'), Charts.token('--danger')],
- centerValue: Math.round(a.offerAcceptance.accepted / (a.offerAcceptance.accepted + a.offerAcceptance.declined || 1) * 100) + '%',
- centerLabel: 'Accept rate'
- });
- Charts.horizontalBar(document.getElementById('anPipeline'), {
- labels: a.pipeline.map(p => p.stage), data: a.pipeline.map(p => p.count),
- colors: Charts.PALETTE
- });
- Charts.bar(document.getElementById('anDept'), { labels: a.departments.map(d => d.dept), data: a.departments.map(d => d.apps) });
- const topRecs = [...DB.recruiters].sort((x, y) => y.hires - x.hires).slice(0, 8);
- Charts.horizontalBar(document.getElementById('anRec'), { labels: topRecs.map(r => r.name), data: topRecs.map(r => r.hires) });
- Charts.line(document.getElementById('anTTH'), { labels: a.hiringTrend.labels, area: true, yFmt: v => v + 'd', datasets: [{ label: 'Time to Hire', data: a.timeToHire, color: Charts.PALETTE[0] }] });
- Charts.line(document.getElementById('anTTF'), { labels: a.hiringTrend.labels, area: true, yFmt: v => v + 'd', datasets: [{ label: 'Time to Fill', data: a.timeToFill, color: Charts.PALETTE[2] }] });
- }
- };
-};
diff --git a/js/api.js b/js/api.js
deleted file mode 100644
index 304253a..0000000
--- a/js/api.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/* ============================================================
- api.js — minimal HTTP client for the FastAPI backend
- ============================================================ */
-window.Api = {
- base: 'http://localhost:8000',
-
- async get(path, params) {
- const url = new URL(path.replace(/^\//, ''), this.base.endsWith('/') ? this.base : this.base + '/');
- if (params) {
- Object.entries(params).forEach(([key, value]) => {
- if (value !== undefined && value !== null && value !== '') {
- url.searchParams.set(key, value);
- }
- });
- }
- const res = await fetch(url.toString());
- let body = null;
- try { body = await res.json(); } catch (_) { body = null; }
- if (!res.ok) {
- const detail = body && body.detail != null ? body.detail : res.statusText;
- throw new Error(typeof detail === 'string' ? detail : JSON.stringify(detail));
- }
- return body;
- }
-};
diff --git a/js/app.js b/js/app.js
deleted file mode 100644
index a1ffce7..0000000
--- a/js/app.js
+++ /dev/null
@@ -1,317 +0,0 @@
-/* ============================================================
- app.js — Core: router, sidebar, topbar, theme, search, init
- ============================================================ */
-window.Router = {};
-window.App = {};
-
-const ROUTES = {
- dashboard: { title: 'Dashboard' }, inbox: { title: 'Recruitment Inbox' }, jobs: { title: 'Jobs' }, candidates: { title: 'Candidates' },
- talentpool: { title: 'Talent Pool' }, pipeline: { title: 'Pipeline' }, interviews: { title: 'Interviews' },
- assessments: { title: 'Assessments' }, offers: { title: 'Offers' }, managers: { title: 'Hiring Managers' },
- calendar: { title: 'Calendar' }, reports: { title: 'Reports' }, analytics: { title: 'Analytics' },
- notifications: { title: 'Notifications' }, settings: { title: 'Settings' }, help: { title: 'Help' },
- import: { title: 'CV Import' }, jobboard: { title: 'Job Board' }, recruiterhub: { title: 'Recruiter Hub' },
- aiassistant: { title: 'AI Assistant' }, aistudio: { title: 'AI Studio' }, rbac: { title: 'Access Control' },
- tasks: { title: 'Tasks' }
-};
-
-let currentRoute = 'dashboard';
-
-Router.go = function (route) {
- if (!ROUTES[route]) route = 'dashboard';
- location.hash = route;
-};
-Router.reload = function () { Router.render(currentRoute); };
-
-Router.render = function (route) {
- currentRoute = route;
- const view = (window.Views[route] || window.Views.dashboard)();
- const main = document.getElementById('main-content');
- main.innerHTML = view.html;
- main.scrollTop = 0;
- if (view.onMount) view.onMount();
-
- // Expose the route so CSS can react to it (e.g. hide the AI launcher on
- // the AI Assistant page, where it sat on top of the chat send button).
- // Deliberately NOT `data-route`: initNav() binds a click handler to every
- // [data-route] element, and matching that would fire on any click.
- document.documentElement.setAttribute('data-view', route);
-
- // active nav
- document.querySelectorAll('.nav-item').forEach(n => n.classList.toggle('active', n.dataset.route === route));
- document.title = 'TalentFlow · ' + (ROUTES[route] ? ROUTES[route].title : 'ATS');
-
- // close mobile sidebar + AI dock
- if (App.setNavOpen) App.setNavOpen(false);
- else {
- document.getElementById('sidebar').classList.remove('mobile-open');
- document.getElementById('scrim').classList.remove('open');
- }
- const dock = document.getElementById('aiDock');
- if (dock) dock.classList.remove('open');
-};
-
-function handleHash() {
- const route = (location.hash || '#dashboard').slice(1);
- Router.render(ROUTES[route] ? route : 'dashboard');
-}
-
-// ---------------- Theme ----------------
-// `persist` is false when the OS drives the change, so following the
-// system stays the default until the user makes an explicit choice.
-App.applyTheme = function (theme, persist) {
- document.documentElement.setAttribute('data-theme', theme);
- if (persist) { try { localStorage.setItem('tf-theme', theme); } catch (e) {} }
- const btn = document.getElementById('themeToggle');
- if (btn) {
- btn.setAttribute('aria-pressed', theme === 'dark' ? 'true' : 'false');
- btn.setAttribute('title', theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode');
- btn.setAttribute('aria-label', btn.getAttribute('title'));
- }
-};
-App.setTheme = function (theme) {
- App.applyTheme(theme, true);
- // re-render current view so canvas charts pick up new theme colors
- Router.render(currentRoute);
-};
-App.toggleTheme = function () {
- const cur = document.documentElement.getAttribute('data-theme');
- App.setTheme(cur === 'dark' ? 'light' : 'dark');
-};
-
-// ---------------- Session (tf-auth from /auth/) ----------------
-App.getSession = function () {
- try {
- const raw = localStorage.getItem('tf-auth');
- if (!raw) return null;
- return JSON.parse(raw);
- } catch (e) {
- return null;
- }
-};
-
-function initialsFromName(name) {
- const parts = String(name || '').trim().split(/\s+/).filter(Boolean);
- if (!parts.length) return '?';
- if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
- return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
-}
-
-App.hydrateProfile = function () {
- const session = App.getSession();
- const data = (session && session.data) || {};
- const name = data.name || 'Guest';
- const email = data.email || '';
- const role = data.role_name || data.role || 'Member';
- const initials = initialsFromName(name);
-
- document.querySelectorAll('.profile-name').forEach(el => { el.textContent = name; });
- document.querySelectorAll('.profile-role').forEach(el => { el.textContent = role; });
- document.querySelectorAll('.dp-name').forEach(el => { el.textContent = name; });
- document.querySelectorAll('.dp-email').forEach(el => { el.textContent = email; });
- document.querySelectorAll('.avatar-grad').forEach(el => { el.textContent = initials; });
-};
-
-App.signOut = function () {
- try { localStorage.removeItem('tf-auth'); } catch (e) {}
- window.location.assign('/auth/');
-};
-
-// ---------------- Toast passthrough ----------------
-App.toast = function (msg, type, title) { UI.toast(msg, type, title); };
-
-// ---------------- Badges ----------------
-App.updateBadges = function () {
- const openJobs = DB.jobs.filter(j => j.status === 'Open').length;
- setBadge('navJobsBadge', openJobs);
- const unread = DB.notifications.filter(n => n.unread).length;
- setBadge('navNotifBadge', unread);
- const emailUnread = (window.Inbox && Array.isArray(Inbox._emails))
- ? Inbox._emails.filter(e => e.unread).length
- : 0;
- const inboxUnread = DB.inbox.filter(i => i.unread).length + emailUnread;
- setBadge('navInboxBadge', inboxUnread);
- const openTasks = DB.tasks.filter(t => !t.done).length;
- setBadge('navTaskBadge', openTasks);
-};
-function setBadge(id, n) {
- const el = document.getElementById(id);
- if (!el) return;
- el.textContent = n;
- el.style.display = n ? '' : 'none';
-}
-App.markAllNotifsRead = function () {
- DB.notifications.forEach(n => n.unread = false);
- App.updateBadges();
- App.renderNotifDropdown();
- UI.toast('All notifications marked as read', 'success');
-};
-
-// ---------------- Topbar dropdown content ----------------
-App.renderNotifDropdown = function () {
- const list = document.getElementById('notifList');
- list.className = 'dd-scroll';
- list.innerHTML = DB.notifications.slice(0, 6).map(n => `
-
-
${UI.icon(n.icon)}
-
${n.title}
${n.text}
${n.time}
-
`).join('');
-};
-App.renderMessages = function () {
- const list = document.getElementById('messagesList');
- list.className = 'dd-scroll';
- list.innerHTML = DB.messages.map(m => `
-
- ${UI.avatar(m.name, m.initials, m.color)}
-
${m.name}
${m.text}
${m.time} ago
-
`).join('');
-};
-
-// ---------------- Global search ----------------
-App.search = function (q) {
- const box = document.getElementById('searchResults');
- q = q.trim().toLowerCase();
- if (!q) { box.classList.remove('open'); return; }
-
- const jobs = DB.jobs.filter(j => (j.title + j.id + j.department).toLowerCase().includes(q)).slice(0, 4);
- const cands = DB.candidates.filter(c => (c.name + c.email + c.jobTitle).toLowerCase().includes(q)).slice(0, 4);
- const mgrs = DB.managers.filter(m => m.name.toLowerCase().includes(q)).slice(0, 3);
-
- let html = '';
- if (jobs.length) html += `
Jobs
` + jobs.map(j =>
- `
${UI.icon('briefcase')} ${j.title}
${j.id} · ${j.department}
`).join('');
- if (cands.length) html += `
Candidates
` + cands.map(c =>
- `
${UI.avatar(c.name, c.initials, c.color)}
`).join('');
- if (mgrs.length) html += `
Hiring Managers
` + mgrs.map(m =>
- `
${UI.avatar(m.name, m.initials, m.color)}
`).join('');
- if (!html) html = `
No results for "${q}"
`;
-
- box.innerHTML = html;
- box.classList.add('open');
-};
-App.searchGo = function (route, cb) {
- document.getElementById('searchResults').classList.remove('open');
- document.getElementById('globalSearch').value = '';
- Router.go(route);
- if (cb) setTimeout(cb, 120);
-};
-
-// ---------------- Dropdown behavior ----------------
-function initDropdowns() {
- document.querySelectorAll('.dropdown').forEach(dd => {
- const toggle = dd.querySelector('[data-dd-toggle]');
- toggle.addEventListener('click', e => {
- e.stopPropagation();
- const wasOpen = dd.classList.contains('open');
- document.querySelectorAll('.dropdown.open').forEach(o => o.classList.remove('open'));
- if (!wasOpen) dd.classList.add('open');
- });
- dd.querySelector('[data-dd-panel]').addEventListener('click', e => e.stopPropagation());
- });
- document.addEventListener('click', () => {
- document.querySelectorAll('.dropdown.open').forEach(o => o.classList.remove('open'));
- document.getElementById('searchResults').classList.remove('open');
- });
-}
-
-// ---------------- Nav link interception ----------------
-function initNav() {
- document.querySelectorAll('[data-route]').forEach(el => {
- el.addEventListener('click', e => {
- if (el.tagName === 'A') { /* href hash handles it */ }
- const route = el.dataset.route;
- if (route) { e.preventDefault(); Router.go(route); document.querySelectorAll('.dropdown.open').forEach(o => o.classList.remove('open')); }
- });
- });
-}
-
-// ---------------- Init ----------------
-function init() {
- // Theme: an explicit past choice wins; otherwise follow the OS and keep
- // following it until the user picks a side themselves.
- const mq = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
- let saved = null;
- try { saved = localStorage.getItem('tf-theme'); } catch (e) {}
- App.applyTheme(saved || (mq && mq.matches ? 'dark' : 'light'), false);
- if (mq && !saved) {
- const onSystemChange = e => {
- let s = null;
- try { s = localStorage.getItem('tf-theme'); } catch (err) {}
- if (s) return; // user has chosen; stop following
- App.applyTheme(e.matches ? 'dark' : 'light', false);
- Router.render(currentRoute); // recolour canvas charts
- };
- mq.addEventListener ? mq.addEventListener('change', onSystemChange)
- : mq.addListener(onSystemChange);
- }
-
- document.getElementById('themeToggle').addEventListener('click', App.toggleTheme);
-
- // Canvas charts are drawn at a fixed pixel size, so they blur when the
- // window changes width. Re-render on resize — but never while a modal or
- // the AI dock is open, since that would discard what the user is doing.
- // Width only: on iOS/Android the address bar collapsing fires `resize` with
- // a height change on nearly every scroll, and re-rendering there would tear
- // the view out from under the user mid-gesture.
- let resizeTimer, lastW = window.innerWidth;
- window.addEventListener('resize', () => {
- if (window.innerWidth === lastW) return;
- lastW = window.innerWidth;
- clearTimeout(resizeTimer);
- resizeTimer = setTimeout(() => {
- const busy = document.getElementById('modalRoot').classList.contains('open')
- || document.getElementById('aiDock').classList.contains('open');
- if (!busy) Router.render(currentRoute);
- }, 220);
- }, { passive: true });
- window.addEventListener('orientationchange', () => {
- lastW = -1; // force the next resize through
- });
-
- // AI Assistant floating dock
- document.getElementById('aiFab').addEventListener('click', () => AI._dockOpen());
-
- // sidebar collapse (desktop)
- document.getElementById('sidebarCollapse').addEventListener('click', () => {
- document.getElementById('sidebar').classList.toggle('collapsed');
- });
- // Mobile nav drawer. `nav-open` on is what CSS keys off — the FAB
- // sits before .scrim in the DOM, so no sibling selector can reach it, and
- // :has() would exclude older Safari/Firefox.
- App.setNavOpen = function (open) {
- document.getElementById('sidebar').classList.toggle('mobile-open', open);
- document.getElementById('scrim').classList.toggle('open', open);
- document.documentElement.classList.toggle('nav-open', open);
- // Stop the page behind the drawer from scrolling under the user's finger.
- document.body.style.overflow = open ? 'hidden' : '';
- };
- document.getElementById('mobileMenu').addEventListener('click', () => {
- App.setNavOpen(!document.getElementById('sidebar').classList.contains('mobile-open'));
- });
- document.getElementById('scrim').addEventListener('click', () => App.setNavOpen(false));
-
- // search
- const search = document.getElementById('globalSearch');
- search.addEventListener('input', () => App.search(search.value));
- search.addEventListener('click', e => e.stopPropagation());
- document.addEventListener('keydown', e => {
- if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); search.focus(); }
- if (e.key === 'Escape') { UI.closeModal(); document.getElementById('searchResults').classList.remove('open'); document.getElementById('aiDock').classList.remove('open'); App.setNavOpen(false); }
- });
-
- initDropdowns();
- initNav();
- App.hydrateProfile();
- App.renderNotifDropdown();
- App.renderMessages();
- App.updateBadges();
-
- window.addEventListener('hashchange', handleHash);
- handleHash();
-
- // redraw charts on resize (debounced)
- let rt;
- window.addEventListener('resize', () => { clearTimeout(rt); rt = setTimeout(() => Router.reload(), 250); });
-}
-
-document.addEventListener('DOMContentLoaded', init);
diff --git a/js/assessments.js b/js/assessments.js
deleted file mode 100644
index 0f2eb55..0000000
--- a/js/assessments.js
+++ /dev/null
@@ -1,126 +0,0 @@
-/* ============================================================
- assessments.js — Assessments listing & assign
- ============================================================ */
-window.Views = window.Views || {};
-window.Assessments = {};
-
-Views.assessments = function () {
- const filters = { q: '', status: '', type: '' };
- let table;
-
- const stats = {
- total: DB.assessments.length,
- completed: DB.assessments.filter(a => a.status === 'Completed').length,
- pending: DB.assessments.filter(a => ['Pending', 'In Progress'].includes(a.status)).length,
- avg: Math.round(DB.assessments.filter(a => a.score).reduce((s, a) => s + a.score, 0) / (DB.assessments.filter(a => a.score).length || 1))
- };
-
- function apply() {
- const rows = DB.assessments.filter(a => {
- if (filters.status && a.status !== filters.status) return false;
- if (filters.type && a.type !== filters.type) return false;
- if (filters.q && !(a.candidate + a.jobTitle + a.type).toLowerCase().includes(filters.q.toLowerCase())) return false;
- return true;
- });
- table.update(rows);
- }
-
- table = UI.dataTable({
- pageSize: 8,
- rows: DB.assessments,
- columns: [
- { key: 'candidate', label: 'Candidate', sortable: true, render: a => `
${UI.avatar(a.candidate, a.initials, a.color)}
${a.candidate}
${a.jobTitle}
` },
- { key: 'type', label: 'Assessment', sortable: true, render: a => `
${a.type}
${a.duration}
` },
- { key: 'assigned', label: 'Assigned', sortable: true, sortValue: a => a.assigned.getTime(), render: a => `
${DB.fmtShort(a.assigned)} ` },
- { key: 'due', label: 'Due', sortable: true, sortValue: a => a.due.getTime(), render: a => `
${DB.fmtShort(a.due)} ` },
- { key: 'score', label: 'Score', sortable: true, align: 'center', render: a => a.score !== null ? UI.scoreChip(a.score) : '
— ' },
- { key: 'status', label: 'Status', sortable: true, render: a => UI.badge(a.status) },
- { key: '_a', label: 'Actions', align: 'right', render: a => `
-
- ${UI.icon('eye')}
- ${UI.icon('mail')}
-
` }
- ]
- });
-
- const statusOpts = ['
All Status '].concat(['Completed', 'In Progress', 'Pending', 'Expired'].map(s => `
${s} `)).join('');
- const typeOpts = ['
All Types '].concat([...new Set(DB.assessments.map(a => a.type))].map(t => `
${t} `)).join('');
- const statCard = (label, val, icn, cls) => `
${label} ${UI.icon(icn)}
${val}
`;
-
- const html = `
-
-
-
Assessments Coding tests, take-homes, and evaluations
-
${UI.icon('plus')} Assign Assessment
-
-
- ${statCard('Total Assigned', stats.total, 'file', 'i-indigo')}
- ${statCard('Completed', stats.completed, 'check-circle', 'i-green')}
- ${statCard('In Progress / Pending', stats.pending, 'clock', 'i-amber')}
- ${statCard('Average Score', stats.avg + '%', 'target', 'i-teal')}
-
-
-
`;
-
- return {
- html,
- onMount() {
- table.mount();
- const s = document.getElementById('asSearch');
- s.oninput = () => { filters.q = s.value; apply(); };
- document.getElementById('asStatus').onchange = e => { filters.status = e.target.value; apply(); };
- document.getElementById('asType').onchange = e => { filters.type = e.target.value; apply(); };
- }
- };
-};
-
-Assessments.view = function (id) {
- const a = DB.assessments.find(x => x.id === id);
- const body = `
-
${UI.avatar(a.candidate, a.initials, a.color, 'avatar-lg')}
-
${a.candidate}
${a.type} · ${a.jobTitle}
-
${UI.badge(a.status)}
-
-
-
-
Assigned
${DB.fmtDate(a.assigned)}
-
-
- ${a.score !== null ? `
-
-
-
${a.score}%
-
Overall Score
-
-
${UI.pbar(a.score)}
-
Section Breakdown
- ${['Problem Solving', 'Code Quality', 'Communication', 'Time Management'].map(sec => {
- const sc = DB.int(60, 98);
- return `
${sec} ${UI.pbar(sc)}
${sc}% `;
- }).join('')}` : `
${UI.icon('clock')}
Assessment not completed Results will appear once the candidate submits.
`}`;
- const footer = `
Close
-
View Candidate `;
- UI.modal({ title: 'Assessment Result', subtitle: a.id, body, footer });
-};
-
-Assessments.assign = function () {
- const opt = arr => arr.map(o => `
${o} `).join('');
- const body = `
`;
- const footer = `
Cancel
-
${UI.icon('send')} Assign `;
- UI.modal({ title: 'Assign Assessment', subtitle: 'Send an evaluation to a candidate', body, footer });
-};
diff --git a/js/candidates.js b/js/candidates.js
deleted file mode 100644
index ad80747..0000000
--- a/js/candidates.js
+++ /dev/null
@@ -1,441 +0,0 @@
-/* ============================================================
- candidates.js — Candidate list, filters, profile modal w/ tabs
- ============================================================ */
-window.Views = window.Views || {};
-window.Candidates = {};
-
-Views.candidates = function () {
- const f = { q: '', job: '', skill: '', dept: '', location: '', exp: '', edu: '', recruiter: '', manager: '', source: '', ats: '', stage: '', interview: '', notice: '', availability: '' };
- const selected = new Set();
- let sortMode = 'relevance';
- let table;
- Candidates._selected = selected;
-
- function relevance(c) {
- // composite relevance: ATS + matched-skill ratio + recency
- const req = (DB.getJob(c.jobId) || {}).skills || [];
- const skillRatio = req.length ? c.matchedSkills.length / req.length : 0.5;
- const recency = 1 - Math.min(1, (new Date('2026-07-09') - c.applied) / (90 * 864e5));
- return Math.round(c.aiScore * 0.7 + skillRatio * 20 + recency * 10);
- }
-
- function apply() {
- let rows = DB.candidates.filter(c => {
- if (f.job && c.jobTitle !== f.job) return false;
- if (f.skill && !c.skills.includes(f.skill)) return false;
- if (f.dept && c.department !== f.dept) return false;
- if (f.location && c.location !== f.location) return false;
- if (f.exp === '0-2' && c.experience > 2) return false;
- if (f.exp === '3-5' && (c.experience < 3 || c.experience > 5)) return false;
- if (f.exp === '6-9' && (c.experience < 6 || c.experience > 9)) return false;
- if (f.exp === '10+' && c.experience < 10) return false;
- if (f.edu && c.education !== f.edu) return false;
- if (f.recruiter && c.recruiter !== f.recruiter) return false;
- if (f.manager) { const job = DB.getJob(c.jobId); if (!job || job.manager !== f.manager) return false; }
- if (f.source && c.source !== f.source) return false;
- if (f.ats === '85+' && c.aiScore < 85) return false;
- if (f.ats === '70-84' && (c.aiScore < 70 || c.aiScore > 84)) return false;
- if (f.ats === '<70' && c.aiScore >= 70) return false;
- if (f.stage && c.stage !== f.stage) return false;
- if (f.interview && c.interviewStatus !== f.interview) return false;
- if (f.notice && c.noticePeriod !== f.notice) return false;
- if (f.availability && c.availability !== f.availability) return false;
- if (f.q) { const q = f.q.toLowerCase(); if (!(c.name + c.email + c.jobTitle + c.currentCompany + c.recruiter + c.skills.join(' ')).toLowerCase().includes(q)) return false; }
- return true;
- });
- if (sortMode === 'relevance') rows = [...rows].sort((a, b) => relevance(b) - relevance(a));
- else if (sortMode === 'ats') rows = [...rows].sort((a, b) => b.aiScore - a.aiScore);
- else if (sortMode === 'recent') rows = [...rows].sort((a, b) => b.applied - a.applied);
- else if (sortMode === 'name') rows = [...rows].sort((a, b) => a.name.localeCompare(b.name));
- table.update(rows);
- updateBulkBar();
- const cnt = document.getElementById('canResultCount');
- if (cnt) cnt.textContent = rows.length + ' candidate' + (rows.length === 1 ? '' : 's');
- }
-
- function updateBulkBar() {
- const bar = document.getElementById('bulkBar');
- if (!bar) return;
- if (selected.size) { bar.style.display = 'flex'; document.getElementById('bulkCount').textContent = selected.size + ' selected'; }
- else bar.style.display = 'none';
- }
-
- table = UI.dataTable({
- pageSize: 10,
- rows: DB.candidates,
- columns: [
- { key: '_sel', label: '', render: c => `
${UI.icon('check')} ` },
- { key: 'name', label: 'Candidate', sortable: true, render: c => `
${UI.avatar(c.name, c.initials, c.color)}
${c.name} ${c.favorite ? '' + UI.icon('star') + ' ' : ''}
${c.currentTitle} · ${c.location}
` },
- { key: 'jobTitle', label: 'Applied Job', sortable: true, render: c => `
${c.jobTitle}
${c.department}
` },
- { key: 'experience', label: 'Exp', sortable: true, align: 'center', render: c => `
${c.experience} y` },
- { key: '_rel', label: 'Relevance', sortable: true, align: 'center', sortValue: c => relevance(c), render: c => `
${relevance(c)}% ` },
- { key: 'stage', label: 'Stage', sortable: true, render: c => UI.badge(c.stage) },
- { key: 'aiScore', label: 'ATS', sortable: true, align: 'center', render: c => `
${UI.scoreChip(c.aiScore)} ` },
- { key: 'availability', label: 'Availability', render: c => `
${c.availability} ${c.noticePeriod} notice
` },
- { key: '_a', label: 'Actions', align: 'right', render: c => `
-
- ${UI.icon('star')}
- ${UI.icon('target')}
- ${UI.icon('eye')}
- ${UI.icon('check')}
-
` }
- ],
- onRender(el) {
- el.querySelectorAll('[data-sel]').forEach(chk => chk.onclick = () => {
- const id = chk.dataset.sel;
- if (selected.has(id)) selected.delete(id); else selected.add(id);
- chk.classList.toggle('on'); updateBulkBar();
- });
- el.querySelectorAll('[data-fav]').forEach(st => st.onclick = () => {
- const c = DB.getCandidate(st.dataset.fav); c.favorite = !c.favorite;
- st.classList.toggle('on'); UI.toast(c.favorite ? c.name + ' added to favorites' : 'Removed from favorites', 'success');
- });
- }
- });
-
- const optList = (arr, label) => [`
${label} `].concat(arr.map(o => `
${o} `)).join('');
- const jobTitles = [...new Set(DB.candidates.map(c => c.jobTitle))];
-
- const filterPanel = `
-
-
Job ${optList(jobTitles, 'Any Job')}
-
Skill ${optList(DB.skillsPool, 'Any Skill')}
-
Department ${optList(DB.departments, 'Any Dept')}
-
Location ${optList(DB.locations, 'Any Location')}
-
Experience ${optList(['0-2', '3-5', '6-9', '10+'], 'Any Exp')}
-
Education ${optList(DB.educationLevels, 'Any')}
-
Recruiter ${optList(DB.recruiters.map(r => r.name), 'Any Recruiter')}
-
Hiring Manager ${optList(DB.managers.map(m => m.name), 'Any Manager')}
-
Source ${optList(DB.sources, 'Any Source')}
-
ATS Score ${optList(['85+', '70-84', '<70'], 'Any Score')}
-
Pipeline Stage ${optList(DB.stages, 'Any Stage')}
-
Interview Status ${optList(['Not Scheduled', 'Scheduled', 'Completed'], 'Any')}
-
Notice Period ${optList(['Immediate', '2 weeks', '1 month', '2 months', '3 months'], 'Any')}
-
Availability ${optList(['Immediate', '2 weeks', '1 month', 'Passive'], 'Any')}
-
`;
-
- // recently viewed strip
- const rv = DB.recentlyViewed.slice(0, 6).map(id => DB.getCandidate(id)).filter(Boolean);
- const rvHtml = rv.length ? `
- Recently viewed:
- ${rv.map(c => `${UI.avatar(c.name, c.initials, c.color)} ${c.name.split(' ')[0]} `).join('')}
-
` : '';
-
- const html = `
-
-
-
Candidates ${DB.candidates.length} candidates · ranked by AI relevance
-
- ${UI.icon('bookmark')} Save Search
- ${UI.icon('download')} Export
- ${UI.icon('plus')} Add Candidate
-
-
- ${rvHtml}
-
-
${UI.icon('check')}
-
0 selected
-
-
${UI.icon('mail')} Bulk Email
-
${UI.icon('users')} Assign
-
${UI.icon('check')} Advance
-
${UI.icon('x')} Reject
-
${UI.icon('x')} Clear
-
-
-
`;
-
- return {
- html,
- onMount() {
- table.mount();
- apply();
- const s = document.getElementById('canSearch');
- s.oninput = () => { f.q = s.value; apply(); };
- document.getElementById('canSort').onchange = e => { sortMode = e.target.value; apply(); };
- document.getElementById('filterToggle').onclick = () => {
- const p = document.getElementById('filterPanel');
- p.style.display = p.style.display === 'none' ? 'grid' : 'none';
- };
- document.querySelectorAll('#filterPanel [data-f]').forEach(sel => sel.onchange = e => { f[e.target.dataset.f] = e.target.value; apply(); });
- }
- };
-};
-
-// ---------------- Bulk actions ----------------
-Candidates.bulk = function (action) {
- const ids = [...Candidates._selected];
- if (!ids.length) return;
- if (action === 'email') UI.toast(`Bulk email drafted to ${ids.length} candidates`, 'success');
- else if (action === 'assign') {
- const opts = DB.recruiters.map(r => `
${r.name} `).join('');
- UI.modal({ title: 'Bulk Assign Recruiter', subtitle: ids.length + ' candidates',
- body: `
Assign to ${opts}
`,
- footer: `
Cancel Assign ` });
- return;
- }
- else if (action === 'advance') { ids.forEach(id => Candidates._advanceSilent(id)); UI.toast(`${ids.length} candidates advanced`, 'success'); Router.reload(); return; }
- else if (action === 'reject') { ids.forEach(id => { const c = DB.getCandidate(id); c.stage = 'Rejected'; c.status = 'Rejected'; }); UI.toast(`${ids.length} candidates rejected`, 'warning'); Router.reload(); return; }
- Candidates.bulkClear();
-};
-Candidates._bulkAssign = function () {
- const rec = document.getElementById('bulkRec').value;
- [...Candidates._selected].forEach(id => { DB.getCandidate(id).recruiter = rec; });
- UI.closeModal(); UI.toast('Recruiter assigned to selected candidates', 'success');
- Candidates.bulkClear(); Router.reload();
-};
-Candidates.bulkClear = function () { Candidates._selected.clear(); Router.reload(); };
-Candidates._advanceSilent = function (id) {
- const c = DB.getCandidate(id);
- const order = ['Applied', 'Screening', 'Assessment', 'Interview', 'Offer', 'Hired'];
- const i = order.indexOf(c.stage);
- if (i > -1 && i < order.length - 1) { c.stage = order[i + 1]; c.status = c.stage; }
-};
-
-// ---------------- ATS Match detail ----------------
-Candidates.atsMatch = function (id) {
- const c = DB.getCandidate(id);
- const job = DB.getJob(c.jobId) || {};
- const recCls = c.recommendation === 'Strong Match' ? 'recc-strong' : c.recommendation === 'Potential Match' ? 'recc-potential' : 'recc-weak';
- const ringColor = c.aiScore >= 82 ? 'var(--success)' : c.aiScore >= 65 ? 'var(--warning)' : 'var(--danger)';
- const sub = c.subScores;
- const subRow = (label, val) => `
${label} ${UI.pbar(val)}
${val}% `;
-
- const body = `
-
-
${UI.icon(c.recommendation === 'Weak Match' ? 'x-circle' : 'check-circle')}
-
${c.recommendation}
-
${c.name} for ${c.jobTitle}
-
-
-
-
- ${subRow('Skills', sub.skills)}
- ${subRow('Experience', sub.experience)}
- ${subRow('Education', sub.education)}
- ${subRow('Keywords', sub.keywords)}
- ${subRow('Location', sub.location)}
- ${subRow('Salary', sub.salary)}
-
-
-
Matched Skills (${c.matchedSkills.length})
-
${c.matchedSkills.length ? c.matchedSkills.map(s => `${UI.icon('check')} ${s} `).join('') : '— '}
-
Missing Skills (${c.missingSkills.length})
-
${c.missingSkills.length ? c.missingSkills.map(s => `${UI.icon('x')} ${s} `).join('') : 'None — full match '}
-
-
${UI.icon('sparkles')} Score computed from JD keywords, resume parsing, experience, education, location and salary alignment. Connect an AI model to refine with semantic matching.
`;
- UI.modal({
- title: 'ATS Match Analysis', subtitle: c.id + ' · ' + c.jobTitle, body, size: 'modal-lg',
- footer: `
Close View Full Profile `
- });
-};
-
-Candidates.toggleFav = function (id, btn) {
- const c = DB.getCandidate(id); c.favorite = !c.favorite;
- if (btn) { btn.classList.toggle('on'); btn.innerHTML = UI.icon('star') + (c.favorite ? ' Favorited' : ' Favorite'); }
- UI.toast(c.favorite ? c.name + ' added to favorites' : 'Removed from favorites', 'success');
-};
-
-Candidates.advance = function (id) {
- const c = DB.getCandidate(id);
- const order = ['Applied', 'Screening', 'Assessment', 'Interview', 'Offer', 'Hired'];
- const i = order.indexOf(c.stage);
- if (i === -1 || i >= order.length - 1) { UI.toast(c.name + ' cannot be advanced further', 'warning'); return; }
- c.stage = order[i + 1]; c.status = c.stage;
- UI.toast(`${c.name} moved to ${c.stage}`, 'success');
- Router.reload();
-};
-
-// ---------------- Candidate profile w/ tabs ----------------
-Candidates.openProfile = function (id) {
- const c = DB.getCandidate(id);
- // track recently viewed
- const rvIdx = DB.recentlyViewed.indexOf(id);
- if (rvIdx > -1) DB.recentlyViewed.splice(rvIdx, 1);
- DB.recentlyViewed.unshift(id);
- if (DB.recentlyViewed.length > 12) DB.recentlyViewed.pop();
- const tabs = ['Overview', 'Resume', 'Timeline', 'Interview', 'Notes', 'Activity', 'Documents', 'Feedback'];
- const body = `
-
- ${UI.avatar(c.name, c.initials, c.color, 'avatar-lg')}
-
-
${c.name}
-
${c.currentTitle} at ${c.currentCompany}
-
${UI.badge(c.stage)} ${UI.badge(c.source, 'b-gray')}
- ${c.experience} yrs exp
-
-
${UI.scoreChip(c.aiScore)}
AI Match
-
-
- ${tabs.map((t, i) => `
${t}
`).join('')}
-
-
- ${Candidates._pane('Overview', c)}
- ${Candidates._pane('Resume', c)}
- ${Candidates._pane('Timeline', c)}
- ${Candidates._pane('Interview', c)}
- ${Candidates._pane('Notes', c)}
- ${Candidates._pane('Activity', c)}
- ${Candidates._pane('Documents', c)}
- ${Candidates._pane('Feedback', c)}
-
`;
- const footer = `
${UI.icon('star')} ${c.favorite ? 'Favorited' : 'Favorite'}
-
${UI.icon('target')} ATS Match
-
${UI.icon('mail')} Message
-
${UI.icon('check')} Advance Stage `;
- UI.modal({ title: 'Candidate Profile', subtitle: c.id, body, footer, size: 'modal-lg' });
-
- const paneEls = document.querySelectorAll('#canPanes .tab-pane');
- document.querySelectorAll('#canTabs .tab').forEach(tab => tab.onclick = () => {
- document.querySelectorAll('#canTabs .tab').forEach(t => t.classList.remove('active'));
- tab.classList.add('active');
- paneEls.forEach(p => p.classList.remove('active'));
- paneEls[+tab.dataset.tab].classList.add('active');
- });
-};
-
-Candidates._pane = function (name, c) {
- const active = name === 'Overview' ? 'active' : '';
- let content = '';
- if (name === 'Overview') {
- content = `
-
-
-
-
-
-
Current Company
${c.currentCompany}
-
Experience
${c.experience} years
-
-
-
-
Applied On
${DB.fmtDate(c.applied)}
-
Expected Salary
${DB.moneyK(c.salary)}
-
Rating
⭐ ${c.rating} / 5.0
-
-
Skills
-
${c.skills.map(s => `${s} `).join('')}
`;
- } else if (name === 'Resume') {
- content = `
-
${c.name} ${c.currentTitle} · ${c.location}
-
-
Summary
-
Results-driven ${c.currentTitle.toLowerCase()} with ${c.experience} years of experience across ${c.department.toLowerCase()}. Passionate about building high-quality products and collaborating with cross-functional teams.
-
Experience
-
${c.currentTitle} — ${c.currentCompany}
2021 – Present
-
Associate — ${DB.pick(DB.companies)}
2018 – 2021
-
Education
-
${c.education}
-
-
${UI.icon('download')} Download PDF `;
- } else if (name === 'Timeline') {
- const events = [
- { icon: 'user-plus', title: 'Application received', meta: DB.fmtDate(c.applied), desc: `Applied via ${c.source}` },
- { icon: 'star', title: 'AI screening completed', meta: '1 day later', desc: `Match score: ${c.aiScore}%` },
- { icon: 'phone', title: 'Recruiter screen', meta: '3 days later', desc: `Call with ${c.recruiter}` },
- { icon: 'calendar', title: 'Technical interview', meta: '1 week later', desc: 'Panel of 3 interviewers' },
- { icon: 'check', title: `Moved to ${c.stage}`, meta: 'Recently', desc: 'Current stage in pipeline' }
- ];
- content = `
${events.map(e => `
-
${UI.icon(e.icon)}
-
${e.title}
${e.meta}
${e.desc}
`).join('')}
`;
- } else if (name === 'Interview') {
- const ivs = DB.interviews.filter(i => i.candidateId === c.id);
- content = ivs.length ? `
${ivs.map(iv => `
-
${UI.icon('calendar')}
-
${iv.type}
${DB.fmtDate(iv.when)} · ${iv.meeting}
-
${UI.badge(iv.status)}
`).join('')}
`
- : `
${UI.icon('calendar')}
No interviews scheduled Schedule an interview to get started.
-
Schedule Interview `;
- } else if (name === 'Notes') {
- content = `
-
Add a note
-
${UI.icon('plus')} Add Note
-
-
${UI.avatar(c.recruiter)}
${c.recruiter}
Strong communication skills, great culture fit. Recommend advancing.
2 days ago
-
${UI.avatar('Asfand Ahmed', 'AA')}
Asfand Ahmed
Reviewed portfolio — impressive work. Schedule technical round.
4 days ago
-
`;
- } else if (name === 'Activity') {
- content = `
-
${UI.icon('eye')} Profile viewed by ${c.recruiter}
1h ago
-
${UI.icon('mail')} Email sent: Interview invitation
1 day ago
-
${UI.icon('star')} Assessment score updated to ${c.aiScore}%
2 days ago
-
${UI.icon('user-plus')} Applied for ${c.jobTitle}
${DB.fmtDate(c.applied)}
-
`;
- } else if (name === 'Documents') {
- const docs = [{ n: 'Resume.pdf', s: '284 KB' }, { n: 'Cover_Letter.pdf', s: '112 KB' }, { n: 'Portfolio.pdf', s: '4.2 MB' }, { n: 'References.docx', s: '48 KB' }];
- content = `
${docs.map(d => `
-
${UI.icon('file')}
-
-
${UI.icon('download')} `).join('')}
`;
- } else if (name === 'Feedback') {
- const scores = ['Strong Hire', 'Hire', 'Lean Hire'];
- content = `
- ${[0, 1, 2].map(i => `
${UI.avatar(DB.recruiters[i].name, DB.recruiters[i].initials, DB.recruiters[i].color)}
-
${DB.recruiters[i].name}
${['Excellent technical depth and clear communication.', 'Good problem solving, would benefit from more system design exposure.', 'Solid candidate, positive team energy.'][i]}
-
${UI.badge(scores[i])}
`).join('')}
-
-
${UI.icon('plus')} Submit Scorecard `;
- }
- return `
${content}
`;
-};
-
-Candidates.openAdd = function () {
- const opt = (arr) => arr.map(o => `
${o} `).join('');
- const body = `
`;
- const footer = `
Cancel
-
${UI.icon('check')} Add Candidate `;
- UI.modal({ title: 'Add Candidate', subtitle: 'Manually add a candidate to the pipeline', body, footer });
-};
-Candidates._add = function () {
- const form = document.getElementById('canForm');
- UI.clearErrors(form);
- const f = Object.fromEntries(new FormData(form));
- let ok = true;
- if (!f.name.trim()) { UI.fieldError(form.querySelector('[name=name]'), 'Required'); ok = false; }
- if (!/^\S+@\S+\.\S+$/.test(f.email)) { UI.fieldError(form.querySelector('[name=email]'), 'Valid email required'); ok = false; }
- if (!ok) { UI.toast('Please fix the highlighted fields', 'error'); return; }
- const job = DB.jobs.find(j => j.title === f.job) || DB.jobs[0];
- const score = DB.int(55, 95);
- DB.candidates.unshift({
- id: 'CAN-' + (5001 + DB.candidates.length), name: f.name, initials: DB.initials(f.name), color: DB.avatarColor(f.name),
- email: f.email, phone: f.phone || '+1 (555) 000-0000', jobId: job.id, jobTitle: job.title, department: job.department,
- experience: +f.experience || 1, currentCompany: f.company || '—', currentTitle: job.title, location: job.location,
- stage: f.stage, status: f.stage, aiScore: score, source: f.source, recruiter: job.recruiter, recruiterId: job.recruiterId,
- applied: new Date('2026-07-09'), education: "Bachelor's Degree", skills: job.skills.slice(0, 4), rating: '4.0', salary: 120000,
- matchedSkills: job.skills.slice(0, 3), missingSkills: job.skills.slice(3), recommendation: score >= 82 ? 'Strong Match' : score >= 65 ? 'Potential Match' : 'Weak Match',
- subScores: { skills: score, experience: 80, education: 80, keywords: score, location: 100, salary: 90 },
- noticePeriod: '1 month', availability: '2 weeks', certifications: [], favorite: false, interviewStatus: 'Not Scheduled'
- });
- UI.closeModal();
- UI.toast('Candidate added to pipeline', 'success');
- Router.reload();
-};
diff --git a/js/dashboard.js b/js/dashboard.js
deleted file mode 100644
index fc99eb7..0000000
--- a/js/dashboard.js
+++ /dev/null
@@ -1,170 +0,0 @@
-/* ============================================================
- dashboard.js — Main dashboard view
- ============================================================ */
-window.Views = window.Views || {};
-
-Views.dashboard = function () {
- const k = DB.kpis;
- const kpiCards = [
- { label: 'Open Jobs', value: k.openJobs, icon: 'briefcase', cls: 'i-indigo', trend: '+8%', dir: 'up', foot: 'vs last month' },
- { label: 'Total Candidates', value: k.totalCandidates, icon: 'users', cls: 'i-blue', trend: '+12%', dir: 'up', foot: 'active in pipeline' },
- { label: 'Interviews Today', value: k.interviewsToday, icon: 'calendar', cls: 'i-purple', trend: '3 upcoming', dir: 'flat', foot: 'next at 2:00 PM' },
- { label: 'Offers Accepted', value: k.offersAccepted, icon: 'check-circle', cls: 'i-green', trend: '+5%', dir: 'up', foot: `of ${k.offersSent} sent` }
- ];
- const kpiCards2 = [
- { label: 'Time to Hire', value: k.timeToHire + ' days', icon: 'clock', cls: 'i-teal', trend: '-3 days', dir: 'up', foot: 'faster than target' },
- { label: 'Time to Fill', value: k.timeToFill + ' days', icon: 'target', cls: 'i-amber', trend: '-2 days', dir: 'up', foot: '41 day average' },
- { label: 'Cost per Hire', value: DB.money(k.costPerHire), icon: 'dollar', cls: 'i-red', trend: '+4%', dir: 'down', foot: 'above budget' },
- { label: 'Closed Jobs', value: k.closedJobs + k.hires, icon: 'award', cls: 'i-purple', trend: '+15%', dir: 'up', foot: 'this quarter' }
- ];
-
- function trendHtml(dir, txt) {
- if (dir === 'flat') return `
${txt} `;
- const cls = dir === 'up' ? 'trend-up' : 'trend-down';
- const ic = dir === 'up' ? 'trending-up' : 'trending-down';
- return `
${UI.icon(ic)}${txt} `;
- }
- const kpiHtml = arr => arr.map(c => `
-
-
- ${c.label}
- ${UI.icon(c.icon)}
-
-
${c.value}
-
-
`).join('');
-
- // upcoming interviews
- const upcoming = DB.interviews.filter(iv => iv.status === 'Scheduled').slice(0, 5);
- const upcomingHtml = upcoming.length ? upcoming.map(iv => `
-
- ${UI.avatar(iv.candidate, iv.candInitials, iv.color)}
-
-
${iv.candidate}
-
${iv.type} · ${iv.jobTitle}
-
-
-
${DB.fmtShort(iv.when)}
-
${iv.when.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}
-
-
`).join('') : '
No upcoming interviews
';
-
- // recent applications
- const recentApps = [...DB.candidates].sort((a, b) => b.applied - a.applied).slice(0, 5);
- const recentHtml = recentApps.map(c => `
-
- ${UI.avatar(c.name, c.initials, c.color)}
-
-
${c.name}
-
${c.jobTitle}
-
-
${UI.scoreChip(c.aiScore)}
-
`).join('');
-
- // activity feed
- const activityHtml = DB.activity.slice(0, 8).map(a => `
-
-
${UI.icon(a.icon)}
-
-
${a.html}
-
${DB.relTime(a.time)}
-
-
`).join('');
-
- // recruiter performance
- const topRecs = [...DB.recruiters].sort((a, b) => b.hires - a.hires).slice(0, 5);
- const recPerf = topRecs.map(r => `
-
- ${UI.avatar(r.name, r.initials, r.color)}
-
-
${r.name}
-
${r.openReqs} open reqs · ${r.avgTimeToHire}d avg
-
-
-
`).join('');
-
- const html = `
-
-
-
-
Good morning, Asfand 👋
-
Here's what's happening with your hiring today — Thursday, July 9, 2026
-
-
- ${UI.icon('download')} Export
- ${UI.icon('plus')} Create Job
-
-
-
-
${kpiHtml(kpiCards)}
-
${kpiHtml(kpiCards2)}
-
-
-
-
-
Hiring Trend Hires vs applications over the last 7 months
-
7M 1Y
-
-
-
- ${Charts.legend([{ label: 'Applications', color: Charts.PALETTE[4] }, { label: 'Hires', color: Charts.PALETTE[0] }])}
-
-
-
-
Candidate Pipeline Active by stage
-
-
-
-
-
-
-
Upcoming Interviews Next scheduled sessions
-
View all
-
-
-
-
Source Analytics Where candidates come from
-
-
-
-
-
-
-
Recent Applications
-
View all
-
-
-
-
-
-
`;
-
- return {
- html,
- onMount() {
- const a = DB.analytics;
- Charts.line(document.getElementById('chartTrend'), {
- labels: a.hiringTrend.labels, area: true,
- datasets: [
- { label: 'Applications', data: a.hiringTrend.applications, color: Charts.PALETTE[4] },
- { label: 'Hires', data: a.hiringTrend.hires, color: Charts.PALETTE[0] }
- ]
- });
- Charts.horizontalBar(document.getElementById('chartPipeline'), {
- labels: a.pipeline.map(p => p.stage), data: a.pipeline.map(p => p.count),
- colors: Charts.PALETTE
- });
- Charts.bar(document.getElementById('chartSource'), {
- labels: a.sources.map(s => s.source), data: a.sources.map(s => s.count)
- });
- }
- };
-};
diff --git a/js/import.js b/js/import.js
deleted file mode 100644
index a9998c9..0000000
--- a/js/import.js
+++ /dev/null
@@ -1,176 +0,0 @@
-/* ============================================================
- 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 = `
-
-
-
CV Import Upload resumes — we parse, score, match, and dedupe automatically
-
- AI Resume Parser · Ready
-
-
-
-
-
-
-
-
${UI.icon('upload')}
-
Drag & drop resumes here
-
or click to browse — PDF, DOC, DOCX and ZIP supported · up to 20 files
-
${UI.icon('upload')} Browse Files
-
- ${['PDF', 'DOC', 'DOCX', 'ZIP'].map(t => `${t} `).join('')}
-
-
-
- ${UI.icon('sparkles')} Simulate 3 files
- ${UI.icon('layers')} Simulate ZIP (8 CVs)
- Files are processed locally in this demo
-
-
-
-
-
Processing Queue 0 files
-
${UI.icon('check')} Import All
-
-
-
-
-
-
Auto-Processing What happens on upload
-
- ${[
- { 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 => `
${UI.icon(s.i)}
${s.t}
${s.d}
`).join('')}
-
-
-
-
`;
-
- 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 => `
-
-
${UI.icon('file')}
-
-
${i.name}
- ${i.duplicate ? 'DUPLICATE ' : ''}
-
${i.file} · ${i.size}
- ${i.status === 'Uploading' || i.status === 'Parsing' ? `
` :
- `
Best match: ${i.job.title}
`}
-
-
- ${i.status === 'Ready' ? UI.scoreChip(i.atsScore) : `${i.status}${i.status === 'Uploading' ? ' ' + i.progress + '%' : ''} `}
-
-
- ${i.imported ? `Imported ` :
- i.status === 'Ready' ? `Import ` :
- `${UI.icon('clock')} `}
-
-
`).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: `
${UI.icon('users')}
-
A similar candidate already exists
-
${i.name} matches an existing profile (95% similarity on name + email). Importing will create a duplicate.
`,
- footer: `
Cancel
-
Merge
-
Import Anyway `
- });
- 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');
-};
diff --git a/js/inbox.js b/js/inbox.js
deleted file mode 100644
index 28e0008..0000000
--- a/js/inbox.js
+++ /dev/null
@@ -1,419 +0,0 @@
-/* ============================================================
- 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': (Inbox._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 `
${item.source}`;
- }
-
- function renderList() {
- const el = document.getElementById('inboxList');
- if (!el) return;
- const list = filtered();
- if (!list.length) { el.innerHTML = `
${UI.icon('inbox')}
Nothing here No applications in this view.
`; return; }
- el.innerHTML = list.map(i => `
-
- ${UI.avatar(i.name, i.initials, i.color)}
-
-
${i.name} ${i.duplicate ? 'DUP ' : ''}
-
${i.position}
-
${sourceChip(i)} ${UI.badge(i.processing)}
-
-
-
${DB.relTime(Math.round((new Date('2026-07-09T20:00') - i.received) / 60000))}
-
${UI.scoreChip(i.atsScore)}
-
-
`).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 = `
${UI.icon('inbox')}
Select an application Choose an item from the list to view details and take action.
`; return; }
- const rec = DB.atsRecommendationClass;
- const recLabel = i.atsScore >= 82 ? 'Strong Match' : i.atsScore >= 65 ? 'Potential Match' : 'Weak Match';
- el.innerHTML = `
-
-
- ${UI.avatar(i.name, i.initials, i.color, 'avatar-lg')}
-
${i.name}
-
${i.position}
-
${sourceChip(i)} ${UI.badge(i.processing)} ${UI.badge(i.resumeStatus, i.resumeStatus === 'Parsed' ? 'b-green' : i.resumeStatus === 'Failed' ? 'b-red' : 'b-amber')}
-
-
-
-
-
-
-
-
Experience
${i.experience} years
-
Assigned Recruiter
${i.recruiter}
-
Received
${DB.fmtDate(i.received)}
-
Match
${UI.badge(recLabel, rec(recLabel))}
-
-
-
-
-
${UI.icon('paperclip')} ${i.attachment}
-
${UI.icon('eye')} Preview
-
-
${Inbox._resumeText(i)}
-
-
-
- ${UI.icon('user-plus')} Import Candidate
- ${UI.icon('sparkles')} Parse Resume
- ${UI.icon('users')} Assign Recruiter
- ${UI.icon('layers')} Move to Pipeline
- ${UI.icon('edit')} Add Note
- ${UI.icon('x')} Reject
-
-
`;
- }
-
- function renderBody() {
- const body = document.getElementById('inboxBody');
- if (state.tab === 'Email') { body.innerHTML = Inbox._emailView(); Inbox._bindEmail(state); return; }
- body.innerHTML = `
-
`;
- 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 => `
${t} ${c[t]}
`).join('');
-
- const html = `
-
-
-
Recruitment Inbox Every candidate, every source — one unified queue
-
- Microsoft Graph API · Connected
- ${UI.icon('refresh')} Sync
- ${UI.icon('upload')} Upload CVs
-
-
-
-
`;
-
- 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 (2021–Present)\n• ${DB.pick(DB.companies)} — Associate (2018–2021)\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: `
${Inbox._resumeText(i)}
`,
- footer: `
Close ${UI.icon('user-plus')} Import Candidate `,
- 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 => `
${r.name} `).join('');
- UI.modal({
- title: 'Assign Recruiter', subtitle: i.name,
- body: `
Recruiter ${opts}
-
Current workload is factored automatically. This recruiter has ${DB.getRecruiterByName(i.recruiter) ? DB.getRecruiterByName(i.recruiter).openReqs : 5} open reqs.
`,
- footer: `
Cancel Assign `
- });
-};
-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: `
Note
`,
- footer: `
Cancel ${UI.icon('check')} Save Note `
- });
-};
-
-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._emails = [];
-Inbox._lastSync = null;
-
-Inbox._mapApiEmail = function (row) {
- const from = row.sender_name || row.fromEmail || 'Unknown';
- return {
- id: String(row.id),
- from,
- fromEmail: row.fromEmail || '',
- subject: row.subject || '',
- body: row.body || '',
- when: row.when ? new Date(row.when) : new Date(),
- unread: !!row.unread,
- attachment: row.attachment_name || 'Resume.pdf',
- attachmentSize: '—',
- atsScore: 70,
- imported: false,
- jobId: null,
- jobTitle: ''
- };
-};
-
-Inbox._syncLabel = function () {
- if (!Inbox._lastSync) return 'Not synced yet';
- const mins = Math.max(0, Math.round((Date.now() - Inbox._lastSync.getTime()) / 60000));
- if (mins < 1) return 'Just now';
- return DB.relTime(mins);
-};
-
-Inbox._loadEmails = async function () {
- const res = await Api.get('/inbox/fetch');
- const rows = Array.isArray(res.data) ? res.data : [];
- Inbox._emails = rows.map(Inbox._mapApiEmail);
- Inbox._lastSync = new Date();
- return Inbox._emails;
-};
-
-Inbox._refreshEmailCounts = function () {
- const tab = document.querySelector('#inboxTabs .tab[data-tab="Email"]');
- if (tab) {
- const countEl = tab.querySelector('.k-count');
- if (countEl) countEl.textContent = Inbox._emails.filter(e => e.unread).length;
- }
- if (window.App && App.updateBadges) App.updateBadges();
-};
-
-Inbox._emailView = function () {
- return `
-
- Outlook · Microsoft Graph API
- Loading…
- ${UI.icon('refresh')} Sync Mailbox
-
-
-
${UI.icon('mail')}
Loading… Fetching mailbox from the server.
-
-
`;
-};
-
-Inbox._bindEmail = function (state) {
- const detail = document.getElementById('emailDetail');
-
- function renderDetail() {
- const e = Inbox._emails.find(x => x.id === state.emailSelected);
- if (!e) { detail.innerHTML = `
${UI.icon('mail')}
Select an email Preview email body and resume attachments here.
`; return; }
- detail.innerHTML = `
-
-
${e.subject} ${e.imported ? UI.badge('Imported', 'b-green') : UI.badge('New', 'b-blue')}
-
- ${UI.avatar(e.from, e.initials, e.color)}
-
${e.from}
${e.fromEmail} · ${DB.fmtDate(e.when)}
-
-
${e.body}
-
-
${UI.icon('file')}
-
${e.attachment}
${e.attachmentSize} · PDF
-
${UI.scoreChip(e.atsScore)}
- ${UI.icon('eye')} Preview
-
-
- ${e.imported ? `${UI.icon('check')} Already Imported ` :
- `${UI.icon('user-plus')} Import Candidate `}
- ${UI.icon('mail')} Reply
- ${UI.icon('trash')} Archive
-
-
`;
- }
-
- function paintList() {
- const list = document.getElementById('emailList');
- const meta = document.getElementById('emailSyncMeta');
- if (!list) return;
- if (!Inbox._emails.length) {
- list.innerHTML = `
${UI.icon('mail')}
Nothing here No emails in the mailbox.
`;
- } else {
- list.innerHTML = Inbox._emails.map(e => `
-
- ${UI.avatar(e.from, e.initials, e.color)}
-
-
${e.from}
-
${e.subject}
-
-
-
${DB.fmtShort(e.when)}
-
`).join('');
- list.querySelectorAll('.inbox-item').forEach(row => row.onclick = () => {
- state.emailSelected = row.dataset.email;
- const e = Inbox._emails.find(x => x.id === state.emailSelected); if (e) e.unread = false;
- list.querySelectorAll('.inbox-item').forEach(r => r.classList.remove('active', 'unread'));
- row.classList.add('active');
- renderDetail(); Inbox._refreshEmailCounts();
- });
- }
- if (meta) meta.textContent = `Last sync: ${Inbox._syncLabel()} · ${Inbox._emails.filter(e => e.unread).length} unread`;
- renderDetail();
- Inbox._refreshEmailCounts();
- }
-
- Inbox._paintEmail = paintList;
- renderDetail();
-
- Inbox._loadEmails()
- .then(() => paintList())
- .catch(err => {
- const list = document.getElementById('emailList');
- const meta = document.getElementById('emailSyncMeta');
- if (list) list.innerHTML = `
${UI.icon('mail')}
Couldn't load mailbox ${err.message || 'Request failed'}
`;
- if (meta) meta.textContent = 'Sync failed';
- UI.toast(err.message || 'Failed to load mailbox', 'error');
- renderDetail();
- });
-};
-
-Inbox.syncMailbox = async function () {
- UI.toast('Fetching from Outlook…', 'info');
- try {
- await Inbox._loadEmails();
- if (typeof Inbox._paintEmail === 'function') Inbox._paintEmail();
- UI.toast('Mailbox synced', 'success');
- } catch (err) {
- UI.toast(err.message || 'Sync failed', 'error');
- }
-};
-
-Inbox._importEmail = function (id) {
- const e = Inbox._emails.find(x => x.id === id);
- if (!e) return;
- const job = DB.getJob(e.jobId) || DB.jobs[0];
- DB.candidates.unshift({
- id: 'CAN-' + (5001 + DB.candidates.length), name: e.from, initials: e.initials || DB.initials(e.from), color: e.color || DB.avatarColor(e.from),
- 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;
- if (typeof Inbox._paintEmail === 'function') Inbox._paintEmail();
- App.updateBadges();
- UI.toast(`${e.from} imported from Outlook → ${job.title}`, 'success');
-};
diff --git a/js/interviews.js b/js/interviews.js
deleted file mode 100644
index 54ae7b1..0000000
--- a/js/interviews.js
+++ /dev/null
@@ -1,209 +0,0 @@
-/* ============================================================
- 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 => `
${UI.avatar(iv.candidate, iv.candInitials, iv.color)}
${iv.candidate}
${iv.jobTitle}
` },
- { 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 => `
${DB.fmtShort(iv.when)}
${iv.when.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })} · ${iv.duration}m
` },
- { key: 'meeting', label: 'Type', render: iv => `
${UI.icon(iv.meeting === 'Video Call' ? 'video' : iv.meeting === 'Phone' ? 'phone' : 'map')} ${iv.meeting} ` },
- { 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) : '
— ' },
- { key: '_a', label: 'Actions', align: 'right', render: iv => `
-
- ${UI.icon('eye')}
- ${UI.icon('star')}
-
` }
- ]
- });
-
- const statusOpts = ['
All Status '].concat(['Scheduled', 'Completed', 'Cancelled', 'No Show'].map(s => `
${s} `)).join('');
- const typeOpts = ['
All Rounds '].concat(DB.interviewTypes.map(t => `
${t} `)).join('');
-
- const statCard = (label, val, icn, cls) => `
${label} ${UI.icon(icn)}
${val}
`;
-
- const upcomingHtml = upcoming.map(iv => `
-
- ${UI.avatar(iv.candidate, iv.candInitials, iv.color)}
-
${iv.candidate}
${iv.type} · ${iv.meeting}
-
${DB.fmtShort(iv.when)}
${iv.when.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}
-
`).join('');
-
- const html = `
-
-
-
Interviews Manage and track all interview activity
-
- ${UI.icon('calendar')} Calendar View
- ${UI.icon('plus')} Schedule Interview
-
-
-
- ${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')}
-
-
-
-
-
Up Next Scheduled sessions
-
-
-
-
`;
-
- 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 => `
${t.name} `).join('');
-
- const ratingRow = (crit) => `
-
-
${crit}
-
- ${[1, 2, 3, 4, 5].map(n => `${UI.icon('star')} `).join('')}
-
-
`;
-
- const body = `
-
${UI.avatar(iv.candidate, iv.candInitials, iv.color, 'avatar-lg')}
-
${iv.candidate}
${iv.type} · ${iv.jobTitle}
- ${UI.badge(iv.status)}
-
-
-
Dynamic Form
-
Upload Sheet
-
Both
-
-
-
-
-
Evaluation Template ${tmplOpts}
-
${tmpl.criteria.map(ratingRow).join('')}
-
Comments
-
-
-
-
-
-
${UI.icon('upload')}
-
Upload evaluation sheet
-
PDF, DOC, or DOCX · scanned scorecards supported
-
${['PDF', 'DOC', 'DOCX'].map(t => `${t} `).join('')}
-
-
-
-
-
Capture structured ratings and attach a signed sheet — both are stored on the scorecard.
-
${tmpl.criteria.slice(0, 3).map(ratingRow).join('')}
-
${UI.icon('file')}
-
Interviewer_Scorecard.pdf
Attached · 214 KB
${UI.badge('Uploaded', 'b-green')}
-
-
`;
-
- const footer = `
Cancel
-
${UI.icon('check')} Submit Scorecard `;
- 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 => `
${o} `).join('');
- const body = `
`;
- const footer = `
Cancel
-
${UI.icon('calendar')} Schedule `;
- UI.modal({ title: 'Schedule Interview', subtitle: 'Set up a new interview session', body, footer });
-};
diff --git a/js/jobboard.js b/js/jobboard.js
deleted file mode 100644
index efaf466..0000000
--- a/js/jobboard.js
+++ /dev/null
@@ -1,172 +0,0 @@
-/* ============================================================
- jobboard.js — Job Posting Center: publish + track performance
- ============================================================ */
-window.Views = window.Views || {};
-window.JobBoard = {};
-
-Views.jobboard = function () {
- const totals = DB.publishings.reduce((a, p) => ({ views: a.views + p.views, clicks: a.clicks + p.clicks, apps: a.apps + p.apps }), { views: 0, clicks: 0, apps: 0 });
- const conv = totals.views ? ((totals.apps / totals.views) * 100).toFixed(1) : 0;
-
- const statCard = (label, val, icn, cls, sub) => `
${label} ${UI.icon(icn)}
${val}
`;
-
- // per-platform aggregate
- const platAgg = {};
- DB.publishings.forEach(p => {
- if (!platAgg[p.platform]) platAgg[p.platform] = { views: 0, clicks: 0, apps: 0, jobs: 0 };
- platAgg[p.platform].views += p.views; platAgg[p.platform].clicks += p.clicks; platAgg[p.platform].apps += p.apps; platAgg[p.platform].jobs++;
- });
- const platRows = Object.entries(platAgg).sort((a, b) => b[1].apps - a[1].apps);
-
- // publishing table
- const table = UI.dataTable({
- pageSize: 8,
- rows: DB.publishings,
- columns: [
- { key: 'jobTitle', label: 'Job', sortable: true, render: p => `
${p.jobTitle}
${p.jobId}
` },
- { key: 'platform', label: 'Platform', sortable: true, render: p => { const pl = DB.publishPlatforms.find(x => x.name === p.platform) || {}; return `
${UI.icon(pl.icon || 'briefcase')} ${p.platform}`; } },
- { key: 'status', label: 'Status', sortable: true, render: p => UI.badge(p.status, p.status === 'Live' ? 'b-green' : p.status === 'Paused' ? 'b-amber' : 'b-blue') },
- { key: 'views', label: 'Views', sortable: true, align: 'right', render: p => p.views.toLocaleString() },
- { key: 'clicks', label: 'Clicks', sortable: true, align: 'right', render: p => p.clicks.toLocaleString() },
- { key: 'apps', label: 'Applications', sortable: true, align: 'right', render: p => `
${p.apps} ` },
- { key: '_conv', label: 'Conversion', sortable: true, sortValue: p => p.apps / p.views, render: p => `
${((p.apps / p.views) * 100).toFixed(1)}% ` },
- { key: '_a', label: '', align: 'right', render: p => `
${UI.icon('external')} ` }
- ]
- });
-
- const html = `
-
-
-
Job Board Publish requisitions across channels and track performance
-
- ${UI.icon('trending-up')} Analytics
- ${UI.icon('send')} Publish a Job
-
-
-
-
- ${statCard('Total Views', totals.views.toLocaleString(), 'eye', 'i-blue', 'across all platforms')}
- ${statCard('Total Clicks', totals.clicks.toLocaleString(), 'target', 'i-purple', ((totals.clicks / totals.views) * 100).toFixed(1) + '% CTR')}
- ${statCard('Applications', totals.apps.toLocaleString(), 'users', 'i-green', 'from job boards')}
- ${statCard('Conversion Rate', conv + '%', 'trending-up', 'i-teal', 'view → application')}
-
-
-
-
-
Platform Performance Applications by channel
-
-
-
-
-
- ${DB.publishPlatforms.map(p => `
-
${UI.icon(p.icon)}
-
${p.name}
${p.cost === 'Free' ? 'Free posting' : 'Paid · ' + p.cost}
- ${p.connected ? UI.badge('Connected', 'b-green') : `
Connect `}
-
`).join('')}
-
-
-
-
-
-
Active Postings ${DB.publishings.length} live postings across ${platRows.length} platforms
-
${UI.icon('download')} Export
- ${table.html}
-
-
`;
-
- return {
- html,
- onMount() {
- table.mount();
- Charts.horizontalBar(document.getElementById('jbChart'), {
- labels: platRows.map(p => p[0]), data: platRows.map(p => p[1].apps)
- });
- }
- };
-};
-
-// ---------------- Publish flow (stepper modal) ----------------
-JobBoard.publishFlow = function (jobId) {
- const state = { step: 1, jobId: jobId || DB.jobs.filter(j => j.status === 'Open')[0].id, platforms: ['Career Portal'] };
- JobBoard._state = state;
- JobBoard._renderFlow();
-};
-
-JobBoard._renderFlow = function () {
- const state = JobBoard._state;
- const steps = ['Select Job', 'Approval', 'Platforms', 'Publish'];
- const stepper = `
${steps.map((s, i) => {
- const n = i + 1;
- const cls = n < state.step ? 'done' : n === state.step ? 'active' : '';
- return `
${n < state.step ? '✓' : n}
${s}
${i < steps.length - 1 ? `
` : ''}`;
- }).join('')}
`;
-
- let body = stepper;
- if (state.step === 1) {
- const opts = DB.jobs.filter(j => j.status !== 'Draft').map(j => `
${j.title} · ${j.id} `).join('');
- const job = DB.getJob(state.jobId);
- body += `
Select requisition to publish ${opts}
-
-
${UI.icon('briefcase')}
-
${job.title}
${job.department} · ${job.location} · ${job.type}
-
`;
- } else if (state.step === 2) {
- body += `
-
${UI.icon('check-circle')}
-
Approval granted
Approved by Department Head · Budget confirmed
-
Hiring Manager sign-off ${UI.badge('Approved', 'b-green')}
-
Finance budget approval ${UI.badge('Approved', 'b-green')}
-
Compliance review ${UI.badge('Approved', 'b-green')}
-
`;
- } else if (state.step === 3) {
- body += `
Select the platforms to publish this role to
-
- ${DB.publishPlatforms.map(p => `
`).join('')}
-
`;
- } else if (state.step === 4) {
- body += `
-
${UI.icon('check-circle')}
-
Published Successfully
-
${DB.getJob(state.jobId).title} is now live on ${state.platforms.length} platform${state.platforms.length > 1 ? 's' : ''}
-
- ${state.platforms.map(p => { const pl = DB.publishPlatforms.find(x => x.name === p); return `${pl.name} `; }).join('')}
-
-
`;
- }
-
- let footer;
- if (state.step === 4) footer = `
${UI.icon('check')} Done `;
- else footer = `
${state.step === 1 ? 'Cancel' : 'Back'}
-
${state.step === 3 ? UI.icon('send') + ' Publish' : 'Continue'} `;
-
- UI.modal({ title: 'Publish Job', subtitle: 'Distribute this requisition to job boards', body, footer, size: 'modal-lg' });
-
- if (state.step === 3) {
- document.querySelectorAll('#platGrid .platform-card').forEach(card => card.onclick = () => {
- const name = card.dataset.plat;
- const idx = state.platforms.indexOf(name);
- if (idx > -1) state.platforms.splice(idx, 1); else state.platforms.push(name);
- card.classList.toggle('selected');
- });
- }
-};
-JobBoard._next = function () {
- const state = JobBoard._state;
- if (state.step === 1) { const sel = document.getElementById('pubJob'); if (sel) state.jobId = sel.value; }
- if (state.step === 3 && !state.platforms.length) { UI.toast('Select at least one platform', 'warning'); return; }
- state.step++;
- if (state.step === 4) {
- // create publishing records
- const job = DB.getJob(state.jobId);
- state.platforms.forEach(p => {
- DB.publishings.unshift({ jobId: job.id, jobTitle: job.title, platform: p, status: 'Live', views: DB.int(0, 30), clicks: 0, apps: 0, published: new Date('2026-07-09') });
- });
- }
- JobBoard._renderFlow();
-};
-JobBoard._back = function () { JobBoard._state.step--; JobBoard._renderFlow(); };
diff --git a/js/jobs.js b/js/jobs.js
deleted file mode 100644
index ff58987..0000000
--- a/js/jobs.js
+++ /dev/null
@@ -1,252 +0,0 @@
-/* ============================================================
- jobs.js — Jobs listing, filters, create/edit/view/delete
- ============================================================ */
-window.Views = window.Views || {};
-window.Jobs = {};
-
-Views.jobs = function () {
- const filters = { q: '', dept: '', status: '', type: '' };
- let table;
-
- function apply() {
- let rows = DB.jobs.filter(j => {
- if (filters.dept && j.department !== filters.dept) return false;
- if (filters.status && j.status !== filters.status) return false;
- if (filters.type && j.type !== filters.type) return false;
- if (filters.q) {
- const q = filters.q.toLowerCase();
- if (!(j.title + j.id + j.department + j.manager + j.recruiter + j.location).toLowerCase().includes(q)) return false;
- }
- return true;
- });
- table.update(rows);
- }
-
- table = UI.dataTable({
- pageSize: 8,
- rows: DB.jobs,
- columns: [
- { key: 'id', label: 'Job ID', sortable: true, render: j => `
${j.id} ` },
- { key: 'title', label: 'Job Title', sortable: true, render: j => `
${j.title}
${j.businessUnit} · ${j.grade}
` },
- { key: 'department', label: 'Department', sortable: true },
- { key: 'manager', label: 'Hiring Manager', sortable: true, render: j => `
${UI.avatar(j.manager)}${j.manager}
` },
- { key: 'location', label: 'Location', sortable: true, render: j => `
${j.location} ` },
- { key: 'type', label: 'Type', render: j => UI.badge(j.type, 'b-gray') },
- { key: 'applications', label: 'Apps', sortable: true, align: 'center', render: j => `
${j.applications} ` },
- { key: 'status', label: 'Status', sortable: true, render: j => UI.badge(j.status) },
- { key: 'created', label: 'Created', sortable: true, sortValue: j => j.created.getTime(), render: j => `
${DB.fmtShort(j.created)} ` },
- { key: '_a', label: 'Actions', align: 'right', render: j => `
-
- ${UI.icon('eye')}
- ${UI.icon('send')}
- ${UI.icon('edit')}
- ${UI.icon('trash')}
-
` }
- ]
- });
-
- const deptOpts = ['
All Departments '].concat(DB.departments.map(d => `
${d} `)).join('');
- const statusOpts = ['
All Status '].concat(DB.jobStatuses.map(s => `
${s} `)).join('');
- const typeOpts = ['
All Types '].concat(DB.empTypes.map(t => `
${t} `)).join('');
-
- const html = `
-
-
-
Jobs ${DB.jobs.length} requisitions · ${DB.kpis.openJobs} currently open
-
- ${UI.icon('download')} Export
- ${UI.icon('plus')} Create Job
-
-
-
-
`;
-
- return {
- html,
- onMount() {
- table.mount();
- const s = document.getElementById('jobSearch');
- s.oninput = () => { filters.q = s.value; apply(); };
- document.getElementById('jobDept').onchange = e => { filters.dept = e.target.value; apply(); };
- document.getElementById('jobStatus').onchange = e => { filters.status = e.target.value; apply(); };
- document.getElementById('jobType').onchange = e => { filters.type = e.target.value; apply(); };
- }
- };
-};
-
-// ---------------- View job ----------------
-Jobs.view = function (id) {
- const j = DB.getJob(id);
- const body = `
-
-
${UI.icon('briefcase')}
-
-
${j.title}
-
${j.id} · ${j.department} · ${j.businessUnit}
-
-
${UI.badge(j.status)}
-
-
-
Hiring Manager
${j.manager}
-
Assigned Recruiter
${j.recruiter} ${(() => { const r = DB.getRecruiterByName(j.recruiter); return r ? `${r.workload}% load ` : ''; })()} Reassign
-
-
-
-
-
Salary Range
${DB.moneyK(j.salaryMin)} – ${DB.moneyK(j.salaryMax)}
-
Experience
${j.experience}
-
-
Deadline
${DB.fmtDate(j.deadline)}
-
-
-
Description
${j.description}
-
Key Responsibilities
-
${j.responsibilities.map(r => `${r} `).join('')}
-
Required Skills
-
${j.skills.map(s => `${s} `).join('')}
-
Benefits
-
${j.benefits.map(s => `${s} `).join('')}
-
-
Hiring progress ${UI.pbar(j.progress)}
${j.progress}% `;
- const footer = `
Close
-
${UI.icon('send')} Publish
-
${UI.icon('edit')} Edit Job `;
- UI.modal({ title: 'Job Details', subtitle: j.id, body, footer, size: 'modal-lg' });
-};
-
-Jobs.reassign = function (id) {
- const j = DB.getJob(id);
- const opts = DB.recruiters.map(r => `
${r.name} — ${r.workload}% load · ${r.openReqs} reqs `).join('');
- UI.modal({
- title: 'Reassign Recruiter', subtitle: j.title,
- body: `
Assigned Recruiter ${DB.recruiters.map(r => `${r.name} — ${r.workload}% load `).join('')}
-
Workload is recalculated automatically across the recruiter's assigned requisitions.
`,
- footer: `
Cancel Reassign `
- });
-};
-Jobs._doReassign = function (id) {
- const j = DB.getJob(id);
- j.recruiter = document.getElementById('reassignRec').value;
- UI.closeModal(); UI.toast('Recruiter reassigned', 'success');
- if (typeof Router !== 'undefined') Router.reload();
-};
-
-// ---------------- Create / Edit form ----------------
-Jobs.openCreate = function () { Jobs._form(null); };
-Jobs.openEdit = function (id) { Jobs._form(DB.getJob(id)); };
-
-Jobs._form = function (job) {
- const isEdit = !!job;
- const opt = (arr, sel) => arr.map(o => `
${o} `).join('');
- const body = `
-
`;
- const footer = `
Cancel
-
${UI.icon('check')} ${isEdit ? 'Save Changes' : 'Create Job'} `;
- UI.modal({ title: isEdit ? 'Edit Job' : 'Create New Job', subtitle: isEdit ? job.id : 'Fill in the details to post a requisition', body, footer, size: 'modal-lg' });
-};
-
-Jobs._save = function (id) {
- const form = document.getElementById('jobForm');
- UI.clearErrors(form);
- const f = Object.fromEntries(new FormData(form));
- let ok = true;
- const req = (name, cond) => { if (!cond) { UI.fieldError(form.querySelector(`[name="${name}"]`), 'Required'); ok = false; } };
- req('title', f.title.trim());
- req('description', f.description.trim());
- req('salaryMin', f.salaryMin && +f.salaryMin > 0);
- if (!ok) { UI.toast('Please fix the highlighted fields', 'error'); return; }
-
- const skills = f.skills.split(',').map(s => s.trim()).filter(Boolean);
- const benefits = f.benefits.split(',').map(s => s.trim()).filter(Boolean);
- const responsibilities = f.responsibilities.split('\n').map(s => s.trim()).filter(Boolean);
-
- if (id) {
- const job = DB.getJob(id);
- Object.assign(job, {
- title: f.title, department: f.department, businessUnit: f.businessUnit, grade: f.grade, type: f.type,
- manager: f.manager, recruiter: f.recruiter, salaryMin: +f.salaryMin, salaryMax: +f.salaryMax || +f.salaryMin + 20000,
- experience: f.experience, education: f.education, location: f.location, vacancies: +f.vacancies || 1,
- description: f.description, responsibilities, skills: skills.length ? skills : job.skills, benefits: benefits.length ? benefits : job.benefits, status: f.status
- });
- UI.toast('Job updated successfully', 'success');
- } else {
- const newJob = {
- id: 'JOB-' + (1001 + DB.jobs.length), title: f.title, department: f.department, businessUnit: f.businessUnit,
- grade: f.grade, manager: f.manager, managerId: '', recruiter: f.recruiter, recruiterId: '', location: f.location,
- type: f.type, vacancies: +f.vacancies || 1, applications: 0, status: f.status, created: new Date('2026-07-09'),
- deadline: f.deadline ? new Date(f.deadline) : new Date('2026-08-09'), salaryMin: +f.salaryMin, salaryMax: +f.salaryMax || +f.salaryMin + 20000,
- experience: f.experience || '3+ years', education: f.education, skills, benefits, description: f.description,
- responsibilities: responsibilities.length ? responsibilities : ['Own key projects'], progress: 0
- };
- DB.jobs.unshift(newJob);
- UI.toast('Job created successfully', 'success');
- App.updateBadges();
- }
- UI.closeModal();
- Router.reload();
-};
-
-Jobs.confirmDelete = function (id) {
- const j = DB.getJob(id);
- const body = `
-
${UI.icon('trash')}
-
Delete "${j.title}"?
-
This will permanently remove requisition ${j.id} and its ${j.applications} applications. This action cannot be undone.
`;
- const footer = `
Cancel
-
${UI.icon('trash')} Delete Job `;
- UI.modal({ title: 'Confirm Deletion', body, footer });
-};
-Jobs._delete = function (id) {
- const i = DB.jobs.findIndex(j => j.id === id);
- if (i > -1) DB.jobs.splice(i, 1);
- UI.closeModal();
- UI.toast('Job deleted', 'success');
- App.updateBadges();
- Router.reload();
-};
diff --git a/js/misc.js b/js/misc.js
deleted file mode 100644
index 0edc541..0000000
--- a/js/misc.js
+++ /dev/null
@@ -1,207 +0,0 @@
-/* ============================================================
- misc.js — Hiring Managers, Calendar, Notifications, Help
- ============================================================ */
-window.Views = window.Views || {};
-
-// ---------------- Hiring Managers ----------------
-Views.managers = function () {
- function card(m) {
- const jobs = DB.jobs.filter(j => j.manager === m.name && j.status === 'Open');
- return `
-
-
- ${UI.avatar(m.name, m.initials, m.color, 'avatar-lg')}
-
-
-
-
${m.openReqs} Open Reqs
-
${m.teamSize} Team Size
-
-
-
- ${UI.icon('mail')} ${m.email.split('@')[0]}
- View
-
-
-
`;
- }
- const html = `
-
-
-
Hiring Managers ${DB.managers.length} managers · ${DB.managers.reduce((s, m) => s + m.openReqs, 0)} active requisitions
-
${UI.icon('plus')} Add Manager
-
-
${DB.managers.map(card).join('')}
-
`;
- return { html };
-};
-Views._mgrDetail = function (id) {
- const m = DB.getManager(id);
- const jobs = DB.jobs.filter(j => j.manager === m.name);
- const body = `
-
${UI.avatar(m.name, m.initials, m.color, 'avatar-lg')}
-
${m.name}
${m.title}
-
${UI.badge(m.department, 'b-indigo')}${m.teamSize} reports
-
-
${m.openReqs} Open Reqs
-
${jobs.length} Total Jobs
-
${jobs.reduce((s, j) => s + j.applications, 0)} Applications
-
-
Hiring Manager Portal
-
- ${UI.icon('plus')} Raise Requisition
- ${UI.icon('users')} Review Candidates
- ${UI.icon('calendar')} Schedule Interview
- ${UI.icon('check-circle')} Approve Offers
-
-
Requisitions
-
${jobs.length ? jobs.map(j => `
-
${UI.icon('briefcase')}
-
${j.title}
${j.applications} applications
${UI.badge(j.status)}
`).join('') : '
No requisitions
'}
`;
- UI.modal({ title: 'Hiring Manager', subtitle: m.id, body, size: 'modal-lg', footer: `
Close ${UI.icon('mail')} Message ` });
-};
-
-// ---------------- Calendar ----------------
-Views.calendar = function () {
- const state = { month: 6, year: 2026 }; // July 2026 (0-indexed)
- const evColors = { 'Phone Screen': 'b-blue', 'Technical': 'b-indigo', 'System Design': 'b-purple', 'Onsite Loop': 'b-teal', 'Hiring Manager': 'b-amber', 'Culture Fit': 'b-green', 'Final Round': 'b-red' };
-
- function build() {
- const first = new Date(state.year, state.month, 1);
- const startDow = first.getDay();
- const daysInMonth = new Date(state.year, state.month + 1, 0).getDate();
- const prevDays = new Date(state.year, state.month, 0).getDate();
- const cells = [];
- for (let i = startDow - 1; i >= 0; i--) cells.push({ day: prevDays - i, other: true });
- for (let d = 1; d <= daysInMonth; d++) cells.push({ day: d, other: false, date: new Date(state.year, state.month, d) });
- while (cells.length % 7 !== 0 || cells.length < 42) cells.push({ day: cells.length - daysInMonth - startDow + 1, other: true });
-
- const today = new Date('2026-07-09');
- const dow = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
- let html = dow.map(d => `
${d}
`).join('');
- cells.slice(0, 42).forEach(c => {
- let evs = '';
- if (!c.other && c.date) {
- const dayEvents = DB.interviews.filter(iv => iv.when.toDateString() === c.date.toDateString());
- evs = dayEvents.slice(0, 3).map(iv => `
${iv.when.toLocaleTimeString('en-US', { hour: 'numeric' })} ${iv.candidate.split(' ')[0]}
`).join('');
- if (dayEvents.length > 3) evs += `
+${dayEvents.length - 3} more
`;
- }
- const isToday = !c.other && c.date && c.date.toDateString() === today.toDateString();
- html += `
`;
- });
- return html;
- }
-
- const monthName = new Date(state.year, state.month).toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
- const todayIvs = DB.interviews.filter(iv => iv.when.toDateString() === new Date('2026-07-09').toDateString());
-
- const html = `
-
-
-
Calendar Interview schedule at a glance
-
-
- ${UI.icon('chevron-left')}
- ${monthName}
- ${UI.icon('chevron-right')}
-
-
${UI.icon('plus')} Schedule
-
-
-
-
-
-
${todayIvs.length ? todayIvs.map(iv => `
-
${UI.avatar(iv.candidate, iv.candInitials, iv.color)}
-
${iv.candidate}
${iv.type}
-
${iv.when.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}
`).join('') : '
No interviews today
'}
-
-
-
`;
-
- return {
- html,
- onMount() {
- const upd = () => {
- document.getElementById('calGrid').innerHTML = build();
- document.getElementById('calMonth').textContent = new Date(state.year, state.month).toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
- };
- document.getElementById('calPrev').onclick = () => { state.month--; if (state.month < 0) { state.month = 11; state.year--; } upd(); };
- document.getElementById('calNext').onclick = () => { state.month++; if (state.month > 11) { state.month = 0; state.year++; } upd(); };
- }
- };
-};
-
-// ---------------- Notifications ----------------
-Views.notifications = function () {
- const rows = DB.notifications.map((n, i) => `
-
-
${UI.icon(n.icon)}
-
${n.title}
${n.text}
${n.time}
- ${n.unread ? '
' : ''}
-
`).join('');
- const html = `
-
-
-
Notifications Stay on top of hiring activity
-
- ${UI.icon('check')} Mark all read
- ${UI.icon('more')}
-
-
-
-
`;
- return { html };
-};
-
-// ---------------- Help ----------------
-Views.help = function () {
- const faqs = [
- { q: 'How do I create a new job requisition?', a: 'Navigate to Jobs and click "Create Job". Fill in the required fields marked with an asterisk and click Save. The job will immediately appear in your listings.' },
- { q: 'How does the AI candidate score work?', a: 'The AI score (0–100) evaluates how well a candidate matches the job requirements based on skills, experience, and education. Higher scores indicate stronger matches.' },
- { q: 'Can I move candidates between pipeline stages?', a: 'Yes. Open the Pipeline view and simply drag any candidate card between stage columns. The candidate\'s status updates automatically.' },
- { q: 'How do I schedule an interview?', a: 'Go to Interviews or Calendar and click "Schedule Interview". Select the candidate, round, date, time, and interviewers.' },
- { q: 'How do I export reports?', a: 'On the Reports page, use the "Export Report" button for a full PDF, or the CSV buttons on individual tables.' }
- ];
- const resources = [
- { icn: 'file', t: 'Documentation', d: 'Complete product guides', cls: 'i-indigo' },
- { icn: 'video', t: 'Video Tutorials', d: 'Watch step-by-step walkthroughs', cls: 'i-red' },
- { icn: 'message', t: 'Live Chat', d: 'Chat with our support team', cls: 'i-green' },
- { icn: 'users', t: 'Community', d: 'Connect with other recruiters', cls: 'i-purple' }
- ];
- const html = `
-
-
Help Center Find answers and get support
-
-
-
How can we help you?
-
Search our knowledge base or browse the topics below
-
-
-
-
-
-
-
- ${resources.map(r => `
-
${UI.icon(r.icn)}
-
${r.t}
${r.d}
`).join('')}
-
-
-
Frequently Asked Questions
-
- ${faqs.map((f, i) => `
-
${f.q} ${UI.icon('chevron-right')}
-
${f.a}
`).join('')}
-
-
-
`;
- return { html };
-};
-Views._toggleFaq = function (i) {
- const a = document.getElementById('faqA' + i);
- const chev = document.getElementById('faqChev' + i);
- const open = a.style.display === 'block';
- a.style.display = open ? 'none' : 'block';
- chev.style.transform = open ? 'rotate(0deg)' : 'rotate(90deg)';
-};
diff --git a/js/offers.js b/js/offers.js
deleted file mode 100644
index ddb61a8..0000000
--- a/js/offers.js
+++ /dev/null
@@ -1,139 +0,0 @@
-/* ============================================================
- offers.js — Offer management
- ============================================================ */
-window.Views = window.Views || {};
-window.Offers = {};
-
-Views.offers = function () {
- const filters = { q: '', status: '' };
- let table;
-
- const stats = {
- sent: DB.offers.filter(o => o.status !== 'Draft').length,
- accepted: DB.offers.filter(o => o.status === 'Accepted').length,
- pending: DB.offers.filter(o => ['Sent', 'Negotiating'].includes(o.status)).length,
- rate: Math.round(DB.offers.filter(o => o.status === 'Accepted').length / (DB.offers.filter(o => ['Accepted', 'Declined'].includes(o.status)).length || 1) * 100)
- };
-
- function apply() {
- const rows = DB.offers.filter(o => {
- if (filters.status && o.status !== filters.status) return false;
- if (filters.q && !(o.candidate + o.jobTitle + o.recruiter).toLowerCase().includes(filters.q.toLowerCase())) return false;
- return true;
- });
- table.update(rows);
- }
-
- table = UI.dataTable({
- pageSize: 8,
- rows: DB.offers,
- columns: [
- { key: 'candidate', label: 'Candidate', sortable: true, render: o => `
${UI.avatar(o.candidate, o.initials, o.color)}
${o.candidate}
${o.jobTitle}
` },
- { key: 'department', label: 'Department', sortable: true },
- { key: 'base', label: 'Base Salary', sortable: true, align: 'right', render: o => `
${DB.money(o.base)} ` },
- { key: 'equity', label: 'Equity', render: o => `
${o.equity} ` },
- { key: 'bonus', label: 'Bonus', align: 'center', render: o => `
${o.bonus} ` },
- { key: 'sent', label: 'Sent', sortable: true, sortValue: o => o.sent.getTime(), render: o => `
${DB.fmtShort(o.sent)} ` },
- { key: 'status', label: 'Status', sortable: true, render: o => UI.badge(o.status) },
- { key: '_a', label: 'Actions', align: 'right', render: o => `
-
- ${UI.icon('eye')}
- ${UI.icon('send')}
-
` }
- ]
- });
-
- const statusOpts = ['
All Status '].concat(['Sent', 'Accepted', 'Negotiating', 'Declined', 'Draft', 'Expired'].map(s => `
${s} `)).join('');
- const statCard = (label, val, icn, cls) => `
${label} ${UI.icon(icn)}
${val}
`;
-
- const html = `
-
-
-
Offers Track offer letters and acceptance
-
${UI.icon('plus')} Create Offer
-
-
- ${statCard('Offers Sent', stats.sent, 'send', 'i-indigo')}
- ${statCard('Accepted', stats.accepted, 'check-circle', 'i-green')}
- ${statCard('Awaiting Response', stats.pending, 'clock', 'i-amber')}
- ${statCard('Acceptance Rate', stats.rate + '%', 'trending-up', 'i-teal')}
-
-
-
`;
-
- return {
- html,
- onMount() {
- table.mount();
- const s = document.getElementById('ofSearch');
- s.oninput = () => { filters.q = s.value; apply(); };
- document.getElementById('ofStatus').onchange = e => { filters.status = e.target.value; apply(); };
- }
- };
-};
-
-Offers.view = function (id) {
- const o = DB.offers.find(x => x.id === id);
- const total = o.base + Math.round(o.base * parseInt(o.bonus) / 100);
- const body = `
-
${UI.avatar(o.candidate, o.initials, o.color, 'avatar-lg')}
-
${o.candidate}
${o.jobTitle} · ${o.department}
-
${UI.badge(o.status)}
-
-
Compensation Package
-
-
Base Salary
${DB.money(o.base)}
-
-
-
Est. Total Cash
${DB.money(total)}
-
-
-
-
Sent On
${DB.fmtDate(o.sent)}
-
Expires
${DB.fmtDate(o.expires)}
-
-
-
`;
- const footer = `
Close
-
${UI.icon('download')} Download
-
${UI.icon('send')} Resend Offer `;
- UI.modal({ title: 'Offer Details', subtitle: o.id, body, footer, size: 'modal-lg' });
-};
-
-Offers.create = function () {
- const opt = arr => arr.map(o => `
${o} `).join('');
- const body = `
`;
- const footer = `
Cancel
-
${UI.icon('send')} Send Offer `;
- UI.modal({ title: 'Create Offer', subtitle: 'Generate and send an offer letter', body, footer });
-};
-Offers._save = function () {
- const form = document.getElementById('offerForm');
- UI.clearErrors(form);
- const f = Object.fromEntries(new FormData(form));
- if (!f.base || +f.base <= 0) { UI.fieldError(form.querySelector('[name=base]'), 'Required'); UI.toast('Enter a base salary', 'error'); return; }
- const cand = DB.candidates.find(c => c.name === f.candidate) || DB.candidates[0];
- DB.offers.unshift({
- id: 'OFR-' + (9001 + DB.offers.length), candidate: cand.name, candidateId: cand.id, initials: cand.initials, color: cand.color,
- jobTitle: cand.jobTitle, department: cand.department, status: 'Sent', base: +f.base, equity: f.equity || '10k RSU',
- bonus: (f.bonus || 10) + '%', sent: new Date('2026-07-09'), expires: f.expires ? new Date(f.expires) : new Date('2026-07-23'), recruiter: cand.recruiter
- });
- UI.closeModal();
- UI.toast('Offer sent successfully', 'success');
- Router.reload();
-};
diff --git a/js/pipeline.js b/js/pipeline.js
deleted file mode 100644
index 2f54be5..0000000
--- a/js/pipeline.js
+++ /dev/null
@@ -1,166 +0,0 @@
-/* ============================================================
- 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 `
-
${st.name} ${cards.length}
-
- ${cards.map(c => Pipeline._card(c)).join('')}
-
`;
- }).join('');
- }
-
- const jobOpts = ['
All Jobs '].concat(DB.jobs.filter(j => j.status === 'Open').map(j => `
${j.title} `)).join('');
-
- const html = `
-
-
-
Pipeline Drag candidates between stages to update their status
-
- ${jobOpts}
- ${UI.icon('plus')} Add Candidate
-
-
-
${columns()}
-
`;
-
- 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 `
-
${UI.avatar(c.name, c.initials, c.color)}
-
${c.name}
${c.currentTitle}
-
${c.jobTitle}
-
${c.skills.slice(0, 3).map(s => `${s} `).join('')}
-
${c.currentCompany} ${UI.scoreChip(c.aiScore)}
-
`;
-};
-
-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 = `
${UI.icon('search')}
No talent found `; return; }
- grid.innerHTML = list.map(c => `
-
-
-
- ${UI.avatar(c.name, c.initials, c.color, 'avatar-lg')}
-
${c.name}
${c.currentTitle}
- ${UI.scoreChip(c.aiScore)}
-
-
${c.skills.slice(0, 4).map(s => `${s} `).join('')}
-
-
- ${UI.icon('briefcase')} ${c.experience} yrs
- ${c.currentCompany}
- ${UI.badge(c.source, 'b-gray')}
-
-
-
`).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 = ['
All Departments '].concat(DB.departments.map(d => `
${d} `)).join('');
-
- const html = `
-
-
-
Talent Pool ${pool.length} silver-medalists & passive candidates to re-engage
-
${UI.icon('send')} Start Campaign
-
-
-
-
`;
-
- 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(); };
- }
- };
-};
diff --git a/js/rbac.js b/js/rbac.js
deleted file mode 100644
index 31c9981..0000000
--- a/js/rbac.js
+++ /dev/null
@@ -1,117 +0,0 @@
-/* ============================================================
- rbac.js — Enterprise Role Based Access Control
- Microsoft Admin Center-style permission matrix
- ============================================================ */
-window.Views = window.Views || {};
-window.RBAC = {};
-
-Views.rbac = function () {
- const state = { roleIdx: 0 };
- RBAC._state = state;
-
- const html = `
-
-
-
Access Control Enterprise RBAC — configure permissions for every role and module
-
- ${UI.icon('plus')} New Role
- ${UI.icon('check')} Save Changes
-
-
-
-
`;
-
- return {
- html,
- onMount() { RBAC._renderRoles(); RBAC._renderDetail(); }
- };
-};
-
-RBAC._renderRoles = function () {
- const el = document.getElementById('roleList');
- el.innerHTML = DB.rbacRoles.map((r, i) => `
-
-
${UI.icon('shield')}
-
${r.name}
${r.users} user${r.users === 1 ? '' : 's'}
-
`).join('');
- el.querySelectorAll('.role-item').forEach(item => item.onclick = () => {
- RBAC._state.roleIdx = +item.dataset.idx;
- RBAC._renderRoles(); RBAC._renderDetail();
- });
-};
-
-RBAC._renderDetail = function () {
- const r = DB.rbacRoles[RBAC._state.roleIdx];
- const el = document.getElementById('rbacDetail');
-
- const matrixRows = DB.rbacModules.map(mod => `
-
- ${mod}
- ${DB.permTypes.map((pt, pi) => `${UI.icon('check')} `).join('')}
- `).join('');
-
- el.innerHTML = `
-
-
${UI.icon('shield')}
-
${r.name} ${r.desc}
-
- ${r.users} users
- Grant all
- Revoke all
-
-
-
-
- Module ${DB.permTypes.map(p => `${p} `).join('')}
- ${matrixRows}
-
-
`;
-
- el.querySelectorAll('.perm-check').forEach(chk => chk.onclick = () => {
- const mod = chk.dataset.mod, pi = +chk.dataset.perm;
- r.matrix[mod][pi] = !r.matrix[mod][pi];
- chk.classList.toggle('on');
- });
-};
-
-RBAC.toggleAll = function (on) {
- const r = DB.rbacRoles[RBAC._state.roleIdx];
- DB.rbacModules.forEach(mod => r.matrix[mod] = r.matrix[mod].map(() => on));
- RBAC._renderDetail();
- UI.toast(on ? 'All permissions granted for ' + r.name : 'All permissions revoked for ' + r.name, on ? 'success' : 'warning');
-};
-
-RBAC.addRole = function () {
- UI.modal({
- title: 'Create Role', subtitle: 'Define a new access role',
- body: `
`,
- footer: `
Cancel ${UI.icon('check')} Create Role `
- });
-};
-RBAC._saveRole = function () {
- const form = document.getElementById('roleForm');
- UI.clearErrors(form);
- const f = Object.fromEntries(new FormData(form));
- if (!f.name.trim()) { UI.fieldError(form.querySelector('[name=name]'), 'Required'); return; }
- const levelMap = { 'View only': 'View', 'Editor': 'Edit', 'Approver': 'Approve', 'Manager': 'Manage', 'Administrator': 'Administrator' };
- const level = levelMap[f.template] || 'View';
- const idxMap = { 'View': 1, 'Edit': 3, 'Approve': 5, 'Manage': 7, 'Administrator': 8 };
- const cutoff = idxMap[level];
- const matrix = {};
- DB.rbacModules.forEach(mod => matrix[mod] = DB.permTypes.map((p, i) => i < cutoff));
- DB.rbacRoles.push({ name: f.name, users: 0, color: f.color, desc: f.desc || 'Custom role', level, matrix });
- RBAC._state.roleIdx = DB.rbacRoles.length - 1;
- UI.closeModal(); RBAC._renderRoles(); RBAC._renderDetail();
- UI.toast('Role "' + f.name + '" created', 'success');
-};
diff --git a/js/recruiterhub.js b/js/recruiterhub.js
deleted file mode 100644
index b9622e7..0000000
--- a/js/recruiterhub.js
+++ /dev/null
@@ -1,121 +0,0 @@
-/* ============================================================
- recruiterhub.js — Personalized recruiter dashboard + leaderboard
- ============================================================ */
-window.Views = window.Views || {};
-window.RecruiterHub = {};
-
-Views.recruiterhub = function () {
- const state = { recId: DB.recruiters[0].id };
- RecruiterHub._state = state;
-
- const recOpts = DB.recruiters.map(r => `
${r.name} `).join('');
-
- const html = `
-
-
-
Recruiter Hub Personalized performance dashboard & workload
-
- ${recOpts}
- ${UI.icon('download')} Export
-
-
-
-
`;
-
- return {
- html,
- onMount() {
- RecruiterHub._render();
- document.getElementById('recSelect').onchange = e => { state.recId = e.target.value; RecruiterHub._render(); };
- }
- };
-};
-
-RecruiterHub._render = function () {
- const r = DB.getRecruiter(RecruiterHub._state.recId);
- const el = document.getElementById('recHubBody');
- const slaCls = r.sla === 'On Track' ? 'b-green' : r.sla === 'At Risk' ? 'b-amber' : 'b-red';
-
- const kpi = (label, val, icn, cls, sub) => `
${label} ${UI.icon(icn)}
${val}
${sub ? `` : ''}
`;
-
- // leaderboard
- const board = [...DB.recruiters].sort((a, b) => b.hires - a.hires).slice(0, 8);
- const leaderHtml = board.map((rec, i) => {
- const rankCls = i === 0 ? 'gold' : i === 1 ? 'silver' : i === 2 ? 'bronze' : '';
- return `
-
${i + 1}
- ${UI.avatar(rec.name, rec.initials, rec.color)}
-
${rec.name}
${rec.efficiency}% efficiency · ${rec.avgTimeToHire}d avg
-
-
`;
- }).join('');
-
- // heatmap
- const maxHeat = 5;
- const heatColor = v => { const t = v / maxHeat; return t === 0 ? 'var(--bg-sunken)' : `rgba(79,70,229,${0.2 + t * 0.8})`; };
- const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
- const heatHtml = `
-
${['W1', 'W2', 'W3', 'W4', 'W5'].map(w => `
${w}
`).join('')}
- ${days.map((d, di) => `
${d}
${r.heatmap[di].map(v => `
`).join('')}`).join('')}
-
-
Less ${[0, 1, 2, 3, 5].map(v => ` `).join('')} More
`;
-
- el.innerHTML = `
-
-
- ${UI.avatar(r.name, r.initials, 'rgba(255,255,255,.18)', 'avatar-lg')}
-
${r.name}
${r.department} Recruiter · ⭐ ${r.rating} rating
-
-
${r.efficiency}%
Efficiency
-
${UI.badge(r.sla, slaCls)}
-
-
-
-
- ${kpi('Open Positions', r.openPositions, 'briefcase', 'i-indigo', 'active reqs')}
- ${kpi('Closed Positions', r.closedPositions, 'check-circle', 'i-green', 'this year')}
- ${kpi('Avg Time to Hire', r.avgTimeToHire + 'd', 'clock', 'i-teal', 'target 30d')}
- ${kpi('Avg Time to Fill', r.avgTimeToFill + 'd', 'target', 'i-amber', 'req → offer')}
-
-
- ${kpi('Interviews Today', r.interviewsToday, 'calendar', 'i-purple')}
- ${kpi('Offers Pending', r.offersPending, 'file', 'i-blue')}
- ${kpi('Awaiting Approval', r.jobsAwaitingApproval, 'clock', 'i-amber')}
- ${kpi('Jobs Overdue', r.jobsOverdue, 'alert', 'i-red')}
-
-
- ${kpi('Conversion Rate', r.conversionRate + '%', 'trending-up', 'i-green', 'applicant → hire')}
- ${kpi('Interview Completion', r.interviewCompletion + '%', 'check-square', 'i-teal')}
- ${kpi('Avg Response Time', r.avgResponseTime + 'h', 'zap', 'i-purple', 'to candidates')}
- ${kpi('TAT Performance', r.tat + '%', 'award', 'i-indigo', 'turnaround')}
-
-
-
-
-
Monthly Hiring Trend Hires per month
-
-
-
-
Workload Heatmap Interview load
-
${heatHtml}
-
-
-
-
-
-
Recruiter Leaderboard Top performers by hires
-
${leaderHtml}
-
-
-
Candidate Pipeline This recruiter's active candidates
-
-
-
`;
-
- Charts.line(document.getElementById('recTrend'), { labels: DB.analytics.hiringTrend.labels, area: true, datasets: [{ label: 'Hires', data: r.monthlyTrend, color: Charts.PALETTE[0] }] });
- const stageCounts = ['Applied', 'Screening', 'Assessment', 'Interview', 'Offer', 'Hired'].map(() => DB.int(2, 14));
- Charts.horizontalBar(document.getElementById('recPipeline'), {
- labels: ['Applied', 'Screening', 'Assessment', 'Interview', 'Offer', 'Hired'], data: stageCounts,
- colors: Charts.PALETTE
- });
-};
diff --git a/js/reports.js b/js/reports.js
deleted file mode 100644
index 6ee9e97..0000000
--- a/js/reports.js
+++ /dev/null
@@ -1,109 +0,0 @@
-/* ============================================================
- reports.js — Reports page (cards, charts, table, export)
- ============================================================ */
-window.Views = window.Views || {};
-
-Views.reports = function () {
- const a = DB.analytics;
- const reportCards = [
- { title: 'Total Hires (YTD)', val: a.hiringTrend.hires.reduce((s, v) => s + v, 0), icn: 'award', cls: 'i-green', sub: '+18% vs last year' },
- { title: 'Total Applications', val: a.hiringTrend.applications.reduce((s, v) => s + v, 0).toLocaleString(), icn: 'users', cls: 'i-blue', sub: 'across all channels' },
- { title: 'Avg. Time to Hire', val: '27 days', icn: 'clock', cls: 'i-teal', sub: '3 days faster' },
- { title: 'Avg. Cost per Hire', val: '$4,280', icn: 'dollar', cls: 'i-amber', sub: 'within budget' }
- ];
-
- const deptRows = a.departments.map(d => {
- const rate = Math.round((d.open ? d.apps / (d.open * 40) : 0.5) * 100);
- return { dept: d.dept, open: d.open, apps: d.apps, hires: DB.int(1, 8), ttf: DB.int(28, 52), rate: Math.min(rate, 98) };
- });
-
- const table = UI.dataTable({
- pageSize: 10,
- rows: deptRows,
- columns: [
- { key: 'dept', label: 'Department', sortable: true, render: r => `
${r.dept} ` },
- { key: 'open', label: 'Open Roles', sortable: true, align: 'center' },
- { key: 'apps', label: 'Applications', sortable: true, align: 'center', render: r => `
${r.apps} ` },
- { key: 'hires', label: 'Hires', sortable: true, align: 'center' },
- { key: 'ttf', label: 'Time to Fill', sortable: true, align: 'center', render: r => r.ttf + ' days' },
- { key: 'rate', label: 'Fill Rate', sortable: true, render: r => `
${UI.pbar(r.rate)}
${r.rate}% ` }
- ]
- });
-
- const reportTypes = [
- { name: 'Hiring Funnel Report', desc: 'Conversion rates across each pipeline stage', icn: 'filter', cls: 'i-indigo' },
- { name: 'Source Effectiveness', desc: 'ROI and quality by sourcing channel', icn: 'target', cls: 'i-teal' },
- { name: 'Diversity & Inclusion', desc: 'Demographic breakdown of the pipeline', icn: 'users', cls: 'i-purple' },
- { name: 'Recruiter Scorecard', desc: 'Individual performance metrics', icn: 'award', cls: 'i-amber' },
- { name: 'Offer Analysis', desc: 'Acceptance rates and compensation trends', icn: 'file', cls: 'i-green' },
- { name: 'Interview Analytics', desc: 'Interviewer load and feedback quality', icn: 'calendar', cls: 'i-blue' }
- ];
-
- const html = `
-
-
-
Reports Recruitment metrics and downloadable insights
-
- Last 7 months This quarter This year
- ${UI.icon('download')} Export Report
-
-
-
-
- ${reportCards.map(c => `
${c.title} ${UI.icon(c.icn)}
${c.val}
`).join('')}
-
-
-
-
-
Hiring Funnel Stage-by-stage conversion
-
${UI.icon('download')}
-
-
-
-
Time to Hire vs Fill Monthly trend (days)
-
- ${Charts.legend([{ label: 'Time to Hire', color: Charts.PALETTE[0] }, { label: 'Time to Fill', color: Charts.PALETTE[2] }])}
-
-
-
-
-
Department Performance Hiring breakdown by team
-
${UI.icon('download')} CSV
- ${table.html}
-
-
-
-
Report Library Generate a detailed report
-
- ${reportTypes.map(r => `
-
-
-
${UI.icon(r.icn)}
-
${r.name}
-
${r.desc}
-
Generate ${UI.icon('chevron-right')}
-
-
`).join('')}
-
-
-
`;
-
- return {
- html,
- onMount() {
- table.mount();
- const funnel = [{ stage: 'Applied', v: 100 }, { stage: 'Screened', v: 62 }, { stage: 'Assessed', v: 41 }, { stage: 'Interviewed', v: 28 }, { stage: 'Offered', v: 14 }, { stage: 'Hired', v: 9 }];
- Charts.bar(document.getElementById('rptFunnel'), {
- labels: funnel.map(f => f.stage), data: funnel.map(f => f.v),
- colors: Charts.PALETTE, yFmt: v => v + '%'
- });
- Charts.groupedBar(document.getElementById('rptTime'), {
- labels: DB.analytics.hiringTrend.labels,
- datasets: [
- { label: 'Time to Hire', data: DB.analytics.timeToHire, color: Charts.PALETTE[0] },
- { label: 'Time to Fill', data: DB.analytics.timeToFill, color: Charts.PALETTE[2] }
- ], yFmt: v => v + 'd'
- });
- }
- };
-};
diff --git a/js/settings.js b/js/settings.js
deleted file mode 100644
index 2db2378..0000000
--- a/js/settings.js
+++ /dev/null
@@ -1,193 +0,0 @@
-/* ============================================================
- settings.js — Settings page with many tabs
- ============================================================ */
-window.Views = window.Views || {};
-window.Settings = {};
-
-Views.settings = function () {
- const tabs = ['General', 'Users', 'Roles', 'Permissions', 'Notifications', 'Email Templates', 'Career Portal', 'Branding', 'Security', 'Appearance'];
-
- const html = `
-
-
-
Settings Configure your workspace and team preferences
-
${UI.icon('check')} Save Changes
-
-
${tabs.map((t, i) => `
${t}
`).join('')}
-
- ${tabs.map((t, i) => `
${Settings.pane(t)}
`).join('')}
-
-
`;
-
- return {
- html,
- onMount() {
- const panes = document.querySelectorAll('#setPanes .tab-pane');
- document.querySelectorAll('#setTabs .tab').forEach(tab => tab.onclick = () => {
- document.querySelectorAll('#setTabs .tab').forEach(t => t.classList.remove('active'));
- tab.classList.add('active');
- panes.forEach(p => p.classList.remove('active'));
- panes[+tab.dataset.tab].classList.add('active');
- if (tab.textContent === 'Appearance') Settings._bindTheme();
- });
- Settings._bindTheme();
- }
- };
-};
-
-function toggleRow(title, desc, checked) {
- return `
`;
-}
-
-Settings.pane = function (name) {
- if (name === 'General') {
- return `
-
-
- ${toggleRow('Auto-archive stale jobs', 'Automatically close requisitions inactive for 90 days', true)}
- ${toggleRow('Duplicate detection', 'Flag candidates that already exist in the system', true)}
-
`;
- }
- if (name === 'Users') {
- const rows = DB.users.map(u => `
- ${UI.avatar(u.name, u.initials, u.color)}
- ${UI.badge(u.role, 'b-indigo')}
- ${UI.badge(u.status)}
- ${u.lastActive}
- ${UI.icon('edit')} ${UI.icon('trash')}
- `).join('');
- return `
-
Team Members ${DB.users.length} users
-
${UI.icon('plus')} Invite User
-
User Role Status Last Active Actions ${rows}
-
`;
- }
- if (name === 'Roles') {
- return `
Roles Define access levels
-
${UI.icon('plus')} Add Role
-
- ${DB.roles.map(r => `
-
${UI.icon('users')}
-
-
${r.users} users
${r.perms}
-
${UI.icon('edit')}
-
`).join('')}
-
`;
- }
- if (name === 'Permissions') {
- const modules = ['Jobs', 'Candidates', 'Interviews', 'Offers', 'Reports', 'Settings'];
- const perms = ['View', 'Create', 'Edit', 'Delete'];
- return `
Permission Matrix Recruiter role
-
Recruiter Hiring Manager Administrator
-
-
`;
- }
- if (name === 'Notifications') {
- return `
-
Email Notifications
- ${toggleRow('New applications', 'Get notified when a candidate applies', true)}
- ${toggleRow('Interview reminders', 'Reminders 30 minutes before interviews', true)}
- ${toggleRow('Offer responses', 'When candidates accept or decline offers', true)}
- ${toggleRow('Weekly digest', 'A summary of hiring activity every Monday', false)}
-
In-App Notifications
- ${toggleRow('Mentions', 'When a teammate @mentions you', true)}
- ${toggleRow('Stage changes', 'When a candidate moves stages', false)}
- ${toggleRow('Task assignments', 'When you are assigned a task', true)}
-
`;
- }
- if (name === 'Email Templates') {
- const templates = ['Application Received', 'Interview Invitation', 'Assessment Assignment', 'Offer Letter', 'Rejection — Post Interview', 'Reference Request'];
- return `
Email Templates
-
${UI.icon('plus')} New Template
-
- ${templates.map(t => `
${UI.icon('mail')}
-
${t}
Last edited 3 days ago
- ${UI.badge('Active', 'b-green')}
${UI.icon('edit')} `).join('')}
-
`;
- }
- if (name === 'Career Portal') {
- return `
-
-
- ${toggleRow('Public job board', 'Make open roles visible to the public', true)}
- ${toggleRow('Allow one-click apply', 'Let candidates apply with LinkedIn', true)}
- ${toggleRow('Show salary ranges', 'Display compensation on job listings', false)}
- ${toggleRow('Enable referrals', 'Employees can refer candidates', true)}
-
`;
- }
- if (name === 'Branding') {
- return `
-
Company Logo Displayed on career pages and emails
-
UB Upload
-
Brand Color Primary accent across the portal
-
- ${['#004d43', '#ceff71', '#25e9a5', '#8e92ff', '#1a3134', '#eafff4'].map(c => ` `).join('')}
-
-
-
`;
- }
- if (name === 'Security') {
- return `
- ${toggleRow('Two-factor authentication', 'Require 2FA for all team members', true)}
- ${toggleRow('Single Sign-On (SSO)', 'Enable SAML-based SSO login', false)}
- ${toggleRow('IP allowlist', 'Restrict access to approved IP ranges', false)}
- ${toggleRow('Audit logging', 'Track all data access and changes', true)}
-
-
-
Data Retention Auto-delete candidate data after set period
-
24 months 12 months 36 months
-
`;
- }
- if (name === 'Appearance') {
- return `
-
Theme
-
-
- ${toggleRow('Compact mode', 'Reduce spacing for denser layouts', false)}
- ${toggleRow('Show animations', 'Enable transitions and motion', true)}
-
`;
- }
- return '';
-};
-
-Settings._bindTheme = function () {
- document.querySelectorAll('.theme-opt').forEach(opt => opt.onclick = () => {
- const mode = opt.dataset.themeSet;
- if (mode === 'system') { App.setTheme(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); }
- else App.setTheme(mode);
- UI.toast('Theme updated to ' + mode, 'success');
- });
-};
diff --git a/js/tasks.js b/js/tasks.js
deleted file mode 100644
index 8a70485..0000000
--- a/js/tasks.js
+++ /dev/null
@@ -1,131 +0,0 @@
-/* ============================================================
- tasks.js — Recruitment Tasks (with saved searches & favorites)
- ============================================================ */
-window.Views = window.Views || {};
-window.Tasks = {};
-
-Views.tasks = function () {
- const state = { filter: 'All' };
-
- function render() {
- const el = document.getElementById('taskList');
- if (!el) return;
- let list = DB.tasks;
- if (state.filter === 'Open') list = list.filter(t => !t.done);
- else if (state.filter === 'Completed') list = list.filter(t => t.done);
- else if (state.filter === 'Overdue') list = list.filter(t => !t.done && t.due < new Date('2026-07-09'));
- else if (['High', 'Medium', 'Low'].includes(state.filter)) list = list.filter(t => t.priority === state.filter);
-
- if (!list.length) { el.innerHTML = `
${UI.icon('check-square')}
All caught up No tasks in this view.
`; return; }
- const prCls = { High: 'b-red', Medium: 'b-amber', Low: 'b-gray' };
- el.innerHTML = list.map(t => {
- const overdue = !t.done && t.due < new Date('2026-07-09');
- return `
-
${UI.icon('check')}
-
-
${t.title}
-
${UI.icon('users')} ${t.assignee} · ${t.type}
-
-
- ${UI.badge(t.priority, prCls[t.priority])}
-
${overdue ? 'Overdue · ' : 'Due '}${DB.fmtShort(t.due)}
-
-
`;
- }).join('');
- el.querySelectorAll('.checkbox').forEach(chk => chk.onclick = e => {
- e.stopPropagation();
- const t = DB.tasks.find(x => x.id === chk.dataset.id);
- t.done = !t.done; render(); App.updateBadges();
- UI.toast(t.done ? 'Task completed' : 'Task reopened', t.done ? 'success' : 'info');
- });
- }
- Tasks._render = render;
-
- const openCount = DB.tasks.filter(t => !t.done).length;
- const overdueCount = DB.tasks.filter(t => !t.done && t.due < new Date('2026-07-09')).length;
- const filters = ['All', 'Open', 'Completed', 'Overdue', 'High', 'Medium', 'Low'];
-
- // saved searches sidebar
- const savedHtml = DB.savedSearches.map(s => `
-
-
${UI.icon('bookmark')}
-
-
${s.count}
-
`).join('');
-
- const html = `
-
-
-
Tasks ${openCount} open · ${overdueCount} overdue
-
${UI.icon('plus')} New Task
-
-
-
-
-
${filters.map((f, i) => `${f} `).join('')}
-
-
-
-
-
Saved Searches Quick candidate filters
-
${UI.icon('plus')}
-
-
-
-
`;
-
- return {
- html,
- onMount() {
- render();
- document.querySelectorAll('#taskSeg button').forEach(b => b.onclick = () => {
- document.querySelectorAll('#taskSeg button').forEach(x => x.classList.remove('active'));
- b.classList.add('active'); state.filter = b.dataset.f; render();
- });
- }
- };
-};
-
-Tasks.open = function (id) {
- const t = DB.tasks.find(x => x.id === id);
- const c = DB.getCandidate(t.candidateId);
- UI.modal({
- title: t.title, subtitle: t.id + ' · ' + t.type,
- body: `
-
-
-
Due Date
${DB.fmtDate(t.due)}
-
Status
${t.done ? 'Completed' : 'Open'}
- ${c ? `
` : ''}
-
-
Notes
`,
- footer: `
Close
- ${c ? `
View Candidate ` : ''}
-
${UI.icon('check')} Mark Complete `
- });
-};
-Tasks._complete = function (id) { const t = DB.tasks.find(x => x.id === id); t.done = true; Tasks._render(); App.updateBadges(); UI.toast('Task completed', 'success'); };
-
-Tasks.add = function () {
- const opt = arr => arr.map(o => `
${o} `).join('');
- UI.modal({
- title: 'New Task', subtitle: 'Create a recruitment task',
- body: `
`,
- footer: `
Cancel ${UI.icon('check')} Create Task `
- });
-};
-Tasks._save = function () {
- const form = document.getElementById('taskForm');
- UI.clearErrors(form);
- const f = Object.fromEntries(new FormData(form));
- if (!f.title.trim()) { UI.fieldError(form.querySelector('[name=title]'), 'Required'); return; }
- DB.tasks.unshift({ id: 'TSK-' + (50001 + DB.tasks.length), title: f.title, candidateId: null, priority: f.priority, due: f.due ? new Date(f.due) : new Date('2026-07-16'), assignee: f.assignee, done: false, type: f.type });
- UI.closeModal(); Tasks._render(); App.updateBadges();
- UI.toast('Task created', 'success');
-};
diff --git a/js/ui.js b/js/ui.js
deleted file mode 100644
index 97ad730..0000000
--- a/js/ui.js
+++ /dev/null
@@ -1,252 +0,0 @@
-/* ============================================================
- ui.js — Reusable UI primitives & helpers
- Exposes global `UI`
- ============================================================ */
-(function () {
- 'use strict';
-
- const ICONS = {
- 'user-plus': '
',
- 'calendar': '
',
- 'check': '
',
- 'check-circle': '
',
- 'x': '
',
- 'x-circle': '
',
- 'file': '
',
- 'star': '
',
- 'message': '
',
- 'info': '
',
- 'alert': '
',
- 'eye': '
',
- 'edit': '
',
- 'trash': '
',
- 'more': '
',
- 'plus': '
',
- 'download': '
',
- 'filter': '
',
- 'clock': '
',
- 'mail': '
',
- 'phone': '
',
- 'map': '
',
- 'briefcase': '
',
- 'trending-up': '
',
- 'trending-down': '
',
- 'users': '
',
- 'award': '
',
- 'dollar': '
',
- 'target': '
',
- 'send': '
',
- 'video': '
',
- 'search': '
',
- 'chevron-left': '
',
- 'chevron-right': '
',
- 'refresh': '
',
- 'copy': '
',
- 'upload': '
',
- 'linkedin': '
',
- 'inbox': '
',
- 'sparkles': '
',
- 'zap': '
',
- 'grid': '
',
- 'bookmark': '
',
- 'paperclip': '
',
- 'external': '
',
- 'shield': '
',
- 'lock': '
',
- 'flame': '
',
- 'bell': '
',
- 'layers': '
',
- 'list': '
',
- 'check-square': '
',
- 'arrow-right': '
'
- };
-
- function icon(name, cls) { return `
${ICONS[name] || ICONS['info']} `; }
-
- function avatar(name, initials, color, cls) {
- const bg = color || DB.avatarColor(name || '');
- const init = initials || DB.initials(name || '?');
- return `
${init} `;
- }
-
- // ---------- badge helpers ----------
- const statusMap = {
- 'Open': 'b-green', 'Closed': 'b-gray', 'On Hold': 'b-amber', 'Draft': 'b-blue',
- 'Applied': 'b-blue', 'Screening': 'b-purple', 'Assessment': 'b-amber', 'Interview': 'b-indigo',
- 'Offer': 'b-teal', 'Hired': 'b-green', 'Rejected': 'b-red',
- 'Scheduled': 'b-blue', 'Completed': 'b-green', 'Cancelled': 'b-red', 'No Show': 'b-amber',
- 'Sent': 'b-blue', 'Accepted': 'b-green', 'Negotiating': 'b-amber', 'Declined': 'b-red', 'Expired': 'b-gray',
- 'In Progress': 'b-amber', 'Pending': 'b-gray', 'Active': 'b-green', 'Invited': 'b-amber',
- 'Strong Hire': 'b-green', 'Hire': 'b-teal', 'Lean Hire': 'b-amber', 'No Hire': 'b-red'
- };
- function badge(text, cls) { return `
${text} `; }
-
- function scoreChip(score) {
- // Theme tokens, not fixed hex — the old greens/blues dropped to ~2.6:1 on dark cards.
- const color = score >= 85 ? 'var(--success)' : score >= 70 ? 'var(--warning)' : score >= 55 ? 'var(--info)' : 'var(--danger)';
- return `
${score} `;
- }
-
- function pbar(pct, cls) {
- const c = pct >= 80 ? 'green' : pct >= 50 ? '' : pct >= 30 ? 'amber' : 'red';
- return `
`;
- }
-
- function avatarStack(names, max) {
- max = max || 3;
- const shown = names.slice(0, max);
- const extra = names.length - max;
- let html = '
';
- shown.forEach(n => html += avatar(n, DB.initials(n)));
- if (extra > 0) html += `+${extra} `;
- return html + '
';
- }
-
- // ---------- Modal ----------
- function modal({ title, subtitle, body, footer, size }) {
- const root = document.getElementById('modalRoot');
- root.innerHTML = `
-
-
-
-
${title} ${subtitle ? `
${subtitle}
` : ''}
-
${icon('x')}
-
-
${body}
- ${footer ? `` : ''}
-
`;
- root.classList.add('open');
- document.body.style.overflow = 'hidden';
- root.querySelectorAll('[data-close]').forEach(el => el.addEventListener('click', closeModal));
- return root;
- }
- function closeModal() {
- const root = document.getElementById('modalRoot');
- root.classList.remove('open');
- root.innerHTML = '';
- document.body.style.overflow = '';
- }
-
- // ---------- Toast ----------
- function toast(msg, type = 'info', title) {
- const root = document.getElementById('toastRoot');
- const cfg = {
- success: { i: 'check-circle', c: 'i-green', t: 'Success' },
- error: { i: 'x-circle', c: 'i-red', t: 'Error' },
- info: { i: 'info', c: 'i-blue', t: 'Notice' },
- warning: { i: 'alert', c: 'i-amber', t: 'Warning' }
- }[type] || { i: 'info', c: 'i-blue', t: 'Notice' };
- const el = document.createElement('div');
- el.className = 'toast';
- el.innerHTML = `
-
${icon(cfg.i)}
-
-
${icon('x')} `;
- root.appendChild(el);
- const remove = () => { el.classList.add('out'); setTimeout(() => el.remove(), 300); };
- el.querySelector('.toast-close').onclick = remove;
- setTimeout(remove, 4200);
- }
-
- // ---------- Sortable / paginated table ----------
- function dataTable(config) {
- // config: { columns:[{key,label,sortable,render,align}], rows, pageSize, empty }
- const state = { sortKey: null, sortDir: 1, page: 1, rows: config.rows };
- const pageSize = config.pageSize || 10;
- const id = 'tbl_' + Math.random().toString(36).slice(2, 8);
-
- function sorted() {
- let r = state.rows;
- if (state.sortKey) {
- const col = config.columns.find(c => c.key === state.sortKey);
- r = [...r].sort((a, b) => {
- let va = col.sortValue ? col.sortValue(a) : a[state.sortKey];
- let vb = col.sortValue ? col.sortValue(b) : b[state.sortKey];
- if (typeof va === 'string') { va = va.toLowerCase(); vb = (vb || '').toLowerCase(); }
- if (va < vb) return -1 * state.sortDir;
- if (va > vb) return 1 * state.sortDir;
- return 0;
- });
- }
- return r;
- }
- function render() {
- const rows = sorted();
- const total = rows.length;
- const pages = Math.max(1, Math.ceil(total / pageSize));
- if (state.page > pages) state.page = pages;
- const start = (state.page - 1) * pageSize;
- const pageRows = rows.slice(start, start + pageSize);
-
- const thead = config.columns.map(c => {
- const sortedCls = state.sortKey === c.key ? (state.sortDir === 1 ? 'sorted-asc' : 'sorted-desc') : '';
- const ind = c.sortable ? `
${state.sortKey === c.key ? (state.sortDir === 1 ? '▲' : '▼') : '⇅'} ` : '';
- return `
${c.label}${ind} `;
- }).join('');
-
- let tbody;
- if (!pageRows.length) {
- tbody = `
- ${icon('search')}
No results found ${config.empty || 'Try adjusting your filters or search.'}
`;
- } else {
- tbody = pageRows.map(row => `
${config.columns.map(c =>
- `${c.render ? c.render(row) : (row[c.key] ?? '')} `).join('')} `).join('');
- }
-
- const from = total ? start + 1 : 0, to = Math.min(start + pageSize, total);
- const pager = pageButtons(state.page, pages);
-
- const el = document.getElementById(id);
- el.innerHTML = `
-
- `;
-
- el.querySelectorAll('th.sortable').forEach(th => th.onclick = () => {
- const k = th.dataset.sort;
- if (state.sortKey === k) state.sortDir *= -1; else { state.sortKey = k; state.sortDir = 1; }
- render();
- });
- el.querySelectorAll('[data-page]').forEach(b => b.onclick = () => {
- const p = b.dataset.page;
- if (p === 'prev') state.page = Math.max(1, state.page - 1);
- else if (p === 'next') state.page = Math.min(pages, state.page + 1);
- else state.page = +p;
- render();
- });
- if (config.onRender) config.onRender(el);
- }
- function pageButtons(cur, pages) {
- let btns = `
${icon('chevron-left')} `;
- const list = [];
- for (let i = 1; i <= pages; i++) {
- if (i === 1 || i === pages || Math.abs(i - cur) <= 1) list.push(i);
- else if (list[list.length - 1] !== '…') list.push('…');
- }
- list.forEach(i => btns += i === '…' ? `
… ` : `
${i} `);
- btns += `
${icon('chevron-right')} `;
- return btns;
- }
- // public API
- return {
- html: `
`,
- mount: render,
- update(rows) { state.rows = rows; state.page = 1; render(); }
- };
- }
-
- function fieldError(inputEl, msg) {
- inputEl.classList.add('err');
- let err = inputEl.parentElement.querySelector('.field-error');
- if (err) { err.textContent = msg; err.classList.add('show'); }
- }
- function clearErrors(form) {
- form.querySelectorAll('.err').forEach(e => e.classList.remove('err'));
- form.querySelectorAll('.field-error').forEach(e => e.classList.remove('show'));
- }
-
- window.UI = { icon, avatar, badge, scoreChip, pbar, avatarStack, modal, closeModal, toast, dataTable, fieldError, clearErrors, ICONS };
-})();
diff --git a/tools/check_evidence_citations.py b/tools/check_evidence_citations.py
index 353991a..b494496 100755
--- a/tools/check_evidence_citations.py
+++ b/tools/check_evidence_citations.py
@@ -37,11 +37,42 @@ from __future__ import annotations
import os
import re
+import subprocess
import sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DOCS = os.path.join(REPO, 'docs', 'architecture')
+# --- prototype evidence outlives the prototype ------------------------------
+# The React migration deletes the browser-only prototype (`index.html`, `js/`,
+# and moves `css/styles.css` into the web app). Around 600 citations in this
+# package are evidence *about that prototype* — they were true when written and
+# stay true of it forever, so they must not rot just because the working tree
+# moved on. Resolve those paths from the `prototype-final` tag, which marks the
+# last commit where the prototype existed intact.
+#
+# If the tag is absent (a fresh clone that never fetched tags, or a checkout
+# from before the migration) this falls back to the working tree, so the script
+# keeps working either way.
+LEGACY_TAG = 'prototype-final'
+LEGACY_PREFIXES = ('js/', 'index.html', 'css/', 'devserver.py')
+
+
+def is_legacy(path: str) -> bool:
+ return path.startswith(LEGACY_PREFIXES)
+
+
+def read_from_tag(path: str) -> list[str] | None:
+ """Contents of `path` at LEGACY_TAG, or None if unavailable."""
+ try:
+ out = subprocess.run(
+ ['git', '-C', REPO, 'show', f'{LEGACY_TAG}:{path}'],
+ capture_output=True, check=True,
+ )
+ except (OSError, subprocess.CalledProcessError):
+ return None
+ return out.stdout.decode('utf-8', 'replace').split('\n')
+
# Only extensions that exist in this repository today. Citations to planned files
# (.sql migrations, .tsx components, .yml workflows) are intentionally not matched —
# they cannot be verified and are design intent, not evidence.
@@ -158,15 +189,28 @@ def main() -> int:
return 1
cache: dict[str, list[str] | None] = {}
+ from_tag: set[str] = set()
+
+ def read_worktree(path: str) -> list[str] | None:
+ try:
+ with open(os.path.join(REPO, path), encoding='utf-8') as fh:
+ return fh.read().split('\n')
+ except OSError:
+ return None
def source(path: str) -> list[str] | None:
if path not in cache:
- full = os.path.join(REPO, path)
- try:
- with open(full, encoding='utf-8') as fh:
- cache[path] = fh.read().split('\n')
- except OSError:
- cache[path] = None
+ if is_legacy(path):
+ # Prefer the tagged prototype so these citations are stable whether
+ # or not the files still exist in the tree.
+ tagged = read_from_tag(path)
+ if tagged is not None:
+ from_tag.add(path)
+ cache[path] = tagged
+ else:
+ cache[path] = read_worktree(path)
+ else:
+ cache[path] = read_worktree(path)
return cache[path]
docs = []
@@ -241,13 +285,20 @@ def main() -> int:
f'{claim_checked} claim/anchor pairings checked '
f'({len(CLAIM_RULES)} rules)'
)
+ if from_tag:
+ print(
+ f'{len(from_tag)} prototype file(s) resolved from `{LEGACY_TAG}` '
+ f'rather than the working tree: {", ".join(sorted(from_tag))}'
+ )
if problems:
print(f'\n{len(problems)} bad citation(s):\n', file=sys.stderr)
for p in problems:
print(f' {p}', file=sys.stderr)
print(
'\nFix the citation, or update ANCHORS in this script if the source moved '
- 'deliberately.',
+ f'deliberately. Prototype paths ({", ".join(LEGACY_PREFIXES)}) resolve from '
+ f'the `{LEGACY_TAG}` tag, so moving or deleting them in the working tree '
+ 'does not affect this check.',
file=sys.stderr,
)
return 1