forms done
parent
1eea85f47e
commit
80a3b290b9
|
|
@ -93,3 +93,4 @@ frontend/dist/index.html
|
|||
frontend/dist/**
|
||||
nginx.conf
|
||||
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
|
||||
halves of Annexure E — Interview Analysis (technical + behavioral) and
|
||||
Cultural Fit. Dual-key like assessments: exactly one of inbox_id /
|
||||
manual_upload_candidate_id.
|
||||
HR Evaluation (form_type stays "cultural_fit"). Dual-key like assessments:
|
||||
exactly one of inbox_id / manual_upload_candidate_id.
|
||||
|
||||
Permissioned with the interviews module tags (interviews.view to read,
|
||||
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
|
||||
Fit (the two halves of Annexure E), and the Offer (Annexure J fields on the
|
||||
offers table). Employee Requisition (Annexure A) lives on the Requisitions
|
||||
screen, not on a candidate.
|
||||
/* The Forms tab of the candidate profile modal — Interview Analysis + HR
|
||||
Evaluation (the two halves of Annexure E; form_type stays "cultural_fit" —
|
||||
only its title/criteria grew to the fuller HR Evaluation section in
|
||||
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
|
||||
backend is the single authority for the paper forms' exact wording. The
|
||||
|
|
@ -161,7 +163,7 @@ export default function CandidateFormsTab({ userId, live }) {
|
|||
}
|
||||
const segTabs = [
|
||||
{ key: 'interview_analysis', label: 'Interview Analysis' },
|
||||
{ key: 'cultural_fit', label: 'Cultural Fit' },
|
||||
{ key: 'cultural_fit', label: 'HR Evaluation' },
|
||||
...(!isManager ? [{ key: 'offer', label: 'Offer' }] : []),
|
||||
]
|
||||
|
||||
|
|
@ -233,7 +235,7 @@ function SummaryStrip({ summary, evalCount }) {
|
|||
<div className="hf-summary" style={{ marginBottom: 6 }}>
|
||||
<ScoreTile label="Technical" value={summary.technical_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
|
||||
label="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. */
|
||||
|
||||
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}
|
||||
rating={rating} ratingPending={setRating.isPending} onRating={(n) => setRating.mutate(n)}
|
||||
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 ? (
|
||||
<>
|
||||
<PreviousApplications row={live} />
|
||||
|
|
|
|||
|
|
@ -275,6 +275,9 @@ function blankForm() {
|
|||
date_needed: '',
|
||||
type: '',
|
||||
job_description: '',
|
||||
period_from: '',
|
||||
period_to: '',
|
||||
jd_available: false,
|
||||
to_replace: '',
|
||||
grade: '',
|
||||
recruitment_title: '',
|
||||
|
|
@ -284,6 +287,7 @@ function blankForm() {
|
|||
recommended_grade: '',
|
||||
employee_name: '',
|
||||
employee_department: '',
|
||||
entity: '',
|
||||
initiated_by: '',
|
||||
initiated_date: '',
|
||||
recommended_by: '',
|
||||
|
|
@ -310,6 +314,9 @@ function fromRow(row) {
|
|||
date_needed: toDateInput(pos.date_needed),
|
||||
type: pos.type || '',
|
||||
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 || '',
|
||||
grade: rep.grade || '',
|
||||
recruitment_title: rep.title || '',
|
||||
|
|
@ -319,6 +326,7 @@ function fromRow(row) {
|
|||
recommended_grade: rep.recommended_grade || '',
|
||||
employee_name: ref.employee_name || '',
|
||||
employee_department: ref.employee_department || '',
|
||||
entity: ref.entity || '',
|
||||
initiated_by: row.initiated_by || '',
|
||||
initiated_date: toDateInput(row.initiated_date),
|
||||
recommended_by: row.recommended_by || '',
|
||||
|
|
@ -330,7 +338,7 @@ function fromRow(row) {
|
|||
approved_by_svp: row.approved_by_svp === true,
|
||||
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_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),
|
||||
type: emptyToNull(f.type),
|
||||
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_date: emptyToNull(f.initiated_date),
|
||||
|
|
@ -377,10 +388,12 @@ function toPayload(f) {
|
|||
? {
|
||||
employee_name: emptyToNull(f.employee_name),
|
||||
employee_department: emptyToNull(f.employee_department),
|
||||
entity: emptyToNull(f.entity),
|
||||
}
|
||||
: {
|
||||
employee_name: null,
|
||||
employee_department: null,
|
||||
entity: null,
|
||||
},
|
||||
}
|
||||
return body
|
||||
|
|
@ -468,6 +481,22 @@ function RequisitionEditor({ row, allowed, onClose, onSaved, toast }) {
|
|||
))}
|
||||
</select>
|
||||
</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">
|
||||
<label>Job description</label>
|
||||
<textarea
|
||||
|
|
@ -477,6 +506,16 @@ function RequisitionEditor({ row, allowed, onClose, onSaved, toast }) {
|
|||
onChange={(e) => set('job_description', e.target.value)}
|
||||
/>
|
||||
</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>
|
||||
|
||||
|
|
@ -564,6 +603,10 @@ function RequisitionEditor({ row, allowed, onClose, onSaved, toast }) {
|
|||
onChange={(e) => set('employee_department', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label>Entity</label>
|
||||
<input value={fields.entity} onChange={(e) => set('entity', e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue