/**
* Candidate Self-Service Portal.
*
* "Check status" covers the one real read endpoint for candidates
* (GET /api/candidates/by-email/{email}) in full.
*
* "Submit a new application" is now a real, working form — POST
* /api/applications (azure-functions/api_blueprint.py), which uploads the
* resume to Blob Storage and lets the existing blob-triggered function
* (Phase 3) pick it up and run the normal ResumeParser.py pipeline. In
* mock mode (the default here, see frontend/README.md) this validates
* input the same way but doesn't actually upload or process anything —
* switch config.js to mode: "real" against a deployed API to exercise it
* for real. jd_id/cover_note are collected and sent (visible in the
* audit log) but NOT acted on by the scoring pipeline yet — see the
* endpoint's docstring for why; not faked as if they influenced ranking.
*/
function CandidatePortal(props) {
var api = props.api;
var state = React.useState({ email: "", result: null, error: null, loading: false, checked: false });
var data = state[0];
var setData = state[1];
function checkStatus(e) {
e.preventDefault();
if (!data.email.trim()) return;
setData(function (d) { return Object.assign({}, d, { loading: true, error: null, checked: false }); });
api.getCandidateByEmail(data.email.trim())
.then(function (result) {
setData(function (d) { return Object.assign({}, d, { loading: false, result: result, checked: true }); });
})
.catch(function (err) {
setData(function (d) { return Object.assign({}, d, { loading: false, result: null, error: err.message, checked: true }); });
});
}
return (
Candidate Portal
Check your application status
{data.checked && data.result && (
Hi {data.result.Name || "there"} — your application is currently:
{data.result.ingested_at && (
Submitted {new Date(data.result.ingested_at).toLocaleDateString()}
)}
{data.result.pipeline_stages && data.result.pipeline_stages.length > 0 && (
)}
)}
{data.checked && data.error && (
{data.error}
)}
);
}
var EMPTY_APPLICATION = {
fullName: "", email: "", phone: "", linkedin: "", jdId: "", coverNote: "",
resumeFile: null,
};
function ApplicationForm(props) {
var api = props.api;
var formState = React.useState(EMPTY_APPLICATION);
var form = formState[0];
var setForm = formState[1];
var jdsState = React.useState([]);
var jds = jdsState[0];
var setJds = jdsState[1];
var submitState = React.useState({ submitting: false, result: null, error: null });
var submit = submitState[0];
var setSubmit = submitState[1];
React.useEffect(function () {
api.listJds()
.then(function (r) { setJds(r.jds || []); })
.catch(function () { setJds([]); }); // non-fatal -- the JD dropdown just stays empty/"Not specified"
}, []);
function setField(name) {
return function (e) {
var value = e.target.value;
setForm(function (f) {
var updated = Object.assign({}, f);
updated[name] = value;
return updated;
});
};
}
function readFileAsBase64(file) {
return new Promise(function (resolve, reject) {
var reader = new FileReader();
reader.onload = function () {
// reader.result is "data:application/pdf;base64,AAAA..." -- strip the prefix
var commaIndex = reader.result.indexOf(",");
resolve(reader.result.slice(commaIndex + 1));
};
reader.onerror = function () { reject(new Error("Could not read the selected file.")); };
reader.readAsDataURL(file);
});
}
function handleSubmit(e) {
e.preventDefault();
if (!form.resumeFile) {
setSubmit({ submitting: false, result: null, error: "Please choose a resume PDF to upload." });
return;
}
setSubmit({ submitting: true, result: null, error: null });
readFileAsBase64(form.resumeFile)
.then(function (resumeBase64) {
return api.submitApplication({
fullName: form.fullName.trim(),
email: form.email.trim(),
phone: form.phone.trim() || null,
linkedin: form.linkedin.trim() || null,
jdId: form.jdId || null,
coverNote: form.coverNote.trim() || null,
resumeBase64: resumeBase64,
resumeFilename: form.resumeFile.name,
});
})
.then(function (result) {
setSubmit({ submitting: false, result: result, error: null });
setForm(EMPTY_APPLICATION);
})
.catch(function (err) {
setSubmit({ submitting: false, result: null, error: err.message });
});
}
return (
Submit a new application
Fields marked * are required. Everything you enter here is recorded with your submission
(see frontend/README.md) — resume parsing (name, email, skills, experience, etc.) is done
fresh from the resume itself, not from what you type below.
{submit.result && (
{submit.result.message || "Application submitted."}
)}
{submit.error && (
{submit.error}
)}
);
}
/** Read-only hiring-stage stepper for the candidate's OWN status check —
* same stage list/markup StageTimelineModal uses for recruiters, no
* "record a stage" form here (candidates can't advance their own stage).
* Rendered inline on this one page, not as a popout, since there's only
* ever one candidate's timeline to show here. */
function OwnStageTimeline(props) {
var reachedStages = props.timeline.map(function (t) { return t.stage; });
var currentStage = props.timeline.length ? props.timeline[props.timeline.length - 1].stage : null;
return (
Hiring process
{props.stages.map(function (stageName) {
var entry = props.timeline.filter(function (t) { return t.stage === stageName; })[0];
var reached = reachedStages.indexOf(stageName) !== -1;
var isCurrent = stageName === currentStage;
return (
{stageName}
{entry && entry.logged_at && (
{new Date(entry.logged_at).toLocaleDateString()}
)}
);
})}
);
}