From ccff9044648ea272e6e98d1584918acf9b55bba2 Mon Sep 17 00:00:00 2001 From: Pierre Date: Fri, 6 Feb 2026 11:34:16 +0100 Subject: [PATCH] Added few functions to the platform --- components/AdherentForm.tsx | 32 +- components/AdherentsTable.tsx | 876 +++++++++++++++++++++++++++- components/AlertModal.tsx | 127 ++++ components/ChatWindow.tsx | 32 +- components/ChauffeurForm.tsx | 32 +- components/ChauffeursTable.tsx | 742 ++++++++++++++++++++++- components/ConfigurationContent.tsx | 133 ++++- components/ConfirmModal.tsx | 10 +- components/GroupSettingsModal.tsx | 109 +++- components/NewConversationModal.tsx | 46 +- components/ParametresContent.tsx | 36 +- components/UniversProForm.tsx | 32 +- components/UniversProTable.tsx | 637 +++++++++++++++++++- prisma/dev.db | Bin 180224 -> 180224 bytes 14 files changed, 2770 insertions(+), 74 deletions(-) create mode 100644 components/AlertModal.tsx diff --git a/components/AdherentForm.tsx b/components/AdherentForm.tsx index 6e26cef..05b32c2 100644 --- a/components/AdherentForm.tsx +++ b/components/AdherentForm.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useEffect } from 'react'; +import AlertModal from './AlertModal'; interface Adherent { id: string; @@ -26,6 +27,12 @@ interface AdherentFormProps { export default function AdherentForm({ adherent, onClose }: AdherentFormProps) { const [loading, setLoading] = useState(false); + const [alertModal, setAlertModal] = useState<{ + show: boolean; + type: 'success' | 'error' | 'info' | 'warning'; + title: string; + message: string; + } | null>(null); const [options, setOptions] = useState<{ situation: Array<{ id: string; value: string }>; prescripteur: Array<{ id: string; value: string }>; @@ -126,11 +133,21 @@ export default function AdherentForm({ adherent, onClose }: AdherentFormProps) { onClose(); } else { const error = await response.json(); - alert(error.error || 'Une erreur est survenue'); + setAlertModal({ + show: true, + type: 'error', + title: 'Erreur', + message: error.error || 'Une erreur est survenue', + }); } } catch (error) { console.error('Erreur:', error); - alert('Une erreur est survenue'); + setAlertModal({ + show: true, + type: 'error', + title: 'Erreur', + message: 'Une erreur est survenue', + }); } finally { setLoading(false); } @@ -492,6 +509,17 @@ export default function AdherentForm({ adherent, onClose }: AdherentFormProps) { + + {/* Modal d'alerte */} + {alertModal && ( + setAlertModal(null)} + /> + )} ); } diff --git a/components/AdherentsTable.tsx b/components/AdherentsTable.tsx index 130a69a..a17a361 100644 --- a/components/AdherentsTable.tsx +++ b/components/AdherentsTable.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import AdherentForm from './AdherentForm'; +import ConfirmModal from './ConfirmModal'; interface Adherent { id: string; @@ -14,6 +15,7 @@ interface Adherent { situation?: string | null; prescripteur?: string | null; facturation?: string | null; + forfait?: string | null; commentaire?: string | null; telephoneSecondaire?: string | null; instructions?: string | null; @@ -26,6 +28,19 @@ export default function AdherentsTable() { const [showForm, setShowForm] = useState(false); const [editingAdherent, setEditingAdherent] = useState(null); const [viewingAdherent, setViewingAdherent] = 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); const fetchAdherents = async (searchTerm: string = '') => { setLoading(true); @@ -58,23 +73,46 @@ export default function AdherentsTable() { }, []); const handleDelete = async (id: string) => { - if (!confirm('Êtes-vous sûr de vouloir supprimer cet adhérent ?')) { - return; - } + setConfirmDeleteModal({ + show: true, + id, + }); + }; + + const confirmDelete = async () => { + if (!confirmDeleteModal?.id) return; try { - const response = await fetch(`/api/adherents/${id}`, { + const response = await fetch(`/api/adherents/${confirmDeleteModal.id}`, { method: 'DELETE', }); if (response.ok) { fetchAdherents(search); + setResultModal({ + show: true, + type: 'success', + title: 'Suppression réussie', + message: 'L\'adhérent a été supprimé avec succès', + }); } else { - alert('Erreur lors de la suppression'); + setResultModal({ + show: true, + type: 'error', + title: 'Erreur', + message: 'Erreur lors de la suppression', + }); } } catch (error) { console.error('Erreur lors de la suppression:', error); - alert('Erreur lors de la suppression'); + setResultModal({ + show: true, + type: 'error', + title: 'Erreur', + message: 'Erreur lors de la suppression', + }); + } finally { + setConfirmDeleteModal(null); } }; @@ -110,6 +148,556 @@ export default function AdherentsTable() { return date.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' }); }; + const handleSelectAll = (checked: boolean) => { + if (checked) { + setSelectedIds(new Set(adherents.map(a => a.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 adhérent à exporter', + }); + return; + } + + const selectedAdherents = adherents.filter(a => selectedIds.has(a.id)); + + // Créer les en-têtes CSV avec tous les champs disponibles + const headers = [ + 'Nom', + 'Prénom', + 'Date de naissance', + 'Adresse', + 'Email', + 'Téléphone', + 'Téléphone secondaire', + 'Situation', + 'Prescripteur', + 'Facturation', + 'Forfait', + 'Commentaire', + 'Instructions' + ]; + + // Créer les lignes CSV + const rows = selectedAdherents.map(adherent => { + const formatDateForCSV = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' }); + }; + + return [ + adherent.nom || '', + adherent.prenom || '', + formatDateForCSV(adherent.dateNaissance), + adherent.adresse || '', + adherent.email || '', + adherent.telephone || '', + adherent.telephoneSecondaire || '', + adherent.situation || '', + adherent.prescripteur || '', + adherent.facturation || '', + adherent.forfait || '', + (adherent.commentaire || '').replace(/"/g, '""'), // Échapper les guillemets + (adherent.instructions || '').replace(/"/g, '""') // Échapper les guillemets + ]; + }); + + // Créer le contenu CSV + const csvContent = [ + headers.join(','), + ...rows.map(row => row.map(cell => `"${cell}"`).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', `adherents_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', + 'Adresse', + 'Email', + 'Téléphone', + 'Téléphone secondaire', + 'Situation', + 'Prescripteur', + 'Facturation', + 'Forfait', + 'Commentaire', + 'Instructions' + ]; + + // Ligne d'exemple + const exampleRow = [ + 'Dupont', + 'Jean', + '15/03/1980', + '123 Rue de la Paix, 75001 Paris', + 'jean.dupont@example.com', + '0123456789', + '0987654321', + 'Aucun', + 'Aucun', + 'Aucun', + 'Aucun', + '', + '' + ]; + + 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_adherents.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; + } + + // Charger les options existantes + let existingOptions: { + situation: Array<{ id: string; value: string }>; + prescripteur: Array<{ id: string; value: string }>; + facturation: Array<{ id: string; value: string }>; + forfait: Array<{ id: string; value: string }>; + } = { + situation: [], + prescripteur: [], + facturation: [], + forfait: [], + }; + + try { + const optionsResponse = await fetch('/api/settings/adherent-options'); + if (optionsResponse.ok) { + const optionsData = await optionsResponse.json(); + existingOptions = { + situation: optionsData.situation || [], + prescripteur: optionsData.prescripteur || [], + facturation: optionsData.facturation || [], + forfait: optionsData.forfait || [], + }; + } + } catch (error) { + console.error('Erreur lors du chargement des options:', error); + } + + // Fonction pour créer une nouvelle option si elle n'existe pas + const ensureOptionExists = async (type: 'situation' | 'prescripteur' | 'facturation' | 'forfait', value: string): Promise => { + if (!value || value.trim() === '') return true; // Valeur vide, pas besoin de créer + + const existing = existingOptions[type].find(opt => opt.value.toLowerCase() === value.toLowerCase()); + if (existing) return true; // L'option existe déjà + + try { + const response = await fetch('/api/settings/adherent-options', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + type, + value: value.trim(), + }), + }); + + if (response.ok) { + const newOption = await response.json(); + existingOptions[type].push(newOption); + return true; + } else { + // Si l'option existe déjà (erreur 400), c'est OK + const errorData = await response.json(); + if (errorData.error?.includes('existe déjà')) { + return true; + } + return false; + } + } catch (error) { + console.error(`Erreur lors de la création de l'option ${type}:`, error); + return false; + } + }; + + // 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; + // Utiliser le séparateur le plus fréquent, ou point-virgule par défaut pour les fichiers français + 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; + }; + + // 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); + + // Fonction pour normaliser les noms de colonnes (enlever accents, espaces, mettre en minuscule) + const normalizeHeader = (header: string): string => { + return header + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') // Enlever les accents + .replace(/\s+/g, '') // Enlever les espaces + .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; + }; + + // 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, chercher une colonne qui contient "date" ET "naissance" ou des variantes + 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 adresseIndex = findColumnIndex(['adresse']); + const adresseHeader = adresseIndex >= 0 ? headers[adresseIndex] : 'Adresse'; + const emailIndex = findColumnIndex(['email', 'mail', 'courriel']); + const emailHeader = emailIndex >= 0 ? headers[emailIndex] : 'Email'; + const telephoneIndex = findColumnIndex(['telephone', 'téléphone', 'tel', 'phone'], ['secondaire', 'sec']); + const telephoneHeader = telephoneIndex >= 0 ? headers[telephoneIndex] : 'Téléphone'; + const telephoneSecondaireIndex = findColumnIndex(['telephone', 'téléphone', 'tel', 'phone'], []); // Trouver celui qui contient "secondaire" + const telephoneSecondaireHeader = (() => { + for (let i = 0; i < headers.length; i++) { + const normalized = normalizeHeader(headers[i]); + if ((normalized.includes('telephone') || normalized.includes('tel') || normalized.includes('phone')) && + (normalized.includes('secondaire') || normalized.includes('sec') || normalized.includes('2'))) { + return headers[i]; + } + } + return 'Téléphone secondaire'; + })(); + const situationIndex = findColumnIndex(['situation']); + const situationHeader = situationIndex >= 0 ? headers[situationIndex] : 'Situation'; + const prescripteurIndex = findColumnIndex(['prescripteur']); + const prescripteurHeader = prescripteurIndex >= 0 ? headers[prescripteurIndex] : 'Prescripteur'; + const facturationIndex = findColumnIndex(['facturation']); + const facturationHeader = facturationIndex >= 0 ? headers[facturationIndex] : 'Facturation'; + const forfaitIndex = findColumnIndex(['forfait']); + const forfaitHeader = forfaitIndex >= 0 ? headers[forfaitIndex] : 'Forfait'; + const commentaireIndex = findColumnIndex(['commentaire', 'comment']); + const commentaireHeader = commentaireIndex >= 0 ? headers[commentaireIndex] : 'Commentaire'; + const instructionsIndex = findColumnIndex(['instructions', 'instruction']); + const instructionsHeader = instructionsIndex >= 0 ? headers[instructionsIndex] : 'Instructions'; + + // Vérifier que les colonnes obligatoires sont présentes (après le mapping) + 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 (adresseIndex === -1) missingRequiredColumns.push('Adresse'); + if (emailIndex === -1) missingRequiredColumns.push('Email'); + if (telephoneIndex === -1) missingRequiredColumns.push('Téléphone'); + + if (missingRequiredColumns.length > 0) { + // Afficher les colonnes disponibles pour aider au débogage + 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[] = []; + const newOptionsCreated: 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 adresse = adresseIndex >= 0 ? (row[adresseIndex] || '').trim() : ''; + const email = emailIndex >= 0 ? (row[emailIndex] || '').trim() : ''; + const telephone = telephoneIndex >= 0 ? (row[telephoneIndex] || '').trim() : ''; + + // 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 (!adresse) missingFields.push(adresseHeader); + if (!email) missingFields.push(emailHeader); + if (!telephone) missingFields.push(telephoneHeader); + + if (missingFields.length > 0) { + errorCount++; + const fieldsList = missingFields.join(', '); + errors.push(`Ligne ${i + 2}: Colonnes manquantes ou vides : ${fieldsList}`); + continue; + } + + // Convertir la date au format ISO + let dateISO = ''; + try { + const dateParts = dateNaissance.split('/'); + if (dateParts.length === 3) { + dateISO = `${dateParts[2]}-${dateParts[1]}-${dateParts[0]}`; + } else { + dateISO = 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; + } + + // Récupérer les valeurs des champs optionnels + const situationValue = situationIndex >= 0 ? row[situationIndex]?.trim() : ''; + const prescripteurValue = prescripteurIndex >= 0 ? row[prescripteurIndex]?.trim() : ''; + const facturationValue = facturationIndex >= 0 ? row[facturationIndex]?.trim() : ''; + const forfaitValue = forfaitIndex >= 0 ? row[forfaitIndex]?.trim() : ''; + + // Créer les options manquantes avant de créer l'adhérent + if (situationValue) { + const existed = existingOptions.situation.find(opt => opt.value.toLowerCase() === situationValue.toLowerCase()); + if (!existed) { + const created = await ensureOptionExists('situation', situationValue); + if (created) { + newOptionsCreated.push(`Situation: "${situationValue}"`); + } + } + } + + if (prescripteurValue) { + const existed = existingOptions.prescripteur.find(opt => opt.value.toLowerCase() === prescripteurValue.toLowerCase()); + if (!existed) { + const created = await ensureOptionExists('prescripteur', prescripteurValue); + if (created) { + newOptionsCreated.push(`Prescripteur: "${prescripteurValue}"`); + } + } + } + + if (facturationValue) { + const existed = existingOptions.facturation.find(opt => opt.value.toLowerCase() === facturationValue.toLowerCase()); + if (!existed) { + const created = await ensureOptionExists('facturation', facturationValue); + if (created) { + newOptionsCreated.push(`Facturation: "${facturationValue}"`); + } + } + } + + if (forfaitValue) { + const existed = existingOptions.forfait.find(opt => opt.value.toLowerCase() === forfaitValue.toLowerCase()); + if (!existed) { + const created = await ensureOptionExists('forfait', forfaitValue); + if (created) { + newOptionsCreated.push(`Forfait: "${forfaitValue}"`); + } + } + } + + try { + const response = await fetch('/api/adherents', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + nom, + prenom, + dateNaissance: dateISO, + adresse, + email, + telephone, + telephoneSecondaire: telephoneSecondaireIndex >= 0 ? row[telephoneSecondaireIndex]?.trim() || null : null, + situation: situationValue || null, + prescripteur: prescripteurValue || null, + facturation: facturationValue || null, + forfait: forfaitValue || null, + commentaire: commentaireIndex >= 0 ? row[commentaireIndex]?.trim() || null : null, + instructions: instructionsIndex >= 0 ? row[instructionsIndex]?.trim() || null : null, + }), + }); + + if (response.ok) { + successCount++; + } else { + errorCount++; + try { + const errorData = await response.json(); + // Essayer d'identifier les champs problématiques depuis le message d'erreur + let errorMsg = errorData.error || 'Erreur lors de l\'import'; + if (errorMsg.includes('champs obligatoires')) { + // Si l'erreur vient du serveur, on peut ajouter plus de contexte + 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'; + + let message = errorCount === 0 + ? `${successCount} adhérent(s) importé(s) avec succès` + : `${successCount} adhérent(s) importé(s) avec succès, ${errorCount} erreur(s)`; + + // Ajouter les informations sur les nouvelles options créées + if (newOptionsCreated.length > 0) { + message += `\n\n${newOptionsCreated.length} nouvelle(s) option(s) ajoutée(s) aux sélecteurs.`; + } + + // Combiner les erreurs et les nouvelles options dans les détails + const details: string[] = []; + if (newOptionsCreated.length > 0) { + details.push('Nouvelles options créées :'); + details.push(...newOptionsCreated); + if (errors.length > 0) { + details.push(''); + details.push('Erreurs :'); + } + } + if (errors.length > 0) { + details.push(...errors); + } + + setResultModal({ + show: true, + type: errorCount === 0 ? 'success' : 'error', + title, + message, + details: details.length > 0 ? details : undefined, + }); + + // Rafraîchir la liste + fetchAdherents(search); + setShowImportModal(false); + }; + + reader.readAsText(file, 'UTF-8'); + }; + return ( <> {/* Barre de recherche et actions */} @@ -147,17 +735,24 @@ export default function AdherentsTable() { Nouvel adhérent - - @@ -174,6 +769,14 @@ export default function AdherentsTable() { + @@ -185,6 +788,14 @@ export default function AdherentsTable() { {adherents.map((adherent) => ( +
+ 0 && selectedIds.size === adherents.length} + onChange={(e) => handleSelectAll(e.target.checked)} + className="w-4 h-4 text-lblue border-gray-300 rounded focus:ring-lblue" + /> + NOM CONTACT ADRESSE
+ handleSelectOne(adherent.id, e.target.checked)} + className="w-4 h-4 text-lblue border-gray-300 rounded focus:ring-lblue" + /> +
@@ -285,6 +896,242 @@ export default function AdherentsTable() { /> )} + {/* Modal import */} + {showImportModal && ( +
+
+ {/* Header */} +
+
+
+

+ Importer des adhérents +

+

+ 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, Adresse, Email et Téléphone sont obligatoires
  • +
  • Le format de date attendu est JJ/MM/AAAA
  • +
  • Les autres champs 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 && ( +
+

+ Erreurs : +

+
+
    + {resultModal.details.slice(0, 10).map((error, index) => ( +
  • + {error} +
  • + ))} + {resultModal.details.length > 10 && ( +
  • + ... et {resultModal.details.length - 10} autre(s) erreur(s) +
  • + )} +
+
+
+ )} +
+ + {/* Footer */} +
+
+ +
+
+
+
+ )} + + {/* Modal de confirmation de suppression */} + {confirmDeleteModal && ( + setConfirmDeleteModal(null)} + /> + )} + {/* Modal vue détaillée */} {viewingAdherent && (
@@ -418,6 +1265,17 @@ export default function AdherentsTable() {
)} + {viewingAdherent.forfait && ( +
+
+ Forfait +
+
+ {viewingAdherent.forfait} +
+
+ )} + {viewingAdherent.telephoneSecondaire && (
diff --git a/components/AlertModal.tsx b/components/AlertModal.tsx new file mode 100644 index 0000000..322f505 --- /dev/null +++ b/components/AlertModal.tsx @@ -0,0 +1,127 @@ +'use client'; + +interface AlertModalProps { + isOpen: boolean; + type: 'success' | 'error' | 'info' | 'warning'; + title: string; + message: string; + onClose: () => void; +} + +export default function AlertModal({ + isOpen, + type, + title, + message, + onClose, +}: AlertModalProps) { + if (!isOpen) return null; + + const getStyles = () => { + switch (type) { + case 'success': + return { + bg: 'bg-green-50', + titleColor: 'text-green-900', + messageColor: 'text-green-800', + icon: ( + + + + ), + button: 'bg-green-600 text-white hover:bg-green-700', + }; + case 'error': + return { + bg: 'bg-red-50', + titleColor: 'text-red-900', + messageColor: 'text-red-800', + icon: ( + + + + ), + button: 'bg-red-600 text-white hover:bg-red-700', + }; + case 'warning': + return { + bg: 'bg-orange-50', + titleColor: 'text-orange-900', + messageColor: 'text-orange-800', + icon: ( + + + + ), + button: 'bg-orange-600 text-white hover:bg-orange-700', + }; + default: + return { + bg: 'bg-blue-50', + titleColor: 'text-blue-900', + messageColor: 'text-blue-800', + icon: ( + + + + ), + button: 'bg-blue-600 text-white hover:bg-blue-700', + }; + } + }; + + const styles = getStyles(); + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+
+
+ {styles.icon} +

+ {title} +

+
+ +
+
+ + {/* Content */} +
+

+ {message} +

+
+ + {/* Footer */} +
+
+ +
+
+
+
+ ); +} diff --git a/components/ChatWindow.tsx b/components/ChatWindow.tsx index c64f55c..e2e4724 100644 --- a/components/ChatWindow.tsx +++ b/components/ChatWindow.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react'; import useSWR from 'swr'; +import AlertModal from './AlertModal'; interface User { id: string; @@ -61,6 +62,12 @@ export default function ChatWindow({ const [currentUser, setCurrentUser] = useState(null); const [typingUsers, setTypingUsers] = useState([]); const [readReceipts, setReadReceipts] = useState>({}); + const [alertModal, setAlertModal] = useState<{ + show: boolean; + type: 'success' | 'error' | 'info' | 'warning'; + title: string; + message: string; + } | null>(null); const messagesEndRef = useRef(null); const fileInputRef = useRef(null); const typingTimeoutRef = useRef(null); @@ -198,11 +205,21 @@ export default function ChatWindow({ }).catch(console.error); } else { const error = await response.json(); - alert(error.error || 'Erreur lors de l\'envoi du message'); + setAlertModal({ + show: true, + type: 'error', + title: 'Erreur', + message: error.error || 'Erreur lors de l\'envoi du message', + }); } } catch (error) { console.error('Erreur:', error); - alert('Erreur lors de l\'envoi du message'); + setAlertModal({ + show: true, + type: 'error', + title: 'Erreur', + message: 'Erreur lors de l\'envoi du message', + }); } finally { setIsSending(false); } @@ -584,6 +601,17 @@ export default function ChatWindow({
+ + {/* Modal d'alerte */} + {alertModal && ( + setAlertModal(null)} + /> + )}
); } diff --git a/components/ChauffeurForm.tsx b/components/ChauffeurForm.tsx index afb7532..ffa1df6 100644 --- a/components/ChauffeurForm.tsx +++ b/components/ChauffeurForm.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useEffect } from 'react'; +import AlertModal from './AlertModal'; interface Chauffeur { id: string; @@ -24,6 +25,12 @@ interface ChauffeurFormProps { export default function ChauffeurForm({ chauffeur, onClose }: ChauffeurFormProps) { const [loading, setLoading] = useState(false); + const [alertModal, setAlertModal] = useState<{ + show: boolean; + type: 'success' | 'error' | 'info' | 'warning'; + title: string; + message: string; + } | null>(null); const [formData, setFormData] = useState({ nom: '', prenom: '', @@ -83,11 +90,21 @@ export default function ChauffeurForm({ chauffeur, onClose }: ChauffeurFormProps onClose(); } else { const error = await response.json(); - alert(error.error || 'Une erreur est survenue'); + setAlertModal({ + show: true, + type: 'error', + title: 'Erreur', + message: error.error || 'Une erreur est survenue', + }); } } catch (error) { console.error('Erreur:', error); - alert('Une erreur est survenue'); + setAlertModal({ + show: true, + type: 'error', + title: 'Erreur', + message: 'Une erreur est survenue', + }); } finally { setLoading(false); } @@ -355,6 +372,17 @@ export default function ChauffeurForm({ chauffeur, onClose }: ChauffeurFormProps
+ + {/* Modal d'alerte */} + {alertModal && ( + setAlertModal(null)} + /> + )} ); } diff --git a/components/ChauffeursTable.tsx b/components/ChauffeursTable.tsx index 846fc18..5a0070d 100644 --- a/components/ChauffeursTable.tsx +++ b/components/ChauffeursTable.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import ChauffeurForm from './ChauffeurForm'; +import ConfirmModal from './ConfirmModal'; interface Chauffeur { id: string; @@ -25,6 +26,19 @@ export default function ChauffeursTable() { 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); const fetchChauffeurs = async (searchTerm: string = '') => { setLoading(true); @@ -57,23 +71,46 @@ export default function ChauffeursTable() { }, []); const handleDelete = async (id: string) => { - if (!confirm('Êtes-vous sûr de vouloir supprimer ce chauffeur ?')) { - return; - } + setConfirmDeleteModal({ + show: true, + id, + }); + }; + + const confirmDelete = async () => { + if (!confirmDeleteModal?.id) return; try { - const response = await fetch(`/api/chauffeurs/${id}`, { + 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 { - alert('Erreur lors de la suppression'); + setResultModal({ + show: true, + type: 'error', + title: 'Erreur', + message: 'Erreur lors de la suppression', + }); } } catch (error) { console.error('Erreur lors de la suppression:', error); - alert('Erreur lors de la suppression'); + setResultModal({ + show: true, + type: 'error', + title: 'Erreur', + message: 'Erreur lors de la suppression', + }); + } finally { + setConfirmDeleteModal(null); } }; @@ -127,6 +164,435 @@ export default function ChauffeursTable() { 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 */} @@ -164,17 +630,24 @@ export default function ChauffeursTable() { Nouveau chauffeur - - @@ -191,6 +664,14 @@ export default function ChauffeursTable() { + @@ -202,6 +683,14 @@ export default function ChauffeursTable() { {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
+ handleSelectOne(chauffeur.id, e.target.checked)} + className="w-4 h-4 text-lblue border-gray-300 rounded focus:ring-lblue" + /> +
@@ -294,6 +783,241 @@ export default function ChauffeursTable() { /> )} + {/* 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 */} {viewingChauffeur && (
diff --git a/components/ConfigurationContent.tsx b/components/ConfigurationContent.tsx index 9a42c8d..9d25cd1 100644 --- a/components/ConfigurationContent.tsx +++ b/components/ConfigurationContent.tsx @@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, memo } from 'react'; import { useRouter } from 'next/navigation'; import { useNotification } from './NotificationProvider'; import { AVAILABLE_PAGES } from '@/lib/pages'; +import ConfirmModal from './ConfirmModal'; interface AdherentOption { id: string; @@ -185,6 +186,18 @@ export default function ConfigurationContent() { facturation: '', forfait: '', }); + const [confirmDeleteOptionModal, setConfirmDeleteOptionModal] = useState<{ + show: boolean; + id: string | null; + } | null>(null); + const [confirmDeleteUserModal, setConfirmDeleteUserModal] = useState<{ + show: boolean; + userId: string | null; + } | null>(null); + const [confirmDeleteRoleModal, setConfirmDeleteRoleModal] = useState<{ + show: boolean; + roleId: string | null; + } | null>(null); const fetchOptions = useCallback(async () => { setLoading(true); @@ -286,13 +299,20 @@ export default function ConfigurationContent() { setEditingValue(''); }, []); - const handleDelete = useCallback(async (id: string) => { - if (!confirm('Êtes-vous sûr de vouloir supprimer cette option ?')) { - return; - } + const handleDelete = useCallback((id: string) => { + setConfirmDeleteOptionModal({ + show: true, + id, + }); + }, []); + + const confirmDeleteOption = useCallback(async () => { + if (!confirmDeleteOptionModal?.id) return; + + const idToDelete = confirmDeleteOptionModal.id; try { - const response = await fetch(`/api/settings/adherent-options/${id}`, { + const response = await fetch(`/api/settings/adherent-options/${idToDelete}`, { method: 'DELETE', }); @@ -306,8 +326,10 @@ export default function ConfigurationContent() { } catch (error) { console.error('Erreur:', error); showNotification('Erreur lors de la suppression', 'error'); + } finally { + setConfirmDeleteOptionModal(null); } - }, [showNotification, fetchOptions]); + }, [confirmDeleteOptionModal, fetchOptions, showNotification]); const handleNewValueChange = useCallback((type: 'situation' | 'prescripteur' | 'facturation' | 'forfait', value: string) => { setNewValue((prev) => ({ ...prev, [type]: value })); @@ -375,13 +397,18 @@ export default function ConfigurationContent() { } }; - const handleDeleteUser = async (userId: string) => { - if (!confirm('Êtes-vous sûr de vouloir supprimer cet utilisateur ? Cette action est irréversible.')) { - return; - } + const handleDeleteUser = (userId: string) => { + setConfirmDeleteUserModal({ + show: true, + userId, + }); + }; + + const confirmDeleteUser = async () => { + if (!confirmDeleteUserModal?.userId) return; try { - const response = await fetch(`/api/users/${userId}`, { + const response = await fetch(`/api/users/${confirmDeleteUserModal.userId}`, { method: 'DELETE', }); @@ -599,6 +626,20 @@ export default function ConfigurationContent() {
)} + + {/* Modal de confirmation de suppression utilisateur */} + {confirmDeleteUserModal && ( + setConfirmDeleteUserModal(null)} + /> + )} ); }; @@ -800,13 +841,18 @@ export default function ConfigurationContent() { } }; - const handleDeleteRole = async (roleId: string) => { - if (!confirm('Êtes-vous sûr de vouloir supprimer ce rôle ?')) { - return; - } + const handleDeleteRole = (roleId: string) => { + setConfirmDeleteRoleModal({ + show: true, + roleId, + }); + }; + + const confirmDeleteRole = async () => { + if (!confirmDeleteRoleModal?.roleId) return; try { - const response = await fetch(`/api/roles/${roleId}`, { + const response = await fetch(`/api/roles/${confirmDeleteRoleModal.roleId}`, { method: 'DELETE', }); @@ -820,6 +866,8 @@ export default function ConfigurationContent() { } catch (error) { console.error('Erreur:', error); showNotification('Erreur lors de la suppression', 'error'); + } finally { + setConfirmDeleteRoleModal(null); } }; @@ -1023,6 +1071,19 @@ export default function ConfigurationContent() {
)} + {/* Modal de confirmation de suppression rôle */} + {confirmDeleteRoleModal && ( + setConfirmDeleteRoleModal(null)} + /> + )} ); }; @@ -1216,6 +1277,46 @@ export default function ConfigurationContent() { + + {/* Modales de confirmation */} + {confirmDeleteOptionModal && ( + setConfirmDeleteOptionModal(null)} + /> + )} + + {confirmDeleteUserModal && ( + setConfirmDeleteUserModal(null)} + /> + )} + + {confirmDeleteRoleModal && ( + setConfirmDeleteRoleModal(null)} + /> + )} ); } diff --git a/components/ConfirmModal.tsx b/components/ConfirmModal.tsx index a01ab97..d2307ae 100644 --- a/components/ConfirmModal.tsx +++ b/components/ConfirmModal.tsx @@ -35,8 +35,14 @@ export default function ConfirmModal({ }; return ( -
-
+
+
e.stopPropagation()} + > {/* Header */}

{title}

diff --git a/components/GroupSettingsModal.tsx b/components/GroupSettingsModal.tsx index e65277f..af12b68 100644 --- a/components/GroupSettingsModal.tsx +++ b/components/GroupSettingsModal.tsx @@ -2,6 +2,8 @@ import { useState, useEffect } from 'react'; import useSWR from 'swr'; +import AlertModal from './AlertModal'; +import ConfirmModal from './ConfirmModal'; interface User { id: string; @@ -41,6 +43,16 @@ export default function GroupSettingsModal({ const [selectedUsers, setSelectedUsers] = useState([]); const [isUpdating, setIsUpdating] = useState(false); const [isAdding, setIsAdding] = useState(false); + const [alertModal, setAlertModal] = useState<{ + show: boolean; + type: 'success' | 'error' | 'info' | 'warning'; + title: string; + message: string; + } | null>(null); + const [confirmModal, setConfirmModal] = useState<{ + show: boolean; + userId: string | null; + } | null>(null); const { data: users, error } = useSWR( search ? `/api/users?search=${encodeURIComponent(search)}` : null, @@ -51,7 +63,12 @@ export default function GroupSettingsModal({ const handleUpdateName = async () => { if (!groupName.trim()) { - alert('Le nom du groupe ne peut pas être vide'); + setAlertModal({ + show: true, + type: 'warning', + title: 'Nom requis', + message: 'Le nom du groupe ne peut pas être vide', + }); return; } @@ -70,11 +87,21 @@ export default function GroupSettingsModal({ onClose(); } else { const error = await response.json(); - alert(error.error || 'Erreur lors de la mise à jour'); + setAlertModal({ + show: true, + type: 'error', + title: 'Erreur', + message: error.error || 'Erreur lors de la mise à jour', + }); } } catch (error) { console.error('Erreur:', error); - alert('Erreur lors de la mise à jour'); + setAlertModal({ + show: true, + type: 'error', + title: 'Erreur', + message: 'Erreur lors de la mise à jour', + }); } finally { setIsUpdating(false); } @@ -82,7 +109,12 @@ export default function GroupSettingsModal({ const handleAddParticipants = async () => { if (selectedUsers.length === 0) { - alert('Veuillez sélectionner au moins un utilisateur'); + setAlertModal({ + show: true, + type: 'warning', + title: 'Sélection requise', + message: 'Veuillez sélectionner au moins un utilisateur', + }); return; } @@ -102,24 +134,39 @@ export default function GroupSettingsModal({ onUpdate(); } else { const error = await response.json(); - alert(error.error || 'Erreur lors de l\'ajout des participants'); + setAlertModal({ + show: true, + type: 'error', + title: 'Erreur', + message: error.error || 'Erreur lors de l\'ajout des participants', + }); } } catch (error) { console.error('Erreur:', error); - alert('Erreur lors de l\'ajout des participants'); + setAlertModal({ + show: true, + type: 'error', + title: 'Erreur', + message: 'Erreur lors de l\'ajout des participants', + }); } finally { setIsAdding(false); } }; const handleRemoveParticipant = async (userId: string) => { - if (!confirm('Êtes-vous sûr de vouloir retirer cet utilisateur du groupe ?')) { - return; - } + setConfirmModal({ + show: true, + userId, + }); + }; + + const confirmRemoveParticipant = async () => { + if (!confirmModal?.userId) return; try { const response = await fetch( - `/api/conversations/${conversation.id}/participants?userId=${userId}`, + `/api/conversations/${conversation.id}/participants?userId=${confirmModal.userId}`, { method: 'DELETE', } @@ -127,13 +174,26 @@ export default function GroupSettingsModal({ if (response.ok) { onUpdate(); + setConfirmModal(null); } else { const error = await response.json(); - alert(error.error || 'Erreur lors du retrait du participant'); + setAlertModal({ + show: true, + type: 'error', + title: 'Erreur', + message: error.error || 'Erreur lors du retrait du participant', + }); + setConfirmModal(null); } } catch (error) { console.error('Erreur:', error); - alert('Erreur lors du retrait du participant'); + setAlertModal({ + show: true, + type: 'error', + title: 'Erreur', + message: 'Erreur lors du retrait du participant', + }); + setConfirmModal(null); } }; @@ -295,6 +355,31 @@ export default function GroupSettingsModal({
+ + {/* Modal d'alerte */} + {alertModal && ( + setAlertModal(null)} + /> + )} + + {/* Modal de confirmation */} + {confirmModal && ( + setConfirmModal(null)} + /> + )}
); } diff --git a/components/NewConversationModal.tsx b/components/NewConversationModal.tsx index a83c45f..17ad74f 100644 --- a/components/NewConversationModal.tsx +++ b/components/NewConversationModal.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import useSWR from 'swr'; +import AlertModal from './AlertModal'; interface User { id: string; @@ -25,6 +26,12 @@ export default function NewConversationModal({ const [conversationType, setConversationType] = useState<'direct' | 'group'>('direct'); const [groupName, setGroupName] = useState(''); const [isCreating, setIsCreating] = useState(false); + const [alertModal, setAlertModal] = useState<{ + show: boolean; + type: 'success' | 'error' | 'info' | 'warning'; + title: string; + message: string; + } | null>(null); // Récupérer l'utilisateur actuel const { data: currentUser } = useSWR('/api/auth/me', fetcher); @@ -47,12 +54,22 @@ export default function NewConversationModal({ const handleCreate = async () => { if (selectedUsers.length === 0) { - alert('Veuillez sélectionner au moins un utilisateur'); + setAlertModal({ + show: true, + type: 'warning', + title: 'Sélection requise', + message: 'Veuillez sélectionner au moins un utilisateur', + }); return; } if (conversationType === 'group' && !groupName.trim()) { - alert('Veuillez entrer un nom pour le groupe'); + setAlertModal({ + show: true, + type: 'warning', + title: 'Nom requis', + message: 'Veuillez entrer un nom pour le groupe', + }); return; } @@ -75,11 +92,21 @@ export default function NewConversationModal({ onConversationCreated(conversation.id); } else { const error = await response.json(); - alert(error.error || 'Erreur lors de la création de la conversation'); + setAlertModal({ + show: true, + type: 'error', + title: 'Erreur', + message: error.error || 'Erreur lors de la création de la conversation', + }); } } catch (error) { console.error('Erreur:', error); - alert('Erreur lors de la création de la conversation'); + setAlertModal({ + show: true, + type: 'error', + title: 'Erreur', + message: 'Erreur lors de la création de la conversation', + }); } finally { setIsCreating(false); } @@ -234,6 +261,17 @@ export default function NewConversationModal({
+ + {/* Modal d'alerte */} + {alertModal && ( + setAlertModal(null)} + /> + )} ); } diff --git a/components/ParametresContent.tsx b/components/ParametresContent.tsx index 4c69081..aa79f2f 100644 --- a/components/ParametresContent.tsx +++ b/components/ParametresContent.tsx @@ -4,6 +4,7 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { useNotification } from './NotificationProvider'; import useSWR from 'swr'; +import ConfirmModal from './ConfirmModal'; interface User { id: string; @@ -49,6 +50,10 @@ export default function ParametresContent() { prescripteur: '', facturation: '', }); + const [confirmDeleteModal, setConfirmDeleteModal] = useState<{ + show: boolean; + id: string | null; + } | null>(null); useEffect(() => { fetchOptions(); @@ -148,13 +153,18 @@ export default function ParametresContent() { setEditingValue(''); }; - const handleDelete = async (id: string) => { - if (!confirm('Êtes-vous sûr de vouloir supprimer cette option ?')) { - return; - } + const handleDelete = (id: string) => { + setConfirmDeleteModal({ + show: true, + id, + }); + }; + + const confirmDelete = async () => { + if (!confirmDeleteModal?.id) return; try { - const response = await fetch(`/api/settings/adherent-options/${id}`, { + const response = await fetch(`/api/settings/adherent-options/${confirmDeleteModal.id}`, { method: 'DELETE', }); @@ -168,6 +178,8 @@ export default function ParametresContent() { } catch (error) { console.error('Erreur:', error); showNotification('Erreur lors de la suppression', 'error'); + } finally { + setConfirmDeleteModal(null); } }; @@ -366,6 +378,20 @@ export default function ParametresContent() { )} + + {/* Modal de confirmation de suppression */} + {confirmDeleteModal && ( + setConfirmDeleteModal(null)} + /> + )} ); } diff --git a/components/UniversProForm.tsx b/components/UniversProForm.tsx index c2eeb20..9ef6d43 100644 --- a/components/UniversProForm.tsx +++ b/components/UniversProForm.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState, useEffect } from 'react'; +import AlertModal from './AlertModal'; interface UniversPro { id: string; @@ -19,6 +20,12 @@ interface UniversProFormProps { export default function UniversProForm({ contact, onClose }: UniversProFormProps) { const [loading, setLoading] = useState(false); + const [alertModal, setAlertModal] = useState<{ + show: boolean; + type: 'success' | 'error' | 'info' | 'warning'; + title: string; + message: string; + } | null>(null); const [formData, setFormData] = useState({ nom: '', prenom: '', @@ -61,11 +68,21 @@ export default function UniversProForm({ contact, onClose }: UniversProFormProps onClose(); } else { const error = await response.json(); - alert(error.error || 'Une erreur est survenue'); + setAlertModal({ + show: true, + type: 'error', + title: 'Erreur', + message: error.error || 'Une erreur est survenue', + }); } } catch (error) { console.error('Erreur:', error); - alert('Une erreur est survenue'); + setAlertModal({ + show: true, + type: 'error', + title: 'Erreur', + message: 'Une erreur est survenue', + }); } finally { setLoading(false); } @@ -241,6 +258,17 @@ export default function UniversProForm({ contact, onClose }: UniversProFormProps + + {/* Modal d'alerte */} + {alertModal && ( + setAlertModal(null)} + /> + )} ); } diff --git a/components/UniversProTable.tsx b/components/UniversProTable.tsx index a7a38fa..d2818c8 100644 --- a/components/UniversProTable.tsx +++ b/components/UniversProTable.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import UniversProForm from './UniversProForm'; +import ConfirmModal from './ConfirmModal'; interface UniversPro { id: string; @@ -20,6 +21,19 @@ export default function UniversProTable() { const [showForm, setShowForm] = useState(false); const [editingContact, setEditingContact] = useState(null); const [viewingContact, setViewingContact] = 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); const fetchContacts = async (searchTerm: string = '') => { setLoading(true); @@ -52,23 +66,46 @@ export default function UniversProTable() { }, []); const handleDelete = async (id: string) => { - if (!confirm('Êtes-vous sûr de vouloir supprimer ce contact ?')) { - return; - } + setConfirmDeleteModal({ + show: true, + id, + }); + }; + + const confirmDelete = async () => { + if (!confirmDeleteModal?.id) return; try { - const response = await fetch(`/api/univers-pro/${id}`, { + const response = await fetch(`/api/univers-pro/${confirmDeleteModal.id}`, { method: 'DELETE', }); if (response.ok) { fetchContacts(search); + setResultModal({ + show: true, + type: 'success', + title: 'Suppression réussie', + message: 'Le contact a été supprimé avec succès', + }); } else { - alert('Erreur lors de la suppression'); + setResultModal({ + show: true, + type: 'error', + title: 'Erreur', + message: 'Erreur lors de la suppression', + }); } } catch (error) { console.error('Erreur lors de la suppression:', error); - alert('Erreur lors de la suppression'); + setResultModal({ + show: true, + type: 'error', + title: 'Erreur', + message: 'Erreur lors de la suppression', + }); + } finally { + setConfirmDeleteModal(null); } }; @@ -99,6 +136,331 @@ export default function UniversProTable() { return `${prenom.charAt(0)}${nom.charAt(0)}`.toUpperCase(); }; + const handleSelectAll = (checked: boolean) => { + if (checked) { + setSelectedIds(new Set(contacts.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 contact à exporter', + }); + return; + } + + const selectedContacts = contacts.filter(c => selectedIds.has(c.id)); + + // Créer les en-têtes CSV avec tous les champs disponibles + const headers = [ + 'Nom', + 'Prénom', + 'Téléphone', + 'Email', + 'Adresse', + 'Nom entreprise' + ]; + + // Créer les lignes CSV + const rows = selectedContacts.map(contact => { + return [ + contact.nom || '', + contact.prenom || '', + contact.telephone || '', + contact.email || '', + contact.adresse || '', + contact.nomEntreprise || '' + ]; + }); + + // 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', `contacts_pro_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', + 'Téléphone', + 'Email', + 'Adresse', + 'Nom entreprise' + ]; + + // Ligne d'exemple + const exampleRow = [ + 'Dupont', + 'Jean', + '0123456789', + 'jean.dupont@example.com', + '123 Rue de la Paix, 75001 Paris', + 'BNP Paribas' + ]; + + 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_contacts_pro.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'; + 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 nomEntrepriseIndex = findColumnIndex(['entreprise', 'company', 'societe', 'société']); + const nomEntrepriseHeader = nomEntrepriseIndex >= 0 ? headers[nomEntrepriseIndex] : 'Nom entreprise'; + + // 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 (telephoneIndex === -1) missingRequiredColumns.push('Téléphone'); + if (emailIndex === -1) missingRequiredColumns.push('Email'); + if (adresseIndex === -1) missingRequiredColumns.push('Adresse'); + if (nomEntrepriseIndex === -1) missingRequiredColumns.push('Nom entreprise'); + + 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 telephone = telephoneIndex >= 0 ? (row[telephoneIndex] || '').trim() : ''; + const email = emailIndex >= 0 ? (row[emailIndex] || '').trim() : ''; + const adresse = adresseIndex >= 0 ? (row[adresseIndex] || '').trim() : ''; + const nomEntreprise = nomEntrepriseIndex >= 0 ? (row[nomEntrepriseIndex] || '').trim() : ''; + + // 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 (!telephone) missingFields.push(telephoneHeader); + if (!email) missingFields.push(emailHeader); + if (!adresse) missingFields.push(adresseHeader); + if (!nomEntreprise) missingFields.push(nomEntrepriseHeader); + + if (missingFields.length > 0) { + errorCount++; + const fieldsList = missingFields.join(', '); + errors.push(`Ligne ${i + 2}: Colonnes manquantes ou vides : ${fieldsList}`); + continue; + } + + try { + const response = await fetch('/api/univers-pro', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + nom, + prenom, + telephone, + email, + adresse, + nomEntreprise, + }), + }); + + 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} contact(s) importé(s) avec succès` + : `${successCount} contact(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 + fetchContacts(search); + setShowImportModal(false); + }; + + reader.readAsText(file, 'UTF-8'); + }; + return ( <> {/* Barre de recherche et actions */} @@ -136,17 +498,24 @@ export default function UniversProTable() { Nouveau contact - - @@ -163,6 +532,14 @@ export default function UniversProTable() { + @@ -173,6 +550,14 @@ export default function UniversProTable() { {contacts.map((contact) => ( +
+ 0 && selectedIds.size === contacts.length} + onChange={(e) => handleSelectAll(e.target.checked)} + className="w-4 h-4 text-lblue border-gray-300 rounded focus:ring-lblue" + /> + NOM CONTACT ADRESSE
+ handleSelectOne(contact.id, e.target.checked)} + className="w-4 h-4 text-lblue border-gray-300 rounded focus:ring-lblue" + /> +
@@ -243,6 +628,240 @@ export default function UniversProTable() { /> )} + {/* Modal import */} + {showImportModal && ( +
+
+ {/* Header */} +
+
+
+

+ Importer des contacts +

+

+ 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 : +

+
    +
  • Tous les champs sont obligatoires : Nom, Prénom, Téléphone, Email, Adresse, Nom entreprise
  • +
  • Assurez-vous que le fichier utilise l'encodage UTF-8
  • +
  • Le séparateur peut être une virgule (,) ou un point-virgule (;)
  • +
+
+
+
+ + {/* 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 */} {viewingContact && (
diff --git a/prisma/dev.db b/prisma/dev.db index dfccbb3f280e55a255b0e77b1172cb8ec4222812..52fe9f909f15d297d7c22dea2618e9d11e0ead5d 100644 GIT binary patch delta 2954 zcmcguUu;uV7{6z|ceiVM?^(qbMmO4SHpH;8ZfmTuD#uE|F7*` zOEKw&Vj?_%E?Ni>eIUXfi~-TT@Sq{X$O}ePq7ND&W+oa&A`dR%!SCGLf7{BFiM!d? zd(U^i-}%n(eBUo;a?&_CX*|2OiM{`B`!LvRM?Ws=T@-e4ZCu-D=2jzDJi+M9g3ZS4 z+GOwb4g~B0hS?B|$HeitM+~~%Zm%yWdZWo>saPgD`jV0=Boo=4QYaJ>6V%Qpuj;A3 z{sG56MRJ5BM@)3=5#@ZhW6dsB%Hd=>EXrAmLbT9kX~@I?FySFgcqin5k`NQXgi6GJWud6i zP)+y$W1(AR;f3xhwHgak2a#E5voVMJoX#G5n{$w1EG3*oZPVhJ*E@HJiI5x=Q@3We zrNu;0aty@^W0D+~(nF!KTQg}XF@5LEM(d*NiTb+zZUY6R;)fJ|h_B!)P;lq*mJXeO zo>^cz2v}F~t{}asT_?1t$z1>FX)nDv`egt0Z$Dqxx)|OwfiC=Z@~1!11$fU*&pkS_ zW-%5I!{D0>H()Rp9|dGl5EyFu!oq>+GYdz}7yFZzf$|LX{rY3NRcB>w@3ZJdnw?~A zknntSXVA(Dg0Pxlg7K)5i;U*Rpcn=wl$`7d_m6q=Y(*@&{{;KG~<)h=l~Y zDuMo32m<>^U@qsCBzL|*0^3WQIAmRul`>g}l7Kc+@+ADuux4lfV$mAC&dSA0Y%Ao& zo#x_@SAg5B5&4XitTk_gbuF_q9q8aN<|T?Yc2jt_v73`k7Qk&cny11|yuciuT2I;Z zKo~=nlmz;oUJSqebk-6>n-IMyQHI&q;>e6>VD*n#Ie{)Xbym~85_=Z8^rnAI_waj| zr1_z#)Nu(p7($A*bWMA`Gi)KRowLFvG}q*Ytxd)-$Ptd04I{4ER%TpBIfm!?o{|!IP zQ~Vk3w&`c`KuR}F#bKTmY#a3CU{s+CWXOq<_6`Nz0e{w;C`-b!-cUQnsaaU&LDxVH z!!H3wzbclo@mOI(9%-Oe+E7u|F9NgYk!Atx1DCf3<|~j&Az=A|!STX~d!&&V){m(; zH#NJ>$O`t}3YwpMt*U(GrI=I%a32BBC|PgJ#Qse@RqDFoyG zkY@1|Mf(kKZmX+k1z!TtL;_X_hN6T1eyMzJeQppRh?7}|W;Zvpg3D5lEasxVbefQ$ zhPqRdI~)zyv$=Ka(0;OPZU?b%?Gk9<&{Bt_=QGLd-$ zXf2D)6sRp}s3`7*rXr1Wo;*|TWa10=h|HJ5@w%YMlD0!rM-7iTbHgnv(X8Y z1M4Ww|B5=+viKZ@FXPAfM|=*5zJVX$Q|kXIpjf~~qt1rPl?X~i_12X*2jBMk<1}$P zJE~TXu`bkrbzP_~RNty|q59C~St9CBA5*Z5t{ZStf-%gh%SLtNLpqQzoTd~ZChR0g(?gN3+=Kj$&2)R^BUUjtmOfH jyuEzYp<8L2rGh}NHRP6Js!Vm#uAU`e%adR(H+}gp%8SD? delta 698 zcmY+CT}V@57{||ZJ~~r(&a>4fEKj?!C_jNb$+swq3d=G_>G^U{zf}&~^QHI6tv?kcFKn_l#=&pg z;b9MP`;0d?cxbXP4q?*ZZPmT2f)Seni#aX9$sQy~*6O6NPgDfe>Lfj!)pTBhE?`S* zZ@-(t7}$XwR%pRgE`Al?iKaL%UK2y2EP6yn_$Vw8t%?^3cs=*H(R?ulUE2lXhrwP7XK@&NNyQN~ zdW&$F?V@?v7%4)>xuGJtjOC$XH}JAovT$4^Hki}`F(vHt`BJI0stl$E;wk^ZD@jdB zMKm>;Rx;YJ_lQ<{Gpn^&Ulp66h|PJNAY$WKwPOT}(72nC7#V8pMC3AIrz>ow(;o?p)xe=V+5uZQffLAPOFJHjf}VMUpqDFISS`+ s9p4b?8I)~ovlF5TEgDwSm7l^m*jCqHM(tzKO{`b?5B2+G59#MW0SZv+wg3PC