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 subjects = data.subjects || []; const periods = data.periods || []; const grades = data.grades || []; // 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> = {}; try { const res = await fetch(`/api/student-submissions/${student.id}`); if (res.ok) { const { submissions } = await res.json(); const subsMap: Record = {}; (submissions || []).forEach((s: any) => { subsMap[s.prova_id] = { 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 => e.subjectId === subject.id && e.periodId === period.id && e.status === 'published'); if (linkedExams.length > 0) { linkedExams.forEach(exam => { const existingGrade = grades.find(g => g.studentId === student.id && g.subjectId === subject.id && g.period === period.id && (g as any).examId === exam.id); periodGrades[exam.id] = existingGrade ? existingGrade.value : ''; }); } else { const existingGrade = grades.find(g => g.studentId === student.id && g.subjectId === subject.id && g.period === period.id && !(g as any).examId); periodGrades['direct'] = existingGrade ? existingGrade.value : ''; } initialGrades[subject.id][period.id] = periodGrades; }); }); setStudentGrades(initialGrades); }; const handleSaveGrades = () => { if (!selectedStudent) return; const newGradesList: Grade[] = [...grades.filter(g => g.studentId !== selectedStudent.id)]; 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)) { newGradesList.push({ id: crypto.randomUUID(), studentId: selectedStudent.id, subjectId, period: periodId, value: numValue, ...(examId !== 'direct' ? { examId } : {}) } as Grade); } }); }); }); updateData({ grades: newGradesList }); dbService.saveData({ ...data, grades: newGradesList }); setSelectedStudent(null); showAlert('Sucesso', '✅ Notas salvas com sucesso!', 'success'); }; const calculateGeneralAverage = () => { let totalSum = 0; let totalCount = 0; Object.entries(studentGrades).forEach(([subjectId, subjectPeriods]) => { const periodSums: number[] = []; Object.values(subjectPeriods).forEach((examValues: any) => { const sum = Object.values(examValues).reduce((a: number, b: any) => a + (b !== '' ? Number(b) : 0), 0); if (Object.values(examValues).some(v => v !== '')) { periodSums.push(sum); } }); if (periodSums.length > 0) { const subjectAvg = periodSums.reduce((a, b) => a + b, 0) / periodSums.length; totalSum += subjectAvg; totalCount++; } }); return totalCount > 0 ? (totalSum / totalCount).toFixed(2) : '0.00'; }; const getStudentGeneralAverage = (studentId: string) => { const studentGradesList = 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 periodSums: Record = {}; subGrades.forEach(g => { periodSums[g.period] = (periodSums[g.period] || 0) + g.value; }); const periodsCount = Object.keys(periodSums).length; if (periodsCount > 0) { const totalSum = Object.values(periodSums).reduce((a, b) => a + b, 0); subjectAverages.push(totalSum / periodsCount); } }); 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 => e.subjectId === subject.id && e.status === 'published'); return (

{subject.name}

{(() => { const linkedExams = (data.exams || []).filter(e => e.subjectId === subject.id && e.status === 'published'); 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 pSums = Object.values(subjectGrades).map((exVals: any) => Object.values(exVals).reduce((a: number, b: any) => a + (b !== '' ? Number(b) : 0), 0)); const valid = pSums.filter(s => s > 0); return valid.length > 0 ? (valid.reduce((a, b) => a + b, 0) / valid.length).toFixed(1) : '0.0'; })()}
{periods.map(period => { const linkedExams = (data.exams || []).filter(e => e.subjectId === subject.id && e.periodId === period.id && e.status === 'published'); const periodGrades = studentGrades[subject.id]?.[period.id] || {}; const periodSum = Object.values(periodGrades).reduce((a: number, b: any) => a + (b !== '' ? Number(b) : 0), 0); return (
Total: {periodSum.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}

)} {studentSubmissions[exam.id] && (
{studentSubmissions[exam.id].acertos} Acertos {studentSubmissions[exam.id].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;