Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/tools/slatematch/Capture.tsx |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/tools/slatematch/Capture.tsx |
| Original SHA-256 | e4af9536789d16f79101533e513e11230e23105685c3d5f34b8ad5ed969878e6 |
| Git blob SHA | 43bf020f5304dd05d65e2ef1ef00b8d455aa66e3 |
| Reading-copy SHA-256 | e4af9536789d16f79101533e513e11230e23105685c3d5f34b8ad5ed969878e6 |
Recovered source transcript
Download complete source text ↓207 lines · 8.2 KB
import { useState } from 'react'
import { Plus, Target } from 'lucide-react'
import { Link } from 'react-router-dom'
import { ToolHeader } from '../../components/ToolHeader'
import { Modal, Panel } from '../../components/ui'
import { slateBrand } from './brand'
import { useCreateTrackedOpportunity, useTrackedOpportunities } from './captureQueries'
import { fmtDeadline, inputCls, SOURCE_TYPES, STATUS_LABEL, STATUSES } from './captureConstants'
const FILTERS: [string, string][] = [['', 'All'], ...STATUSES.slice(0, 6)]
const chip = (on: boolean) =>
`rounded-full px-3 py-1 text-xs font-semibold transition ${
on
? 'bg-blue/25 text-blue shadow-[inset_0_0_0_1px_var(--color-blue)]'
: 'bg-navy-900 text-faint hover:text-muted border border-line/60'
}`
export function Capture() {
const [status, setStatus] = useState('')
const [open, setOpen] = useState(false)
const list = useTrackedOpportunities({ status })
const create = useCreateTrackedOpportunity()
const [title, setTitle] = useState('')
const [sourceType, setSourceType] = useState('manual')
const [agency, setAgency] = useState('')
const [solNumber, setSolNumber] = useState('')
const [deadline, setDeadline] = useState('')
const [primeOrSub, setPrimeOrSub] = useState('')
const [newStatus, setNewStatus] = useState('identified')
const submit = () => {
create.mutate(
{
title,
source_type: sourceType,
agency_name: agency,
solicitation_number: solNumber,
response_deadline: deadline,
prime_or_sub: primeOrSub,
status: newStatus,
},
{
onSuccess: () => {
setTitle('')
setSourceType('manual')
setAgency('')
setSolNumber('')
setDeadline('')
setPrimeOrSub('')
setNewStatus('identified')
setOpen(false)
},
},
)
}
const rows = list.data ?? []
return (
<div className="space-y-4">
<ToolHeader name={slateBrand.name} tagline="Opportunity capture" accent={slateBrand.accent}>
<button
onClick={() => setOpen(true)}
className="flex items-center gap-1.5 rounded-lg bg-gradient-to-r from-blue to-navy-700 px-3 py-2 text-sm font-bold text-white hover:brightness-110"
>
<Plus size={15} />
New opportunity
</button>
</ToolHeader>
<div className="flex flex-wrap items-center gap-2">
{FILTERS.map(([k, label]) => (
<button key={k || 'all'} className={chip(status === k)} onClick={() => setStatus(k)}>
{label}
</button>
))}
</div>
<Panel>
{list.isLoading ? (
<p className="p-4 text-sm text-muted">Loading opportunities...</p>
) : rows.length === 0 ? (
<div className="flex flex-col items-center gap-2 p-10 text-center">
<Target size={28} className="text-faint" />
<p className="text-sm text-muted">No tracked opportunities yet.</p>
<p className="text-xs text-faint">Capture one from SAM.gov or enter it manually to begin the pursuit.</p>
</div>
) : (
<ul className="divide-y divide-line/50">
{rows.map((o) => (
<li key={o.id}>
<Link
to={`/slate/capture/${o.id}`}
className="flex items-center gap-4 px-4 py-3 transition hover:bg-navy-900/60"
>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-semibold text-ink">{o.title || 'Untitled opportunity'}</p>
<p className="truncate text-xs text-faint">
{[o.agency_name, o.solicitation_number].filter(Boolean).join(' · ') || 'No agency yet'}
</p>
</div>
<span className="rounded-full bg-navy-900 px-2.5 py-1 text-[11px] font-semibold text-muted">
{STATUS_LABEL[o.status] ?? o.status}
</span>
<span className="w-28 text-right text-xs text-faint">{fmtDeadline(o.response_deadline)}</span>
</Link>
</li>
))}
</ul>
)}
</Panel>
<Modal open={open} onClose={() => setOpen(false)} title="New opportunity">
<div className="space-y-3">
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Title</span>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Data Engineering Support Services"
className={inputCls}
/>
</label>
<div className="grid grid-cols-2 gap-3">
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Source</span>
<select value={sourceType} onChange={(e) => setSourceType(e.target.value)} className={inputCls}>
{SOURCE_TYPES.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">Prime or sub</span>
<select value={primeOrSub} onChange={(e) => setPrimeOrSub(e.target.value)} className={inputCls}>
<option value="">Not set</option>
<option value="prime">Prime</option>
<option value="sub">Subcontract</option>
</select>
</label>
</div>
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Agency or customer</span>
<input
value={agency}
onChange={(e) => setAgency(e.target.value)}
placeholder="Department of Veterans Affairs"
className={inputCls}
/>
</label>
<div className="grid grid-cols-2 gap-3">
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Solicitation or reference</span>
<input
value={solNumber}
onChange={(e) => setSolNumber(e.target.value)}
placeholder="VA-26-00062403"
className={inputCls}
/>
</label>
<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={deadline} onChange={(e) => setDeadline(e.target.value)} className={inputCls} />
</label>
</div>
<label className="flex flex-col gap-1.5">
<span className="text-[11px] font-bold uppercase tracking-wider text-muted">Status</span>
<select value={newStatus} onChange={(e) => setNewStatus(e.target.value)} className={inputCls}>
{STATUSES.map(([k, v]) => (
<option key={k} value={k}>
{v}
</option>
))}
</select>
</label>
<p className="text-[11px] text-faint">
<Target size={11} className="mr-1 inline" />
A manual opportunity supports every downstream step a SAM.gov one does.
</p>
{create.isError && <p className="text-xs text-risk">{(create.error as Error).message}</p>}
<div className="flex justify-end gap-2 pt-1">
<button
onClick={() => setOpen(false)}
className="rounded-lg border border-line/70 px-3 py-2 text-sm font-semibold text-muted hover:text-ink"
>
Cancel
</button>
<button
onClick={submit}
disabled={!title.trim() || create.isPending}
className="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"
>
Capture opportunity
</button>
</div>
</div>
</Modal>
</div>
)
}