2026-10-16 09:31:45 -06:00
|
|
|
// Packet endpoints from the phase 2 contract.
|
|
|
|
|
import { api, request } from "./client";
|
|
|
|
|
import { asList, ifMatch, qs } from "./query";
|
|
|
|
|
import type {
|
|
|
|
|
AttachPagesBody,
|
|
|
|
|
BomLine,
|
2026-10-16 17:13:31 -06:00
|
|
|
BomPatch,
|
2026-10-16 22:46:56 -06:00
|
|
|
DeptPerson,
|
2026-10-16 09:31:45 -06:00
|
|
|
FieldCas,
|
|
|
|
|
Guarded,
|
2026-10-16 22:46:56 -06:00
|
|
|
HeatTableBody,
|
|
|
|
|
HeatTagBody,
|
2026-10-16 09:31:45 -06:00
|
|
|
LogRow,
|
|
|
|
|
PacketCard,
|
|
|
|
|
PacketCreate,
|
|
|
|
|
PacketHeader,
|
|
|
|
|
PacketListQuery,
|
|
|
|
|
PacketRead,
|
2026-10-17 20:08:53 -06:00
|
|
|
QcFields,
|
2026-10-16 17:13:31 -06:00
|
|
|
ReceiptBody,
|
|
|
|
|
ShortBody,
|
2026-10-16 22:46:56 -06:00
|
|
|
StageFields,
|
2026-10-16 09:31:45 -06:00
|
|
|
TransitionBody,
|
2026-10-16 22:46:56 -06:00
|
|
|
WeldBody,
|
2026-10-16 17:13:31 -06:00
|
|
|
WhStageBody,
|
2026-10-16 09:31:45 -06:00
|
|
|
WorkOrder,
|
|
|
|
|
} from "./types";
|
|
|
|
|
|
|
|
|
|
const base = (id: number) => `/api/packets/${encodeURIComponent(String(id))}`;
|
|
|
|
|
|
|
|
|
|
export const packetsApi = {
|
|
|
|
|
list: async (q: PacketListQuery = {}, signal?: AbortSignal): Promise<PacketCard[]> =>
|
|
|
|
|
asList<PacketCard>(await api.get<unknown>(`/api/packets${qs({ limit: 500, ...q })}`, signal), "packets"),
|
|
|
|
|
get: (id: number, signal?: AbortSignal) => api.get<PacketRead>(base(id), signal),
|
|
|
|
|
log: async (id: number, opts: { limit?: number; before?: number } = {}, signal?: AbortSignal): Promise<LogRow[]> =>
|
|
|
|
|
asList<LogRow>(await api.get<unknown>(`${base(id)}/log${qs({ limit: opts.limit ?? 300, before: opts.before })}`, signal), "log"),
|
|
|
|
|
create: (body: PacketCreate) => api.post<PacketRead>("/api/packets", body),
|
|
|
|
|
patch: (id: number, body: FieldCas<PacketHeader>) => api.patch<PacketRead>(base(id), body),
|
|
|
|
|
transition: (id: number, body: TransitionBody) => api.post<PacketRead>(`${base(id)}/transitions`, body),
|
|
|
|
|
putWorkOrders: (id: number, version: number, work_orders: WorkOrder[], extra: Guarded = {}) =>
|
|
|
|
|
request<PacketRead>(`${base(id)}/work-orders`, { method: "PUT", body: { work_orders, ...extra }, headers: ifMatch(version) }),
|
|
|
|
|
putBom: (id: number, version: number, bom: BomLine[], extra: Guarded = {}) =>
|
|
|
|
|
request<PacketRead>(`${base(id)}/bom`, { method: "PUT", body: { bom, ...extra }, headers: ifMatch(version) }),
|
|
|
|
|
attachPages: (id: number, body: AttachPagesBody) => api.post<PacketRead>(`${base(id)}/pages`, body),
|
|
|
|
|
removePages: (id: number, n: number, body: { reason?: string } & Guarded = {}) =>
|
|
|
|
|
api.delWithBody<PacketRead>(`${base(id)}/pages/${n}`, body),
|
2026-10-17 20:08:53 -06:00
|
|
|
/** Phase 6: kind from the closed set; the title defaults to the kind label on the server. */
|
|
|
|
|
addDoc: (id: number, body: { asset_id: string; kind: string; title?: string; note?: string } & Guarded) =>
|
2026-10-16 09:31:45 -06:00
|
|
|
api.post<PacketRead>(`${base(id)}/docs`, body),
|
2026-10-17 20:08:53 -06:00
|
|
|
/** Takes a document out of the data book. The row stays, with the reason. */
|
|
|
|
|
removeDoc: (id: number, docId: number, body: { reason: string } & Guarded) =>
|
|
|
|
|
api.delWithBody<PacketRead>(`${base(id)}/docs/${docId}`, body),
|
2026-10-16 09:31:45 -06:00
|
|
|
move: (id: number, body: { unit_id: number; reason?: string } & Guarded) => api.post<PacketRead>(`${base(id)}/move`, body),
|
|
|
|
|
remove: (id: number, body: { reason: string } & Guarded) => api.delWithBody<PacketRead | null>(base(id), body),
|
|
|
|
|
setVclosed: (id: number, closed: boolean, extra: Guarded = {}) =>
|
|
|
|
|
api.post<PacketRead>(`${base(id)}/vclosed`, { closed, ...extra }),
|
|
|
|
|
databook: (id: number) => api.post<PacketRead>(`${base(id)}/databook`, {}),
|
2026-10-16 14:45:38 -06:00
|
|
|
|
|
|
|
|
/* Phase 3: VISUAL packets. */
|
|
|
|
|
revApprove: (id: number, body: { reason: string } & Guarded) => api.post<PacketRead>(`${base(id)}/rev-approve`, body),
|
|
|
|
|
handDrawing: (id: number, body: { asset_id: string; reason: string } & Guarded) =>
|
|
|
|
|
api.post<PacketRead>(`${base(id)}/hand/drawing`, body),
|
|
|
|
|
/** Multipart cannot carry JSON, so the signature rides as sign_factor and sign_secret. */
|
|
|
|
|
handBom: (id: number, file: File, reason: string, extra: Guarded = {}) => {
|
|
|
|
|
const fd = new FormData();
|
|
|
|
|
fd.append("file", file, file.name);
|
|
|
|
|
fd.append("reason", reason);
|
|
|
|
|
if (extra.sign) {
|
|
|
|
|
fd.append("sign_factor", extra.sign.factor);
|
|
|
|
|
fd.append("sign_secret", extra.sign.secret);
|
|
|
|
|
}
|
|
|
|
|
if (extra.it_reason) fd.append("it_reason", extra.it_reason);
|
|
|
|
|
return request<PacketRead>(`${base(id)}/hand/bom`, { method: "POST", body: fd });
|
|
|
|
|
},
|
|
|
|
|
removeHandBom: (id: number, extra: Guarded = {}) => api.delWithBody<PacketRead>(`${base(id)}/hand/bom`, extra),
|
|
|
|
|
|
|
|
|
|
/* Change orders. */
|
|
|
|
|
coApprove: (id: number, no: number, extra: Guarded = {}) => api.post<PacketRead>(`${base(id)}/change-orders/${no}/approve`, extra),
|
|
|
|
|
coReject: (id: number, no: number, body: { reason: string } & Guarded) =>
|
|
|
|
|
api.post<PacketRead>(`${base(id)}/change-orders/${no}/reject`, body),
|
|
|
|
|
coHold: (id: number, no: number, hold: boolean, extra: Guarded = {}) =>
|
|
|
|
|
api.post<PacketRead>(`${base(id)}/change-orders/${no}/hold`, { hold, ...extra }),
|
2026-10-16 17:13:31 -06:00
|
|
|
|
|
|
|
|
/* Phase 4: the warehouse. */
|
|
|
|
|
/** One BOM line. If-Match is the line's row_version. */
|
|
|
|
|
patchBomLine: (id: number, item: number, rowVersion: number, body: BomPatch, g: Guarded = {}) =>
|
|
|
|
|
request<PacketRead>(`${base(id)}/bom/${item}`, { method: "PATCH", body: { ...body, ...g }, headers: ifMatch(rowVersion) }),
|
|
|
|
|
/** Opens the short on a line, or updates the open one. expect_open says which the caller saw. */
|
|
|
|
|
saveShort: (id: number, item: number, body: ShortBody, g: Guarded = {}) =>
|
|
|
|
|
api.post<PacketRead>(`${base(id)}/bom/${item}/short`, { ...body, ...g }),
|
|
|
|
|
/** "Not short after all": closes the open short. */
|
|
|
|
|
clearShort: (id: number, item: number, body: { expect_open: boolean }, g: Guarded = {}) =>
|
|
|
|
|
api.delWithBody<PacketRead>(`${base(id)}/bom/${item}/short`, { ...body, ...g }),
|
|
|
|
|
receiveShort: (id: number, item: number, body: ReceiptBody, g: Guarded = {}) =>
|
|
|
|
|
api.post<PacketRead>(`${base(id)}/bom/${item}/short/receipts`, { ...body, ...g }),
|
|
|
|
|
setWhStage: (id: number, body: WhStageBody, g: Guarded = {}) => api.post<PacketRead>(`${base(id)}/wh`, { ...body, ...g }),
|
2026-10-16 22:46:56 -06:00
|
|
|
|
|
|
|
|
/* Phase 5: the shop. Every answer is the packet. */
|
|
|
|
|
addWeld: (id: number, body: WeldBody, g: Guarded = {}) => api.post<PacketRead>(`${base(id)}/welds`, { ...body, ...g }),
|
|
|
|
|
/** If-Match is the weld's row_version. */
|
|
|
|
|
patchWeld: (id: number, no: number, rowVersion: number, body: WeldBody, g: Guarded = {}) =>
|
|
|
|
|
request<PacketRead>(`${base(id)}/welds/${no}`, { method: "PATCH", body: { ...body, ...g }, headers: ifMatch(rowVersion) }),
|
|
|
|
|
removeWeld: (id: number, no: number, rowVersion: number, body: { reason?: string } & Guarded = {}) =>
|
|
|
|
|
request<PacketRead>(`${base(id)}/welds/${no}`, { method: "DELETE", body, headers: ifMatch(rowVersion) }),
|
|
|
|
|
addHeatTag: (id: number, body: HeatTagBody, g: Guarded = {}) => api.post<PacketRead>(`${base(id)}/heat-tags`, { ...body, ...g }),
|
|
|
|
|
patchHeatTag: (id: number, no: number, rowVersion: number, body: HeatTagBody, g: Guarded = {}) =>
|
|
|
|
|
request<PacketRead>(`${base(id)}/heat-tags/${no}`, { method: "PATCH", body: { ...body, ...g }, headers: ifMatch(rowVersion) }),
|
|
|
|
|
removeHeatTag: (id: number, no: number, rowVersion: number, body: { reason?: string } & Guarded = {}) =>
|
|
|
|
|
request<PacketRead>(`${base(id)}/heat-tags/${no}`, { method: "DELETE", body, headers: ifMatch(rowVersion) }),
|
|
|
|
|
putHeatTable: (id: number, page: number, body: HeatTableBody, g: Guarded = {}) =>
|
|
|
|
|
api.put<PacketRead>(`${base(id)}/heat-table/${page}`, { ...body, ...g }),
|
|
|
|
|
removeHeatTable: (id: number, page: number, g: Guarded = {}) => api.delWithBody<PacketRead>(`${base(id)}/heat-table/${page}`, g),
|
|
|
|
|
/** The weld record fields with a field-level base: 409 field_conflict when someone changed one first. */
|
|
|
|
|
patchStage: (id: number, body: FieldCas<StageFields>) => api.patch<PacketRead>(`${base(id)}/stage`, body),
|
|
|
|
|
setChecklist: (id: number, key: string, done: boolean, g: Guarded = {}) =>
|
|
|
|
|
api.put<PacketRead>(`${base(id)}/stage/checklist/${encodeURIComponent(key)}`, { done, ...g }),
|
|
|
|
|
clock: (id: number, op: "start" | "pause" | "resume", g: Guarded = {}) => api.post<PacketRead>(`${base(id)}/stage/clock`, { op, ...g }),
|
|
|
|
|
coVerify: (id: number, no: number, body: { dept: string; note?: string } & Guarded) =>
|
|
|
|
|
api.post<PacketRead>(`${base(id)}/change-orders/${no}/verify`, body),
|
2026-10-17 20:08:53 -06:00
|
|
|
|
|
|
|
|
/* Phase 6: the Q.C. inspection. Every answer is the packet. */
|
|
|
|
|
/** One check of the open inspection. */
|
|
|
|
|
qcCheck: (id: number, key: string, done: boolean, g: Guarded = {}) =>
|
|
|
|
|
api.put<PacketRead>(`${base(id)}/qc/checks/${encodeURIComponent(key)}`, { done, ...g }),
|
|
|
|
|
/** The visual exam of one weld. result "" clears it. */
|
|
|
|
|
qcWeld: (id: number, no: number, body: { result?: "accept" | "reject" | ""; note?: string }, g: Guarded = {}) =>
|
|
|
|
|
api.put<PacketRead>(`${base(id)}/qc/welds/${no}`, { ...body, ...g }),
|
|
|
|
|
/** The scalar inspection fields with a field-level base: 409 field_conflict when someone changed one first. */
|
|
|
|
|
qcPatch: (id: number, body: FieldCas<QcFields>) => api.patch<PacketRead>(`${base(id)}/qc`, body),
|
2026-10-16 22:46:56 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** GET /api/departments/{code}/people: the welder picker and the weld sheet's stamp. */
|
|
|
|
|
export const peopleApi = {
|
|
|
|
|
list: async (dept: string, signal?: AbortSignal): Promise<DeptPerson[]> =>
|
|
|
|
|
asList<DeptPerson>(await api.get<unknown>(`/api/departments/${encodeURIComponent(dept)}/people`, signal), "people"),
|
2026-10-16 09:31:45 -06:00
|
|
|
};
|