- C1 (issue 1): over target follows VISUAL ticket hours, not the timer. - C2 (issue 24): a Q.C. reject always needs a reason; accept needs an NCR number after a reject without one. - C3 (issue 25): an incomplete unit ITP blocks filing. - C4 (issue 27): prints open the print dialog on the page, no new tab. - C5 (issue 31): filing is blocked while VISUAL shows a work order open; the feed sets vclosed on VISUAL packets. - C6 (issue 8): the data book leaves out the history pages. - C7 (issue 18): the supervisor approves each received short; accept and filing wait for it. - C8 (issue 23): per-department checklists, edited by the department supervisor or Admin, frozen at START. - C9 (issue 32): Q.C. and Clerical may run the stamp check. - C10 (issue 33): the PM screen and reports are for PS-ProjectMgmt and Admin only. - C11 (issue 36): the Stall screen lists the packets assigned to the welder, at the supervisor's stall. Migration 0011. Docs: PLAN (also issue 9 confirmed, issue 4 asked), POLICY, SECURITY, SCHEMA, FEED, README, CHANGELOG. Co-Authored-By: Claude Opus 5.5 <[email protected]>
117 lines
5.2 KiB
TypeScript
117 lines
5.2 KiB
TypeScript
// 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<PrintErrorListener>();
|
|
|
|
/** 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/<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");
|
|
}
|