165 lines
5.6 KiB
TypeScript
165 lines
5.6 KiB
TypeScript
import Link from "next/link";
|
|
import { prisma } from "@/lib/prisma";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export default async function AdminDashboardPage() {
|
|
const [
|
|
projectsTotal,
|
|
projectsActive,
|
|
assembliesTotal,
|
|
partsTotal,
|
|
operationsTotal,
|
|
operationsInProgress,
|
|
machinesActive,
|
|
templatesActive,
|
|
operatorsActive,
|
|
adminsActive,
|
|
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_progress" } }),
|
|
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 } }),
|
|
prisma.project.findMany({
|
|
orderBy: { updatedAt: "desc" },
|
|
take: 5,
|
|
select: {
|
|
id: true,
|
|
code: true,
|
|
name: true,
|
|
status: true,
|
|
updatedAt: true,
|
|
_count: { select: { assemblies: true } },
|
|
},
|
|
}),
|
|
]);
|
|
|
|
return (
|
|
<div className="mx-auto max-w-7xl px-4 py-8">
|
|
<h1 className="text-2xl font-semibold tracking-tight">Dashboard</h1>
|
|
<p className="text-slate-500 mt-1">Snapshot of the shop. Click any tile to dive in.</p>
|
|
|
|
<div className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
<Tile
|
|
href="/admin/projects"
|
|
title="Projects"
|
|
primary={projectsTotal}
|
|
secondary={`${projectsActive} active · ${assembliesTotal} assemblies · ${partsTotal} parts`}
|
|
/>
|
|
<Tile
|
|
href="/admin/projects"
|
|
title="Operations"
|
|
primary={operationsTotal}
|
|
secondary={`${operationsInProgress} in progress`}
|
|
/>
|
|
<Tile
|
|
href="/admin/machines"
|
|
title="Machines"
|
|
primary={machinesActive}
|
|
secondary="active"
|
|
/>
|
|
<Tile
|
|
href="/admin/operations"
|
|
title="Operation templates"
|
|
primary={templatesActive}
|
|
secondary="active"
|
|
/>
|
|
<Tile
|
|
href="/admin/users"
|
|
title="Users"
|
|
primary={adminsActive + operatorsActive}
|
|
secondary={`${adminsActive} admin${adminsActive === 1 ? "" : "s"} · ${operatorsActive} operator${operatorsActive === 1 ? "" : "s"}`}
|
|
/>
|
|
<div className="rounded-xl bg-white border border-slate-200 p-5">
|
|
<h2 className="font-medium text-slate-700">Fasteners & POs</h2>
|
|
<p className="text-sm text-slate-500 mt-1">Purchasing lifecycle lands in step 6.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<section className="mt-10">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<h2 className="text-lg font-semibold">Recent projects</h2>
|
|
<Link href="/admin/projects" className="text-sm text-blue-600 hover:underline">
|
|
All projects →
|
|
</Link>
|
|
</div>
|
|
<div className="rounded-xl bg-white border border-slate-200 overflow-hidden">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-slate-50 text-left text-slate-600 border-b border-slate-200">
|
|
<tr>
|
|
<th className="px-4 py-2 font-medium">Code</th>
|
|
<th className="px-4 py-2 font-medium">Name</th>
|
|
<th className="px-4 py-2 font-medium">Status</th>
|
|
<th className="px-4 py-2 font-medium">Assemblies</th>
|
|
<th className="px-4 py-2 font-medium">Updated</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{recentProjects.map((p) => (
|
|
<tr key={p.id} className="border-b border-slate-100 last:border-0">
|
|
<td className="px-4 py-3 font-mono">
|
|
<Link href={`/admin/projects/${p.id}`} className="text-blue-600 hover:underline">
|
|
{p.code}
|
|
</Link>
|
|
</td>
|
|
<td className="px-4 py-3">{p.name}</td>
|
|
<td className="px-4 py-3 text-slate-600">{p.status}</td>
|
|
<td className="px-4 py-3 text-slate-600">{p._count.assemblies}</td>
|
|
<td className="px-4 py-3 text-slate-500">
|
|
{p.updatedAt.toLocaleDateString(undefined, {
|
|
year: "numeric",
|
|
month: "short",
|
|
day: "numeric",
|
|
})}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
{recentProjects.length === 0 && (
|
|
<tr>
|
|
<td colSpan={5} className="px-4 py-10 text-center text-slate-500">
|
|
No projects yet.{" "}
|
|
<Link href="/admin/projects" className="text-blue-600 hover:underline">
|
|
Create one
|
|
</Link>
|
|
.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Tile({
|
|
href,
|
|
title,
|
|
primary,
|
|
secondary,
|
|
}: {
|
|
href: string;
|
|
title: string;
|
|
primary: number;
|
|
secondary: string;
|
|
}) {
|
|
return (
|
|
<Link
|
|
href={href}
|
|
className="block rounded-xl bg-white border border-slate-200 p-5 transition hover:border-slate-400 hover:shadow-sm"
|
|
>
|
|
<h2 className="font-medium text-slate-700">{title}</h2>
|
|
<p className="text-3xl font-semibold tracking-tight mt-2">{primary}</p>
|
|
<p className="text-xs text-slate-500 mt-1">{secondary}</p>
|
|
</Link>
|
|
);
|
|
}
|