espace-paie-odentas/app/(app)/contrats-multi/[id]/page.tsx
2025-10-12 17:05:46 +02:00

800 lines
32 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 Link from "next/link";
import { useParams } from "next/navigation";
import { useMemo, useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { ArrowLeft, Download, Info, Loader2, Clock, CheckCircle, Euro } from "lucide-react";
import { NotesSection } from "@/components/NotesSection";
import { Button } from "@/components/ui/button";
import { ConfirmationModal } from "@/components/ui/confirmation-modal";
import { toast } from "sonner";
import { usePageTitle } from "@/hooks/usePageTitle";
/* =========================
Types attendus du backend
========================= */
type StatutSimple = "oui" | "non" | "na" | "en_attente";
type EtatDemande =
| "pre-demande"
| "demande-recue"
| "envoye"
| "signe"
| "modification"
| "traitee"
| "non_commence";
type ContratMultiDetail = {
id: string;
numero: string; // ex YW2KSC85
regime: "CDDU_MULTI";
is_multi_mois: true; // drapeau utile
salarie: { nom: string; email?: string };
salarie_matricule?: string; // Matricule API du/de la salarié·e pour lien fiche
production: string;
objet?: string;
profession: string;
categorie_prof?: string;
type_salaire?: string;
salaire_demande?: string;
date_debut: string; // ISO
date_fin: string; // ISO
panier_repas?: StatutSimple;
// État & signatures
etat_demande: EtatDemande;
contrat_signe_employeur: StatutSimple;
contrat_signe_salarie: StatutSimple;
etat_contrat?: "non_commence" | "en_cours" | "termine";
// PDFs rattachés à la demande
pdf_contrat?: { available: boolean; url?: string };
pdf_avenant?: { available: boolean; url?: string };
// Déclarations
dpae?: "a_traiter" | "envoyee" | "refusee" | "retour_ok";
// Temps de travail cumulé
jours_travailles?: string | number;
nb_representations?: number;
nb_services_repetitions?: number;
nb_heures_repetitions?: number;
nb_heures_annexes?: number;
nb_cachets_aem?: number;
nb_heures_aem?: number;
};
type PaieMulti = {
id: string;
ordre: number; // 1,2,3… (Numéro de paie)
mois: number; // 1..12
annee: number; // 2024
aem_statut?: "a_traiter" | "ok" | string | null;
paie_pdf?: string | null; // URL
net_a_payer?: string | null;
net_avant_pas?: string | null;
brut?: string | null;
cout_total?: string | null;
transfer_done?: boolean | null; // Champ pour le statut de paiement (même que payslips)
traite?: "oui" | "non" | string | null;
paie_traitee?: string | null; // Période de paie (ex: "01/08/2025 31/08/2025")
};
/* ==============
Data fetching
============== */
function useContrat(id: string) {
return useQuery({
queryKey: ["contrat-multi", id],
queryFn: async () => {
const res = await fetch(`/api/contrats/${id}`, {
credentials: "include",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
});
if (res.status === 403) {
const msg = await res.text().catch(() => "");
throw new Error(msg || "access_denied");
}
if (res.status === 404) {
throw new Error("not_found");
}
if (!res.ok) {
const t = await res.text().catch(() => "");
throw new Error(t || `http_${res.status}`);
}
const data = (await res.json()) as ContratMultiDetail;
return data;
},
staleTime: 15_000,
enabled: Boolean(id),
retry: (failureCount, error: any) => {
const msg = String(error?.message || "");
if (msg.includes("access_denied") || msg.includes("not_found")) return false;
return failureCount < 3;
},
});
}
function usePaies(id: string) {
return useQuery({
queryKey: ["contrat-multi-paies", id],
queryFn: async () => {
if (process.env.NODE_ENV !== 'production') {
console.log("🔍 Fetching paies for contract:", id);
}
const res = await fetch(`/api/contrats/${id}/paies?regime=CDDU_MULTI`, {
credentials: "include",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
});
if (process.env.NODE_ENV !== 'production') {
console.log("📥 Paies response:", { status: res.status, statusText: res.statusText, contractId: id });
}
if (res.status === 403) {
const msg = await res.text().catch(() => "");
if (process.env.NODE_ENV !== 'production') {
console.error("❌ Paies access denied:", msg);
}
throw new Error(msg || "access_denied");
}
if (res.status === 404) {
if (process.env.NODE_ENV !== 'production') {
console.log(" Paies not found (404), returning empty list");
}
// Pas bloquant pour la page : retourne une liste vide
return { items: [] as PaieMulti[] };
}
if (!res.ok) {
const t = await res.text().catch(() => "");
if (process.env.NODE_ENV !== 'production') {
console.error("❌ Paies error:", { status: res.status, error: t, contractId: id });
}
throw new Error(t || `http_${res.status}`);
}
const data = (await res.json()) as { items: PaieMulti[] };
if (process.env.NODE_ENV !== 'production') {
console.log("✅ Paies data received:", { contractId: id, itemsCount: data.items?.length || 0, data });
}
return data;
},
staleTime: 15_000,
enabled: Boolean(id),
retry: (failureCount, error: any) => {
const msg = String(error?.message || "");
if (msg.includes("access_denied")) return false;
return failureCount < 3;
},
});
}
/* =========
Helpers
========= */
function Section({ title, children }: { title: React.ReactNode; children: React.ReactNode }) {
return (
<section className="rounded-2xl border bg-white">
<div className="px-4 py-3 border-b font-medium text-slate-700 bg-slate-50/60">
{title}
</div>
<div className="p-4">{children}</div>
</section>
);
}
function formatPeriodDisplay(periodStr?: string | null) {
if (!periodStr) return null;
// Expected formats: "YYYY-MM-DD YYYY-MM-DD" or similar. Split on '' or '-'
const parts = periodStr.split(/\s*\s*|\s*-\s*/);
if (parts.length === 0) return periodStr;
const left = parts[0]?.trim();
const right = parts[1]?.trim();
try {
const parseISO = (s?: string | null) => {
if (!s) return null;
// If format is YYYY-MM-DD or YYYY-MM, ensure correct parsing
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
const m2 = s.match(/^(\d{4})-(\d{2})$/);
if (m2) return new Date(Number(m2[1]), Number(m2[2]) - 1, 1);
const d = new Date(s);
return isNaN(d.getTime()) ? null : d;
};
const L = parseISO(left);
const R = parseISO(right);
if (L && R) return `${formatDateFR(L)} ${formatDateFR(R)}`;
if (L) return formatDateFR(L);
if (R) return formatDateFR(R);
return periodStr;
} catch (e) {
return periodStr;
}
}
function formatDateFR(iso?: string | Date) {
if (!iso) return "—";
const d = iso instanceof Date ? iso : new Date(String(iso));
if (isNaN(d.getTime())) return String(iso);
return d.toLocaleDateString("fr-FR", { day: "2-digit", month: "2-digit", year: "numeric" });
}
function Field({
label,
value,
action,
}: {
label: string;
value?: React.ReactNode;
action?: React.ReactNode;
}) {
return (
<div className="grid grid-cols-1 sm:grid-cols-[220px_1fr_auto] gap-2 items-center py-2">
<div className="text-sm text-slate-500">{label}</div>
<div className="text-sm">{value ?? <span className="text-slate-400"></span>}</div>
{action ? <div className="justify-self-end">{action}</div> : null}
</div>
);
}
function Badge({
children,
tone = "default" as "default" | "ok" | "warn" | "error" | "info",
}: {
children: React.ReactNode;
tone?: "default" | "ok" | "warn" | "error" | "info";
}) {
const cls =
tone === "ok"
? "bg-emerald-100 text-emerald-800"
: tone === "warn"
? "bg-amber-100 text-amber-800"
: tone === "error"
? "bg-rose-100 text-rose-800"
: tone === "info"
? "bg-sky-100 text-sky-800"
: "bg-slate-100 text-slate-700";
return <span className={`text-xs px-2 py-1 rounded-full ${cls}`}>{children}</span>;
}
function boolBadge(v?: StatutSimple | boolean | null) {
if (typeof v === "boolean") return v ? <Badge tone="ok">Oui</Badge> : <Badge tone="error">Non</Badge>;
if (v === "oui") return <Badge tone="ok">Oui</Badge>;
if (v === "non") return <Badge tone="error">Non</Badge>;
if (v === "en_attente") return <Badge tone="info">En cours</Badge>;
return <Badge>n/a</Badge>;
}
function stateBadgeDemande(s?: EtatDemande) {
const input = String(s || "").trim();
const normalized = input
.toLowerCase()
.normalize("NFD")
.replace(/\p{Diacritic}/gu, "")
.replace(/\s+/g, " ")
.replace(/-/g, " ")
.replace(/_/g, " ");
// Mapping des 4 états principaux avec couleurs adaptées
type Style = { bg: string; border: string; text: string; label: string };
// Détection intelligente des états
let style: Style;
if (normalized.includes("pre") || normalized.includes("demande")) {
// Pré-demande - Gris (neutre)
style = {
bg: "bg-slate-50",
border: "border-slate-200",
text: "text-slate-700",
label: "Pré-demande"
};
} else if (normalized.includes("recu") || normalized.includes("recue")) {
// Reçue - Bleu (information)
style = {
bg: "bg-blue-50",
border: "border-blue-200",
text: "text-blue-800",
label: "Reçue"
};
} else if (normalized.includes("cours") || normalized.includes("traitement")) {
// En cours de traitement - Orange (en attente)
style = {
bg: "bg-orange-50",
border: "border-orange-200",
text: "text-orange-800",
label: "En cours de traitement"
};
} else if (normalized.includes("traitee") || normalized.includes("traite")) {
// Traitée - Vert (succès)
style = {
bg: "bg-emerald-50",
border: "border-emerald-200",
text: "text-emerald-800",
label: "Traitée"
};
} else {
// Fallback - Affichage de la valeur brute avec style neutre
style = {
bg: "bg-slate-50",
border: "border-slate-200",
text: "text-slate-700",
label: input || "—"
};
}
return (
<div className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg border text-xs ${style.bg} ${style.border} ${style.text}`}>
{style.label}
</div>
);
}
function formatEUR(v?: string | number | null) {
if (v === null || v === undefined || v === "") return "—";
// Autorise nombres sous forme de chaîne avec séparateurs
const num = typeof v === "number" ? v : Number(String(v).replace(/[^0-9,.-]/g, "").replace(",", "."));
if (!Number.isFinite(num)) return String(v);
return num.toLocaleString("fr-FR", { style: "currency", currency: "EUR" });
}
function monthLabelSafe(m?: number) {
if (typeof m !== "number" || !Number.isFinite(m) || m < 1 || m > 12) return null;
return new Date(2025, m - 1, 1).toLocaleDateString("fr-FR", { month: "long" });
}
function payLabel(p: PaieMulti) {
const ml = monthLabelSafe(p.mois);
if (ml && p.annee) return `${ml} ${p.annee}`;
if (ml) return ml;
if (p.annee) return String(p.annee);
return "";
}
/* =====
Page
===== */
export default function ContratMultiPage() {
const { id } = useParams<{ id: string }>();
const { data, isLoading, isError, error } = useContrat(id);
// Titre dynamique basé sur le numéro du contrat
const contractTitle = data?.numero ? `Contrat ${data.numero}` : `Contrat multi-mois`;
usePageTitle(contractTitle);
const { data: paiesData, isLoading: paiesLoading, isError: paiesError, error: paiesErrorObj } = usePaies(id);
// State pour la modale de confirmation de paiement
const [showPaymentModal, setShowPaymentModal] = useState<boolean>(false);
const [selectedPaieId, setSelectedPaieId] = useState<string>("");
const [selectedPaieStatus, setSelectedPaieStatus] = useState<boolean>(false);
// Query client pour la mise à jour du cache
const queryClient = useQueryClient();
// Mutation pour marquer une paie multi comme payée/non payée
const markAsPaidMutation = useMutation({
mutationFn: async ({ payslipId, transferDone }: { payslipId: string; transferDone: boolean }) => {
const response = await fetch(`/api/payslips/${payslipId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ transfer_done: transferDone }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Erreur lors de la mise à jour');
}
return response.json();
},
onSuccess: (_, variables) => {
// Recharger les données des paies
queryClient.invalidateQueries({ queryKey: ["contrat-multi-paies", id] });
toast.success(variables.transferDone ? "Paiement marqué comme effectué !" : "Paiement marqué comme non effectué !");
setShowPaymentModal(false);
setSelectedPaieId("");
setSelectedPaieStatus(false);
},
onError: (error: Error) => {
toast.error("Erreur : " + error.message);
},
});
// Fonction pour ouvrir le modal de confirmation
const handleTogglePayment = (paieId: string, currentStatus: boolean) => {
setSelectedPaieId(paieId);
setSelectedPaieStatus(!currentStatus);
setShowPaymentModal(true);
};
// Fonction pour confirmer le changement de statut
const confirmTogglePayment = () => {
if (selectedPaieId) {
markAsPaidMutation.mutate({
payslipId: selectedPaieId,
transferDone: selectedPaieStatus
});
}
};
// Calcule l'état du contrat en fonction des dates et de l'état de la demande (même logique que contrats/[id])
const etatContratCalcule = useMemo(() => {
if (!data) return undefined;
// Si l'état de la demande est Annulée → Annulé
const etatDemandeNorm = String(data.etat_demande || "")
.toLowerCase()
.normalize("NFD")
.replace(/\p{Diacritic}/gu, "");
if (etatDemandeNorm.includes("annule")) {
return "Annulé" as const;
}
// Si dates manquantes, on retombe sur l'ancien fallback
if (!data.date_debut || !data.date_fin) return undefined;
const toLocalDateOnly = (iso: string) => {
const d = new Date(iso);
// Normalise à minuit local pour une comparaison en J/J inclusif
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
};
const start = toLocalDateOnly(data.date_debut);
const end = toLocalDateOnly(data.date_fin);
const today = new Date();
const current = new Date(today.getFullYear(), today.getMonth(), today.getDate());
if (current < start) return "Non commencé" as const;
if (current > end) return "Terminé" as const;
return "En cours" as const; // inclusif entre début et fin
}, [data]);
const title = useMemo(() => (data ? `Contrat n° ${data.numero}` : "Contrat"), [data]);
if (isLoading) {
return (
<div className="rounded-2xl border bg-white p-10 text-center text-slate-500">
<Loader2 className="w-4 h-4 inline animate-spin mr-2" />
Chargement du contrat
</div>
);
}
if (isError || !data) {
return (
<div className="rounded-2xl border bg-white p-6">
<div className="text-rose-600 font-medium mb-2">Impossible de charger ce contrat.</div>
<div className="text-sm text-slate-500">{(error as any)?.message || "Erreur inconnue"}</div>
<div className="mt-4">
<Link className="inline-flex items-center gap-2 px-3 py-2 rounded-lg border" href="/contrats">
<ArrowLeft className="w-4 h-4" /> Retour aux contrats
</Link>
</div>
</div>
);
}
const paies = paiesData?.items ?? [];
return (
<div className="space-y-5">
{/* Titre + breadcrumb */}
<div className="flex flex-col md:flex-row md:items-center gap-3">
<Link href="/contrats" className="inline-flex items-center gap-1 text-sm underline">
<ArrowLeft className="w-4 h-4" /> Retour contrats terminés
</Link>
</div>
<div className="rounded-2xl border bg-white p-4">
<div className="flex flex-wrap items-center gap-2">
<div className="text-lg font-semibold">{title}</div>
<div className="h-4 w-px bg-slate-200 mx-1" />
<div className="text-sm text-slate-500">CDDU · Multi-mois</div>
</div>
</div>
{/* Disposition 2 colonnes (colonnes indépendantes en hauteur) */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
{/* Colonne gauche : Demande puis Temps de travail réel */}
<div className="flex flex-col gap-5">
<Section title="Demande">
<Field
label="Salarié"
value={
data.salarie_matricule ? (
<Link href={`/salaries/${encodeURIComponent(data.salarie_matricule)}`} className="underline">
{data.salarie?.nom || data.salarie_matricule}
</Link>
) : (
data.salarie?.nom
)
}
/>
<Field
label="Contrat de travail PDF"
value={
data.pdf_contrat?.available ? (
<a className="inline-flex items-center gap-2 underline" href={data.pdf_contrat.url} target="_blank" rel="noreferrer">
<Download className="w-4 h-4" /> Télécharger
</a>
) : (
<span className="text-slate-400">Bientôt disponible</span>
)
}
/>
<Field
label="Avenant contrat PDF"
value={
data.pdf_avenant?.available ? (
<a className="inline-flex items-center gap-2 underline" href={data.pdf_avenant.url} target="_blank" rel="noreferrer">
<Download className="w-4 h-4" /> Télécharger
</a>
) : (
<span className="text-slate-400">n/a</span>
)
}
/>
<Field label="État de la demande" value={stateBadgeDemande(data.etat_demande)} />
<Field label="Contrat signé par employeur" value={boolBadge(data.contrat_signe_employeur)} />
<Field label="Contrat signé par salarié·e" value={boolBadge(data.contrat_signe_salarie)} />
<Field
label="État du contrat"
value={
etatContratCalcule === "Annulé" ? (
<Badge tone="error">Annulé</Badge>
) : etatContratCalcule === "En cours" ? (
<Badge tone="info">En cours</Badge>
) : etatContratCalcule === "Terminé" ? (
<Badge tone="ok">Terminé</Badge>
) : etatContratCalcule === "Non commencé" ? (
<Badge>Non commencé</Badge>
) : (
// Fallback sur l'ancien champ si le calcul n'est pas possible
data.etat_contrat === "en_cours" ? <Badge tone="info">En cours</Badge>
: data.etat_contrat === "termine" ? <Badge tone="ok">Terminé</Badge>
: <Badge>Non commencé</Badge>
)
}
/>
<Field label="Spectacle" value={data.production} />
<Field label="Numéro dobjet" value={data.objet} />
<Field label="Profession" value={data.profession} />
<Field label="Catégorie professionnelle" value={data.categorie_prof} />
<Field label="Type de salaire demandé" value={data.type_salaire} />
<Field label="Salaire demandé" value={data.salaire_demande} />
<Field label="Début contrat" value={formatDateFR(data.date_debut)} />
<Field label="Fin contrat" value={formatDateFR(data.date_fin)} />
<Field label="Panier repas" value={boolBadge(data.panier_repas)} />
</Section>
<Section title="Temps de travail réel">
<Field label="Jours travaillés" value={data.jours_travailles ?? "—"} />
<Field label="Nbre de représentations" value={data.nb_representations ?? 0} />
<Field label="Nbre de services répétitions" value={data.nb_services_repetitions ?? 0} />
<Field label="Nbre d'heures répétitions" value={data.nb_heures_repetitions ?? 0} />
<Field label="Nbre d'heures Annexes 8" value={data.nb_heures_annexes ?? 0} />
<Field label="Nombre de cachets AEM" value={data.nb_cachets_aem ?? 0} />
<Field label="Nombre d'heures AEM" value={data.nb_heures_aem ?? 0} />
</Section>
</div>
{/* Colonne droite : Déclarations au-dessus des Paies */}
<div className="flex flex-col gap-5">
<Section title="Déclarations">
{(() => {
const raw = String(data.dpae || "");
const norm = raw.normalize("NFD").replace(/\p{Diacritic}/gu, "").trim().toLowerCase();
if (norm === "ok" || norm === "envoyee" || norm === "retourok" || norm === "retour_ok") {
return (
<Field
label="DPAE"
value={(
<span className="inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-800">
<CheckCircle className="w-3 h-3" /> Effectuée
</span>
)}
/>
);
}
if (norm === "a traiter" || norm === "a_traiter" || norm === "a_traiter") {
return (
<Field
label="DPAE"
value={(
<span className="inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded-full bg-amber-100 text-amber-800">
<Clock className="w-3 h-3" /> En cours
</span>
)}
/>
);
}
return <Field label="DPAE" value={<span className="text-xs text-slate-400"></span>} />;
})()}
</Section>
<Section title={<><span>Paies</span><span className="ml-2 text-xs italic text-slate-500">- Cliquez sur la carte d'une paie pour afficher ses documents de paie et sur le symbole € pour la marquer comme payée ou non payée.</span></>}>
{paiesLoading ? (
<div className="px-3 py-8 text-center text-slate-500">
<Loader2 className="w-4 h-4 inline animate-spin mr-2" /> Chargement des paies…
</div>
) : paiesError ? (
<div className="px-3 py-8 text-center text-rose-600">
Erreur lors du chargement des paies.
{process.env.NODE_ENV !== 'production' && (
<div className="mt-2 text-xs text-slate-400">{(paiesErrorObj as any)?.message || 'Erreur inconnue'}</div>
)}
</div>
) : paies.length === 0 ? (
<div className="px-3 py-8 text-center text-slate-500">
Aucune paie trouvée.
{process.env.NODE_ENV === 'development' && (
<div className="mt-2 text-xs text-slate-400 space-y-1">
<div>Debug: Contract ID = {id}</div>
<div>Debug: Contract Reference = {data?.numero || 'non définie'}</div>
<div className="text-yellow-600">
Vérifiez que le champ "Référence API" dans la table "Paies CDDU Multi-Mois" contient la valeur "{data?.numero || '???'}".
</div>
</div>
)}
</div>
) : (
<div className="space-y-4">
{paies
.slice()
.sort((a,b) => (b.ordre ?? 0) - (a.ordre ?? 0))
.map((p) => {
const label = payLabel(p) || (p.ordre ? `Paie ${p.ordre}` : 'Paie');
// Normalize aem_statut for robust checks (API can return 'OK', 'ok', etc.)
const aemNorm = String((p as any).aem_statut || '').normalize('NFD').replace(/\p{Diacritic}/gu, '').trim().toLowerCase();
const aemOk = aemNorm === 'ok' || aemNorm === 'oui' || aemNorm.includes('ok');
const aemPending = aemNorm.includes('a_traiter') || aemNorm.includes('a traiter') || aemNorm.includes('traiter');
const CardInner = (
<div className="h-full rounded-2xl border bg-white p-4 hover:shadow-md transition-shadow relative">
{/* Bouton de paiement en position absolue */}
{p.traite === 'oui' && (
<Button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleTogglePayment(p.id, p.transfer_done || false);
}}
className="absolute bottom-2 right-2 bg-gradient-to-r from-indigo-200 via-purple-200 to-pink-200 text-black hover:from-indigo-300 hover:via-purple-300 hover:to-pink-300 text-xs px-2 py-1 h-auto z-10"
disabled={markAsPaidMutation.isPending}
title={p.transfer_done ? "Valider" : "Valider"}
>
<Euro className="w-3 h-3" />
</Button>
)}
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:gap-3">
<div className="flex items-center gap-2">
{/* Numéro de paie */}
<span className="text-[11px] px-2 py-0.5 rounded-full bg-indigo-100 text-indigo-800 border border-indigo-200/60">
# {p.ordre ?? ''}
</span>
{/* Période (Paie traitée) */}
{
// Prefer explicit period_start/period_end from the API when present
(p as any).period_start && (p as any).period_end ? (
<span className="text-[11px] px-2 py-1 rounded-lg bg-gradient-to-br from-sky-50 to-blue-100 text-blue-800 border border-blue-200/70 shadow-[0_1px_0_rgba(255,255,255,0.6)]">
{formatDateFR((p as any).period_start)} {formatDateFR((p as any).period_end)}
</span>
) : p.paie_traitee ? (
<span className="text-[11px] px-2 py-1 rounded-lg bg-gradient-to-br from-sky-50 to-blue-100 text-blue-800 border border-blue-200/70 shadow-[0_1px_0_rgba(255,255,255,0.6)]">
{formatPeriodDisplay(p.paie_traitee)}
</span>
) : null
}
</div>
<div className="sm:ml-auto flex items-center gap-2">
{/* 1. Traitée */}
{p.traite === 'non' ? (
<span className="inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded-full bg-amber-100 text-amber-800">
<Clock className="w-3 h-3" /> À traiter
</span>
) : p.traite === 'oui' ? (
<span className="inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-800">
<CheckCircle className="w-3 h-3" /> Traitée
</span>
) : null}
{/* 2. AEM */}
{aemPending ? (
<span className="inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded-full bg-amber-100 text-amber-800">
<Clock className="w-3 h-3" /> AEM
</span>
) : aemOk ? (
<span className="inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-800">
<CheckCircle className="w-3 h-3" /> AEM OK
</span>
) : (
<span className="text-xs text-slate-400">AEM —</span>
)}
{/* 3. Payée avec même style que les autres */}
{p.transfer_done ? (
<span className="inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-800">
<CheckCircle className="w-3 h-3" /> Payée
</span>
) : (
<span className="inline-flex items-center gap-1 text-[11px] px-2 py-0.5 rounded-full bg-slate-100 text-slate-600">
<Clock className="w-3 h-3" /> Non payée
</span>
)}
</div>
</div>
<div className="mt-3 grid grid-cols-4 gap-3 text-sm">
<div>
<div className="text-slate-500">Net avant PAS</div>
<div className="font-medium">{formatEUR((p as any).net_avant_pas ?? p.net_avant_pas)}</div>
</div>
<div>
<div className="text-slate-500">Net à payer</div>
<div className="font-medium">{formatEUR(p.net_a_payer)}</div>
</div>
<div>
<div className="text-slate-500">Brut</div>
<div>{formatEUR(p.brut)}</div>
</div>
<div>
<div className="text-slate-500">Coût total</div>
<div>{formatEUR(p.cout_total)}</div>
</div>
</div>
</div>
);
return p.paie_pdf ? (
<a key={p.id} href={p.paie_pdf} target="_blank" rel="noreferrer" className="group block">
{CardInner}
</a>
) : (
<div key={p.id} className="opacity-70 cursor-not-allowed group block" title="PDF de paie indisponible">
{CardInner}
</div>
);
})}
</div>
)}
</Section>
</div>
</div>
{/* Notes */}
<NotesSection contractId={id} contractRef={data?.numero} />
{/* Modale de confirmation de paiement */}
<ConfirmationModal
isOpen={showPaymentModal}
title={selectedPaieStatus ? "Confirmer le paiement" : "Confirmer l'annulation"}
description={
selectedPaieStatus
? "Êtes-vous sûr de vouloir marquer cette paie comme payée ? Cette action mettra à jour le statut de paiement dans le système."
: "Êtes-vous sûr de vouloir marquer cette paie comme non payée ? Cette action annulera le statut de paiement dans le système."
}
confirmText={selectedPaieStatus ? "Valider" : "Valider"}
cancelText="Annuler"
onConfirm={confirmTogglePayment}
onCancel={() => {
setShowPaymentModal(false);
setSelectedPaieId("");
setSelectedPaieStatus(false);
}}
confirmButtonVariant="gradient"
isLoading={markAsPaidMutation.isPending}
/>
</div>
);
}