HR-ATS-Portal/js/jobs.js

253 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

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

/* ============================================================
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 => `<span class="cell-mono">${j.id}</span>` },
{ key: 'title', label: 'Job Title', sortable: true, render: j => `<div class="cell-primary">${j.title}</div><div class="cell-sub">${j.businessUnit} · ${j.grade}</div>` },
{ key: 'department', label: 'Department', sortable: true },
{ key: 'manager', label: 'Hiring Manager', sortable: true, render: j => `<div class="user-cell">${UI.avatar(j.manager)}<span>${j.manager}</span></div>` },
{ key: 'location', label: 'Location', sortable: true, render: j => `<span class="text-muted">${j.location}</span>` },
{ key: 'type', label: 'Type', render: j => UI.badge(j.type, 'b-gray') },
{ key: 'applications', label: 'Apps', sortable: true, align: 'center', render: j => `<b>${j.applications}</b>` },
{ 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 => `<span class="text-muted">${DB.fmtShort(j.created)}</span>` },
{ key: '_a', label: 'Actions', align: 'right', render: j => `
<div class="row-actions">
<button class="act-btn" data-tip="View" onclick="Jobs.view('${j.id}')">${UI.icon('eye')}</button>
<button class="act-btn" data-tip="Publish" onclick="JobBoard.publishFlow('${j.id}')">${UI.icon('send')}</button>
<button class="act-btn" data-tip="Edit" onclick="Jobs.openEdit('${j.id}')">${UI.icon('edit')}</button>
<button class="act-btn danger" data-tip="Delete" onclick="Jobs.confirmDelete('${j.id}')">${UI.icon('trash')}</button>
</div>` }
]
});
const deptOpts = ['<option value="">All Departments</option>'].concat(DB.departments.map(d => `<option>${d}</option>`)).join('');
const statusOpts = ['<option value="">All Status</option>'].concat(DB.jobStatuses.map(s => `<option>${s}</option>`)).join('');
const typeOpts = ['<option value="">All Types</option>'].concat(DB.empTypes.map(t => `<option>${t}</option>`)).join('');
const html = `
<div class="page">
<div class="page-head">
<div><h1 class="page-title">Jobs</h1><p class="page-sub">${DB.jobs.length} requisitions · ${DB.kpis.openJobs} currently open</p></div>
<div class="page-head-actions">
<button class="btn btn-secondary" onclick="UI.toast('Jobs exported to CSV','success')">${UI.icon('download')} Export</button>
<button class="btn btn-primary" onclick="Jobs.openCreate()">${UI.icon('plus')} Create Job</button>
</div>
</div>
<div class="card">
<div class="card-body" style="padding-bottom:0">
<div class="toolbar">
<div class="toolbar-search">${UI.icon('search')}<input id="jobSearch" placeholder="Search jobs, IDs, managers…"/></div>
<select class="select" id="jobDept">${deptOpts}</select>
<select class="select" id="jobStatus">${statusOpts}</select>
<select class="select" id="jobType">${typeOpts}</select>
</div>
</div>
${table.html}
</div>
</div>`;
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 = `
<div class="flex items-center gap-16 mb-18">
<span class="kpi-icn i-indigo" style="width:52px;height:52px;border-radius:14px">${UI.icon('briefcase')}</span>
<div>
<div style="font-size:19px;font-weight:700">${j.title}</div>
<div class="text-muted">${j.id} · ${j.department} · ${j.businessUnit}</div>
</div>
<div style="margin-left:auto">${UI.badge(j.status)}</div>
</div>
<div class="info-grid mb-18">
<div class="info-item"><div class="il">Hiring Manager</div><div class="iv">${j.manager}</div></div>
<div class="info-item"><div class="il">Assigned Recruiter</div><div class="iv flex items-center gap-8">${j.recruiter} ${(() => { const r = DB.getRecruiterByName(j.recruiter); return r ? `<span class="badge ${r.workload > 80 ? 'b-red' : r.workload > 60 ? 'b-amber' : 'b-green'} badge-plain" style="font-size:10px">${r.workload}% load</span>` : ''; })()} <button class="link-btn" onclick="Jobs.reassign('${j.id}')">Reassign</button></div></div>
<div class="info-item"><div class="il">Location</div><div class="iv">${j.location}</div></div>
<div class="info-item"><div class="il">Employment Type</div><div class="iv">${j.type}</div></div>
<div class="info-item"><div class="il">Grade</div><div class="iv">${j.grade}</div></div>
<div class="info-item"><div class="il">Vacancies</div><div class="iv">${j.vacancies}</div></div>
<div class="info-item"><div class="il">Salary Range</div><div class="iv">${DB.moneyK(j.salaryMin)} ${DB.moneyK(j.salaryMax)}</div></div>
<div class="info-item"><div class="il">Experience</div><div class="iv">${j.experience}</div></div>
<div class="info-item"><div class="il">Education</div><div class="iv">${j.education}</div></div>
<div class="info-item"><div class="il">Deadline</div><div class="iv">${DB.fmtDate(j.deadline)}</div></div>
</div>
<div class="divider"></div>
<div style="margin-bottom:16px"><div class="il" style="font-size:12px;color:var(--text-3);font-weight:600;text-transform:uppercase;margin-bottom:6px">Description</div><p style="color:var(--text-2)">${j.description}</p></div>
<div style="margin-bottom:16px"><div class="il" style="font-size:12px;color:var(--text-3);font-weight:600;text-transform:uppercase;margin-bottom:6px">Key Responsibilities</div>
<ul style="padding-left:18px;color:var(--text-2)">${j.responsibilities.map(r => `<li>${r}</li>`).join('')}</ul></div>
<div style="margin-bottom:16px"><div class="il" style="font-size:12px;color:var(--text-3);font-weight:600;text-transform:uppercase;margin-bottom:6px">Required Skills</div>
<div class="k-tags">${j.skills.map(s => `<span class="tag">${s}</span>`).join('')}</div></div>
<div><div class="il" style="font-size:12px;color:var(--text-3);font-weight:600;text-transform:uppercase;margin-bottom:6px">Benefits</div>
<div class="k-tags">${j.benefits.map(s => `<span class="tag">${s}</span>`).join('')}</div></div>
<div class="divider"></div>
<div class="flex items-center gap-12"><span class="text-muted text-sm">Hiring progress</span><div style="flex:1">${UI.pbar(j.progress)}</div><span class="fw-600">${j.progress}%</span></div>`;
const footer = `<button class="btn btn-secondary" onclick="UI.closeModal()">Close</button>
<button class="btn btn-secondary" onclick="UI.closeModal();JobBoard.publishFlow('${j.id}')">${UI.icon('send')} Publish</button>
<button class="btn btn-primary" onclick="UI.closeModal();Jobs.openEdit('${j.id}')">${UI.icon('edit')} Edit Job</button>`;
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 => `<option ${r.name === j.recruiter ? 'selected' : ''}>${r.name}${r.workload}% load · ${r.openReqs} reqs</option>`).join('');
UI.modal({
title: 'Reassign Recruiter', subtitle: j.title,
body: `<div class="form-field"><label>Assigned Recruiter</label><select id="reassignRec">${DB.recruiters.map(r => `<option value="${r.name}" ${r.name === j.recruiter ? 'selected' : ''}>${r.name}${r.workload}% load</option>`).join('')}</select></div>
<p class="text-muted text-sm" style="margin-top:10px">Workload is recalculated automatically across the recruiter's assigned requisitions.</p>`,
footer: `<button class="btn btn-secondary" onclick="UI.closeModal()">Cancel</button><button class="btn btn-primary" onclick="Jobs._doReassign('${id}')">Reassign</button>`
});
};
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 => `<option ${o === sel ? 'selected' : ''}>${o}</option>`).join('');
const body = `
<form id="jobForm" novalidate>
<div class="form-grid">
<div class="form-field col-span-2">
<label>Job Title <span class="req">*</span></label>
<input name="title" value="${job ? job.title : ''}" placeholder="e.g. Senior Product Designer"/>
<span class="field-error">Job title is required</span>
</div>
<div class="form-field"><label>Department <span class="req">*</span></label>
<select name="department">${opt(DB.departments, job && job.department)}</select></div>
<div class="form-field"><label>Business Unit</label><select name="businessUnit">${opt(DB.businessUnits, job && job.businessUnit)}</select></div>
<div class="form-field"><label>Grade</label><select name="grade">${opt(DB.grades, job && job.grade)}</select></div>
<div class="form-field"><label>Employment Type</label><select name="type">${opt(DB.empTypes, job && job.type)}</select></div>
<div class="form-field"><label>Hiring Manager <span class="req">*</span></label>
<select name="manager">${opt(DB.managers.map(m => m.name), job && job.manager)}</select></div>
<div class="form-field"><label>Recruiter <span class="req">*</span></label>
<select name="recruiter">${opt(DB.recruiters.map(r => r.name), job && job.recruiter)}</select></div>
<div class="form-field"><label>Salary Min ($) <span class="req">*</span></label>
<input name="salaryMin" type="number" value="${job ? job.salaryMin : ''}" placeholder="90000"/>
<span class="field-error">Enter a valid amount</span></div>
<div class="form-field"><label>Salary Max ($)</label><input name="salaryMax" type="number" value="${job ? job.salaryMax : ''}" placeholder="130000"/></div>
<div class="form-field"><label>Experience</label><input name="experience" value="${job ? job.experience : ''}" placeholder="5+ years"/></div>
<div class="form-field"><label>Education</label><select name="education">${opt(DB.educationLevels, job && job.education)}</select></div>
<div class="form-field"><label>Location <span class="req">*</span></label><select name="location">${opt(DB.locations, job && job.location)}</select></div>
<div class="form-field"><label>Vacancies</label><input name="vacancies" type="number" value="${job ? job.vacancies : 1}" min="1"/></div>
<div class="form-field col-span-2"><label>Job Description <span class="req">*</span></label>
<textarea name="description" placeholder="Describe the role…">${job ? job.description : ''}</textarea>
<span class="field-error">Description is required</span></div>
<div class="form-field col-span-2"><label>Responsibilities</label>
<textarea name="responsibilities" placeholder="One per line…">${job ? job.responsibilities.join('\n') : ''}</textarea></div>
<div class="form-field col-span-2"><label>Required Skills</label>
<input name="skills" value="${job ? job.skills.join(', ') : ''}" placeholder="React, TypeScript, System Design"/></div>
<div class="form-field col-span-2"><label>Benefits</label>
<input name="benefits" value="${job ? job.benefits.join(', ') : ''}" placeholder="Equity, 401(k), Unlimited PTO"/></div>
<div class="form-field"><label>Deadline</label><input name="deadline" type="date"/></div>
<div class="form-field"><label>Status</label><select name="status">${opt(DB.jobStatuses, job ? job.status : 'Open')}</select></div>
</div>
</form>`;
const footer = `<button class="btn btn-secondary" onclick="UI.closeModal()">Cancel</button>
<button class="btn btn-primary" onclick="Jobs._save(${isEdit ? `'${job.id}'` : 'null'})">${UI.icon('check')} ${isEdit ? 'Save Changes' : 'Create Job'}</button>`;
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 = `<div class="flex gap-16 items-center">
<span class="kpi-icn i-red" style="width:48px;height:48px;border-radius:12px;flex-shrink:0">${UI.icon('trash')}</span>
<div><p style="font-weight:600;font-size:15px">Delete "${j.title}"?</p>
<p class="text-muted" style="margin-top:4px">This will permanently remove requisition ${j.id} and its ${j.applications} applications. This action cannot be undone.</p></div></div>`;
const footer = `<button class="btn btn-secondary" onclick="UI.closeModal()">Cancel</button>
<button class="btn btn-danger" onclick="Jobs._delete('${id}')">${UI.icon('trash')} Delete Job</button>`;
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();
};