83 lines
3.5 KiB
TypeScript
83 lines
3.5 KiB
TypeScript
// 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<QcFolderUnitRow, "itp" | "has_itp_doc" | "status">, 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<string | null>(null);
|
||
|
|
useEffect(() => {
|
||
|
|
if (!toast) return;
|
||
|
|
const t = window.setTimeout(() => setToast(null), 2600);
|
||
|
|
return () => window.clearTimeout(t);
|
||
|
|
}, [toast]);
|
||
|
|
return [toast, setToast];
|
||
|
|
}
|