B2
Build and Push Docker Image / build (push) Successful in 44s

This commit is contained in:
jason
2026-04-22 09:39:00 -05:00
parent e0dfac2d48
commit a165428f14
5 changed files with 124 additions and 3 deletions
@@ -55,6 +55,7 @@ export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string
id: op.id,
sequence: op.sequence,
name: op.name,
kind: op.kind,
qrToken: op.qrToken,
machineName: op.machine?.name ?? null,
machineKind: op.machine?.kind ?? null,
@@ -64,6 +65,8 @@ export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string
qcRequired: op.qcRequired,
plannedMinutes: op.plannedMinutes,
plannedUnits: op.plannedUnits,
unitsCompleted: op.unitsCompleted,
status: op.status,
},
};
@@ -80,9 +80,12 @@ export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string
operations: part.operations.map((op) => ({
sequence: op.sequence,
name: op.name,
kind: op.kind,
machineName: op.machine?.name ?? null,
qcRequired: op.qcRequired,
qrToken: op.qrToken,
unitsCompleted: op.unitsCompleted,
status: op.status,
})),
};
@@ -94,6 +97,7 @@ export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string
id: op.id,
sequence: op.sequence,
name: op.name,
kind: op.kind,
qrToken: op.qrToken,
machineName: op.machine?.name ?? null,
machineKind: op.machine?.kind ?? null,
@@ -103,6 +107,8 @@ export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string
qcRequired: op.qcRequired,
plannedMinutes: op.plannedMinutes,
plannedUnits: op.plannedUnits,
unitsCompleted: op.unitsCompleted,
status: op.status,
},
}));
+2 -1
View File
@@ -31,13 +31,14 @@ Items that came out of floor-testing, not in the original roadmap.
| A6 | 3D STEP viewer embedded on operator scan card + admin assembly page (shared `StepViewerPanel`, load-on-tap) | **done** |
| A7 | "Done" button auto-detects partial: if typed units < remaining, step ends `partial` and releases claim instead of locking `completed` | **done** |
| A8 | QC fail workflow: `kind="qc"` dedicated inspection steps + `qc_failed` status blocks reclaim until admin hits qc-reset (landed together with Step 9) | **done** |
| A9 | Progress + live status on traveler cover + op cards (`X of Y done` driven by `unitsCompleted`, status pill matching the UI) — reprints reflect reality | **done** |
### Planned
| Step | What | Notes |
| ---- | ---- | ----- |
| B1 | Operator `/op` dashboard lists **resumable** ops (`status = "partial"` with no current claim) | Needed because A5+A7 mean partial ops become unclaimed — they currently only re-surface when someone physically re-scans the card |
| B2 | Progress column on traveler cover + op cards (`X of Y done`) driven by `unitsCompleted` | Makes paper reflect reality when a traveler is re-printed mid-run |
| ~~B2~~ | ~~Progress column on traveler cover + op cards~~ | Landed as A9 — cover sublines + op-card "Progress" meta row + live status pill on both |
| ~~B3~~ | ~~QC fail workflow on close~~ | Landed as A8 alongside Step 9 — `qc_failed` locks the step; admin `qc-reset` rolls it back to `pending`/`partial` based on `unitsCompleted` |
Hours-by-machine / plan-vs-actual reporting is already scoped in Step 7. Fresh-testing findings are tracked ad-hoc.
+112 -1
View File
@@ -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(" · ");
+1 -1
View File
File diff suppressed because one or more lines are too long