SPB Git

Repository Public

page.tsx 8.7 KB 194 lines tsx

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 &amp; 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
Files main
Menu

Theme