"use client"; // Plan d'étude : formulaire de création (date d'examen, jours disponibles, // minutes par séance, portée des semaines) puis calendrier vertical cochable. import { useCallback, useEffect, useMemo, useState } from "react"; import { CalendarDays, Check, RefreshCw } from "lucide-react"; import { Badge, Button, Card, Label, ProgressBar, Skeleton, Spinner, cn } from "@/components/ui"; import { ErrorBanner, fetchJson, fmtDate, postJson } from "./shared"; type PlanItem = { kind: "review" | "flashcards" | "quiz" | "exam" | "reading"; conceptSlug: string | null; label: string; minutes: number; done?: boolean }; type PlanDay = { date: string; items: PlanItem[] }; type Plan = { id: number; examDate: string; config: { weekdays: number[]; minutesPerSession: number; weeksScope: [number, number] }; days: PlanDay[]; createdAt: string; }; const WEEKDAYS: { value: number; label: string; full: string }[] = [ { value: 1, label: "L", full: "Lundi" }, { value: 2, label: "M", full: "Mardi" }, { value: 3, label: "M", full: "Mercredi" }, { value: 4, label: "J", full: "Jeudi" }, { value: 5, label: "V", full: "Vendredi" }, { value: 6, label: "S", full: "Samedi" }, { value: 0, label: "D", full: "Dimanche" }, ]; const KIND_BADGES: Record = { review: { label: "Réviser", tone: "brand" }, flashcards: { label: "Cartes", tone: "green" }, quiz: { label: "Quiz", tone: "amber" }, exam: { label: "Examen", tone: "gold" }, reading: { label: "Lecture", tone: "neutral" }, }; function dayLabel(iso: string): string { return new Date(iso + "T12:00:00").toLocaleDateString("fr-CA", { weekday: "long", day: "numeric", month: "long" }); } export function PlanApp({ course }: { course: string }) { const [plan, setPlan] = useState(undefined); // undefined = chargement const [error, setError] = useState(null); const [showForm, setShowForm] = useState(false); const [busy, setBusy] = useState(false); // Formulaire const [examDate, setExamDate] = useState(""); const [weekdays, setWeekdays] = useState([1, 3, 6]); const [minutes, setMinutes] = useState(60); const [weekFrom, setWeekFrom] = useState(1); const [weekTo, setWeekTo] = useState(14); const load = useCallback(() => { setError(null); fetchJson<{ plan: Plan | null }>(`/api/learning/${course}/plan`) .then((d) => setPlan(d.plan)) .catch((e) => { setPlan(null); setError(e instanceof Error ? e.message : "Erreur de chargement."); }); }, [course]); useEffect(() => { load(); }, [load]); async function create(e: React.FormEvent) { e.preventDefault(); if (!examDate || weekdays.length === 0) return; setBusy(true); setError(null); try { await postJson(`/api/learning/${course}/plan`, { action: "create", examDate, weekdays, minutesPerSession: minutes, weeksScope: [Math.min(weekFrom, weekTo), Math.max(weekFrom, weekTo)], }); setShowForm(false); load(); } catch (err) { setError(err instanceof Error ? err.message : "Erreur de création du plan."); } finally { setBusy(false); } } async function toggle(date: string, itemIndex: number, done: boolean) { if (!plan) return; // Optimiste setPlan((p) => { if (!p) return p; const days = p.days.map((d) => d.date === date ? { ...d, items: d.items.map((it, i) => (i === itemIndex ? { ...it, done } : it)) } : d ); return { ...p, days }; }); try { await postJson(`/api/learning/${course}/plan`, { action: "toggle", planId: plan.id, date, itemIndex, done }); } catch (err) { setError(err instanceof Error ? err.message : "Erreur d'enregistrement."); load(); } } const todayISO = new Date().toLocaleDateString("fr-CA"); // YYYY-MM-DD local const progress = useMemo(() => { if (!plan) return { done: 0, total: 0 }; let done = 0, total = 0; for (const d of plan.days) for (const it of d.items) { total++; if (it.done) done++; } return { done, total }; }, [plan]); if (plan === undefined) { return (
); } // ---------- Formulaire ---------- if (!plan || showForm) { return (

Plan d'étude

Un calendrier réaliste jusqu'à votre examen : espacement (chaque notion revient plus d'une fois) et entrelacement (plusieurs thèmes par séance), pondéré par vos faiblesses.

setExamDate(e.target.value)} className="w-full h-10 px-3.5 rounded-lg bg-card border border-app text-sm text-fg outline-none focus:border-brand-400 focus:ring-2 focus:ring-brand-500/25" />
{WEEKDAYS.map((d) => { const on = weekdays.includes(d.value); return ( ); })}
{weekdays.length === 0 &&

Choisissez au moins un jour.

}
setMinutes(Number(e.target.value))} className="w-full accent-[#1d64b0]" />
20 min180 min
{error && }
{plan && ( )}
); } // ---------- Calendrier ---------- return (

Plan d'étude

Examen le {fmtDate(plan.examDate + "T12:00:00")} ·{" "} {plan.config.minutesPerSession} min/séance · semaines {plan.config.weeksScope[0]}–{plan.config.weeksScope[1]}

Progression du plan {progress.done}/{progress.total} activités
0 ? "green" : "brand"} />
{error && }
    {plan.days.map((day) => { const isToday = day.date === todayISO; const isPast = day.date < todayISO; const allDone = day.items.every((it) => it.done); return (
  1. {dayLabel(day.date)}

    {isToday && Aujourd'hui} {allDone && Complété} {day.items.reduce((s, it) => s + it.minutes, 0)} min
      {day.items.map((it, i) => { const kb = KIND_BADGES[it.kind] ?? KIND_BADGES.review; return (
    • ); })}
  2. ); })}
); }