import React, { useState, useEffect, useMemo } from 'react';
import { initializeApp } from 'firebase/app';
import {
getAuth,
signInWithCustomToken,
signInAnonymously,
onAuthStateChanged
} from 'firebase/auth';
import {
getFirestore,
collection,
doc,
setDoc,
onSnapshot,
addDoc,
deleteDoc,
updateDoc
} from 'firebase/firestore';
import {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
LineChart, Line
} from 'recharts';
import {
User,
GraduationCap,
TrendingUp,
CheckCircle2,
AlertCircle,
BookOpen,
Users,
ChevronRight,
ClipboardList,
Plus,
Trash2,
Lock,
LogOut,
Upload,
FileSpreadsheet,
MessageSquare,
Search,
ArrowLeft,
ThumbsUp,
UserPlus
} from 'lucide-react';
// --- Configuration Firebase ---
const firebaseConfig = JSON.parse(__firebase_config);
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);
const appId = typeof __app_id !== 'undefined' ? __app_id : 'pestel-eval-app';
const App = () => {
const [user, setUser] = useState(null);
const [role, setRole] = useState(null);
const [currentStudent, setCurrentStudent] = useState(null);
const [selectedStudentForTeacher, setSelectedStudentForTeacher] = useState(null);
const [students, setStudents] = useState([]);
const [allEvaluations, setAllEvaluations] = useState([]);
const [activeStep, setActiveStep] = useState(0);
const [newStudentName, setNewStudentName] = useState("");
const [newStudentClass, setNewStudentClass] = useState("");
const [loading, setLoading] = useState(true);
const [importing, setImporting] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
// --- Chargement de SheetJS ---
useEffect(() => {
const script = document.createElement('script');
script.src = "https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js";
script.async = true;
document.body.appendChild(script);
return () => { document.body.removeChild(script); };
}, []);
// --- Initialisation Auth ---
useEffect(() => {
const initAuth = async () => {
try {
if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) {
await signInWithCustomToken(auth, __initial_auth_token);
} else {
await signInAnonymously(auth);
}
} catch (err) { console.error("Auth error:", err); }
};
initAuth();
const unsubscribe = onAuthStateChanged(auth, (u) => {
setUser(u);
setLoading(false);
});
return () => unsubscribe();
}, []);
// --- Données ---
useEffect(() => {
if (!user) return;
const unsubStudents = onSnapshot(collection(db, 'artifacts', appId, 'public', 'data', 'students'), (snap) => {
const list = snap.docs.map(doc => ({ id: doc.id, ...doc.data() }));
list.sort((a, b) => (a.class || "").localeCompare(b.class || "") || a.name.localeCompare(b.name));
setStudents(list);
});
const unsubEvals = onSnapshot(collection(db, 'artifacts', appId, 'public', 'data', 'evaluations'), (snap) => {
setAllEvaluations(snap.docs.map(doc => ({ id: doc.id, ...doc.data() })));
});
return () => { unsubStudents(); unsubEvals(); };
}, [user]);
// --- Logique Métier ---
const skills = [
{ id: 'ident', label: 'Identifier les facteurs du macro-environnement' },
{ id: 'expl', label: 'Expliquer les composantes du modèle PESTEL' },
{ id: 'impact', label: 'Analyser les impacts des facteurs sur l\'activité' },
{ id: 'mobil', label: 'Mobiliser PESTEL dans une situation contextualisée' }
];
const phases = [
{ id: 'diagnostique', label: 'Phase Diagnostique', desc: 'Rappel des notions' },
{ id: 'formative', label: 'Phase Formative', desc: 'Exercices guidés' },
{ id: 'sommative', label: 'Phase Sommative', desc: 'Cas pratique autonome' }
];
const handleUpdateEval = async (phaseId, skillId, value) => {
if (!currentStudent || !user) return;
const evalId = `${currentStudent.id}_${phaseId}`;
const evalRef = doc(db, 'artifacts', appId, 'public', 'data', 'evaluations', evalId);
const existing = allEvaluations.find(e => e.id === evalId) || { scores: {}, teacherFeedback: {} };
const newScores = { ...existing.scores, [skillId]: parseInt(value) };
await setDoc(evalRef, {
...existing,
studentId: currentStudent.id,
studentName: currentStudent.name,
studentClass: currentStudent.class || "Non définie",
phaseId,
scores: newScores,
updatedAt: Date.now()
});
};
const handleTeacherFeedback = async (studentId, phaseId, skillId, comment) => {
if (!user) return;
const evalId = `${studentId}_${phaseId}`;
const evalRef = doc(db, 'artifacts', appId, 'public', 'data', 'evaluations', evalId);
const existing = allEvaluations.find(e => e.id === evalId);
const teacherFeedback = existing?.teacherFeedback || {};
teacherFeedback[skillId] = comment;
if (existing) {
await updateDoc(evalRef, { teacherFeedback });
} else {
await setDoc(evalRef, {
studentId,
phaseId,
scores: {},
teacherFeedback,
updatedAt: Date.now()
});
}
};
const handleAddStudent = async () => {
if (!user || !newStudentName.trim()) return;
try {
await addDoc(collection(db, 'artifacts', appId, 'public', 'data', 'students'), {
name: newStudentName.trim(),
class: newStudentClass.trim() || "Non définie",
createdAt: Date.now()
});
setNewStudentName("");
setNewStudentClass("");
setSearchQuery(""); // Réinitialise la recherche pour voir le nouvel élève
} catch (err) {
console.error("Erreur lors de l'ajout manuel:", err);
}
};
const handleDeleteStudent = async (id) => {
if (!user) return;
if (confirm(`Voulez-vous vraiment supprimer cet élève ?`)) {
await deleteDoc(doc(db, 'artifacts', appId, 'public', 'data', 'students', id));
}
};
const handleFileUpload = (e) => {
const file = e.target.files[0];
if (!file || !window.XLSX) return;
setImporting(true);
const reader = new FileReader();
reader.onload = async (evt) => {
const bstr = evt.target.result;
const wb = window.XLSX.read(bstr, { type: 'binary' });
const ws = wb.Sheets[wb.SheetNames[0]];
const data = window.XLSX.utils.sheet_to_json(ws);
for (const row of data) {
const nom = row.Nom || row.nom || "";
const prenom = row.Prénom || row.prenom || "";
const classe = row.Classe || row.classe || "Non définie";
if (nom || prenom) {
await addDoc(collection(db, 'artifacts', appId, 'public', 'data', 'students'), {
name: `${prenom} ${nom}`.trim(),
class: String(classe),
createdAt: Date.now()
});
}
}
setImporting(false);
e.target.value = null;
};
reader.readAsBinaryString(file);
};
const getProgressionForId = (studentId) => {
return phases.map(p => {
const evalData = allEvaluations.find(e => e.studentId === studentId && e.phaseId === p.id);
const scores = evalData ? Object.values(evalData.scores) : [];
const avg = scores.length > 0 ? scores.reduce((a, b) => a + b, 0) / skills.length : 0;
return { name: p.label.split(' ')[1], score: avg };
});
};
const filteredStudents = useMemo(() => {
return students.filter(s =>
s.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
s.class.toLowerCase().includes(searchQuery.toLowerCase())
);
}, [students, searchQuery]);
if (loading) return
Chargement de l'application...
;
// --- LOGIN ---
if (!role) {
return (
Eco-Eval PESTEL
Portail d'auto-évaluation pédagogique
);
}
// --- SÉLECTION ÉLÈVE ---
if (role === 'student' && !currentStudent) {
return (
Bonjour, qui êtes-vous ?
setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-indigo-500"
/>
{filteredStudents.map(s => (
))}
);
}
return (
{role === 'student' ? (
/* --- VUE ÉLÈVE --- */
{phases.map((p, idx) => (
))}
{phases[activeStep].label}
{phases[activeStep].desc}
{skills.map(skill => {
const currentEval = allEvaluations.find(e => e.studentId === currentStudent.id && e.phaseId === phases[activeStep].id);
const value = currentEval?.scores?.[skill.id] || 0;
const feedback = currentEval?.teacherFeedback?.[skill.id];
return (
= 2 ? 'bg-indigo-100 text-indigo-700' : 'bg-slate-100 text-slate-500'
}`}>
{['Non débuté', 'Débutant', 'En progrès', 'Acquis', 'Maîtrisé'][value]}
handleUpdateEval(phases[activeStep].id, skill.id, e.target.value)} className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-indigo-600" />
{feedback && (
Commentaire du professeur :
"{feedback}"
)}
);
})}
Conseils
Surveille les bulles de commentaires oranges dans tes phases d'évaluation pour lire les conseils personnalisés de ton professeur.
) : (
/* --- VUE ENSEIGNANT --- */
{!selectedStudentForTeacher ? (
/* --- LISTE DES ÉLÈVES (PROF) --- */
{/* FORMULAIRE D'AJOUT MANUEL (CORRIGÉ ET DÉPLACÉ) */}
Ajouter un élève manuellement
{filteredStudents.map(s => {
const hasFeedback = allEvaluations.some(e => e.studentId === s.id && e.teacherFeedback && Object.keys(e.teacherFeedback).length > 0);
const isComplete = allEvaluations.some(e => e.studentId === s.id && e.phaseId === 'sommative');
return (
setSelectedStudentForTeacher(s)} className="p-4 bg-slate-50 border border-slate-100 rounded-2xl hover:border-indigo-300 hover:shadow-md transition-all cursor-pointer relative group">
{s.name}
{s.class}
{hasFeedback &&
}
{isComplete ? "Validé" : "En cours"}
);
})}
{filteredStudents.length === 0 && (
Aucun élève ne correspond à votre recherche.
)}
) : (
/* --- DÉTAIL ÉLÈVE + VALIDATION (PROF) --- */
{phases.map(phase => (
{phase.label}
{phase.desc}
{skills.map(skill => {
const evalData = allEvaluations.find(e => e.studentId === selectedStudentForTeacher.id && e.phaseId === phase.id);
const score = evalData?.scores?.[skill.id] || 0;
const feedback = evalData?.teacherFeedback?.[skill.id] || "";
return (
{skill.label}
= 3 ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-500'}`}>
Auto-éval : {score}/4
);
})}
))}
)}
)}
);
};
export default App;