Files
MAD-Platform/components/ParticipationFinanciereList.tsx
2026-02-16 14:43:02 +01:00

795 lines
37 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useNotification } from './NotificationProvider';
import ConfirmModal from './ConfirmModal';
import ParticipationEditModal from './ParticipationEditModal';
import { getParticipationRef } from '@/lib/participation-ref';
interface Participation {
id: string;
destinataireEmail: string;
destinataireNom: string;
destinataireType: string;
montant: number | null;
complement: string | null;
filePath: string | null;
statut: string;
createdAt: string;
adherent: {
id: string;
nom: string;
prenom: string;
email: string;
};
trajet: {
id: string;
date: string;
adresseDepart: string;
adresseArrivee: string;
statut: string;
chauffeur: {
id: string;
nom: string;
prenom: string;
} | null;
};
}
const STATUT_CONFIG: Record<string, { label: string; className: string; dot: string }> = {
en_attente: {
label: "En attente d'envoi",
className: 'bg-amber-100 text-amber-800',
dot: 'bg-amber-500',
},
envoye: {
label: 'Envoyé',
className: 'bg-blue-100 text-blue-800',
dot: 'bg-blue-500',
},
paye: {
label: 'Payé',
className: 'bg-emerald-100 text-emerald-800',
dot: 'bg-emerald-500',
},
archive: {
label: 'Archivé',
className: 'bg-gray-200 text-gray-700',
dot: 'bg-gray-500',
},
};
export default function ParticipationFinanciereList() {
const { showNotification } = useNotification();
const [participations, setParticipations] = useState<Participation[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [participationToDelete, setParticipationToDelete] = useState<string | null>(null);
const [sendingId, setSendingId] = useState<string | null>(null);
const [editingParticipation, setEditingParticipation] = useState<Participation | null>(null);
const [openMenuId, setOpenMenuId] = useState<string | null>(null);
const [menuAnchor, setMenuAnchor] = useState<{ top: number; left: number } | null>(null);
const [statusFilter, setStatusFilter] = useState<string>('');
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [bulkUpdating, setBulkUpdating] = useState(false);
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false);
const [openBulkDropdown, setOpenBulkDropdown] = useState<'statut' | 'actions' | null>(null);
useEffect(() => {
fetchParticipations();
}, []);
useEffect(() => {
const handleClickOutside = () => {
setOpenMenuId(null);
setMenuAnchor(null);
};
if (openMenuId) {
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}
}, [openMenuId]);
useEffect(() => {
const handleClickOutside = () => setOpenBulkDropdown(null);
if (openBulkDropdown) {
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}
}, [openBulkDropdown]);
const fetchParticipations = async () => {
setLoading(true);
try {
const response = await fetch('/api/participations');
if (response.ok) {
const data = await response.json();
setParticipations(data);
} else {
showNotification('error', 'Erreur lors du chargement des participations');
}
} catch {
showNotification('error', 'Erreur lors du chargement des participations');
} finally {
setLoading(false);
}
};
const handleViewPdf = (id: string) => {
window.open(`/api/participations/${id}/pdf`, '_blank', 'noopener,noreferrer');
};
const handleSendEmail = async (id: string) => {
setSendingId(id);
setOpenMenuId(null);
try {
const response = await fetch(`/api/participations/${id}/send`, {
method: 'POST',
});
const data = await response.json();
if (response.ok) {
showNotification('success', data.message);
fetchParticipations();
} else {
showNotification('error', data.error || "Erreur lors de l'envoi");
}
} catch {
showNotification('error', "Erreur lors de l'envoi de l'email");
} finally {
setSendingId(null);
}
};
const handleEditClick = (p: Participation) => {
setEditingParticipation(p);
setOpenMenuId(null);
};
const handleEditSuccess = () => {
setEditingParticipation(null);
fetchParticipations();
showNotification('success', 'Participation mise à jour');
};
const handleDeleteClick = (id: string) => {
setParticipationToDelete(id);
setShowDeleteConfirm(true);
setOpenMenuId(null);
};
const handleDelete = async () => {
if (!participationToDelete) return;
setShowDeleteConfirm(false);
try {
const response = await fetch(`/api/participations/${participationToDelete}`, {
method: 'DELETE',
});
if (response.ok) {
showNotification('success', 'Participation supprimée');
fetchParticipations();
} else {
const data = await response.json();
showNotification('error', data.error || 'Erreur lors de la suppression');
}
} catch {
showNotification('error', 'Erreur lors de la suppression');
} finally {
setParticipationToDelete(null);
}
};
const handleStatutChange = async (id: string, newStatut: string) => {
setOpenMenuId(null);
setMenuAnchor(null);
try {
const response = await fetch(`/api/participations/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ statut: newStatut }),
});
if (response.ok) {
fetchParticipations();
} else {
showNotification('error', 'Erreur lors du changement de statut');
}
} catch {
showNotification('error', 'Erreur lors du changement de statut');
}
};
const filteredParticipations = participations.filter((p) => {
const search = searchTerm.toLowerCase();
const ref = getParticipationRef(p.id).toLowerCase();
const chauffeurName = p.trajet.chauffeur
? `${p.trajet.chauffeur.prenom} ${p.trajet.chauffeur.nom}`.toLowerCase()
: '';
const matchesSearch =
p.adherent.nom.toLowerCase().includes(search) ||
p.adherent.prenom.toLowerCase().includes(search) ||
ref.includes(search) ||
chauffeurName.includes(search) ||
p.destinataireNom.toLowerCase().includes(search);
const matchesStatus = !statusFilter || p.statut === statusFilter;
return matchesSearch && matchesStatus;
});
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedIds(new Set(filteredParticipations.map((p) => p.id)));
} else {
setSelectedIds(new Set());
}
};
const handleSelectOne = (id: string, checked: boolean) => {
const next = new Set(selectedIds);
if (checked) next.add(id);
else next.delete(id);
setSelectedIds(next);
};
const handleBulkStatusChange = async (newStatut: string) => {
if (selectedIds.size === 0) return;
setBulkUpdating(true);
try {
const results = await Promise.allSettled(
Array.from(selectedIds).map((id) =>
fetch(`/api/participations/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ statut: newStatut }),
})
)
);
const failed = results.filter((r) => r.status === 'rejected' || (r.status === 'fulfilled' && !r.value.ok));
if (failed.length === 0) {
showNotification('success', `${selectedIds.size} participation(s) mise(s) à jour`);
setSelectedIds(new Set());
fetchParticipations();
} else {
showNotification('error', `${failed.length} mise(s) à jour ont échoué`);
}
} catch {
showNotification('error', 'Erreur lors de la mise à jour en masse');
} finally {
setBulkUpdating(false);
}
};
const handleBulkSendEmail = async () => {
if (selectedIds.size === 0) return;
setBulkUpdating(true);
try {
const results = await Promise.allSettled(
Array.from(selectedIds).map((id) =>
fetch(`/api/participations/${id}/send`, { method: 'POST' })
)
);
const success = results.filter((r) => r.status === 'fulfilled' && r.value.ok).length;
const failed = results.filter((r) => r.status === 'rejected' || (r.status === 'fulfilled' && !r.value.ok));
if (failed.length === 0) {
showNotification('success', `${success} participation(s) envoyée(s) par email`);
setSelectedIds(new Set());
fetchParticipations();
} else {
showNotification(
failed.length === results.length ? 'error' : 'success',
success > 0 ? `${success} envoyée(s), ${failed.length} échec(s)` : `${failed.length} envoi(s) ont échoué`
);
if (success > 0) fetchParticipations();
}
} catch {
showNotification('error', 'Erreur lors de l\'envoi en masse');
} finally {
setBulkUpdating(false);
}
};
const handleBulkDeleteConfirm = async () => {
if (selectedIds.size === 0) return;
setShowBulkDeleteConfirm(false);
const idsToDelete = Array.from(selectedIds);
setBulkUpdating(true);
try {
const results = await Promise.allSettled(
idsToDelete.map((id) =>
fetch(`/api/participations/${id}`, { method: 'DELETE' })
)
);
const failed = results.filter((r) => r.status === 'rejected' || (r.status === 'fulfilled' && !r.value.ok));
if (failed.length === 0) {
showNotification('success', `${idsToDelete.length} participation(s) supprimée(s)`);
} else {
showNotification('error', `${failed.length} suppression(s) ont échoué`);
}
setSelectedIds(new Set());
fetchParticipations();
} catch {
showNotification('error', 'Erreur lors de la suppression en masse');
} finally {
setBulkUpdating(false);
}
};
return (
<>
{/* Barre de recherche - même design que Adhérents / Chauffeurs */}
<div className="bg-white rounded-lg shadow-sm p-3 sm:p-4 mb-4 sm:mb-6">
<div className="flex flex-col md:flex-row gap-4 items-center">
<div className="flex-1 w-full md:w-auto">
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<input
type="text"
placeholder="Rechercher (adhérent, n° document, chauffeur...)"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-lblue focus:border-transparent"
/>
</div>
</div>
</div>
</div>
{/* Filtres statut + actions en masse */}
{(participations.length > 0 || selectedIds.size > 0) && (
<div className="bg-white rounded-lg shadow-sm p-3 sm:p-4 mb-3 sm:mb-4">
<div className="flex flex-col gap-3 sm:gap-4">
{/* Filtres par statut */}
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs font-medium text-gray-500 uppercase tracking-wider">Statut :</span>
{[
{ key: '', label: 'Tous' },
...Object.entries(STATUT_CONFIG).map(([key, config]) => ({ key, label: config.label })),
].map(({ key, label }) => (
<button
key={key || 'all'}
onClick={() => setStatusFilter(key)}
className={`px-3 py-1.5 text-xs font-medium rounded-full transition-colors ${
statusFilter === key
? key
? `${STATUT_CONFIG[key]?.className ?? 'bg-gray-200'} ring-2 ring-offset-1 ring-gray-300`
: 'bg-lblue text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
{label}
</button>
))}
</div>
{/* Barre d'actions en masse - 2 menus déroulants */}
{selectedIds.size > 0 && (
<div className="flex flex-col sm:flex-row sm:flex-wrap items-stretch sm:items-center gap-3 pt-2 border-t border-gray-100 sm:border-t-0 sm:pt-0">
<span className="text-sm text-gray-600 order-first sm:order-none">
{selectedIds.size} sélectionné{selectedIds.size > 1 ? 's' : ''}
</span>
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
{/* Menu Changer le statut */}
<div className="relative w-full sm:w-auto">
<button
onClick={(e) => {
e.stopPropagation();
setOpenBulkDropdown(openBulkDropdown === 'statut' ? null : 'statut');
}}
disabled={bulkUpdating}
className="w-full sm:w-auto inline-flex items-center justify-center gap-2 px-4 py-2.5 sm:py-2 text-sm font-medium rounded-lg bg-lblue text-white hover:bg-dblue disabled:opacity-50 transition-colors"
>
Changer le statut
<svg className={`w-4 h-4 transition-transform flex-shrink-0 ${openBulkDropdown === 'statut' ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{openBulkDropdown === 'statut' && (
<div
className="absolute left-0 right-0 sm:right-auto sm:w-52 top-full mt-1 z-50 w-full min-w-[12rem] bg-white rounded-lg shadow-lg border border-gray-200 py-1"
onClick={(e) => e.stopPropagation()}
>
{Object.entries(STATUT_CONFIG).map(([key, config]) => (
<button
key={key}
onClick={() => { handleBulkStatusChange(key); setOpenBulkDropdown(null); }}
disabled={bulkUpdating}
className="w-full flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 disabled:opacity-50"
>
<span className={`w-2.5 h-2.5 rounded-full ${config.dot}`} />
{config.label}
</button>
))}
</div>
)}
</div>
{/* Menu Actions */}
<div className="relative w-full sm:w-auto">
<button
onClick={(e) => {
e.stopPropagation();
setOpenBulkDropdown(openBulkDropdown === 'actions' ? null : 'actions');
}}
disabled={bulkUpdating}
className="w-full sm:w-auto inline-flex items-center justify-center gap-2 px-4 py-2.5 sm:py-2 text-sm font-medium rounded-lg border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 disabled:opacity-50 transition-colors"
>
Actions
<svg className={`w-4 h-4 transition-transform flex-shrink-0 ${openBulkDropdown === 'actions' ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{openBulkDropdown === 'actions' && (
<div
className="absolute left-0 right-0 sm:left-0 sm:right-auto sm:w-52 top-full mt-1 z-50 w-full min-w-[12rem] bg-white rounded-lg shadow-lg border border-gray-200 py-1"
onClick={(e) => e.stopPropagation()}
>
<button
onClick={() => { handleBulkSendEmail(); setOpenBulkDropdown(null); }}
disabled={bulkUpdating}
className="w-full flex items-center gap-2.5 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 disabled:opacity-50"
>
<svg className="w-4 h-4 text-lgreen" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
Envoyer par email
</button>
<button
onClick={() => { setShowBulkDeleteConfirm(true); setOpenBulkDropdown(null); }}
disabled={bulkUpdating}
className="w-full flex items-center gap-2.5 px-4 py-2.5 text-sm text-red-600 hover:bg-red-50 disabled:opacity-50"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Supprimer
</button>
</div>
)}
</div>
</div>
<button
onClick={() => setSelectedIds(new Set())}
className="text-xs text-gray-500 hover:text-gray-700 underline self-start sm:ml-auto"
>
Annuler
</button>
</div>
)}
</div>
</div>
)}
{/* Tableau - même structure que Adhérents / Chauffeurs */}
<div className="bg-white rounded-lg shadow-sm overflow-hidden">
{loading ? (
<div className="p-6 sm:p-8 text-center text-sm text-gray-500">Chargement...</div>
) : filteredParticipations.length === 0 ? (
<div className="p-6 sm:p-8 text-center text-sm text-gray-500">
{searchTerm || statusFilter
? 'Aucune participation trouvée pour ces critères'
: "Aucune participation financière. Les documents sont créés automatiquement lorsqu'un trajet est terminé ou validé."}
</div>
) : (
<>
{/* Vue desktop - Tableau */}
<div className="hidden md:block overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<input
type="checkbox"
checked={filteredParticipations.length > 0 && selectedIds.size === filteredParticipations.length}
onChange={(e) => handleSelectAll(e.target.checked)}
className="w-4 h-4 text-lblue border-gray-300 rounded focus:ring-lblue"
/>
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ADHÉRENT</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">N° DOCUMENT</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">CHAUFFEUR</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">MONTANT</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">STATUT</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ACTIONS</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredParticipations.map((p) => {
const ref = getParticipationRef(p.id);
const chauffeur = p.trajet.chauffeur;
const montant = p.montant != null ? `${p.montant.toFixed(2).replace('.', ',')}` : '—';
const statutConfig = STATUT_CONFIG[p.statut] || STATUT_CONFIG.en_attente;
return (
<tr key={p.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<input
type="checkbox"
checked={selectedIds.has(p.id)}
onChange={(e) => handleSelectOne(p.id, e.target.checked)}
className="w-4 h-4 text-lblue border-gray-300 rounded focus:ring-lblue"
/>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-lblue flex items-center justify-center text-white font-semibold">
{p.adherent.prenom.charAt(0)}{p.adherent.nom.charAt(0)}
</div>
<div>
<div className="text-sm font-semibold text-gray-900">
{p.adherent.prenom} {p.adherent.nom}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="font-mono text-sm text-gray-900">{ref}</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{chauffeur ? (
<span className="text-sm text-gray-900">
{chauffeur.prenom} {chauffeur.nom}
</span>
) : (
<span className="text-sm text-gray-400 italic">Non assigné</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="text-sm font-semibold text-gray-900">{montant}</span>
</td>
<td className="px-6 py-4 whitespace-nowrap overflow-visible">
<button
onClick={(e) => {
e.stopPropagation();
if (openMenuId === p.id) {
setOpenMenuId(null);
setMenuAnchor(null);
} else {
const rect = e.currentTarget.getBoundingClientRect();
setOpenMenuId(p.id);
setMenuAnchor({ top: rect.bottom + 4, left: rect.left });
}
}}
className={`px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full cursor-pointer hover:opacity-90 transition-opacity ${statutConfig.className}`}
>
{statutConfig.label}
</button>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex items-center gap-3">
<button
onClick={() => handleViewPdf(p.id)}
className="text-lblue hover:text-dblue"
title="Voir le PDF"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</button>
<button
onClick={() => handleSendEmail(p.id)}
disabled={sendingId === p.id}
className="text-lgreen hover:text-dgreen disabled:opacity-50"
title="Envoyer par email"
>
{sendingId === p.id ? (
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
) : (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
)}
</button>
<button
onClick={() => handleEditClick(p)}
className="text-lblue hover:text-dblue"
title="Modifier"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button
onClick={() => handleDeleteClick(p.id)}
className="text-red-500 hover:text-red-700"
title="Supprimer"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{/* Vue mobile - Cartes */}
<div className="md:hidden divide-y divide-gray-200">
{filteredParticipations.map((p) => {
const ref = getParticipationRef(p.id);
const chauffeur = p.trajet.chauffeur;
const montant = p.montant != null ? `${p.montant.toFixed(2).replace('.', ',')}` : '—';
const statutConfig = STATUT_CONFIG[p.statut] || STATUT_CONFIG.en_attente;
return (
<div key={p.id} className="p-3 sm:p-4 hover:bg-gray-50">
<div className="flex items-start gap-3">
<input
type="checkbox"
checked={selectedIds.has(p.id)}
onChange={(e) => handleSelectOne(p.id, e.target.checked)}
className="w-4 h-4 text-lblue border-gray-300 rounded focus:ring-lblue mt-3 flex-shrink-0"
/>
<div className="w-12 h-12 rounded-full bg-lblue flex items-center justify-center text-white font-semibold flex-shrink-0">
{p.adherent.prenom.charAt(0)}{p.adherent.nom.charAt(0)}
</div>
<div className="flex-1 min-w-0">
<div className="mb-2">
<div className="text-base font-semibold text-gray-900">
{p.adherent.prenom} {p.adherent.nom}
</div>
<div className="font-mono text-xs text-gray-500">{ref}</div>
</div>
<div className="mb-2">
<div className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-0.5">Chauffeur</div>
{chauffeur ? (
<div className="text-sm text-gray-900">{chauffeur.prenom} {chauffeur.nom}</div>
) : (
<div className="text-sm text-gray-400 italic">Non assigné</div>
)}
</div>
<div className="mb-2">
<div className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-0.5">Montant</div>
<div className="text-sm font-semibold text-gray-900">{montant}</div>
</div>
<div className="mb-2">
<button
onClick={(e) => {
e.stopPropagation();
if (openMenuId === p.id) {
setOpenMenuId(null);
setMenuAnchor(null);
} else {
const rect = e.currentTarget.getBoundingClientRect();
setOpenMenuId(p.id);
setMenuAnchor({ top: rect.bottom + 4, left: rect.left });
}
}}
className={`px-2 py-1 inline-flex text-xs leading-4 font-semibold rounded-full cursor-pointer ${statutConfig.className}`}
>
{statutConfig.label}
</button>
</div>
<div className="grid grid-cols-2 sm:flex sm:flex-wrap gap-2 sm:gap-4 pt-2 border-t border-gray-200">
<button
onClick={() => handleViewPdf(p.id)}
className="flex items-center justify-center sm:justify-start gap-1.5 text-lblue hover:text-dblue text-xs sm:text-sm py-2 sm:py-0"
>
<svg className="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
Voir
</button>
<button
onClick={() => handleSendEmail(p.id)}
disabled={sendingId === p.id}
className="flex items-center justify-center sm:justify-start gap-1.5 text-lgreen hover:text-dgreen text-xs sm:text-sm disabled:opacity-50 py-2 sm:py-0"
>
<svg className="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
{sendingId === p.id ? 'Envoi...' : 'Envoyer'}
</button>
<button
onClick={() => handleEditClick(p)}
className="flex items-center justify-center sm:justify-start gap-1.5 text-lblue hover:text-dblue text-xs sm:text-sm py-2 sm:py-0"
>
<svg className="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Modifier
</button>
<button
onClick={() => handleDeleteClick(p.id)}
className="flex items-center justify-center sm:justify-start gap-1.5 text-red-500 hover:text-red-700 text-xs sm:text-sm py-2 sm:py-0"
>
<svg className="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Supprimer
</button>
</div>
</div>
</div>
</div>
);
})}
</div>
</>
)}
</div>
{/* Menu statut en portail pour passer au-dessus du tableau */}
{openMenuId && menuAnchor && typeof document !== 'undefined' && (() => {
const p = filteredParticipations.find((x) => x.id === openMenuId);
if (!p) return null;
return createPortal(
<div
className="fixed z-[9999] w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-1"
style={{ top: menuAnchor.top, left: menuAnchor.left }}
onClick={(e) => e.stopPropagation()}
>
<div className="px-3 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wider border-b border-gray-100">
Changer le statut
</div>
{Object.entries(STATUT_CONFIG).map(([key, config]) => (
<button
key={key}
onClick={() => handleStatutChange(p.id, key)}
disabled={key === p.statut}
className={`w-full text-left px-3 py-2 text-sm hover:bg-gray-50 flex items-center gap-2 ${
key === p.statut ? 'text-gray-400' : 'text-gray-700'
}`}
>
<span className={`w-2 h-2 rounded-full ${config.dot}`} />
{config.label}
{key === p.statut && (
<svg className="w-4 h-4 ml-auto text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)}
</button>
))}
</div>,
document.body
);
})()}
<ConfirmModal
isOpen={showDeleteConfirm}
title="Supprimer la participation"
message="Êtes-vous sûr de vouloir supprimer cette participation financière ? Le document PDF sera également supprimé."
confirmText="Supprimer"
cancelText="Annuler"
confirmColor="danger"
onConfirm={handleDelete}
onCancel={() => {
setShowDeleteConfirm(false);
setParticipationToDelete(null);
}}
/>
<ConfirmModal
isOpen={showBulkDeleteConfirm}
title="Supprimer les participations sélectionnées"
message={`Êtes-vous sûr de vouloir supprimer les ${selectedIds.size} participation(s) sélectionnée(s) ? Les documents PDF seront également supprimés.`}
confirmText="Supprimer tout"
cancelText="Annuler"
confirmColor="danger"
onConfirm={handleBulkDeleteConfirm}
onCancel={() => setShowBulkDeleteConfirm(false)}
/>
{editingParticipation && (
<ParticipationEditModal
participation={editingParticipation}
onClose={() => setEditingParticipation(null)}
onSuccess={handleEditSuccess}
/>
)}
</>
);
}