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