Files
PacketStation/frontend/src/packet/shopUtil.ts
T

114 lines
5.0 KiB
TypeScript
Raw Normal View History

// 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<TransitionBody, "action" | "expect">,
opts?: { sign?: string; ok?: string },
) => Promise<boolean>;
/** 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 ?? [];
}