From 02d56d90bca6352e35b1494dd99ad8d51c4ad416 Mon Sep 17 00:00:00 2001 From: neckfire Date: Sat, 15 Aug 2026 16:34:00 +0200 Subject: [PATCH] =?UTF-8?q?Page=20admin=20:=20statistiques=20r=C3=A9par?= =?UTF-8?q?=C3=A9es=20et=20refonte=20responsive=20dans=20la=20DA=20du=20si?= =?UTF-8?q?te?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/pages/Admin.tsx | 34 +- src/styles/Admin.module.css | 632 +++++++++++++++++++++++++++++++++--- 2 files changed, 595 insertions(+), 71 deletions(-) diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx index 49c2b52..fa2275e 100644 --- a/src/pages/Admin.tsx +++ b/src/pages/Admin.tsx @@ -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() { {sortedUsers.map(u=> ( - {u.id} - {u.email} - {u.full_name||'-'} - {u.role} + {u.id} + {u.email} + {u.full_name||'-'} + {u.role} @@ -212,9 +212,9 @@ export default function Admin() { { setSortKey('id'); setSortDir(sortDir==='asc'?'desc':'asc') }}>ID - { setSortKey('title'); setSortDir(sortDir==='asc'?'desc':'asc') }}>Title + { setSortKey('title'); setSortDir(sortDir==='asc'?'desc':'asc') }}>Titre { setSortKey('source'); setSortDir(sortDir==='asc'?'desc':'asc') }}>Source - { setSortKey('ingredient'); setSortDir(sortDir==='asc'?'desc':'asc') }}>Ingredient + { setSortKey('ingredient'); setSortDir(sortDir==='asc'?'desc':'asc') }}>Ingrédient { setSortKey('date'); setSortDir(sortDir==='asc'?'desc':'asc') }}>Date @@ -222,11 +222,11 @@ export default function Admin() { {sortedDocs.map(d=> ( - {d.id} - {d.title} - {d.source} - {d.ingredient||'-'} - {d.date||'-'} + {d.id} + {d.title} + {d.source} + {d.ingredient||'-'} + {d.date||'-'} ))} @@ -242,16 +242,16 @@ export default function Admin() {
Total documents
{stats?.total_documents ?? '-'}
-
Documents CIR
{stats?.cir ?? '-'}
-
Documents SCCS
{stats?.sccs ?? '-'}
-
Total utilisateurs
{stats?.users ?? '-'}
+
Documents CIR
{stats?.cir_documents ?? '-'}
+
Documents SCCS
{stats?.sccs_documents ?? '-'}
+
Total utilisateurs
{stats?.total_users ?? '-'}
CIR vs SCCS
-
CIR
{stats?.cir||0}
-
SCCS
{stats?.sccs||0}
+
CIR
{stats?.cir_documents||0}
+
SCCS
{stats?.sccs_documents||0}
diff --git a/src/styles/Admin.module.css b/src/styles/Admin.module.css index f572718..29fc969 100644 --- a/src/styles/Admin.module.css +++ b/src/styles/Admin.module.css @@ -1,58 +1,582 @@ -.page{padding:20px;max-width:1200px;margin:0 auto} -.header{display:flex;align-items:center;justify-content:space-between;gap:16px} -.tabs{display:flex;gap:8px} -.tabs button{background:transparent;border:1px solid #e6e9ef;padding:8px 12px;border-radius:8px;cursor:pointer} -.tabs .active{background:#eef2ff;border-color:#c7b3ff} -.content{margin-top:16px} -.spinner{padding:12px;background:#f8fafc;border:1px solid #e6eef6;border-radius:8px} -.error{padding:12px;background:#fff1f2;border:1px solid #fecaca;color:#b91c1c;border-radius:8px} +/* Administration — aligné sur la DA des autres pages : + accent violet #7c3aed (celui de la Sidebar), échelle de gris Tailwind, + cartes blanches bordées, rayons 0.5/0.75rem, unités rem. */ -.panelHeader{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;gap:12px} -.search{padding:8px;border:1px solid #e6eef6;border-radius:6px} - -.tableWrap{overflow:auto;background:#fff;border:1px solid #eef2f7;padding:8px;border-radius:8px} -.table{width:100%;border-collapse:collapse} -.table th,.table td{padding:10px;border-bottom:1px solid #f1f5f9;text-align:left} -.rowActions{display:flex;gap:8px} -.danger{background:#ef4444;color:#fff;border:none;padding:8px 10px;border-radius:6px;cursor:pointer} - -.badgeAdmin{background:#7c3aed;color:#fff;padding:4px 8px;border-radius:999px;font-weight:600} -.badgeUser{background:#94a3b8;color:#fff;padding:4px 8px;border-radius:999px;font-weight:600} -.badgeCIR{background:#2563eb;color:#fff;padding:4px 8px;border-radius:6px} -.badgeSCCS{background:#10b981;color:#fff;padding:4px 8px;border-radius:6px} - -.empty{padding:12px;color:#64748b} - -.cards{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:16px} -.card{background:#fff;padding:16px;border-radius:8px;display:flex;flex-direction:column;gap:8px;align-items:flex-start} -.cardLabel{font-size:14px;color:#64748b} -.cardValue{font-size:20px;font-weight:700} - -.chartWrap{background:#fff;padding:12px;border-radius:8px} -.bars{display:flex;flex-direction:column;gap:8px} -.barRow{display:flex;align-items:center;gap:8px} -.barTitle{width:60px} -.barFill{height:14px;background:#2563eb;border-radius:6px;flex:1} -.barFillAlt{background:#10b981} -.barVal{width:40px;text-align:right} - -.scraperControls{display:flex;gap:20px;align-items:flex-start} -.logList{background:#fff;padding:12px;border-radius:8px} -.status{font-weight:700} - -.modalBackdrop{position:fixed;left:0;right:0;top:0;bottom:0;background:rgba(2,6,23,0.45);display:flex;align-items:center;justify-content:center;z-index:9999} -.modal{background:#fff;padding:20px;border-radius:8px;min-width:320px} -.modalActions{display:flex;gap:8px;justify-content:flex-end;margin-top:12px} - -@media (max-width: 768px){ - .cards{grid-template-columns:repeat(2,1fr)} - .table th, .table td{display:block} - .table tr{margin-bottom:12px;border-bottom:none} - .tableWrap{padding:0} +.page { + padding: 2rem; + max-width: 80rem; + margin: 0 auto; } -@media (max-width: 480px){ - .cards{grid-template-columns:1fr} - .scraperControls{flex-direction:column} - .tabs{flex-wrap:wrap} +/* ── En-tête et onglets ─────────────────────────────────────── */ + +.header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; + margin-bottom: 1.75rem; } + +.header h1 { + font-size: 2.25rem; + font-weight: 700; + color: rgb(17, 24, 39); + line-height: 1.1; + margin: 0; +} + +.tabs { + display: flex; + gap: 0.5rem; +} + +.tabs button { + background: transparent; + border: 1px solid rgb(229, 231, 235); + color: rgb(55, 65, 81); + padding: 0.5rem 0.875rem; + border-radius: 0.5rem; + font-size: 0.9375rem; + font-weight: 500; + cursor: pointer; + white-space: nowrap; + transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease; +} + +.tabs button:hover { + background: rgb(249, 250, 251); + border-color: rgb(209, 213, 219); +} + +.tabs .active, +.tabs .active:hover { + background: rgba(124, 58, 237, 0.08); + border-color: rgba(124, 58, 237, 0.45); + color: #7c3aed; +} + +.tabs button:focus-visible, +.danger:focus-visible, +.search:focus-visible { + outline: 2px solid #7c3aed; + outline-offset: 2px; +} + +.content { + margin-top: 0.5rem; +} + +/* ── États ──────────────────────────────────────────────────── */ + +.spinner { + padding: 0.875rem 1rem; + background: rgb(249, 250, 251); + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + color: rgb(75, 85, 99); + margin-bottom: 1rem; +} + +.error { + padding: 0.875rem 1rem; + background: rgb(254, 242, 242); + border: 1px solid rgb(254, 202, 202); + color: rgb(185, 28, 28); + border-radius: 0.75rem; + margin-bottom: 1rem; +} + +.empty { + padding: 1.5rem; + text-align: center; + color: rgb(107, 114, 128); +} + +/* ── Barre de panneau ───────────────────────────────────────── */ + +.panelHeader { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; + margin-bottom: 1rem; + color: rgb(75, 85, 99); + font-size: 0.9375rem; +} + +.search { + padding: 0.5rem 0.75rem; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.5rem; + font-size: 0.9375rem; + min-width: 14rem; + flex: 1 1 14rem; + max-width: 22rem; +} + +/* ── Tableaux ───────────────────────────────────────────────── */ + +.tableWrap { + background: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + overflow-x: auto; +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: 0.9375rem; +} + +.table th, +.table td { + padding: 0.75rem 1rem; + text-align: left; + border-bottom: 1px solid rgb(243, 244, 246); +} + +.table th { + background: rgb(249, 250, 251); + color: rgb(55, 65, 81); + font-weight: 600; + white-space: nowrap; + cursor: pointer; + user-select: none; +} + +.table th:hover { + color: #7c3aed; +} + +.table tbody tr:last-child td { + border-bottom: none; +} + +.table tbody tr:hover { + background: rgb(249, 250, 251); +} + +.rowActions { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.rowActions button { + background: white; + border: 1px solid rgb(229, 231, 235); + color: rgb(55, 65, 81); + padding: 0.375rem 0.75rem; + border-radius: 0.5rem; + font-size: 0.875rem; + cursor: pointer; +} + +.rowActions button:hover { + border-color: rgba(124, 58, 237, 0.45); + color: #7c3aed; +} + +.danger, +.rowActions .danger { + background: rgb(239, 68, 68); + color: white; + border: 1px solid rgb(239, 68, 68); +} + +.danger:hover, +.rowActions .danger:hover { + background: rgb(220, 38, 38); + border-color: rgb(220, 38, 38); + color: white; +} + +/* ── Badges ─────────────────────────────────────────────────── */ + +.badgeAdmin, +.badgeUser, +.badgeCIR, +.badgeSCCS { + display: inline-block; + padding: 0.1875rem 0.625rem; + font-size: 0.8125rem; + font-weight: 600; + white-space: nowrap; +} + +.badgeAdmin, +.badgeUser { + border-radius: 999px; +} + +.badgeAdmin { + background: rgba(124, 58, 237, 0.1); + color: #6d28d9; + border: 1px solid rgba(124, 58, 237, 0.35); +} + +.badgeUser { + background: rgb(243, 244, 246); + color: rgb(75, 85, 99); + border: 1px solid rgb(229, 231, 235); +} + +.badgeCIR, +.badgeSCCS { + border-radius: 0.5rem; +} + +.badgeCIR { + background: rgba(37, 99, 235, 0.1); + color: rgb(29, 78, 216); + border: 1px solid rgba(37, 99, 235, 0.3); +} + +.badgeSCCS { + background: rgba(16, 185, 129, 0.12); + color: rgb(4, 120, 87); + border: 1px solid rgba(16, 185, 129, 0.3); +} + +/* ── Statistiques ───────────────────────────────────────────── */ + +.cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); + gap: 1rem; + margin-bottom: 1.25rem; +} + +.card { + background: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 1.25rem; + display: flex; + flex-direction: column; + gap: 0.375rem; + align-items: flex-start; +} + +.card svg { + width: 1.25rem; + height: 1.25rem; + color: #7c3aed; + margin-bottom: 0.25rem; +} + +.cardLabel { + font-size: 0.875rem; + color: rgb(107, 114, 128); +} + +.cardValue { + font-size: 1.75rem; + font-weight: 700; + color: rgb(17, 24, 39); + line-height: 1.1; +} + +.chartWrap { + background: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 1.25rem; +} + +.chartLabel { + font-weight: 600; + color: rgb(17, 24, 39); + margin-bottom: 1rem; +} + +.bars { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.barRow { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.barTitle { + width: 3.5rem; + flex-shrink: 0; + font-size: 0.875rem; + font-weight: 500; + color: rgb(55, 65, 81); +} + +/* La largeur est posée en style inline par le composant : min-width garde une + amorce visible quand la valeur est nulle. */ +.barFill { + height: 0.625rem; + min-width: 0.625rem; + background: rgb(37, 99, 235); + border-radius: 999px; +} + +.barFillAlt { + background: rgb(16, 185, 129); +} + +.barVal { + width: 3rem; + flex-shrink: 0; + text-align: right; + font-variant-numeric: tabular-nums; + color: rgb(75, 85, 99); +} + +/* ── Scrapers ───────────────────────────────────────────────── */ + +.scraperControls { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); + gap: 1rem; + align-items: start; +} + +.scraperControls > div:first-child { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.scraperControls button { + background: #7c3aed; + color: white; + border: 1px solid #7c3aed; + padding: 0.625rem 1rem; + border-radius: 0.5rem; + font-size: 0.9375rem; + font-weight: 500; + cursor: pointer; +} + +.scraperControls button:hover { + background: #6d28d9; + border-color: #6d28d9; +} + +.logList { + background: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 1.25rem; +} + +.logList h4 { + margin: 0 0 0.75rem; + font-size: 0.9375rem; + font-weight: 600; + color: rgb(17, 24, 39); +} + +.logList ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.logList li { + padding: 0.625rem 0.75rem; + background: rgb(249, 250, 251); + border: 1px solid rgb(243, 244, 246); + border-radius: 0.5rem; + font-size: 0.875rem; + color: rgb(75, 85, 99); + overflow-wrap: anywhere; +} + +.status { + font-weight: 600; + color: rgb(17, 24, 39); +} + +/* ── Modale ─────────────────────────────────────────────────── */ + +.modalBackdrop { + position: fixed; + inset: 0; + background: rgba(17, 24, 39, 0.5); + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + z-index: 9999; +} + +.modal { + background: white; + border-radius: 0.75rem; + padding: 1.5rem; + width: 100%; + max-width: 26rem; + box-shadow: 0 10px 25px rgba(17, 24, 39, 0.15); +} + +.modalActions { + display: flex; + gap: 0.5rem; + justify-content: flex-end; + margin-top: 1.25rem; + flex-wrap: wrap; +} + +.modalActions button { + background: white; + border: 1px solid rgb(229, 231, 235); + color: rgb(55, 65, 81); + padding: 0.5rem 1rem; + border-radius: 0.5rem; + font-size: 0.9375rem; + cursor: pointer; +} + +.modalActions button:last-child { + background: #7c3aed; + border-color: #7c3aed; + color: white; +} + +/* ── Responsive ─────────────────────────────────────────────── */ + +@media (max-width: 1024px) { + .scraperControls { + grid-template-columns: 1fr; + } + + .scraperControls > div:first-child { + flex-direction: row; + flex-wrap: wrap; + } +} + +@media (max-width: 768px) { + .page { + padding: 1.25rem 1rem; + } + + .header { + align-items: flex-start; + flex-direction: column; + gap: 0.875rem; + } + + .header h1 { + font-size: 1.75rem; + } + + /* Onglets en bande défilante plutôt qu'en retour à la ligne. */ + .tabs { + width: 100%; + overflow-x: auto; + padding-bottom: 0.25rem; + scrollbar-width: none; + } + + .tabs::-webkit-scrollbar { + display: none; + } + + /* Chaque ligne devient une carte, avec l'intitulé de colonne en regard. */ + .tableWrap { + background: transparent; + border: none; + border-radius: 0; + overflow-x: visible; + } + + .table, + .table tbody, + .table tr, + .table td { + display: block; + width: 100%; + } + + .table thead { + display: none; + } + + .table tr { + background: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 0.25rem 0; + margin-bottom: 0.75rem; + } + + .table tbody tr:hover { + background: white; + } + + .table td { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.5rem 1rem; + border-bottom: none; + text-align: right; + overflow-wrap: anywhere; + } + + .table td::before { + content: attr(data-label); + font-weight: 600; + color: rgb(107, 114, 128); + font-size: 0.8125rem; + text-align: left; + flex-shrink: 0; + } + + .rowActions { + justify-content: stretch; + padding-top: 0.75rem; + border-top: 1px solid rgb(243, 244, 246); + margin-top: 0.25rem; + } + + .rowActions button { + flex: 1; + padding: 0.5rem 0.75rem; + } + + .search { + max-width: none; + } +} + +@media (max-width: 480px) { + .cards { + grid-template-columns: 1fr; + } + + .scraperControls > div:first-child { + flex-direction: column; + } + + .scraperControls button { + width: 100%; + } + + .barTitle { + width: 2.75rem; + } +} + +/* NB : le site n'a pas de mode sombre réel — index.css bascule --bg sur le body + mais aucune page n'utilise ces variables et toutes gardent des cartes claires. + Cette page suit la même convention pour rester cohérente avec ses sœurs. */