diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 123014d..75ec53f 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -296,6 +296,7 @@ function TutorApp({ currentUser, onLogout }) {
const [programTree, setProgramTree] = useState(null)
const [selectedProgramNodeKey, setSelectedProgramNodeKey] = useState('')
const [programReviewForm, setProgramReviewForm] = useState({ status: 'needs_changes', comment: '' })
+ const [activeAdminTab, setActiveAdminTab] = useState('overview')
const [studentProgram, setStudentProgram] = useState(null)
const [activeAssetIndex, setActiveAssetIndex] = useState(0)
const [activeSectionIndex, setActiveSectionIndex] = useState(0)
@@ -1682,6 +1683,452 @@ function TutorApp({ currentUser, onLogout }) {
)
}
+ if (!isStudentUser) {
+ const validatedCards = cardProgress.filter((item) => item.validated).length
+ const averageComprehension = cardProgress.length
+ ? Math.round(cardProgress.reduce((sum, item) => sum + item.comprehension_score, 0) / cardProgress.length)
+ : 0
+ const reviewRows = programRows.filter(({ node }) => node.type === 'lesson' || node.type === 'fiche' || node.type === 'card')
+ const reviewQueue = reviewRows.filter(({ node }) => ['needs_changes', 'blocked', 'draft', undefined].includes(node.review?.status))
+ const selectedNodeStatus = selectedProgramNode?.status || {}
+ const totalLessonCards = studentProgram?.assets?.reduce((sum, asset) => sum + (asset.sections?.length || 0), 0) || 0
+ const remainingCards = Math.max(totalLessonCards - validatedCards, 0)
+ const adminTabs = [
+ { id: 'overview', label: 'Vue d’ensemble' },
+ { id: 'students', label: 'Élèves' },
+ { id: 'program', label: 'Programme global' },
+ { id: 'validation', label: 'Validation contenus' },
+ { id: 'factory', label: 'Atelier génération' },
+ { id: 'settings', label: 'Paramètres globaux' },
+ ]
+
+ return (
+
+
+
+
+
+
+ {activeAdminTab === 'overview' && (
+ <>
+
+
+ Élèves actifs
+ {students.length}
+ {selectedStudent ? `${selectedStudent.first_name} sélectionné` : 'Aucun élève sélectionné'}
+
+
+ Leçon active
+ {studentProgram?.title || 'À affecter'}
+ {studentProgram ? `${studentProgram.asset_count} fiche(s) · ${totalLessonCards} cards` : 'Passe par l’onglet Élèves'}
+
+
+ Compréhension
+ {averageComprehension}%
+ {validatedCards} card(s) validée(s), {remainingCards} restante(s)
+
+
+ File de revue
+ {reviewQueue.length}
+ Contenus à relire, modifier ou intégrer
+
+
+
+
+
+
+
+ Priorités
+
À traiter maintenant
+
+
+
+
+
+
+
+
+
+
+
+
+ Production
+
État du programme
+
+
+
+
+ Leçons: {programStatus?.lesson_count ?? 0}
+ CM1: {programStatus?.lessons_by_grade?.CM1 ?? 0}
+ Dossier: {programStatus?.content_root_exists === false ? 'absent' : 'OK'}
+
+
+
+ >
+ )}
+
+ {activeAdminTab === 'students' && (
+
+
+
+
+ {selectedStudent ? (
+ <>
+
+
+
Fiche élève
+
{selectedStudent.first_name}
+
{selectedStudent.grade} · {selectedStudent.age} ans
+
+
+
+
+
+ Leçon courante
+ {studentProgram?.title || 'Aucune leçon'}
+ {studentProgram ? `${studentProgram.subject} · ${studentProgram.asset_count} fiche(s)` : 'Affecte une leçon ci-dessous.'}
+
+
+ Résultats
+ {averageComprehension}%
+ {cardProgress.length} card(s) observée(s)
+
+
+ Contenu fait
+ {validatedCards}
+ {remainingCards} card(s) encore à travailler
+
+
+
+
+
+ {studentProgram?.assets?.length > 0 && (
+ <>
+
+
+ >
+ )}
+
+
+
+
+
+
+ Compréhension des cards
+ {cardProgress.length ? (
+
+ {cardProgress.map((item) => (
+
+ {item.section_title}
+ {Math.round(item.comprehension_score)}% · essais: {item.attempts}
+
+ ))}
+
+ ) : Aucune card validée pour le moment.
}
+
+
+
+ Logs de séance
+ {sessionLogs.length ? (
+
+ {sessionLogs.map((log) => (
+
+
+ {log.lesson_title}
+ {new Date(log.ended_at).toLocaleString('fr-FR')}
+
+
+
+
+
+
+ ))}
+
+ ) : Aucun log enregistré.
}
+
+
+
+ {selectedSessionLog && (
+
+
+
Détail de séance
+
+
+
+
Résultats des cards
+ {parseLogJson(selectedSessionLog.card_results).map((item) => (
+
+ {item.section_title}
+ {Math.round(item.comprehension_score)}% · {item.validated ? 'validée' : 'vue/tentée'}
+
+ ))}
+
Conversation
+ {parseLogJson(selectedSessionLog.conversation).map((message, index) => (
+
+ {message.role === 'assistant' ? 'Professeur TOP' : 'Élève'}: {message.content}
+
+ ))}
+
+
+ )}
+ >
+ ) : (
+
+ Aucun élève sélectionné
+ Choisis un élève ou crée un compte pour configurer son programme.
+
+ )}
+
+
+ )}
+
+ {activeAdminTab === 'program' && (
+
+
+
+
Arborescence du programme
+
+
+
+ {programRows.map(({ node, depth }) => (
+
+ ))}
+
+
+
+
+ )}
+
+ {activeAdminTab === 'validation' && (
+
+
+
+
+
+
+
{selectedProgramNode?.type || 'Sélection'}
+
{selectedProgramNode?.label || 'Choisis une card ou une fiche'}
+
{selectedProgramNode?.path || selectedProgramNode?.key || 'La zone centrale affiche le contenu à analyser en grand.'}
+
+ {selectedProgramNode?.review?.status &&
{selectedProgramNode.review.status}}
+
+
+ {selectedProgramNode?.url ? (
+

+ ) : (
+
Aucun visuel direct pour cet élément.
+ )}
+
+
+
+
+
+ )}
+
+ {activeAdminTab === 'factory' && (
+
+
+
Atelier
+
Planifier une génération
+
+
+
+
+
+
+
+
+
Pipeline
+
Assimilation des contenus
+
+ 1. Génération nocturne
+ 2. File de validation
+ 3. Remarque ou validation
+ 4. Assimilation au programme
+ 5. Disponibilité dans le mode élève
+
+
+
+ )}
+
+ {activeAdminTab === 'settings' && (
+
+
+
Application
+
Paramètres globaux
+
+
+
+
+
+
+
+
Système
+
Diagnostic
+
+ API: active
+ Contenus: {programStatus?.content_root_exists === false ? 'à vérifier' : 'OK'}
+ Leçons: {programStatus?.lesson_count ?? 0}
+
+
+
+ )}
+
+ {errorMessage && {errorMessage}
}
+
+
+ )
+ }
+
return (