← back to Homesonspec
apps/admin/src/app/page.tsx
87 lines
import Link from "next/link";
import { prisma } from "@homesonspec/database";
import { CreatedChip } from "@homesonspec/shared-ui";
export const dynamic = "force-dynamic";
export default async function AdminDashboard() {
const [openReviews, publishedHomes, staleHomes, sources, openCorrections, recentLeads, recentEvents] =
await Promise.all([
prisma.reviewItem.count({ where: { status: "OPEN" } }),
prisma.inventoryHome.count({ where: { status: "PUBLISHED" } }),
prisma.inventoryHome.count({ where: { freshness: { in: ["STALE", "REVIEW_REQUIRED"] } } }),
prisma.sourceRegistry.findMany({ orderBy: { key: "asc" } }),
prisma.correctionReport.count({ where: { status: "OPEN" } }),
prisma.lead.findMany({ orderBy: { createdAt: "desc" }, take: 5 }),
prisma.validationEvent.findMany({ where: { passed: false }, orderBy: { runAt: "desc" }, take: 5 }),
]);
const stats: [string, number | string, string][] = [
["Published homes", publishedHomes, "/validation"],
["Open review items", openReviews, "/review"],
["Stale / review-required", staleHomes, "/validation"],
["Open correction reports", openCorrections, "/corrections"],
];
return (
<div>
<h1 className="text-2xl font-bold">Operations dashboard</h1>
<div className="mt-4 grid gap-4 sm:grid-cols-4">
{stats.map(([label, value, href]) => (
<Link key={label} href={href} className="rounded-xl border border-neutral-200 bg-white p-4 shadow-sm hover:border-teal-400">
<div className="text-3xl font-bold">{value}</div>
<div className="mt-1 text-sm text-neutral-500">{label}</div>
</Link>
))}
</div>
<h2 className="mt-8 text-lg font-semibold">Source health</h2>
<div className="mt-3 grid gap-3 sm:grid-cols-3">
{sources.map((source) => (
<div key={source.id} className="rounded-lg border border-neutral-200 bg-white p-3 text-sm shadow-sm">
<div className="flex items-center justify-between">
<span className="font-semibold">{source.name}</span>
<span className={`rounded-full px-2 py-0.5 text-xs font-medium ${
source.health === "HEALTHY" ? "bg-emerald-100 text-emerald-800" :
source.health === "DEGRADED" ? "bg-amber-100 text-amber-800" :
"bg-red-100 text-red-800"}`}>
{source.health}
</span>
</div>
<div className="mt-1 text-xs text-neutral-500">{source.collectionMethod} · media rights: {source.mediaRights}</div>
<div className="mt-2"><CreatedChip date={source.createdAt} /></div>
</div>
))}
</div>
<div className="mt-8 grid gap-6 lg:grid-cols-2">
<section>
<h2 className="text-lg font-semibold">Recent failed validations</h2>
<ul className="mt-2 space-y-2 text-sm">
{recentEvents.map((event) => (
<li key={event.id} className="rounded-lg border border-neutral-200 bg-white p-3 shadow-sm">
<span className="font-mono text-xs text-red-700">{event.ruleId}</span>
<p className="mt-0.5 text-neutral-600">{event.message}</p>
<div className="mt-1"><CreatedChip date={event.runAt} /></div>
</li>
))}
{recentEvents.length === 0 && <li className="text-neutral-400">None.</li>}
</ul>
</section>
<section>
<h2 className="text-lg font-semibold">Recent leads</h2>
<ul className="mt-2 space-y-2 text-sm">
{recentLeads.map((lead) => (
<li key={lead.id} className="rounded-lg border border-neutral-200 bg-white p-3 shadow-sm">
<span className="font-medium">{lead.name}</span> <span className="text-neutral-500">({lead.email})</span>
<div className="mt-1"><CreatedChip date={lead.createdAt} /></div>
</li>
))}
{recentLeads.length === 0 && <li className="text-neutral-400">None yet.</li>}
</ul>
</section>
</div>
</div>
);
}