156 lines
4.8 KiB
TypeScript
156 lines
4.8 KiB
TypeScript
// 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 };
|