SPB Git

Repository Public

page.tsx 4.1 KB 100 lines tsx

1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// /admin/candidatures — le pipeline RH : les demandes « carrieres » vues3// comme un processus d'embauche (nouvelle → à analyser → entrevue → offre →4// embauchée / refusée), avec compteur par étape et liste filtrable.5import { redirect } from "next/navigation";6import { getAdminSession, canViewCategory } from "@/lib/comms/admin-auth";7import { listSubmissions } from "@/lib/comms/queries";8import { JOB_STATUSES, PRIORITIES } from "@/lib/comms/categories";910export const dynamic = "force-dynamic";1112function fmtDate(iso: string): string {13  return iso.slice(0, 10);14}1516export default async function CandidaturesPage({17  searchParams,18}: {19  searchParams: Promise<Record<string, string | string[] | undefined>>;20}) {21  const session = await getAdminSession();22  if (!session) redirect("/admin/connexion");23  if (!canViewCategory(session, "carrieres"))24    return (25      <p className="gk-mono mt-10 text-[12px] text-ink-3">26        Votre rôle ne donne pas accès aux candidatures (rôle « Ressources humaines » requis).27      </p>28    );29  const sp = await searchParams;30  const stage = typeof sp.stage === "string" && JOB_STATUSES[sp.stage] ? sp.stage : "";3132  const { rows: all } = listSubmissions(session, {33    category: "carrieres",34    view: "actives",35    page: 1,36  });37  const byStage = new Map<string, number>();38  for (const r of all) byStage.set(r.status, (byStage.get(r.status) ?? 0) + 1);39  const rows = stage ? all.filter((r) => r.status === stage) : all;4041  return (42    <>43      <p className="kicker">Candidatures</p>44      <h1 className="gk-display mt-2 text-[clamp(22px,3vw,30px)] font-bold tracking-[-0.03em]">45        Pipeline d&apos;embauche · {all.length} candidature{all.length > 1 ? "s" : ""}46      </h1>4748      <div className="mt-6 flex flex-wrap gap-2">49        <a href="/admin/candidatures" className={`adm-chip no-underline ${!stage ? "adm-chip--lime" : ""}`}>50          Toutes · {all.length}51        </a>52        {Object.entries(JOB_STATUSES).map(([k, v]) => (53          <a54            key={k}55            href={`/admin/candidatures?stage=${k}`}56            className={`adm-chip no-underline ${stage === k ? "adm-chip--lime" : ""}`}57          >58            {v} · {byStage.get(k) ?? 0}59          </a>60        ))}61      </div>6263      <div className="gk-card mt-6 overflow-hidden !shadow-none">64        {rows.length === 0 ? (65          <p className="gk-mono p-6 text-[12px] text-ink-3">Aucune candidature à cette étape.</p>66        ) : (67          rows.map((r) => (68            <a69              key={r.id}70              href={`/admin/demande/${r.reference}`}71              className={`adm-row ${r.is_read ? "" : "adm-row--unread"}`}72            >73              <div className="flex flex-wrap items-center gap-x-3 gap-y-1">74                <span className="gk-mono text-[11px] font-bold text-green">{r.reference}</span>75                <span className="adm-chip adm-chip--ghost !text-[9px]">76                  {JOB_STATUSES[r.status] ?? r.status}77                </span>78                <span className={`adm-chip adm-chip--${r.priority} !text-[9px]`}>79                  {PRIORITIES[r.priority as keyof typeof PRIORITIES] ?? r.priority}80                </span>81                <span className="gk-mono ml-auto text-[10.5px] text-ink-3">{fmtDate(r.created_at)}</span>82              </div>83              <p className="mt-1 truncate text-[13.5px]">84                <strong>{[r.first_name, r.last_name].filter(Boolean).join(" ") || r.email}</strong>85                {r.job_title ? ` — ${r.job_title}` : ""}86                {r.sub_category?.startsWith("Candidature spontanée") ? " · spontanée" : ""}87                {r.assignee ? <span className="gk-mono text-[11px] text-ink-3"> · suivi : {r.assignee}</span> : null}88              </p>89            </a>90          ))91        )}92      </div>93      <p className="gk-mono mt-4 text-[10.5px] leading-relaxed text-ink-3">94        Les CV et documents personnels sont accessibles depuis chaque fiche,95        réservés aux rôles autorisés, et servis hors du dossier public.96      </p>97    </>98  );99}100
Files main
Menu

Theme