espace-paie-odentas/components/contrats/DocumentsCard.tsx

434 lines
No EOL
16 KiB
TypeScript

// components/contrats/DocumentsCard.tsx
"use client";
import { useState, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { FileText, CheckCircle2, RefreshCw, Download, X, ExternalLink, AlertCircle } from "lucide-react";
import { toast } from "sonner";
interface DocumentsCardProps {
contractId: string;
contractNumber?: string;
contractData?: {
pdf_contrat?: { available: boolean; url?: string };
contrat_signe_employeur?: string;
contrat_signe_salarie?: string;
};
showPayslips?: boolean; // Par défaut true, si false ne charge pas les fiches de paie
}
interface SignedPdfData {
hasSignedPdf: boolean;
signedUrl?: string;
s3Key?: string;
}
interface PayslipData {
id: string;
pay_number: number;
period_start: string;
period_end: string;
pay_date: string;
gross_amount: string;
net_amount: string;
net_after_withholding: string;
processed: boolean;
aem_status: string;
signedUrl: string | null;
hasDocument: boolean;
}
export default function DocumentsCard({ contractId, contractNumber, contractData, showPayslips = true }: DocumentsCardProps) {
const [signedPdfData, setSignedPdfData] = useState<SignedPdfData | null>(null);
const [loadingSignedPdf, setLoadingSignedPdf] = useState(true);
const [payslips, setPayslips] = useState<PayslipData[]>([]);
const [loadingPayslips, setLoadingPayslips] = useState(true);
// États pour le modal
const [isModalOpen, setIsModalOpen] = useState(false);
const [currentDocumentUrl, setCurrentDocumentUrl] = useState<string>("");
const [currentDocumentTitle, setCurrentDocumentTitle] = useState<string>("");
const [pdfError, setPdfError] = useState(false);
// Fonction pour ouvrir un document dans le modal
const openDocumentInModal = (url: string, title: string) => {
setCurrentDocumentUrl(url);
setCurrentDocumentTitle(title);
setPdfError(false);
setIsModalOpen(true);
};
// Fonction pour fermer le modal
const closeModal = () => {
setIsModalOpen(false);
setCurrentDocumentUrl("");
setCurrentDocumentTitle("");
setPdfError(false);
};
// Fonction pour ouvrir le contrat PDF original
const openOriginalPdf = () => {
if (contractData?.pdf_contrat?.url) {
openDocumentInModal(contractData.pdf_contrat.url, "Contrat CDDU");
} else {
toast.error("URL du contrat PDF non disponible");
}
};
// Fonction pour ouvrir le contrat signé
const openSignedPdf = async () => {
if (signedPdfData?.signedUrl) {
openDocumentInModal(signedPdfData.signedUrl, "Contrat CDDU signé");
} else {
toast.error("URL du contrat signé non disponible");
}
};
// Fonction pour ouvrir une fiche de paie
const openPayslip = (payslip: PayslipData) => {
if (payslip.signedUrl) {
openDocumentInModal(
payslip.signedUrl,
`Fiche de paie #${payslip.pay_number}`
);
} else {
toast.error("Document de paie non disponible");
}
};
// Fonction pour ouvrir dans un nouvel onglet
const openInNewTab = () => {
if (currentDocumentUrl) {
window.open(currentDocumentUrl, '_blank');
}
};
// Fonction pour télécharger
const downloadDocument = () => {
if (currentDocumentUrl) {
const link = document.createElement('a');
link.href = currentDocumentUrl;
link.download = `${currentDocumentTitle.replace(/\s+/g, '_')}.pdf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
};
// Récupération du contrat signé
useEffect(() => {
const fetchSignedPdf = async () => {
try {
const response = await fetch(`/api/contrats/${contractId}/signed-pdf`, {
credentials: 'include',
headers: { Accept: 'application/json' }
});
if (response.ok) {
const data = await response.json();
setSignedPdfData(data);
} else if (response.status === 404) {
setSignedPdfData({ hasSignedPdf: false });
} else {
console.error("Erreur lors de la récupération du contrat signé");
setSignedPdfData({ hasSignedPdf: false });
}
} catch (error) {
console.error("Erreur:", error);
setSignedPdfData({ hasSignedPdf: false });
} finally {
setLoadingSignedPdf(false);
}
};
fetchSignedPdf();
}, [contractId]);
// Effet pour bloquer le défilement et gérer la touche Échap quand le modal est ouvert
useEffect(() => {
if (isModalOpen) {
// Bloquer le défilement
document.body.style.overflow = 'hidden';
// Gérer la touche Échap
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closeModal();
}
};
document.addEventListener('keydown', handleEscape);
return () => {
document.body.style.overflow = '';
document.removeEventListener('keydown', handleEscape);
};
}
}, [isModalOpen]);
// Récupération des fiches de paie
useEffect(() => {
// Ne charger les fiches de paie que si showPayslips est true
if (!showPayslips) {
setLoadingPayslips(false);
return;
}
const fetchPayslips = async () => {
try {
const response = await fetch(`/api/contrats/${contractId}/payslip-urls`, {
credentials: 'include',
headers: { Accept: 'application/json' }
});
if (response.ok) {
const data = await response.json();
setPayslips(data.payslips || []);
} else {
console.error("Erreur lors de la récupération des fiches de paie");
setPayslips([]);
}
} catch (error) {
console.error("Erreur:", error);
setPayslips([]);
} finally {
setLoadingPayslips(false);
}
};
fetchPayslips();
}, [contractId, showPayslips]);
// Formatage des dates
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('fr-FR');
};
// Formatage des montants
const formatEUR = (value: string) => {
const num = parseFloat(value);
if (isNaN(num)) return value;
return new Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(num);
};
// Déterminons s'il y a des documents à afficher
const hasSignedContract = signedPdfData?.hasSignedPdf && signedPdfData?.signedUrl;
const hasOriginalContract = contractData?.pdf_contrat?.available && contractData?.pdf_contrat?.url;
const bothPartiesSigned = contractData?.contrat_signe_employeur === 'oui' && contractData?.contrat_signe_salarie === 'oui';
const hasAnyDocument = hasSignedContract || hasOriginalContract || (showPayslips && payslips.some(p => p.hasDocument));
return (
<Card className="rounded-3xl">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="size-5" /> Documents
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{/* Contrat CDDU signé */}
{loadingSignedPdf ? (
<div className="flex items-center gap-3 p-3 border rounded-lg bg-gray-50">
<RefreshCw className="size-5 text-gray-400 animate-spin" />
<div className="flex-1">
<p className="font-medium text-gray-600">Vérification du contrat signé...</p>
</div>
</div>
) : hasSignedContract ? (
<div
onClick={openSignedPdf}
className="flex items-center gap-3 p-3 border-2 border-green-200 bg-green-50 rounded-lg cursor-pointer hover:bg-green-100 transition-colors"
>
<CheckCircle2 className="size-5 text-green-600" />
<div className="flex-1">
<p className="font-medium text-green-800">Contrat CDDU signé</p>
<p className="text-sm text-green-600">
Document électroniquement signé
</p>
</div>
<div className="text-xs text-green-600">
Cliquez pour ouvrir
</div>
</div>
) : hasOriginalContract ? (
bothPartiesSigned ? (
// Les deux parties ont signé - contrat cliquable
<div
onClick={openOriginalPdf}
className="flex items-center gap-3 p-3 border-2 border-blue-200 bg-blue-50 rounded-lg cursor-pointer hover:bg-blue-100 transition-colors"
>
<FileText className="size-5 text-blue-600" />
<div className="flex-1">
<p className="font-medium text-blue-800">Contrat CDDU</p>
<p className="text-sm text-blue-600">
Document signé par toutes les parties
</p>
</div>
<div className="text-xs text-blue-600">
Cliquez pour ouvrir
</div>
</div>
) : (
// Pas toutes les signatures - contrat grisé, non cliquable
<div className="flex items-center gap-3 p-3 border-2 border-gray-200 bg-gray-50 rounded-lg opacity-60 cursor-not-allowed">
<FileText className="size-5 text-gray-400" />
<div className="flex-1">
<p className="font-medium text-gray-600">Contrat CDDU</p>
<p className="text-sm text-gray-500">
En attente de réception de toutes les signatures
</p>
</div>
<div className="text-xs text-gray-500">
Non disponible
</div>
</div>
)
) : null}
{/* Fiches de paie - afficher uniquement si showPayslips est true */}
{showPayslips && (
<>
{loadingPayslips ? (
<div className="flex items-center gap-3 p-3 border rounded-lg bg-gray-50">
<RefreshCw className="size-5 text-gray-400 animate-spin" />
<div className="flex-1">
<p className="font-medium text-gray-600">Chargement des fiches de paie...</p>
</div>
</div>
) : payslips.filter(p => p.hasDocument).map((payslip) => (
<div
key={payslip.id}
onClick={() => openPayslip(payslip)}
className="flex items-center gap-3 p-3 border-2 border-blue-200 bg-blue-50 rounded-lg cursor-pointer hover:bg-blue-100 transition-colors"
>
<Download className="size-5 text-blue-600" />
<div className="flex-1">
<p className="font-medium text-blue-800">
Fiche de paie #{payslip.pay_number}
</p>
<p className="text-sm text-blue-600">
Période: {formatDate(payslip.period_start)} - {formatDate(payslip.period_end)}
</p>
<p className="text-xs text-blue-500 mt-1">
Net à payer: {formatEUR(payslip.net_after_withholding)}
</p>
</div>
<div className="text-xs text-blue-600">
Cliquez pour ouvrir
</div>
</div>
))}
</>
)}
{/* Message quand aucun document n'est disponible */}
{!loadingSignedPdf && !loadingPayslips && !hasAnyDocument && (
<div className="text-center text-muted-foreground py-6">
<FileText className="size-8 mx-auto mb-2 opacity-50" />
<p className="font-medium">Aucun document disponible</p>
<p className="text-sm">Les documents apparaîtront ici une fois le contrat traité et signé</p>
</div>
)}
</CardContent>
{/* Modal de visualisation de document */}
{isModalOpen && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
onClick={(e) => {
// Fermer si on clique sur le fond (pas sur le contenu)
if (e.target === e.currentTarget) {
closeModal();
}
}}
>
<div className="relative w-[95vw] h-[95vh] max-w-7xl bg-white rounded-2xl shadow-2xl flex flex-col overflow-hidden">
{/* Header du modal */}
<div className="flex items-center justify-between px-6 py-4 border-b bg-gradient-to-r from-slate-50 to-white">
<div className="flex items-center gap-3">
<FileText className="size-5 text-slate-600" />
<div className="flex items-center gap-2">
<h2 className="text-lg font-semibold text-slate-900">{currentDocumentTitle}</h2>
{contractNumber && (
<>
<span className="text-slate-400"></span>
<span className="text-sm text-slate-600 font-medium">Contrat n° {contractNumber}</span>
</>
)}
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={downloadDocument}
className="px-3 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors flex items-center gap-2"
title="Télécharger"
>
<Download className="size-4" />
Télécharger
</button>
<button
onClick={openInNewTab}
className="px-3 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors flex items-center gap-2"
title="Ouvrir dans un nouvel onglet"
>
<ExternalLink className="size-4" />
Nouvel onglet
</button>
<button
onClick={closeModal}
className="p-2 text-slate-500 hover:text-slate-700 hover:bg-slate-100 rounded-lg transition-colors"
title="Fermer"
>
<X className="size-5" />
</button>
</div>
</div>
{/* Corps du modal avec iframe */}
<div className="flex-1 relative bg-slate-100">
{pdfError ? (
<div className="absolute inset-0 flex flex-col items-center justify-center p-8 text-center">
<AlertCircle className="size-16 text-amber-500 mb-4" />
<h3 className="text-xl font-semibold text-slate-900 mb-2">
Impossible d'afficher le PDF
</h3>
<p className="text-slate-600 mb-6 max-w-md">
Le document ne peut pas être affiché dans cette fenêtre.
Utilisez les boutons ci-dessus pour le télécharger ou l'ouvrir dans un nouvel onglet.
</p>
<div className="flex gap-3">
<button
onClick={downloadDocument}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors flex items-center gap-2"
>
<Download className="size-4" />
Télécharger le PDF
</button>
<button
onClick={openInNewTab}
className="px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors flex items-center gap-2"
>
<ExternalLink className="size-4" />
Ouvrir dans un nouvel onglet
</button>
</div>
</div>
) : (
<iframe
src={currentDocumentUrl}
className="w-full h-full border-0"
title={currentDocumentTitle}
onError={() => setPdfError(true)}
/>
)}
</div>
</div>
</div>
)}
</Card>
);
}