Repository Public
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// /admin/courriel — la boîte courriel du panel, version client-mail moderne :3// dossiers avec compteurs, recherche plein champ, rangées riches (avatar à4// teinte stable, sujet + extrait, heure relative, pièces jointes, chips),5// actions rapides lu/archiver sans quitter la liste. Mobile-first : pilules6// défilantes, cibles tactiles généreuses.7import { redirect } from "next/navigation";8import { getAdminSession } from "@/lib/comms/admin-auth";9import { findAdminById } from "@/lib/comms/db";10import { listMail, folderCounts, MAIL_FOLDERS } from "@/lib/comms/mail";11import { MailComposer, MailRowQuick } from "./MailClient";12import { timeAgo, initialsOf, avatarStyle } from "../format";1314export const dynamic = "force-dynamic";1516export default async function MailboxPage({17 searchParams,18}: {19 searchParams: Promise<Record<string, string | string[] | undefined>>;20}) {21 const session = await getAdminSession();22 if (!session) redirect("/admin/connexion");23 const sp = await searchParams;24 const folder =25 typeof sp.folder === "string" && sp.folder in MAIL_FOLDERS ? sp.folder : "inbox";26 const q = typeof sp.q === "string" ? sp.q.slice(0, 120) : "";27 const page = Math.max(1, Number(sp.page) || 1);28 const superAdmin = session.roles.includes("super_admin");29 const { rows, total } = listMail(session.id, superAdmin, folder, page, 30, q || undefined);30 const counts = folderCounts(session.id, superAdmin);31 const admin = findAdminById(session.id);32 const myEmail = admin?.email ?? "";33 const pages = Math.max(1, Math.ceil(total / 30));3435 return (36 <>37 {/* en-tête : identité de la boîte + composeur */}38 <div className="flex flex-wrap items-start justify-between gap-4">39 <div className="flex min-w-0 items-center gap-4">40 <span className="adm-avatar !h-[52px] !w-[52px] !text-[19px]" style={avatarStyle(myEmail)}>41 {initialsOf(session.displayName, myEmail || session.username)}42 </span>43 <div className="min-w-0">44 <p className="kicker">Courriel</p>45 <h1 className="gk-display mt-1 truncate text-[clamp(20px,3vw,28px)] leading-tight font-bold tracking-[-0.03em]">46 {myEmail || "Adresse non définie"}47 </h1>48 <p className="gk-mono mt-[2px] text-[10.5px] text-ink-3">49 {counts.inbox.unread > 050 ? `${counts.inbox.unread} non lu${counts.inbox.unread > 1 ? "s" : ""} · `51 : ""}52 envoi & réception Resend53 {superAdmin ? " · messages non routés du domaine inclus" : ""}54 </p>55 </div>56 </div>57 <MailComposer fromEmail={myEmail} />58 </div>5960 {/* dossiers (compteurs) + recherche */}61 <div className="mt-6 flex flex-wrap items-center gap-3">62 <div className="hscroll max-w-full">63 {Object.entries(MAIL_FOLDERS).map(([k, v]) => (64 <a65 key={k}66 href={`/admin/courriel?folder=${k}`}67 className={`adm-chip !py-[6px] no-underline ${folder === k ? "adm-chip--lime" : ""}`}68 >69 {v}70 <span className={folder === k ? "text-ink-2" : "text-ink-3"}>71 {counts[k].total}72 </span>73 {k === "inbox" && counts.inbox.unread > 0 ? (74 <span className="rounded-full bg-ink px-[7px] py-[1px] text-[9px] text-lime">75 {counts.inbox.unread}76 </span>77 ) : null}78 </a>79 ))}80 </div>81 <form method="get" action="/admin/courriel" className="ml-auto flex w-full items-center gap-2 sm:w-auto">82 <input type="hidden" name="folder" value={folder} />83 <input84 name="q"85 defaultValue={q}86 placeholder="Rechercher (expéditeur, sujet…)"87 className="field w-full !py-[9px] !text-[13px] sm:w-[260px]"88 aria-label="Rechercher dans la boîte"89 />90 <button type="submit" className="btn btn-ghost !min-h-[40px] !px-4 !text-[13px]">91 ⌕92 </button>93 {q ? (94 <a href={`/admin/courriel?folder=${folder}`} className="gk-mono flex-none text-[10px] font-bold text-ink-3 uppercase underline underline-offset-4">95 Effacer96 </a>97 ) : null}98 </form>99 </div>100101 {/* liste */}102 <div className="gk-card mt-5 overflow-hidden !shadow-none">103 {rows.length === 0 ? (104 <div className="px-6 py-12 text-center">105 <p className="gk-display text-[17px] font-bold">106 {q ? "Aucun résultat" : "Boîte vide"}107 </p>108 <p className="gk-mono mt-2 text-[11px] leading-relaxed text-ink-3">109 {q110 ? `Rien ne correspond à « ${q} » dans ${MAIL_FOLDERS[folder].toLowerCase()}.`111 : folder === "inbox"112 ? "Les courriels reçus sur votre adresse apparaîtront ici."113 : "Aucun message dans ce dossier."}114 </p>115 </div>116 ) : (117 rows.map((m) => {118 const to = JSON.parse(m.to_emails) as string[];119 const isOut = m.direction === "out";120 const counterpartEmail = isOut ? (to[0] ?? "?") : m.from_email;121 const counterpartName = isOut122 ? to.join(", ")123 : (m.from_name ?? m.from_email);124 return (125 <div key={m.id} className={`adm-row !flex items-center !py-0 !pr-3 !pl-0 ${m.is_read ? "" : "adm-row--unread"}`}>126 <a127 href={`/admin/courriel/${m.id}`}128 className="flex min-w-0 flex-1 items-center gap-3 py-3 pl-4 no-underline sm:gap-4"129 >130 <span className="adm-avatar" style={avatarStyle(counterpartEmail)}>131 {initialsOf(isOut ? null : m.from_name, counterpartEmail)}132 </span>133 <span className="min-w-0 flex-1">134 <span className="flex items-baseline gap-2">135 <span className={`truncate text-[13.5px] ${m.is_read ? "font-medium" : "font-bold"}`}>136 {isOut ? `À : ${counterpartName}` : counterpartName}137 </span>138 {m.admin_id === null ? (139 <span className="adm-chip adm-chip--haute flex-none !px-2 !text-[8.5px]">Non routé</span>140 ) : null}141 {m.submission_id ? (142 <span className="adm-chip adm-chip--lime flex-none !px-2 !text-[8.5px]">Demande liée</span>143 ) : null}144 </span>145 <span className="mt-[1px] block truncate text-[13px]">146 <span className={m.is_read ? "text-ink-2" : "font-semibold text-ink"}>147 {m.subject || "(sans sujet)"}148 </span>149 {m.snippet ? (150 <span className="text-ink-3"> — {m.snippet}</span>151 ) : null}152 </span>153 </span>154 <span className="flex flex-none flex-col items-end gap-[3px]">155 <span className={`gk-mono text-[10px] ${m.is_read ? "text-ink-3" : "font-bold text-green"}`}>156 {timeAgo(m.created_at)}157 </span>158 <span className="flex items-center gap-2">159 {m.att_count > 0 ? (160 <span className="gk-mono text-[10px] text-ink-3">📎 {m.att_count}</span>161 ) : null}162 {!m.is_read ? (163 <span className="h-[8px] w-[8px] rounded-full bg-green" aria-label="non lu" />164 ) : null}165 </span>166 </span>167 </a>168 {/* actions rapides sans ouvrir le message */}169 <MailRowQuick id={m.id} isRead={!!m.is_read} folder={m.folder} />170 </div>171 );172 })173 )}174 </div>175176 {pages > 1 ? (177 <div className="gk-mono mt-5 flex items-center gap-4 text-[12px] font-bold">178 {page > 1 ? (179 <a href={`/admin/courriel?folder=${folder}&page=${page - 1}${q ? `&q=${encodeURIComponent(q)}` : ""}`} className="underline underline-offset-4">180 ← Précédente181 </a>182 ) : null}183 <span className="text-ink-3">Page {page} / {pages} · {total} message{total > 1 ? "s" : ""}</span>184 {page < pages ? (185 <a href={`/admin/courriel?folder=${folder}&page=${page + 1}${q ? `&q=${encodeURIComponent(q)}` : ""}`} className="underline underline-offset-4">186 Suivante →187 </a>188 ) : null}189 </div>190 ) : null}191 </>192 );193}194