// The shop board, the stall and the department people (phase 5 contract, api/shop.py and api/departments.py). // These types belong to the Supervisor and Stall screens. The packet page types stay in types.ts. import { api } from "./client"; import { asList, qs } from "./query"; import type { PacketCard } from "./types"; export interface ShopClock { /** Live on the server at the time of the answer: stored plus the running time. */ elapsed_ms: number; running: boolean; } export interface ShopCo { label: string; /** "pending" while Clerical reviews a change from VISUAL, "approved" once it applies. */ review: string; } /** A card on the shop board: the phase 4 card plus the station, welder, clock and open change orders. */ export interface ShopCard extends Omit { welder_stamp?: string | null; clock?: ShopClock | null; target_hrs?: number | null; open_cos: ShopCo[]; return_note?: string | null; } export interface ShopStation { id: number; name: string; packets: ShopCard[]; } export type BoardLevel = "lead" | "supervisor" | "admin"; export interface ShopDept { code: string; name: string; weld: boolean; level: BoardLevel; stations: ShopStation[]; } export interface ShopKpis { ready: number; at_stations: number; review: number; elapsed_ms: number; target_hrs: number; } /** One open change order on a packet in scope, or one waiting on a department of mine. */ export interface ShopCoRow { packet_id: number; unit_number: string; op: string | null; title: string | null; dept_code: string; co_no: number; label: string; review: string; hold: boolean; pending_depts: string[]; /** I can verify one of the pending departments. */ mine: boolean; reason: string | null; ecn: string | null; issued_at: string | null; } export const SHOP_COLUMNS = ["ready", "review", "qc", "warehouse", "complete"] as const; export type ShopColumn = (typeof SHOP_COLUMNS)[number]; export interface ShopBoard { depts: ShopDept[]; kpis: ShopKpis; cos: ShopCoRow[]; columns: Record; } export interface ShopBoardQuery { /** A department code, or "all". */ dept?: string; project_id?: number; unit_id?: number; } export interface StationOption { dept_code: string; dept_name: string; id: number; name: string; } export interface StallAnswer { station: { id: number; name: string; dept_code: string; dept_name: string }; mine: ShopCard[]; submitted: ShopCard[]; } export interface DeptPerson { id: string; display_name: string; stamp: string | null; level: "worker" | "lead" | "supervisor"; station_limits: number[]; } /** A card with every list present, so the screens never trip on a missing field. */ export function normaliseCard(raw: ShopCard): ShopCard { return { ...raw, open_cos: Array.isArray(raw.open_cos) ? raw.open_cos : [] }; } function cards(raw: unknown): ShopCard[] { return Array.isArray(raw) ? (raw as ShopCard[]).map(normaliseCard) : []; } /** Every department, column and number present, even when the server leaves one out. */ export function normaliseBoard(raw: Partial | null): ShopBoard { const cols = (raw?.columns ?? {}) as Partial>; const columns = Object.fromEntries(SHOP_COLUMNS.map((c) => [c, cards(cols[c])])) as ShopBoard["columns"]; const depts = (Array.isArray(raw?.depts) ? raw.depts : []).map((d) => ({ ...d, stations: (Array.isArray(d.stations) ? d.stations : []).map((s) => ({ ...s, packets: cards(s.packets) })), })); return { depts, kpis: { ready: 0, at_stations: 0, review: 0, elapsed_ms: 0, target_hrs: 0, ...(raw?.kpis ?? {}) }, cos: Array.isArray(raw?.cos) ? raw.cos.map((c) => ({ ...c, pending_depts: Array.isArray(c.pending_depts) ? c.pending_depts : [] })) : [], columns, }; } export async function getBoard(q: ShopBoardQuery = {}, signal?: AbortSignal): Promise { return normaliseBoard(await api.get | null>(`/api/shop/board${qs({ ...q })}`, signal)); } export async function getStations(signal?: AbortSignal): Promise { return asList(await api.get("/api/shop/stations", signal), "stations"); } export async function getStall(stationId: number, signal?: AbortSignal): Promise { const raw = await api.get | null>(`/api/shop/stall${qs({ station_id: stationId })}`, signal); return { station: raw?.station ?? { id: stationId, name: "", dept_code: "", dept_name: "" }, mine: cards(raw?.mine), submitted: cards(raw?.submitted), }; } export async function getPeople(dept: string, signal?: AbortSignal): Promise { return asList(await api.get(`/api/departments/${encodeURIComponent(dept)}/people`, signal), "people"); } export const shopApi = { getBoard, getStations, getStall, getPeople };