import React, { useEffect, useState } from 'react' import styles from '../styles/Admin.module.css' import { API_URL } from '../services/theapi' import { FileText, Database, Users } from 'lucide-react' type User = { id: string; email: string; full_name?: string; role: 'admin' | 'user' } type Document = { id: string; title: string; source: 'CIR' | 'SCCS'; ingredient?: string; date?: string } type ScraperLog = { id: string; when: string; source: string; status: 'pending' | 'running' | 'success' | 'error' } type Stats = { total_documents?: number; cir_documents?: number; sccs_documents?: number; total_users?: number } export default function Admin() { const [tab, setTab] = useState<'users'|'documents'|'stats'|'scrapers'>('users') const [users, setUsers] = useState([]) const [docs, setDocs] = useState([]) const [stats, setStats] = useState(null) const [logs, setLogs] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [confirmOpen, setConfirmOpen] = useState(false) const [confirmAction, setConfirmAction] = useState<() => void>(()=>{}) const [confirmText, setConfirmText] = useState('') const [search, setSearch] = useState('') const [sortKey, setSortKey] = useState(null) const [sortDir, setSortDir] = useState<'asc'|'desc'>('asc') // Jeton posé par App.tsx au login (handleAuthSuccess). const token = localStorage.getItem('authToken') ?? '' useEffect(() => { const fetchTab = async () => { setError(null) setLoading(true) try{ if(tab==='users'){ const res = await fetch(`${API_URL}/admin/users?token=${token}`) if(!res.ok) throw new Error('Impossible de charger les utilisateurs') const data = await res.json() setUsers(data || []) } else if(tab==='documents'){ const res = await fetch(`${API_URL}/documents`) if(!res.ok) throw new Error('Impossible de charger les documents') const data = await res.json() setDocs(data || []) } else if(tab==='stats'){ const res = await fetch(`${API_URL}/admin/stats?token=${token}`) if(!res.ok) throw new Error('Impossible de charger les statistiques') const data = await res.json() setStats(data) } else if(tab==='scrapers'){ // logs endpoint (fallback) const res = await fetch(`${API_URL}/admin/scrapers/logs?token=${token}`) if(!res.ok) throw new Error('Impossible de charger les logs de scrapers') const data = await res.json() setLogs(data || []) } }catch(e: unknown){ const msg = e instanceof Error ? e.message : String(e) setError(msg || 'Erreur réseau') }finally{setLoading(false)} } fetchTab() }, [tab]) function openConfirm(text: string, action: ()=>void){ setConfirmText(text) setConfirmAction(()=>action) setConfirmOpen(true) } async function deleteUser(id: string){ try{ setLoading(true) const res = await fetch(`${API_URL}/admin/users/${id}?token=${token}`, { method: 'DELETE' }) if(!res.ok) throw new Error('Suppression impossible') setUsers(u=>u.filter(x=>x.id!==id)) window.dispatchEvent(new CustomEvent('app:toast', { detail: { message: 'Utilisateur supprimé', type: 'success' } })) }catch(e: unknown){ const msg = e instanceof Error ? e.message : String(e) setError(msg||'Erreur') window.dispatchEvent(new CustomEvent('app:toast', { detail: { message: msg||'Erreur', type: 'error' } })) }finally{setLoading(false)} } async function toggleRole(u: User){ try{ setLoading(true) const newRole = u.role === 'admin' ? 'user' : 'admin' const res = await fetch(`${API_URL}/admin/users/${u.id}/role?token=${token}`, { method: 'PATCH', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ role: newRole }) }) if(!res.ok) throw new Error('Impossible de modifier le rôle') setUsers(list=>list.map(x=> x.id===u.id ? {...x, role:newRole} : x)) window.dispatchEvent(new CustomEvent('app:toast', { detail: { message: `Rôle mis à jour: ${newRole}`, type: 'success' } })) }catch(e: unknown){ const msg = e instanceof Error ? e.message : String(e); setError(msg||'Erreur'); window.dispatchEvent(new CustomEvent('app:toast', { detail: { message: msg||'Erreur', type: 'error' } })) }finally{ setLoading(false) } } async function deleteDocument(id:string){ try{ setLoading(true) const res = await fetch(`${API_URL}/admin/documents/${id}?token=${token}`, { method: 'DELETE' }) if(!res.ok) throw new Error('Suppression impossible') setDocs(d=>d.filter(x=>x.id!==id)) window.dispatchEvent(new CustomEvent('app:toast', { detail: { message: 'Document supprimé', type: 'success' } })) }catch(e: unknown){ const msg = e instanceof Error ? e.message : String(e); setError(msg||'Erreur'); window.dispatchEvent(new CustomEvent('app:toast', { detail: { message: msg||'Erreur', type: 'error' } })) }finally{ setLoading(false) } } async function runScraper(source: 'CIR'|'SCCS'){ try{ setLoading(true) const res = await fetch(`${API_URL}/admin/scrapers/run?source=${source}&token=${token}`, { method: 'POST' }) if(!res.ok) throw new Error('Erreur lors du lancement') const entry = await res.json() setLogs(l=>[entry, ...l].slice(0,5)) window.dispatchEvent(new CustomEvent('app:toast', { detail: { message: `Scraper ${source} lancé`, type: 'success' } })) }catch(e: unknown){ const msg = e instanceof Error ? e.message : String(e); setError(msg||'Erreur'); window.dispatchEvent(new CustomEvent('app:toast', { detail: { message: msg||'Erreur', type: 'error' } })) }finally{ setLoading(false) } } const filteredDocs = docs.filter(d => d.title.toLowerCase().includes(search.toLowerCase()) || (d.ingredient||'').toLowerCase().includes(search.toLowerCase())) const sortedUsers = React.useMemo(()=>{ if(!sortKey) return users return [...users].sort((a: User, b: User) =>{ const ra = a as unknown as Record const rb = b as unknown as Record const va = typeof ra[sortKey as string] === 'string' ? (ra[sortKey as string] as string) : '' const vb = typeof rb[sortKey as string] === 'string' ? (rb[sortKey as string] as string) : '' if(typeof va === 'string' && typeof vb === 'string'){ return sortDir==='asc' ? va.localeCompare(vb) : vb.localeCompare(va) } return 0 }) },[users, sortKey, sortDir]) const sortedDocs = React.useMemo(()=>{ if(!sortKey) return filteredDocs return [...filteredDocs].sort((a: Document, b: Document) =>{ const ra = a as unknown as Record const rb = b as unknown as Record const va = typeof ra[sortKey as string] === 'string' ? (ra[sortKey as string] as string) : '' const vb = typeof rb[sortKey as string] === 'string' ? (rb[sortKey as string] as string) : '' if(typeof va === 'string' && typeof vb === 'string'){ return sortDir==='asc' ? va.localeCompare(vb) : vb.localeCompare(va) } return 0 }) },[filteredDocs, sortKey, sortDir]) return (

Administration

{loading &&
Chargement...
} {error &&
{error}
} {tab==='users' && (
{users.length} utilisateurs au total
{sortedUsers.map(u=> ( ))}
{ setSortKey('id'); setSortDir(sortDir==='asc'?'desc':'asc') }}>ID { setSortKey('email'); setSortDir(sortDir==='asc'?'desc':'asc') }}>Email { setSortKey('full_name'); setSortDir(sortDir==='asc'?'desc':'asc') }}>Nom complet { setSortKey('role'); setSortDir(sortDir==='asc'?'desc':'asc') }}>Rôle
{u.id} {u.email} {u.full_name||'-'} {u.role}
)} {tab==='documents' && (
{docs.length} documents au total
setSearch(e.target.value)} />
{sortedDocs.map(d=> ( ))}
{ setSortKey('id'); setSortDir(sortDir==='asc'?'desc':'asc') }}>ID { setSortKey('title'); setSortDir(sortDir==='asc'?'desc':'asc') }}>Titre { setSortKey('source'); setSortDir(sortDir==='asc'?'desc':'asc') }}>Source { setSortKey('ingredient'); setSortDir(sortDir==='asc'?'desc':'asc') }}>Ingrédient { setSortKey('date'); setSortDir(sortDir==='asc'?'desc':'asc') }}>Date
{d.id} {d.title} {d.source} {d.ingredient||'-'} {d.date||'-'}
{filteredDocs.length===0 &&
Aucun document trouvé.
}
)} {tab==='stats' && (
Total documents
{stats?.total_documents ?? '-'}
Documents CIR
{stats?.cir_documents ?? '-'}
Documents SCCS
{stats?.sccs_documents ?? '-'}
Total utilisateurs
{stats?.total_users ?? '-'}
CIR vs SCCS
CIR
{stats?.cir_documents||0}
SCCS
{stats?.sccs_documents||0}
)} {tab==='scrapers' && (
Scrapers

Dernières exécutions

    {logs.map(l=> (
  • {l.when} — {l.source} — {l.status}
  • ))} {logs.length===0 &&
  • Aucune exécution.
  • }
)}
{confirmOpen && (

{confirmText}

)}
) }