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

97 lines
4.5 KiB
TypeScript
Raw Normal View History

2026-10-17 23:49:14 -06:00
// Phase 7 helpers the close panel, the print menu and the tabs share: opening PDFs, the print rows, the kiosk
// code, the VISUAL observed line.
import { packetsApi } from "../api/packets";
import type { PacketRead, PrintKind, PrintRow, VcloseCheck, WorkOrder } from "../api/types";
import { fmtTime } from "../lib/format";
/** The rule's signature meanings for FILE and ADMIN_REOPEN. */
export const SIGN_FILE = "File in the data book";
export const SIGN_REOPEN = "Reopen a filed packet";
/** Opens a PDF (or any server URL) in a new tab. Every PDF view and print in the packet goes through here. */
export function openTab(url: string): void {
window.open(url, "_blank", "noopener");
}
/** Opens a printed copy of one kind. The server stamps it and records the print. */
export function openPrint(id: number, kind: PrintKind, opts: { sheet?: number | undefined; asset?: string | undefined } = {}): void {
openTab(packetsApi.printUrl(id, kind, opts));
}
/** The asset id of a "/_blob/<id>" URL, for documents whose answer has no asset_id. */
export function assetOfUrl(url: string): string | undefined {
const m = /\/_blob\/([^/?#]+)/.exec(url);
return m?.[1] ? decodeURIComponent(m[1]) : undefined;
}
export interface PrintRowDef {
kind: Exclude<PrintKind, "doc">;
label: string;
desc: string;
}
/** The rows of the print menu for this packet, as the old openPrintMenu. */
export function printRows(p: PacketRead): PrintRowDef[] {
const drawings = p.pages.filter((x) => x.type === "drawing");
const live = drawings.filter((x) => !x.void).length;
const voids = drawings.filter((x) => !!x.void).length;
const one = live === 1;
const rows: PrintRowDef[] = [
{
kind: "traveler",
label: "Job traveler",
desc: "Job info, work order QR codes for clocking on, BOM and heat numbers, checklist with blanks to stamp, welds, sign-offs, notes",
},
{ kind: "map-h", label: "Drawing with heat map", desc: one ? "The drawing with its heat table" : `All ${live} drawing sheets with heat tables` },
{ kind: "map-w", label: "Drawing with weld map", desc: one ? "The drawing with weld locations" : `All ${live} drawing sheets with weld locations` },
{ kind: "clean", label: "Drawing (no markup)", desc: "The approved drawing without markup" },
];
if (voids) {
rows.push({
kind: "void",
label: "Superseded drawings (VOID)",
desc: `${voids} replaced drawing sheet${voids === 1 ? "" : "s"} with their weld marks, stamped VOID`,
});
}
if (p.status === "complete" || p.status === "filed") {
rows.push({ kind: "databook", label: "Data book", desc: "The full manufacturing data book (same as the PDF export)" });
}
return rows;
}
/**
* The labor kiosk code of a work order, as the old qrText: `*%2{wo}${lot}${split}${sub}${seq}%*`. The columns stay
* text (never cast); an empty split or sub is "0".
*/
export function kioskCode(w: Pick<WorkOrder, "wo_base" | "lot" | "split" | "sub" | "seq">): string {
const seq = w.seq === null || w.seq === undefined ? "" : String(w.seq);
return `*%2${w.wo_base}$${w.lot}$${w.split || "0"}$${w.sub || "0"}$${seq}%*`;
}
/** "{date} · {what} · {pages} pages · {name}". */
export function printLineText(r: PrintRow): string {
const pages = r.pages === null || r.pages === undefined ? null : `${r.pages} page${r.pages === 1 ? "" : "s"}`;
return [fmtTime(r.at), r.what, pages, r.by?.display_name ?? null].filter((x): x is string => !!x).join(" · ");
}
/** The VISUAL observed state as one line, or null when there is nothing to say (a hand packet). */
export function visualLine(c: VcloseCheck | null | undefined): string | null {
if (!c) return null;
const open = c.open_wos ?? [];
if (c.closed_at) return `VISUAL shows the job closed since ${fmtTime(c.closed_at)}.`;
if (open.length) return `VISUAL still shows ${open.length} work order${open.length === 1 ? "" : "s"} open: ${open.join(", ")}.`;
if (!c.observed_at) return "The feed has not checked VISUAL yet.";
return null;
}
/**
* True when the close and file panel shows for this packet and user: filed (read-only for everyone), or complete for
* whoever may close, file or reopen, and for everyone after a reopen (the broken seal line).
*/
export function showsCloseFile(p: PacketRead): boolean {
if (p.removed) return false;
if (p.status === "filed") return true;
if (p.status !== "complete") return false;
return !!p.seal?.broken_at || p.can.includes("set_vclosed") || p.can.includes("file") || !!p.blocked.file || p.can.includes("admin_reopen");
}