/* ============================================================
data.js — Realistic dummy dataset + generators
Exposes global `DB`
============================================================ */
(function () {
'use strict';
// ---------- seeded pseudo-random for stable data ----------
let seed = 88123;
function rand() { seed = (seed * 9301 + 49297) % 233280; return seed / 233280; }
function pick(arr) { return arr[Math.floor(rand() * arr.length)]; }
function pickN(arr, n) {
const copy = arr.slice(), out = [];
for (let i = 0; i < n && copy.length; i++) out.push(copy.splice(Math.floor(rand() * copy.length), 1)[0]);
return out;
}
function int(min, max) { return Math.floor(rand() * (max - min + 1)) + min; }
// ---------- reference lists ----------
const departments = ['Engineering', 'Product', 'Design', 'Marketing', 'Sales', 'Finance', 'People Ops', 'Customer Success', 'Data & Analytics', 'Operations'];
const businessUnits = ['Core Platform', 'Growth', 'Enterprise', 'Consumer', 'Corporate'];
const locations = ['San Francisco, CA', 'New York, NY', 'Austin, TX', 'London, UK', 'Remote — US', 'Remote — EU', 'Berlin, DE', 'Toronto, CA', 'Singapore', 'Seattle, WA'];
const empTypes = ['Full-time', 'Part-time', 'Contract', 'Internship'];
const grades = ['L2', 'L3', 'L4', 'L5', 'L6', 'L7'];
const jobStatuses = ['Open', 'On Hold', 'Closed', 'Draft'];
const educationLevels = ["Bachelor's Degree", "Master's Degree", "PhD", "Associate Degree", "High School"];
const stages = ['Applied', 'Screening', 'Assessment', 'Interview', 'Offer', 'Hired', 'Rejected'];
const sources = ['LinkedIn', 'Company Site', 'Referral', 'Indeed', 'Job Fair', 'Agency', 'GitHub', 'AngelList'];
const companies = ['Stripe', 'Airbnb', 'Datadog', 'Notion', 'Figma', 'Shopify', 'Snowflake', 'Twilio', 'Coinbase', 'Atlassian', 'Asana', 'Ramp', 'Brex', 'Vercel', 'Retool', 'Amplitude', 'Segment', 'MongoDB', 'HashiCorp', 'Cloudflare'];
const firstNames = ['Olivia', 'Liam', 'Emma', 'Noah', 'Ava', 'Ethan', 'Sophia', 'Mason', 'Isabella', 'Lucas', 'Mia', 'Aiden', 'Amelia', 'James', 'Harper', 'Benjamin', 'Evelyn', 'Elijah', 'Abigail', 'Daniel', 'Priya', 'Wei', 'Diego', 'Fatima', 'Yuki', 'Omar', 'Ingrid', 'Kwame', 'Sofia', 'Arjun', 'Chloe', 'Marcus', 'Nadia', 'Tariq', 'Lena', 'Hassan', 'Grace', 'Felix', 'Zara', 'Kai'];
const lastNames = ['Chen', 'Patel', 'Kim', 'Rodriguez', 'Nguyen', 'Johnson', 'Williams', 'Garcia', 'Müller', 'Rossi', 'Silva', 'Okafor', 'Anderson', 'Ahmed', 'Cohen', 'Novak', 'Yamamoto', 'Brown', 'Dubois', 'Schmidt', 'Ivanov', 'Lopez', 'Martins', 'Andersson', 'Reyes', 'Khan', 'Tremblay', 'Fischer', 'Costa', 'Walsh'];
const jobTitlesByDept = {
'Engineering': ['Senior Backend Engineer', 'Staff Frontend Engineer', 'Platform Engineer', 'Engineering Manager', 'Site Reliability Engineer', 'Mobile Engineer (iOS)', 'Full-Stack Engineer'],
'Product': ['Senior Product Manager', 'Group Product Manager', 'Technical Product Manager', 'Product Operations Lead'],
'Design': ['Senior Product Designer', 'UX Researcher', 'Brand Designer', 'Design Systems Lead'],
'Marketing': ['Growth Marketing Manager', 'Content Strategist', 'Demand Generation Lead', 'SEO Specialist'],
'Sales': ['Enterprise Account Executive', 'Sales Development Rep', 'Solutions Engineer', 'Sales Manager'],
'Finance': ['Financial Analyst', 'Controller', 'FP&A Manager', 'Accounting Lead'],
'People Ops': ['HR Business Partner', 'Recruiting Coordinator', 'People Operations Manager', 'Compensation Analyst'],
'Customer Success': ['Customer Success Manager', 'Support Engineer', 'Onboarding Specialist', 'CS Team Lead'],
'Data & Analytics': ['Senior Data Scientist', 'Analytics Engineer', 'Data Platform Engineer', 'ML Engineer'],
'Operations': ['Operations Manager', 'Business Operations Analyst', 'Procurement Lead', 'Program Manager']
};
const skillsPool = ['JavaScript', 'TypeScript', 'React', 'Node.js', 'Python', 'Go', 'AWS', 'Kubernetes', 'GraphQL', 'PostgreSQL', 'System Design', 'Figma', 'Product Strategy', 'SQL', 'Data Modeling', 'Leadership', 'A/B Testing', 'Salesforce', 'Communication', 'Roadmapping'];
const benefitsPool = ['Equity', '401(k) match', 'Unlimited PTO', 'Health & Dental', 'Remote stipend', 'Learning budget', 'Parental leave', 'Wellness program'];
function fullName() { return pick(firstNames) + ' ' + pick(lastNames); }
function initials(name) { return name.split(' ').map(p => p[0]).slice(0, 2).join('').toUpperCase(); }
function email(name) { return name.toLowerCase().replace(/[^a-z ]/g, '').replace(/ /g, '.') + '@' + pick(['gmail.com', 'outlook.com', 'proton.me', 'icloud.com']); }
function phone() { return '+1 (' + int(200, 989) + ') ' + int(200, 999) + '-' + int(1000, 9999); }
function daysAgo(n) { const d = new Date('2026-07-09T09:00:00'); d.setDate(d.getDate() - n); return d; }
function fmtDate(d) { return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); }
function fmtShort(d) { return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); }
const avatarColors = ['var(--av-1)', 'var(--av-2)', 'var(--av-3)', 'var(--av-4)', 'var(--av-5)', 'var(--av-6)', 'var(--av-7)', 'var(--av-8)'];
function avatarColor(name) { let s = 0; for (const c of name) s += c.charCodeAt(0); return avatarColors[s % avatarColors.length]; }
// ---------- Recruiters ----------
const recruiters = [];
for (let i = 0; i < 15; i++) {
const name = fullName();
recruiters.push({
id: 'REC-' + (101 + i), name, initials: initials(name), email: email(name),
color: avatarColor(name),
hires: int(4, 34), openReqs: int(2, 12), avgTimeToHire: int(18, 46),
offerAccept: int(72, 98), rating: (rand() * 1.4 + 3.5).toFixed(1)
});
}
// ---------- Hiring Managers ----------
const managers = [];
for (let i = 0; i < 12; i++) {
const name = fullName();
const dept = departments[i % departments.length];
managers.push({
id: 'HM-' + (201 + i), name, initials: initials(name), email: email(name).replace(/@.*/, '@utopiabrands.com'),
color: avatarColor(name), department: dept, title: 'Director, ' + dept,
openReqs: int(1, 6), teamSize: int(6, 40)
});
}
// ---------- Jobs ----------
const jobs = [];
for (let i = 0; i < 26; i++) {
const dept = pick(departments);
const title = pick(jobTitlesByDept[dept]);
const mgr = pick(managers);
const rec = pick(recruiters);
const status = i < 16 ? 'Open' : pick(jobStatuses);
const created = daysAgo(int(3, 120));
const apps = status === 'Draft' ? 0 : int(12, 220);
jobs.push({
id: 'JOB-' + (1001 + i), title, department: dept, businessUnit: pick(businessUnits),
grade: pick(grades), manager: mgr.name, managerId: mgr.id, recruiter: rec.name, recruiterId: rec.id,
location: pick(locations), type: pick(empTypes), vacancies: int(1, 5), applications: apps,
status, created, deadline: daysAgo(-int(5, 60)),
salaryMin: int(80, 160) * 1000, salaryMax: 0, experience: int(2, 10) + '+ years',
education: pick(educationLevels), skills: pickN(skillsPool, int(4, 7)), benefits: pickN(benefitsPool, int(3, 5)),
description: 'We are looking for a ' + title + ' to join our ' + dept + ' team. You will drive high-impact initiatives, collaborate cross-functionally, and help shape the future of our product.',
responsibilities: ['Own and deliver key projects end-to-end', 'Collaborate with cross-functional partners', 'Mentor peers and raise the quality bar', 'Contribute to strategy and roadmap planning'],
progress: status === 'Closed' ? 100 : int(10, 92)
});
jobs[i].salaryMax = jobs[i].salaryMin + int(20, 60) * 1000;
}
// ---------- Candidates ----------
const openJobs = jobs.filter(j => j.status === 'Open');
const candidates = [];
const stageWeights = ['Applied', 'Applied', 'Applied', 'Screening', 'Screening', 'Assessment', 'Interview', 'Interview', 'Offer', 'Hired', 'Rejected', 'Rejected'];
for (let i = 0; i < 100; i++) {
const name = fullName();
const job = pick(openJobs.length ? openJobs : jobs);
const stage = pick(stageWeights);
const applied = daysAgo(int(1, 75));
candidates.push({
id: 'CAN-' + (5001 + i), name, initials: initials(name), color: avatarColor(name),
email: email(name), phone: phone(),
jobId: job.id, jobTitle: job.title, department: job.department,
experience: int(1, 14), currentCompany: pick(companies), currentTitle: pick(jobTitlesByDept[job.department] || ['Specialist']),
location: pick(locations), stage, status: stage,
aiScore: int(52, 98), source: pick(sources), recruiter: job.recruiter, recruiterId: job.recruiterId,
applied, education: pick(educationLevels), skills: pickN(skillsPool, int(3, 6)),
rating: (rand() * 2 + 3).toFixed(1),
salary: int(90, 190) * 1000
});
}
// ---------- Interviews ----------
const interviewTypes = ['Phone Screen', 'Technical', 'System Design', 'Onsite Loop', 'Hiring Manager', 'Culture Fit', 'Final Round'];
const meetingTypes = ['Video Call', 'On-site', 'Phone'];
const interviewStatuses = ['Scheduled', 'Completed', 'Cancelled', 'No Show'];
const interviews = [];
const interviewPool = candidates.filter(c => ['Screening', 'Assessment', 'Interview', 'Offer'].includes(c.stage));
for (let i = 0; i < 40; i++) {
const cand = pick(interviewPool.length ? interviewPool : candidates);
const offset = int(-20, 14);
const when = daysAgo(-offset);
when.setHours(int(9, 17), pick([0, 30]), 0, 0);
const status = offset > 0 ? 'Scheduled' : pick(['Completed', 'Completed', 'Completed', 'Cancelled', 'No Show']);
interviews.push({
id: 'INT-' + (7001 + i), candidate: cand.name, candidateId: cand.id, candInitials: cand.initials, color: cand.color,
jobTitle: cand.jobTitle, type: pick(interviewTypes), meeting: pick(meetingTypes),
when, status, duration: pick([30, 45, 60, 90]),
interviewers: pickN([...recruiters, ...managers].map(r => r.name), int(1, 3)),
feedback: status === 'Completed' ? pick(['Strong Hire', 'Hire', 'Lean Hire', 'No Hire']) : null,
score: status === 'Completed' ? int(2, 5) : null
});
}
interviews.sort((a, b) => a.when - b.when);
// ---------- Assessments ----------
const assessTypes = ['Coding Challenge', 'Take-home Project', 'Cognitive Test', 'Personality Assessment', 'SQL Test', 'Case Study'];
const assessments = [];
for (let i = 0; i < 24; i++) {
const cand = pick(candidates);
const status = pick(['Completed', 'Completed', 'In Progress', 'Pending', 'Expired']);
assessments.push({
id: 'ASM-' + (8001 + i), candidate: cand.name, candidateId: cand.id, initials: cand.initials, color: cand.color,
jobTitle: cand.jobTitle, type: pick(assessTypes), status,
score: status === 'Completed' ? int(45, 99) : null,
assigned: daysAgo(int(1, 30)), due: daysAgo(-int(1, 10)),
duration: pick(['45 min', '60 min', '90 min', '2 hours', '3 days'])
});
}
// ---------- Offers ----------
const offerStatuses = ['Sent', 'Accepted', 'Negotiating', 'Declined', 'Draft', 'Expired'];
const offers = [];
const offerPool = candidates.filter(c => ['Offer', 'Hired'].includes(c.stage));
for (let i = 0; i < 22; i++) {
const cand = offerPool[i % offerPool.length] || pick(candidates);
const status = cand.stage === 'Hired' ? 'Accepted' : pick(offerStatuses);
const base = int(95, 210) * 1000;
offers.push({
id: 'OFR-' + (9001 + i), candidate: cand.name, candidateId: cand.id, initials: cand.initials, color: cand.color,
jobTitle: cand.jobTitle, department: cand.department, status,
base, equity: int(0, 40) + 'k RSU', bonus: int(5, 25) + '%',
sent: daysAgo(int(1, 25)), expires: daysAgo(-int(3, 14)), recruiter: cand.recruiter
});
}
// ---------- Activity feed ----------
const activityTemplates = [
{ icon: 'user-plus', color: 'i-green', text: (n, j) => `${n} applied for ${j}` },
{ icon: 'calendar', color: 'i-blue', text: (n, j) => `Interview scheduled with ${n} for ${j}` },
{ icon: 'check', color: 'i-teal', text: (n, j) => `${n} moved to Offer stage` },
{ icon: 'file', color: 'i-purple', text: (n, j) => `Offer sent to ${n} for ${j}` },
{ icon: 'star', color: 'i-amber', text: (n, j) => `${n} completed an assessment` },
{ icon: 'x', color: 'i-red', text: (n, j) => `${n} was rejected for ${j}` }
];
const activity = [];
for (let i = 0; i < 18; i++) {
const t = pick(activityTemplates);
const cand = pick(candidates);
activity.push({ icon: t.icon, color: t.color, html: t.text(cand.name, cand.jobTitle), time: int(1, 300), candidateId: cand.id });
}
activity.sort((a, b) => a.time - b.time);
function relTime(mins) {
if (mins < 60) return mins + 'm ago';
if (mins < 1440) return Math.floor(mins / 60) + 'h ago';
return Math.floor(mins / 1440) + 'd ago';
}
// ---------- Notifications ----------
const notifications = [
{ icon: 'user-plus', color: 'i-green', title: 'New application', text: candidates[0].name + ' applied for ' + candidates[0].jobTitle, time: '8m ago', unread: true },
{ icon: 'calendar', color: 'i-blue', title: 'Interview reminder', text: 'Technical interview at 2:00 PM today', time: '25m ago', unread: true },
{ icon: 'check', color: 'i-teal', title: 'Offer accepted', text: offers[0].candidate + ' accepted the offer 🎉', time: '1h ago', unread: true },
{ icon: 'message', color: 'i-purple', title: 'New message', text: 'Hiring manager left feedback on a candidate', time: '2h ago', unread: true },
{ icon: 'star', color: 'i-amber', title: 'Assessment completed', text: assessments[0].candidate + ' scored 87% on Coding Challenge', time: '4h ago', unread: false },
{ icon: 'file', color: 'i-indigo', title: 'Approval needed', text: 'Job requisition JOB-1004 awaits your approval', time: '6h ago', unread: false },
{ icon: 'x', color: 'i-red', title: 'Interview cancelled', text: 'Culture fit interview was cancelled by candidate', time: '1d ago', unread: false }
];
// ---------- Messages ----------
const messages = [
{ name: managers[0].name, initials: managers[0].initials, color: managers[0].color, text: 'Can we move the onsite to Thursday?', time: '10m', unread: true },
{ name: recruiters[2].name, initials: recruiters[2].initials, color: recruiters[2].color, text: 'Shortlist is ready for your review.', time: '40m', unread: true },
{ name: candidates[5].name, initials: candidates[5].initials, color: candidates[5].color, text: 'Thank you! Looking forward to it.', time: '2h', unread: false },
{ name: managers[3].name, initials: managers[3].initials, color: managers[3].color, text: 'Approved the offer — go ahead.', time: '5h', unread: false }
];
// ---------- Aggregate analytics ----------
const analytics = {
hiringTrend: { labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'], hires: [12, 18, 15, 22, 19, 26, 24], applications: [180, 240, 210, 320, 290, 380, 410] },
pipeline: stages.filter(s => s !== 'Rejected').map(s => ({ stage: s, count: candidates.filter(c => c.stage === s).length })),
sources: sources.map(s => ({ source: s, count: candidates.filter(c => c.source === s).length })),
departments: departments.map(d => ({ dept: d, open: jobs.filter(j => j.department === d && j.status === 'Open').length, apps: candidates.filter(c => c.department === d).length })),
offerAcceptance: { accepted: offers.filter(o => o.status === 'Accepted').length, declined: offers.filter(o => o.status === 'Declined').length, pending: offers.filter(o => ['Sent', 'Negotiating'].includes(o.status)).length },
timeToHire: [32, 28, 35, 30, 26, 29, 27],
timeToFill: [45, 42, 48, 40, 38, 41, 39]
};
// ---------- KPIs ----------
const today = new Date('2026-07-09');
const kpis = {
openJobs: jobs.filter(j => j.status === 'Open').length,
closedJobs: jobs.filter(j => j.status === 'Closed').length,
totalCandidates: candidates.length,
interviewsToday: interviews.filter(iv => iv.when.toDateString() === today.toDateString()).length || 5,
offersSent: offers.filter(o => o.status !== 'Draft').length,
offersAccepted: offers.filter(o => o.status === 'Accepted').length,
timeToHire: 27,
timeToFill: 39,
costPerHire: 4280,
hires: candidates.filter(c => c.stage === 'Hired').length
};
// ---------- Roles / users for settings ----------
const roles = [
{ name: 'Administrator', users: 3, desc: 'Full access to all settings and data', perms: 'All permissions' },
{ name: 'Recruiter', users: 15, desc: 'Manage jobs, candidates, and interviews', perms: '18 permissions' },
{ name: 'Hiring Manager', users: 12, desc: 'Review candidates and provide feedback', perms: '9 permissions' },
{ name: 'Interviewer', users: 24, desc: 'View assigned interviews and submit scorecards', perms: '5 permissions' },
{ name: 'Coordinator', users: 6, desc: 'Schedule interviews and manage logistics', perms: '11 permissions' }
];
const users = recruiters.slice(0, 8).map((r, i) => ({
name: r.name, initials: r.initials, color: r.color, email: r.email,
role: pick(['Administrator', 'Recruiter', 'Hiring Manager', 'Coordinator']),
status: i % 5 === 0 ? 'Invited' : 'Active', lastActive: pick(['2m ago', '1h ago', 'Yesterday', '3d ago'])
}));
// ============================================================
// ENTERPRISE DATA (phase 2)
// ============================================================
// ---------- Candidate sources w/ metadata ----------
const sourceMeta = {
'Microsoft Outlook': { icon: 'mail', color: '#0078d4', channel: 'Email' },
'Career Portal': { icon: 'briefcase', color: 'var(--c1)', channel: 'Portal' },
'Manual CV Upload': { icon: 'upload', color: 'var(--c2)', channel: 'Upload' },
'LinkedIn': { icon: 'linkedin', color: '#0a66c2', channel: 'Job Board' },
'Indeed': { icon: 'briefcase', color: '#2164f3', channel: 'Job Board' },
'Rozee': { icon: 'briefcase', color: '#e5322d', channel: 'Job Board' },
'Mustakbil': { icon: 'briefcase', color: '#00a651', channel: 'Job Board' },
'Employee Referral': { icon: 'users', color: 'var(--c3)', channel: 'Referral' },
'Recruitment Agency': { icon: 'briefcase', color: 'var(--c6)', channel: 'Agency' },
'Campus Hiring': { icon: 'award', color: 'var(--c5)', channel: 'Campus' },
'Walk-in': { icon: 'user-plus', color: 'var(--c6)', channel: 'Direct' }
};
const inboxSources = Object.keys(sourceMeta);
const processingStatuses = ['Unread', 'Imported', 'Processed', 'Rejected', 'Duplicate'];
const resumeStatuses = ['Parsed', 'Pending', 'Parsing', 'Failed'];
// ---------- Recruitment Inbox items ----------
const inbox = [];
for (let i = 0; i < 48; i++) {
const name = fullName();
const job = pick(openJobs.length ? openJobs : jobs);
const src = pick(inboxSources);
const proc = i < 14 ? 'Unread' : pick(processingStatuses);
const rs = proc === 'Unread' ? pick(['Pending', 'Parsing']) : pick(['Parsed', 'Parsed', 'Parsed', 'Failed']);
const received = daysAgo(int(0, 10));
received.setHours(int(7, 20), int(0, 59), 0, 0);
inbox.push({
id: 'APP-' + (30001 + i), name, initials: initials(name), color: avatarColor(name),
position: job.title, jobId: job.id, source: src, sourceMeta: sourceMeta[src],
resumeStatus: rs, atsScore: int(48, 97), recruiter: pick(recruiters).name,
received, processing: proc, unread: proc === 'Unread',
email: email(name), phone: phone(), experience: int(1, 12),
attachment: name.split(' ')[0] + '_Resume.pdf',
duplicate: proc === 'Duplicate'
});
}
inbox.sort((a, b) => b.received - a.received);
// ---------- Outlook Emails ----------
const emailSubjects = [
'Application for {job}', 'Interested in the {job} role', 'CV — {job} position',
'Referral: strong candidate for {job}', 'Following up on my application', 'Resume attached — {job}'
];
const emails = [];
for (let i = 0; i < 20; i++) {
const name = fullName();
const job = pick(openJobs.length ? openJobs : jobs);
const when = daysAgo(int(0, 6)); when.setHours(int(7, 20), int(0, 59), 0, 0);
emails.push({
id: 'EML-' + (40001 + i), from: name, fromEmail: email(name), initials: initials(name), color: avatarColor(name),
subject: pick(emailSubjects).replace('{job}', job.title), jobTitle: job.title, jobId: job.id,
preview: `Dear Hiring Team, I am writing to express my strong interest in the ${job.title} position. With ${int(3, 12)} years of experience, I believe I would be a great fit…`,
body: `Dear Hiring Team,\n\nI am writing to express my strong interest in the ${job.title} position at Utopia Brands. With over ${int(3, 12)} years of experience in ${job.department.toLowerCase()}, I have developed a strong foundation in delivering high-impact work.\n\nIn my current role at ${pick(companies)}, I have led cross-functional initiatives and consistently exceeded targets. I am confident my background aligns well with your requirements.\n\nPlease find my resume attached for your review. I would welcome the opportunity to discuss how I can contribute to your team.\n\nBest regards,\n${name}\n${phone()}`,
attachment: name.split(' ')[0] + '_CV.pdf', attachmentSize: int(120, 480) + ' KB',
when, unread: i < 8, imported: i >= 14, atsScore: int(50, 95)
});
}
emails.sort((a, b) => b.when - a.when);
// ---------- Publishing platforms ----------
const publishPlatforms = [
{ name: 'Career Portal', icon: 'briefcase', color: 'var(--c1)', connected: true, cost: 'Free' },
{ name: 'LinkedIn', icon: 'linkedin', color: '#0a66c2', connected: true, cost: '$$$' },
{ name: 'Indeed', icon: 'briefcase', color: '#2164f3', connected: true, cost: '$$' },
{ name: 'Rozee', icon: 'briefcase', color: '#e5322d', connected: true, cost: '$' },
{ name: 'Mustakbil', icon: 'briefcase', color: '#00a651', connected: true, cost: '$' },
{ name: 'Google Jobs', icon: 'search', color: '#ea4335', connected: true, cost: 'Free' },
{ name: 'Facebook Jobs', icon: 'users', color: '#1877f2', connected: false, cost: '$$' },
{ name: 'Glassdoor', icon: 'briefcase', color: '#0caa41', connected: false, cost: '$$' }
];
// per-job publishing records
const publishings = [];
openJobs.slice(0, 14).forEach(job => {
const plats = pickN(publishPlatforms.filter(p => p.connected).map(p => p.name), int(2, 5));
plats.forEach(pl => {
const views = int(120, 4200), clicks = Math.round(views * (rand() * 0.3 + 0.08)), apps = Math.round(clicks * (rand() * 0.25 + 0.05));
publishings.push({ jobId: job.id, jobTitle: job.title, platform: pl, status: pick(['Live', 'Live', 'Live', 'Pending', 'Paused']), views, clicks, apps, published: daysAgo(int(2, 40)) });
});
});
// ---------- Extend recruiters with enterprise metrics ----------
recruiters.forEach((r, idx) => {
r.openPositions = int(3, 12);
r.closedPositions = int(4, 28);
r.avgTimeToFill = r.avgTimeToHire + int(6, 16);
r.workload = int(28, 96);
r.interviewsToday = int(0, 5);
r.offersPending = int(0, 6);
r.jobsAwaitingApproval = int(0, 4);
r.jobsOverdue = int(0, 3);
r.efficiency = int(62, 97);
r.conversionRate = int(8, 26);
r.interviewCompletion = int(72, 99);
r.avgResponseTime = (rand() * 6 + 1).toFixed(1);
r.sla = pick(['On Track', 'On Track', 'On Track', 'At Risk', 'Breached']);
r.tat = int(78, 99);
r.monthlyTrend = Array.from({ length: 7 }, () => int(2, 9));
r.heatmap = Array.from({ length: 7 }, () => Array.from({ length: 5 }, () => int(0, 5))); // 7 days x 5 weeks-ish
r.department = departments[idx % departments.length];
});
// ---------- Extend candidates with ATS match detail ----------
candidates.forEach(c => {
const job = getJobLocal(c.jobId);
const required = job ? job.skills : pickN(skillsPool, 5);
const matched = required.filter(s => c.skills.includes(s));
const missing = required.filter(s => !c.skills.includes(s));
c.matchedSkills = matched.length ? matched : c.skills.slice(0, 2);
c.missingSkills = missing;
c.recommendation = c.aiScore >= 82 ? 'Strong Match' : c.aiScore >= 65 ? 'Potential Match' : 'Weak Match';
c.subScores = {
skills: Math.min(99, c.aiScore + int(-8, 8)),
experience: Math.min(99, c.aiScore + int(-12, 10)),
education: Math.min(99, c.aiScore + int(-10, 12)),
keywords: Math.min(99, c.aiScore + int(-14, 8)),
location: pick([100, 100, 80, 60]),
salary: pick([100, 90, 75, 55])
};
c.noticePeriod = pick(['Immediate', '2 weeks', '1 month', '2 months', '3 months']);
c.availability = pick(['Immediate', 'Immediate', '2 weeks', '1 month', 'Passive']);
c.certifications = pickN(['AWS Certified', 'PMP', 'Scrum Master', 'Google Analytics', 'CFA', 'None'], int(0, 2));
c.favorite = false;
c.interviewStatus = pick(['Not Scheduled', 'Scheduled', 'Completed', 'Not Scheduled']);
});
function getJobLocal(id) { return jobs.find(j => j.id === id); }
// ---------- Recruitment Tasks ----------
const taskTypes = ['Screen candidate', 'Schedule interview', 'Send offer', 'Review CV', 'Follow up', 'Submit scorecard', 'Approve requisition', 'Reference check'];
const tasks = [];
for (let i = 0; i < 16; i++) {
const cand = pick(candidates);
tasks.push({
id: 'TSK-' + (50001 + i), title: pick(taskTypes) + ' — ' + cand.name, candidateId: cand.id,
priority: pick(['High', 'Medium', 'Medium', 'Low']), due: daysAgo(-int(-3, 7)),
assignee: pick(recruiters).name, done: i % 6 === 0, type: pick(['Interview', 'Review', 'Offer', 'Admin'])
});
}
// ---------- Saved searches / filters ----------
const savedSearches = [
{ name: 'Senior Engineers · SF', count: 24, filters: 'Engineering · L5+ · San Francisco' },
{ name: 'Strong Match Designers', count: 11, filters: 'Design · ATS ≥ 85' },
{ name: 'Immediate Availability', count: 38, filters: 'Availability: Immediate' },
{ name: 'Referrals to Review', count: 9, filters: 'Source: Referral · Unread' }
];
// ---------- Interview evaluation templates ----------
const evalTemplates = [
{ name: 'Engineering — Technical', dept: 'Engineering', criteria: ['Technical Skills', 'Problem Solving', 'System Design', 'Communication', 'Culture Fit'] },
{ name: 'Product — PM Loop', dept: 'Product', criteria: ['Product Sense', 'Analytical', 'Execution', 'Leadership', 'Communication'] },
{ name: 'Design — Portfolio', dept: 'Design', criteria: ['Craft', 'Process', 'Collaboration', 'Communication', 'Culture Fit'] },
{ name: 'General — Behavioral', dept: 'All', criteria: ['Communication', 'Technical', 'Problem Solving', 'Leadership', 'Culture Fit'] }
];
// ---------- RBAC modules & permission types ----------
const rbacModules = ['Dashboard', 'Recruitment Inbox', 'Jobs', 'Candidates', 'Pipeline', 'Interviews', 'Assessments', 'Offers', 'Reports', 'Analytics', 'Job Board', 'Settings', 'RBAC & Users'];
const permTypes = ['View', 'Create', 'Edit', 'Delete', 'Approve', 'Export', 'Manage', 'Configure'];
const rbacRoles = [
{ name: 'System Administrator', users: 2, color: 'var(--av-8)', desc: 'Unrestricted access to all modules and configuration', level: 'Administrator' },
{ name: 'HR Administrator', users: 4, color: 'var(--av-7)', desc: 'Manage recruitment operations and team settings', level: 'Manage' },
{ name: 'Recruiter', users: 15, color: 'var(--av-1)', desc: 'Full candidate and job lifecycle management', level: 'Edit' },
{ name: 'Hiring Manager', users: 12, color: 'var(--av-3)', desc: 'Review candidates, interview, approve offers', level: 'Approve' },
{ name: 'Department Head', users: 8, color: 'var(--av-2)', desc: 'Oversight of departmental hiring and approvals', level: 'Approve' },
{ name: 'CEO', users: 1, color: 'var(--av-5)', desc: 'Executive dashboards and reporting', level: 'View' },
{ name: 'Interviewer', users: 24, color: 'var(--av-6)', desc: 'Access assigned interviews and submit scorecards', level: 'View' },
{ name: 'Candidate', users: 0, color: 'var(--av-4)', desc: 'Self-service portal access (external)', level: 'View' }
];
// default permission matrix per role (module -> [bool x permTypes])
function buildMatrix(level) {
const idx = { 'View': 1, 'Edit': 3, 'Approve': 5, 'Manage': 7, 'Administrator': 8 }[level] || 1;
const m = {};
rbacModules.forEach(mod => { m[mod] = permTypes.map((p, i) => i < idx); });
return m;
}
rbacRoles.forEach(r => r.matrix = buildMatrix(r.level));
// ---------- Future AI modules ----------
const aiModules = [
{ name: 'Resume Ranking', desc: 'Automatically rank applicants for any job by fit', icon: 'award', cls: 'i-indigo', status: 'Beta' },
{ name: 'Candidate Matching', desc: 'Match candidates to the best-fit open roles', icon: 'target', cls: 'i-teal', status: 'Beta' },
{ name: 'Resume Summary', desc: 'One-click AI summary of any resume', icon: 'file', cls: 'i-blue', status: 'Beta' },
{ name: 'Candidate Comparison', desc: 'Side-by-side AI comparison of shortlisted candidates', icon: 'users', cls: 'i-purple', status: 'Coming Soon' },
{ name: 'JD Generator', desc: 'Generate polished job descriptions from a brief', icon: 'edit', cls: 'i-green', status: 'Beta' },
{ name: 'Email Generator', desc: 'Draft candidate emails in your brand voice', icon: 'mail', cls: 'i-amber', status: 'Beta' },
{ name: 'Offer Letter Generator', desc: 'Auto-generate compliant offer letters', icon: 'file', cls: 'i-red', status: 'Coming Soon' },
{ name: 'Interview Question Generator', desc: 'Role-specific question banks on demand', icon: 'message', cls: 'i-indigo', status: 'Beta' },
{ name: 'Candidate Recommendation', desc: 'Proactive "you should talk to…" suggestions', icon: 'star', cls: 'i-teal', status: 'Coming Soon' },
{ name: 'Recruitment Analytics', desc: 'Natural-language analytics across your funnel', icon: 'trending-up', cls: 'i-blue', status: 'Beta' },
{ name: 'Hiring Forecast', desc: 'Predict time-to-hire and pipeline health', icon: 'trending-up', cls: 'i-purple', status: 'Coming Soon' },
{ name: 'Skill Gap Analysis', desc: 'Identify missing skills across your pipeline', icon: 'target', cls: 'i-green', status: 'Beta' },
{ name: 'Recruiter Copilot', desc: 'An assistant embedded in every workflow', icon: 'message', cls: 'i-amber', status: 'Beta' },
{ name: 'Hiring Insights', desc: 'Weekly AI-generated hiring insights digest', icon: 'info', cls: 'i-red', status: 'Coming Soon' },
{ name: 'Natural Language Search', desc: 'Search candidates in plain English', icon: 'search', cls: 'i-indigo', status: 'Beta' }
];
// ---------- AI assistant suggested prompts ----------
const aiPrompts = [
{ icon: 'award', text: 'Rank candidates', prompt: 'Rank the top candidates for the Senior Backend Engineer role.' },
{ icon: 'users', text: 'Compare candidates', prompt: 'Compare the top 3 candidates in the Interview stage.' },
{ icon: 'edit', text: 'Generate job description', prompt: 'Write a job description for a Staff Frontend Engineer.' },
{ icon: 'message', text: 'Interview questions', prompt: 'Generate technical interview questions for a Data Scientist.' },
{ icon: 'file', text: 'Summarize resume', prompt: "Summarize the resume of the last applicant." },
{ icon: 'mail', text: 'Generate email', prompt: 'Draft an interview invitation email for a candidate.' },
{ icon: 'file', text: 'Write offer letter', prompt: 'Write an offer letter for a Product Manager at $160k.' },
{ icon: 'star', text: 'Suggest candidate', prompt: 'Suggest the best candidate for the open Design role.' },
{ icon: 'target', text: 'Skill gap analysis', prompt: 'What skills are missing from my Engineering pipeline?' },
{ icon: 'trending-up', text: 'Hiring recommendation', prompt: 'Give me a hiring recommendation for this week.' },
{ icon: 'briefcase', text: 'Pipeline analysis', prompt: 'Analyze the health of my current pipeline.' },
{ icon: 'award', text: 'Recruiter productivity', prompt: 'How productive is my recruiting team this month?' }
];
// ---------- ATS matching helper ----------
function atsRecommendationClass(rec) {
return rec === 'Strong Match' ? 'b-green' : rec === 'Potential Match' ? 'b-amber' : 'b-red';
}
// runtime state buckets
const recentlyViewed = [];
const favorites = { candidates: [], jobs: [] };
// ---------- Expose ----------
window.DB = {
departments, businessUnits, locations, empTypes, grades, jobStatuses, educationLevels, stages, sources, skillsPool, benefitsPool,
recruiters, managers, jobs, candidates, interviews, assessments, offers,
activity, notifications, messages, analytics, kpis, roles, users,
interviewTypes, meetingTypes, companies,
// enterprise
sourceMeta, inboxSources, processingStatuses, resumeStatuses, inbox, emails,
publishPlatforms, publishings, tasks, savedSearches, evalTemplates,
rbacModules, permTypes, rbacRoles, aiModules, aiPrompts,
recentlyViewed, favorites,
// helpers
fmtDate, fmtShort, relTime, initials, avatarColor, int, pick, atsRecommendationClass,
money: n => '$' + n.toLocaleString('en-US'),
moneyK: n => '$' + Math.round(n / 1000) + 'k',
getJob: id => jobs.find(j => j.id === id),
getCandidate: id => candidates.find(c => c.id === id),
getManager: id => managers.find(m => m.id === id),
getRecruiter: id => recruiters.find(r => r.id === id),
getRecruiterByName: n => recruiters.find(r => r.name === n)
};
})();