import { useState } from 'react'
import { ArrowLeft, ArrowRight, Plus, ShieldCheck, UserX } from 'lucide-react'
import { Link, useParams } from 'react-router-dom'
import { ToolHeader } from '../../components/ToolHeader'
import { Modal, Panel } from '../../components/ui'
import { useCandidates } from '../../lib/queries'
import type { RApplication, RFunnelStage } from '../../lib/types'
import { recruitBrand } from './brand'
import { useDispositions, useMoveStage, usePipelineBoard, useSubmitCandidate } from './pipelineQueries'
const inputCls =
'w-full rounded-lg border border-line/70 bg-navy-900 px-3 py-2 text-sm text-ink outline-none focus:border-recruit'
// The ladder, mirrored so the board can offer the next and previous rung. The server is the authority on
// whether a move is legal; this only decides what is offered.
const LADDER = ['sourced', 'screened', 'submitted', 'interviewing', 'offered', 'placed']
function stageColor(stage: string): string {
if (stage === 'placed') return 'var(--color-ok)'
if (stage === 'rejected' || stage === 'withdrawn') return 'var(--color-muted)'
if (stage === 'offered') return 'var(--color-gold)'
return 'var(--color-blue-soft)'
}
// The funnel bar. Width is the share of the widest rung, so the shape reads as a funnel; the label
// carries the actual count so the picture never has to be trusted on its own.
function FunnelBar({ stage, max }: { stage: RFunnelStage; max: number }) {
const pct = max > 0 ? (stage.count / max) * 100 : 0
return (
)
}
export function Pipeline() {
const { reqId } = useParams()
const q = usePipelineBoard(reqId ?? null)
const disp = useDispositions()
const submit = useSubmitCandidate(reqId ?? '')
const move = useMoveStage(reqId ?? undefined)
// The candidate list is reused rather than duplicated: submitting to a pipeline is choosing from the
// bench the rest of the product already maintains. Through the shared hook, not a second inline query
// on the same key, so any option added there (a select, a staleTime) actually reaches this screen.
const candidates = useCandidates()
const [addOpen, setAddOpen] = useState(false)
const [candId, setCandId] = useState('')
const [startStage, setStartStage] = useState('sourced')
const [touch, setTouch] = useState('')
const [disposing, setDisposing] = useState<{ app: RApplication; to: string } | null>(null)
const [code, setCode] = useState('')
const [reason, setReason] = useState('')
if (q.isLoading) return Loading pipeline…
if (q.isError || !q.data) {
return (
Could not load this pipeline. {(q.error as Error)?.message}
)
}
const b = q.data
const maxRung = b.funnel.length ? b.funnel[0].count : 0
const doMove = (app: RApplication, to: string) => {
if (to === 'rejected' || to === 'withdrawn') {
setDisposing({ app, to })
setCode('')
setReason('')
return
}
move.mutate({ id: app.id, expected_stage: app.stage, to_stage: to })
}
const confirmDisposition = () => {
if (!disposing) return
move.mutate(
{
id: disposing.app.id,
expected_stage: disposing.app.stage,
to_stage: disposing.to,
disposition_code: code,
reason,
},
{ onSuccess: () => setDisposing(null) },
)
}
const options = (disp.data?.dispositions ?? []).filter(
(d) => !disposing || d.outcome === '' || d.outcome === disposing.to,
)
return (
Job order
{b.requisition_title}
{b.total} in pipeline · {b.active} active · {b.placed} of {b.openings} placed ·{' '}
{b.seats_left === 0 ? 'all seats filled' : `${b.seats_left} seat${b.seats_left === 1 ? '' : 's'} left`}
{/* Counted as high water marks, not current position. A candidate rejected at interview still
counts as having reached interview, which is why this series can never widen. */}
{b.funnel.map((s) => (
))}
{b.total === 0 && (
No candidates yet. Add one to start the pipeline.
)}
{b.total === 0 ? (
Conversion appears once candidates move.
) : (
{b.conversion.map((c) => (
{c.from_label} {c.to_label}
{c.rate}%
))}
)}
{b.dispositions.length > 0 && (
Why candidates left
{/* Coded, not prose, because this is the grouping an adverse impact review needs. */}
{col.applications.map((a) => {
const i = LADDER.indexOf(a.stage)
const next = i >= 0 && i + 1 < LADDER.length ? LADDER[i + 1] : null
const prev = i > 0 ? LADDER[i - 1] : null
return (
{a.candidate_name}
{/* Clearance is a self reported indicator throughout this product, never a
verified status. The client FSO verifies through DISS at onboarding. */}
{a.clearance_level.replace(/_/g, ' ')}
{a.days_in_stage.toFixed(1)}d here
{a.furthest_stage !== a.stage && reached {a.furthest_stage_label}}