SPB Git forge

spb/toveli

Public
14commits 1branches 0releases
1.1 MBsize
maindefault branch
3 h agolast push
TypeScript 50.6% JavaScript 46.1% SQL 2.1% Shell 0.6% CSS 0.5%
2.9 KB · 32 lines tsx
Raw Blame History
1import Link from "next/link";2import { sql, eq } from "drizzle-orm";3import { db } from "@/db/client";4import { listings, reports, users, emailDeliveries, auditEvents } from "@/db/schema";5import { Stat } from "@/components/ui";6import { fmtInt } from "@/lib/format";7import { meta } from "@/lib/db";8export const dynamic = "force-dynamic";9export default async function AdminHome() {10  const d = db();11  const [[l], [r], [u], [m], [a]] = await Promise.all([12    d.select({ pending: sql<number>`sum(case when status='PENDING_REVIEW' then 1 else 0 end)::int`, published: sql<number>`sum(case when status='PUBLISHED' then 1 else 0 end)::int`, total: sql<number>`count(*)::int` }).from(listings),13    d.select({ open: sql<number>`count(*)::int` }).from(reports).where(eq(reports.status, "OPEN")),14    d.select({ n: sql<number>`count(*)::int`, d7: sql<number>`sum(case when created_at > now() - interval '7 days' then 1 else 0 end)::int` }).from(users),15    d.select({ n: sql<number>`count(*)::int`, failed: sql<number>`sum(case when status='FAILED' then 1 else 0 end)::int`, mocked: sql<number>`sum(case when status='MOCKED' then 1 else 0 end)::int` }).from(emailDeliveries).where(sql`created_at > now() - interval '7 days'`),16    d.select({ n: sql<number>`count(*)::int` }).from(auditEvents).where(sql`created_at > now() - interval '24 hours'`),17  ]);18  return (19    <div className="space-y-5">20      <h1 className="text-2xl font-extrabold tracking-tight">Tableau de bord</h1>21      <div className="grid grid-cols-2 md:grid-cols-5 gap-3">22        <Link href="/admin/annonces"><Stat label="Annonces à vérifier" value={fmtInt(l.pending)} tone={l.pending ? "warn" : undefined} sub={`${l.published} publiées · ${l.total} au total`} /></Link>23        <Link href="/admin/signalements"><Stat label="Signalements ouverts" value={fmtInt(r.open)} tone={r.open ? "warn" : undefined} /></Link>24        <Stat label="Comptes" value={fmtInt(u.n)} sub={`${u.d7} cette semaine`} />25        <Stat label="Courriels (7 j)" value={fmtInt(m.n)} sub={`${m.failed} échecs · ${m.mocked} simulés${m.mocked ? " (transport mock)" : ""}`} tone={m.failed ? "warn" : undefined} />26        <Stat label="Événements d'audit (24 h)" value={fmtInt(a.n)} />27      </div>28      <div className="tv-card p-4 text-sm"><p className="font-semibold">Sources et synchronisation</p><p className="text-ink-2">Base canonique construite le {meta("built_at") ?? "—"} · {fmtInt(Number(meta("listings_sale")))} à vendre · {fmtInt(Number(meta("listings_rent")))} à louer · {fmtInt(Number(meta("properties")))} propriétés · {fmtInt(Number(meta("valuations")))} valuations. Journal : <code>data/sync.log</code> sur le nœud.</p>{!process.env.RESEND_API_KEY && <p className="text-warn mt-2">RESEND_API_KEY absente : les courriels sont simulés (data/outbox/mails.jsonl). Configurer une clé et vérifier le domaine d'envoi avant l'ouverture publique des comptes.</p>}</div>29    </div>30  );31}32