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

202 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
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)}`);
};
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">
{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>
{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>
);
}