2026-03-04 14:01:00 -05:00
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
|
|
import { api } from '../stores/api';
|
|
|
|
|
|
import PageHeader from '../components/PageHeader';
|
|
|
|
|
|
|
|
|
|
|
|
export default function GererMembres() {
|
|
|
|
|
|
const [membres, setMembres] = useState([]);
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
const [detail, setDetail] = useState(null);
|
|
|
|
|
|
const [confirmAction, setConfirmAction] = useState(null);
|
|
|
|
|
|
const [msg, setMsg] = useState('');
|
2026-05-15 16:41:17 -04:00
|
|
|
|
const [code, setCode] = useState(null);
|
|
|
|
|
|
const [expiration, setExpiration] = useState(null);
|
|
|
|
|
|
const [inviteLoading, setInviteLoading] = useState(false);
|
|
|
|
|
|
const [copied, setCopied] = useState(false);
|
2026-03-04 14:01:00 -05:00
|
|
|
|
|
|
|
|
|
|
const charger = async () => {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await api.get('/membres/');
|
|
|
|
|
|
setMembres(data);
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { charger(); }, []);
|
|
|
|
|
|
|
2026-05-15 16:41:17 -04:00
|
|
|
|
const genererInvitation = async () => {
|
|
|
|
|
|
setInviteLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await api.post('/invitations/');
|
|
|
|
|
|
setCode(data.code);
|
|
|
|
|
|
setExpiration(new Date(data.expire_le).toLocaleString('fr-CA'));
|
|
|
|
|
|
} catch (e) { alert(e.message); }
|
|
|
|
|
|
setInviteLoading(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const copierInvitation = () => {
|
|
|
|
|
|
navigator.clipboard.writeText(code).then(() => {
|
|
|
|
|
|
setCopied(true);
|
|
|
|
|
|
setTimeout(() => setCopied(false), 2000);
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const partagerSMS = () => {
|
|
|
|
|
|
const texte = `Rejoins le Groupe Méditation sur l'app. Ton code d'invitation : ${code}`;
|
|
|
|
|
|
window.open(`sms:?body=${encodeURIComponent(texte)}`);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-04 14:01:00 -05:00
|
|
|
|
const desactiver = async (id) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await api.patch(`/membres/${id}/desactiver`);
|
|
|
|
|
|
setMsg(res.message);
|
|
|
|
|
|
setConfirmAction(null);
|
|
|
|
|
|
setDetail(null);
|
|
|
|
|
|
charger();
|
|
|
|
|
|
setTimeout(() => setMsg(''), 3000);
|
|
|
|
|
|
} catch (e) { alert(e.message); }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const droitOubli = async (id) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await api.request('DELETE', `/membres/${id}/oubli`);
|
|
|
|
|
|
setMsg(res.message);
|
|
|
|
|
|
setConfirmAction(null);
|
|
|
|
|
|
setDetail(null);
|
|
|
|
|
|
charger();
|
|
|
|
|
|
setTimeout(() => setMsg(''), 3000);
|
|
|
|
|
|
} catch (e) { alert(e.message); }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Vue détail d'un membre
|
|
|
|
|
|
if (detail) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="min-h-screen bg-gray-100">
|
|
|
|
|
|
<div className="bg-aa-blue text-white p-4 flex items-center gap-3">
|
|
|
|
|
|
<button onClick={() => { setDetail(null); setConfirmAction(null); }} className="text-2xl">←</button>
|
|
|
|
|
|
<h1 className="text-lg font-bold">{detail.prenom} {detail.nom}</h1>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="p-4 space-y-4">
|
|
|
|
|
|
{msg && <div className="bg-green-50 text-green-700 p-3 rounded-xl text-center text-sm font-medium">{msg}</div>}
|
|
|
|
|
|
|
|
|
|
|
|
<div className="bg-white rounded-2xl p-4 shadow-sm space-y-3">
|
|
|
|
|
|
<div><p className="text-xs text-gray-400">Prénom</p><p className="font-medium">{detail.prenom}</p></div>
|
|
|
|
|
|
<div><p className="text-xs text-gray-400">Nom</p><p className="font-medium">{detail.nom}</p></div>
|
|
|
|
|
|
<div><p className="text-xs text-gray-400">Téléphone</p><p className="font-medium">{detail.telephone || '—'}</p></div>
|
|
|
|
|
|
<div><p className="text-xs text-gray-400">Courriel</p><p className="font-medium">{detail.courriel || '—'}</p></div>
|
|
|
|
|
|
<div><p className="text-xs text-gray-400">Date d'abstinence</p><p className="font-medium">{detail.date_abstinence || '—'}</p></div>
|
|
|
|
|
|
<div><p className="text-xs text-gray-400">Statut</p>
|
|
|
|
|
|
<span className={`inline-block px-2 py-1 rounded-full text-xs font-bold ${detail.actif ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
|
|
|
|
|
|
{detail.actif ? 'Actif' : 'Inactif'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Contact rapide */}
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
|
{detail.telephone && (
|
|
|
|
|
|
<a href={`tel:${detail.telephone}`}
|
|
|
|
|
|
className="p-3 bg-green-50 border border-green-200 rounded-xl text-center text-green-700 font-bold text-sm">
|
|
|
|
|
|
📞 Appeler
|
|
|
|
|
|
</a>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{detail.courriel && (
|
|
|
|
|
|
<a href={`mailto:${detail.courriel}`}
|
|
|
|
|
|
className="p-3 bg-blue-50 border border-blue-200 rounded-xl text-center text-blue-700 font-bold text-sm">
|
|
|
|
|
|
✉️ Courriel
|
|
|
|
|
|
</a>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Actions */}
|
|
|
|
|
|
{detail.actif && !confirmAction && (
|
|
|
|
|
|
<button onClick={() => setConfirmAction('desactiver')}
|
|
|
|
|
|
className="w-full p-3 bg-orange-50 border border-orange-200 text-orange-700 rounded-xl font-bold text-sm">
|
|
|
|
|
|
Désactiver ce membre
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{!confirmAction && (
|
|
|
|
|
|
<button onClick={() => setConfirmAction('oubli')}
|
|
|
|
|
|
className="w-full p-3 bg-red-50 border border-red-200 text-red-700 rounded-xl font-bold text-sm">
|
|
|
|
|
|
Droit à l'oubli (irréversible)
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Confirmations */}
|
|
|
|
|
|
{confirmAction === 'desactiver' && (
|
|
|
|
|
|
<div className="bg-orange-50 rounded-2xl p-4 border border-orange-200">
|
|
|
|
|
|
<p className="text-sm text-orange-800 mb-3">Désactiver {detail.prenom} ? Le membre ne pourra plus se connecter.</p>
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
<button onClick={() => desactiver(detail.id)} className="flex-1 p-3 bg-orange-500 text-white rounded-xl font-bold">Confirmer</button>
|
|
|
|
|
|
<button onClick={() => setConfirmAction(null)} className="flex-1 p-3 bg-gray-200 text-gray-700 rounded-xl font-bold">Annuler</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{confirmAction === 'oubli' && (
|
|
|
|
|
|
<div className="bg-red-50 rounded-2xl p-4 border border-red-200">
|
|
|
|
|
|
<p className="text-sm text-red-800 mb-3">⚠️ Cette action est irréversible. Toutes les données personnelles de {detail.prenom} seront anonymisées.</p>
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
<button onClick={() => droitOubli(detail.id)} className="flex-1 p-3 bg-red-600 text-white rounded-xl font-bold">Supprimer</button>
|
|
|
|
|
|
<button onClick={() => setConfirmAction(null)} className="flex-1 p-3 bg-gray-200 text-gray-700 rounded-xl font-bold">Annuler</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="min-h-screen bg-gray-100">
|
2026-05-15 16:41:17 -04:00
|
|
|
|
<PageHeader titre="Gestion des membres" sousTitre="Membres, invitations, désactivation" />
|
|
|
|
|
|
<div className="p-4 space-y-4">
|
2026-03-04 14:01:00 -05:00
|
|
|
|
{msg && <div className="bg-green-50 text-green-700 p-3 rounded-xl text-center text-sm font-medium mb-2">{msg}</div>}
|
2026-05-15 16:41:17 -04:00
|
|
|
|
|
|
|
|
|
|
<div className="bg-white rounded-2xl p-4 shadow-sm space-y-3">
|
|
|
|
|
|
<div className="flex items-center justify-between gap-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h2 className="font-bold text-gray-800">Inviter un membre</h2>
|
|
|
|
|
|
<p className="text-xs text-gray-400">Code à usage unique, expiration automatique.</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button onClick={genererInvitation} disabled={inviteLoading}
|
|
|
|
|
|
className="px-4 py-2 bg-green-600 text-white rounded-xl text-sm font-bold disabled:opacity-40">
|
|
|
|
|
|
{inviteLoading ? '...' : 'Générer'}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{code && (
|
|
|
|
|
|
<div className="bg-green-50 rounded-xl p-3 border border-green-100">
|
|
|
|
|
|
<p className="text-xs text-green-700">Code d'invitation</p>
|
|
|
|
|
|
<p className="text-2xl font-mono font-bold tracking-widest text-green-800">{code}</p>
|
|
|
|
|
|
<p className="text-xs text-green-600 mt-1">Expire le {expiration}</p>
|
|
|
|
|
|
<div className="flex gap-2 mt-3">
|
|
|
|
|
|
<button onClick={copierInvitation}
|
|
|
|
|
|
className="flex-1 py-2 bg-white text-green-700 rounded-xl text-sm font-bold border border-green-200">
|
|
|
|
|
|
{copied ? 'Copié' : 'Copier'}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button onClick={partagerSMS}
|
|
|
|
|
|
className="flex-1 py-2 bg-green-600 text-white rounded-xl text-sm font-bold">
|
|
|
|
|
|
SMS
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-04 14:01:00 -05:00
|
|
|
|
{loading ? (
|
|
|
|
|
|
<p className="text-center text-gray-400 py-8">Chargement...</p>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
membres.map((m) => (
|
|
|
|
|
|
<button key={m.id} onClick={() => setDetail(m)}
|
|
|
|
|
|
className="w-full bg-white rounded-2xl p-4 shadow-sm flex items-center justify-between active:scale-[0.98] transition">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="font-bold text-gray-800">{m.prenom} {m.nom}</p>
|
|
|
|
|
|
<p className="text-xs text-gray-400">{m.telephone || m.courriel || '—'}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span className="text-gray-300">→</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|