From ff5d08ed726a43c4d03f3c9a8f6bc4488d94d3c8 Mon Sep 17 00:00:00 2001 From: sebvtl728 Date: Mon, 22 Jun 2026 10:45:58 +0200 Subject: [PATCH] refactor: remove Bilans component and related styles to clean up project structure --- .DS_Store | Bin 10244 -> 10244 bytes frontend/src/components/Admin/Bilans.css | 610 ----------- frontend/src/components/Admin/Bilans.jsx | 1188 ---------------------- frontend/src/components/Pages/Tools.jsx | 9 +- frontend/src/main.jsx | 2 - frontend/stats.html | 2 +- 6 files changed, 2 insertions(+), 1809 deletions(-) delete mode 100644 frontend/src/components/Admin/Bilans.css delete mode 100644 frontend/src/components/Admin/Bilans.jsx diff --git a/.DS_Store b/.DS_Store index 714bdfdb6f96326629a0efec1536bb06f5b32e97..a45131f6c1ede4a7dfd73daba07e04fdd77d7d1f 100644 GIT binary patch delta 53 zcmV-50LuS_P=rvBPXQ9KP`eKS5|a!Ndy^Ltkq0$6EFd*CHIuIq_p@FTsSL4jNCC44 LAo>KeNfrYG$>9*o delta 377 zcmZn(XbG6$&nUGqU^hRb)MOrk>UwqtJ%%!dVuoafe1;T;)SPs~;N<+=0tPT()CN*$ zDsuB(T#|C~lYlZDaTT8g6DA*X#HK2R2vr3cJjm8f{wOFmd4@m(yRLzbf|;SwWO>2w z^*juD5T`N3GvtC { - const defs = allDefaults[cpKey] || {}; - let idx = 1; - const items = []; - ["Performance", "Competence", "Connaissance"].forEach((bucket) => { - (defs[bucket] || []).forEach((label) => { - items.push({ - id: `${cpKey}-${bucket}-${idx++}`, - label, - checked: false, - level: "ec", - bucket, - }); - }); - }); - return items; -}; - -const Bilan = () => { - const [currentCp, setCurrentCp] = useState("CP1"); - const [items, setItems] = useState(() => buildItemsFromDefaults("CP1")); - const [firstName, setFirstName] = useState(""); - const [lastName, setLastName] = useState(""); - const [newCriterion, setNewCriterion] = useState(""); - const [aiNotes, setAiNotes] = useState(""); - const [aiTone, setAiTone] = useState("neutre"); - const [aiPrepend, setAiPrepend] = useState(false); - const [openaiKey, setOpenaiKey] = useState(""); - const [openaiModel, setOpenaiModel] = useState("gpt-4o-mini"); - const [observation, setObservation] = useState(""); - const [status, setStatus] = useState(null); - const [isAiLoading, setIsAiLoading] = useState(false); - const [dragOverBucket, setDragOverBucket] = useState(null); - const draggingIdRef = useRef(null); - const navigate = useNavigate(); - const token = getToken(); - - useEffect(() => { - if (!token) { - navigate("/admin/login"); - } - }, [token, navigate]); - - useEffect(() => { - setItems(buildItemsFromDefaults(currentCp)); - }, [currentCp]); - - const bucketOrder = useMemo(() => { - const base = ["Performance", "Competence", "Connaissance"]; - return items.some((it) => it.bucket === "Unassigned") - ? ["Unassigned", ...base] - : base; - }, [items]); - - const anySelected = useMemo( - () => items.some((item) => item.checked), - [items] - ); - - const showStatus = useCallback((message, type = "info", withProgress = false) => { - setStatus({ - id: Date.now(), - message, - type, - withProgress, - }); - }, []); - - useEffect(() => { - if (!status || status.withProgress) return; - const timeout = setTimeout( - () => setStatus(null), - status.type === "info" ? 3000 : 5000 - ); - return () => clearTimeout(timeout); - }, [status]); - - const updateItem = useCallback((id, updater) => { - setItems((prev) => - prev.map((item) => (item.id === id ? { ...item, ...updater } : item)) - ); - }, []); - - const handleToggleAll = useCallback((checked) => { - setItems((prev) => prev.map((item) => ({ ...item, checked }))); - }, []); - - const handleAddCriterion = useCallback(() => { - const label = newCriterion.trim(); - if (label.length < 3) return; - setItems((prev) => [ - ...prev, - { - id: `custom-${Date.now()}`, - label, - checked: false, - level: "ec", - bucket: "Unassigned", - source: "Personnalisé", - }, - ]); - setNewCriterion(""); - }, [newCriterion]); - - const handleRemoveCriterion = useCallback((id) => { - setItems((prev) => prev.filter((item) => item.id !== id)); - }, []); - - const handleDragStart = useCallback((id) => { - draggingIdRef.current = id; - }, []); - - const handleDragEnd = useCallback(() => { - draggingIdRef.current = null; - setDragOverBucket(null); - }, []); - - const handleBucketDragOver = useCallback((bucketKey, event) => { - event.preventDefault(); - if (dragOverBucket !== bucketKey) { - setDragOverBucket(bucketKey); - } - }, [dragOverBucket]); - - const handleBucketDrop = useCallback((bucketKey, event) => { - event.preventDefault(); - setDragOverBucket(null); - const id = draggingIdRef.current; - if (!id) return; - setItems((prev) => - prev.map((item) => - item.id === id ? { ...item, bucket: bucketKey } : item - ) - ); - draggingIdRef.current = null; - }, []); - - const handleGenerateObservation = useCallback(() => { - const total = items.length; - const selected = items.filter((item) => item.checked); - const counts = { - na: selected.filter((c) => c.level === "na").length, - ec: selected.filter((c) => c.level === "ec").length, - ac: selected.filter((c) => c.level === "ac").length, - ma: selected.filter((c) => c.level === "ma").length, - }; - const validated = counts.ac + counts.ma; - const percentage = total ? Math.round((validated / total) * 100) : 0; - const buckets = ["Performance", "Competence", "Connaissance"]; - - let text = "## BILAN D'ÉVALUATION DES COMPÉTENCES\n"; - text += "==================================================\n\n"; - if (firstName || lastName) { - text += `Apprenant : ${(firstName + " " + lastName).trim()}\n\n`; - } - - text += "### SYNTHÈSE GLOBALE :\n"; - text += `- Non acquis : ${counts.na}\n`; - text += `- En cours d’acquisition : ${counts.ec}\n`; - text += `- Acquis : ${counts.ac}\n`; - text += `- Maîtrisé : ${counts.ma}\n`; - text += `- Total validés (Acquis + Maîtrisé) : ${validated} / ${total} (${percentage}%)\n\n`; - - const bucketStats = buckets - .map((bucket) => { - const all = items.filter((item) => item.bucket === bucket); - if (!all.length) return null; - const ok = all.filter( - (item) => - item.checked && (item.level === "ac" || item.level === "ma") - ).length; - const pct = Math.round((ok / all.length) * 100); - return { label: BUCKET_LABEL[bucket], ok, total: all.length, pct }; - }) - .filter(Boolean); - - if (bucketStats.length) { - text += "### DÉTAIL PAR CATÉGORIE (taux de validation / total) :\n"; - bucketStats.forEach((stat) => { - text += `- ${stat.label} : ${stat.pct}% (${stat.ok}/${stat.total})\n`; - }); - text += "\n"; - } - - const validatedItems = selected.filter( - (item) => item.level === "ac" || item.level === "ma" - ); - text += "### COMPÉTENCES VALIDÉES :\n"; - if (validatedItems.length) { - BUCKETS.forEach((bucket) => { - const subset = validatedItems.filter((item) => item.bucket === bucket); - if (!subset.length) return; - text += `• ${BUCKET_LABEL[bucket]} :\n`; - subset.forEach((item) => { - text += ` - ${item.label} (${LEVEL_LABEL[item.level]})\n`; - }); - }); - } else { - text += "Aucune compétence validée pour l’instant.\n"; - } - text += "\n"; - - const toImprove = selected.filter( - (item) => item.level === "na" || item.level === "ec" - ); - if (toImprove.length) { - text += "### COMPÉTENCES À RENFORCER :\n"; - BUCKETS.forEach((bucket) => { - const subset = toImprove.filter((item) => item.bucket === bucket); - if (!subset.length) return; - text += `• ${BUCKET_LABEL[bucket]} :\n`; - subset.forEach((item) => { - text += ` - ${item.label} — ${LEVEL_LABEL[item.level]}\n`; - }); - }); - text += "\n"; - } - - text += "## RECOMMANDATIONS :\n"; - if (counts.ma > counts.ac && counts.ma > counts.ec && counts.ma > counts.na) { - text += "- Proposer des projets plus complexes pour entretenir la motivation.\n"; - text += "- Donner plus d’autonomie et de responsabilités.\n"; - } - if (counts.ac >= counts.ma && counts.ac >= counts.ec) { - text += "- Varier les exercices pour transformer les acquis en maîtrise.\n"; - text += "- Introduire progressivement des notions avancées.\n"; - } - if (counts.ec > counts.ac && counts.ec > counts.ma) { - text += "- Renforcer la pratique guidée pour consolider les acquis fragiles.\n"; - text += "- Planifier des révisions régulières sur les notions encore instables.\n"; - } - if (counts.na > 0) { - text += "- Revoir en priorité les notions non acquises avec un accompagnement rapproché.\n"; - text += "- Travailler avec des exercices simples et progressifs pour poser les bases.\n"; - } - if (validated === 0) { - text += "- Mettre en place un plan de formation renforcé avec objectifs courts et progressifs.\n"; - text += "- Prévoir un suivi rapproché et des feedbacks fréquents.\n"; - } - text += "\n"; - - text += "### INFORMATIONS COMPLÉMENTAIRES :\n"; - text += `- Date d'évaluation : ${new Date().toLocaleDateString("fr-FR")}\n`; - text += `- Nombre total de critères : ${total}\n`; - text += `- Sélectionnés : ${selected.length} | Validés (Acquis/Maîtrisé) : ${validated} | À renforcer : ${toImprove.length}\n`; - - if (aiNotes.trim()) { - text += `\nNOTE FORMATEUR :\n${aiNotes.trim()}\n`; - } - - setObservation(text); - showStatus("Observation générée.", "success"); - }, [aiNotes, firstName, items, lastName, showStatus]); - - const insertAISummaryIntoObservation = useCallback( - (text) => { - setObservation((prev) => - aiPrepend - ? `${text}\n\n${prev || ""}`.trim() - : `${prev || ""}${prev ? "\n\n" : ""}${text}`.trim() - ); - }, - [aiPrepend] - ); - - const buildPayloadForAI = useCallback(() => { - const selected = items.filter((item) => item.checked); - const byBucket = (array, bucket) => - array.filter((item) => item.bucket === bucket).map((item) => item.label); - - const acquis = selected.filter((item) => item.level === "ac"); - const maitrise = selected.filter((item) => item.level === "ma"); - const inProgress = selected.filter((item) => item.level === "ec"); - const notAcquired = selected.filter((item) => item.level === "na"); - - return { - prenom: firstName.trim(), - nom: lastName.trim(), - tone: aiTone, - counts: { - total: items.length, - selected: selected.length, - acquis: acquis.length, - maitrise: maitrise.length, - inProgress: inProgress.length, - notAcquired: notAcquired.length, - }, - performance: { - acquis: byBucket(acquis, "Performance"), - maitrise: byBucket(maitrise, "Performance"), - improve: byBucket([...notAcquired, ...inProgress], "Performance"), - }, - competence: { - acquis: byBucket(acquis, "Competence"), - maitrise: byBucket(maitrise, "Competence"), - improve: byBucket([...notAcquired, ...inProgress], "Competence"), - }, - connaissance: { - acquis: byBucket(acquis, "Connaissance"), - maitrise: byBucket(maitrise, "Connaissance"), - improve: byBucket([...notAcquired, ...inProgress], "Connaissance"), - }, - }; - }, [aiTone, firstName, items, lastName]); - - const generateAISummaryOpenAI = useCallback(async () => { - if (!openaiKey.trim()) { - showStatus("Renseigne ta clé OpenAI (champ sk-...).", "error"); - return; - } - - const payload = buildPayloadForAI(); - if (!payload.counts.selected) { - showStatus("Sélectionne quelques critères avant d'utiliser l'IA.", "error"); - return; - } - - const observationTexte = observation.trim(); - const notesFormateur = aiNotes.trim(); - - setIsAiLoading(true); - showStatus("Génération du résumé via OpenAI…", "info", true); - - const callOpenAI = async (model) => { - const response = await fetch("https://api.openai.com/v1/chat/completions", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${openaiKey.trim()}`, - }, - body: JSON.stringify({ - model, - temperature: 0.6, - messages: [ - { - role: "system", - content: ` -Tu es formateur en développement web. -À partir des données (critères et niveaux), du bilan automatique affiché et des notes éventuelles, rédige une synthèse finale d’environ 8 à 10 phrases. -Contraintes : valoriser les réussites, évoquer les difficultés, reformuler les notes formateur, varier le vocabulaire et les connecteurs, adopter un ton professionnel bienveillant. - `, - }, - { - role: "user", - content: `Données JSON : ${JSON.stringify(payload)} - -Observation affichée : -${observationTexte} - -Notes formateur : -${notesFormateur}`, - }, - ], - }), - }); - if (!response.ok) { - const text = await response.text(); - throw new Error(`HTTP ${response.status} — ${text}`); - } - return response.json(); - }; - - const tried = new Set(); - const attemptOrder = [ - openaiModel, - ...FALLBACK_MODELS.filter((model) => model !== openaiModel), - ]; - - try { - let content = ""; - let usedModel = openaiModel; - for (const model of attemptOrder) { - if (tried.has(model)) continue; - tried.add(model); - try { - const data = await callOpenAI(model); - content = - data?.choices?.[0]?.message?.content || - data?.choices?.[0]?.delta?.content || - ""; - usedModel = model; - if (content) break; - } catch (err) { - if (tried.size === attemptOrder.length) { - throw err; - } - } - } - - if (!content) { - throw new Error("Réponse vide du modèle."); - } - - insertAISummaryIntoObservation( - `## RESUME FORMATEUR\n----------------------------\n${content.trim()}` - ); - showStatus(`Résumé OpenAI inséré (modèle : ${usedModel}).`, "success"); - } catch (error) { - showStatus(`Erreur OpenAI : ${error.message || error}`, "error"); - } finally { - setStatus((prev) => (prev ? { ...prev, withProgress: false } : prev)); - setIsAiLoading(false); - } - }, [ - aiNotes, - buildPayloadForAI, - insertAISummaryIntoObservation, - observation, - openaiKey, - openaiModel, - showStatus, - ]); - - const downloadPDF = useCallback(async () => { - const textRaw = observation.trim(); - if (!textRaw) return; - - try { - showStatus("Préparation du PDF…", "info", true); - const pdfDoc = await PDFDocument.create(); - const A4 = { w: 595.28, h: 841.89 }; - let curr = pdfDoc.addPage([A4.w, A4.h]); - - const font = await pdfDoc.embedFont(StandardFonts.TimesRoman); - const boldFont = await pdfDoc.embedFont(StandardFonts.TimesRomanBold); - const fontSize = 11; - const margin = 40; - const maxWidth = A4.w - margin * 2; - let y = A4.h - 130; - - const logoBytes = await fetch(LOGO_URL).then((res) => res.arrayBuffer()); - const logo = await pdfDoc.embedPng(logoBytes); - const logoDims = logo.scale(0.15); - - const drawHeaderFooter = (page, pageNumber) => { - page.drawImage(logo, { - x: 40, - y: A4.h - 80, - width: logoDims.width, - height: logoDims.height, - }); - - const h1Title = titles[currentCp]?.h1 || ""; - const title = - "Bilan d’évaluation" + (h1Title ? " – " + h1Title : ""); - const textWidth = font.widthOfTextAtSize(title, 16); - - page.drawText(title, { - x: (A4.w - textWidth) / 2, - y: A4.h - 110, - size: 16, - font, - }); - - page.drawText(`Page ${pageNumber}`, { - x: A4.w - 160, - y: 20, - size: 10, - font, - }); - page.drawText(new Date().toLocaleDateString("fr-FR"), { - x: 40, - y: 20, - size: 10, - font, - }); - }; - - const wrap = (line) => { - const words = line.split(/\s+/); - const out = []; - let acc = ""; - for (const word of words) { - const test = acc ? `${acc} ${word}` : word; - const fontToUse = acc.includes("**") ? boldFont : font; - if ( - fontToUse.widthOfTextAtSize(test.replace(/\*\*/g, ""), fontSize) > - maxWidth - ) { - if (acc) out.push(acc); - acc = word; - } else { - acc = test; - } - } - if (acc) out.push(acc); - return out; - }; - - const parseMarkdown = (text) => { - const lines = text.split("\n"); - const parsed = []; - - for (const line of lines) { - if (/^###\s+/.test(line)) { - parsed.push({ type: "h3", text: line.replace(/^###\s+/, "") }); - } else if (/^##\s+/.test(line)) { - parsed.push({ type: "h2", text: line.replace(/^##\s+/, "") }); - } else if (/^#\s+/.test(line)) { - parsed.push({ type: "h1", text: line.replace(/^#\s+/, "") }); - } else if (line.trim() === "") { - parsed.push({ type: "br" }); - } else { - parsed.push({ type: "p", text: line }); - } - } - return parsed; - }; - - const parsed = parseMarkdown(textRaw); - let pageNumber = 1; - drawHeaderFooter(curr, pageNumber); - y -= 40; - - for (const block of parsed) { - if (y < margin + fontSize * 2) { - curr = pdfDoc.addPage([A4.w, A4.h]); - pageNumber++; - drawHeaderFooter(curr, pageNumber); - y = A4.h - 120; - y -= 40; - } - - if (block.type === "h1") { - curr.drawText(block.text, { - x: margin, - y, - size: 18, - font: boldFont, - }); - y -= 28; - continue; - } - if (block.type === "h2") { - curr.drawText(block.text, { - x: margin, - y, - size: 15, - font: boldFont, - }); - y -= 24; - continue; - } - if (block.type === "h3") { - curr.drawText(block.text, { - x: margin, - y, - size: 13, - font: boldFont, - }); - y -= 20; - continue; - } - if (block.type === "br") { - y -= 12; - continue; - } - - const wrappedLines = wrap(block.text); - for (const line of wrappedLines) { - if (y < margin + fontSize * 2) { - curr = pdfDoc.addPage([A4.w, A4.h]); - pageNumber++; - drawHeaderFooter(curr, pageNumber); - y = A4.h - 120; - y -= 40; - } - - let offsetX = margin; - let buffer = ""; - let inBold = false; - - for (let i = 0; i < line.length; i += 1) { - if (line[i] === "*" && line[i + 1] === "*") { - if (buffer) { - const activeFont = inBold ? boldFont : font; - curr.drawText(buffer, { - x: offsetX, - y, - size: fontSize, - font: activeFont, - }); - offsetX += activeFont.widthOfTextAtSize(buffer, fontSize); - buffer = ""; - } - inBold = !inBold; - i += 1; - } else { - buffer += line[i]; - } - } - - if (buffer) { - const activeFont = inBold ? boldFont : font; - curr.drawText(buffer, { - x: offsetX, - y, - size: fontSize, - font: activeFont, - }); - } - - y -= 16; - } - } - - const bytes = await pdfDoc.save(); - const blob = new Blob([bytes], { type: "application/pdf" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - const prenom = firstName.trim() || "Prenom"; - const nom = lastName.trim() || "Nom"; - const titre = titles[currentCp]?.h1 || "Bilan"; - const date = new Date().toISOString().slice(0, 10); - const fileName = `${nom}-${prenom}_${date}_${titre}.pdf`; - - a.href = url; - a.download = fileName; - document.body.appendChild(a); - a.click(); - a.remove(); - URL.revokeObjectURL(url); - showStatus("PDF exporté.", "success"); - } catch (error) { - showStatus(`Erreur PDF : ${error.message || error}`, "error"); - } finally { - setStatus((prev) => (prev ? { ...prev, withProgress: false } : prev)); - } - }, [currentCp, firstName, lastName, observation, showStatus]); - - const currentTitle = titles[currentCp] || { h1: "", h2: "" }; - - return ( -
- Logo watermark -
-
- -
- - -
-
-

{currentTitle.h1}

-

{currentTitle.h2}

-
-
- -
- setFirstName(event.target.value)} - /> - setLastName(event.target.value)} - /> -
- -
- - - setNewCriterion(event.target.value)} - /> - -
- -
- {bucketOrder.map((bucket) => { - const itemsInBucket = items.filter((item) => item.bucket === bucket); - return ( -
-

- {BUCKET_LABEL[bucket]}{" "} - ({itemsInBucket.length}) -

-
handleBucketDragOver(bucket, event)} - onDragLeave={() => - dragOverBucket === bucket && setDragOverBucket(null) - } - onDrop={(event) => handleBucketDrop(bucket, event)} - > - {itemsInBucket.map((item) => ( -
handleDragStart(item.id)} - onDragEnd={handleDragEnd} - > - - updateItem(item.id, { checked: event.target.checked }) - } - /> - -
- Niveau : - {LEVELS.map((level) => ( - - - updateItem(item.id, { level: level.key }) - } - /> - - - ))} -
- -
- ))} -
-
- ); - })} -
- -
- Résumé -
-
- -