'use client'; import { useState, useEffect } from 'react'; import ChauffeurForm from './ChauffeurForm'; import ConfirmModal from './ConfirmModal'; import { useBodyScrollLock } from '@/lib/body-scroll-lock'; interface Chauffeur { id: string; nom: string; prenom: string; dateNaissance: string; telephone: string; email: string; adresse: string; heuresContrat: number; dateDebutContrat: string; dateFinContrat: string | null; heuresRestantes?: number; status?: string; } export default function ChauffeursTable() { const [chauffeurs, setChauffeurs] = useState([]); const [search, setSearch] = useState(''); const [loading, setLoading] = useState(true); const [showForm, setShowForm] = useState(false); const [editingChauffeur, setEditingChauffeur] = useState(null); const [viewingChauffeur, setViewingChauffeur] = useState(null); const [selectedIds, setSelectedIds] = useState>(new Set()); const [showImportModal, setShowImportModal] = useState(false); const [resultModal, setResultModal] = useState<{ show: boolean; type: 'success' | 'error' | 'info'; title: string; message: string; details?: string[]; } | null>(null); const [confirmDeleteModal, setConfirmDeleteModal] = useState<{ show: boolean; id: string | null; } | null>(null); useBodyScrollLock(showImportModal || !!(resultModal?.show) || !!viewingChauffeur); const fetchChauffeurs = async (searchTerm: string = '') => { setLoading(true); try { const url = searchTerm ? `/api/chauffeurs?search=${encodeURIComponent(searchTerm)}` : '/api/chauffeurs'; const response = await fetch(url); if (response.ok) { const data = await response.json(); setChauffeurs(data); } } catch (error) { console.error('Erreur lors du chargement des chauffeurs:', error); } finally { setLoading(false); } }; useEffect(() => { const timeoutId = setTimeout(() => { fetchChauffeurs(search); }, 300); // Debounce de 300ms pour la recherche return () => clearTimeout(timeoutId); }, [search]); useEffect(() => { fetchChauffeurs(); }, []); const handleDelete = async (id: string) => { setConfirmDeleteModal({ show: true, id, }); }; const confirmDelete = async () => { if (!confirmDeleteModal?.id) return; try { const response = await fetch(`/api/chauffeurs/${confirmDeleteModal.id}`, { method: 'DELETE', }); if (response.ok) { fetchChauffeurs(search); setResultModal({ show: true, type: 'success', title: 'Suppression réussie', message: 'Le chauffeur a été supprimé avec succès', }); } else { setResultModal({ show: true, type: 'error', title: 'Erreur', message: 'Erreur lors de la suppression', }); } } catch (error) { console.error('Erreur lors de la suppression:', error); setResultModal({ show: true, type: 'error', title: 'Erreur', message: 'Erreur lors de la suppression', }); } finally { setConfirmDeleteModal(null); } }; const handleEdit = (chauffeur: Chauffeur) => { setEditingChauffeur(chauffeur); setShowForm(true); }; const handleView = async (id: string) => { try { const response = await fetch(`/api/chauffeurs/${id}`); if (response.ok) { const data = await response.json(); setViewingChauffeur(data); } } catch (error) { console.error('Erreur lors de la récupération:', error); } }; const handleFormClose = () => { setShowForm(false); setEditingChauffeur(null); fetchChauffeurs(search); }; const getInitials = (nom: string, prenom: string) => { return `${prenom.charAt(0)}${nom.charAt(0)}`.toUpperCase(); }; const getStatusColor = (status: string) => { switch (status) { case 'Disponible': return 'bg-lblue text-white'; case 'Vacances': return 'bg-lblue text-white'; case 'Arrêt Maladie': return 'bg-lorange text-white'; default: return 'bg-gray-200 text-gray-700'; } }; const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' }); }; const getProgressPercentage = (restantes: number, total: number) => { return ((total - restantes) / total) * 100; }; const handleSelectAll = (checked: boolean) => { if (checked) { setSelectedIds(new Set(chauffeurs.map(c => c.id))); } else { setSelectedIds(new Set()); } }; const handleSelectOne = (id: string, checked: boolean) => { const newSelected = new Set(selectedIds); if (checked) { newSelected.add(id); } else { newSelected.delete(id); } setSelectedIds(newSelected); }; const handleExport = () => { if (selectedIds.size === 0) { setResultModal({ show: true, type: 'info', title: 'Aucune sélection', message: 'Veuillez sélectionner au moins un chauffeur à exporter', }); return; } const selectedChauffeurs = chauffeurs.filter(c => selectedIds.has(c.id)); // Créer les en-têtes CSV avec tous les champs disponibles const headers = [ 'Nom', 'Prénom', 'Date de naissance', 'Téléphone', 'Email', 'Adresse', 'Heures contrat', 'Date début contrat', 'Date fin contrat', 'Status' ]; // Créer les lignes CSV const rows = selectedChauffeurs.map(chauffeur => { const formatDateForCSV = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' }); }; return [ chauffeur.nom || '', chauffeur.prenom || '', formatDateForCSV(chauffeur.dateNaissance), chauffeur.telephone || '', chauffeur.email || '', chauffeur.adresse || '', chauffeur.heuresContrat?.toString() || '', formatDateForCSV(chauffeur.dateDebutContrat), chauffeur.dateFinContrat ? formatDateForCSV(chauffeur.dateFinContrat) : '', chauffeur.status || 'Disponible' ]; }); // Créer le contenu CSV const csvContent = [ headers.join(','), ...rows.map(row => row.map(cell => `"${cell.replace(/"/g, '""')}"`).join(',')) ].join('\n'); // Créer le blob et télécharger const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' }); // BOM pour Excel const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', `chauffeurs_export_${new Date().toISOString().split('T')[0]}.csv`); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); // Réinitialiser la sélection setSelectedIds(new Set()); }; const handleDownloadTemplate = () => { const headers = [ 'Nom', 'Prénom', 'Date de naissance', 'Téléphone', 'Email', 'Adresse', 'Heures contrat', 'Date début contrat', 'Date fin contrat', 'Status' ]; // Ligne d'exemple const exampleRow = [ 'Dupont', 'Jean', '15/03/1980', '0123456789', 'jean.dupont@example.com', '123 Rue de la Paix, 75001 Paris', '35', '01/01/2024', '', 'Disponible' ]; const csvContent = [ headers.join(','), exampleRow.map(cell => `"${cell}"`).join(',') ].join('\n'); const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', 'modele_import_chauffeurs.csv'); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); }; const handleFileUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = async (e) => { const text = e.target?.result as string; const lines = text.split('\n').filter(line => line.trim()); if (lines.length < 2) { setResultModal({ show: true, type: 'error', title: 'Fichier invalide', message: 'Le fichier CSV doit contenir au moins une ligne d\'en-tête et une ligne de données', }); return; } // Détecter le séparateur (virgule ou point-virgule) const detectSeparator = (firstLine: string): string => { const commaCount = (firstLine.match(/,/g) || []).length; const semicolonCount = (firstLine.match(/;/g) || []).length; return semicolonCount >= commaCount ? ';' : ','; }; // Parser le CSV (gestion simple des guillemets avec détection automatique du séparateur) const parseCSVLine = (line: string, separator: string): string[] => { const result: string[] = []; let current = ''; let inQuotes = false; for (let i = 0; i < line.length; i++) { const char = line[i]; if (char === '"') { if (inQuotes && line[i + 1] === '"') { current += '"'; i++; } else { inQuotes = !inQuotes; } } else if (char === separator && !inQuotes) { result.push(current.trim()); current = ''; } else { current += char; } } result.push(current.trim()); return result; }; // Fonction pour normaliser les noms de colonnes const normalizeHeader = (header: string): string => { return header .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/\s+/g, '') .trim(); }; // Fonction pour trouver une colonne avec plusieurs variantes possibles const findColumnIndex = (patterns: string[], excludePatterns: string[] = []): number => { for (let i = 0; i < headers.length; i++) { const normalized = normalizeHeader(headers[i]); const matchesPattern = patterns.some(pattern => normalized.includes(pattern)); const matchesExclude = excludePatterns.some(pattern => normalized.includes(pattern)); if (matchesPattern && !matchesExclude) { return i; } } return -1; }; // Détecter le séparateur depuis la première ligne const separator = detectSeparator(lines[0]); const headers = parseCSVLine(lines[0], separator); const dataLines = lines.slice(1); // Mapping des colonnes avec leurs noms d'affichage const nomIndex = findColumnIndex(['nom'], ['prenom', 'prénom']); const nomHeader = nomIndex >= 0 ? headers[nomIndex] : 'Nom'; const prenomIndex = findColumnIndex(['prenom', 'prénom']); const prenomHeader = prenomIndex >= 0 ? headers[prenomIndex] : 'Prénom'; // Pour la date de naissance const dateNaissanceIndex = (() => { for (let i = 0; i < headers.length; i++) { const normalized = normalizeHeader(headers[i]); if ((normalized.includes('date') || normalized.includes('naissance') || normalized.includes('birth') || normalized.includes('dob')) && (normalized.includes('naissance') || normalized.includes('birth') || normalized.includes('dob') || normalized.includes('date'))) { return i; } } return -1; })(); const dateNaissanceHeader = dateNaissanceIndex >= 0 ? headers[dateNaissanceIndex] : 'Date de naissance'; const telephoneIndex = findColumnIndex(['telephone', 'téléphone', 'tel', 'phone']); const telephoneHeader = telephoneIndex >= 0 ? headers[telephoneIndex] : 'Téléphone'; const emailIndex = findColumnIndex(['email', 'mail', 'courriel']); const emailHeader = emailIndex >= 0 ? headers[emailIndex] : 'Email'; const adresseIndex = findColumnIndex(['adresse']); const adresseHeader = adresseIndex >= 0 ? headers[adresseIndex] : 'Adresse'; const heuresContratIndex = findColumnIndex(['heures', 'contrat'], ['debut', 'début', 'fin']); const heuresContratHeader = heuresContratIndex >= 0 ? headers[heuresContratIndex] : 'Heures contrat'; const dateDebutContratIndex = findColumnIndex(['debut', 'début'], ['fin']); const dateDebutContratHeader = dateDebutContratIndex >= 0 ? headers[dateDebutContratIndex] : 'Date début contrat'; const dateFinContratIndex = findColumnIndex(['fin'], ['debut', 'début']); const dateFinContratHeader = dateFinContratIndex >= 0 ? headers[dateFinContratIndex] : 'Date fin contrat'; const statusIndex = findColumnIndex(['status', 'statut']); const statusHeader = statusIndex >= 0 ? headers[statusIndex] : 'Status'; // Vérifier que les colonnes obligatoires sont présentes const missingRequiredColumns: string[] = []; if (nomIndex === -1) missingRequiredColumns.push('Nom'); if (prenomIndex === -1) missingRequiredColumns.push('Prénom'); if (dateNaissanceIndex === -1) missingRequiredColumns.push('Date de naissance'); if (telephoneIndex === -1) missingRequiredColumns.push('Téléphone'); if (emailIndex === -1) missingRequiredColumns.push('Email'); if (adresseIndex === -1) missingRequiredColumns.push('Adresse'); if (heuresContratIndex === -1) missingRequiredColumns.push('Heures contrat'); if (dateDebutContratIndex === -1) missingRequiredColumns.push('Date début contrat'); if (missingRequiredColumns.length > 0) { const availableColumns = headers.length > 0 ? headers : ['Aucune colonne détectée']; setResultModal({ show: true, type: 'error', title: 'Colonnes manquantes', message: `Le fichier CSV ne contient pas les colonnes obligatoires suivantes : ${missingRequiredColumns.join(', ')}`, details: [ 'Colonnes détectées dans le fichier :', ...availableColumns.map(col => ` • ${col}`), '', 'Conseil : Vérifiez que les noms de colonnes correspondent exactement au modèle (les accents et la casse sont importants).' ], }); return; } let successCount = 0; let errorCount = 0; const errors: string[] = []; for (let i = 0; i < dataLines.length; i++) { const row = parseCSVLine(dataLines[i], separator); if (row.length === 0) continue; const nom = nomIndex >= 0 ? (row[nomIndex] || '').trim() : ''; const prenom = prenomIndex >= 0 ? (row[prenomIndex] || '').trim() : ''; const dateNaissance = dateNaissanceIndex >= 0 ? (row[dateNaissanceIndex] || '').trim() : ''; const telephone = telephoneIndex >= 0 ? (row[telephoneIndex] || '').trim() : ''; const email = emailIndex >= 0 ? (row[emailIndex] || '').trim() : ''; const adresse = adresseIndex >= 0 ? (row[adresseIndex] || '').trim() : ''; const heuresContrat = heuresContratIndex >= 0 ? (row[heuresContratIndex] || '').trim() : ''; const dateDebutContrat = dateDebutContratIndex >= 0 ? (row[dateDebutContratIndex] || '').trim() : ''; const dateFinContrat = dateFinContratIndex >= 0 ? (row[dateFinContratIndex] || '').trim() : ''; const status = statusIndex >= 0 ? (row[statusIndex] || '').trim() : 'Disponible'; // Validation des champs obligatoires avec identification précise des colonnes manquantes const missingFields: string[] = []; if (!nom) missingFields.push(nomHeader); if (!prenom) missingFields.push(prenomHeader); if (!dateNaissance) missingFields.push(dateNaissanceHeader); if (!telephone) missingFields.push(telephoneHeader); if (!email) missingFields.push(emailHeader); if (!adresse) missingFields.push(adresseHeader); if (!heuresContrat) missingFields.push(heuresContratHeader); if (!dateDebutContrat) missingFields.push(dateDebutContratHeader); if (missingFields.length > 0) { errorCount++; const fieldsList = missingFields.join(', '); errors.push(`Ligne ${i + 2}: Colonnes manquantes ou vides : ${fieldsList}`); continue; } // Convertir les dates au format ISO let dateNaissanceISO = ''; let dateDebutContratISO = ''; let dateFinContratISO: string | null = null; try { const dateParts = dateNaissance.split('/'); if (dateParts.length === 3) { dateNaissanceISO = `${dateParts[2]}-${dateParts[1]}-${dateParts[0]}`; } else { dateNaissanceISO = new Date(dateNaissance).toISOString().split('T')[0]; } } catch { errorCount++; errors.push(`Ligne ${i + 2}: Colonne "${dateNaissanceHeader}" - Format de date invalide (attendu: JJ/MM/AAAA)`); continue; } try { const dateParts = dateDebutContrat.split('/'); if (dateParts.length === 3) { dateDebutContratISO = `${dateParts[2]}-${dateParts[1]}-${dateParts[0]}`; } else { dateDebutContratISO = new Date(dateDebutContrat).toISOString().split('T')[0]; } } catch { errorCount++; errors.push(`Ligne ${i + 2}: Colonne "${dateDebutContratHeader}" - Format de date invalide (attendu: JJ/MM/AAAA)`); continue; } if (dateFinContrat) { try { const dateParts = dateFinContrat.split('/'); if (dateParts.length === 3) { dateFinContratISO = `${dateParts[2]}-${dateParts[1]}-${dateParts[0]}`; } else { dateFinContratISO = new Date(dateFinContrat).toISOString().split('T')[0]; } } catch { // Si la date de fin est invalide, on continue sans elle (elle est optionnelle) } } // Valider les heures contrat const heuresContratNum = parseInt(heuresContrat, 10); if (isNaN(heuresContratNum) || heuresContratNum <= 0) { errorCount++; errors.push(`Ligne ${i + 2}: Colonne "${heuresContratHeader}" - Doit être un nombre positif`); continue; } try { const response = await fetch('/api/chauffeurs', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ nom, prenom, dateNaissance: dateNaissanceISO, telephone, email, adresse, heuresContrat: heuresContratNum, dateDebutContrat: dateDebutContratISO, dateFinContrat: dateFinContratISO, }), }); if (response.ok) { successCount++; } else { errorCount++; try { const errorData = await response.json(); let errorMsg = errorData.error || 'Erreur lors de l\'import'; if (errorMsg.includes('champs obligatoires')) { errorMsg = `Erreur de validation : ${errorMsg}`; } errors.push(`Ligne ${i + 2}: ${errorMsg}`); } catch { errors.push(`Ligne ${i + 2}: Erreur serveur lors de l'import`); } } } catch (error) { errorCount++; errors.push(`Ligne ${i + 2}: Erreur réseau - ${error instanceof Error ? error.message : 'Connexion impossible'}`); } } // Afficher les résultats dans une modale const title = errorCount > 0 ? `Import terminé avec ${errorCount} erreur(s)` : 'Import réussi'; const message = errorCount === 0 ? `${successCount} chauffeur(s) importé(s) avec succès` : `${successCount} chauffeur(s) importé(s) avec succès, ${errorCount} erreur(s)`; setResultModal({ show: true, type: errorCount === 0 ? 'success' : 'error', title, message, details: errors.length > 0 ? errors : undefined, }); // Rafraîchir la liste fetchChauffeurs(search); setShowImportModal(false); }; reader.readAsText(file, 'UTF-8'); }; return ( <> {/* Barre de recherche et actions */}
{/* Barre de recherche */}
setSearch(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" />
{/* Boutons d'action */}
{/* Tableau - Desktop */}
{loading ? (
Chargement...
) : chauffeurs.length === 0 ? (
Aucun chauffeur trouvé
) : ( <> {/* Vue desktop - Tableau */}
{chauffeurs.map((chauffeur) => ( ))}
0 && selectedIds.size === chauffeurs.length} onChange={(e) => handleSelectAll(e.target.checked)} className="w-4 h-4 text-lblue border-gray-300 rounded focus:ring-lblue" /> NOM CONTACT ADRESSE NOMBRES D'HEURES STATUS ACTIONS
handleSelectOne(chauffeur.id, e.target.checked)} className="w-4 h-4 text-lblue border-gray-300 rounded focus:ring-lblue" />
{getInitials(chauffeur.nom, chauffeur.prenom)}
{chauffeur.prenom} {chauffeur.nom}
Né le {formatDate(chauffeur.dateNaissance)}
{chauffeur.telephone}
{chauffeur.email}
{chauffeur.adresse}
{chauffeur.heuresRestantes || chauffeur.heuresContrat}h restantes sur {chauffeur.heuresContrat}h
{chauffeur.status && ( {chauffeur.status} )}
{/* Vue mobile - Cartes */}
{chauffeurs.map((chauffeur) => (
{/* Checkbox */} handleSelectOne(chauffeur.id, e.target.checked)} className="w-4 h-4 text-lblue border-gray-300 rounded focus:ring-lblue mt-1" /> {/* Avatar */}
{getInitials(chauffeur.nom, chauffeur.prenom)}
{/* Contenu principal */}
{/* Nom et date de naissance */}
{chauffeur.prenom} {chauffeur.nom}
Né le {formatDate(chauffeur.dateNaissance)}
{/* Contact */} {/* Adresse */}
Adresse
{chauffeur.adresse}
{/* Heures */}
{chauffeur.heuresRestantes || chauffeur.heuresContrat}h restantes sur {chauffeur.heuresContrat}h
{/* Status */} {chauffeur.status && (
{chauffeur.status}
)} {/* Actions */}
))}
)}
{/* Modal formulaire */} {showForm && ( )} {/* Modal import */} {showImportModal && (
{/* Header */}

Importer des chauffeurs

Téléchargez le modèle d'exemple, remplissez-le et importez-le ici

{/* Contenu */}
{/* Télécharger le modèle */}

Télécharger le modèle d'exemple

Téléchargez le fichier CSV modèle pour voir le format attendu et remplir vos données.

{/* Upload fichier */}

ou glissez-déposez

CSV jusqu'à 10MB

{/* Instructions */}

Instructions :

  • Les champs Nom, Prénom, Date de naissance, Téléphone, Email, Adresse, Heures contrat et Date début contrat sont obligatoires
  • Le format de date attendu est JJ/MM/AAAA
  • Les champs Date fin contrat et Status sont optionnels
  • Assurez-vous que le fichier utilise l'encodage UTF-8
{/* Footer */}
)} {/* Modal résultat */} {resultModal && resultModal.show && (
setResultModal(null)} >
e.stopPropagation()} > {/* Header */}
{resultModal.type === 'success' && ( )} {resultModal.type === 'error' && ( )} {resultModal.type === 'info' && ( )}

{resultModal.title}

{/* Contenu */}

{resultModal.message}

{resultModal.details && resultModal.details.length > 0 && (

{resultModal.details[0].includes('Colonnes détectées') ? 'Détails :' : 'Erreurs :'}

    {resultModal.details.map((detail, index) => (
  • {detail}
  • ))}
)}
{/* Footer */}
)} {/* Modal de confirmation de suppression */} {confirmDeleteModal && ( setConfirmDeleteModal(null)} /> )} {/* Modal vue détaillée - Design épuré */} {viewingChauffeur && (
{/* Header épuré */}
{getInitials(viewingChauffeur.nom, viewingChauffeur.prenom)}

{viewingChauffeur.prenom} {viewingChauffeur.nom}

Informations détaillées du chauffeur

{viewingChauffeur.status && (
{viewingChauffeur.status}
)}
{/* Contenu scrollable */}
{/* Actions rapides */}
{/* Carte Informations personnelles */}

Informations personnelles

Date de naissance

{formatDate(viewingChauffeur.dateNaissance)}

Adresse

{viewingChauffeur.adresse}

{/* Carte Informations contractuelles */}

Informations contractuelles

Contrat d'heure

{viewingChauffeur.heuresContrat}h

Date de début

{formatDate(viewingChauffeur.dateDebutContrat)}

{viewingChauffeur.dateFinContrat && (

Date de fin

{formatDate(viewingChauffeur.dateFinContrat)}

)} {viewingChauffeur.heuresRestantes !== undefined && (

Heures restantes

{viewingChauffeur.heuresRestantes}h / {viewingChauffeur.heuresContrat}h

{Math.round((viewingChauffeur.heuresRestantes / viewingChauffeur.heuresContrat) * 100)}% d'heures restantes

)}
{/* Footer avec actions */}
)} ); }