'use client'; import { useState, useEffect } from 'react'; import { useBodyScrollLock } from '@/lib/body-scroll-lock'; import UniversProForm from './UniversProForm'; import ConfirmModal from './ConfirmModal'; interface UniversPro { id: string; nom: string; prenom: string; telephone: string; email: string; adresse: string; nomEntreprise: string; } export default function UniversProTable() { const [contacts, setContacts] = useState([]); const [search, setSearch] = useState(''); const [loading, setLoading] = useState(true); 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); useBodyScrollLock(showImportModal || !!(resultModal?.show) || !!viewingContact); const fetchContacts = async (searchTerm: string = '') => { setLoading(true); try { const url = searchTerm ? `/api/univers-pro?search=${encodeURIComponent(searchTerm)}` : '/api/univers-pro'; const response = await fetch(url); if (response.ok) { const data = await response.json(); setContacts(data); } } catch (error) { console.error('Erreur lors du chargement des contacts:', error); } finally { setLoading(false); } }; useEffect(() => { const timeoutId = setTimeout(() => { fetchContacts(search); }, 300); return () => clearTimeout(timeoutId); }, [search]); useEffect(() => { fetchContacts(); }, []); const handleDelete = async (id: string) => { setConfirmDeleteModal({ show: true, id, }); }; const confirmDelete = async () => { if (!confirmDeleteModal?.id) return; try { 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 { 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 = (contact: UniversPro) => { setEditingContact(contact); setShowForm(true); }; const handleView = async (id: string) => { try { const response = await fetch(`/api/univers-pro/${id}`); if (response.ok) { const data = await response.json(); setViewingContact(data); } } catch (error) { console.error('Erreur lors de la récupération:', error); } }; const handleFormClose = () => { setShowForm(false); setEditingContact(null); fetchContacts(search); }; const getInitials = (nom: string, prenom: string) => { 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 */}
{/* 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...
) : contacts.length === 0 ? (
Aucun contact trouvé
) : ( <> {/* Vue desktop - Tableau */}
{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 ENTREPRISE ACTIONS
handleSelectOne(contact.id, e.target.checked)} className="w-4 h-4 text-lblue border-gray-300 rounded focus:ring-lblue" />
{getInitials(contact.nom, contact.prenom)}
{contact.prenom} {contact.nom}
{contact.telephone}
{contact.email}
{contact.adresse}
{contact.nomEntreprise}
{/* Vue mobile - Cartes */}
{contacts.map((contact) => (
{/* Checkbox */} handleSelectOne(contact.id, e.target.checked)} className="w-4 h-4 text-lblue border-gray-300 rounded focus:ring-lblue mt-1" /> {/* Avatar */}
{getInitials(contact.nom, contact.prenom)}
{/* Contenu principal */}
{/* Nom */}
{contact.prenom} {contact.nom}
{/* Contact */} {/* Adresse */}
Adresse
{contact.adresse}
{/* Entreprise */}
Entreprise
{contact.nomEntreprise}
{/* Actions */}
))}
)}
{/* Modal formulaire */} {showForm && ( )} {/* 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 - Design épuré */} {viewingContact && (
{/* Header épuré */}
{getInitials(viewingContact.nom, viewingContact.prenom)}

{viewingContact.prenom} {viewingContact.nom}

Informations détaillées du contact professionnel

{/* Contenu scrollable */}
{/* Actions rapides */}
{/* Carte Informations de contact */}

Informations de contact

Adresse

{viewingContact.adresse}

{/* Carte Informations entreprise */}

Informations entreprise

Nom de l'entreprise

{viewingContact.nomEntreprise}

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