'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 = { 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([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [participationToDelete, setParticipationToDelete] = useState(null); const [sendingId, setSendingId] = useState(null); const [editingParticipation, setEditingParticipation] = useState(null); const [openMenuId, setOpenMenuId] = useState(null); const [menuAnchor, setMenuAnchor] = useState<{ top: number; left: number } | null>(null); const [statusFilter, setStatusFilter] = useState(''); const [selectedIds, setSelectedIds] = useState>(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 */}
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" />
{/* Filtres statut + actions en masse */} {(participations.length > 0 || selectedIds.size > 0) && (
{/* Filtres par statut */}
Statut : {[ { key: '', label: 'Tous' }, ...Object.entries(STATUT_CONFIG).map(([key, config]) => ({ key, label: config.label })), ].map(({ key, label }) => ( ))}
{/* Barre d'actions en masse - 2 menus déroulants */} {selectedIds.size > 0 && (
{selectedIds.size} sélectionné{selectedIds.size > 1 ? 's' : ''}
{/* Menu Changer le statut */}
{openBulkDropdown === 'statut' && (
e.stopPropagation()} > {Object.entries(STATUT_CONFIG).map(([key, config]) => ( ))}
)}
{/* Menu Actions */}
{openBulkDropdown === 'actions' && (
e.stopPropagation()} >
)}
)}
)} {/* Tableau - même structure que Adhérents / Chauffeurs */}
{loading ? (
Chargement...
) : filteredParticipations.length === 0 ? (
{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é."}
) : ( <> {/* Vue desktop - Tableau */}
{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 ( ); })}
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" /> ADHÉRENT N° DOCUMENT CHAUFFEUR MONTANT STATUT ACTIONS
handleSelectOne(p.id, e.target.checked)} className="w-4 h-4 text-lblue border-gray-300 rounded focus:ring-lblue" />
{p.adherent.prenom.charAt(0)}{p.adherent.nom.charAt(0)}
{p.adherent.prenom} {p.adherent.nom}
{ref} {chauffeur ? ( {chauffeur.prenom} {chauffeur.nom} ) : ( Non assigné )} {montant}
{/* Vue mobile - Cartes */}
{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 (
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" />
{p.adherent.prenom.charAt(0)}{p.adherent.nom.charAt(0)}
{p.adherent.prenom} {p.adherent.nom}
{ref}
Chauffeur
{chauffeur ? (
{chauffeur.prenom} {chauffeur.nom}
) : (
Non assigné
)}
Montant
{montant}
); })}
)}
{/* 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(
e.stopPropagation()} >
Changer le statut
{Object.entries(STATUT_CONFIG).map(([key, config]) => ( ))}
, document.body ); })()} { setShowDeleteConfirm(false); setParticipationToDelete(null); }} /> setShowBulkDeleteConfirm(false)} /> {editingParticipation && ( setEditingParticipation(null)} onSuccess={handleEditSuccess} /> )} ); }