Files
PacketStation/frontend/src/api/shop.ts
T

156 lines
4.8 KiB
TypeScript
Raw Normal View History

// 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<PacketCard, "open_cos"> {
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<ShopColumn, ShopCard[]>;
}
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<ShopBoard> | null): ShopBoard {
const cols = (raw?.columns ?? {}) as Partial<Record<ShopColumn, unknown>>;
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<ShopBoard> {
return normaliseBoard(await api.get<Partial<ShopBoard> | null>(`/api/shop/board${qs({ ...q })}`, signal));
}
export async function getStations(signal?: AbortSignal): Promise<StationOption[]> {
return asList<StationOption>(await api.get<unknown>("/api/shop/stations", signal), "stations");
}
export async function getStall(stationId: number, signal?: AbortSignal): Promise<StallAnswer> {
const raw = await api.get<Partial<StallAnswer> | 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<DeptPerson[]> {
return asList<DeptPerson>(await api.get<unknown>(`/api/departments/${encodeURIComponent(dept)}/people`, signal), "people");
}
export const shopApi = { getBoard, getStations, getStall, getPeople };