forms done
parent
1eea85f47e
commit
80a3b290b9
|
|
@ -93,3 +93,4 @@ frontend/dist/index.html
|
||||||
frontend/dist/**
|
frontend/dist/**
|
||||||
nginx.conf
|
nginx.conf
|
||||||
smoke.test.mjs
|
smoke.test.mjs
|
||||||
|
Annex**
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
-- 037_requisition_period_jd_entity.sql
|
||||||
|
-- Annexure A revision 2 added three fields the original 020 table never had:
|
||||||
|
-- "If not permanent, specify the period From/To" on the position block, the
|
||||||
|
-- mandatory "JD Available Yes/No" flag (distinct from the free-text
|
||||||
|
-- job_description), and "Entity" alongside Employee Name/Department in the
|
||||||
|
-- internal-recommendation block. Matches Requisition in
|
||||||
|
-- backend/candidate_forms/models.py. Applied at startup by
|
||||||
|
-- alembic_setup.run_manual_sql() — needed because prod boots with
|
||||||
|
-- DB_AUTOGENERATE=false and never autogenerates new columns.
|
||||||
|
|
||||||
|
ALTER TABLE app.requisitions
|
||||||
|
ADD COLUMN IF NOT EXISTS period_from date,
|
||||||
|
ADD COLUMN IF NOT EXISTS period_to date,
|
||||||
|
ADD COLUMN IF NOT EXISTS jd_available boolean,
|
||||||
|
ADD COLUMN IF NOT EXISTS entity varchar;
|
||||||
|
|
@ -5,8 +5,8 @@ import { request } from '../lib/apiClient'
|
||||||
|
|
||||||
The digitized hiring forms: Annexure A (Employee Requisition), and the two
|
The digitized hiring forms: Annexure A (Employee Requisition), and the two
|
||||||
halves of Annexure E — Interview Analysis (technical + behavioral) and
|
halves of Annexure E — Interview Analysis (technical + behavioral) and
|
||||||
Cultural Fit. Dual-key like assessments: exactly one of inbox_id /
|
HR Evaluation (form_type stays "cultural_fit"). Dual-key like assessments:
|
||||||
manual_upload_candidate_id.
|
exactly one of inbox_id / manual_upload_candidate_id.
|
||||||
|
|
||||||
Permissioned with the interviews module tags (interviews.view to read,
|
Permissioned with the interviews module tags (interviews.view to read,
|
||||||
interviews.create to fill, interviews.edit to amend). Creating is
|
interviews.create to fill, interviews.edit to amend). Creating is
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
/* The Forms tab of the candidate profile modal — Interview Analysis + Cultural
|
/* The Forms tab of the candidate profile modal — Interview Analysis + HR
|
||||||
Fit (the two halves of Annexure E), and the Offer (Annexure J fields on the
|
Evaluation (the two halves of Annexure E; form_type stays "cultural_fit" —
|
||||||
offers table). Employee Requisition (Annexure A) lives on the Requisitions
|
only its title/criteria grew to the fuller HR Evaluation section in
|
||||||
screen, not on a candidate.
|
revision 2), and the Offer (Annexure J fields on the offers table).
|
||||||
|
Employee Requisition (Annexure A) lives on the Requisitions screen, not on
|
||||||
|
a candidate.
|
||||||
|
|
||||||
Field and criterion labels are rendered from GET /forms/definitions — the
|
Field and criterion labels are rendered from GET /forms/definitions — the
|
||||||
backend is the single authority for the paper forms' exact wording. The
|
backend is the single authority for the paper forms' exact wording. The
|
||||||
|
|
@ -161,7 +163,7 @@ export default function CandidateFormsTab({ userId, live }) {
|
||||||
}
|
}
|
||||||
const segTabs = [
|
const segTabs = [
|
||||||
{ key: 'interview_analysis', label: 'Interview Analysis' },
|
{ key: 'interview_analysis', label: 'Interview Analysis' },
|
||||||
{ key: 'cultural_fit', label: 'Cultural Fit' },
|
{ key: 'cultural_fit', label: 'HR Evaluation' },
|
||||||
...(!isManager ? [{ key: 'offer', label: 'Offer' }] : []),
|
...(!isManager ? [{ key: 'offer', label: 'Offer' }] : []),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -233,7 +235,7 @@ function SummaryStrip({ summary, evalCount }) {
|
||||||
<div className="hf-summary" style={{ marginBottom: 6 }}>
|
<div className="hf-summary" style={{ marginBottom: 6 }}>
|
||||||
<ScoreTile label="Technical" value={summary.technical_avg} />
|
<ScoreTile label="Technical" value={summary.technical_avg} />
|
||||||
<ScoreTile label="Behavioral" value={summary.behavioral_avg} />
|
<ScoreTile label="Behavioral" value={summary.behavioral_avg} />
|
||||||
<ScoreTile label="Cultural Fit" value={summary.cultural_avg} />
|
<ScoreTile label="HR Evaluation" value={summary.cultural_avg} />
|
||||||
<ScoreTile
|
<ScoreTile
|
||||||
label="Combined Overall"
|
label="Combined Overall"
|
||||||
value={summary.combined_overall}
|
value={summary.combined_overall}
|
||||||
|
|
@ -335,7 +337,7 @@ function RatingTable({ section, defs, ratings, onRate }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
Interview Analysis / Cultural Fit — data-driven off the definition's
|
Interview Analysis / HR Evaluation — data-driven off the definition's
|
||||||
sections; both types share this component. */
|
sections; both types share this component. */
|
||||||
|
|
||||||
function RatedEvaluationForm({ formType, def, defs, rows, userId, link, live, canCreate, canEdit }) {
|
function RatedEvaluationForm({ formType, def, defs, rows, userId, link, live, canCreate, canEdit }) {
|
||||||
|
|
|
||||||
|
|
@ -389,7 +389,7 @@ export default function CandidateProfile({
|
||||||
candidate={live || c} stage={stageLabel} nextStage={nextStage} onTab={changeTab} onAction={openAction}
|
candidate={live || c} stage={stageLabel} nextStage={nextStage} onTab={changeTab} onAction={openAction}
|
||||||
rating={rating} ratingPending={setRating.isPending} onRating={(n) => setRating.mutate(n)}
|
rating={rating} ratingPending={setRating.isPending} onRating={(n) => setRating.mutate(n)}
|
||||||
atsScore={atsScore} recommendation={recommendation}
|
atsScore={atsScore} recommendation={recommendation}
|
||||||
atsAction={canScoreAts && can('candidates.create') ? <button className="btn btn-secondary btn-sm" disabled={scoreAts.isPending} onClick={() => scoreAts.mutate()}><Icon name="sparkles" />{scoreAts.isPending ? 'Scoring…' : 'Score with ATS'}</button> : null}
|
atsAction={canRerunAts && can('candidates.create') ? <button className="btn btn-secondary btn-sm" disabled={rerunAts.isPending} onClick={() => rerunAts.mutate()}><Icon name="sparkles" />{rerunAts.isPending ? 'Scoring…' : 'Score with ATS'}</button> : null}
|
||||||
/> : live ? (
|
/> : live ? (
|
||||||
<>
|
<>
|
||||||
<PreviousApplications row={live} />
|
<PreviousApplications row={live} />
|
||||||
|
|
|
||||||
|
|
@ -275,6 +275,9 @@ function blankForm() {
|
||||||
date_needed: '',
|
date_needed: '',
|
||||||
type: '',
|
type: '',
|
||||||
job_description: '',
|
job_description: '',
|
||||||
|
period_from: '',
|
||||||
|
period_to: '',
|
||||||
|
jd_available: false,
|
||||||
to_replace: '',
|
to_replace: '',
|
||||||
grade: '',
|
grade: '',
|
||||||
recruitment_title: '',
|
recruitment_title: '',
|
||||||
|
|
@ -284,6 +287,7 @@ function blankForm() {
|
||||||
recommended_grade: '',
|
recommended_grade: '',
|
||||||
employee_name: '',
|
employee_name: '',
|
||||||
employee_department: '',
|
employee_department: '',
|
||||||
|
entity: '',
|
||||||
initiated_by: '',
|
initiated_by: '',
|
||||||
initiated_date: '',
|
initiated_date: '',
|
||||||
recommended_by: '',
|
recommended_by: '',
|
||||||
|
|
@ -310,6 +314,9 @@ function fromRow(row) {
|
||||||
date_needed: toDateInput(pos.date_needed),
|
date_needed: toDateInput(pos.date_needed),
|
||||||
type: pos.type || '',
|
type: pos.type || '',
|
||||||
job_description: pos.job_description || '',
|
job_description: pos.job_description || '',
|
||||||
|
period_from: toDateInput(pos.period_from),
|
||||||
|
period_to: toDateInput(pos.period_to),
|
||||||
|
jd_available: pos.jd_available === true,
|
||||||
to_replace: rep.to_replace || '',
|
to_replace: rep.to_replace || '',
|
||||||
grade: rep.grade || '',
|
grade: rep.grade || '',
|
||||||
recruitment_title: rep.title || '',
|
recruitment_title: rep.title || '',
|
||||||
|
|
@ -319,6 +326,7 @@ function fromRow(row) {
|
||||||
recommended_grade: rep.recommended_grade || '',
|
recommended_grade: rep.recommended_grade || '',
|
||||||
employee_name: ref.employee_name || '',
|
employee_name: ref.employee_name || '',
|
||||||
employee_department: ref.employee_department || '',
|
employee_department: ref.employee_department || '',
|
||||||
|
entity: ref.entity || '',
|
||||||
initiated_by: row.initiated_by || '',
|
initiated_by: row.initiated_by || '',
|
||||||
initiated_date: toDateInput(row.initiated_date),
|
initiated_date: toDateInput(row.initiated_date),
|
||||||
recommended_by: row.recommended_by || '',
|
recommended_by: row.recommended_by || '',
|
||||||
|
|
@ -330,7 +338,7 @@ function fromRow(row) {
|
||||||
approved_by_svp: row.approved_by_svp === true,
|
approved_by_svp: row.approved_by_svp === true,
|
||||||
approved_by_date_svp: toDateInput(row.approved_by_date_svp),
|
approved_by_date_svp: toDateInput(row.approved_by_date_svp),
|
||||||
is_replacement: hasAny(rep, ['to_replace', 'grade', 'title', 'date_separated', 'justification', 'budget', 'recommended_grade']),
|
is_replacement: hasAny(rep, ['to_replace', 'grade', 'title', 'date_separated', 'justification', 'budget', 'recommended_grade']),
|
||||||
is_referral: hasAny(ref, ['employee_name', 'employee_department']),
|
is_referral: hasAny(ref, ['employee_name', 'employee_department', 'entity']),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -343,6 +351,9 @@ function toPayload(f) {
|
||||||
date_needed: emptyToNull(f.date_needed),
|
date_needed: emptyToNull(f.date_needed),
|
||||||
type: emptyToNull(f.type),
|
type: emptyToNull(f.type),
|
||||||
job_description: emptyToNull(f.job_description),
|
job_description: emptyToNull(f.job_description),
|
||||||
|
period_from: emptyToNull(f.period_from),
|
||||||
|
period_to: emptyToNull(f.period_to),
|
||||||
|
jd_available: f.jd_available,
|
||||||
},
|
},
|
||||||
initiated_by: emptyToNull(f.initiated_by),
|
initiated_by: emptyToNull(f.initiated_by),
|
||||||
initiated_date: emptyToNull(f.initiated_date),
|
initiated_date: emptyToNull(f.initiated_date),
|
||||||
|
|
@ -377,10 +388,12 @@ function toPayload(f) {
|
||||||
? {
|
? {
|
||||||
employee_name: emptyToNull(f.employee_name),
|
employee_name: emptyToNull(f.employee_name),
|
||||||
employee_department: emptyToNull(f.employee_department),
|
employee_department: emptyToNull(f.employee_department),
|
||||||
|
entity: emptyToNull(f.entity),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
employee_name: null,
|
employee_name: null,
|
||||||
employee_department: null,
|
employee_department: null,
|
||||||
|
entity: null,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return body
|
return body
|
||||||
|
|
@ -468,6 +481,22 @@ function RequisitionEditor({ row, allowed, onClose, onSaved, toast }) {
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form-field">
|
||||||
|
<label>If not permanent, period from</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={fields.period_from}
|
||||||
|
onChange={(e) => set('period_from', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-field">
|
||||||
|
<label>Period to</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={fields.period_to}
|
||||||
|
onChange={(e) => set('period_to', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="form-field col-span-2">
|
<div className="form-field col-span-2">
|
||||||
<label>Job description</label>
|
<label>Job description</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
|
@ -477,6 +506,16 @@ function RequisitionEditor({ row, allowed, onClose, onSaved, toast }) {
|
||||||
onChange={(e) => set('job_description', e.target.value)}
|
onChange={(e) => set('job_description', e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form-field">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={fields.jd_available}
|
||||||
|
onChange={(e) => set('jd_available', e.target.checked)}
|
||||||
|
/>
|
||||||
|
{' '}JD available <span className="hf-note" style={{ margin: 0 }}>(mandatory — sourcing will not start without it)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -564,6 +603,10 @@ function RequisitionEditor({ row, allowed, onClose, onSaved, toast }) {
|
||||||
onChange={(e) => set('employee_department', e.target.value)}
|
onChange={(e) => set('employee_department', e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form-field">
|
||||||
|
<label>Entity</label>
|
||||||
|
<input value={fields.entity} onChange={(e) => set('entity', e.target.value)} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue