Page admin : statistiques réparées et refonte responsive dans la DA du site
Build & Deploy / build (push) Successful in 22s

Statistiques : le type Stats attendait cir/sccs/users alors que /admin/stats
renvoie cir_documents/sccs_documents/total_users. Trois cartes sur quatre
affichaient « - » et les deux barres restaient à zéro. Contrat aligné sur l'API.

Style : la feuille n'utilisait aucune couleur du site — palette bleu ardoise,
unités en px, cartes sans bordure. Reprise de la DA des autres pages : accent
violet #7c3aed (celui de la Sidebar), échelle de gris et rayons 0.5/0.75rem de
DocumentsPage, cartes blanches bordées, unités rem.

Responsive :
- onglets en bande défilante au lieu d'un retour à la ligne qui cassait l'entête ;
- les tableaux deviennent des cartes sous 768px, chaque cellule portant son
  intitulé de colonne via data-label + ::before — l'ancien display:block empilait
  les valeurs sans dire à quoi elles correspondaient ;
- cartes de stats en grille auto-fit, plus de paliers codés en dur ;
- panneau scrapers en deux colonnes, empilé sous 1024px, boutons pleine largeur
  sous 480px ;
- modale contrainte à la largeur de l'écran, focus visible au clavier.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-15 16:34:00 +02:00
co-authored by Claude Opus 5
parent 4890a29ba3
commit 02d56d90bc
2 changed files with 595 additions and 71 deletions
+17 -17
View File
@@ -7,7 +7,7 @@ type User = { id: string; email: string; full_name?: string; role: 'admin' | 'us
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?: number; sccs?: number; users?: number }
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')
@@ -184,10 +184,10 @@ export default function Admin() {
<tbody>
{sortedUsers.map(u=> (
<tr key={u.id}>
<td>{u.id}</td>
<td>{u.email}</td>
<td>{u.full_name||'-'}</td>
<td><span className={u.role==='admin'?styles.badgeAdmin:styles.badgeUser}>{u.role}</span></td>
<td data-label="ID">{u.id}</td>
<td data-label="Email">{u.email}</td>
<td data-label="Nom complet">{u.full_name||'-'}</td>
<td data-label="Rôle"><span className={u.role==='admin'?styles.badgeAdmin:styles.badgeUser}>{u.role}</span></td>
<td className={styles.rowActions}>
<button className={styles.danger} onClick={()=>openConfirm('Supprimer cet utilisateur ?', ()=>deleteUser(u.id))}>Supprimer</button>
<button onClick={()=>openConfirm(u.role==='admin'? 'Rétrograder cet utilisateur ?' : 'Passer admin ?', ()=>toggleRole(u))}>{u.role==='admin'?'Rétrograder':'Passer admin'}</button>
@@ -212,9 +212,9 @@ export default function Admin() {
<thead>
<tr>
<th onClick={()=>{ setSortKey('id'); setSortDir(sortDir==='asc'?'desc':'asc') }}>ID</th>
<th onClick={()=>{ setSortKey('title'); setSortDir(sortDir==='asc'?'desc':'asc') }}>Title</th>
<th onClick={()=>{ setSortKey('title'); setSortDir(sortDir==='asc'?'desc':'asc') }}>Titre</th>
<th onClick={()=>{ setSortKey('source'); setSortDir(sortDir==='asc'?'desc':'asc') }}>Source</th>
<th onClick={()=>{ setSortKey('ingredient'); setSortDir(sortDir==='asc'?'desc':'asc') }}>Ingredient</th>
<th onClick={()=>{ setSortKey('ingredient'); setSortDir(sortDir==='asc'?'desc':'asc') }}>Ingrédient</th>
<th onClick={()=>{ setSortKey('date'); setSortDir(sortDir==='asc'?'desc':'asc') }}>Date</th>
<th></th>
</tr>
@@ -222,11 +222,11 @@ export default function Admin() {
<tbody>
{sortedDocs.map(d=> (
<tr key={d.id}>
<td>{d.id}</td>
<td>{d.title}</td>
<td><span className={d.source==='CIR'?styles.badgeCIR:styles.badgeSCCS}>{d.source}</span></td>
<td>{d.ingredient||'-'}</td>
<td>{d.date||'-'}</td>
<td data-label="ID">{d.id}</td>
<td data-label="Titre">{d.title}</td>
<td data-label="Source"><span className={d.source==='CIR'?styles.badgeCIR:styles.badgeSCCS}>{d.source}</span></td>
<td data-label="Ingrédient">{d.ingredient||'-'}</td>
<td data-label="Date">{d.date||'-'}</td>
<td className={styles.rowActions}><button className={styles.danger} onClick={()=>openConfirm('Supprimer ce document ?', ()=>deleteDocument(d.id))}>Supprimer</button></td>
</tr>
))}
@@ -242,16 +242,16 @@ export default function Admin() {
<section>
<div className={styles.cards}>
<div className={styles.card}><FileText/><div className={styles.cardLabel}>Total documents</div><div className={styles.cardValue}>{stats?.total_documents ?? '-'}</div></div>
<div className={styles.card}><Database/><div className={styles.cardLabel}>Documents CIR</div><div className={styles.cardValue}>{stats?.cir ?? '-'}</div></div>
<div className={styles.card}><Database/><div className={styles.cardLabel}>Documents SCCS</div><div className={styles.cardValue}>{stats?.sccs ?? '-'}</div></div>
<div className={styles.card}><Users/><div className={styles.cardLabel}>Total utilisateurs</div><div className={styles.cardValue}>{stats?.users ?? '-'}</div></div>
<div className={styles.card}><Database/><div className={styles.cardLabel}>Documents CIR</div><div className={styles.cardValue}>{stats?.cir_documents ?? '-'}</div></div>
<div className={styles.card}><Database/><div className={styles.cardLabel}>Documents SCCS</div><div className={styles.cardValue}>{stats?.sccs_documents ?? '-'}</div></div>
<div className={styles.card}><Users/><div className={styles.cardLabel}>Total utilisateurs</div><div className={styles.cardValue}>{stats?.total_users ?? '-'}</div></div>
</div>
<div className={styles.chartWrap}>
<div className={styles.chartLabel}>CIR vs SCCS</div>
<div className={styles.bars}>
<div className={styles.barRow}><div className={styles.barTitle}>CIR</div><div className={styles.barFill} style={{width:`${(stats?.cir||0)/(Math.max(1, (stats?.cir||0)+(stats?.sccs||0))) * 100}%`}}></div><div className={styles.barVal}>{stats?.cir||0}</div></div>
<div className={styles.barRow}><div className={styles.barTitle}>SCCS</div><div className={`${styles.barFill} ${styles.barFillAlt}`} style={{width:`${(stats?.sccs||0)/(Math.max(1, (stats?.cir||0)+(stats?.sccs||0))) * 100}%`}}></div><div className={styles.barVal}>{stats?.sccs||0}</div></div>
<div className={styles.barRow}><div className={styles.barTitle}>CIR</div><div className={styles.barFill} style={{width:`${(stats?.cir_documents||0)/(Math.max(1, (stats?.cir_documents||0)+(stats?.sccs_documents||0))) * 100}%`}}></div><div className={styles.barVal}>{stats?.cir_documents||0}</div></div>
<div className={styles.barRow}><div className={styles.barTitle}>SCCS</div><div className={`${styles.barFill} ${styles.barFillAlt}`} style={{width:`${(stats?.sccs_documents||0)/(Math.max(1, (stats?.cir_documents||0)+(stats?.sccs_documents||0))) * 100}%`}}></div><div className={styles.barVal}>{stats?.sccs_documents||0}</div></div>
</div>
</div>
</section>