/** * Job Description Management (manage/jds/*, jd_management.py). * * Persona: the existing Recruiter (jds.create) creates/uploads/AI-generates/ * edits/submits JDs; the existing Recruitment Administrator and Admin * (jds.approve) approve, reject, publish, and archive them -- the same * propose/approve split already established for bulk candidate decisions * (screening.propose/screening.approve), not a new pattern invented for * this feature. Hiring Manager gets read-only jds.read for context. * * PUBLISH is the only action that makes a JD live for candidate matching -- * it syncs into jd_folder/{jd_id}.txt, which the existing, already- * verified resume-scoring pipeline (jd_manager.py) reads unchanged. Draft/ * pending/rejected/archived JDs never reach that file, so they never * affect scoring. */ var JD_STATUS_BADGE = { draft: "badge-neutral", pending_approval: "badge-neutral", approved: "badge-good", published: "badge-good", rejected: "badge-bad", archived: "badge-neutral", }; var JD_STATUS_LABEL = { draft: "Draft", pending_approval: "Pending Approval", approved: "Approved", published: "Published", rejected: "Rejected", archived: "Archived", }; var JD_EMPTY_FORM = { title: "", description: "", responsibilities: "", required_qualifications: "", preferred_qualifications: "", required_skills: "", preferred_skills: "", required_experience: "", seniority_level: "", employment_type: "", location: "", work_arrangement: "", department: "", openings_count: "", salary_range: "", hiring_contact: "", application_deadline: "", additional_requirements: "", }; function _jdLinesToList(text) { return (text || "").split("\n").map(function (l) { return l.trim(); }).filter(Boolean); } function _jdListToLines(list) { return (list || []).join("\n"); } function JDManagement(props) { var api = props.api; var permissions = props.permissions || []; var canCreate = permissions.indexOf("jds.create") !== -1; var canApprove = permissions.indexOf("jds.approve") !== -1; var tabState = React.useState("board"); // "board" | "create" | "detail" var tab = tabState[0]; var setTab = tabState[1]; var selectedIdState = React.useState(null); var selectedId = selectedIdState[0]; var setSelectedId = selectedIdState[1]; function openDetail(jdId) { setSelectedId(jdId); setTab("detail"); } function backToBoard() { setSelectedId(null); setTab("board"); } return (
Create, upload, AI-generate, and manage job descriptions. Publishing a JD is the only action that makes it live for candidate matching.
No job descriptions found.
} {visible.length > 0 && (| Title | Status | Department | Created by | Updated | |
|---|---|---|---|---|---|
| {jd.title} | {JD_STATUS_LABEL[jd.status] || jd.status} | {jd.department || "—"} | {jd.created_by} | {jd.updated_at || jd.created_at} |
Real, billable LLM call -- fills the form below as a draft for you to review and edit.
Generated by {aiMeta.provider} / {aiMeta.model} at {new Date(aiMeta.generated_at).toLocaleString()} -- still a draft, edit freely below.
}PDF only. Extracted fields populate the form below for you to review and edit -- nothing is saved until you submit this form.
{uploadBusy &&Extracting…
} {uploadResult && !uploadResult.extraction_succeeded &&Loading…
; if (data.error) return ({JD_STATUS_LABEL[jd.status] || jd.status} {jd.source && {jd.source}}
Created by {jd.created_by} on {new Date(jd.created_at).toLocaleString()}
{jd.published_at &&
{jd.description}
} {jd.department &&Department: {jd.department}
} {jd.location &&Location: {jd.location}{jd.work_arrangement ? " (" + jd.work_arrangement + ")" : ""}
} {jd.employment_type &&Employment type: {jd.employment_type}
} {jd.seniority_level &&Seniority: {jd.seniority_level}
} {jd.required_experience &&Required experience: {jd.required_experience}
} {jd.salary_range &&Salary range: {jd.salary_range}
} {jd.hiring_contact &&Hiring contact: {jd.hiring_contact}
} {jd.application_deadline &&Application deadline: {jd.application_deadline}
} {jd.responsibilities && jd.responsibilities.length > 0 && ({jd.additional_requirements}
This JD can't be edited in its current state ({JD_STATUS_LABEL[jd.status]}).
)}No audit events recorded yet.
} {data.audit_log.length > 0 && (| Time | Action | Actor |
|---|---|---|
| {new Date(e.logged_at).toLocaleString()} | {e.action} | {e.actor} |