TypeScript 50.6%
JavaScript 46.1%
SQL 2.1%
Shell 0.6%
CSS 0.5%
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