diff --git a/frontend/src/screens/CvBank.jsx b/frontend/src/screens/CvBank.jsx
index 70cf1bf..41bb596 100644
--- a/frontend/src/screens/CvBank.jsx
+++ b/frontend/src/screens/CvBank.jsx
@@ -10,19 +10,14 @@
job. Read live from their application rather than copied
here, so there is one source of truth and nothing to sync.
- Two very different numbers live on this screen and must not be confused:
-
- Match free, deterministic keyword overlap against the job picked in
- "Rank against job". It orders the pile. It is not an assessment.
- ATS a real paid score, and only present once someone ran one. The
- "Score against job" action is what runs it, deliberately per-row.
-
- That split is the whole design: ranking the bank costs nothing and happens
- automatically when a job opens, so scoring can stay explicit and cheap.
+ ATS is a real paid score. Each row picks a job, then Run ATS scores that
+ one candidate. Speculative rows are assigned to the job first (so the banked
+ CV links like any other application), then scored. Silver medalists stay on
+ their inbox application and score via score_inbox when a message id exists.
============================================================ */
import { useEffect, useMemo, useState } from 'react'
-import { useNavigate, useSearchParams } from 'react-router-dom'
+import { useNavigate } from 'react-router-dom'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import Modal from '../ui/Modal'
@@ -30,6 +25,7 @@ import OpenResumeButton from '../ui/OpenResumeButton'
import PageHeader from '../ui/PageHeader'
import { DataTableHead, DEFAULT_PAGE_SIZE, Pagination, pageWindow, useDataTable } from '../ui/DataTable'
import { Avatar, Badge, EmptyState, Icon, ScoreChip, SkeletonRows } from '../ui/primitives'
+import { PickRoleModal } from '../ui/SuggestedRoles'
import { useToast } from '../ui/Toast'
import { qk } from '../lib/queryKeys'
import { exportStyledXlsx } from '../lib/exportXlsx'
@@ -55,10 +51,11 @@ const SOURCE_BADGE = {
/* Chips past this are collapsed into "+N" — a CV with 25 skills would
otherwise make one row taller than the rest of the page. */
const SKILL_CHIPS = 4
+const SUGGESTED_CHIPS = 3
const EMPTY_FILTERS = { source: '', band: '', years: '' }
-async function fetchBank({ limit, offset, search, filters, jobPostId }) {
+async function fetchBank({ limit, offset, search, filters }) {
const res = await candidatesApi.listCvBank({
top: limit,
skip: offset,
@@ -66,7 +63,6 @@ async function fetchBank({ limit, offset, search, filters, jobPostId }) {
source: filters.source || undefined,
band: filters.band || undefined,
minYears: filters.years ? Number(filters.years) : undefined,
- jobPostId: jobPostId || undefined,
})
const rows = Array.isArray(res?.data) ? res.data : []
return {
@@ -75,30 +71,6 @@ async function fetchBank({ limit, offset, search, filters, jobPostId }) {
}
}
-async function fetchJobs() {
- const res = await candidatesApi.listJobs()
- const rows = Array.isArray(res?.data) ? res.data : []
- return rows.map((row) => ({ id: String(row.id), title: row.title }))
-}
-
-/** The free deterministic rank. Drawn as a plain bar, never as a ScoreChip —
- a recruiter must not read it in the same visual language as a real ATS score. */
-function MatchCell({ rank, hasJob }) {
- if (!hasJob) return Pick a job
- if (rank == null) return —
- return (
-
-
{rank}/100
-
-
-
-
- )
-}
-
function AtsCell({ score, recommendation }) {
if (score == null) return Not scored
return (
@@ -113,11 +85,43 @@ function AtsCell({ score, recommendation }) {
)
}
+function SuggestedJobsCell({ jobs }) {
+ if (!jobs?.length) return null
+ return (
+
- Match is free keyword overlap against the selected
- job — it orders this list, it does not assess anyone. ATS is a real
- scored result and only appears once someone runs one.
+ Pick a job on a row, then Run ATS for a
+ real scored result. Speculative CVs are linked to that job; silver medalists
+ stay on their existing application.