espace-paie-odentas/components/staff/BulkPdfProgressModal.tsx
2025-10-12 17:05:46 +02:00

187 lines
No EOL
6.2 KiB
TypeScript

// components/staff/BulkPdfProgressModal.tsx
"use client";
import React from "react";
import { X, FileDown, CheckCircle, XCircle, Clock, Loader2 } from "lucide-react";
type ContractProgress = {
id: string;
contractNumber?: string;
employeeName?: string;
status: 'pending' | 'processing' | 'success' | 'error';
error?: string;
filename?: string;
};
type BulkPdfProgressModalProps = {
isOpen: boolean;
onClose: () => void;
contracts: ContractProgress[];
onCancel?: () => void;
isProcessing: boolean;
totalCount: number;
completedCount: number;
successCount: number;
errorCount: number;
};
export default function BulkPdfProgressModal({
isOpen,
onClose,
contracts,
onCancel,
isProcessing,
totalCount,
completedCount,
successCount,
errorCount
}: BulkPdfProgressModalProps) {
if (!isOpen) return null;
const progressPercentage = totalCount > 0 ? (completedCount / totalCount) * 100 : 0;
const getStatusIcon = (status: ContractProgress['status']) => {
switch (status) {
case 'pending':
return <Clock className="size-4 text-gray-400" />;
case 'processing':
return <Loader2 className="size-4 text-blue-500 animate-spin" />;
case 'success':
return <CheckCircle className="size-4 text-green-500" />;
case 'error':
return <XCircle className="size-4 text-red-500" />;
}
};
const getStatusText = (contract: ContractProgress) => {
switch (contract.status) {
case 'pending':
return 'En attente';
case 'processing':
return 'Génération en cours...';
case 'success':
return `PDF créé: ${contract.filename || 'Fichier généré'}`;
case 'error':
return `Erreur: ${contract.error || 'Erreur inconnue'}`;
}
};
const getStatusColor = (status: ContractProgress['status']) => {
switch (status) {
case 'pending':
return 'text-gray-600';
case 'processing':
return 'text-blue-600';
case 'success':
return 'text-green-600';
case 'error':
return 'text-red-600';
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-2xl max-h-[80vh] flex flex-col">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<FileDown className="size-5 text-orange-600" />
<h2 className="text-lg font-semibold">Génération des PDFs</h2>
</div>
{!isProcessing && (
<button
onClick={onClose}
className="p-1 hover:bg-gray-100 rounded"
>
<X className="size-5" />
</button>
)}
</div>
{/* Progress Bar */}
<div className="mb-4">
<div className="flex justify-between text-sm text-gray-600 mb-2">
<span>Progression: {completedCount}/{totalCount}</span>
<span>{Math.round(progressPercentage)}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-500 h-2 rounded-full transition-all duration-300"
style={{ width: `${progressPercentage}%` }}
/>
</div>
</div>
{/* Summary Stats */}
<div className="grid grid-cols-3 gap-4 mb-4">
<div className="text-center p-2 bg-blue-50 rounded">
<div className="text-lg font-semibold text-blue-600">{successCount}</div>
<div className="text-xs text-blue-600">Succès</div>
</div>
<div className="text-center p-2 bg-red-50 rounded">
<div className="text-lg font-semibold text-red-600">{errorCount}</div>
<div className="text-xs text-red-600">Erreurs</div>
</div>
<div className="text-center p-2 bg-gray-50 rounded">
<div className="text-lg font-semibold text-gray-600">{totalCount - completedCount}</div>
<div className="text-xs text-gray-600">Restants</div>
</div>
</div>
{/* Contracts List */}
<div className="flex-1 overflow-y-auto border rounded max-h-64">
<div className="space-y-1 p-2">
{contracts.map((contract) => (
<div
key={contract.id}
className={`flex items-center gap-3 p-2 rounded text-sm ${
contract.status === 'processing' ? 'bg-blue-50' :
contract.status === 'success' ? 'bg-green-50' :
contract.status === 'error' ? 'bg-red-50' : 'bg-gray-50'
}`}
>
{getStatusIcon(contract.status)}
<div className="flex-1 min-w-0">
<div className="font-medium truncate">
{contract.contractNumber || contract.id}
</div>
<div className="text-xs text-gray-500 truncate">
{contract.employeeName || 'Nom non disponible'}
</div>
</div>
<div className={`text-xs ${getStatusColor(contract.status)} max-w-xs truncate`}>
{getStatusText(contract)}
</div>
</div>
))}
</div>
</div>
{/* Footer */}
<div className="flex justify-between items-center mt-4 pt-4 border-t">
<div className="text-sm text-gray-500">
{isProcessing ? 'Génération en cours...' : 'Génération terminée'}
</div>
<div className="flex gap-2">
{isProcessing && onCancel && (
<button
onClick={onCancel}
className="px-4 py-2 text-sm bg-gray-500 text-white rounded hover:bg-gray-600 transition-colors"
>
Annuler
</button>
)}
{!isProcessing && (
<button
onClick={onClose}
className="px-4 py-2 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
>
Fermer
</button>
)}
</div>
</div>
</div>
</div>
);
}