273 lines
9 KiB
TypeScript
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>
|
|
);
|
|
}
|