step 9 and cleanup
Build and Push Docker Image / build (push) Successful in 1m4s

This commit is contained in:
jason
2026-04-22 09:27:01 -05:00
parent c8c86c9ca4
commit e0dfac2d48
18 changed files with 1521 additions and 85 deletions
+124 -60
View File
@@ -10,6 +10,7 @@ export type ScanOp = {
id: string;
sequence: number;
name: string;
kind: string;
status: string;
qcRequired: boolean;
instructions: string | null;
@@ -70,6 +71,13 @@ export default function ScanClient({ initialOp, viewer }: { initialOp: ScanOp; v
const active = op.status === "in_progress";
const partial = op.status === "partial";
const completed = op.status === "completed";
const qcFailed = op.status === "qc_failed";
// Dedicated inspection step: we show only the QC panel + Done button and
// hide unit counts / partial previews. Close demands a pass/fail record.
const isQcStep = op.kind === "qc";
// Any path that forces the inline QC block — dedicated QC ops, or work ops
// the admin flagged qcRequired.
const qcMandatory = isQcStep || op.qcRequired;
// Total units the shop needs to run through this op to satisfy the project:
// assembly.qty (how many assemblies we're building) × part.qty (parts per
@@ -133,8 +141,12 @@ export default function ScanClient({ initialOp, viewer }: { initialOp: ScanOp; v
}
function onClose() {
if (op.qcRequired && qcPassed === null) {
setError("This step requires QC — mark pass or fail before completing");
if (qcMandatory && qcPassed === null) {
setError(
isQcStep
? "Inspection step — mark pass or fail to complete"
: "This step requires QC — mark pass or fail before completing",
);
return;
}
startTransition(async () => {
@@ -189,18 +201,26 @@ export default function ScanClient({ initialOp, viewer }: { initialOp: ScanOp; v
? "bg-amber-100 text-amber-800"
: partial
? "bg-orange-100 text-orange-800"
: "bg-slate-100 text-slate-700"
: qcFailed
? "bg-red-100 text-red-800"
: "bg-slate-100 text-slate-700"
}`}
>
{op.status === "in_progress"
? "in progress"
: op.status === "partial"
? "partial"
: op.status}
: op.status === "qc_failed"
? "QC failed"
: op.status}
</span>
{op.qcRequired ? (
{isQcStep ? (
<span className="inline-flex items-center rounded-full bg-blue-100 text-blue-800 text-xs px-2 py-1">
Inspection step
</span>
) : op.qcRequired ? (
<span className="inline-flex items-center rounded-full bg-purple-100 text-purple-800 text-xs px-2 py-1">
QC
QC required
</span>
) : null}
</div>
@@ -299,7 +319,17 @@ export default function ScanClient({ initialOp, viewer }: { initialOp: ScanOp; v
</div>
) : null}
{isOperator && !completed ? (
{qcFailed ? (
<div className="rounded-2xl bg-red-50 border border-red-200 p-5 text-center">
<div className="text-red-900 font-semibold">Blocked QC failed</div>
<div className="text-sm text-red-800 mt-1">
The last run on this step failed inspection. An admin has to clear the failure
before the step can be reworked.
</div>
</div>
) : null}
{isOperator && !completed && !qcFailed ? (
<div className="rounded-2xl bg-white border border-slate-200 p-5 space-y-4">
{!claimedByMe && op.claimedBy && op.claimedByUserId !== viewer.id ? (
<div className="rounded-md bg-amber-50 border border-amber-200 text-amber-900 text-sm px-3 py-2">
@@ -316,37 +346,46 @@ export default function ScanClient({ initialOp, viewer }: { initialOp: ScanOp; v
{isPending
? partial
? "Resuming…"
: "Claiming…"
: isQcStep
? "Starting inspection…"
: "Claiming…"
: partial
? "Resume this step"
: "Start this step"}
: isQcStep
? "Start inspection"
: "Start this step"}
</button>
) : (
<>
{/* Dedicated QC ops don't track units — the purpose is the
pass/fail record, not a count — so we hide the units input
entirely and keep only the free-form note. */}
<div className="grid grid-cols-1 gap-3">
<label className="block">
<span className="text-sm font-medium text-slate-900">
Units processed
{op.unitsCompleted > 0 ? (
<span className="ml-2 text-xs text-slate-500 font-normal">
{op.unitsCompleted} of {totalUnits} already done
</span>
) : null}
</span>
<input
type="number"
inputMode="numeric"
min={0}
value={units}
onChange={(e) => setUnits(e.target.value)}
placeholder={
op.unitsCompleted > 0
? `${Math.max(0, totalUnits - op.unitsCompleted)} remaining`
: op.plannedUnits?.toString() ?? totalUnits.toString()
}
className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-base"
/>
</label>
{!isQcStep ? (
<label className="block">
<span className="text-sm font-medium text-slate-900">
Units processed
{op.unitsCompleted > 0 ? (
<span className="ml-2 text-xs text-slate-500 font-normal">
{op.unitsCompleted} of {totalUnits} already done
</span>
) : null}
</span>
<input
type="number"
inputMode="numeric"
min={0}
value={units}
onChange={(e) => setUnits(e.target.value)}
placeholder={
op.unitsCompleted > 0
? `${Math.max(0, totalUnits - op.unitsCompleted)} remaining`
: op.plannedUnits?.toString() ?? totalUnits.toString()
}
className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-base"
/>
</label>
) : null}
<label className="block">
<span className="text-sm font-medium text-slate-900">Note (optional)</span>
<textarea
@@ -358,9 +397,11 @@ export default function ScanClient({ initialOp, viewer }: { initialOp: ScanOp; v
</label>
</div>
{op.qcRequired ? (
{qcMandatory ? (
<div className="rounded-md bg-purple-50 border border-purple-200 p-3 space-y-2">
<div className="text-sm font-medium text-purple-900">QC check (required)</div>
<div className="text-sm font-medium text-purple-900">
{isQcStep ? "Inspection result (required)" : "QC check (required)"}
</div>
<div className="flex gap-2">
<button
type="button"
@@ -385,6 +426,13 @@ export default function ScanClient({ initialOp, viewer }: { initialOp: ScanOp; v
Fail
</button>
</div>
{qcPassed === false ? (
<div className="rounded-md bg-red-50 border border-red-200 text-red-800 text-xs px-3 py-2">
Submitting <span className="font-semibold">Fail</span> will lock this step
in <span className="font-semibold">QC failed</span> an admin has to clear
it before anyone can rework it.
</div>
) : null}
{qcPassed !== null ? (
<textarea
value={qcNotes}
@@ -402,39 +450,55 @@ export default function ScanClient({ initialOp, viewer }: { initialOp: ScanOp; v
close route's partial-detection logic: units blank or >=
remaining means "fully done", anything less is a partial
handoff that releases the claim so the next operator can pick
it up.
it up. QC ops don't apply — they're all-or-nothing.
*/}
{(() => {
const typed = units ? Number(units) : 0;
const remaining = Math.max(0, totalUnits - op.unitsCompleted);
const willPartial = typed > 0 && typed < remaining;
return willPartial ? (
<div className="rounded-md bg-orange-50 border border-orange-200 text-orange-900 text-xs px-3 py-2">
Pressing <span className="font-semibold">Done</span> with {typed} of {remaining}{" "}
remaining will mark this step <span className="font-semibold">Partial</span> and
release the claim so another operator can resume. Enter{" "}
<span className="font-mono">{remaining}</span> (or leave blank) if you actually
finished the batch.
</div>
) : null;
})()}
{!isQcStep
? (() => {
const typed = units ? Number(units) : 0;
const remaining = Math.max(0, totalUnits - op.unitsCompleted);
const willPartial = typed > 0 && typed < remaining;
return willPartial ? (
<div className="rounded-md bg-orange-50 border border-orange-200 text-orange-900 text-xs px-3 py-2">
Pressing <span className="font-semibold">Done</span> with {typed} of{" "}
{remaining} remaining will mark this step{" "}
<span className="font-semibold">Partial</span> and release the claim so
another operator can resume. Enter{" "}
<span className="font-mono">{remaining}</span> (or leave blank) if you
actually finished the batch.
</div>
) : null;
})()
: null}
<div className="grid grid-cols-2 gap-3">
<button
onClick={onRelease}
disabled={isPending}
className="h-14 rounded-xl bg-white border border-slate-300 text-slate-900 text-lg font-medium disabled:opacity-60"
>
{isPending ? "…" : "Pause"}
</button>
{/* Pause doesn't make sense on a dedicated inspection step —
either the checker passes, fails, or walks away without
stamping anything. Show Done only for QC ops. */}
{isQcStep ? (
<button
onClick={onClose}
disabled={isPending}
className="h-14 rounded-xl bg-emerald-600 text-white text-lg font-medium disabled:bg-emerald-400"
className="w-full h-14 rounded-xl bg-emerald-600 text-white text-lg font-medium disabled:bg-emerald-400"
>
{isPending ? "…" : "Done"}
{isPending ? "…" : qcPassed === false ? "Submit failure" : "Submit inspection"}
</button>
</div>
) : (
<div className="grid grid-cols-2 gap-3">
<button
onClick={onRelease}
disabled={isPending}
className="h-14 rounded-xl bg-white border border-slate-300 text-slate-900 text-lg font-medium disabled:opacity-60"
>
{isPending ? "…" : "Pause"}
</button>
<button
onClick={onClose}
disabled={isPending}
className="h-14 rounded-xl bg-emerald-600 text-white text-lg font-medium disabled:bg-emerald-400"
>
{isPending ? "…" : "Done"}
</button>
</div>
)}
</>
)}
</div>