Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/tools/slatematch/AmendmentView.tsx |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/tools/slatematch/AmendmentView.tsx |
| Original SHA-256 | 712a06b2f79cd8a95c93368265b509f9b42d9db67694b3f7db712d61c198f559 |
| Git blob SHA | dec32ffe952d1d65c4ec9c017689ef0b9c84e3bc |
| Reading-copy SHA-256 | 712a06b2f79cd8a95c93368265b509f9b42d9db67694b3f7db712d61c198f559 |
Recovered source transcript
Download complete source text ↓184 lines · 8.6 KB
import { AlertTriangle, ArrowLeft, FileClock, ShieldAlert } from 'lucide-react'
import { Link, useParams } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'
import { ToolHeader } from '../../components/ToolHeader'
import { Panel } from '../../components/ui'
import { request } from '../../lib/api'
import { slateBrand } from './brand'
import { useTrackedOpportunity } from './captureQueries'
// THE AMENDMENT VIEW. Directive Section 4 item 11: an amendment FLAGS affected approved requirements for a
// named human to accept, and nothing silently rewrites one.
//
// THE SCREEN IS BUILT AROUND THAT SENTENCE. It shows the version history so a reader can see that versions are
// added and never edited, the alerts so they can see somebody was told, and the approved requirements reachable
// from this opportunity so they can see WHAT IS AT STAKE. There is deliberately no button here that applies an
// amendment to a requirement, because no such action exists: the requirement changes only by a human going to
// the draft screen and making a new decision, which is the whole safety property.
interface AmendmentPayload {
versions: Array<{
version_number: number
amendment_number: string
source_version_label: string
source_payload_hash: string
fetched_at: string
is_current: boolean
}>
alerts: Array<{
id: string
alert_type: string
severity: string
title: string
detail: string
status: string
created_at: string
}>
affected_requirements: Array<{
requirement_id: string
draft_id: string
labor_category: string
clearance_level: string
quantity: number
source_scope_item_id: string
source_scope_title: string
}>
}
export function AmendmentView() {
const { id = '' } = useParams()
const opp = useTrackedOpportunity(id)
const q = useQuery({
queryKey: ['slatematch', 'amendments', id],
queryFn: () => request<AmendmentPayload>(`/opportunities/tracked/${encodeURIComponent(id)}/amendments`),
enabled: !!id,
})
const versions = q.data?.versions ?? []
const alerts = q.data?.alerts ?? []
const affected = q.data?.affected_requirements ?? []
return (
<div className="space-y-6">
<ToolHeader name={slateBrand.name} tagline="Amendments" 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>}
<Link to={`/slate/capture/${id}`} className="inline-flex items-center gap-1.5 text-sm font-semibold text-muted hover:text-ink">
<ArrowLeft size={14} /> Back to the opportunity
</Link>
{q.isLoading && <p className="text-sm text-faint">Loading the amendment history.</p>}
<Panel title={`Version history, ${versions.length}`}>
<p className="mb-3 text-xs text-faint">
Versions are immutable. The database refuses an update to anything but the current flag and refuses a
delete outright, so this list only ever grows.
</p>
{versions.length === 0 && <p className="text-sm text-faint">No versions recorded yet.</p>}
<ol className="space-y-2">
{versions.map((v) => (
<li key={v.version_number} className="flex flex-wrap items-center gap-2 rounded-md border border-slate-200 p-3">
<FileClock size={14} className="text-muted" />
<span className="text-sm font-semibold text-ink">Version {v.version_number}</span>
{v.is_current && (
<span className="rounded-full bg-emerald-50 px-2 py-0.5 text-xs font-semibold text-emerald-700">current</span>
)}
{v.amendment_number && (
<span className="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-600">
amendment {v.amendment_number}
</span>
)}
{v.source_version_label && <span className="text-xs text-faint">{v.source_version_label}</span>}
<span className="text-xs text-faint">{v.fetched_at.slice(0, 19)}</span>
<span className="text-xs text-faint" title={v.source_payload_hash}>
payload {v.source_payload_hash.slice(0, 10)}
</span>
</li>
))}
</ol>
{versions.length > 0 && versions.filter((v) => v.is_current).length !== 1 && (
// FINDING D9 LEFT A VISIBLE SCAR AND THIS IS IT. An opportunity with no current version, or two, is a
// broken invariant, and it is surfaced rather than left for a background job to trip over again.
<p className="mt-3 inline-flex items-start gap-1.5 rounded bg-rose-50 p-2 text-xs font-semibold text-rose-800">
<AlertTriangle size={13} className="mt-0.5 shrink-0" />
<span>
{versions.filter((v) => v.is_current).length} versions are flagged current. Exactly one should be.
This is the invariant whose breach stopped amendment recording for this opportunity in July.
</span>
</p>
)}
</Panel>
<Panel title={`Alerts raised, ${alerts.length}`}>
{alerts.length === 0 && <p className="text-sm text-faint">No amendment alerts for this opportunity.</p>}
<ul className="space-y-2">
{alerts.map((a) => (
<li key={a.id} className="rounded-md border border-slate-200 p-3">
<div className="flex flex-wrap items-center gap-2">
<span
className={`rounded-full px-2 py-0.5 text-xs font-semibold ${
a.severity === 'urgent' ? 'bg-rose-50 text-rose-700' : 'bg-amber-50 text-amber-700'
}`}
>
{a.severity}
</span>
<span className="text-sm font-semibold text-ink">{a.title}</span>
<span className="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-600">
{a.status}
</span>
</div>
{a.detail && <p className="mt-1 text-sm text-muted">{a.detail}</p>}
<p className="mt-1 text-xs text-faint">{a.created_at.slice(0, 19)}</p>
</li>
))}
</ul>
</Panel>
<Panel title={`Approved requirements derived from this opportunity, ${affected.length}`}>
<p className="mb-3 text-xs text-faint">
These are live requirements the scoring engine matches against. An amendment FLAGS them; it never
rewrites them. Changing one means a named human making a new decision on the draft screen.
</p>
{affected.length === 0 && (
<p className="text-sm text-faint">Nothing has been promoted from this opportunity yet, so nothing is at risk.</p>
)}
<ul className="space-y-2">
{affected.map((r) => (
<li key={r.requirement_id} className="rounded-md border border-slate-200 p-3">
<div className="flex flex-wrap items-center gap-2">
{alerts.length > 0 && <ShieldAlert size={14} className="text-amber-600" />}
<span className="text-sm font-semibold text-ink">{r.labor_category}</span>
<span className="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-600">
{r.quantity} needed
</span>
<span className="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-600">
{r.clearance_level}
</span>
</div>
{/* THE LINEAGE, rendered so a flag can be walked back to the cited span it came from. */}
<p className="mt-1 text-xs text-faint">
derived from scope item {r.source_scope_title || r.source_scope_item_id.slice(0, 8) || 'not recorded'}
{r.requirement_id ? ` , requirement ${r.requirement_id.slice(0, 8)}` : ''}
</p>
</li>
))}
</ul>
{alerts.length > 0 && affected.length > 0 && (
<p className="mt-3 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>
The source changed and {affected.length} approved requirement
{affected.length === 1 ? ' is' : 's are'} derived from it. Review the scope, then decide. Nothing has
been altered automatically.
</span>
</p>
)}
</Panel>
</div>
)
}