113 lines
4.7 KiB
TypeScript
113 lines
4.7 KiB
TypeScript
// 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<readonly [string, string]> = [
|
|||
|
|
["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<string, QcWeldExam> | 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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** "<time>: REJECTED by Quinn QC: note · NCR-2026-0021 · welds W1, W3". */
|
|||
|
|
export function historyLine(h: QcHistoryRow): string {
|
|||
|
|
const who = h.inspector || h.by?.display_name || "";
|
|||
|
|
const parts = [noteWithoutNcr(h.note, h.ncr), h.ncr, h.rejected_welds?.length ? `welds ${h.rejected_welds.join(", ")}` : null].filter(
|
|||
|
|
(x): x is string => !!x,
|
|||
|
|
);
|
|||
|
|
return `${fmtTime(h.at)}: ${String(h.result ?? "").toUpperCase()}${who ? ` by ${who}` : ""}${parts.length ? `: ${parts.join(" · ")}` : ""}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const REJECTED_SHOWN_ON = new Set(["review", "assigned", "in_progress"]);
|
|||
|
|
|
|||
|
|
/** True when the packet shows the "Q.C. rejected" callout: back in the shop after a reject. */
|
|||
|
|
export function showsQcRejected(p: PacketRead): boolean {
|
|||
|
|
return REJECTED_SHOWN_ON.has(p.status) && p.qc_last?.result === "rejected";
|
|||
|
|
}
|