Repository Public
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'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