617 lines
No EOL
28 KiB
TypeScript
617 lines
No EOL
28 KiB
TypeScript
"use client";
|
||
import { useState, useMemo, useEffect } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import { api } from "@/lib/fetcher";
|
||
import { ChevronLeft, ChevronRight, Loader2, Search, Plus, Pencil, Copy, Table } from "lucide-react";
|
||
|
||
// --- 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
|
||
};
|
||
|
||
type ClientInfo = {
|
||
id: string;
|
||
name: string;
|
||
api_name?: string;
|
||
} | null;
|
||
|
||
// --- Hook d'accès API - MODIFIÉ pour récupérer clientInfo dynamiquement
|
||
function useContrats(params: { regime: "CDDU" | "RG"; status: "en_cours" | "termines"; page: number; limit: number; q?: string; month?: number; year?: number; period?: string; org?: string | null }){
|
||
const { regime, status, page, limit, q, month, year, period, org } = params;
|
||
|
||
// Récupération dynamique des infos client via /api/me
|
||
const { data: clientInfo } = useQuery({
|
||
queryKey: ["client-info"],
|
||
queryFn: async () => {
|
||
try {
|
||
const res = await fetch("/api/me", {
|
||
cache: "no-store",
|
||
headers: { Accept: "application/json" },
|
||
credentials: "include",
|
||
});
|
||
if (!res.ok) return null;
|
||
const me = await res.json();
|
||
return {
|
||
id: me.active_org_id || null,
|
||
name: me.active_org_name || "Organisation",
|
||
api_name: me.active_org_api_name,
|
||
} as ClientInfo;
|
||
} catch {
|
||
return null;
|
||
}
|
||
},
|
||
staleTime: 30_000, // Cache 30s
|
||
});
|
||
|
||
return useQuery({
|
||
queryKey: [
|
||
"contrats",
|
||
regime,
|
||
status,
|
||
page,
|
||
limit,
|
||
q,
|
||
status === "termines" ? (period || "Y") : undefined,
|
||
status === "termines" ? year : undefined,
|
||
clientInfo?.id,
|
||
org,
|
||
],
|
||
queryFn: () => {
|
||
const base = `/contrats?regime=${encodeURIComponent(regime)}&status=${status}&page=${page}&limit=${limit}`;
|
||
const parts: string[] = [];
|
||
// If the UI requested an explicit org filter, pass it as `org_id` query param
|
||
if (org) {
|
||
parts.push(`org_id=${encodeURIComponent(org)}`);
|
||
}
|
||
if (q) parts.push(`q=${encodeURIComponent(q)}`);
|
||
if (status === "termines" && year) {
|
||
parts.push(`year=${year}`);
|
||
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}`);
|
||
}
|
||
}
|
||
const qs = parts.length ? `&${parts.join("&")}` : "";
|
||
|
||
// Build final clientInfo to pass to api(): if UI provided explicit org filter, override id
|
||
const finalClientInfo = clientInfo ? { ...clientInfo, id: org ?? clientInfo.id } : (org ? { id: org, name: "Organisation", api_name: undefined } as ClientInfo : null);
|
||
|
||
return api<{ items: Contrat[]; page: number; limit: number; hasMore: boolean; total?: number; totalPages?: number }>(base + qs, {}, finalClientInfo);
|
||
},
|
||
staleTime: 15_000,
|
||
placeholderData: (prev) => prev,
|
||
enabled: !!clientInfo || typeof org === 'string', // allow when we have an explicit org or clientInfo
|
||
});
|
||
}
|
||
|
||
// --- 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 dark:bg-slate-800 dark:text-slate-300" },
|
||
"Reçue": { label: "Reçue", className: "bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300" },
|
||
"envoye": { label: "Envoyé", className: "bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300" },
|
||
"signe": { label: "Contrat signé", className: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300" },
|
||
"modification": { label: "Modifier la demande", className: "bg-rose-100 text-rose-800 dark:bg-rose-900/40 dark:text-rose-300" },
|
||
"traitee": { label: "Traitée", className: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300" },
|
||
"en_cours": { label: "En cours", className: "bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300" },
|
||
};
|
||
|
||
function humanizeEtat(raw?: string){
|
||
if (!raw) return undefined as unknown as string;
|
||
const r = raw.toLowerCase();
|
||
if (r.includes("cours") || r.includes("traitement")) return "en_cours";
|
||
if (r.includes("reç") || r.includes("rec")) return "Reçue";
|
||
if (r.includes("envoy")) return "envoye";
|
||
if (r.includes("modif")) return "modification";
|
||
if (r.includes("sign")) return "signe";
|
||
if (r.includes("pre")) return "pre-demande";
|
||
if (r.includes("traitée") || r.includes("traitee")) return "traitee";
|
||
return undefined as unknown as string;
|
||
}
|
||
|
||
// --- Composant de pagination
|
||
type PaginationProps = {
|
||
page: number;
|
||
totalPages: number;
|
||
total: number;
|
||
limit: number;
|
||
onPageChange: (page: number) => void;
|
||
onLimitChange: (limit: number) => void;
|
||
isFetching?: boolean;
|
||
itemsCount: number;
|
||
position?: 'top' | 'bottom';
|
||
};
|
||
|
||
function Pagination({ page, totalPages, total, limit, onPageChange, onLimitChange, isFetching, itemsCount, position = 'bottom' }: PaginationProps) {
|
||
return (
|
||
<div className={`p-3 flex flex-col sm:flex-row items-center gap-3 ${position === 'bottom' ? 'border-t dark:border-slate-800' : ''}`}>
|
||
{/* Navigation */}
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => onPageChange(Math.max(1, page - 1))}
|
||
disabled={page === 1}
|
||
className="px-2 py-1 rounded-lg border dark:border-slate-800 disabled:opacity-40 hover:bg-slate-50 dark:hover:bg-slate-800 disabled:hover:bg-transparent"
|
||
>
|
||
<ChevronLeft className="w-4 h-4"/>
|
||
</button>
|
||
<div className="text-sm">
|
||
Page <strong>{page}</strong> sur <strong>{totalPages || 1}</strong>
|
||
</div>
|
||
<button
|
||
onClick={() => onPageChange(page + 1)}
|
||
disabled={page >= totalPages}
|
||
className="px-2 py-1 rounded-lg border dark:border-slate-800 disabled:opacity-40 hover:bg-slate-50 dark:hover:bg-slate-800 disabled:hover:bg-transparent"
|
||
>
|
||
<ChevronRight className="w-4 h-4"/>
|
||
</button>
|
||
</div>
|
||
|
||
{/* Sélecteur de limite */}
|
||
<div className="flex items-center gap-2 text-sm">
|
||
<span className="text-slate-600 dark:text-slate-400">Afficher :</span>
|
||
<select
|
||
value={limit}
|
||
onChange={(e) => onLimitChange(parseInt(e.target.value, 10))}
|
||
className="px-2 py-1 rounded-lg border dark:border-slate-800 bg-white dark:bg-slate-900 text-sm"
|
||
>
|
||
<option value={10}>10</option>
|
||
<option value={50}>50</option>
|
||
<option value={100}>100</option>
|
||
</select>
|
||
</div>
|
||
|
||
{/* Informations */}
|
||
<div className="sm:ml-auto text-sm text-slate-600 dark:text-slate-400">
|
||
{isFetching ? (
|
||
<span className="flex items-center gap-1">
|
||
<Loader2 className="w-3 h-3 animate-spin" />
|
||
Mise à jour…
|
||
</span>
|
||
) : (
|
||
<span>
|
||
{total > 0 ? (
|
||
<>
|
||
<strong>{total}</strong> contrat{total > 1 ? 's' : ''} au total
|
||
{itemsCount > 0 && ` • ${itemsCount} affiché${itemsCount > 1 ? 's' : ''}`}
|
||
</>
|
||
) : (
|
||
'Aucun contrat'
|
||
)}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function safeEtat(etat?: string){
|
||
const key = humanizeEtat(etat) as keyof typeof ETATS | undefined;
|
||
if (key && ETATS[key]) return ETATS[key];
|
||
const label = etat ? etat.charAt(0).toUpperCase() + etat.slice(1) : "—";
|
||
return {
|
||
label,
|
||
className: "bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300",
|
||
} as { label: string; className: string };
|
||
}
|
||
|
||
|
||
export default function PageContrats(){
|
||
const [status, setStatus] = useState<"en_cours" | "termines">("en_cours");
|
||
const [page, setPage] = useState(1);
|
||
const [limit, setLimit] = useState(10);
|
||
const [q, setQ] = useState("");
|
||
const [regime, setRegime] = useState<"CDDU" | "RG">("CDDU");
|
||
const router = useRouter();
|
||
|
||
const detailHref = (c: Contrat) => {
|
||
// Si l'utilisateur est sur l'onglet Régime général, on envoie vers la page RG
|
||
if (regime === "RG") {
|
||
return `/contrats-rg/${c.id}`;
|
||
}
|
||
// Sinon, on applique la logique CDDU / multi-mois
|
||
const regimeUpper = (c.regime || "").toUpperCase();
|
||
if (c.is_multi_mois === true || regimeUpper === "CDDU_MULTI") {
|
||
return `/contrats-multi/${c.id}`;
|
||
}
|
||
return `/contrats/${c.id}`;
|
||
};
|
||
|
||
const now = new Date();
|
||
const [period, setPeriod] = useState<string>(() => "Y");
|
||
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]);
|
||
|
||
// We'll call useContrats after selectedOrg is declared below
|
||
|
||
function switchTab(s: "en_cours" | "termines"){
|
||
setStatus(s); setPage(1);
|
||
}
|
||
|
||
// Fetch /api/me to determine staff and organizations list for filter
|
||
const { data: meData } = useQuery({
|
||
queryKey: ["me-info"],
|
||
queryFn: async () => {
|
||
try {
|
||
const res = await fetch('/api/me', { cache: 'no-store', credentials: 'include' });
|
||
if (!res.ok) return null;
|
||
return await res.json();
|
||
} catch { return null; }
|
||
},
|
||
});
|
||
|
||
const [orgs, setOrgs] = useState<Array<{ id: string; name: string }>>([]);
|
||
const [selectedOrg, setSelectedOrg] = useState<string | null>(null);
|
||
|
||
const { data, isLoading, isError, error, isFetching } = useContrats({
|
||
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,
|
||
org: selectedOrg || null,
|
||
});
|
||
const items = data?.items ?? [];
|
||
const hasMore = data?.hasMore ?? false;
|
||
const total = data?.total ?? 0;
|
||
const totalPages = data?.totalPages ?? 0;
|
||
|
||
useEffect(() => {
|
||
// Load organizations for the selector
|
||
let mounted = true;
|
||
(async () => {
|
||
try {
|
||
const r = await fetch('/api/organizations', { cache: 'no-store', credentials: 'include' });
|
||
if (!r.ok) return;
|
||
const json = await r.json();
|
||
if (!mounted) return;
|
||
const items = (json && json.items) ? json.items : (Array.isArray(json) ? json : []);
|
||
setOrgs((items || []).map((o: any) => ({ id: o.id, name: o.name })));
|
||
} catch (e) {
|
||
// ignore
|
||
}
|
||
})();
|
||
return () => { mounted = false; };
|
||
}, []);
|
||
|
||
return (
|
||
<div className="space-y-5">
|
||
{/* En-tête + Recherche */}
|
||
<section className="relative overflow-visible rounded-2xl border bg-white dark:bg-slate-900 dark:border-slate-800 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 dark:border-slate-800 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>
|
||
{/* Organization filter visible only for staff users */}
|
||
{meData?.is_staff && (
|
||
<select
|
||
value={selectedOrg || ""}
|
||
onChange={(e) => { setSelectedOrg(e.target.value || null); setPage(1); }}
|
||
className="px-3 py-2 rounded-lg border dark:border-slate-800 bg-white dark:bg-slate-900 text-sm"
|
||
>
|
||
<option value="">Toutes les structures</option>
|
||
{orgs.map(o => <option key={o.id} value={o.id}>{o.name}</option>)}
|
||
</select>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Régime tabs */}
|
||
<div className="mt-3 inline-flex rounded-xl border dark:border-slate-800 p-1 bg-slate-50 dark:bg-slate-800/50">
|
||
<button onClick={()=>{ setRegime("CDDU"); setPage(1); }} className={`px-3 py-1.5 text-sm rounded-lg ${regime==='CDDU' ? 'bg-white dark:bg-slate-900 shadow border dark:border-slate-700' : 'opacity-80'}`}>CDDU</button>
|
||
<button onClick={()=>{ setRegime("RG"); setPage(1); }} className={`px-3 py-1.5 text-sm rounded-lg ${regime==='RG' ? 'bg-white dark:bg-slate-900 shadow border dark:border-slate-700' : 'opacity-80'}`}>Régime général</button>
|
||
</div>
|
||
|
||
{/* Onglets + action */}
|
||
<div className="mt-4 flex items-center gap-3">
|
||
<div className="inline-flex rounded-xl border dark:border-slate-800 p-1 bg-slate-50 dark:bg-slate-800/50">
|
||
<button onClick={()=>switchTab("en_cours")} className={`px-3 py-1.5 text-sm rounded-lg ${status==='en_cours' ? 'bg-white dark:bg-slate-900 shadow border dark:border-slate-700' : 'opacity-80'}`}>En cours</button>
|
||
<button onClick={()=>switchTab("termines")} className={`px-3 py-1.5 text-sm rounded-lg ${status==='termines' ? 'bg-white dark:bg-slate-900 shadow border dark:border-slate-700' : 'opacity-80'}`}>Terminés</button>
|
||
</div>
|
||
<div className="ml-auto flex items-center gap-2">
|
||
<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>
|
||
{regime === 'CDDU' && (
|
||
(() => {
|
||
const isStaff = Boolean(meData?.is_staff);
|
||
if (isStaff) {
|
||
return (
|
||
<a
|
||
href="/contrats/nouveau/saisie-tableau"
|
||
className="inline-flex items-center gap-2 px-3 py-2 rounded-lg border dark:border-slate-800 bg-white hover:bg-slate-50 dark:bg-slate-900 dark:hover:bg-slate-800 whitespace-nowrap"
|
||
>
|
||
<Table className="w-4 h-4" /> Saisie en tableau
|
||
</a>
|
||
);
|
||
}
|
||
return (
|
||
<div className="group relative inline-block">
|
||
<button
|
||
type="button"
|
||
aria-disabled="true"
|
||
disabled
|
||
className="inline-flex items-center gap-2 px-3 py-2 rounded-lg border dark:border-slate-800 bg-white text-slate-400 dark:text-slate-500 opacity-60 cursor-not-allowed whitespace-nowrap"
|
||
>
|
||
<Table className="w-4 h-4" /> Saisie en tableau
|
||
</button>
|
||
<div
|
||
role="tooltip"
|
||
className="pointer-events-none absolute right-0 top-full mt-2 z-10 w-64 px-3 py-2 rounded-lg bg-slate-900 text-white dark:bg-slate-800 text-xs shadow-lg opacity-0 group-hover:opacity-100 transition"
|
||
>
|
||
Cette fonction sera bientôt disponible.
|
||
<div className="absolute left-4 -top-1 w-2 h-2 rotate-45 bg-slate-900 dark:bg-slate-800" />
|
||
</div>
|
||
</div>
|
||
);
|
||
})()
|
||
)}
|
||
</div>
|
||
</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 dark:text-slate-300">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 dark:border-slate-800 bg-white dark:bg-slate-900 text-sm"
|
||
>
|
||
<optgroup label="Année">
|
||
<option value="Y">Toute l'année</option>
|
||
</optgroup>
|
||
<optgroup label="Semestres">
|
||
<option value="S1">1er semestre (janv–juin)</option>
|
||
<option value="S2">2e semestre (juil–déc)</option>
|
||
</optgroup>
|
||
<optgroup label="Trimestres">
|
||
<option value="Q1">T1 (janv–mars)</option>
|
||
<option value="Q2">T2 (avr–juin)</option>
|
||
<option value="Q3">T3 (juil–sept)</option>
|
||
<option value="Q4">T4 (oct–dé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 dark:border-slate-800 bg-white dark:bg-slate-900 text-sm"
|
||
>
|
||
{yearOptions.map(y => <option key={y} value={y}>{y}</option>)}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</section>
|
||
|
||
{/* Pagination supérieure */}
|
||
{items.length > 0 && (
|
||
<section className="rounded-2xl border bg-white dark:bg-slate-900 dark:border-slate-800">
|
||
<Pagination
|
||
page={page}
|
||
totalPages={totalPages}
|
||
total={total}
|
||
limit={limit}
|
||
onPageChange={(newPage) => setPage(newPage)}
|
||
onLimitChange={(newLimit) => { setLimit(newLimit); setPage(1); }}
|
||
isFetching={isFetching}
|
||
itemsCount={items.length}
|
||
position="top"
|
||
/>
|
||
</section>
|
||
)}
|
||
|
||
{/* Tableau */}
|
||
<section className="rounded-2xl border bg-white dark:bg-slate-900 dark:border-slate-800">
|
||
<div className="overflow-x-auto overflow-visible pb-6">
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="border-b dark:border-slate-800 bg-slate-50/80 dark:bg-slate-800/40">
|
||
<Th>État</Th>
|
||
<Th>Référence</Th>
|
||
<Th>Salarié</Th>
|
||
{/* Structure column visible only to staff: we rely on server /api/me to set clientInfo.isStaff via cookie/session. */}
|
||
<Th className="hidden sm:table-cell">Structure</Th>
|
||
<Th>{regime === 'RG' ? 'Analytique' : 'Production'}</Th>
|
||
<Th>Profession</Th>
|
||
<Th>Début</Th>
|
||
<Th>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 dark:border-slate-800">
|
||
<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 dark:bg-purple-900/40 dark:text-purple-200">Multi‑mois</span>
|
||
)}
|
||
</div>
|
||
</Td>
|
||
<Td>{c.salarie_nom}</Td>
|
||
<Td className="hidden sm:table-cell">{(c as any).org_name || ''}</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">
|
||
<div className="inline-flex items-center gap-2">
|
||
{
|
||
// Détecter si le contrat est en Régime général
|
||
}
|
||
{(() => {
|
||
const regimeUpper = (c.regime || "").toUpperCase();
|
||
const isRG = regimeUpper === "RG";
|
||
// Bouton Modifier : si RG => afficher variante désactivée avec tooltip
|
||
if (isRG) {
|
||
return (
|
||
<div className="group relative inline-block">
|
||
<button
|
||
type="button"
|
||
aria-disabled="true"
|
||
disabled
|
||
className="inline-flex items-center justify-center w-8 h-8 rounded-lg border dark:border-slate-800 opacity-60 cursor-not-allowed"
|
||
>
|
||
<Pencil className="w-4 h-4" />
|
||
</button>
|
||
<div
|
||
role="tooltip"
|
||
className="pointer-events-none absolute right-full top-1/2 -translate-y-1/2 z-10 mr-2 w-72 px-3 py-2 rounded-lg bg-slate-900 text-white dark:bg-slate-800 text-xs shadow-lg opacity-0 group-hover:opacity-100 transition"
|
||
>
|
||
La modification et la duplication d'un contrat Régime général n'est pas encore possible depuis l'Espace Paie, veuillez nous contacter.
|
||
<div className="absolute -right-1 top-1/2 -translate-y-1/2 w-2 h-2 rotate-45 bg-slate-900 dark:bg-slate-800" />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// sinon lien normal vers l'édition
|
||
return (
|
||
<a
|
||
href={`/contrats/${c.id}/edit`}
|
||
className="inline-flex items-center justify-center w-8 h-8 rounded-lg border dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800"
|
||
aria-label="Modifier le contrat"
|
||
title="Modifier le contrat"
|
||
>
|
||
<Pencil className="w-4 h-4" />
|
||
</a>
|
||
);
|
||
})()}
|
||
|
||
{(() => {
|
||
const regimeUpper = (c.regime || "").toUpperCase();
|
||
const isRG = regimeUpper === "RG";
|
||
if (isRG) {
|
||
return (
|
||
<div className="group relative inline-block">
|
||
<button
|
||
type="button"
|
||
disabled
|
||
aria-disabled="true"
|
||
className="inline-flex items-center justify-center w-8 h-8 rounded-lg border dark:border-slate-800 opacity-60 cursor-not-allowed"
|
||
>
|
||
<Copy className="w-4 h-4" />
|
||
</button>
|
||
<div
|
||
role="tooltip"
|
||
className="pointer-events-none absolute right-full top-1/2 -translate-y-1/2 z-10 mr-2 w-72 px-3 py-2 rounded-lg bg-slate-900 text-white dark:bg-slate-800 text-xs shadow-lg opacity-0 group-hover:opacity-100 transition"
|
||
>
|
||
La modification et la duplication d'un contrat Régime général n'est pas encore possible depuis l'Espace Paie, veuillez nous contacter.
|
||
<div className="absolute -right-1 top-1/2 -translate-y-1/2 w-2 h-2 rotate-45 bg-slate-900 dark:bg-slate-800" />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<button
|
||
type="button"
|
||
onClick={() => router.push(`/contrats/nouveau?dupe_id=${encodeURIComponent(c.id)}`)}
|
||
className="inline-flex items-center justify-center w-8 h-8 rounded-lg border dark:border-slate-800 hover:bg-slate-50 dark:hover:bg-slate-800"
|
||
aria-label="Dupliquer le contrat"
|
||
title="Dupliquer le contrat"
|
||
>
|
||
<Copy className="w-4 h-4" />
|
||
</button>
|
||
);
|
||
})()}
|
||
</div>
|
||
</Td>
|
||
</tr>
|
||
))
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* Pagination inférieure */}
|
||
<Pagination
|
||
page={page}
|
||
totalPages={totalPages}
|
||
total={total}
|
||
limit={limit}
|
||
onPageChange={(newPage) => setPage(newPage)}
|
||
onLimitChange={(newLimit) => { setLimit(newLimit); setPage(1); }}
|
||
isFetching={isFetching}
|
||
itemsCount={items.length}
|
||
position="bottom"
|
||
/>
|
||
</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){
|
||
const d = new Date(iso);
|
||
if (isNaN(d.getTime())) return "";
|
||
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}`;
|
||
} |