Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/tools/slatematch/OpportunityDetail.tsx |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/tools/slatematch/OpportunityDetail.tsx |
| Original SHA-256 | 070feaa1f62eb3d4d04e65bb271c3736ac9b3d4080893ef888b1391a297ee456 |
| Git blob SHA | dad9ba8881a2e778f0a81486504f21061187326d |
| Reading-copy SHA-256 | 070feaa1f62eb3d4d04e65bb271c3736ac9b3d4080893ef888b1391a297ee456 |
Recovered source transcript
Download complete source text ↓216 lines · 8.2 KB
import type { ChangeEvent } from 'react'
import { useEffect, useState } from 'react'
import { ArrowLeft, Save } from 'lucide-react'
import { Link, useParams } from 'react-router-dom'
import { KeyPersonnelPanel, TeamingPanel } from '../../screens/TeamingPanels'
import { ToolHeader } from '../../components/ToolHeader'
import { Panel } from '../../components/ui'
import { slateBrand } from './brand'
import { useTrackedOpportunity, useUpdateTrackedOpportunity } from './captureQueries'
import { inputCls, PRIME_OR_SUB, SOURCE_LABEL, STATUSES, toDateInput } from './captureConstants'
type Form = {
title: string
agency_name: string
office_name: string
solicitation_number: string
notice_type: string
naics_code: string
psc_code: string
set_aside_code: string
place_of_performance: string
incumbent_name: string
source_url: string
response_deadline: string
prime_or_sub: string
status: string
}
const EMPTY: Form = {
title: '',
agency_name: '',
office_name: '',
solicitation_number: '',
notice_type: '',
naics_code: '',
psc_code: '',
set_aside_code: '',
place_of_performance: '',
incumbent_name: '',
source_url: '',
response_deadline: '',
prime_or_sub: '',
status: 'identified',
}
export function OpportunityDetail() {
const { id = '' } = useParams()
const q = useTrackedOpportunity(id)
const update = useUpdateTrackedOpportunity()
const [form, setForm] = useState<Form>(EMPTY)
const [saved, setSaved] = useState(false)
useEffect(() => {
const o = q.data
if (!o) return
setForm({
title: o.title,
agency_name: o.agency_name,
office_name: o.office_name,
solicitation_number: o.solicitation_number,
notice_type: o.notice_type,
naics_code: o.naics_code,
psc_code: o.psc_code,
set_aside_code: o.set_aside_code,
place_of_performance: o.place_of_performance,
incumbent_name: o.incumbent_name,
source_url: o.source_url,
response_deadline: toDateInput(o.response_deadline),
prime_or_sub: o.prime_or_sub,
status: o.status || 'identified',
})
}, [q.data])
const set = (k: keyof Form) => (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
setForm((f) => ({ ...f, [k]: e.target.value }))
setSaved(false)
}
const save = () => {
update.mutate({ id, patch: { ...form } }, { onSuccess: () => setSaved(true) })
}
const field = (label: string, k: keyof Form, placeholder = '') => (
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">{label}</span>
<input value={form[k]} onChange={set(k)} placeholder={placeholder} className={inputCls} />
</label>
)
return (
<div className="space-y-4">
<ToolHeader name={slateBrand.name} tagline="Opportunity intelligence" accent={slateBrand.accent}>
<Link to="/slate/capture" className="flex items-center gap-1.5 text-sm font-semibold text-muted hover:text-ink">
<ArrowLeft size={15} /> Back to capture
</Link>
</ToolHeader>
{/* THE PHASE 2 PATH, in the order it is walked: documents in, scope reviewed, requirements built, and
amendments watched. These four screens existed as API routes with no way to reach them, which is the
same shape of defect as the promotion routes that were built and never mounted. */}
<nav className="flex flex-wrap gap-2">
<Link
to={`/slate/capture/${id}/documents`}
className="rounded-md border border-line/70 px-3 py-1.5 text-sm font-semibold text-ink hover:bg-surface"
>
Documents
</Link>
<Link
to={`/slate/capture/${id}/scope`}
className="rounded-md border border-line/70 px-3 py-1.5 text-sm font-semibold text-ink hover:bg-surface"
>
Review scope
</Link>
<Link
to={`/slate/capture/${id}/requirements`}
className="rounded-md border border-line/70 px-3 py-1.5 text-sm font-semibold text-ink hover:bg-surface"
>
Build requirements
</Link>
<Link
to={`/slate/capture/${id}/amendments`}
className="rounded-md border border-line/70 px-3 py-1.5 text-sm font-semibold text-ink hover:bg-surface"
>
Amendments
</Link>
</nav>
{q.isLoading ? (
<Panel>
<p className="p-4 text-sm text-muted">Loading...</p>
</Panel>
) : q.isError ? (
<Panel>
<p className="p-4 text-sm text-risk">Opportunity not found.</p>
</Panel>
) : (
<Panel>
<div className="space-y-4 p-4">
<div>
<p className="text-lg font-bold text-ink">{form.title || 'Untitled opportunity'}</p>
<p className="text-xs text-faint">Source {SOURCE_LABEL[q.data?.source_type ?? ''] ?? q.data?.source_type}</p>
</div>
{field('Title', 'title', 'Data Engineering Support Services')}
<div className="grid grid-cols-2 gap-3">
{field('Agency or customer', 'agency_name')}
{field('Office', 'office_name')}
</div>
<div className="grid grid-cols-2 gap-3">
{field('Solicitation or reference', 'solicitation_number')}
{field('Notice type', 'notice_type')}
</div>
<div className="grid grid-cols-3 gap-3">
{field('NAICS', 'naics_code')}
{field('PSC', 'psc_code')}
{field('Set aside', 'set_aside_code')}
</div>
{field('Place of performance', 'place_of_performance')}
<div className="grid grid-cols-2 gap-3">
{field('Incumbent', 'incumbent_name')}
{field('Source URL', 'source_url')}
</div>
<div className="grid grid-cols-3 gap-3">
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Response deadline</span>
<input type="date" value={form.response_deadline} onChange={set('response_deadline')} className={inputCls} />
</label>
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Prime or sub</span>
<select value={form.prime_or_sub} onChange={set('prime_or_sub')} className={inputCls}>
{PRIME_OR_SUB.map(([k, v]) => (
<option key={k} value={k}>
{v}
</option>
))}
</select>
</label>
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Status</span>
<select value={form.status} onChange={set('status')} className={inputCls}>
{STATUSES.map(([k, v]) => (
<option key={k} value={k}>
{v}
</option>
))}
</select>
</label>
</div>
{update.isError && <p className="text-xs text-risk">{(update.error as Error).message}</p>}
<div className="flex items-center justify-end gap-3 pt-1">
{saved && <span className="text-xs font-semibold text-blue">Saved</span>}
<button
onClick={save}
disabled={update.isPending}
className="flex items-center gap-1.5 rounded-lg bg-gradient-to-r from-blue to-navy-700 px-4 py-2 text-sm font-bold text-white hover:brightness-110 disabled:opacity-50"
>
<Save size={15} /> Save intelligence
</button>
</div>
</div>
</Panel>
)}
{/* TEAMING AND KEY PERSONNEL sit on the CAPTURE record, not the proposal, because that is when the
decisions are made. A teaming agreement is negotiated while the pursuit is being shaped, and a key
person is committed before the bid goes out. Hanging either off the proposal would mean they could
only be recorded after the moment they mattered. */}
<TeamingPanel opportunityId={id} />
<KeyPersonnelPanel opportunityId={id} />
</div>
)
}