// Q.C. on the packet: the closed lists, NCR numbers and the texts the packet page builds. Pure functions. import type { ItpHold, ItpHoldItem, PacketRead, QcHistoryRow, QcWeldExam } from "../api/types"; import { fmtTime } from "./format"; /** The NDE methods (the old NDE_METHODS). */ export const NDE_METHODS = ["None required", "RT", "UT", "MT", "PT"] as const; export const NDE_NONE = "None required"; /** The NDE results. Empty means no result yet. */ export const NDE_RESULTS = ["", "Acceptable", "Rejectable"] as const; /** Packet document kinds and their labels (the old PDOC_TYPES), in the old order. */ export const DOC_KINDS: ReadonlyArray = [ ["nde", "NDE report (RT / UT / MT / PT)"], ["hydro", "Hydrotest report"], ["dimensional", "Dimensional report"], ["pmi", "PMI report"], ["coating", "Coating / paint report"], ["coc", "Certificate of conformance"], ["customer", "Customer inspection report"], ["ncr", "NCR"], ["other", "Other"], ]; /** The label of a document kind. An unknown kind shows as it is. */ export function docKindLabel(kind: string): string { return DOC_KINDS.find(([k]) => k === kind)?.[1] ?? kind; } /** The accept routes besides the shop departments. The en dash is the old text. */ export const ROUTE_DATABOOK = "Clerical – Data Book"; export const ROUTE_SHIPPING = "Shipping"; const RX_NCR = /^NCR-\d{4}-\d{4}$/; /** "NCR-2026-0021" from " ncr-2026-0021 ". The server does the same. */ export function normNcr(text: string): string { return text.trim().toUpperCase(); } /** A well-formed NCR number, after trimming and upper-casing. */ export function isNcr(text: string): boolean { return RX_NCR.test(normNcr(text)); } /** The NCR's page in pesco-ncr. */ export function ncrUrl(ncr: string): string { return `https://ncr.pescoinc.biz/ncrs/${encodeURIComponent(normNcr(ncr))}`; } /** "W3" from 3. */ export function weldId(no: number | string): string { return `W${no}`; } /** The rejected welds of a visual exam, by weld number: [{id: "W1", note: "undercut"}, ...]. */ export function rejectedWelds(exam: Record | null | undefined): Array<{ id: string; note: string | null }> { return Object.entries(exam ?? {}) .filter(([, v]) => v?.result === "reject") .map(([k, v]) => ({ no: Number(k), id: weldId(k), note: v.note?.trim() || null })) .sort((a, b) => (Number.isFinite(a.no) && Number.isFinite(b.no) ? a.no - b.no : a.id.localeCompare(b.id))) .map(({ id, note }) => ({ id, note })); } /** "2 welds", "1 weld". */ export function plural(n: number, word: string): string { return `${n} ${word}${n === 1 ? "" : "s"}`; } /** The old itpHoldNote head: "ITP L073028-ITP: 2 open hold / witness points for this stage." */ export function holdText(hold: ItpHold): string { const n = hold.count; return `ITP ${hold.no ?? ""}: ${n} open hold / witness ${hold.rejected ? "or rejected " : ""}point${n === 1 ? "" : "s"}${hold.pages ? " for this stage" : ""}.`; } /** "4.2 Prior to vessel closing (H)" or "(rejected, NCR NCR-2026-0021)" or "(rejected, NCR pending)". */ export function holdItemText(i: ItpHoldItem): string { const why = i.rejected ? `rejected, NCR ${i.ncr || "pending"}` : i.codes.join(", "); return `${i.n} ${i.t} (${why})`; } /** The first four points joined by " · ", then "· and N more". */ export function holdItemsText(hold: ItpHold, show = 4): string { const head = hold.items.slice(0, show).map(holdItemText).join(" · "); const more = hold.items.length - show; return more > 0 ? `${head} · and ${more} more` : head; } /** A reject note without the NCR number in front: the server stores "NCR-2026-0021 · reason". */ export function noteWithoutNcr(note: string | null | undefined, ncr: string | null | undefined): string { const t = (note ?? "").trim(); if (!ncr) return t; if (t === ncr) return ""; const lead = `${ncr} · `; return t.startsWith(lead) ? t.slice(lead.length) : t; } /** "