Améliorations diverses
This commit is contained in:
@@ -3,6 +3,7 @@ import * as XLSX from 'xlsx';
|
|||||||
import { api } from '../../api.js';
|
import { api } from '../../api.js';
|
||||||
import ConfirmModal from '../../components/ConfirmModal.jsx';
|
import ConfirmModal from '../../components/ConfirmModal.jsx';
|
||||||
import Modal from '../../components/Modal.jsx';
|
import Modal from '../../components/Modal.jsx';
|
||||||
|
import ResultBanner from '../../components/ResultBanner.jsx';
|
||||||
import { fmt, Badge, UserStatusBadge } from './adminHelpers.jsx';
|
import { fmt, Badge, UserStatusBadge } from './adminHelpers.jsx';
|
||||||
|
|
||||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||||
@@ -191,7 +192,7 @@ function PBtn({ onClick, disabled, active, children }) {
|
|||||||
|
|
||||||
// ── Composant principal ────────────────────────────────────────────────────
|
// ── Composant principal ────────────────────────────────────────────────────
|
||||||
// ── Modale invitation ──────────────────────────────────────────────────────
|
// ── Modale invitation ──────────────────────────────────────────────────────
|
||||||
function InviteUserModal({ open, onClose }) {
|
function InviteUserModal({ open, onClose, onSent }) {
|
||||||
const [form, setForm] = useState({ email: '', role: 'user' });
|
const [form, setForm] = useState({ email: '', role: 'user' });
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [err, setErr] = useState(null);
|
const [err, setErr] = useState(null);
|
||||||
@@ -207,6 +208,7 @@ function InviteUserModal({ open, onClose }) {
|
|||||||
try {
|
try {
|
||||||
await api.post('/admin/invitations', { email: form.email, role: form.role });
|
await api.post('/admin/invitations', { email: form.email, role: form.role });
|
||||||
setSent(true);
|
setSent(true);
|
||||||
|
onSent?.();
|
||||||
} catch (e) { setErr(e.message); }
|
} catch (e) { setErr(e.message); }
|
||||||
finally { setLoading(false); }
|
finally { setLoading(false); }
|
||||||
};
|
};
|
||||||
@@ -262,6 +264,65 @@ function InviteUserModal({ open, onClose }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Modale renvoi invitation ───────────────────────────────────────────────
|
||||||
|
function ResendInviteModal({ user, onClose, onSent }) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [err, setErr] = useState(null);
|
||||||
|
const [sent, setSent] = useState(false);
|
||||||
|
|
||||||
|
const handleClose = () => { setSent(false); setErr(null); onClose(); };
|
||||||
|
|
||||||
|
const handleSend = async () => {
|
||||||
|
setLoading(true); setErr(null);
|
||||||
|
try {
|
||||||
|
await api.post('/admin/invitations', { email: user.email, role: user.role });
|
||||||
|
setSent(true);
|
||||||
|
onSent?.();
|
||||||
|
} catch (e) { setErr(e.message); }
|
||||||
|
finally { setLoading(false); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const identity = user?.display_name ? `${user.display_name} (${user.email})` : user?.email;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={!!user} title="Renvoyer l'invitation" onClose={handleClose} width={420}
|
||||||
|
footer={sent ? (
|
||||||
|
<button className="btn btn-primary" onClick={handleClose}>Fermer</button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button className="btn btn-outline" onClick={handleClose} disabled={loading}>Annuler</button>
|
||||||
|
<button className="btn btn-primary" onClick={handleSend} disabled={loading}>
|
||||||
|
{loading ? 'Envoi…' : 'Renvoyer'}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{sent ? (
|
||||||
|
<div style={{ textAlign: 'center', padding: '12px 0' }}>
|
||||||
|
<div style={{ width: 52, height: 52, borderRadius: '50%', background: '#f0fdf4', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 16px' }}>
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#16a34a" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<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"/>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Modale création utilisateur ────────────────────────────────────────────
|
// ── Modale création utilisateur ────────────────────────────────────────────
|
||||||
function CreateUserModal({ open, onClose, onCreated }) {
|
function CreateUserModal({ open, onClose, onCreated }) {
|
||||||
const [form, setForm] = useState({ email: '', password: '', displayName: '', role: 'user' });
|
const [form, setForm] = useState({ email: '', password: '', displayName: '', role: 'user' });
|
||||||
@@ -334,6 +395,7 @@ export default function UsersSection({ currentUserId }) {
|
|||||||
const [confirmAction, setConfirmAction] = useState(null);
|
const [confirmAction, setConfirmAction] = useState(null);
|
||||||
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 [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');
|
||||||
@@ -382,18 +444,20 @@ export default function UsersSection({ currentUserId }) {
|
|||||||
const verifyEmail = u => confirm("Vérifier l'email",
|
const verifyEmail = u => confirm("Vérifier l'email",
|
||||||
`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 resendVerif = async u => {
|
const resendInvitation = u => setResendTarget(u);
|
||||||
try { await api.post('/auth/resend-verification', { email: u.email }); alert(`Email envoyé à ${u.email}`); }
|
|
||||||
catch (e) { setErr(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} ?`,
|
||||||
async () => { await api.patch(`/admin/users/${u.id}/status`, { status }); load(); }, labels[status]);
|
async () => { await api.patch(`/admin/users/${u.id}/status`, { status }); load(); }, labels[status]);
|
||||||
};
|
};
|
||||||
const deleteUser = u => confirm("Supprimer l'utilisateur",
|
const deleteUser = u => {
|
||||||
`Supprimer définitivement ${u.display_name || u.email} ? Toutes ses données seront effacées.`,
|
const identity = u.display_name ? `${u.display_name} (${u.email})` : u.email;
|
||||||
async () => { await api.del(`/admin/users/${u.id}`); load(); });
|
confirm(
|
||||||
|
"Supprimer l'utilisateur",
|
||||||
|
`Supprimer définitivement le compte de ${identity} ? Cette action est irréversible et effacera toutes ses données.`,
|
||||||
|
async () => { await api.del(`/admin/users/${u.id}`); load(); }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const openMenuFor = (e, u) => {
|
const openMenuFor = (e, u) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -606,8 +670,8 @@ export default function UsersSection({ currentUserId }) {
|
|||||||
}}>
|
}}>
|
||||||
<MenuBtn icon={<IcoEdit />} label={openMenu.user.role === 'admin' ? '→ Utilisateur' : '→ Admin'} onClick={() => { setOpenMenu(null); toggleRole(openMenu.user); }} />
|
<MenuBtn icon={<IcoEdit />} label={openMenu.user.role === 'admin' ? '→ Utilisateur' : '→ 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={<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="Renvoyer la vérification" onClick={() => { setOpenMenu(null); resendVerif(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'); }} />}
|
||||||
@@ -623,6 +687,13 @@ export default function UsersSection({ currentUserId }) {
|
|||||||
<InviteUserModal
|
<InviteUserModal
|
||||||
open={showInvite}
|
open={showInvite}
|
||||||
onClose={() => setShowInvite(false)}
|
onClose={() => setShowInvite(false)}
|
||||||
|
onSent={() => load()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ResendInviteModal
|
||||||
|
user={resendTarget}
|
||||||
|
onClose={() => setResendTarget(null)}
|
||||||
|
onSent={() => load()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CreateUserModal
|
<CreateUserModal
|
||||||
|
|||||||
Reference in New Issue
Block a user