IA Assistant version initiale

This commit is contained in:
ocroguennec committed 2026-09-05 10:54:50 +02:00
1 parent 3e8508d48b
commit 662003fdd3
29 files changed
+4253 -10

No files matched your search

+47 -1
View File
@@ -60,6 +60,33 @@ const IcoPlay = () => <svg width="13" height="13" viewBox="0 0 24 24" fill="no
const IcoTrash = () => <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/></svg>;
const IcoDots = () => <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="5" r="1" fill="currentColor"/><circle cx="12" cy="12" r="1" fill="currentColor"/><circle cx="12" cy="19" r="1" fill="currentColor"/></svg>;
const IcoSearch = () => <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>;
const IcoSparkle = () => <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3v4M12 17v4M3 12h4M17 12h4M5.6 5.6l2.8 2.8M15.6 15.6l2.8 2.8M18.4 5.6l-2.8 2.8M8.4 15.6l-2.8 2.8"/></svg>;
// Interrupteur compact — accès individuel à l'Assistant IA (même style visuel
// que le Toggle de AdminIA.jsx, dupliqué ici car non partagé entre les deux
// pages). Un clic déclenche toggleAiAccess(u), qui passe par une confirmation.
function AiToggle({ checked, onClick, disabled, title }) {
return (
<button
type="button"
onClick={onClick}
disabled={disabled}
title={title}
style={{
display: 'inline-flex', alignItems: 'center', width: 36, height: 20, borderRadius: 10,
background: checked ? 'var(--primary)' : 'var(--border)', position: 'relative',
border: 'none', cursor: disabled ? 'default' : 'pointer', padding: 0, flexShrink: 0,
transition: 'background 0.2s', opacity: disabled ? 0.5 : 1,
}}
>
<span style={{
position: 'absolute', top: 2, left: checked ? 18 : 2, width: 16, height: 16,
borderRadius: '50%', background: '#fff', transition: 'left 0.2s',
boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
}} />
</button>
);
}
function MenuBtn({ onClick, icon, label, danger = false }) {
return (
@@ -82,6 +109,7 @@ function toRows(users) {
ID: u.id, Nom: u.display_name || '', Email: u.email,
Statut: STATUS_FR[computedStatus(u)] || '',
Rôle: u.role === 'admin' ? 'Admin' : 'Utilisateur',
'Assistant IA': u.ai_enabled ? 'Activé' : 'Désactivé',
'2FA': u.totp_enabled ? 'Oui' : 'Non',
'Inscrit le': u.created_at ? u.created_at.replace('T', ' ').slice(0, 16) : '',
Plateformes: u.nb_plateformes ?? 0,
@@ -100,7 +128,7 @@ function dlBlob(content, filename, type) {
}
function toCSV(users) {
const headers = ['ID', 'Nom', 'Email', 'Statut', 'Rôle', '2FA', 'Inscrit le', 'Plateformes', 'Investissements', 'Documents', 'Taille documents'];
const headers = ['ID', 'Nom', 'Email', 'Statut', 'Rôle', 'Assistant IA', '2FA', 'Inscrit le', 'Plateformes', 'Investissements', 'Documents', 'Taille documents'];
const rows = toRows(users).map(r => Object.values(r).map(v => `"${String(v).replace(/"/g,'""')}"`).join(','));
return '' + [headers.map(h => `"${h}"`).join(','), ...rows].join('\n');
}
@@ -455,6 +483,15 @@ export default function UsersSection({ currentUserId }) {
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]);
};
const toggleAiAccess = u => {
const enable = !u.ai_enabled;
confirm(
enable ? "Activer l'accès IA" : "Désactiver l'accès IA",
`${enable ? 'Activer' : 'Désactiver'} l'accès à l'Assistant IA pour ${u.display_name || u.email} ?`,
async () => { await api.patch(`/admin/users/${u.id}/ai-access`, { aiEnabled: enable }); load(); },
'Confirmer',
);
};
const deleteUser = u => {
const identity = u.display_name ? `${u.display_name} (${u.email})` : u.email;
confirm(
@@ -615,6 +652,7 @@ export default function UsersSection({ currentUserId }) {
<th style={{ paddingLeft: 20 }}>Utilisateur</th>
<th>Statut</th>
<th>Rôle</th>
<th>Assistant IA</th>
<th>2FA</th>
<th>Inscrit le</th>
<th style={{ textAlign: 'right' }}>Nb de plateformes</th>
@@ -650,6 +688,13 @@ export default function UsersSection({ currentUserId }) {
</td>
<td><UserStatusBadge status={u.status || 'active'} emailVerified={u.email_verified} /></td>
<td><Badge role={u.role} /></td>
<td>
<AiToggle
checked={!!u.ai_enabled}
onClick={() => toggleAiAccess(u)}
title={u.ai_enabled ? "Désactiver l'accès à l'Assistant IA" : "Activer l'accès à l'Assistant IA"}
/>
</td>
<td>
{u.totp_enabled
? <span style={{ fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 10, background: '#eff6ff', color: '#1d4ed8', border: '1px solid #bfdbfe' }}>🔐 Actif</span>
@@ -691,6 +736,7 @@ export default function UsersSection({ currentUserId }) {
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' ? 'Rétrograder en utilisateur' : 'Promouvoir en admin'} onClick={() => { setOpenMenu(null); toggleRole(openMenu.user); }} />
<MenuBtn icon={<IcoSparkle />} label={openMenu.user.ai_enabled ? "Désactiver l'accès IA" : "Activer l'accès IA"} onClick={() => { setOpenMenu(null); toggleAiAccess(openMenu.user); }} />
{!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); }} />