// 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"; import { printPdf } from "../lib/printPdf"; /** 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, to view it. Prints do not come here (change C4). */ export function openTab(url: string): void { window.open(url, "_blank", "noopener"); } /** The short error when a print cannot be made. */ export const PRINT_FAILED = "The print could not be made. Try again."; type PrintErrorListener = (text: string) => void; const printErrorListeners = new Set(); /** The packet page listens here to show a failed print. Returns the unsubscribe. */ export function onPrintError(fn: PrintErrorListener): () => void { printErrorListeners.add(fn); return () => { printErrorListeners.delete(fn); }; } /** * Prints a stamped copy of one kind straight to the browser's print dialog (change C4). The server stamps it and * records the print when the PDF is fetched. A failure goes to the onPrintError listeners. */ export function openPrint(id: number, kind: PrintKind, opts: { sheet?: number | undefined; asset?: string | undefined } = {}): void { void printPdf(packetsApi.printUrl(id, kind, opts)).catch(() => { for (const fn of printErrorListeners) fn(PRINT_FAILED); }); } /** The asset id of a "/_blob/" 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; 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): 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"); }