Files
mrp-qrcode/app/op/page.tsx
T
jason 04ae88ca0d
Build and Push Docker Image / build (push) Successful in 45s
QoL changes and additions
2026-04-22 13:16:42 -05:00

131 lines
4.8 KiB
TypeScript

import Link from "next/link";
import { requireOperator } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
/**
* Operator dashboard. Because phones often have the browser open between scans,
* we want this page to answer one question fast: "what am I currently working
* on?" Active claims are the headline list; below that we show a generic
* "scan a card to start" hint so a fresh operator knows what to do.
*/
const OP_INCLUDE = {
machine: { select: { name: true } },
part: {
select: {
code: true,
name: true,
qty: true,
assembly: {
select: {
code: true,
qty: true,
project: { select: { code: true, name: true } },
},
},
},
},
} as const;
export default async function OperatorHomePage() {
const user = await requireOperator();
const [claims, resumable] = await Promise.all([
prisma.operation.findMany({
where: { claimedByUserId: user.id, status: "in_progress" },
orderBy: { claimedAt: "desc" },
include: OP_INCLUDE,
}),
// B1: partial ops are unclaimed after a release and only re-surface when
// someone physically re-scans the card. List them here so any operator
// can pick up where the last shift left off.
prisma.operation.findMany({
where: { status: "partial", claimedByUserId: null },
orderBy: { updatedAt: "desc" },
take: 50,
include: OP_INCLUDE,
}),
]);
return (
<div className="mx-auto max-w-3xl px-4 py-6 space-y-6">
<div>
<h1 className="text-2xl font-semibold">Hi, {user.name}</h1>
<p className="text-slate-500 mt-1 text-sm">
Scan a QR card with your phone camera to start a step, or continue an active one below.
</p>
</div>
{claims.length === 0 && resumable.length === 0 ? (
<div className="rounded-xl bg-white border border-slate-200 p-6 text-slate-600 text-sm text-center">
You have no active steps. Scan a traveler QR to begin.
</div>
) : null}
{claims.length > 0 ? (
<div className="space-y-3">
<h2 className="text-sm font-semibold text-slate-900 uppercase tracking-wide">
Active ({claims.length})
</h2>
{claims.map((c) => (
<Link
key={c.id}
href={`/op/scan/${c.qrToken}`}
className="block rounded-xl bg-white border border-slate-200 p-4 hover:border-slate-900 hover:shadow-sm transition"
>
<div className="text-xs text-slate-500">
{c.part.assembly.project.code} · {c.part.assembly.code}
</div>
<div className="font-medium mt-0.5">{c.part.name}</div>
<div className="text-sm text-slate-700 mt-1">
Step {c.sequence}: {c.name}
</div>
<div className="text-xs text-slate-500 mt-1 flex flex-wrap gap-x-3">
<span className="font-mono">{c.part.code}</span>
{c.machine ? <span>{c.machine.name}</span> : null}
{c.claimedAt ? <span>since {new Date(c.claimedAt).toLocaleTimeString()}</span> : null}
</div>
</Link>
))}
</div>
) : null}
{resumable.length > 0 ? (
<div className="space-y-3">
<h2 className="text-sm font-semibold text-slate-900 uppercase tracking-wide">
Resumable ({resumable.length})
</h2>
<p className="text-xs text-slate-500 -mt-1">
Previously started and released with units still to run. Tap to pick up where the last shift left off.
</p>
{resumable.map((c) => {
const total = c.part.qty * c.part.assembly.qty;
const remaining = Math.max(total - c.unitsCompleted, 0);
return (
<Link
key={c.id}
href={`/op/scan/${c.qrToken}`}
className="block rounded-xl bg-white border border-amber-300 p-4 hover:border-amber-500 hover:shadow-sm transition"
>
<div className="text-xs text-slate-500">
{c.part.assembly.project.code} · {c.part.assembly.code}
</div>
<div className="font-medium mt-0.5">{c.part.name}</div>
<div className="text-sm text-slate-700 mt-1">
Step {c.sequence}: {c.name}
</div>
<div className="text-xs text-slate-600 mt-1 flex flex-wrap gap-x-3">
<span className="font-mono">{c.part.code}</span>
{c.machine ? <span>{c.machine.name}</span> : null}
<span className="text-amber-700 font-medium">
{c.unitsCompleted} of {total} done · {remaining} remaining
</span>
</div>
</Link>
);
})}
</div>
) : null}
</div>
);
}