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

265 lines
No EOL
9.4 KiB
TypeScript

"use client";
// components/staff/BulkEmailProgressModal.tsx
import React from "react";
import { X, Mail, CheckCircle, XCircle, Clock, Loader2, AlertTriangle } from "lucide-react";
type EmailProgress = {
id: string;
email: string;
organizationName?: string;
status: 'pending' | 'sending' | 'success' | 'error';
error?: string;
sentAt?: string;
};
type BulkEmailProgressModalProps = {
isOpen: boolean;
onClose: () => void;
emails: EmailProgress[];
onCancel?: () => void;
isProcessing: boolean;
totalCount: number;
completedCount: number;
successCount: number;
errorCount: number;
subject: string;
};
export default function BulkEmailProgressModal({
isOpen,
onClose,
emails,
onCancel,
isProcessing,
totalCount,
completedCount,
successCount,
errorCount,
subject
}: BulkEmailProgressModalProps) {
if (!isOpen) return null;
const progressPercentage = totalCount > 0 ? (completedCount / totalCount) * 100 : 0;
const getStatusIcon = (status: EmailProgress['status']) => {
switch (status) {
case 'pending':
return <Clock className="size-4 text-gray-400" />;
case 'sending':
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 = (email: EmailProgress) => {
switch (email.status) {
case 'pending':
return 'En attente';
case 'sending':
return 'Envoi en cours...';
case 'success':
return `Envoyé ${email.sentAt ? `à ${formatTime(email.sentAt)}` : 'avec succès'}`;
case 'error':
// Améliorer l'affichage des erreurs
const errorMsg = email.error || 'Erreur SES inconnue';
if (errorMsg.includes('Format email source invalide')) {
return 'Erreur: Configuration SES';
} else if (errorMsg.includes('Adresse email invalide')) {
return 'Erreur: Email invalide';
} else if (errorMsg.includes('Quota journalier dépassé')) {
return 'Erreur: Quota SES atteint';
} else if (errorMsg.includes('Limite de débit dépassée')) {
return 'Erreur: Trop rapide, réessayez';
}
return `Erreur: ${errorMsg}`;
}
};
const getStatusColor = (status: EmailProgress['status']) => {
switch (status) {
case 'pending':
return 'text-gray-600';
case 'sending':
return 'text-blue-600';
case 'success':
return 'text-green-600';
case 'error':
return 'text-red-600';
}
};
const formatTime = (dateString?: string) => {
if (!dateString) return '';
try {
return new Intl.DateTimeFormat('fr-FR', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).format(new Date(dateString));
} catch {
return '';
}
};
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-3xl max-h-[80vh] flex flex-col">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Mail className="size-5 text-green-600" />
<div>
<h2 className="text-lg font-semibold">Envoi d'emails groupés</h2>
<p className="text-sm text-gray-600 truncate max-w-md">{subject}</p>
</div>
</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-3 overflow-hidden">
<div
className={`h-3 rounded-full transition-all duration-500 ${
isProcessing
? 'bg-gradient-to-r from-green-500 to-green-600 animate-pulse'
: 'bg-gradient-to-r from-green-500 to-green-600'
}`}
style={{ width: `${progressPercentage}%` }}
/>
</div>
</div>
{/* Summary Stats */}
<div className="grid grid-cols-3 gap-4 mb-4">
<div className="text-center p-3 bg-green-50 rounded-lg">
<div className="text-xl font-bold text-green-600">{successCount}</div>
<div className="text-xs text-green-600 font-medium">✓ Envoyés</div>
</div>
<div className="text-center p-3 bg-red-50 rounded-lg">
<div className="text-xl font-bold text-red-600">{errorCount}</div>
<div className="text-xs text-red-600 font-medium">✗ Échecs</div>
</div>
<div className="text-center p-3 bg-gray-50 rounded-lg">
<div className="text-xl font-bold text-gray-600">{totalCount - completedCount}</div>
<div className="text-xs text-gray-600 font-medium">⏳ Restants</div>
</div>
</div>
{/* Warning for errors */}
{errorCount > 0 && !isProcessing && (
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<div className="flex items-start gap-2 text-yellow-800">
<AlertTriangle className="size-4 mt-0.5 flex-shrink-0" />
<div className="text-sm">
<div className="font-medium mb-1">
{errorCount} email(s) n'ont pas pu être envoyés
</div>
<div className="text-xs text-yellow-700">
Causes possibles : adresses email invalides, configuration SES, quota dépassé.
<br />
Vérifiez les détails ci-dessous et contactez l'administrateur si nécessaire.
</div>
</div>
</div>
</div>
)}
{/* Emails List */}
<div className="flex-1 overflow-y-auto border rounded-lg max-h-80 bg-gray-50">
<div className="space-y-2 p-3">
{emails.map((email, index) => (
<div
key={email.id}
className={`flex items-center gap-3 p-3 rounded-lg text-sm transition-all duration-300 ${
email.status === 'sending' ? 'bg-blue-50 border-l-4 border-blue-400 shadow-sm' :
email.status === 'success' ? 'bg-green-50 border-l-4 border-green-400 shadow-sm' :
email.status === 'error' ? 'bg-red-50 border-l-4 border-red-400 shadow-sm' : 'bg-white border border-gray-200'
}`}
style={{
animationDelay: `${index * 0.05}s`
}}
>
{getStatusIcon(email.status)}
<div className="flex-1 min-w-0">
<div className="font-medium truncate text-gray-900">
{email.email}
</div>
<div className="text-xs text-gray-500 truncate">
{email.organizationName || 'Organisation non spécifiée'}
</div>
</div>
<div className="text-right min-w-0 flex-shrink-0">
<div className={`text-xs font-medium ${getStatusColor(email.status)} truncate`}>
{getStatusText(email)}
</div>
{email.status === 'success' && email.sentAt && (
<div className="text-xs text-gray-400 mt-1">
{formatTime(email.sentAt)}
</div>
)}
</div>
</div>
))}
{emails.length === 0 && (
<div className="text-center py-8 text-gray-500">
<Mail className="size-8 mx-auto mb-2 opacity-50" />
<p>Aucun email en cours de traitement</p>
</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 ? (
<div className="flex items-center gap-2">
<Loader2 className="size-4 animate-spin" />
<span>Envoi en cours... (12 emails/lot, respect limite 14/sec)</span>
</div>
) : (
<span>
Envoi terminé - {successCount} succès, {errorCount} échecs
</span>
)}
</div>
<div className="flex gap-2">
{isProcessing && onCancel && (
<button
onClick={onCancel}
className="px-4 py-2 text-sm bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors"
>
Annuler
</button>
)}
{!isProcessing && (
<button
onClick={onClose}
className="px-4 py-2 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
Fermer
</button>
)}
</div>
</div>
</div>
</div>
);
}