Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/tools/slatematch/BuildRequirements.tsx |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/tools/slatematch/BuildRequirements.tsx |
| Original SHA-256 | b9dc0868fb2b98a4d96ab391a471b2bda4c54cb9aad5c8641493749d9fbde048 |
| Git blob SHA | 535afd09198b7996e51e4358ea5ab9128935058f |
| Reading-copy SHA-256 | b9dc0868fb2b98a4d96ab391a471b2bda4c54cb9aad5c8641493749d9fbde048 |
Recovered source transcript
Download complete source text ↓376 lines · 17.7 KB
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<RequirementDraft | null>(null)
const [form, setForm] = useState({ labor_category: '', quantity: 1, clearance_level: 'unknown', polygraph: 'none', minimum_years_experience: 0, work_location: '' })
const [confirming, setConfirming] = useState<RequirementDraft | null>(null)
const [rejecting, setRejecting] = useState<RequirementDraft | null>(null)
const [rejectReason, setRejectReason] = useState('')
const [note, setNote] = useState('')
const [err, setErr] = useState('')
const run = async (fn: () => Promise<unknown>) => {
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 (
<div className="space-y-6">
<ToolHeader name={slateBrand.name} tagline="Build requirements" accent={slateBrand.accent} />
{/* 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 && <p className="text-sm font-semibold text-ink">{opp.data.title}</p>}
<div className="flex flex-wrap items-center justify-between gap-3">
<Link to={`/slate/capture/${id}/scope`} className="inline-flex items-center gap-1.5 text-sm font-semibold text-muted hover:text-ink">
<ArrowLeft size={14} /> Back to scope review
</Link>
<button
type="button"
disabled={generate.isPending}
onClick={() =>
void run(async () => {
const r = await generate.mutateAsync()
setNote(
r.created === 0 && r.skipped_existing === 0
? 'No accepted staffing scope to derive from yet. Accept staffing items on the review screen first.'
: `Generated ${r.created} draft${r.created === 1 ? '' : 's'}, skipped ${r.skipped_existing} that already existed.`,
)
})
}
className="inline-flex items-center gap-1.5 rounded-md bg-navy px-3 py-1.5 text-sm font-semibold text-white hover:opacity-90 disabled:opacity-40"
>
<Sparkles size={14} /> Generate from accepted scope
</button>
</div>
{note && <p className="rounded bg-sky-50 p-3 text-sm text-sky-900">{note}</p>}
{err && <p className="rounded bg-rose-50 p-3 text-sm font-semibold text-rose-800">{err}</p>}
<Panel title={`Awaiting a decision, ${pending.length}`}>
{drafts.isLoading && <p className="text-sm text-faint">Loading drafts.</p>}
{!drafts.isLoading && pending.length === 0 && (
<p className="text-sm text-faint">
No drafts waiting. Generation is idempotent, so pressing it again after accepting more scope only adds
what is new.
</p>
)}
<ul className="space-y-3">
{pending.map((d) => {
const gaps = missingFields(d)
return (
<li key={d.id} className="rounded-md border border-slate-200 p-3">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-semibold text-ink">{d.labor_category || '(no labor category)'}</span>
<span className="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-600">
{d.quantity} needed
</span>
<span className="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-600">
{d.clearance_level}
</span>
{d.polygraph !== 'none' && (
<span className="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-600">
{d.polygraph} polygraph
</span>
)}
<span className="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-600">
derived by {d.derivation_method || 'unknown'}
</span>
</div>
{gaps.length > 0 && (
<p className="mt-2 inline-flex items-start gap-1.5 rounded bg-amber-50 p-2 text-xs font-semibold text-amber-800">
<AlertTriangle size={13} className="mt-0.5 shrink-0" />
<span>
Missing before this can be promoted: {gaps.join(', ')}. The document did not state these, so
they are blank rather than guessed.
</span>
</p>
)}
<div className="mt-3 flex flex-wrap gap-2">
<button
type="button"
onClick={() => {
setEditing(d)
setForm({
labor_category: d.labor_category,
quantity: d.quantity,
clearance_level: d.clearance_level,
polygraph: d.polygraph,
minimum_years_experience: d.minimum_years_experience,
work_location: d.work_location,
})
}}
className="rounded border border-sky-300 px-2 py-1 text-xs font-semibold text-sky-700 hover:bg-sky-50"
>
Complete by hand
</button>
<button
type="button"
disabled={gaps.length > 0}
title={gaps.length > 0 ? 'Complete the missing fields first' : 'Promote to a live requirement'}
onClick={() => setConfirming(d)}
className="inline-flex items-center gap-1 rounded bg-emerald-600 px-2 py-1 text-xs font-semibold text-white hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40"
>
<ShieldCheck size={12} /> Approve and promote
</button>
<button
type="button"
onClick={() => {
setRejecting(d)
setRejectReason('')
}}
className="rounded border border-rose-300 px-2 py-1 text-xs font-semibold text-rose-700 hover:bg-rose-50"
>
Reject
</button>
</div>
</li>
)
})}
</ul>
</Panel>
{decided.length > 0 && (
<Panel title={`Decided, ${decided.length}`}>
<ul className="space-y-1">
{decided.map((d) => (
<li key={d.id} className="flex flex-wrap items-center gap-2 text-sm">
<span
className={`rounded-full px-2 py-0.5 text-xs font-semibold ${
d.review_status === 'approved' ? 'bg-emerald-50 text-emerald-700' : 'bg-slate-100 text-slate-500'
}`}
>
{d.review_status}
</span>
<span className={d.review_status === 'rejected' ? 'text-slate-500 line-through' : 'text-ink'}>
{d.labor_category}
</span>
{d.approved_requirement_id && (
<span className="text-xs text-faint">requirement {d.approved_requirement_id.slice(0, 8)}</span>
)}
</li>
))}
</ul>
</Panel>
)}
{editing && (
<Modal open title="Complete this draft" onClose={() => setEditing(null)}>
<div className="space-y-3">
<p className="text-xs text-faint">
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.
</p>
<div>
<label className="mb-1 block text-xs font-semibold uppercase tracking-wide text-muted" htmlFor="d-cat">
Labor category
</label>
<input id="d-cat" className={inputCls} value={form.labor_category} onChange={(e) => setForm({ ...form, labor_category: e.target.value })} />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="mb-1 block text-xs font-semibold uppercase tracking-wide text-muted" htmlFor="d-qty">
Quantity
</label>
<input id="d-qty" type="number" min={0} className={inputCls} value={form.quantity} onChange={(e) => setForm({ ...form, quantity: Number(e.target.value) })} />
</div>
<div>
<label className="mb-1 block text-xs font-semibold uppercase tracking-wide text-muted" htmlFor="d-years">
Minimum years
</label>
<input id="d-years" type="number" min={0} className={inputCls} value={form.minimum_years_experience} onChange={(e) => setForm({ ...form, minimum_years_experience: Number(e.target.value) })} />
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="mb-1 block text-xs font-semibold uppercase tracking-wide text-muted" htmlFor="d-clear">
Clearance level
</label>
<select id="d-clear" className={inputCls} value={form.clearance_level} onChange={(e) => setForm({ ...form, clearance_level: e.target.value })}>
{CLEARANCE_LEVELS.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
</div>
<div>
<label className="mb-1 block text-xs font-semibold uppercase tracking-wide text-muted" htmlFor="d-poly">
Polygraph
</label>
<select id="d-poly" className={inputCls} value={form.polygraph} onChange={(e) => setForm({ ...form, polygraph: e.target.value })}>
{POLYGRAPHS.map((p) => (
<option key={p} value={p}>
{p}
</option>
))}
</select>
</div>
</div>
<div>
<label className="mb-1 block text-xs font-semibold uppercase tracking-wide text-muted" htmlFor="d-loc">
Work location
</label>
<input id="d-loc" className={inputCls} value={form.work_location} onChange={(e) => setForm({ ...form, work_location: e.target.value })} />
</div>
<div className="flex justify-end gap-2">
<button type="button" onClick={() => setEditing(null)} className="rounded-md border border-slate-300 px-3 py-1.5 text-sm font-semibold">
Cancel
</button>
<button
type="button"
onClick={() =>
void run(async () => {
await patch.mutateAsync({ draftId: editing.id, patch: form })
setEditing(null)
})
}
className="rounded-md bg-navy px-3 py-1.5 text-sm font-semibold text-white"
>
Save draft
</button>
</div>
</div>
</Modal>
)}
{confirming && (
<Modal open title="Promote to a live requirement" onClose={() => setConfirming(null)}>
<div className="space-y-3">
{/* 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. */}
<p className="text-sm text-ink">
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.
</p>
<dl className="rounded bg-slate-50 p-3 text-sm">
<div className="flex justify-between gap-4 py-0.5">
<dt className="text-muted">Labor category</dt>
<dd className="font-semibold text-ink">{confirming.labor_category}</dd>
</div>
<div className="flex justify-between gap-4 py-0.5">
<dt className="text-muted">Quantity</dt>
<dd className="font-semibold text-ink">{confirming.quantity}</dd>
</div>
<div className="flex justify-between gap-4 py-0.5">
<dt className="text-muted">Clearance</dt>
<dd className="font-semibold text-ink">
{confirming.clearance_level}
{confirming.polygraph !== 'none' ? ` with ${confirming.polygraph} polygraph` : ''}
</dd>
</div>
</dl>
<p className="text-xs text-faint">
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.
</p>
<div className="flex justify-end gap-2">
<button type="button" onClick={() => setConfirming(null)} className="rounded-md border border-slate-300 px-3 py-1.5 text-sm font-semibold">
Cancel
</button>
<button
type="button"
onClick={() =>
void run(async () => {
await approve.mutateAsync(confirming.id)
setConfirming(null)
setNote('Promoted. The requirement is live and the draft is latched to approved.')
})
}
className="rounded-md bg-emerald-600 px-3 py-1.5 text-sm font-semibold text-white"
>
Approve and promote
</button>
</div>
</div>
</Modal>
)}
{rejecting && (
<Modal open title="Reject this draft" onClose={() => setRejecting(null)}>
<div className="space-y-3">
<p className="text-xs text-faint">
A rejection is recorded with your name and is kept. Nothing is deleted, so the record shows what was
proposed and declined.
</p>
<div>
<label className="mb-1 block text-xs font-semibold uppercase tracking-wide text-muted" htmlFor="rej">
Reason
</label>
<textarea id="rej" rows={3} className={inputCls} value={rejectReason} onChange={(e) => setRejectReason(e.target.value)} />
</div>
<div className="flex justify-end gap-2">
<button type="button" onClick={() => setRejecting(null)} className="rounded-md border border-slate-300 px-3 py-1.5 text-sm font-semibold">
Cancel
</button>
<button
type="button"
onClick={() =>
void run(async () => {
await reject.mutateAsync({ draftId: rejecting.id, reason: rejectReason })
setRejecting(null)
})
}
className="rounded-md bg-rose-600 px-3 py-1.5 text-sm font-semibold text-white"
>
Reject draft
</button>
</div>
</div>
</Modal>
)}
</div>
)
}