434 lines
No EOL
16 KiB
TypeScript
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>
|
|
);
|
|
} |