espace-paie-odentas/components/staff/contracts/PayslipCard.tsx

273 lines
9 KiB
TypeScript

"use client";
import { useState, useRef, DragEvent } from "react";
import { CheckCircle2, Upload, FileText, X, Loader2 } from "lucide-react";
import { toast } from "sonner";
type PayslipCardProps = {
payslip: any;
index: number;
contractId: string;
onClick?: () => void;
onUploadComplete?: () => void;
};
export function PayslipCard({ payslip, index, contractId, onClick, onUploadComplete }: PayslipCardProps) {
const [isDragging, setIsDragging] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleDragEnter = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
};
const handleDragLeave = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
};
const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
};
const handleDrop = async (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
const files = Array.from(e.dataTransfer.files);
if (files.length === 0) return;
const file = files[0];
// Vérifier que c'est un PDF
if (file.type !== 'application/pdf') {
toast.error("Seuls les fichiers PDF sont acceptés");
return;
}
await uploadFile(file);
};
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || files.length === 0) return;
const file = files[0];
// Vérifier que c'est un PDF
if (file.type !== 'application/pdf') {
toast.error("Seuls les fichiers PDF sont acceptés");
return;
}
uploadFile(file);
};
const uploadFile = async (file: File) => {
setIsUploading(true);
try {
const formData = new FormData();
formData.append('file', file);
formData.append('contract_id', contractId);
formData.append('payslip_id', payslip.id);
const response = await fetch('/api/staff/payslip-upload', {
method: 'POST',
body: formData,
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Erreur lors de l\'upload');
}
const result = await response.json();
toast.success("Bulletin de paie uploadé avec succès !");
// Notifier le parent pour rafraîchir les données
if (onUploadComplete) {
onUploadComplete();
}
} catch (error) {
console.error('Erreur upload:', error);
toast.error(error instanceof Error ? error.message : "Erreur lors de l'upload");
} finally {
setIsUploading(false);
// Reset le file input
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
};
const handleCardClick = (e: React.MouseEvent) => {
// Ne pas déclencher onClick si on clique sur la zone d'upload
if (isUploading || isDragging) {
return;
}
if (onClick) {
onClick();
}
};
const handleUploadAreaClick = (e: React.MouseEvent) => {
e.stopPropagation();
if (fileInputRef.current) {
fileInputRef.current.click();
}
};
const hasPdf = !!payslip.storage_path;
return (
<div
className={`border rounded-2xl p-4 transition-all ${
isDragging
? 'border-blue-500 bg-blue-50 border-2'
: hasPdf
? 'border-green-200 bg-green-50 hover:bg-green-100'
: 'hover:bg-gray-50'
} ${!isUploading ? 'cursor-pointer' : ''}`}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
onClick={handleCardClick}
>
<div className="space-y-3">
{/* En-tête: numéro, période et date de paie */}
<div className="flex items-center gap-4">
<div className="bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm font-medium">
#{payslip.pay_number || index + 1}
</div>
<div className="flex-1">
<div className="font-medium text-gray-900">
{payslip.period_start && payslip.period_end ? (
`${new Date(payslip.period_start).toLocaleDateString('fr-FR')} - ${new Date(payslip.period_end).toLocaleDateString('fr-FR')}`
) : (
"Période non définie"
)}
</div>
<div className="text-sm text-gray-500">
{payslip.pay_date ? `Paie du ${new Date(payslip.pay_date).toLocaleDateString('fr-FR')}` : "Date non définie"}
</div>
</div>
{/* Indicateur de bulletin PDF */}
{hasPdf && (
<div className="flex items-center gap-2 bg-green-100 text-green-700 px-3 py-1 rounded-full text-xs font-medium">
<CheckCircle2 className="size-4" />
Bulletin uploadé
</div>
)}
</div>
{/* Montants organisés en 2x2 */}
<div className="grid grid-cols-2 gap-4">
<div>
<div className="text-sm text-gray-500">Brut</div>
<div className="font-semibold text-gray-900">
{payslip.gross_amount ? `${parseFloat(payslip.gross_amount).toFixed(2)}` : "Non défini"}
</div>
</div>
<div>
<div className="text-sm text-gray-500">Net avant PAS</div>
<div className="font-semibold text-gray-900">
{payslip.net_amount ? `${parseFloat(payslip.net_amount).toFixed(2)}` : "Non défini"}
</div>
</div>
<div>
<div className="text-sm text-gray-500">Net à payer</div>
<div className="font-semibold text-gray-900">
{payslip.net_after_withholding ? `${parseFloat(payslip.net_after_withholding).toFixed(2)}` : "Non défini"}
</div>
</div>
<div>
<div className="text-sm text-gray-500">Coût employeur</div>
<div className="font-semibold text-gray-900">
{payslip.employer_cost ? `${parseFloat(payslip.employer_cost).toFixed(2)}` : "Non défini"}
</div>
</div>
</div>
{/* Zone de drag & drop ou bouton d'upload */}
{!hasPdf && (
<div
onClick={handleUploadAreaClick}
className={`border-2 border-dashed rounded-xl p-4 text-center transition-all ${
isDragging
? 'border-blue-500 bg-blue-100'
: 'border-gray-300 bg-gray-50 hover:border-blue-400 hover:bg-blue-50'
}`}
>
{isUploading ? (
<div className="flex flex-col items-center gap-2">
<Loader2 className="size-6 text-blue-500 animate-spin" />
<p className="text-sm text-blue-600 font-medium">Upload en cours...</p>
</div>
) : (
<div className="flex flex-col items-center gap-2">
<Upload className="size-6 text-gray-400" />
<p className="text-sm text-gray-600">
<span className="font-medium text-blue-600">Cliquez</span> ou glissez un PDF ici
</p>
<p className="text-xs text-gray-500">Bulletin de paie au format PDF</p>
</div>
)}
<input
ref={fileInputRef}
type="file"
accept="application/pdf"
onChange={handleFileSelect}
className="hidden"
/>
</div>
)}
{/* Indicateurs en bas */}
<div className="flex gap-2 flex-wrap">
{/* Indicateur traitement */}
{!payslip.processed ? (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-50 text-red-600">
À traiter
</span>
) : (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-50 text-green-600">
Traitée
</span>
)}
{/* Indicateur virement */}
{payslip.processed && (
payslip.transfer_done ? (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-emerald-50 text-emerald-600">
Virement OK
</span>
) : (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-orange-50 text-orange-600">
Virement en attente
</span>
)
)}
{/* Indicateur AEM */}
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
payslip.aem_status === 'OK'
? 'bg-green-50 text-green-600'
: payslip.aem_status === 'KO'
? 'bg-red-50 text-red-600'
: 'bg-gray-50 text-gray-600'
}`}>
AEM: {payslip.aem_status || 'N/A'}
</span>
</div>
</div>
</div>
);
}