Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/lib/teaming.ts |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/lib/teaming.ts |
| Original SHA-256 | 319097f1f126c08a6309fa328bc778f9a3aec0900d729191007a648c15724711 |
| Git blob SHA | ac3a638eb4a434a9f40ded8e3e9017eaf89e8f05 |
| Reading-copy SHA-256 | 319097f1f126c08a6309fa328bc778f9a3aec0900d729191007a648c15724711 |
Recovered source transcript
Download complete source text ↓142 lines · 4.8 KB
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { request } from './api'
// TEAMING PARTNERS AND KEY PERSONNEL COMMITMENTS.
//
// The gap this fills is market wide and named: subcontractor tooling forces a choice between managing
// teaming DURING capture and tracking compliance AFTER award, so partner data ends up scattered across a
// capture CRM, a proposal tool and a post award ERP. missionMATCH already tracks an opportunity across that
// whole span, which is why the partner belongs on the capture record rather than the proposal.
export interface TeamingPartner {
id: string
opportunity_id: string
company_name: string
partner_role: 'prime' | 'subcontractor' | 'joint_venture'
status: 'identified' | 'in_discussion' | 'agreement_signed' | 'declined'
uei?: string
cage_code?: string
set_asides: string[]
scope_summary?: string
agreement_signed_on?: string
commitments: number
created_at: string
updated_at: string
}
export interface KeyPersonnelCommitment {
id: string
requirement_id: string
requirement_title?: string
candidate_id?: string
candidate_name?: string
teaming_partner_id?: string
partner_company?: string
partner_person_name?: string
role_title?: string
commitment_type: 'letter_of_intent' | 'contingent_offer' | 'employee' | 'partner_provided'
status: 'proposed' | 'committed' | 'withdrawn' | 'superseded'
signed_on?: string
expires_on?: string
notes?: string
days_until_expiry?: number
// lapsed is the fact a bid review most needs and the one nobody notices: a letter of intent signed for a
// bid that slipped six months is not a commitment any more.
lapsed?: boolean
}
export const PARTNER_ROLES = [
{ value: 'prime', label: 'Prime' },
{ value: 'subcontractor', label: 'Subcontractor' },
{ value: 'joint_venture', label: 'Joint venture' },
]
export const PARTNER_STATUSES = [
{ value: 'identified', label: 'Identified' },
{ value: 'in_discussion', label: 'In discussion' },
{ value: 'agreement_signed', label: 'Agreement signed' },
{ value: 'declined', label: 'Declined' },
]
export const COMMITMENT_TYPES = [
{ value: 'letter_of_intent', label: 'Letter of intent' },
{ value: 'contingent_offer', label: 'Contingent offer' },
{ value: 'employee', label: 'Already an employee' },
{ value: 'partner_provided', label: 'Provided by a partner' },
]
export function partnerColor(status: string): string {
if (status === 'agreement_signed') return 'var(--color-ok)'
if (status === 'in_discussion') return 'var(--color-warn)'
if (status === 'declined') return 'var(--color-risk)'
return 'var(--color-muted)'
}
export function useTeamingPartners(oppID: string) {
return useQuery({
queryKey: ['capture', oppID, 'partners'],
queryFn: () =>
request<{ partners: TeamingPartner[]; count: number }>(
`/opportunities/tracked/${encodeURIComponent(oppID)}/partners`,
),
enabled: !!oppID,
})
}
export function useAddPartner(oppID: string) {
const qc = useQueryClient()
return useMutation({
mutationFn: (v: Partial<TeamingPartner>) =>
request<{ id: string }>(`/opportunities/tracked/${encodeURIComponent(oppID)}/partners`, {
method: 'POST',
body: JSON.stringify(v),
}),
onSuccess: () => void qc.invalidateQueries({ queryKey: ['capture', oppID] }),
})
}
export function useUpdatePartner(oppID: string) {
const qc = useQueryClient()
return useMutation({
mutationFn: (v: { id: string } & Partial<TeamingPartner>) =>
request<{ updated: boolean }>(`/teaming-partners/${encodeURIComponent(v.id)}`, {
method: 'PATCH',
body: JSON.stringify(v),
}),
onSuccess: () => void qc.invalidateQueries({ queryKey: ['capture', oppID] }),
})
}
export function useKeyPersonnel(oppID: string) {
return useQuery({
queryKey: ['capture', oppID, 'key-personnel'],
queryFn: () =>
request<{ commitments: KeyPersonnelCommitment[]; count: number; lapsed: number }>(
`/opportunities/tracked/${encodeURIComponent(oppID)}/key-personnel`,
),
enabled: !!oppID,
})
}
export function useAddCommitment(oppID: string) {
const qc = useQueryClient()
return useMutation({
mutationFn: (v: Record<string, unknown>) =>
request<{ id: string }>('/key-personnel', { method: 'POST', body: JSON.stringify(v) }),
onSuccess: () => void qc.invalidateQueries({ queryKey: ['capture', oppID] }),
})
}
export function useUpdateCommitment(oppID: string) {
const qc = useQueryClient()
return useMutation({
mutationFn: (v: { id: string } & Record<string, unknown>) =>
request<{ updated: boolean }>(`/key-personnel/${encodeURIComponent(v.id)}`, {
method: 'PATCH',
body: JSON.stringify(v),
}),
onSuccess: () => void qc.invalidateQueries({ queryKey: ['capture', oppID] }),
})
}