phase 8
This commit is contained in:
@@ -150,6 +150,9 @@ function ProgramReviewPanel({ selectedNode, reviewForm, onReviewChange, onSaveRe
|
||||
<span>Contextes: {status.contexts ?? 0}</span>
|
||||
<span>Exercices: {status.text_exercises ?? 0}</span>
|
||||
<span>SVG exercices: {status.exercise_svg ?? 0}</span>
|
||||
<span>Tests adaptatifs: {status.adaptive_tests ?? 0}</span>
|
||||
<span>Parcours: {status.adaptive_profiles ?? 0}</span>
|
||||
<span>SVG kit: {status.adaptive_svg ?? 0}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -160,6 +163,12 @@ function ProgramReviewPanel({ selectedNode, reviewForm, onReviewChange, onSaveRe
|
||||
</button>
|
||||
)}
|
||||
|
||||
{selectedNode.content && !selectedNode.url && (
|
||||
<div className="program-node-text-preview">
|
||||
<pre>{selectedNode.content}</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{review.comment && (
|
||||
<div className="admin-review-note">
|
||||
<strong>Dernier commentaire</strong>
|
||||
@@ -387,6 +396,12 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
const [activeSectionIndex, setActiveSectionIndex] = useState(0)
|
||||
const [programStart, setProgramStart] = useState({ asset_index: 0, section_index: 0 })
|
||||
const [cardProgress, setCardProgress] = useState([])
|
||||
const [adaptiveProfile, setAdaptiveProfile] = useState(null)
|
||||
const [adaptiveProfileForm, setAdaptiveProfileForm] = useState({
|
||||
diagnostic_id: '',
|
||||
profile_id: '',
|
||||
notes: '',
|
||||
})
|
||||
const [sessionCardStates, setSessionCardStates] = useState({})
|
||||
const [sessionLogs, setSessionLogs] = useState([])
|
||||
const [selectedSessionLog, setSelectedSessionLog] = useState(null)
|
||||
@@ -540,6 +555,8 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
setProgramLessons([])
|
||||
setProgramStart({ asset_index: 0, section_index: 0 })
|
||||
setCardProgress([])
|
||||
setAdaptiveProfile(null)
|
||||
setAdaptiveProfileForm({ diagnostic_id: '', profile_id: '', notes: '' })
|
||||
setSessionCardStates({})
|
||||
setSessionLogs([])
|
||||
setSelectedSessionLog(null)
|
||||
@@ -576,6 +593,9 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
useEffect(() => {
|
||||
if (selectedStudentId && studentProgram?.id) {
|
||||
loadCardProgress(selectedStudentId, studentProgram.id)
|
||||
if (!isStudentUser) {
|
||||
loadAdaptiveProfile(selectedStudentId, studentProgram.id)
|
||||
}
|
||||
}
|
||||
}, [selectedStudentId, studentProgram?.id])
|
||||
|
||||
@@ -913,6 +933,46 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAdaptiveProfile(studentId, lessonId) {
|
||||
if (!studentId || !lessonId || isStudentUser) return
|
||||
try {
|
||||
const data = await apiFetch(`/admin/students/${studentId}/adaptive-profile?lesson_id=${encodeURIComponent(lessonId)}`)
|
||||
setAdaptiveProfile(data || null)
|
||||
setAdaptiveProfileForm({
|
||||
diagnostic_id: data?.diagnostic_id || '',
|
||||
profile_id: data?.profile_id || '',
|
||||
notes: data?.notes || '',
|
||||
})
|
||||
} catch {
|
||||
setAdaptiveProfile(null)
|
||||
setAdaptiveProfileForm({ diagnostic_id: '', profile_id: '', notes: '' })
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAdaptiveProfile(event) {
|
||||
event.preventDefault()
|
||||
if (!selectedStudentId || !studentProgram?.id || !adaptiveProfileForm.profile_id) return
|
||||
const profile = (studentProgram.adaptive_kit?.profiles || []).find((item) => item.id === adaptiveProfileForm.profile_id)
|
||||
if (!profile) return
|
||||
try {
|
||||
const data = await apiFetch(`/admin/students/${selectedStudentId}/adaptive-profile`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
lesson_id: studentProgram.id,
|
||||
diagnostic_id: adaptiveProfileForm.diagnostic_id || null,
|
||||
profile_id: profile.id,
|
||||
profile_title: profile.title,
|
||||
notes: adaptiveProfileForm.notes,
|
||||
source: 'teacher',
|
||||
}),
|
||||
})
|
||||
setAdaptiveProfile(data)
|
||||
} catch (error) {
|
||||
setErrorMessage(error.message || 'Impossible d’enregistrer le profil adaptatif.')
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSessionLogs(studentId) {
|
||||
if (!studentId || isStudentUser) return
|
||||
try {
|
||||
@@ -1151,6 +1211,7 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
function getLessonContextPayload() {
|
||||
const { asset, section } = getLessonPosition()
|
||||
return {
|
||||
lesson_id: studentProgram?.id || null,
|
||||
lesson_title: activeLessonTitle,
|
||||
asset_title: asset?.title || null,
|
||||
section_title: section?.title || asset?.title || null,
|
||||
@@ -1553,6 +1614,7 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
const lastUserMessage = [...messages].reverse().find((message) => message.role === 'user')
|
||||
const activeLessonTitle = studentProgram?.title || lessonFocus?.label || 'Cours du jour'
|
||||
const lessonAssets = studentProgram?.assets || []
|
||||
const adaptiveKit = studentProgram?.adaptive_kit || null
|
||||
const activeAsset = lessonAssets[activeAssetIndex] || lessonAssets[0] || null
|
||||
const activeSections = activeAsset?.sections || []
|
||||
const activeSection = activeSections[activeSectionIndex] || null
|
||||
@@ -1578,7 +1640,9 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
const lessonSummary = lastAssistantMessage?.content || 'Aucune ancienne séance trouvée pour le moment. Professeur TOP commencera par une reprise courte.'
|
||||
const lastStudentWork = lastUserMessage?.content || 'Aucune réponse récente enregistrée.'
|
||||
const nextLessonAdvice = studentProgram
|
||||
? `Conseil: démarrer la leçon ${studentProgram.title} avec les fiches choisies par le professeur.`
|
||||
? adaptiveKit
|
||||
? `Conseil: si l’élève hésite, lancer le diagnostic adaptatif ${adaptiveKit.title.toLowerCase()} avant de poursuivre.`
|
||||
: `Conseil: démarrer la leçon ${studentProgram.title} avec les fiches choisies par le professeur.`
|
||||
: lessonFocus
|
||||
? `Conseil: reprendre ${lessonFocus.label}, puis lancer un exercice court pour équilibrer la progression.`
|
||||
: 'Conseil: démarrer une séance pour laisser Professeur TOP choisir la prochaine leçon.'
|
||||
@@ -1792,7 +1856,9 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
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 reviewRows = programRows.filter(({ node }) =>
|
||||
['lesson', 'fiche', 'card', 'kit', 'diagnostic', 'parcours', 'support'].includes(node.type)
|
||||
)
|
||||
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
|
||||
@@ -1967,6 +2033,17 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
<strong>{validatedCards}</strong>
|
||||
<p>{remainingCards} card(s) encore à travailler</p>
|
||||
</article>
|
||||
<article>
|
||||
<span>Kit adaptatif</span>
|
||||
<strong>{adaptiveProfile?.profile_title || (adaptiveKit ? 'Disponible' : 'Absent')}</strong>
|
||||
<p>
|
||||
{adaptiveProfile
|
||||
? 'Profil de reprise enregistré'
|
||||
: adaptiveKit
|
||||
? `${adaptiveKit.diagnostics?.length || 0} test(s), ${adaptiveKit.profiles?.length || 0} parcours`
|
||||
: 'Aucun diagnostic relié à cette leçon.'}
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div className="student-settings-grid">
|
||||
@@ -2013,6 +2090,77 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{adaptiveKit && (
|
||||
<section className="adaptive-kit-panel">
|
||||
<div className="admin-panel-head">
|
||||
<div>
|
||||
<span className="stage-label">Adaptatif</span>
|
||||
<h2>Diagnostic et parcours disponibles</h2>
|
||||
</div>
|
||||
<button type="button" className="secondary-button" onClick={() => setActiveAdminTab('validation')}>
|
||||
Valider le kit
|
||||
</button>
|
||||
</div>
|
||||
<div className="adaptive-kit-grid">
|
||||
<article>
|
||||
<strong>Tests diagnostics</strong>
|
||||
{(adaptiveKit.diagnostics || []).map((item) => (
|
||||
<span key={item.id}>{item.title}</span>
|
||||
))}
|
||||
</article>
|
||||
<article>
|
||||
<strong>Parcours profils</strong>
|
||||
{(adaptiveKit.profiles || []).map((item) => (
|
||||
<span key={item.id}>{item.title}</span>
|
||||
))}
|
||||
</article>
|
||||
<article>
|
||||
<strong>Supports visuels</strong>
|
||||
{(adaptiveKit.supports || []).map((item) => (
|
||||
<span key={item.id}>{item.title}</span>
|
||||
))}
|
||||
</article>
|
||||
</div>
|
||||
<form className="adaptive-profile-form" onSubmit={saveAdaptiveProfile}>
|
||||
<label>
|
||||
<span>Test diagnostic de référence</span>
|
||||
<select
|
||||
value={adaptiveProfileForm.diagnostic_id}
|
||||
onChange={(event) => setAdaptiveProfileForm({ ...adaptiveProfileForm, diagnostic_id: event.target.value })}
|
||||
>
|
||||
<option value="">Non précisé</option>
|
||||
{(adaptiveKit.diagnostics || []).map((item) => (
|
||||
<option key={item.id} value={item.id}>{item.title}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Parcours recommandé</span>
|
||||
<select
|
||||
value={adaptiveProfileForm.profile_id}
|
||||
onChange={(event) => setAdaptiveProfileForm({ ...adaptiveProfileForm, profile_id: event.target.value })}
|
||||
>
|
||||
<option value="">Choisir un parcours</option>
|
||||
{(adaptiveKit.profiles || []).map((item) => (
|
||||
<option key={item.id} value={item.id}>{item.title}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="wide-field">
|
||||
<span>Note de suivi</span>
|
||||
<textarea
|
||||
value={adaptiveProfileForm.notes}
|
||||
onChange={(event) => setAdaptiveProfileForm({ ...adaptiveProfileForm, notes: event.target.value })}
|
||||
placeholder="Exemple: inverse encore numérateur/dénominateur, commencer par le parcours verbal."
|
||||
/>
|
||||
</label>
|
||||
<button type="submit" disabled={!adaptiveProfileForm.profile_id}>
|
||||
Enregistrer le profil adaptatif
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div className="admin-dashboard-grid">
|
||||
<section className="admin-preview">
|
||||
<h2>Compréhension des cards</h2>
|
||||
@@ -2150,11 +2298,13 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
{selectedProgramNode?.review?.status && <span className={`review-pill ${selectedProgramNode.review.status}`}>{selectedProgramNode.review.status}</span>}
|
||||
</div>
|
||||
<div className="validation-main-preview">
|
||||
{selectedProgramNode?.url ? (
|
||||
<img src={selectedProgramNode.url} alt={selectedProgramNode.label} />
|
||||
) : (
|
||||
<div className="empty-preview">Aucun visuel direct pour cet élément.</div>
|
||||
)}
|
||||
{selectedProgramNode?.url ? (
|
||||
<img src={selectedProgramNode.url} alt={selectedProgramNode.label} />
|
||||
) : selectedProgramNode?.content ? (
|
||||
<pre className="validation-text-preview">{selectedProgramNode.content}</pre>
|
||||
) : (
|
||||
<div className="empty-preview">Aucun visuel direct pour cet élément.</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -2167,6 +2317,9 @@ function TutorApp({ currentUser, onLogout }) {
|
||||
<span>Contextes: {selectedNodeStatus.contexts ?? '-'}</span>
|
||||
<span>Exercices: {selectedNodeStatus.text_exercises ?? '-'}</span>
|
||||
<span>SVG ex.: {selectedNodeStatus.exercise_svg ?? '-'}</span>
|
||||
<span>Tests: {selectedNodeStatus.adaptive_tests ?? '-'}</span>
|
||||
<span>Parcours: {selectedNodeStatus.adaptive_profiles ?? '-'}</span>
|
||||
<span>SVG kit: {selectedNodeStatus.adaptive_svg ?? '-'}</span>
|
||||
</div>
|
||||
<form className="admin-review-form" onSubmit={saveProgramReview}>
|
||||
<label>
|
||||
|
||||
@@ -216,7 +216,7 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
}
|
||||
|
||||
.student-report-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.student-report-grid article {
|
||||
@@ -350,6 +350,67 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.adaptive-kit-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 16px;
|
||||
background: #f8fbff;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.adaptive-kit-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.adaptive-kit-grid article {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
background: white;
|
||||
padding: 0.85rem;
|
||||
}
|
||||
|
||||
.adaptive-kit-grid span {
|
||||
color: #475569;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.adaptive-profile-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.75rem;
|
||||
border-top: 1px solid #dbeafe;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.adaptive-profile-form label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.adaptive-profile-form .wide-field,
|
||||
.adaptive-profile-form button {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.adaptive-profile-form textarea {
|
||||
min-height: 96px;
|
||||
resize: vertical;
|
||||
border: 1px solid #cfd7e6;
|
||||
border-radius: 12px;
|
||||
padding: 0.8rem 1rem;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.review-fullscreen {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -687,6 +748,23 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
font-size: 0.82rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
.program-node-text-preview,
|
||||
.validation-text-preview {
|
||||
min-height: 260px;
|
||||
max-height: 520px;
|
||||
overflow: auto;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
}
|
||||
.program-node-text-preview pre,
|
||||
.validation-text-preview {
|
||||
margin: 0;
|
||||
color: #14213d;
|
||||
font: 0.95rem/1.55 Inter, system-ui, sans-serif;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.admin-review-note p {
|
||||
margin: 0.35rem 0;
|
||||
line-height: 1.45;
|
||||
@@ -1218,6 +1296,8 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.admin-students-layout,
|
||||
.student-report-grid,
|
||||
.student-settings-grid,
|
||||
.adaptive-kit-grid,
|
||||
.adaptive-profile-form,
|
||||
.validation-layout,
|
||||
.admin-program-browser {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
Reference in New Issue
Block a user