← 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>
  );
}