253 lines
17 KiB
JavaScript
253 lines
17 KiB
JavaScript
/* ============================================================
|
||
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();
|
||
};
|