513 lines
33 KiB
JavaScript
513 lines
33 KiB
JavaScript
/* ============================================================
|
|
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) => `<b>${n}</b> applied for <b>${j}</b>` },
|
|
{ icon: 'calendar', color: 'i-blue', text: (n, j) => `Interview scheduled with <b>${n}</b> for <b>${j}</b>` },
|
|
{ icon: 'check', color: 'i-teal', text: (n, j) => `<b>${n}</b> moved to <b>Offer</b> stage` },
|
|
{ icon: 'file', color: 'i-purple', text: (n, j) => `Offer sent to <b>${n}</b> for <b>${j}</b>` },
|
|
{ icon: 'star', color: 'i-amber', text: (n, j) => `<b>${n}</b> completed an assessment` },
|
|
{ icon: 'x', color: 'i-red', text: (n, j) => `<b>${n}</b> was rejected for <b>${j}</b>` }
|
|
];
|
|
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)
|
|
};
|
|
})();
|