// Q.C. folder helpers: the document types, who may add, dates, the card search, the unit tile text and the toast. import { useEffect, useState } from "react"; import type { ItpSummary, QcCard, QcDoc, QcFolderUnitRow } from "../../api/qc"; import { jobName } from "../../lib/packets"; /** The closed set of Q.C. folder document types (the old QC_TYPES). */ export const QC_TYPES = [ "ITP", "WPS / PQR", "NDE procedure", "Hydrotest procedure", "PMI / material", "Calibration certificate", "Customer spec", "Other", ] as const; /** Q.C. and Clerical add folder documents (QC_DOC_ADD, CL_OR_QC). Admin can, with an IT reason. */ export function canQcDocs(roles: readonly string[]): boolean { return roles.some((r) => ["qc", "qc_supervisor", "clerical", "clerical_supervisor", "admin"].includes(r)); } /** Q.C. starts and works the ITP. Admin can, with an IT reason. */ export function canItp(roles: readonly string[]): boolean { return roles.some((r) => r === "qc" || r === "qc_supervisor" || r === "admin"); } /** "Oct 17, 2026" (the old fmtQcDate), or empty. */ export function fmtQcDate(iso: string | null | undefined): string { if (!iso) return ""; const d = new Date(/^\d{4}-\d{2}-\d{2}$/.test(iso) ? `${iso}T12:00:00` : iso); if (Number.isNaN(d.getTime())) return iso; return d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }); } /** "WPS-101.pdf · added Oct 17, 2026 by Quinn QC", plus the superseded part on an old row. */ export function docMeta(d: QcDoc): string { const parts = [d.file_name ?? "", `added ${fmtQcDate(d.added_at)}${d.added_by ? ` by ${d.added_by.display_name}` : ""}`]; if (d.superseded_by !== null && d.superseded_by !== undefined) { parts.push(`superseded ${fmtQcDate(d.superseded_at)}${d.superseded_by_user ? ` by ${d.superseded_by_user.display_name}` : ""}`); } return parts.filter(Boolean).join(" · "); } /** The unit tile's ITP text and colour: "ITP ✓", "ITP 12/68" (amber with holds), "no ITP" (amber), or null. */ export function itpTile(u: Pick, projectHasItpDoc = false): { text: string; cls: string } | null { const r: ItpSummary | null = u.itp; if (r) { if (r.status === "complete") return { text: "ITP ✓", cls: "ok" }; return { text: `ITP ${r.done}/${r.total}`, cls: r.holds ? "due soon" : "" }; } if (u.has_itp_doc || projectHasItpDoc) return { text: "ITP ✓", cls: "ok" }; if (u.status !== "closed") return { text: "no ITP", cls: "due soon" }; return null; } /** "1 unit", "2 units". */ export function plural(n: number, word: string): string { return `${n} ${word}${n === 1 ? "" : "s"}`; } /** Whether a board card matches the find box: unit, op, title, drawing, customer, project or location. */ export function matchCard(c: QcCard, q: string): boolean { const t = q.trim().toLowerCase(); if (!t) return true; const hay = [jobName(c), c.title, c.drawing, c.customer, c.project?.number, c.project?.name, c.qc?.location, c.qc?.inspector] .filter(Boolean) .join(" ") .toLowerCase(); return t.split(/\s+/).every((w) => hay.includes(w)); } /** A toast text that clears itself after a moment. */ export function useToast(): [string | null, (t: string | null) => void] { const [toast, setToast] = useState(null); useEffect(() => { if (!toast) return; const t = window.setTimeout(() => setToast(null), 2600); return () => window.clearTimeout(t); }, [toast]); return [toast, setToast]; }