'use client'; import { useState, useEffect } from 'react'; import AdherentForm from './AdherentForm'; import ConfirmModal from './ConfirmModal'; interface Adherent { id: string; nom: string; prenom: string; dateNaissance: string; adresse: string; email: string; telephone: string; situation?: string | null; prescripteur?: string | null; facturation?: string | null; forfait?: string | null; commentaire?: string | null; telephoneSecondaire?: string | null; instructions?: string | null; } export default function AdherentsTable() { const [adherents, setAdherents] = useState([]); const [search, setSearch] = useState(''); const [loading, setLoading] = useState(true); 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); try { const url = searchTerm ? `/api/adherents?search=${encodeURIComponent(searchTerm)}` : '/api/adherents'; const response = await fetch(url); if (response.ok) { const data = await response.json(); setAdherents(data); } } catch (error) { console.error('Erreur lors du chargement des adhérents:', error); } finally { setLoading(false); } }; useEffect(() => { const timeoutId = setTimeout(() => { fetchAdherents(search); }, 300); return () => clearTimeout(timeoutId); }, [search]); useEffect(() => { fetchAdherents(); }, []); const handleDelete = async (id: string) => { setConfirmDeleteModal({ show: true, id, }); }; const confirmDelete = async () => { if (!confirmDeleteModal?.id) return; try { 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 { 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 = (adherent: Adherent) => { setEditingAdherent(adherent); setShowForm(true); }; const handleView = async (id: string) => { try { const response = await fetch(`/api/adherents/${id}`); if (response.ok) { const data = await response.json(); setViewingAdherent(data); } } catch (error) { console.error('Erreur lors de la récupération:', error); } }; const handleFormClose = () => { setShowForm(false); setEditingAdherent(null); fetchAdherents(search); }; const getInitials = (nom: string, prenom: string) => { return `${prenom.charAt(0)}${nom.charAt(0)}`.toUpperCase(); }; const formatDate = (dateString: string) => { const date = new Date(dateString); 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 */}
{/* 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...
) : adherents.length === 0 ? (
Aucun adhérent trouvé
) : ( <> {/* Vue desktop - Tableau */}
{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 PRESCRIPTEUR SITUATION ACTIONS
handleSelectOne(adherent.id, e.target.checked)} className="w-4 h-4 text-lblue border-gray-300 rounded focus:ring-lblue" />
{getInitials(adherent.nom, adherent.prenom)}
{adherent.prenom} {adherent.nom}
Né le {formatDate(adherent.dateNaissance)}
{adherent.telephone} (Principal)
{adherent.telephoneSecondaire && (
{adherent.telephoneSecondaire} (Secondaire)
)}
{adherent.email}
{adherent.adresse}
{adherent.prescripteur || '-'}
{adherent.situation || '-'}
{/* Vue mobile - Cartes */}
{adherents.map((adherent) => (
{/* Checkbox */} handleSelectOne(adherent.id, e.target.checked)} className="w-4 h-4 text-lblue border-gray-300 rounded focus:ring-lblue mt-1" /> {/* Avatar */}
{getInitials(adherent.nom, adherent.prenom)}
{/* Contenu principal */}
{/* Nom et date de naissance */}
{adherent.prenom} {adherent.nom}
Né le {formatDate(adherent.dateNaissance)}
{/* Contact */} {/* Adresse */}
Adresse
{adherent.adresse}
{/* Prescripteur et Situation */}
{adherent.prescripteur && (
Prescripteur
{adherent.prescripteur}
)} {adherent.situation && (
Situation
{adherent.situation}
)}
{/* Actions */}
))}
)}
{/* Modal formulaire */} {showForm && ( )} {/* 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 - Design épuré */} {viewingAdherent && (
{/* Header épuré */}
{getInitials(viewingAdherent.nom, viewingAdherent.prenom)}

{viewingAdherent.prenom} {viewingAdherent.nom}

Informations détaillées de l'adhérent

{/* Contenu scrollable */}
{/* Actions rapides */}
Appeler {viewingAdherent.telephoneSecondaire && ( Téléphone secondaire )} Envoyer un email
{/* Carte Informations principales */}

Informations principales

Date de naissance

{formatDate(viewingAdherent.dateNaissance)}

{viewingAdherent.telephoneSecondaire && ( )}

Adresse

{viewingAdherent.adresse}

{/* Carte Informations complémentaires */}

Informations complémentaires

{viewingAdherent.situation && (

Situation

{viewingAdherent.situation}

)} {viewingAdherent.prescripteur && (

Prescripteur

{viewingAdherent.prescripteur}

)} {viewingAdherent.facturation && (

Facturation

{viewingAdherent.facturation}

)} {viewingAdherent.forfait && (

Forfait

{viewingAdherent.forfait}

)} {viewingAdherent.commentaire && (

Commentaire

{viewingAdherent.commentaire}

)} {viewingAdherent.instructions && (

Instructions

{viewingAdherent.instructions}

)} {!viewingAdherent.situation && !viewingAdherent.prescripteur && !viewingAdherent.facturation && !viewingAdherent.forfait && !viewingAdherent.commentaire && !viewingAdherent.instructions && (
Aucune information complémentaire
)}
{/* Footer avec actions */}
)} ); }