Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/screens/CaptureReview.tsx |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/screens/CaptureReview.tsx |
| Original SHA-256 | 52bddac18552a689edbf417b8b05eba5fa1624c386d64d77a9541a7d8beab4b5 |
| Git blob SHA | c19dd918470238781fd402ab79a9148796eb1130 |
| Reading-copy SHA-256 | 52bddac18552a689edbf417b8b05eba5fa1624c386d64d77a9541a7d8beab4b5 |
Recovered source transcript
Download complete source text ↓213 lines · 8.3 KB
import { useState } from 'react'
import { Panel } from '../components/ui'
import { Empty, Note, Toggle, btnGhost, btnPrimary, fieldCls } from '../components/form'
import {
CAPTURE_GATES,
GATE_LABELS,
decisionColor,
useCaptureReadiness,
useCaptureReview,
useRecordGate,
type CaptureGate,
type ReviewRow,
} from '../lib/capture'
// THE CAPTURE REVIEW.
//
// Every live pursuit, how far it has been taken through the gates, and what is unresolved. The number at the
// top is pursuits being worked with no recorded decision at any gate, which is the state this whole phase
// exists to make visible: work in flight that nobody has formally said yes to.
function GateCell({ row, gate }: { row: ReviewRow; gate: CaptureGate }) {
const d = row.latest?.[gate]
if (!d) {
return <span className="text-[11px] text-faint">not decided</span>
}
return (
<span
className="rounded-full px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider"
style={{
color: decisionColor(d.decision),
background: `color-mix(in srgb, ${decisionColor(d.decision)} 14%, transparent)`,
}}
title={`${d.decided_by_name || 'somebody'} on ${d.decided_at.slice(0, 10)}: ${d.rationale}`}
>
{d.decision === 'go' ? 'go' : 'no go'}
</span>
)
}
function RecordForm({ row, onDone }: { row: ReviewRow; onDone: () => void }) {
const [gate, setGate] = useState<CaptureGate>('qualify')
const [decision, setDecision] = useState<'go' | 'no_go'>('go')
const [rationale, setRationale] = useState('')
const record = useRecordGate(row.opportunity_id)
// The advisory this decision is being made against. Read at the moment of deciding, shown on the form,
// and snapshotted into the record, so the number stored is the number somebody actually looked at.
const advisory = useCaptureReadiness(row.opportunity_id)
return (
<div className="mt-2 space-y-2 rounded-md border border-line/70 bg-surface-2/40 px-3 py-2.5">
<div className="flex flex-wrap gap-2">
<select className={fieldCls} value={gate} onChange={(e) => setGate(e.target.value as CaptureGate)}>
{CAPTURE_GATES.map((g) => (
<option key={g} value={g}>
{GATE_LABELS[g]} gate
</option>
))}
</select>
<select
className={fieldCls}
value={decision}
onChange={(e) => setDecision(e.target.value as 'go' | 'no_go')}
>
<option value="go">Go</option>
<option value="no_go">No go</option>
</select>
</div>
{/* THE NUMBER THE DECISION IS BEING MADE AGAINST. Absent is stated as absent: a missing advisory is
never rendered as a zero, and the record stores nothing rather than a number nobody saw. */}
<div className="text-[11px] text-faint">
{advisory.isPending && 'Reading the capture advisory'}
{advisory.isError && 'No capture advisory is available for this pursuit. The decision is recorded without one.'}
{advisory.data && (
<span>
Capture readiness{' '}
<b style={{ color: 'var(--color-ink)' }}>{advisory.data.band ?? 'not banded'}</b>
{advisory.data.score ? ` at ${advisory.data.score}` : ''}. This is what gets stored beside the
decision, as it stands now.
</span>
)}
</div>
<textarea
className={`${fieldCls} min-h-[64px] w-full`}
placeholder="Why. This is the part a status could never carry, and the part that teaches the next pursuit something."
value={rationale}
onChange={(e) => setRationale(e.target.value)}
/>
{record.isError && <Note tone="risk">{(record.error as Error).message}</Note>}
<div className="flex gap-2">
<button
type="button"
className={btnPrimary}
disabled={!rationale.trim() || record.isPending}
onClick={() =>
record.mutate(
{
gate,
decision,
rationale,
// Only sent when the advisory actually answered. Passing undefined stores NULL, which the
// record reads as "no advisory was available", and that is a different and honest fact from
// a zero.
readiness_score: advisory.data?.score,
readiness_band: advisory.data?.band,
},
{ onSuccess: onDone },
)
}
>
{record.isPending ? 'Recording' : 'Record the decision'}
</button>
<button type="button" className={btnGhost} onClick={onDone}>
Cancel
</button>
</div>
</div>
)
}
export function CaptureReview() {
const [includeClosed, setIncludeClosed] = useState(false)
const [recording, setRecording] = useState('')
const q = useCaptureReview(includeClosed)
const v = q.data
return (
<div className="space-y-4">
<Panel
title="Capture review"
hint={v ? (v.undecided === 0 ? 'every pursuit has a decision' : `${v.undecided} with no decision`) : ''}
>
<div className="flex items-center justify-between gap-3">
<p className="text-[11px] leading-relaxed text-faint">
A bid or no bid used to be a status, which recorded the outcome and never who decided, when, or
against what. Every decision here carries all three, and the advisory numbers are kept as they
stood at the time rather than recomputed, so a decision made in March still reads in June as the
decision that was actually made.
</p>
<label className="flex shrink-0 items-center gap-2 text-[11px] text-muted">
<Toggle value={includeClosed} onChange={setIncludeClosed} label="Closed too" />
</label>
</div>
</Panel>
{q.isError ? (
<Panel>
<Note tone="risk">{(q.error as Error).message}</Note>
</Panel>
) : !v ? (
<Panel className="py-10 text-center text-muted">Reading the pipeline</Panel>
) : v.pursuits.length === 0 ? (
<Panel>
<Empty
title="No pursuits being worked"
reason="Track an opportunity under capture and it appears here for a gate decision."
/>
</Panel>
) : (
<Panel title="Pursuits" hint={`${v.total}${v.truncated ? ' shown' : ''}`}>
<ul className="space-y-1">
{v.pursuits.map((p) => (
<li key={p.opportunity_id} className="border-b border-line/40 py-2 last:border-0">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-sm text-ink">{p.title || 'Untitled pursuit'}</div>
<div className="text-[11px] text-faint">
{p.agency || 'agency not recorded'} · {p.status}
{p.lapsed_commitments > 0 && (
<span style={{ color: 'var(--color-warn)' }}>
{' '}
· {p.lapsed_commitments} key personnel not signed
</span>
)}
</div>
</div>
<div className="flex shrink-0 items-center gap-3">
{CAPTURE_GATES.map((g) => (
<span key={g} className="flex items-center gap-1.5 text-[10px] text-faint">
{GATE_LABELS[g]}
<GateCell row={p} gate={g} />
</span>
))}
<button
type="button"
className={btnGhost}
onClick={() =>
setRecording(recording === p.opportunity_id ? '' : p.opportunity_id)
}
>
Record
</button>
</div>
</div>
{recording === p.opportunity_id && (
<RecordForm row={p} onDone={() => setRecording('')} />
)}
</li>
))}
</ul>
{v.truncated && (
<div className="mt-3">
<Note tone="muted">
Showing the most recent {v.pursuits.length}. There are more.
</Note>
</div>
)}
</Panel>
)}
</div>
)
}