"use client"; // Pédagogie : statistiques agrégées et anonymisées par cours — maîtrise par concept, // questions les plus échouées, erreurs récurrentes, examens blancs, signalements, // rétroactions 👍/👎 par jour. import { useMemo, useState } from "react"; import { BookOpen, Flag, ThumbsDown, ThumbsUp } from "lucide-react"; import { PageHeader } from "@/components/app-shell"; import { Badge, Card, EmptyState, Skeleton, Tabs, cn } from "@/components/ui"; import { ErrorBanner, HBarRow, SectionTitle, fmtDate, fmtInt, useFetchJson } from "./shared"; type Pedagogy = { course: string; minStudents: number; conceptStats: { id: number; name: string; week: number | null; students: number; avg_score: number | null; masked: boolean }[]; hardestQuestions: { id: number; question: string; difficulty: number | string | null; concept: string | null; attempts: number; success_pct: number | null }[]; commonErrors: { concept: string | null; n: number }[]; examStats: { title: string; attempts: number; avg_pct: number | null }[]; flagged: { id: number; reason: string | null; created_at: string; resolved: number; message_preview: string }[]; feedback: { day: string; up: number; down: number }[]; }; const COURSES = [ { key: "IMM1003", label: "IMM1003" }, { key: "IMM1033", label: "IMM1033" }, ]; function successTone(pct: number): string { if (pct < 50) return "bg-red-500"; if (pct < 75) return "bg-amber-500"; return "bg-emerald-500"; } export function AdminPedagogy() { const [course, setCourse] = useState("IMM1003"); const { data, error, loading, reload } = useFetchJson(`/api/admin/pedagogy?course=${course}`); const maxErrors = useMemo(() => Math.max(...(data?.commonErrors ?? []).map((e) => e.n), 1), [data]); return (
} /> {loading ? (
{Array.from({ length: 4 }).map((_, i) => ( ))}
) : error || !data ? ( ) : (
{/* Maîtrise par concept */} Maîtrise moyenne par concept {data.conceptStats.length === 0 ? ( } title="Aucun concept" description="Les concepts du cours n'ont pas encore été définis." /> ) : (
{data.conceptStats.map((c) => ( {c.week != null && S{c.week}} {c.name} } value={c.masked ? 1 : c.avg_score ?? 0} muted={c.masked || c.avg_score == null} display={ c.masked ? "— moins de 3 étudiants" : c.avg_score == null ? "aucune donnée" : `${Math.round(c.avg_score * 100)} %` } /> ))}
)}
{/* Questions les plus échouées */} Questions de quiz les plus échouées {data.hardestQuestions.length === 0 ? ( } title="Pas encore assez de tentatives" description="Les statistiques apparaîtront quand au moins 3 étudiant·e·s auront répondu aux quiz." /> ) : (
{data.hardestQuestions.map((q) => { const pct = q.success_pct ?? 0; return ( ); })}
Question Concept Tentatives Réussite
{q.question} {q.concept ?? "—"} {fmtInt(q.attempts)} {Math.round(pct)} %
)}
{/* Erreurs récurrentes */} Erreurs récurrentes par concept {data.commonErrors.length === 0 ? ( } title="Aucune erreur récurrente" description="Le carnet d'erreurs ne contient pas encore de tendances significatives." /> ) : (
{data.commonErrors.map((e, i) => ( 1 ? "s" : ""}`} barClass="bg-amber-500" /> ))}
)}
{/* Examens blancs */} Examens blancs {data.examStats.length === 0 ? ( } title="Aucun examen blanc" description="Aucun examen blanc n'a été créé pour ce cours." /> ) : (
{data.examStats.map((e, i) => ( ))}
Examen Tentatives Moyenne
{e.title} {fmtInt(e.attempts)} {e.avg_pct == null ? ( — moins de 3 étudiants ) : ( {e.avg_pct.toLocaleString("fr-CA")} % )}
)}
{/* Rétroactions par jour */} Rétroactions 👍 / 👎 par jour {/* Signalements */} Signalements {data.flagged.length === 0 ? ( } title="Aucun signalement" description="Aucune réponse n'a été signalée." /> ) : ( <>
    {data.flagged.map((f) => (
  • {f.resolved ? "Résolu" : "Ouvert"} {f.reason && {f.reason}} {fmtDate(f.created_at)}

    {f.message_preview}…

  • ))}

La résolution des signalements depuis cette interface n'est pas encore offerte par l'API — marquez-les résolus directement en base de données pour l'instant.

)}
)}
); } // Graphique divergent : 👍 au-dessus de la ligne de base (bleu), 👎 en dessous (rouge). function FeedbackChart({ feedback }: { feedback: { day: string; up: number; down: number }[] }) { const days = useMemo(() => { const out: { day: string; up: number; down: number }[] = []; for (let i = 29; i >= 0; i--) { const d = new Date(); d.setDate(d.getDate() - i); const key = d.toISOString().slice(0, 10); const row = feedback.find((r) => r.day === key); out.push({ day: key, up: row?.up ?? 0, down: row?.down ?? 0 }); } return out; }, [feedback]); const max = Math.max(...days.map((d) => Math.max(d.up, d.down)), 0); if (max === 0) { return

Aucune rétroaction sur les 30 derniers jours.

; } return (
Positives Négatives
{days.map((d) => (
{d.day.slice(5)}{" "} 👍 {d.up}{" "} 👎 {d.down}
))}
{days[0]?.day.slice(5)} {days[days.length - 1]?.day.slice(5)}
); }