This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user