import { redirect } from "next/navigation"; import Link from "next/link"; import { prisma } from "@/lib/prisma"; import { getCurrentUser } from "@/lib/auth"; import ScanClient, { type ScanOp } from "./ScanClient"; /** * Entry point for a QR scan. Resolves the token server-side so we can: * 1. Redirect unauthenticated visitors to /login/operator with ?next=, * so they land back on this same page after PIN entry. * 2. Show a clean "unknown token" page for bad scans (old card, typo) instead * of the generic 404. * 3. Render the interactive ScanClient with everything the operator needs * to read the work card and claim / pause / complete. * * Admins who scan get a read-only preview — we don't let them claim because * the claim holder is supposed to be the one running the machine. */ export default async function ScanPage({ params }: { params: Promise<{ token: string }> }) { const { token } = await params; const user = await getCurrentUser(); if (!user) { redirect(`/login/operator?next=${encodeURIComponent(`/op/scan/${token}`)}`); } const op = await prisma.operation.findUnique({ where: { qrToken: token }, include: { machine: { select: { id: true, name: true, kind: true } }, part: { select: { id: true, code: true, name: true, material: true, qty: true, stepFile: { select: { id: true, originalName: true, kind: true } }, drawingFile: { select: { id: true, originalName: true, kind: true } }, cutFile: { select: { id: true, originalName: true, kind: true } }, thumbnailFileId: true, assembly: { select: { id: true, code: true, name: true, qty: true, stepFile: { select: { id: true, originalName: true, kind: true } }, drawingFile: { select: { id: true, originalName: true, kind: true } }, cutFile: { select: { id: true, originalName: true, kind: true } }, project: { select: { id: true, code: true, name: true } }, }, }, }, }, claimedBy: { select: { id: true, name: true } }, }, }); if (!op) { return (

QR code not recognised

This card might be from an older print, or the operation was deleted. Ask your supervisor to reprint the traveler.

Back to dashboard
); } if (user.role !== "operator") { // Admin preview: render a read-only summary, no action buttons. return (

Admin preview

); } const claimedByMe = op.claimedByUserId === user.id; return (
); } // Date fields come back as JS Date objects from Prisma; convert for the client // component payload so it can be serialised into the RSC stream without tripping // on non-plain objects. We widen to `ScanOp` which types the dates as strings. function serialise(obj: unknown): ScanOp { return JSON.parse(JSON.stringify(obj)) as ScanOp; }