@@ -55,6 +55,7 @@ export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string
|
|||||||
id: op.id,
|
id: op.id,
|
||||||
sequence: op.sequence,
|
sequence: op.sequence,
|
||||||
name: op.name,
|
name: op.name,
|
||||||
|
kind: op.kind,
|
||||||
qrToken: op.qrToken,
|
qrToken: op.qrToken,
|
||||||
machineName: op.machine?.name ?? null,
|
machineName: op.machine?.name ?? null,
|
||||||
machineKind: op.machine?.kind ?? null,
|
machineKind: op.machine?.kind ?? null,
|
||||||
@@ -64,6 +65,8 @@ export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string
|
|||||||
qcRequired: op.qcRequired,
|
qcRequired: op.qcRequired,
|
||||||
plannedMinutes: op.plannedMinutes,
|
plannedMinutes: op.plannedMinutes,
|
||||||
plannedUnits: op.plannedUnits,
|
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) => ({
|
operations: part.operations.map((op) => ({
|
||||||
sequence: op.sequence,
|
sequence: op.sequence,
|
||||||
name: op.name,
|
name: op.name,
|
||||||
|
kind: op.kind,
|
||||||
machineName: op.machine?.name ?? null,
|
machineName: op.machine?.name ?? null,
|
||||||
qcRequired: op.qcRequired,
|
qcRequired: op.qcRequired,
|
||||||
qrToken: op.qrToken,
|
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,
|
id: op.id,
|
||||||
sequence: op.sequence,
|
sequence: op.sequence,
|
||||||
name: op.name,
|
name: op.name,
|
||||||
|
kind: op.kind,
|
||||||
qrToken: op.qrToken,
|
qrToken: op.qrToken,
|
||||||
machineName: op.machine?.name ?? null,
|
machineName: op.machine?.name ?? null,
|
||||||
machineKind: op.machine?.kind ?? null,
|
machineKind: op.machine?.kind ?? null,
|
||||||
@@ -103,6 +107,8 @@ export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string
|
|||||||
qcRequired: op.qcRequired,
|
qcRequired: op.qcRequired,
|
||||||
plannedMinutes: op.plannedMinutes,
|
plannedMinutes: op.plannedMinutes,
|
||||||
plannedUnits: op.plannedUnits,
|
plannedUnits: op.plannedUnits,
|
||||||
|
unitsCompleted: op.unitsCompleted,
|
||||||
|
status: op.status,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -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** |
|
| 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** |
|
| 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** |
|
| 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
|
### Planned
|
||||||
|
|
||||||
| Step | What | Notes |
|
| 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 |
|
| 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` |
|
| ~~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.
|
Hours-by-machine / plan-vs-actual reporting is already scoped in Step 7. Fresh-testing findings are tracked ad-hoc.
|
||||||
|
|||||||
+112
-1
@@ -28,6 +28,9 @@ export interface OperationCardData {
|
|||||||
id: string;
|
id: string;
|
||||||
sequence: number;
|
sequence: number;
|
||||||
name: string;
|
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;
|
qrToken: string;
|
||||||
machineName: string | null;
|
machineName: string | null;
|
||||||
machineKind: string | null;
|
machineKind: string | null;
|
||||||
@@ -37,6 +40,13 @@ export interface OperationCardData {
|
|||||||
qcRequired: boolean;
|
qcRequired: boolean;
|
||||||
plannedMinutes: number | null;
|
plannedMinutes: number | null;
|
||||||
plannedUnits: 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: {
|
operations: {
|
||||||
sequence: number;
|
sequence: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
kind: string;
|
||||||
machineName: string | null;
|
machineName: string | null;
|
||||||
qcRequired: boolean;
|
qcRequired: boolean;
|
||||||
qrToken: string;
|
qrToken: string;
|
||||||
|
unitsCompleted: number;
|
||||||
|
status: string;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,6 +244,58 @@ function wrapLines(text: string, font: PDFFont, size: number, maxWidth: number):
|
|||||||
return out;
|
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
|
// Operation card
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -252,6 +317,17 @@ async function drawOperationCard(
|
|||||||
size: 10,
|
size: 10,
|
||||||
color: rgb(0.4, 0.4, 0.45),
|
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}`, {
|
drawText(page, `${data.project.code} · ${data.assembly.code}`, {
|
||||||
x: PAGE_WIDTH - MARGIN,
|
x: PAGE_WIDTH - MARGIN,
|
||||||
y: cursor.top - 10,
|
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.plannedMinutes) metaRows.push(["Planned time", `${data.operation.plannedMinutes} min`]);
|
||||||
if (data.operation.plannedUnits) metaRows.push(["Planned units", `${data.operation.plannedUnits}`]);
|
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"]);
|
if (data.operation.qcRequired) metaRows.push(["QC", "Required on close-out"]);
|
||||||
|
|
||||||
for (const [k, v] of metaRows) {
|
for (const [k, v] of metaRows) {
|
||||||
@@ -610,16 +702,35 @@ async function drawCoverSheet(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const textX = MARGIN + rowThumb + 12;
|
const textX = MARGIN + rowThumb + 12;
|
||||||
drawText(page, `${op.sequence}. ${op.name}`, {
|
const titleText = `${op.sequence}. ${op.name}`;
|
||||||
|
drawText(page, titleText, {
|
||||||
x: textX,
|
x: textX,
|
||||||
y: cursor.top - 12,
|
y: cursor.top - 12,
|
||||||
font: fonts.bold,
|
font: fonts.bold,
|
||||||
size: 11,
|
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 = [
|
const subline = [
|
||||||
op.machineName ?? "no machine",
|
op.machineName ?? "no machine",
|
||||||
op.qcRequired ? "QC" : null,
|
op.qcRequired ? "QC" : null,
|
||||||
|
progressText,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(" · ");
|
.join(" · ");
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user