groupe-meditation/frontend/src/pages/GererMembres.jsx

203 lines
8.7 KiB
React
Raw Normal View History

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('');
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(); }, []);
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">
<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>}
<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>
);
}