Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/screens/TeamingPanels.tsx |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/screens/TeamingPanels.tsx |
| Original SHA-256 | 209f58a872920f0f3a645db74237b879177b22878b2248e1cac19a9424bbaf5c |
| Git blob SHA | b3381257e9f10dd9fd9bc91d3e6b814f15e88b7b |
| Reading-copy SHA-256 | 209f58a872920f0f3a645db74237b879177b22878b2248e1cac19a9424bbaf5c |
Recovered source transcript
Download complete source text ↓340 lines · 13.7 KB
import { useState } from 'react'
import { AlertTriangle, Building2, FileSignature, UserCheck } from 'lucide-react'
import { Badge, Panel } from '../components/ui'
import { Empty, Labeled, Note, Select, btnGhost, btnPrimary, fieldCls } from '../components/form'
import {
COMMITMENT_TYPES,
PARTNER_ROLES,
PARTNER_STATUSES,
partnerColor,
useAddCommitment,
useAddPartner,
useKeyPersonnel,
useTeamingPartners,
useUpdateCommitment,
useUpdatePartner,
} from '../lib/teaming'
// TEAMING AND KEY PERSONNEL, on the capture record because that is where the decisions are made.
//
// A teaming agreement is negotiated while a pursuit is being shaped, months before a proposal exists. A key
// person is named in a bid and evaluated by the customer, and if they walk between submission and award the
// score walks with them. Both were invisible to this product until now.
export function TeamingPanel({ opportunityId }: { opportunityId: string }) {
const q = useTeamingPartners(opportunityId)
const add = useAddPartner(opportunityId)
const update = useUpdatePartner(opportunityId)
const [name, setName] = useState('')
const [role, setRole] = useState('subcontractor')
const partners = q.data?.partners ?? []
const unsigned = partners.filter((p) => p.status !== 'agreement_signed' && p.status !== 'declined')
return (
<Panel title="Teaming" hint={partners.length === 1 ? '1 partner' : `${partners.length} partners`}>
{q.isError ? (
<Note tone="risk">{(q.error as Error).message}</Note>
) : partners.length === 0 ? (
<Empty
title="No partners recorded"
reason="Who you bid with is decided during capture, months before a proposal exists. This is where it is recorded so it is still known at the bid review."
/>
) : (
<ul className="space-y-2">
{partners.map((p) => (
<li
key={p.id}
className="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-line/60 bg-navy-950/30 px-3 py-2"
>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<Building2 size={13} className="text-muted" />
<span className="text-sm font-semibold text-ink">{p.company_name}</span>
<Badge color={partnerColor(p.status)} subtle>
{PARTNER_STATUSES.find((s) => s.value === p.status)?.label ?? p.status}
</Badge>
<span className="text-[11px] text-faint">
{PARTNER_ROLES.find((r) => r.value === p.partner_role)?.label}
</span>
</div>
<div className="text-[11px] text-faint">
{p.agreement_signed_on ? `signed ${p.agreement_signed_on}` : 'no agreement on file'}
{p.commitments > 0 &&
` · providing ${p.commitments === 1 ? '1 key person' : `${p.commitments} key people`}`}
{p.uei && ` · UEI ${p.uei}`}
</div>
</div>
<Select
value={p.status}
choices={PARTNER_STATUSES}
onChange={(v) =>
update.mutate({
id: p.id,
status: v as never,
// A signed status must carry a date, which the database enforces. Supplying today is the
// honest default for somebody ticking it the day it happened, and the field on the record
// is what corrects it if it was signed earlier.
agreement_signed_on:
v === 'agreement_signed' && !p.agreement_signed_on
? new Date().toISOString().slice(0, 10)
: p.agreement_signed_on,
})
}
className="text-xs"
/>
</li>
))}
</ul>
)}
{unsigned.length > 0 && (
<div className="mt-3">
<Note tone="warn">
{unsigned.length === 1 ? '1 partner has' : `${unsigned.length} partners have`} no signed agreement.
At a bid review an unsigned partner is a plan rather than a commitment.
</Note>
</div>
)}
<form
className="mt-3 flex flex-wrap items-end gap-2 border-t border-line/40 pt-3"
onSubmit={(e) => {
e.preventDefault()
if (!name.trim()) return
add.mutate(
{ company_name: name.trim(), partner_role: role as never },
{ onSuccess: () => setName('') },
)
}}
>
<Labeled label="Add a partner">
<input
value={name}
maxLength={200}
placeholder="Company name"
onChange={(e) => setName(e.target.value)}
className={`${fieldCls} max-w-xs`}
/>
</Labeled>
<Select value={role} choices={PARTNER_ROLES} onChange={setRole} />
<button type="submit" disabled={!name.trim() || add.isPending} className={btnPrimary}>
Add
</button>
</form>
{add.isError && (
<div className="mt-2">
<Note tone="risk">{(add.error as Error).message}</Note>
</div>
)}
</Panel>
)
}
export function KeyPersonnelPanel({ opportunityId }: { opportunityId: string }) {
const q = useKeyPersonnel(opportunityId)
const partners = useTeamingPartners(opportunityId)
const add = useAddCommitment(opportunityId)
const update = useUpdateCommitment(opportunityId)
const [open, setOpen] = useState(false)
const [form, setForm] = useState({
requirement_id: '',
candidate_id: '',
teaming_partner_id: '',
partner_person_name: '',
role_title: '',
commitment_type: 'letter_of_intent',
signed_on: '',
expires_on: '',
})
const list = q.data?.commitments ?? []
const lapsed = q.data?.lapsed ?? 0
return (
<Panel title="Key personnel" hint={list.length === 1 ? '1 commitment' : `${list.length} commitments`}>
<p className="pb-2 text-[11px] leading-relaxed text-faint">
A key person is named in the bid and evaluated by the customer. If they walk between submission and
award the score walks with them, so what holds them is a letter of intent or a contingent offer, and
both expire.
</p>
{/* THE LAPSED COUNT IS THE HEADLINE. It is the thing a bid review opens this panel to find and the one
nobody notices on their own: a letter signed for a bid that slipped six months is not a commitment. */}
{lapsed > 0 && (
<div className="mb-3">
<Note tone="risk">
<AlertTriangle size={11} className="mr-1 inline" />
{lapsed === 1 ? '1 commitment has' : `${lapsed} commitments have`} lapsed. The person is named in
the bid and nothing currently holds them.
</Note>
</div>
)}
{q.isError ? (
<Note tone="risk">{(q.error as Error).message}</Note>
) : list.length === 0 ? (
<Empty
title="Nobody committed yet"
reason="Record who is named against each evaluated position and what holds them there."
/>
) : (
<ul className="space-y-2">
{list.map((k) => (
<li
key={k.id}
className={`flex flex-wrap items-center justify-between gap-3 rounded-lg border px-3 py-2 ${
k.lapsed ? 'border-risk/40 bg-risk/5' : 'border-line/60 bg-navy-950/30'
}`}
>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2 text-sm">
<UserCheck size={13} className="text-muted" />
<span className="font-semibold text-ink">
{k.candidate_name || k.partner_person_name || 'Unnamed'}
</span>
{k.partner_company && (
<span className="text-[11px] text-faint">via {k.partner_company}</span>
)}
<Badge color={k.lapsed ? 'var(--color-risk)' : 'var(--color-blue)'} subtle>
{COMMITMENT_TYPES.find((c) => c.value === k.commitment_type)?.label ?? k.commitment_type}
</Badge>
</div>
<div className="text-[11px] text-faint">
{k.requirement_title}
{k.role_title ? ` · ${k.role_title}` : ''}
{k.expires_on &&
(k.lapsed
? ` · LAPSED ${k.expires_on}`
: ` · holds until ${k.expires_on}${
k.days_until_expiry !== undefined ? `, ${k.days_until_expiry} days` : ''
}`)}
</div>
</div>
<Select
value={k.status}
choices={[
{ value: 'proposed', label: 'Proposed' },
{ value: 'committed', label: 'Committed' },
{ value: 'withdrawn', label: 'Withdrawn' },
{ value: 'superseded', label: 'Superseded' },
]}
onChange={(v) =>
update.mutate({
id: k.id,
status: v,
signed_on:
v === 'committed' && !k.signed_on ? new Date().toISOString().slice(0, 10) : k.signed_on,
})
}
className="text-xs"
/>
</li>
))}
</ul>
)}
<div className="mt-3 border-t border-line/40 pt-3">
{!open ? (
<button type="button" className={btnGhost} onClick={() => setOpen(true)}>
<FileSignature size={13} /> Commit somebody to a position
</button>
) : (
<form
className="space-y-2"
onSubmit={(e) => {
e.preventDefault()
add.mutate(form, { onSuccess: () => setOpen(false) })
}}
>
<div className="grid gap-2 sm:grid-cols-2">
<Labeled label="Position" hint="the evaluated requirement they are named against">
<input
value={form.requirement_id}
placeholder="requirement id"
onChange={(e) => setForm({ ...form, requirement_id: e.target.value })}
className={fieldCls}
/>
</Labeled>
<Labeled label="Role title">
<input
value={form.role_title}
maxLength={200}
placeholder="Program Manager"
onChange={(e) => setForm({ ...form, role_title: e.target.value })}
className={fieldCls}
/>
</Labeled>
{/* EITHER our candidate OR a partner's person, never both. The database refuses a row naming
neither or both, and saying so here means the refusal is rare rather than routine. */}
<Labeled label="One of your candidates" hint="leave empty if a partner provides them">
<input
value={form.candidate_id}
placeholder="candidate id"
onChange={(e) => setForm({ ...form, candidate_id: e.target.value, teaming_partner_id: '', partner_person_name: '' })}
className={fieldCls}
/>
</Labeled>
<Labeled label="Or a partner provides them">
<Select
value={form.teaming_partner_id}
choices={[
{ value: '', label: 'Not a partner' },
...(partners.data?.partners ?? []).map((p) => ({ value: p.id, label: p.company_name })),
]}
onChange={(v) => setForm({ ...form, teaming_partner_id: v, candidate_id: '' })}
className="w-full"
/>
</Labeled>
{form.teaming_partner_id && (
<Labeled label="Their name">
<input
value={form.partner_person_name}
maxLength={200}
onChange={(e) => setForm({ ...form, partner_person_name: e.target.value })}
className={fieldCls}
/>
</Labeled>
)}
<Labeled label="What holds them">
<Select
value={form.commitment_type}
choices={COMMITMENT_TYPES}
onChange={(v) => setForm({ ...form, commitment_type: v })}
className="w-full"
/>
</Labeled>
<Labeled label="Signed">
<input
type="date"
value={form.signed_on}
onChange={(e) => setForm({ ...form, signed_on: e.target.value })}
className={fieldCls}
/>
</Labeled>
<Labeled label="Holds until" hint="after this it is not a commitment any more">
<input
type="date"
value={form.expires_on}
onChange={(e) => setForm({ ...form, expires_on: e.target.value })}
className={fieldCls}
/>
</Labeled>
</div>
<div className="flex gap-2">
<button type="submit" disabled={add.isPending} className={btnPrimary}>
{add.isPending ? 'Saving' : 'Record it'}
</button>
<button type="button" className={btnGhost} onClick={() => setOpen(false)}>
Cancel
</button>
</div>
{add.isError && <Note tone="risk">{(add.error as Error).message}</Note>}
</form>
)}
</div>
</Panel>
)
}