This commit is contained in:
2026-06-15 22:06:30 +02:00
parent ecc2670fbc
commit f54d352b7f
+37 -35
View File
@@ -265,60 +265,42 @@ function InviteUserModal({ open, onClose, onSent }) {
} }
// ── Modale renvoi invitation ─────────────────────────────────────────────── // ── Modale renvoi invitation ───────────────────────────────────────────────
function ResendInviteModal({ user, onClose, onSent }) { function ResendInviteModal({ user, onClose, onSuccess, onError }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [err, setErr] = useState(null); const [err, setErr] = useState(null);
const [sent, setSent] = useState(false);
const handleClose = () => { setSent(false); setErr(null); onClose(); }; const handleClose = () => { setErr(null); onClose(); };
const handleSend = async () => { const handleSend = async () => {
setLoading(true); setErr(null); setLoading(true); setErr(null);
try { try {
await api.post('/admin/invitations', { email: user.email, role: user.role }); await api.post('/admin/invitations', { email: user.email, role: user.role });
setSent(true); // Ferme d'abord, puis notifie le parent évite le re-render concurrent
onSent?.(); handleClose();
} catch (e) { setErr(e.message); } onSuccess?.(user.email);
finally { setLoading(false); } } catch (e) {
setErr(e.message);
} finally { setLoading(false); }
}; };
const identity = user?.display_name ? `${user.display_name} (${user.email})` : user?.email; const identity = user?.display_name ? `${user.display_name} (${user.email})` : user?.email;
return ( return (
<Modal open={!!user} title="Renvoyer l'invitation" onClose={handleClose} width={420} <Modal open={!!user} title="Renvoyer l'invitation" onClose={handleClose} width={420}
footer={sent ? ( footer={
<button className="btn btn-primary" onClick={handleClose}>Fermer</button>
) : (
<> <>
<button className="btn btn-outline" onClick={handleClose} disabled={loading}>Annuler</button> <button className="btn btn-outline" onClick={handleClose} disabled={loading}>Annuler</button>
<button className="btn btn-primary" onClick={handleSend} disabled={loading}> <button className="btn btn-primary" onClick={handleSend} disabled={loading}>
{loading ? 'Envoi…' : 'Renvoyer'} {loading ? 'Envoi…' : 'Renvoyer'}
</button> </button>
</> </>
)} }
> >
{sent ? ( {err && <div className="error" style={{ marginBottom: 14 }}>{err}</div>}
<div style={{ textAlign: 'center', padding: '12px 0' }}> <p style={{ margin: 0, color: 'var(--text-muted)', fontSize: 14, lineHeight: 1.6 }}>
<div style={{ width: 52, height: 52, borderRadius: '50%', background: '#f0fdf4', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 16px' }}> Renvoyer un nouveau lien d'invitation à <strong style={{ color: 'var(--text)' }}>{identity}</strong> ?
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#16a34a" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <br/>L'ancien lien sera révoqué et remplacé par un nouveau valable 7 jours.
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/> </p>
</svg>
</div>
<p style={{ margin: '0 0 6px', fontWeight: 600, color: 'var(--text)' }}>Invitation renvoyée !</p>
<p style={{ margin: 0, fontSize: 13, color: 'var(--text-muted)' }}>
Un nouveau lien a été envoyé à <strong>{user?.email}</strong>.<br/>
L'ancien lien a été révoqué. Le nouveau est valable 7 jours.
</p>
</div>
) : (
<>
{err && <div className="error" style={{ marginBottom: 14 }}>{err}</div>}
<p style={{ margin: 0, color: 'var(--text-muted)', fontSize: 14, lineHeight: 1.6 }}>
Renvoyer un nouveau lien d'invitation à <strong style={{ color: 'var(--text)' }}>{identity}</strong> ?
<br/>L'ancien lien sera révoqué et remplacé par un nouveau valable 7 jours.
</p>
</>
)}
</Modal> </Modal>
); );
} }
@@ -396,6 +378,7 @@ export default function UsersSection({ currentUserId }) {
const [showCreate, setShowCreate] = useState(false); const [showCreate, setShowCreate] = useState(false);
const [showInvite, setShowInvite] = useState(false); const [showInvite, setShowInvite] = useState(false);
const [resendTarget, setResendTarget] = useState(null); const [resendTarget, setResendTarget] = useState(null);
const [banner, setBanner] = useState(null); // { ok, msg }
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [filterStatus, setFilterStatus] = useState('all'); const [filterStatus, setFilterStatus] = useState('all');
const [filterRole, setFilterRole] = useState('all'); const [filterRole, setFilterRole] = useState('all');
@@ -445,6 +428,12 @@ export default function UsersSection({ currentUserId }) {
`Marquer l'email de ${u.display_name || u.email} comme vérifié ?`, `Marquer l'email de ${u.display_name || u.email} comme vérifié ?`,
async () => { await api.patch(`/admin/users/${u.id}/verify-email`, {}); load(); }, 'Confirmer'); async () => { await api.patch(`/admin/users/${u.id}/verify-email`, {}); load(); }, 'Confirmer');
const resendInvitation = u => setResendTarget(u); const resendInvitation = u => setResendTarget(u);
const resendVerifEmail = async u => {
try {
await api.post('/auth/resend-verification', { email: u.email });
setBanner({ ok: true, msg: `Email de vérification renvoyé à ${u.email}.` });
} catch (e) { setBanner({ ok: false, msg: e.message }); }
};
const setStatus = (u, status) => { const setStatus = (u, status) => {
const labels = { deactivated: 'Désactiver', active: 'Réactiver', locked: 'Verrouiller' }; const labels = { deactivated: 'Désactiver', active: 'Réactiver', locked: 'Verrouiller' };
confirm(`${labels[status]} le compte`, `${labels[status]} le compte de ${u.display_name || u.email} ?`, confirm(`${labels[status]} le compte`, `${labels[status]} le compte de ${u.display_name || u.email} ?`,
@@ -532,6 +521,15 @@ export default function UsersSection({ currentUserId }) {
<> <>
<div className="card" style={{ padding: 0, overflow: 'hidden' }}> <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
{/* Bannière succès/erreur */}
{banner && (
<ResultBanner
result={banner}
onDismiss={() => setBanner(null)}
style={{ margin: '12px 20px 0', borderRadius: 8 }}
/>
)}
{/* En-tête */} {/* En-tête */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '20px 20px 14px', gap: 16, borderBottom: '1px solid var(--border)', flexWrap: 'wrap' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '20px 20px 14px', gap: 16, borderBottom: '1px solid var(--border)', flexWrap: 'wrap' }}>
<div> <div>
@@ -668,10 +666,11 @@ export default function UsersSection({ currentUserId }) {
background: 'var(--surface)', border: '1px solid var(--border)', background: 'var(--surface)', border: '1px solid var(--border)',
borderRadius: 8, boxShadow: '0 4px 20px rgba(0,0,0,0.15)', padding: '4px 0', minWidth: 200, borderRadius: 8, boxShadow: '0 4px 20px rgba(0,0,0,0.15)', padding: '4px 0', minWidth: 200,
}}> }}>
<MenuBtn icon={<IcoEdit />} label={openMenu.user.role === 'admin' ? '→ Utilisateur' : '→ Admin'} onClick={() => { setOpenMenu(null); toggleRole(openMenu.user); }} /> <MenuBtn icon={<IcoEdit />} label={openMenu.user.role === 'admin' ? 'Rétrograder en utilisateur' : 'Promouvoir en admin'} onClick={() => { setOpenMenu(null); toggleRole(openMenu.user); }} />
{!openMenu.user.email_verified && <> {!openMenu.user.email_verified && <>
<MenuBtn icon={<IcoMail />} label="Renvoyer l'invitation" onClick={() => { setOpenMenu(null); resendInvitation(openMenu.user); }} /> <MenuBtn icon={<IcoMail />} label="Renvoyer l'invitation" onClick={() => { setOpenMenu(null); resendInvitation(openMenu.user); }} />
<MenuBtn icon={<IcoCheck />} label="Vérifier l'email" onClick={() => { setOpenMenu(null); verifyEmail(openMenu.user); }} /> <MenuBtn icon={<IcoCheck />} label="Vérifier l'email" onClick={() => { setOpenMenu(null); verifyEmail(openMenu.user); }} />
<MenuBtn icon={<IcoMail />} label="Nouvelle demande de vérification" onClick={() => { setOpenMenu(null); resendVerifEmail(openMenu.user); }} />
</>} </>}
{openMenu.user.id !== currentUserId && <> {openMenu.user.id !== currentUserId && <>
{(!openMenu.user.status || openMenu.user.status === 'active') && <MenuBtn icon={<IcoPause />} label="Désactiver" onClick={() => { setOpenMenu(null); setStatus(openMenu.user, 'deactivated'); }} />} {(!openMenu.user.status || openMenu.user.status === 'active') && <MenuBtn icon={<IcoPause />} label="Désactiver" onClick={() => { setOpenMenu(null); setStatus(openMenu.user, 'deactivated'); }} />}
@@ -693,7 +692,10 @@ export default function UsersSection({ currentUserId }) {
<ResendInviteModal <ResendInviteModal
user={resendTarget} user={resendTarget}
onClose={() => setResendTarget(null)} onClose={() => setResendTarget(null)}
onSent={() => load()} onSuccess={email => {
load();
setBanner({ ok: true, msg: `Invitation renvoyée à ${email}. Le lien est valable 7 jours.` });
}}
/> />
<CreateUserModal <CreateUserModal