+112
-1
@@ -28,6 +28,9 @@ export interface OperationCardData {
|
||||
id: string;
|
||||
sequence: number;
|
||||
name: string;
|
||||
/// "work" | "qc". Dedicated QC ops don't track units, so we suppress the
|
||||
/// "Progress" row and lean on the QC-required call-out instead.
|
||||
kind: string;
|
||||
qrToken: string;
|
||||
machineName: string | null;
|
||||
machineKind: string | null;
|
||||
@@ -37,6 +40,13 @@ export interface OperationCardData {
|
||||
qcRequired: boolean;
|
||||
plannedMinutes: number | null;
|
||||
plannedUnits: number | null;
|
||||
/// Cumulative units logged across every Start→Pause/Done cycle. Printed
|
||||
/// as "X of Y done" so a reprinted traveler reflects real progress.
|
||||
unitsCompleted: number;
|
||||
/// Live status at print time — one of OperationStatuses. Printed as a
|
||||
/// small pill in the header so the shop can tell at a glance whether a
|
||||
/// reprint is mid-run, blocked (qc_failed), already done, etc.
|
||||
status: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,9 +86,12 @@ export interface PartCoverData {
|
||||
operations: {
|
||||
sequence: number;
|
||||
name: string;
|
||||
kind: string;
|
||||
machineName: string | null;
|
||||
qcRequired: boolean;
|
||||
qrToken: string;
|
||||
unitsCompleted: number;
|
||||
status: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
@@ -231,6 +244,58 @@ function wrapLines(text: string, font: PDFFont, size: number, maxWidth: number):
|
||||
return out;
|
||||
}
|
||||
|
||||
// Printed-status pill colours. Keep in sync with the UI tones in
|
||||
// ScanClient / PartDetailClient so the paper traveler matches what operators
|
||||
// see on-screen. Returned as background + text RGB so pdf-lib can fill a
|
||||
// rounded-ish rectangle and overlay the label.
|
||||
function statusPillColours(status: string): { fill: ReturnType<typeof rgb>; text: ReturnType<typeof rgb>; label: string } {
|
||||
switch (status) {
|
||||
case "in_progress":
|
||||
return { fill: rgb(0.87, 0.93, 1), text: rgb(0.16, 0.32, 0.6), label: "IN PROGRESS" };
|
||||
case "partial":
|
||||
return { fill: rgb(1, 0.92, 0.82), text: rgb(0.55, 0.32, 0.06), label: "PARTIAL" };
|
||||
case "completed":
|
||||
return { fill: rgb(0.86, 0.96, 0.88), text: rgb(0.1, 0.42, 0.2), label: "COMPLETED" };
|
||||
case "qc_failed":
|
||||
return { fill: rgb(1, 0.88, 0.88), text: rgb(0.62, 0.1, 0.15), label: "QC FAILED" };
|
||||
default:
|
||||
return { fill: rgb(0.93, 0.95, 0.98), text: rgb(0.35, 0.4, 0.5), label: "PENDING" };
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a small status pill at (x, y) — y is the baseline of the label text.
|
||||
// Returns the total pill width so callers can chain.
|
||||
function drawStatusPill(
|
||||
page: PDFPage,
|
||||
fonts: Fonts,
|
||||
status: string,
|
||||
x: number,
|
||||
y: number,
|
||||
): number {
|
||||
const { fill, text, label } = statusPillColours(status);
|
||||
const size = 8;
|
||||
const padX = 6;
|
||||
const padY = 3;
|
||||
const textW = fonts.bold.widthOfTextAtSize(label, size);
|
||||
const width = textW + padX * 2;
|
||||
const height = size + padY * 2;
|
||||
page.drawRectangle({
|
||||
x,
|
||||
y: y - padY,
|
||||
width,
|
||||
height,
|
||||
color: fill,
|
||||
});
|
||||
drawText(page, label, {
|
||||
x: x + padX,
|
||||
y,
|
||||
font: fonts.bold,
|
||||
size,
|
||||
color: text,
|
||||
});
|
||||
return width;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Operation card
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -252,6 +317,17 @@ async function drawOperationCard(
|
||||
size: 10,
|
||||
color: rgb(0.4, 0.4, 0.45),
|
||||
});
|
||||
// Status pill right after the label so the printed sheet matches the live
|
||||
// status at print time. Pending ops still show a pill (slate) so the field
|
||||
// layout is stable — an orphan "PENDING" badge reads fine.
|
||||
const travelerLabelW = fonts.bold.widthOfTextAtSize("TRAVELER CARD", 10);
|
||||
drawStatusPill(
|
||||
page,
|
||||
fonts,
|
||||
data.operation.status,
|
||||
MARGIN + travelerLabelW + 8,
|
||||
cursor.top - 10,
|
||||
);
|
||||
drawText(page, `${data.project.code} · ${data.assembly.code}`, {
|
||||
x: PAGE_WIDTH - MARGIN,
|
||||
y: cursor.top - 10,
|
||||
@@ -374,6 +450,22 @@ async function drawOperationCard(
|
||||
}
|
||||
if (data.operation.plannedMinutes) metaRows.push(["Planned time", `${data.operation.plannedMinutes} min`]);
|
||||
if (data.operation.plannedUnits) metaRows.push(["Planned units", `${data.operation.plannedUnits}`]);
|
||||
// Work ops: show cumulative progress so a reprinted traveler mid-run
|
||||
// reflects what's actually already produced. QC-dedicated ops don't track
|
||||
// units (close is pass/fail), so we skip the row for them.
|
||||
if (data.operation.kind !== "qc") {
|
||||
const total = data.assembly.qty * data.part.qty;
|
||||
const done = data.operation.unitsCompleted;
|
||||
const remaining = Math.max(0, total - done);
|
||||
metaRows.push([
|
||||
"Progress",
|
||||
done === 0
|
||||
? `0 of ${total} done`
|
||||
: done >= total
|
||||
? `${done} of ${total} — complete`
|
||||
: `${done} of ${total} done (${remaining} remaining)`,
|
||||
]);
|
||||
}
|
||||
if (data.operation.qcRequired) metaRows.push(["QC", "Required on close-out"]);
|
||||
|
||||
for (const [k, v] of metaRows) {
|
||||
@@ -610,16 +702,35 @@ async function drawCoverSheet(
|
||||
});
|
||||
|
||||
const textX = MARGIN + rowThumb + 12;
|
||||
drawText(page, `${op.sequence}. ${op.name}`, {
|
||||
const titleText = `${op.sequence}. ${op.name}`;
|
||||
drawText(page, titleText, {
|
||||
x: textX,
|
||||
y: cursor.top - 12,
|
||||
font: fonts.bold,
|
||||
size: 11,
|
||||
});
|
||||
// Status pill right after the title. We size off the printed title so the
|
||||
// pill hugs the text; on a very long title it'll drift further right,
|
||||
// which is fine — cover rows are wide.
|
||||
const titleW = fonts.bold.widthOfTextAtSize(titleText, 11);
|
||||
drawStatusPill(page, fonts, op.status, textX + titleW + 8, cursor.top - 12);
|
||||
|
||||
// Subline: machine · QC? · progress (work ops only). The progress chunk
|
||||
// is the point of B2: a reprinted cover sheet should show real numbers
|
||||
// instead of forcing the admin to flip back to the screen.
|
||||
const totalUnits = data.part.qty * data.assembly.qty;
|
||||
const progressText =
|
||||
op.kind === "qc"
|
||||
? null
|
||||
: op.unitsCompleted === 0
|
||||
? `0 / ${totalUnits}`
|
||||
: op.unitsCompleted >= totalUnits
|
||||
? `${op.unitsCompleted} / ${totalUnits} done`
|
||||
: `${op.unitsCompleted} / ${totalUnits}`;
|
||||
const subline = [
|
||||
op.machineName ?? "no machine",
|
||||
op.qcRequired ? "QC" : null,
|
||||
progressText,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
|
||||
Reference in New Issue
Block a user