Files
MAD-Platform/components/AdherentsTable.tsx

1424 lines
67 KiB
TypeScript
Raw Normal View History

2026-01-20 19:02:49 +01:00
'use client';
import { useState, useEffect } from 'react';
import AdherentForm from './AdherentForm';
2026-02-06 11:34:16 +01:00
import ConfirmModal from './ConfirmModal';
2026-01-20 19:02:49 +01:00
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;
2026-02-06 11:34:16 +01:00
forfait?: string | null;
2026-01-20 19:02:49 +01:00
commentaire?: string | null;
telephoneSecondaire?: string | null;
instructions?: string | null;
}
export default function AdherentsTable() {
const [adherents, setAdherents] = useState<Adherent[]>([]);
const [search, setSearch] = useState('');
const [loading, setLoading] = useState(true);
const [showForm, setShowForm] = useState(false);
const [editingAdherent, setEditingAdherent] = useState<Adherent | null>(null);
const [viewingAdherent, setViewingAdherent] = useState<Adherent | null>(null);
2026-02-06 11:34:16 +01:00
const [selectedIds, setSelectedIds] = useState<Set<string>>(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);
2026-01-20 19:02:49 +01:00
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) => {
2026-02-06 11:34:16 +01:00
setConfirmDeleteModal({
show: true,
id,
});
};
const confirmDelete = async () => {
if (!confirmDeleteModal?.id) return;
2026-01-20 19:02:49 +01:00
try {
2026-02-06 11:34:16 +01:00
const response = await fetch(`/api/adherents/${confirmDeleteModal.id}`, {
2026-01-20 19:02:49 +01:00
method: 'DELETE',
});
if (response.ok) {
fetchAdherents(search);
2026-02-06 11:34:16 +01:00
setResultModal({
show: true,
type: 'success',
title: 'Suppression réussie',
message: 'L\'adhérent a été supprimé avec succès',
});
2026-01-20 19:02:49 +01:00
} else {
2026-02-06 11:34:16 +01:00
setResultModal({
show: true,
type: 'error',
title: 'Erreur',
message: 'Erreur lors de la suppression',
});
2026-01-20 19:02:49 +01:00
}
} catch (error) {
console.error('Erreur lors de la suppression:', error);
2026-02-06 11:34:16 +01:00
setResultModal({
show: true,
type: 'error',
title: 'Erreur',
message: 'Erreur lors de la suppression',
});
} finally {
setConfirmDeleteModal(null);
2026-01-20 19:02:49 +01:00
}
};
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' });
};
2026-02-06 11:34:16 +01:00
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<HTMLInputElement>) => {
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<boolean> => {
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');
};
2026-01-20 19:02:49 +01:00
return (
<>
{/* Barre de recherche et actions */}
<div className="bg-white rounded-lg shadow-sm p-4 mb-6">
<div className="flex flex-col md:flex-row gap-4 items-center">
{/* Barre de recherche */}
<div className="flex-1 w-full md:w-auto">
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<input
type="text"
placeholder="Rechercher un adhérent..."
value={search}
onChange={(e) => 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"
/>
</div>
</div>
{/* Boutons d'action */}
<div className="flex gap-3">
<button
onClick={() => {
setEditingAdherent(null);
setShowForm(true);
}}
className="flex items-center gap-2 px-4 py-2 bg-lgreen text-white rounded-lg hover:bg-dgreen transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Nouvel adhérent
</button>
2026-02-06 11:34:16 +01:00
<button
onClick={() => setShowImportModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-lblue text-white rounded-lg hover:bg-dblue transition-colors"
>
2026-01-20 19:02:49 +01:00
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Importer
</button>
2026-02-06 11:34:16 +01:00
<button
onClick={handleExport}
disabled={selectedIds.size === 0}
className="flex items-center gap-2 px-4 py-2 bg-lorange text-white rounded-lg hover:bg-dorange transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
2026-01-20 19:02:49 +01:00
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4-4m0 0l-4-4m4 4V4" />
</svg>
2026-02-06 11:34:16 +01:00
Exporter {selectedIds.size > 0 && `(${selectedIds.size})`}
2026-01-20 19:02:49 +01:00
</button>
</div>
</div>
</div>
{/* Tableau */}
<div className="bg-white rounded-lg shadow-sm overflow-hidden">
{loading ? (
<div className="p-8 text-center text-gray-500">Chargement...</div>
) : adherents.length === 0 ? (
<div className="p-8 text-center text-gray-500">Aucun adhérent trouvé</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
2026-02-06 11:34:16 +01:00
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<input
type="checkbox"
checked={adherents.length > 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"
/>
</th>
2026-01-20 19:02:49 +01:00
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">NOM</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">CONTACT</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ADRESSE</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">PRESCRIPTEUR</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">SITUATION</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ACTIONS</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{adherents.map((adherent) => (
<tr key={adherent.id} className="hover:bg-gray-50">
2026-02-06 11:34:16 +01:00
<td className="px-6 py-4 whitespace-nowrap">
<input
type="checkbox"
checked={selectedIds.has(adherent.id)}
onChange={(e) => handleSelectOne(adherent.id, e.target.checked)}
className="w-4 h-4 text-lblue border-gray-300 rounded focus:ring-lblue"
/>
</td>
2026-01-20 19:02:49 +01:00
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-lgreen flex items-center justify-center text-white font-semibold">
{getInitials(adherent.nom, adherent.prenom)}
</div>
<div>
<div className="text-sm font-semibold text-gray-900">
{adherent.prenom} {adherent.nom}
</div>
<div className="text-sm text-gray-500">
le {formatDate(adherent.dateNaissance)}
</div>
</div>
</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2 mb-1">
<svg className="h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
<span className="text-sm text-gray-900 font-medium">{adherent.telephone}</span>
<span className="text-xs text-gray-500">(Principal)</span>
</div>
{adherent.telephoneSecondaire && (
<div className="flex items-center gap-2 mb-1">
<svg className="h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
<span className="text-sm text-gray-600">{adherent.telephoneSecondaire}</span>
<span className="text-xs text-gray-500">(Secondaire)</span>
</div>
)}
<div className="flex items-center gap-2 mt-1">
<svg className="h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<span className="text-sm text-gray-500">{adherent.email}</span>
</div>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-900">{adherent.adresse}</div>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-900">
{adherent.prescripteur || '-'}
</div>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-900">
{adherent.situation || '-'}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex items-center gap-3">
<button
onClick={() => handleView(adherent.id)}
className="text-lblue hover:text-dblue"
title="Voir"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</button>
<button
onClick={() => handleEdit(adherent)}
className="text-lblue hover:text-dblue"
title="Modifier"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button
onClick={() => handleDelete(adherent.id)}
className="text-red-500 hover:text-red-700"
title="Supprimer"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* Modal formulaire */}
{showForm && (
<AdherentForm
adherent={editingAdherent}
onClose={handleFormClose}
/>
)}
2026-02-06 11:34:16 +01:00
{/* Modal import */}
{showImportModal && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4 animate-fadeIn">
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full animate-slideUp border border-gray-200">
{/* Header */}
<div className="border-b border-gray-200 px-6 py-5 bg-white">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-semibold text-gray-900">
Importer des adhérents
</h2>
<p className="text-sm text-gray-500 mt-1">
Téléchargez le modèle d'exemple, remplissez-le et importez-le ici
</p>
</div>
<button
onClick={() => setShowImportModal(false)}
className="text-gray-400 hover:text-gray-600 transition-colors p-1.5 hover:bg-gray-100 rounded"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/* Contenu */}
<div className="px-6 py-6">
<div className="space-y-6">
{/* Télécharger le modèle */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<svg className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="flex-1">
<h3 className="text-sm font-semibold text-blue-900 mb-1">
Télécharger le modèle d'exemple
</h3>
<p className="text-sm text-blue-700 mb-3">
Téléchargez le fichier CSV modèle pour voir le format attendu et remplir vos données.
</p>
<button
onClick={handleDownloadTemplate}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Télécharger le modèle CSV
</button>
</div>
</div>
</div>
{/* Upload fichier */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Sélectionner le fichier CSV à importer
</label>
<div className="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-lg hover:border-lblue transition-colors">
<div className="space-y-1 text-center">
<svg className="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
</svg>
<div className="flex text-sm text-gray-600">
<label htmlFor="file-upload" className="relative cursor-pointer bg-white rounded-md font-medium text-lblue hover:text-dblue focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-lblue">
<span>Cliquez pour sélectionner un fichier</span>
<input
id="file-upload"
name="file-upload"
type="file"
accept=".csv"
onChange={handleFileUpload}
className="sr-only"
/>
</label>
<p className="pl-1">ou glissez-déposez</p>
</div>
<p className="text-xs text-gray-500">CSV jusqu'à 10MB</p>
</div>
</div>
</div>
{/* Instructions */}
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
<h3 className="text-sm font-semibold text-gray-900 mb-2">
Instructions :
</h3>
<ul className="text-sm text-gray-600 space-y-1 list-disc list-inside">
<li>Les champs Nom, Prénom, Date de naissance, Adresse, Email et Téléphone sont obligatoires</li>
<li>Le format de date attendu est JJ/MM/AAAA</li>
<li>Les autres champs sont optionnels</li>
<li>Assurez-vous que le fichier utilise l'encodage UTF-8</li>
</ul>
</div>
</div>
</div>
{/* Footer */}
<div className="border-t border-gray-200 px-6 py-4 bg-gray-50/50">
<div className="flex justify-end">
<button
onClick={() => setShowImportModal(false)}
className="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors"
>
Fermer
</button>
</div>
</div>
</div>
</div>
)}
{/* Modal résultat */}
{resultModal && resultModal.show && (
<div
className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-[60] p-4 animate-fadeIn"
onClick={() => setResultModal(null)}
>
<div
className="bg-white rounded-lg shadow-xl max-w-lg w-full animate-slideUp border border-gray-200"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className={`border-b border-gray-200 px-6 py-5 ${
resultModal.type === 'success' ? 'bg-green-50' :
resultModal.type === 'error' ? 'bg-red-50' :
'bg-blue-50'
}`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{resultModal.type === 'success' && (
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)}
{resultModal.type === 'error' && (
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)}
{resultModal.type === 'info' && (
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)}
<h2 className={`text-xl font-semibold ${
resultModal.type === 'success' ? 'text-green-900' :
resultModal.type === 'error' ? 'text-red-900' :
'text-blue-900'
}`}>
{resultModal.title}
</h2>
</div>
<button
type="button"
onClick={() => setResultModal(null)}
className="text-gray-400 hover:text-gray-600 transition-colors p-1.5 hover:bg-white/50 rounded"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/* Contenu */}
<div className="px-6 py-6">
<p className={`text-sm font-medium mb-4 ${
resultModal.type === 'success' ? 'text-green-800' :
resultModal.type === 'error' ? 'text-red-800' :
'text-blue-800'
}`}>
{resultModal.message}
</p>
{resultModal.details && resultModal.details.length > 0 && (
<div className="mt-4">
<h3 className="text-sm font-semibold text-gray-900 mb-2">
Erreurs :
</h3>
<div className="bg-red-50 border border-red-200 rounded-lg p-4 max-h-64 overflow-y-auto">
<ul className="space-y-1">
{resultModal.details.slice(0, 10).map((error, index) => (
<li key={index} className="text-sm text-red-800">
{error}
</li>
))}
{resultModal.details.length > 10 && (
<li className="text-sm text-red-600 italic">
... et {resultModal.details.length - 10} autre(s) erreur(s)
</li>
)}
</ul>
</div>
</div>
)}
</div>
{/* Footer */}
<div className="border-t border-gray-200 px-6 py-4 bg-gray-50/50">
<div className="flex justify-end">
<button
type="button"
onClick={() => setResultModal(null)}
className={`px-6 py-2 text-sm font-medium rounded-lg transition-colors ${
resultModal.type === 'success'
? 'bg-green-600 text-white hover:bg-green-700' :
resultModal.type === 'error'
? 'bg-red-600 text-white hover:bg-red-700' :
'bg-blue-600 text-white hover:bg-blue-700'
}`}
>
OK
</button>
</div>
</div>
</div>
</div>
)}
{/* Modal de confirmation de suppression */}
{confirmDeleteModal && (
<ConfirmModal
isOpen={confirmDeleteModal.show}
title="Supprimer l'adhérent"
message="Êtes-vous sûr de vouloir supprimer cet adhérent ?"
confirmText="Supprimer"
cancelText="Annuler"
confirmColor="danger"
onConfirm={confirmDelete}
onCancel={() => setConfirmDeleteModal(null)}
/>
)}
2026-02-08 14:16:55 +01:00
{/* Modal vue détaillée - Design épuré */}
2026-01-20 19:02:49 +01:00
{viewingAdherent && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4 animate-fadeIn">
2026-02-08 14:16:55 +01:00
<div className="bg-white rounded-xl shadow-2xl max-w-5xl w-full max-h-[95vh] overflow-hidden flex flex-col animate-slideUp border border-gray-200">
{/* Header épuré */}
<div className="border-b border-gray-200 px-8 py-6 bg-white">
<div className="flex items-start justify-between">
<div className="flex items-center gap-5">
<div className="w-16 h-16 rounded-full bg-lblue flex items-center justify-center text-white font-bold text-xl border-4 border-lblue/10 shadow-sm">
2026-01-20 19:02:49 +01:00
{getInitials(viewingAdherent.nom, viewingAdherent.prenom)}
</div>
<div>
2026-02-08 14:16:55 +01:00
<h2 className="text-2xl font-bold text-gray-900 mb-1">
2026-01-20 19:02:49 +01:00
{viewingAdherent.prenom} {viewingAdherent.nom}
</h2>
2026-02-08 14:16:55 +01:00
<p className="text-gray-500 text-sm">
2026-01-20 19:02:49 +01:00
Informations détaillées de l'adhérent
</p>
</div>
</div>
<button
onClick={() => setViewingAdherent(null)}
2026-02-08 14:16:55 +01:00
className="text-gray-400 hover:text-gray-600 transition-colors p-2 hover:bg-gray-100 rounded-lg"
2026-01-20 19:02:49 +01:00
>
2026-02-08 14:16:55 +01:00
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
2026-01-20 19:02:49 +01:00
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/* Contenu scrollable */}
2026-02-08 14:16:55 +01:00
<div className="flex-1 overflow-y-auto">
<div className="p-8">
{/* Actions rapides */}
<div className="mb-8 flex flex-wrap gap-3">
<a
href={`tel:${viewingAdherent.telephone}`}
className="flex items-center gap-2 px-4 py-2 bg-gray-50 text-gray-700 border border-gray-200 rounded-lg hover:bg-gray-100 hover:border-lblue transition-colors"
>
<svg className="w-5 h-5 text-lblue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
<span className="text-sm font-medium">Appeler</span>
</a>
{viewingAdherent.telephoneSecondaire && (
<a
href={`tel:${viewingAdherent.telephoneSecondaire}`}
className="flex items-center gap-2 px-4 py-2 bg-gray-50 text-gray-700 border border-gray-200 rounded-lg hover:bg-gray-100 hover:border-lblue transition-colors"
>
<svg className="w-5 h-5 text-lblue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
2026-01-20 19:02:49 +01:00
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
2026-02-08 14:16:55 +01:00
<span className="text-sm font-medium">Téléphone secondaire</span>
</a>
)}
<a
href={`mailto:${viewingAdherent.email}`}
className="flex items-center gap-2 px-4 py-2 bg-gray-50 text-gray-700 border border-gray-200 rounded-lg hover:bg-gray-100 hover:border-lblue transition-colors"
>
<svg className="w-5 h-5 text-lblue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<span className="text-sm font-medium">Envoyer un email</span>
</a>
2026-01-20 19:02:49 +01:00
</div>
2026-02-08 14:16:55 +01:00
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Carte Informations principales */}
<div className="bg-white rounded-xl border border-gray-200 p-6 shadow-sm">
<div className="flex items-center gap-3 mb-6 pb-4 border-b border-gray-200">
<div className="w-10 h-10 rounded-lg bg-lblue/10 flex items-center justify-center">
<svg className="w-5 h-5 text-lblue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
2026-01-20 19:02:49 +01:00
</div>
2026-02-08 14:16:55 +01:00
<h3 className="text-lg font-semibold text-gray-900">
Informations principales
</h3>
2026-01-20 19:02:49 +01:00
</div>
2026-02-08 14:16:55 +01:00
<div className="space-y-4">
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Date de naissance</p>
<p className="text-sm font-semibold text-gray-900">{formatDate(viewingAdherent.dateNaissance)}</p>
</div>
2026-01-20 19:02:49 +01:00
</div>
2026-02-08 14:16:55 +01:00
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Téléphone</p>
<a href={`tel:${viewingAdherent.telephone}`} className="text-sm font-semibold text-lblue hover:text-dblue transition-colors">
{viewingAdherent.telephone}
</a>
</div>
2026-01-20 19:02:49 +01:00
</div>
2026-02-08 14:16:55 +01:00
{viewingAdherent.telephoneSecondaire && (
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Téléphone secondaire</p>
<a href={`tel:${viewingAdherent.telephoneSecondaire}`} className="text-sm font-semibold text-lblue hover:text-dblue transition-colors">
{viewingAdherent.telephoneSecondaire}
</a>
</div>
</div>
)}
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Email</p>
<a href={`mailto:${viewingAdherent.email}`} className="text-sm font-semibold text-lblue hover:text-dblue transition-colors break-all">
{viewingAdherent.email}
</a>
</div>
2026-02-06 11:34:16 +01:00
</div>
2026-02-08 14:16:55 +01:00
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Adresse</p>
<p className="text-sm font-semibold text-gray-900">{viewingAdherent.adresse}</p>
</div>
2026-02-06 11:34:16 +01:00
</div>
</div>
2026-02-08 14:16:55 +01:00
</div>
2026-02-06 11:34:16 +01:00
2026-02-08 14:16:55 +01:00
{/* Carte Informations complémentaires */}
<div className="bg-white rounded-xl border border-gray-200 p-6 shadow-sm">
<div className="flex items-center gap-3 mb-6 pb-4 border-b border-gray-200">
<div className="w-10 h-10 rounded-lg bg-lblue/10 flex items-center justify-center">
<svg className="w-5 h-5 text-lblue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
2026-01-20 19:02:49 +01:00
</svg>
</div>
2026-02-08 14:16:55 +01:00
<h3 className="text-lg font-semibold text-gray-900">
Informations complémentaires
</h3>
2026-01-20 19:02:49 +01:00
</div>
2026-02-08 14:16:55 +01:00
<div className="space-y-4">
{viewingAdherent.situation && (
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Situation</p>
<p className="text-sm font-semibold text-gray-900">{viewingAdherent.situation}</p>
</div>
</div>
)}
2026-01-20 19:02:49 +01:00
2026-02-08 14:16:55 +01:00
{viewingAdherent.prescripteur && (
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Prescripteur</p>
<p className="text-sm font-semibold text-gray-900">{viewingAdherent.prescripteur}</p>
</div>
</div>
)}
2026-01-20 19:02:49 +01:00
2026-02-08 14:16:55 +01:00
{viewingAdherent.facturation && (
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 14l6-6m-5.5.5h.01m4.99 5h.01M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16l3.5-2 3.5 2 3.5-2 3.5 2zM10 8.5a.5.5 0 11-1 0 .5.5 0 011 0zm5 0a.5.5 0 11-1 0 .5.5 0 011 0z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Facturation</p>
<p className="text-sm font-semibold text-gray-900">{viewingAdherent.facturation}</p>
</div>
</div>
)}
{viewingAdherent.forfait && (
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Forfait</p>
<p className="text-sm font-semibold text-gray-900">{viewingAdherent.forfait}</p>
</div>
</div>
)}
{viewingAdherent.commentaire && (
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Commentaire</p>
<p className="text-sm font-semibold text-gray-900 whitespace-pre-wrap">{viewingAdherent.commentaire}</p>
</div>
</div>
)}
{viewingAdherent.instructions && (
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Instructions</p>
<p className="text-sm font-semibold text-gray-900 whitespace-pre-wrap">{viewingAdherent.instructions}</p>
</div>
</div>
)}
{!viewingAdherent.situation && !viewingAdherent.prescripteur && !viewingAdherent.facturation && !viewingAdherent.forfait && !viewingAdherent.commentaire && !viewingAdherent.instructions && (
<div className="text-center py-8 text-gray-400 text-sm">
Aucune information complémentaire
</div>
)}
2026-01-20 19:02:49 +01:00
</div>
2026-02-08 14:16:55 +01:00
</div>
2026-01-20 19:02:49 +01:00
</div>
</div>
</div>
2026-02-08 14:16:55 +01:00
{/* Footer avec actions */}
<div className="border-t border-gray-200 px-8 py-5 bg-white">
<div className="flex items-center justify-between">
2026-01-20 19:02:49 +01:00
<button
onClick={() => setViewingAdherent(null)}
2026-02-08 14:16:55 +01:00
className="px-5 py-2.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
2026-01-20 19:02:49 +01:00
>
Fermer
</button>
<button
onClick={() => {
setViewingAdherent(null);
handleEdit(viewingAdherent);
}}
2026-02-08 14:16:55 +01:00
className="px-6 py-2.5 bg-lblue text-white text-sm font-semibold rounded-lg hover:bg-dblue transition-colors flex items-center gap-2"
2026-01-20 19:02:49 +01:00
>
2026-02-08 14:16:55 +01:00
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
2026-01-20 19:02:49 +01:00
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
2026-02-08 14:16:55 +01:00
Modifier l'adhérent
2026-01-20 19:02:49 +01:00
</button>
</div>
</div>
</div>
</div>
)}
</>
);
}