espace-paie-odentas/app/(app)/contrats/ContratsClient.tsx
2025-10-12 17:05:46 +02:00

335 lines
No EOL
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import { api } from "@/lib/fetcher";
import { ChevronLeft, ChevronRight, Loader2, Search, Plus } from "lucide-react";
type ClientInfo = {
id: string;
name: string;
api_name?: string;
} | null;
// --- Types
export type Contrat = {
id: string;
reference: string;
salarie_nom: string; // "LOMPO Mariam"
production: string; // "Les Vies Dansent" / "KB HARMONY"
profession: string; // code + libellé
date_debut: string; // ISO yyyy-mm-dd
date_fin: string; // ISO
etat: "pre-demande" | "Reçue" | "envoye" | "signe" | "modification" | "traitee" | "en_cours";
is_multi_mois?: boolean; // drapeau listé par l'API
regime?: "CDDU_MONO" | "CDDU_MULTI" | "RG" | string; // si l'API renvoie directement le régime
};
// --- Hook d'accès API - MODIFIÉ pour utiliser clientInfo
function useContrats(
clientInfo: ClientInfo,
params: { regime: "CDDU" | "RG"; status: "en_cours" | "termines"; page: number; limit: number; q?: string; month?: number; year?: number; period?: string; }
){
const { regime, status, page, limit, q, month, year, period } = params;
return useQuery({
queryKey: [
"contrats",
regime,
status,
page,
limit,
q,
status === "termines" ? (period || "Y") : undefined,
status === "termines" ? year : undefined,
clientInfo?.id, // AJOUT: inclure l'ID client dans la queryKey
],
queryFn: () => {
const base = `/contrats?regime=${encodeURIComponent(regime)}&status=${status}&page=${page}&limit=${limit}`;
const parts: string[] = [];
if (q) parts.push(`q=${encodeURIComponent(q)}`);
if (status === "termines" && year) {
parts.push(`year=${year}`);
// Dérive month/quarter/semester depuis period
const p = period || "Y";
if (p.startsWith("M")) {
const m = parseInt(p.slice(1), 10);
if (!Number.isNaN(m)) parts.push(`month=${m}`);
} else if (p.startsWith("Q")) {
const qv = parseInt(p.slice(1), 10);
if (!Number.isNaN(qv)) parts.push(`quarter=${qv}`);
} else if (p.startsWith("S")) {
const sv = parseInt(p.slice(1), 10);
if (!Number.isNaN(sv)) parts.push(`semester=${sv}`);
} else {
// "Y" → année entière (pas de mois/quarter/semester)
}
}
const qs = parts.length ? `&${parts.join("&")}` : "";
// MODIFICATION: Passer clientInfo au helper api()
return api<{ items: Contrat[]; page: number; limit: number; hasMore: boolean }>(base + qs, {}, clientInfo);
},
staleTime: 15_000,
placeholderData: (prev) => prev,
enabled: !!clientInfo, // Ne pas exécuter si pas d'infos client
});
}
// --- Mapping état → couleur/texte
const ETATS: Record<Contrat["etat"], { label: string; className: string }> = {
"pre-demande": { label: "Pré-demande", className: "bg-slate-100 text-slate-700" },
"Reçue": { label: "Reçue", className: "bg-blue-100 text-blue-800" },
"envoye": { label: "Envoyé", className: "bg-blue-100 text-blue-800" },
"signe": { label: "Contrat signé", className: "bg-emerald-100 text-emerald-800" },
"modification": { label: "Modifier la demande", className: "bg-rose-100 text-rose-800" },
"traitee": { label: "Traitée", className: "bg-emerald-100 text-emerald-800" },
"en_cours": { label: "En cours", className: "bg-blue-100 text-blue-800" },
};
function humanizeEtat(raw?: string){
if (!raw) return undefined as unknown as string;
const r = raw.toLowerCase();
// D'abord tous les cas "en cours" (y compris "en cours de traitement")
if (r.includes("cours") || r.includes("traitement")) return "en_cours";
// Puis les autres états
if (r.includes("reç") || r.includes("rec")) return "Reçue"; // Reçue / Recue
if (r.includes("envoy")) return "envoye"; // Envoyé / Envoyée
if (r.includes("modif")) return "modification"; // Modification demandée
if (r.includes("sign")) return "signe"; // Signé
if (r.includes("pre")) return "pre-demande"; // Pré-demande
// Traitée : on matche explicitement traitée/traitee, sans attraper "traitement"
if (r.includes("traitée") || r.includes("traitee")) return "traitee";
return undefined as unknown as string; // inconnu → fallback
}
function safeEtat(etat?: string){
const key = humanizeEtat(etat) as keyof typeof ETATS | undefined;
if (key && ETATS[key]) return ETATS[key];
// Fallback affichage simple
const label = etat ? etat.charAt(0).toUpperCase() + etat.slice(1) : "—";
return {
label,
className: "bg-slate-100 text-slate-700",
} as { label: string; className: string };
}
function detailHref(c: Contrat){
const isMulti = c.is_multi_mois === true || (c.regime && c.regime.toUpperCase() === "CDDU_MULTI");
return isMulti ? `/contrats-multi/${c.id}` : `/contrats/${c.id}`;
}
export default function ContratsClient({ clientInfo }: { clientInfo: ClientInfo }) {
const [status, setStatus] = useState<"en_cours" | "termines">("en_cours");
const [page, setPage] = useState(1);
const [limit, setLimit] = useState(10);
const [q, setQ] = useState("");
// Régime: CDDU ou RG
const [regime, setRegime] = useState<"CDDU" | "RG">("CDDU");
// Filtres terminés : période (année entière / semestres / trimestres / mois) + année
const now = new Date();
const [period, setPeriod] = useState<string>(() => "Y"); // par défaut: toute l'année
const [year, setYear] = useState<number>(now.getFullYear());
const yearOptions = useMemo(() => {
const current = now.getFullYear();
const start = 2021;
const count = Math.max(0, current - start + 1);
return Array.from({ length: count }, (_, i) => start + i);
}, [now]);
const { data, isLoading, isError, error, isFetching } = useContrats(clientInfo, {
regime,
status,
page,
limit,
q: q || undefined,
month: status === "termines" && period.startsWith("M") ? parseInt(period.slice(1), 10) : undefined,
year: status === "termines" ? year : undefined,
period: status === "termines" ? period : undefined,
});
const items = data?.items ?? [];
const hasMore = data?.hasMore ?? false;
// Reset pagination quand on change d'onglet
function switchTab(s: "en_cours" | "termines"){
setStatus(s); setPage(1);
}
// Affichage conditionnel si pas d'infos client
if (!clientInfo) {
return (
<div className="space-y-5">
<div className="rounded-2xl border bg-white p-8 text-center">
<div className="text-slate-500">Impossible de récupérer les informations de votre organisation.</div>
</div>
</div>
);
}
return (
<div className="space-y-5">
{/* En-tête + Recherche */}
<section className="rounded-2xl border bg-white p-4">
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
<h1 className="text-xl font-semibold">Contrats & Paies</h1>
<div className="sm:ml-auto flex items-center gap-2 w-full sm:w-auto">
<div className="flex items-center gap-2 px-3 py-2 rounded-xl border w-full sm:w-80">
<Search className="w-4 h-4"/>
<input value={q} onChange={(e)=>{ setQ(e.target.value); setPage(1); }} placeholder="Référence, nom, production…" className="bg-transparent outline-none text-sm flex-1"/>
</div>
</div>
</div>
{/* Régime tabs */}
<div className="mt-3 inline-flex rounded-xl border p-1 bg-slate-50">
<button onClick={()=>{ setRegime("CDDU"); setPage(1); }} className={`px-3 py-1.5 text-sm rounded-lg ${regime==='CDDU' ? 'bg-white shadow border' : 'opacity-80'}`}>CDDU</button>
<button onClick={()=>{ setRegime("RG"); setPage(1); }} className={`px-3 py-1.5 text-sm rounded-lg ${regime==='RG' ? 'bg-white shadow border' : 'opacity-80'}`}>Régime général</button>
</div>
{/* Onglets + action */}
<div className="mt-4 flex items-center justify-between gap-3">
<div className="inline-flex rounded-xl border p-1 bg-slate-50">
<button onClick={()=>switchTab("en_cours")} className={`px-3 py-1.5 text-sm rounded-lg ${status==='en_cours' ? 'bg-white shadow border' : 'opacity-80'}`}>En cours</button>
<button onClick={()=>switchTab("termines")} className={`px-3 py-1.5 text-sm rounded-lg ${status==='termines' ? 'bg-white shadow border' : 'opacity-80'}`}>Terminés</button>
</div>
<a
href={regime === 'CDDU' ? '/contrats/nouveau' : '/contrats-rg/nouveau'}
className="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-emerald-600 text-white hover:bg-emerald-700 whitespace-nowrap"
>
<Plus className="w-4 h-4" /> Nouveau contrat {regime === 'RG' ? 'RG' : 'CDDU'}
</a>
</div>
{status === "termines" && (
<div className="mt-3 flex flex-col sm:flex-row gap-2 sm:items-center">
<div className="text-sm text-slate-600">Filtrer par période :</div>
<div className="flex gap-2">
<select
id="period-select"
value={period}
onChange={(e)=>{ setPeriod(e.target.value); setPage(1); }}
className="px-3 py-2 rounded-lg border bg-white text-sm"
>
<optgroup label="Année">
<option value="Y">Toute l'année</option>
</optgroup>
<optgroup label="Semestres">
<option value="S1">1er semestre (janvjuin)</option>
<option value="S2">2e semestre (juildéc)</option>
</optgroup>
<optgroup label="Trimestres">
<option value="Q1">T1 (janvmars)</option>
<option value="Q2">T2 (avrjuin)</option>
<option value="Q3">T3 (juilsept)</option>
<option value="Q4">T4 (octdéc)</option>
</optgroup>
<optgroup label="Mois">
<option value="M1">Janvier</option>
<option value="M2">Février</option>
<option value="M3">Mars</option>
<option value="M4">Avril</option>
<option value="M5">Mai</option>
<option value="M6">Juin</option>
<option value="M7">Juillet</option>
<option value="M8">Août</option>
<option value="M9">Septembre</option>
<option value="M10">Octobre</option>
<option value="M11">Novembre</option>
<option value="M12">Décembre</option>
</optgroup>
</select>
<select
value={year}
onChange={(e)=>{ setYear(parseInt(e.target.value,10)); setPage(1); }}
className="px-3 py-2 rounded-lg border bg-white text-sm"
>
{yearOptions.map(y => <option key={y} value={y}>{y}</option>)}
</select>
</div>
</div>
)}
</section>
{/* Tableau */}
<section className="rounded-2xl border bg-white">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-slate-50/80">
<Th>État</Th>
<Th>Référence</Th>
<Th>Nom</Th>
<Th>Production</Th>
<Th>Profession</Th>
<Th>Date Début</Th>
<Th>Date Fin</Th>
<Th className="text-right pr-4">Actions</Th>
</tr>
</thead>
<tbody>
{isLoading ? (
<tr><td colSpan={8} className="py-12 text-center text-slate-500"><Loader2 className="w-4 h-4 inline animate-spin mr-2"/> Chargement…</td></tr>
) : isError ? (
<tr><td colSpan={8} className="py-12 text-center text-rose-500">Erreur : {(error as any)?.message || 'imprévue'}</td></tr>
) : items.length === 0 ? (
<tr><td colSpan={8} className="py-12 text-center text-slate-500">{status==='en_cours' ? 'Aucun contrat en cours.' : 'Aucun contrat terminé.'}</td></tr>
) : (
items.map((c)=> (
<tr key={c.id} className="border-b last:border-b-0">
<Td>
{(() => { const e = safeEtat(c.etat as any); return (
<span className={`px-2 py-1 rounded-full text-xs whitespace-nowrap ${e.className}`}>{e.label}</span>
); })()}
</Td>
<Td>
<div className="flex flex-col">
<a href={detailHref(c)} className="underline font-medium">{c.reference}</a>
{(c.is_multi_mois === true || (c.regime && c.regime.toUpperCase() === "CDDU_MULTI")) && (
<span className="mt-1 inline-flex w-fit text-[11px] px-1.5 py-0.5 rounded-full bg-purple-100 text-purple-800">Multimois</span>
)}
</div>
</Td>
<Td>{c.salarie_nom}</Td>
<Td>{c.production}</Td>
<Td>{c.profession}</Td>
<Td>{formatFR(c.date_debut)}</Td>
<Td>{formatFR(c.date_fin)}</Td>
<Td className="text-right pr-4">
<a href={`/contrats/${c.id}/edit`} className="underline">Modifier</a>
</Td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Pagination */}
<div className="p-3 flex items-center gap-3 border-t">
<button onClick={()=> setPage(p=> Math.max(1,p-1))} disabled={page===1} className="px-2 py-1 rounded-lg border disabled:opacity-40"><ChevronLeft className="w-4 h-4"/></button>
<div className="text-sm">Page <strong>{page}</strong></div>
<button onClick={()=> setPage(p=> p + 1)} disabled={!hasMore} className="px-2 py-1 rounded-lg border disabled:opacity-40"><ChevronRight className="w-4 h-4"/></button>
<div className="ml-auto text-sm text-slate-500">{isFetching ? 'Mise à jour' : `${items.length} élément${items.length>1?'s':''}${hasMore ? ' (plus disponibles)' : ''}`}</div>
</div>
</section>
</div>
);
}
function Th({ children, className="" }: any){
return <th className={`text-left font-medium px-3 py-2 ${className}`}>{children}</th>;
}
function Td({ children, className="" }: any){
return <td className={`px-3 py-3 ${className}`}>{children}</td>;
}
function formatFR(iso: string){
// Attend yyyy-mm-dd ou ISO ; renvoie dd/mm/yyyy
const d = new Date(iso);
if (isNaN(d.getTime())) return ""; // rien si invalide
// Masquer la date sentinelle des CDI en cours (01/01/2099)
if (d.getFullYear() === 2099 && d.getMonth() === 0 && d.getDate() === 1) return "";
const dd = String(d.getDate()).padStart(2,'0');
const mm = String(d.getMonth()+1).padStart(2,'0');
const yyyy = d.getFullYear();
return `${dd}/${mm}/${yyyy}`;
}