import { useState } from 'react' import { AlertTriangle, ArrowLeft, ShieldCheck, Sparkles } from 'lucide-react' import { Link, useParams } from 'react-router-dom' import { ToolHeader } from '../../components/ToolHeader' import { Modal, Panel } from '../../components/ui' import { slateBrand } from './brand' import { inputCls } from './captureConstants' import { useTrackedOpportunity } from './captureQueries' import { CLEARANCE_LEVELS, POLYGRAPHS } from './scopeConstants' import type { RequirementDraft } from './scopeApi' import { useApproveDraft, useDrafts, useGenerateDrafts, usePatchDraft, useRejectDraft } from './scopeQueries' // SECTION 6 SCREEN 5, BUILD REQUIREMENTS: generate drafts from accepted staffing scope, show missing required // fields, allow manual completion, require named approval, promote, preserve lineage. // // THIS IS THE ONLY DOOR TO THE SCORING ENGINE, and the screen is written to feel like one. Promotion is a // separate, deliberate act with a confirmation that states what is about to become real and who is about to be // recorded as having said so. The API enforces it at a higher capability than everything else on the review // path, so the UI is the second line and not the only one. // // MISSING FIELDS ARE SHOWN AS GAPS, NEVER PREFILLED. A draft whose clearance the document never stated arrives // as unknown, and unknown is rendered as a blocker rather than as a value. A plausible default in that box // would produce a requirement that no candidate can fail on clearance, which silently removes the heaviest // component of the score. function missingFields(d: RequirementDraft): string[] { const missing: string[] = [] if (!d.labor_category.trim()) missing.push('labor category') if (!d.clearance_level || d.clearance_level === 'unknown') missing.push('clearance level') if (d.quantity < 1) missing.push('quantity') return missing } export function BuildRequirements() { const { id = '' } = useParams() const opp = useTrackedOpportunity(id) const drafts = useDrafts(id) const generate = useGenerateDrafts(id) const patch = usePatchDraft(id) const approve = useApproveDraft(id) const reject = useRejectDraft(id) const [editing, setEditing] = useState(null) const [form, setForm] = useState({ labor_category: '', quantity: 1, clearance_level: 'unknown', polygraph: 'none', minimum_years_experience: 0, work_location: '' }) const [confirming, setConfirming] = useState(null) const [rejecting, setRejecting] = useState(null) const [rejectReason, setRejectReason] = useState('') const [note, setNote] = useState('') const [err, setErr] = useState('') const run = async (fn: () => Promise) => { setErr('') try { await fn() } catch (e) { setErr(e instanceof Error ? e.message : 'the action failed') } } const all = drafts.data ?? [] const pending = all.filter((d) => d.review_status === 'draft' || d.review_status === 'in_review') const decided = all.filter((d) => d.review_status === 'approved' || d.review_status === 'rejected') return (
{/* Which opportunity this is. Every one of these screens is scoped to a single pursuit, and a reviewer arriving from a link needs to know which one before they start accepting things. */} {opp.data?.title &&

{opp.data.title}

}
Back to scope review
{note &&

{note}

} {err &&

{err}

} {drafts.isLoading &&

Loading drafts.

} {!drafts.isLoading && pending.length === 0 && (

No drafts waiting. Generation is idempotent, so pressing it again after accepting more scope only adds what is new.

)}
    {pending.map((d) => { const gaps = missingFields(d) return (
  • {d.labor_category || '(no labor category)'} {d.quantity} needed {d.clearance_level} {d.polygraph !== 'none' && ( {d.polygraph} polygraph )} derived by {d.derivation_method || 'unknown'}
    {gaps.length > 0 && (

    Missing before this can be promoted: {gaps.join(', ')}. The document did not state these, so they are blank rather than guessed.

    )}
  • ) })}
{decided.length > 0 && (
    {decided.map((d) => (
  • {d.review_status} {d.labor_category} {d.approved_requirement_id && ( requirement {d.approved_requirement_id.slice(0, 8)} )}
  • ))}
)} {editing && ( setEditing(null)}>

You are filling in what the document did not state. This is recorded as your edit, and the draft keeps its lineage back to the cited scope item it came from.

setForm({ ...form, labor_category: e.target.value })} />
setForm({ ...form, quantity: Number(e.target.value) })} />
setForm({ ...form, minimum_years_experience: Number(e.target.value) })} />
setForm({ ...form, work_location: e.target.value })} />
)} {confirming && ( setConfirming(null)}>
{/* THE CONFIRMATION SAYS WHAT BECOMES REAL. This is the only step on the whole review path that crosses into what the scoring engine can see, and a reviewer should feel the difference. */}

This creates a live requirement the scoring engine will match candidates against. Your name is recorded as the approver, the draft is latched to approved, and the lineage back to the cited scope item is kept.

Labor category
{confirming.labor_category}
Quantity
{confirming.quantity}
Clearance
{confirming.clearance_level} {confirming.polygraph !== 'none' ? ` with ${confirming.polygraph} polygraph` : ''}

Approving is not reversible by editing. A requirement that turns out wrong is corrected by a new decision, and the audit trail keeps the whole sequence.

)} {rejecting && ( setRejecting(null)}>

A rejection is recorded with your name and is kept. Nothing is deleted, so the record shows what was proposed and declined.