diff --git a/backend/job/app.py b/backend/job/app.py index acd57c8..8ae988c 100644 --- a/backend/job/app.py +++ b/backend/job/app.py @@ -698,7 +698,7 @@ async def fetch_pipeline_candidates( @router.get("/pipeline/candidate/score/fetch") async def fetch_pipeline_candidate_score( user_id:uuid.UUID=Query(...), - job_post_id:uuid.UUID=Query(...), + job_post_id:uuid.UUID=Query(None), current_user: dict = Depends(require_permission(PermissionTag.PIPELINE_VIEW)), session: AsyncSession = Depends(get_session), ): diff --git a/frontend/dist/index.html b/frontend/dist/index.html index 2c57fc8..b2ef9a9 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -23,7 +23,7 @@ - +
diff --git a/frontend/src/api/pipeline.js b/frontend/src/api/pipeline.js index 4f377b0..9cd7775 100644 --- a/frontend/src/api/pipeline.js +++ b/frontend/src/api/pipeline.js @@ -111,6 +111,38 @@ export function listTransitions({ inboxId, manualUploadId, transitionId } = {}) }) } +/** + * One candidate's CURRENT ATS score — GET /pipeline/candidate/score/fetch + * (pipeline.view). Envelope is `{ data: { manual, inbox }, total, status_code }`, + * where each side is `{overall_score, band, job_post_id, computed_at, + * candidate_id, user_id}` or null. + * + * Score only. It carries no matched/missing keywords and no critique — those + * live on the scored-candidate row (/candidate/scored/fetch). + * + * `jobPostId` pins the score to one application; omitted (as the talent pool + * does, where a candidate need not have an assigned post) the newest current + * score across the candidate's applications wins. + */ +export function fetchCandidateScore({ userId, jobPostId } = {}) { + return request('/pipeline/candidate/score/fetch', { + params: { user_id: userId, job_post_id: jobPostId }, + }) +} + +/** + * The two-sided envelope -> one score row, or null. + * + * A candidate is reached either through the inbox (they mailed us) or through + * Add Candidate (manual_upload); both sides resolve the same ats_results table, + * so whichever side answered is the score. Inbox wins a tie because an emailed + * application is the one the pipeline board is showing. + */ +export function toAtsScore(res) { + const data = res?.data ?? {} + return data.inbox ?? data.manual ?? null +} + function sourceFields(row, kind) { if (kind === 'manual') { return { diff --git a/frontend/src/lib/queryKeys.js b/frontend/src/lib/queryKeys.js index 6501ffd..8b08670 100644 --- a/frontend/src/lib/queryKeys.js +++ b/frontend/src/lib/queryKeys.js @@ -43,6 +43,9 @@ export const qk = { all: () => ['pipeline'], board: (p = {}) => ['pipeline', 'board', p], transitions: (inboxId) => ['pipeline', 'transitions', inboxId], + // One candidate's current ats_results score. Fetched on demand (a card + // click), never as part of a list, so it is keyed per candidate + job. + candidateScore: (p = {}) => ['pipeline', 'candidate-score', p], }, analytics: { all: () => ['analytics'], diff --git a/frontend/src/screens/CandidateProfile.jsx b/frontend/src/screens/CandidateProfile.jsx index 2ba242e..1c1ac44 100644 --- a/frontend/src/screens/CandidateProfile.jsx +++ b/frontend/src/screens/CandidateProfile.jsx @@ -102,7 +102,16 @@ function useProfileWrite({ userId, mutationFn, success, onDone }) { }) } -export default function CandidateProfile({ candidate: c, onClose, onAdvance, onToggleFav, onAtsMatch }) { +/** + * `atsScore` / `recommendation` are the CURRENT ats_results row, fetched by the + * caller (Talent Pool reads GET /pipeline/candidate/score/fetch on card click). + * They are optional: a caller that does not fetch it passes nothing and the hero + * falls back to the detail payload's denormalised ai_score, then to the row's. + * When present they WIN, because ats_results is the source the denorm copies. + */ +export default function CandidateProfile({ + candidate: c, atsScore = null, recommendation = null, onClose, onAdvance, onToggleFav, onAtsMatch, +}) { const { toast } = useToast() const [tab, setTab] = useState('Overview') const { data: interviews = [] } = useQuery(seedQuery('interviews')) @@ -213,8 +222,10 @@ export default function CandidateProfile({ candidate: c, onClose, onAdvance, onT