Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/tools/slatematch/scopeApi.ts |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/tools/slatematch/scopeApi.ts |
| Original SHA-256 | 246de7ff65eee15ab0b9c1f8f0f93ec012be43746f42d41863a40e76a9975779 |
| Git blob SHA | 20df8ff386d82f61dbed814ef35ddaedfb0da4b3 |
| Reading-copy SHA-256 | 246de7ff65eee15ab0b9c1f8f0f93ec012be43746f42d41863a40e76a9975779 |
Recovered source transcript
Download complete source text ↓171 lines · 5.9 KB
import { csrfForUpload, request } from '../../lib/api'
// The Phase 2 review path: documents in, scope reviewed, drafts built, one named human promotes. Shapes are
// snake_case to match the Go DTOs exactly, so a field rename on either side fails loudly rather than silently
// rendering undefined.
export interface OpportunityDocument {
id: string
document_type: string
file_name: string
media_type: string
size_bytes: number
sha256: string
extraction_status: string
extraction_error_code: string
is_current: boolean
provenance: string
created_at: string
}
export interface ScopeItem {
id: string
opportunity_id: string
scope_type: string
title: string
description: string
source_document_id: string
source_page_number: number
source_section_label: string
source_quote_hash: string
extraction_method: string
extraction_confidence: number
review_status: string
display_order: number
created_at: string
}
export interface RequirementDraft {
id: string
labor_category: string
quantity: number
clearance_level: string
polygraph: string
minimum_years_experience: number
work_location: string
remote_allowed: boolean
required_skills: string[] | null
review_status: string
approved_requirement_id: string
derivation_method: string
}
const NS = '/opportunities/tracked'
const body = (v: unknown) => ({ body: JSON.stringify(v) })
// UPLOAD USES XMLHttpRequest, NOT fetch, for exactly one reason: Section 6 screen 3 requires upload PROGRESS
// and fetch cannot report it. A solicitation package is tens of megabytes, so a progressless upload looks
// indistinguishable from a hung one, which is how a user learns to distrust a screen.
export function uploadDocument(
opportunityId: string,
file: File,
documentType: string,
onProgress: (percent: number) => void,
): Promise<OpportunityDocument> {
return csrfForUpload().then(
(token) =>
new Promise<OpportunityDocument>((resolve, reject) => {
const form = new FormData()
form.append('file', file)
form.append('document_type', documentType)
const xhr = new XMLHttpRequest()
xhr.open('POST', `/api/opportunities/${encodeURIComponent(opportunityId)}/documents`)
xhr.withCredentials = true
xhr.setRequestHeader('X-CSRF-Token', token)
// The boundary is generated by the browser, so Content-Type is deliberately NOT set here.
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) onProgress(Math.round((e.loaded / e.total) * 100))
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
resolve(JSON.parse(xhr.responseText) as OpportunityDocument)
} catch {
reject(new Error('the server accepted the upload but returned an unreadable body'))
}
return
}
// A 413 is the one failure a user can act on, and it is worth naming rather than showing a number.
if (xhr.status === 413) {
reject(new Error('this file is larger than the server accepts'))
return
}
let detail = `upload failed with status ${xhr.status}`
try {
const parsed = JSON.parse(xhr.responseText) as { error?: string }
if (parsed.error) detail = parsed.error
} catch {
/* a non JSON error body keeps the status line above */
}
reject(new Error(detail))
}
xhr.onerror = () => reject(new Error('the upload could not reach the server'))
xhr.onabort = () => reject(new Error('the upload was cancelled'))
xhr.send(form)
}),
)
}
export const scopeApi = {
documents: (oppId: string) =>
request<{ documents: OpportunityDocument[] }>(`/opportunities/${encodeURIComponent(oppId)}/documents`),
scope: (oppId: string, status?: string) => {
const qs = status ? `?status=${encodeURIComponent(status)}` : ''
return request<ScopeItem[]>(`${NS}/${encodeURIComponent(oppId)}/scope${qs}`)
},
review: (oppId: string, itemId: string, v: Record<string, unknown>) =>
request<ScopeItem>(`${NS}/${encodeURIComponent(oppId)}/scope/items/${encodeURIComponent(itemId)}`, {
method: 'PATCH',
...body(v),
}),
addManual: (oppId: string, v: { scope_type: string; title: string; description: string }) =>
request<ScopeItem>(`${NS}/${encodeURIComponent(oppId)}/scope/items`, { method: 'POST', ...body(v) }),
split: (oppId: string, itemId: string, parts: Array<{ scope_type?: string; title: string; description?: string }>) =>
request<{ children: ScopeItem[] }>(
`${NS}/${encodeURIComponent(oppId)}/scope/items/${encodeURIComponent(itemId)}/split`,
{ method: 'POST', ...body({ parts }) },
),
merge: (
oppId: string,
itemId: string,
v: { absorb_item_ids: string[]; title: string; description?: string; scope_type?: string },
) =>
request<ScopeItem>(`${NS}/${encodeURIComponent(oppId)}/scope/items/${encodeURIComponent(itemId)}/merge`, {
method: 'POST',
...body(v),
}),
drafts: (oppId: string) => request<RequirementDraft[]>(`${NS}/${encodeURIComponent(oppId)}/requirement-drafts`),
generateDrafts: (oppId: string) =>
request<{ created: number; skipped_existing: number }>(
`${NS}/${encodeURIComponent(oppId)}/requirement-drafts/generate`,
{ method: 'POST', body: '{}' },
),
patchDraft: (draftId: string, v: Record<string, unknown>) =>
request<Record<string, unknown>>(`/requirement-drafts/${encodeURIComponent(draftId)}`, {
method: 'PATCH',
...body(v),
}),
approveDraft: (draftId: string) =>
request<Record<string, unknown>>(`/requirement-drafts/${encodeURIComponent(draftId)}/approve`, {
method: 'POST',
body: '{}',
}),
rejectDraft: (draftId: string, reason: string) =>
request<Record<string, unknown>>(`/requirement-drafts/${encodeURIComponent(draftId)}/reject`, {
method: 'POST',
...body({ reason }),
}),
}