import React, { useState } from 'react'; import { SchoolData, Class, Student, Subject, Grade, Period } from '../types'; import { dbService } from '../services/dbService'; import { useDialog } from '../DialogContext'; import { FileText, Plus, Trash2, ChevronRight, Save, GraduationCap, BookOpen, User, X, Search, CheckCircle2, AlertCircle, Calendar, Calculator } from 'lucide-react'; interface ReportCardProps { data: SchoolData; updateData: (newData: Partial) => void; } const ReportCard: React.FC = ({ data, updateData }) => { const { showAlert, showConfirm } = useDialog(); const [selectedClass, setSelectedClass] = useState(null); const [selectedStudent, setSelectedStudent] = useState(null); const [newSubjectName, setNewSubjectName] = useState(''); const [newPeriodName, setNewPeriodName] = useState(''); const [searchTerm, setSearchTerm] = useState(''); const [showConfigManager, setShowConfigManager] = useState(false); const [configTab, setConfigTab] = useState<'subjects' | 'periods'>('subjects'); const [studentGrades, setStudentGrades] = useState>>({}); // subjectId -> periodId -> { examId: value } const [studentSubmissions, setStudentSubmissions] = useState>({}); // examId -> { acertos, erros } const [classGrades, setClassGrades] = useState([]); const subjects = data.subjects || []; const periods = data.periods || []; const grades = data.grades || []; // Buscar todas as notas da turma para mostrar médias na lista React.useEffect(() => { if (selectedClass) { const fetchClassGrades = async () => { try { const studentIds = data.students.filter(s => s.classId === selectedClass.id).map(s => s.id); if (studentIds.length === 0) return; const allGrades: Grade[] = []; for (const id of studentIds) { const res = await fetch(`/api/notas/${id}?t=${Date.now()}`); if (res.ok) { const json = await res.json(); (json.notas || []).forEach((n: any) => { allGrades.push({ id: n.id, studentId: n.aluno_id, subjectId: n.disciplina_id, period: n.periodo_id, examId: n.prova_id, value: Number(n.valor) }); }); } } setClassGrades(allGrades); } catch (e) { console.error('Erro ao buscar notas da turma:', e); } }; fetchClassGrades(); } else { setClassGrades([]); } }, [selectedClass, data.students]); // Helper para normalizar URLs de fotos (vacina contra cache antigo) const normalizePhotoUrl = (url?: string) => { if (!url || typeof url !== 'string') return ''; if (url.startsWith('data:image') || url.startsWith('blob:')) return url; if (url.startsWith('/storage/')) return url; try { const match = url.match(/^https?:\/\/[^\/]+\/(.+)$/); if (match) return `/storage/${match[1]}`; } catch (e) { } return url; }; const handleAddSubject = () => { if (!newSubjectName.trim()) { showAlert('Atenção', '⚠️ Por favor, informe o nome da disciplina.', 'warning'); return; } const newSubject: Subject = { id: crypto.randomUUID(), name: newSubjectName.trim() }; const updatedSubjects = [...subjects, newSubject]; updateData({ subjects: updatedSubjects }); dbService.saveData({ ...data, subjects: updatedSubjects }); setNewSubjectName(''); }; const handleAddPeriod = () => { if (!newPeriodName.trim()) { showAlert('Atenção', '⚠️ Por favor, informe o nome do período.', 'warning'); return; } const newPeriod: Period = { id: crypto.randomUUID(), name: newPeriodName.trim() }; const updatedPeriods = [...periods, newPeriod]; updateData({ periods: updatedPeriods }); dbService.saveData({ ...data, periods: updatedPeriods }); setNewPeriodName(''); }; const handleDeleteSubject = (id: string) => { showConfirm( 'Excluir Disciplina', '⚠️ Tem certeza que deseja excluir esta disciplina? Todas as notas vinculadas serão perdidas.', () => { const updatedSubjects = subjects.filter(s => s.id !== id); const updatedGrades = grades.filter(g => g.subjectId !== id); updateData({ subjects: updatedSubjects, grades: updatedGrades }); dbService.saveData({ ...data, subjects: updatedSubjects, grades: updatedGrades }); } ); }; const handleDeletePeriod = (id: string) => { showConfirm( 'Excluir Período', '⚠️ Tem certeza que deseja excluir este período? Todas as notas vinculadas serão perdidas.', () => { const updatedPeriods = periods.filter(p => p.id !== id); const updatedGrades = grades.filter(g => g.period !== id); updateData({ periods: updatedPeriods, grades: updatedGrades }); dbService.saveData({ ...data, periods: updatedPeriods, grades: updatedGrades }); } ); }; const handleOpenStudentGrades = async (student: Student) => { setSelectedStudent(student); const initialGrades: Record> = {}; // Buscar notas do Postgres (com cache busting) let dbNotas: any[] = []; try { const resNotas = await fetch(`/api/notas/${student.id}?t=${new Date().getTime()}`); if (resNotas.ok) { const json = await resNotas.json(); dbNotas = json.notas || []; } } catch(e) { console.error('Error fetching notas:', e); } let subsMap: Record = {}; try { const res = await fetch(`/api/student-submissions/${student.id}?t=${new Date().getTime()}`); if (res.ok) { const { submissions } = await res.json(); (submissions || []).forEach((s: any) => { // Normalização agressiva para garantir o vínculo const pId = String(s.prova_id || '').trim(); if (pId) { subsMap[pId] = { acertos: s.acertos, erros: s.erros }; } }); setStudentSubmissions(subsMap); } } catch(e) { console.error('Error fetching submissions:', e); } subjects.forEach(subject => { initialGrades[subject.id] = {}; periods.forEach(period => { const periodGrades: any = {}; const linkedExams = (data.exams || []).filter(e => String(e.subjectId).trim() === String(subject.id).trim() && String(e.periodId).trim() === String(period.id).trim() && !!subsMap[String(e.id).trim()] ); if (linkedExams.length > 0) { linkedExams.forEach(exam => { const existingGrade = dbNotas.find(g => String(g.disciplina_id).trim() === String(subject.id).trim() && (String(g.periodo_id).trim() === String(period.id).trim() || String(g.periodo_id).trim() === String(period.name).trim()) && String(g.prova_id).trim() === String(exam.id).trim() ); periodGrades[exam.id] = existingGrade ? Number(existingGrade.valor) : ''; }); } else { const existingGrade = dbNotas.find(g => String(g.disciplina_id).trim() === String(subject.id).trim() && (String(g.periodo_id).trim() === String(period.id).trim() || String(g.periodo_id).trim() === String(period.name).trim()) && !g.prova_id ); periodGrades['direct'] = existingGrade ? Number(existingGrade.valor) : ''; } initialGrades[subject.id][period.id] = periodGrades; }); }); setStudentGrades(initialGrades); }; const handleSaveGrades = async () => { if (!selectedStudent) return; const notasPayload: any[] = []; Object.entries(studentGrades).forEach(([subjectId, periodGrades]) => { Object.entries(periodGrades).forEach(([periodId, examValues]) => { Object.entries(examValues).forEach(([examId, value]) => { const numValue = Number(value); if (numValue > 0 || (value !== '' && numValue === 0)) { notasPayload.push({ aluno_id: selectedStudent.id, disciplina_id: subjectId, periodo_id: periodId, prova_id: examId !== 'direct' ? examId : null, valor: numValue }); } }); }); }); try { const res = await fetch('/api/notas', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ notas: notasPayload }) }); if (res.ok) { setSelectedStudent(null); showAlert('Sucesso', '✅ Notas salvas com sucesso no banco de dados!', 'success'); } else { showAlert('Erro', '❌ Falha ao salvar notas.', 'error'); } } catch(e) { console.error(e); showAlert('Erro', '❌ Erro de conexão ao salvar notas.', 'error'); } }; const calculateGeneralAverage = () => { let totalSum = 0; let totalCount = 0; Object.entries(studentGrades).forEach(([subjectId, subjectPeriods]) => { const periodAvgs: number[] = []; Object.values(subjectPeriods).forEach((examValues: any) => { const validValues = Object.values(examValues).filter(v => v !== ''); if (validValues.length > 0) { const sum: number = validValues.reduce((a, b: any) => a + Number(b), 0); periodAvgs.push(sum / validValues.length); } }); if (periodAvgs.length > 0) { const subjectAvg = periodAvgs.reduce((a, b) => a + b, 0) / periodAvgs.length; totalSum += subjectAvg; totalCount++; } }); return totalCount > 0 ? (totalSum / totalCount).toFixed(2) : '0.00'; }; const getStudentGeneralAverage = (studentId: string) => { // Priorizar notas do Postgres (classGrades) sobre o JSON const studentGradesList = classGrades.length > 0 ? classGrades.filter(g => g.studentId === studentId) : grades.filter(g => g.studentId === studentId); if (studentGradesList.length === 0) return '0.00'; const subjectAverages: number[] = []; const subjectsWithGrades = new Set(studentGradesList.map(g => g.subjectId)); subjectsWithGrades.forEach(subId => { const subGrades = studentGradesList.filter(g => g.subjectId === subId); const periodValues: Record = {}; subGrades.forEach(g => { if (!periodValues[g.period]) periodValues[g.period] = []; periodValues[g.period].push(g.value); }); const periodAvgs: number[] = []; Object.values(periodValues).forEach(values => { if (values.length > 0) { const sum = values.reduce((a, b) => a + b, 0); periodAvgs.push(sum / values.length); } }); if (periodAvgs.length > 0) { const totalSum = periodAvgs.reduce((a, b) => a + b, 0); subjectAverages.push(totalSum / periodAvgs.length); } }); if (subjectAverages.length === 0) return '0.00'; const totalSum = subjectAverages.reduce((a, b) => a + b, 0); return (totalSum / subjectAverages.length).toFixed(2); }; const filteredClasses = data.classes.filter(c => (c.name || '').toLowerCase().includes((searchTerm || '').toLowerCase()) ); return (

Boletim Escolar

Gerencie as notas e o desempenho dos alunos.

{showConfigManager ? (

Gerenciar Configurações

{configTab === 'subjects' ? (
setNewSubjectName(e.target.value)} />
{subjects.map(subject => (
{subject.name}
))} {subjects.length === 0 && (
Nenhuma disciplina cadastrada.
)}
) : (
setNewPeriodName(e.target.value)} />
{periods.map(period => (
{period.name}
))} {periods.length === 0 && (
Nenhum período cadastrado.
)}
)}
) : (
{!selectedClass ? ( <>
setSearchTerm(e.target.value)} />
{filteredClasses.map(cls => { const course = data.courses.find(c => c.id === cls.courseId); const studentCount = data.students.filter(s => s.classId === cls.id).length; return (
setSelectedClass(cls)} className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm hover:shadow-xl hover:border-indigo-200 transition-all cursor-pointer group relative overflow-hidden" >

{cls.name}

{course?.name || 'Curso não encontrado'}

{studentCount} Alunos Matriculados
); })}
) : (

{selectedClass.name}

Selecione um aluno para preencher as notas.

{data.students.filter(s => s.classId === selectedClass.id).length} Alunos
{data.students .filter(s => s.classId === selectedClass.id) .sort((a, b) => a.name.localeCompare(b.name)) .map(student => (
{student.photo ? ( {student.name} ) : ( )}
{student.name}
Média Geral = 6 ? 'text-emerald-600' : 'text-red-600'}`}> {getStudentGeneralAverage(student.id)}
))}
)}
)} {/* GRADES MODAL */} {selectedStudent && (

{selectedStudent.name}

Boletim Escolar • {selectedClass?.name}

{subjects.length === 0 || periods.length === 0 ? (

{subjects.length === 0 ? 'Nenhuma disciplina cadastrada.' : 'Nenhum período cadastrado.'} Por favor, complete as configurações primeiro.

) : (
{subjects.map(subject => { // Encontrar provas vinculadas a esta disciplina const linkedExams = (data.exams || []).filter(e => String(e.subjectId).trim() === String(subject.id).trim()); return (

{subject.name}

{(() => { const linkedExams = (data.exams || []).filter(e => String(e.subjectId).trim() === String(subject.id).trim() && !!studentSubmissions[String(e.id).trim()] ); const provasCount = linkedExams.filter(e => (e as any).evaluationType !== 'activity').length; const atividadesCount = linkedExams.filter(e => (e as any).evaluationType === 'activity').length; return ( <> {provasCount > 0 && (
{provasCount} {provasCount === 1 ? 'Prova' : 'Provas'}
)} {atividadesCount > 0 && (
{atividadesCount} {atividadesCount === 1 ? 'Atividade' : 'Atividades'}
)} ); })()}
MÉDIA: {(() => { const subjectGrades = studentGrades[subject.id] || {}; const pAvgs: number[] = []; Object.values(subjectGrades).forEach((exVals: any) => { const validValues = Object.values(exVals).filter(v => v !== ''); if (validValues.length > 0) { pAvgs.push(validValues.reduce((a, b: any) => a + Number(b), 0) / validValues.length); } }); return pAvgs.length > 0 ? (pAvgs.reduce((a, b) => a + b, 0) / pAvgs.length).toFixed(1) : '0.0'; })()}
{periods.map(period => { const linkedExams = (data.exams || []).filter(e => String(e.subjectId).trim() === String(subject.id).trim() && String(e.periodId).trim() === String(period.id).trim() && !!studentSubmissions[String(e.id).trim()] ); const periodGrades = studentGrades[subject.id]?.[period.id] || {}; const validPeriodValues = Object.values(periodGrades).filter(v => v !== ''); const periodAvg: number = validPeriodValues.length > 0 ? validPeriodValues.reduce((a, b: any) => a + Number(b), 0) / validPeriodValues.length : 0; return (
Média: {periodAvg.toFixed(1)}
{linkedExams.length > 0 ? (
{linkedExams.map(exam => { const isActivity = (exam as any).evaluationType === 'activity'; const maxScore = (exam as any).maxScore ?? 10; return (
{isActivity ? 'Atividade' : 'Prova'} {exam.title}
{exam.description && (

{exam.description}

)} {(() => { const stats = studentSubmissions[String(exam.id).trim()]; if (!stats) return null; return (
{stats.acertos} Acertos {stats.erros} Erros
); })()}
Nota (Máx {maxScore}) { let val = parseFloat(e.target.value); if (val > maxScore) val = maxScore; if (val < 0) val = 0; setStudentGrades(prev => ({ ...prev, [subject.id]: { ...prev[subject.id], [period.id]: { ...prev[subject.id]?.[period.id], [exam.id]: isNaN(val) ? '' : val } } })); }} />
) })}
) : (

Nota Direta (Sem avaliação vinculada)

{ let val = parseFloat(e.target.value); if (val > 10) val = 10; if (val < 0) val = 0; setStudentGrades(prev => ({ ...prev, [subject.id]: { ...prev[subject.id], [period.id]: { direct: isNaN(val) ? '' : val } } })); }} />
)}
); })}
); })} {/* General Average Summary */}

Média Geral

Calculada automaticamente com base em todas as disciplinas.

{calculateGeneralAverage()}
)}
)}
); }; export default ReportCard;