// Small helpers the shop tabs share: the drawing page list, the user's level, the department people. import { useEffect, useState } from "react"; import { packetsApi, peopleApi } from "../api/packets"; import type { BomLine, DeptPerson, Guarded, PacketPage, PacketRead, PacketStatus, ShortReceipt, TransitionBody } from "../api/types"; import type { Mutate } from "./tabs"; import { parseRole } from "../auth/roles"; /** Live drawing pages first, then the VOID copies (the old viewPages). */ export function viewPages(pages: readonly PacketPage[]): PacketPage[] { const dwg = pages.filter((x) => x.type === "drawing"); return [...dwg.filter((x) => !x.void), ...dwg.filter((x) => !!x.void)]; } /** * The page to show: the one picked when it exists, else the first live page. A VOID page that was not picked on * purpose lands on the sheet that replaced it. */ export function landingPage(pages: readonly PacketPage[], picked: number | null, onPurpose: boolean): PacketPage | undefined { let pg = pages.find((x) => x.n === picked) ?? pages[0]; for (let i = 0; i < 10 && pg?.void && !onPurpose; i++) { const next = pages.find((x) => x.n === pg?.void?.new_n); if (!next) break; pg = next; } return pg; } /** "M00543 R2 · VOID" for the page buttons. */ export function pageLabel(pg: PacketPage): string { return `${pg.dwg || `Page ${pg.n}`}${pg.rev ? ` R${pg.rev}` : ""}${pg.void ? " · VOID" : ""}`; } export type Level = "worker" | "lead" | "supervisor"; /** The highest shop level in a department; admin counts as supervisor. */ export function shopLevel(roles: readonly string[], dept: string): Level | null { if (roles.includes("admin")) return "supervisor"; const order: Level[] = ["worker", "lead", "supervisor"]; let best = -1; for (const r of roles) { const p = parseRole(r); if (p.shop && p.dept === dept) best = Math.max(best, order.indexOf(p.level)); } return best < 0 ? null : (order[best] ?? null); } /** May this user verify a change order for the department? Mirrors the server's can_verify. */ export function canVerifyDept(roles: readonly string[], dept: string): boolean { if (roles.includes("admin")) return true; if (dept === "WH") return roles.includes("warehouse_supervisor"); if (dept === "QC") return roles.includes("qc_supervisor"); if (dept === "CL" || dept === "PM") return false; return shopLevel(roles, dept) === "supervisor"; } /** The active people of a shop department, for the welder picker and the weld sheet's stamp. */ export function usePeople(dept: string | null): { people: DeptPerson[]; loaded: boolean; error: string | null } { const [state, setState] = useState<{ dept: string | null; people: DeptPerson[]; error: string | null } | null>(null); useEffect(() => { if (!dept) return; const ctl = new AbortController(); peopleApi .list(dept, ctl.signal) .then((people) => { if (!ctl.signal.aborted) setState({ dept, people, error: null }); }) .catch((err: unknown) => { if (ctl.signal.aborted) return; setState({ dept, people: [], error: err instanceof Error ? err.message : "The people list could not be read." }); }); return () => ctl.abort(); }, [dept]); const mine = state && state.dept === dept ? state : null; return { people: mine?.people ?? [], loaded: !!mine, error: mine?.error ?? null }; } export type ShopTab = "checklist" | "welds"; /** The old missing(): what the stall still has to do before Submit, each with the tab that fixes it. */ export function stallMissing(p: PacketRead): Array<{ t: string; tab: ShopTab }> { const out: Array<{ t: string; tab: ShopTab }> = []; const done = p.stage?.checklist ?? {}; const open = p.checklist_items.filter((i) => !done[i.key]?.done).length; if (open) out.push({ t: `${open} checklist item${open > 1 ? "s" : ""}`, tab: "checklist" }); if (p.dept.weld !== false) { const here = p.welds.some((w) => !w.carried && (w.dept_code === undefined || w.dept_code === null || w.dept_code === p.dept.code)); if (!here) out.push({ t: "no welds marked on the drawing", tab: "welds" }); if (!p.stage?.filler) out.push({ t: "filler metal", tab: "checklist" }); } return out; } export type StepFn = ( action: string, extra?: Omit, opts?: { sign?: string; ok?: string }, ) => Promise; /** A transition with the expect guard, through the page's mutate. */ export function makeStep(p: PacketRead, mutate: Mutate): StepFn { const expect: { status: PacketStatus; dept: string } = { status: p.status, dept: p.dept.code }; return (action, extra = {}, opts = {}) => mutate((g: Guarded) => packetsApi.transition(p.id, { action, expect, ...extra, ...g }), { sign: opts.sign, ok: opts.ok }); } /** * The receipts of a BOM line's shorts, open or closed: `shorts` on the read when the server sends it, else the * open short on the line. */ export function lineReceipts(p: PacketRead, b: BomLine): ShortReceipt[] { if (p.shorts) return p.shorts.filter((s) => s.item === b.item).flatMap((s) => s.receipts ?? []); return b.short?.receipts ?? []; }