SPB Git

Repository Public

page.tsx 5.1 KB 117 lines tsx

1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// /admin — dashboard du centre de communication : les chiffres qui comptent3// (nouvelles demandes, non-lues, urgentes, investisseurs, candidatures,4// légal, sécurité, résolues, délai de première réponse) + demandes récentes.5// Chaque chiffre respecte le RBAC de l'admin connecté.6import { redirect } from "next/navigation";7import { getAdminSession } from "@/lib/comms/admin-auth";8import { dashboardStats, recentSubmissions } from "@/lib/comms/queries";9import { categoryBySlug, PRIORITIES, statusesFor } from "@/lib/comms/categories";1011export const dynamic = "force-dynamic";1213function fmtDate(iso: string): string {14  return iso.replace("T", " ").slice(0, 16);15}1617export default async function AdminDashboard() {18  const session = await getAdminSession();19  if (!session) redirect("/admin/connexion");20  const s = dashboardStats(session);21  const recent = recentSubmissions(session, 8);2223  const TILES: { label: string; value: string | number; href: string; tone?: "lime" | "danger" }[] = [24    { label: "Nouvelles aujourd'hui", value: s.today, href: "/admin/inbox", tone: "lime" },25    { label: "Non lues", value: s.unread, href: "/admin/inbox" },26    { label: "Ouvertes", value: s.open, href: "/admin/inbox" },27    { label: "Urgentes / hautes", value: s.urgent, href: "/admin/inbox?priority=urgente", tone: s.urgent > 0 ? "danger" : undefined },28    { label: "Investisseurs ouvertes", value: s.investors, href: "/admin/inbox?category=investisseurs" },29    { label: "Candidatures (30 j)", value: s.jobs, href: "/admin/candidatures" },30    { label: "Légal & Loi 25 ouvertes", value: s.legal, href: "/admin/inbox?category=legal" },31    { label: "Signalements sécurité", value: s.security, href: "/admin/inbox?category=securite" },32    { label: "Résolues cette semaine", value: s.resolvedWeek, href: "/admin/inbox?view=toutes" },33    {34      label: "1re réponse (moy. 30 j)",35      value: s.avgFirstResponseH != null ? `${s.avgFirstResponseH} h` : "—",36      href: "/admin/inbox",37    },38  ];3940  return (41    <>42      <div className="flex flex-wrap items-end justify-between gap-4">43        <div>44          <p className="kicker">Communication Hub</p>45          <h1 className="gk-display mt-2 text-[clamp(24px,3.5vw,34px)] leading-tight font-bold tracking-[-0.03em]">46            Bonjour, {session.displayName.split(" ")[0]}.47          </h1>48        </div>49        <a href="/admin/inbox" className="btn btn-primary">50          Ouvrir l&apos;inbox {s.unread > 0 ? `(${s.unread} non lues)` : ""}51        </a>52      </div>5354      <div className="mt-8 grid grid-cols-2 gap-3 sm:grid-cols-3 xl:grid-cols-5">55        {TILES.map((t) => (56          <a57            key={t.label}58            href={t.href}59            className={`gk-card gk-card-hover p-4 no-underline ${60              t.tone === "lime" ? "!bg-lime-soft" : t.tone === "danger" ? "!bg-[var(--danger-soft)]" : ""61            }`}62          >63            <p className="stat-huge !text-[clamp(26px,3vw,38px)]">{t.value}</p>64            <p className="klabel mt-2 leading-snug normal-case">{t.label}</p>65          </a>66        ))}67      </div>6869      <div className="sec-head mt-12">70        <p className="kicker">Demandes récentes</p>71        <a href="/admin/inbox" className="sec-index no-underline hover:text-green">72          Tout voir →73        </a>74      </div>75      <div className="gk-card mt-5 overflow-hidden !shadow-none">76        {recent.length === 0 ? (77          <p className="gk-mono p-6 text-[12px] text-ink-3">78            Aucune demande pour l&apos;instant — le centre de communication est prêt.79          </p>80        ) : (81          recent.map((r) => {82            const cat = categoryBySlug(r.category);83            return (84              <a85                key={r.id}86                href={`/admin/demande/${r.reference}`}87                className={`adm-row ${r.is_read ? "" : "adm-row--unread"}`}88              >89                <div className="flex flex-wrap items-center gap-x-3 gap-y-1">90                  <span className="gk-mono text-[11px] font-bold text-green">{r.reference}</span>91                  <span className="adm-chip">{cat?.short ?? r.category}</span>92                  <span className={`adm-chip adm-chip--${r.priority}`}>93                    {PRIORITIES[r.priority as keyof typeof PRIORITIES] ?? r.priority}94                  </span>95                  <span className="adm-chip adm-chip--ghost">96                    {statusesFor(r.category)[r.status] ?? r.status}97                  </span>98                  <span className="gk-mono ml-auto text-[10.5px] text-ink-3">{fmtDate(r.created_at)}</span>99                </div>100                <p className="mt-1 truncate text-[13.5px]">101                  <strong>102                    {r.is_anonymous103                      ? "Anonyme"104                      : [r.first_name, r.last_name].filter(Boolean).join(" ") || r.email || "—"}105                  </strong>106                  {r.organization ? ` · ${r.organization}` : ""}107                  {r.subject ? ` — ${r.subject}` : r.sub_category ? ` — ${r.sub_category}` : ""}108                </p>109              </a>110            );111          })112        )}113      </div>114    </>115  );116}117
Files main
Menu

Theme