import Link from "next/link"; import { prisma } from "@/lib/prisma"; import { getWipOperations, getOverdueProjects, getAuditLog, getQcFailures, formatRelative, } from "@/lib/reports"; export const dynamic = "force-dynamic"; /** * Admin landing. Three jobs: * 1. Top tiles — counts so the shop owner can spot a drop-off at a glance. * 2. WIP + Overdue cards — "what's hot right now, what's slipping." * 3. Recent activity — last handful of audit rows, linking to full log. * * Data comes from lib/reports.ts; the page itself is a dumb composition. */ export default async function AdminDashboardPage() { const now = new Date(); const [ projectsTotal, projectsActive, assembliesTotal, partsTotal, operationsTotal, operationsActive, machinesActive, templatesActive, operatorsActive, adminsActive, wip, overdue, qcFailures, { rows: recentAudit }, recentProjects, ] = await Promise.all([ prisma.project.count(), prisma.project.count({ where: { status: { in: ["planning", "in_progress"] } } }), prisma.assembly.count(), prisma.part.count(), prisma.operation.count(), prisma.operation.count({ where: { status: { in: ["in_progress", "partial"] } } }), prisma.machine.count({ where: { active: true } }), prisma.operationTemplate.count({ where: { active: true } }), prisma.user.count({ where: { role: "operator", active: true } }), prisma.user.count({ where: { role: "admin", active: true } }), getWipOperations(20), getOverdueProjects(now, 10), getQcFailures(10), getAuditLog({ limit: 8 }), prisma.project.findMany({ orderBy: { updatedAt: "desc" }, take: 5, select: { id: true, code: true, name: true, status: true, updatedAt: true, _count: { select: { assemblies: true } }, }, }), ]); return (

Dashboard

Snapshot of the shop. Click any tile to dive in.

{/* ─── Work in progress ─────────────────────────────────────── */}

Work in progress

Active claims plus resumable (partial) steps. {wip.length} shown.

{wip.length === 0 ? (

Nothing active. Operators scan travelers to begin a step.

) : ( {wip.map((op) => { const totalUnits = op.part.qty * op.part.assembly.qty; return ( ); })}
Project · Part Step Machine Operator Status Progress
{op.part.assembly.project.code} {" · "} {op.part.code}
{op.part.name}
#{op.sequence}{" "} {op.name} {op.machine?.name ?? } {op.claimedBy?.name ?? } {op.claimedAt ? (
since {formatRelative(op.claimedAt, now)}
) : null}
{op.unitsCompleted} / {totalUnits}
)}
{/* ─── Overdue projects ─────────────────────────────────────── */}

Overdue projects

{overdue.length === 0 ? (

Nothing overdue. Due dates live on the project edit screen.

) : ( {overdue.map((p) => ( ))}
Code Name Due Late Progress
{p.code} {p.name} {p.dueDate.toLocaleDateString()} {p.daysLate}d
{p.completedOps}/{p.totalOps} ops · {p.progressPct}%
)}
{/* ─── QC failures ──────────────────────────────────────────── */} {qcFailures.length > 0 ? (

QC failures

Steps blocked by a failing inspection. Open the part to review and hit{" "} Reset QC to reopen for rework.

{qcFailures.map((op) => { const last = op.qcRecords[0] ?? null; return ( ); })}
Project · Part Step Kind Failed by Notes When
{op.part.assembly.project.code} {" · "} {op.part.code}
{op.part.name}
#{op.sequence}{" "} {op.name} {op.kind === "qc" ? "Inspection" : "Work"} {last?.operator.name ?? } {last?.notes ? ( {last.notes} ) : ( )} {last ? formatRelative(last.createdAt, now) : formatRelative(op.updatedAt, now)}
) : null} {/* ─── Recent activity (audit log peek) ───────────────────── */}

Recent activity

Full audit log →
{recentAudit.length === 0 ? (

No activity yet.

) : (
    {recentAudit.map((row) => (
  • {formatRelative(row.at, now)} {row.actor?.name ?? "system"} · {row.action} · {row.entity} {row.entityId ? `/${row.entityId.slice(0, 8)}` : ""}
  • ))}
)}
{/* ─── Recent projects ─────────────────────────────────────── */}

Recent projects

All projects →
{recentProjects.map((p) => ( ))} {recentProjects.length === 0 && ( )}
Code Name Status Assemblies Updated
{p.code} {p.name} {p.status} {p._count.assemblies} {p.updatedAt.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric", })}
No projects yet.{" "} Create one .
); } function Tile({ href, title, primary, secondary, }: { href: string; title: string; primary: number | string; secondary: string; }) { return (

{title}

{primary}

{secondary}

); } function StatusPill({ status }: { status: string }) { const tone = status === "in_progress" ? "bg-blue-100 text-blue-800" : status === "partial" ? "bg-orange-100 text-orange-800" : status === "qc_failed" ? "bg-red-100 text-red-800" : "bg-slate-100 text-slate-700"; const label = status === "in_progress" ? "in progress" : status === "qc_failed" ? "QC failed" : status; return ( {label} ); }