import React, { useState } from 'react'; import { SchoolData, Class } from '../types'; import { useDialog } from '../DialogContext'; import { Plus, Edit2, Trash2, X, Clock, User, Book, Printer, RefreshCw, Calendar, Settings } from 'lucide-react'; import { pdfService } from '../services/pdfService'; import LessonSchedule from './LessonSchedule'; import { dbService } from '../services/dbService'; interface ClassesProps { data: SchoolData; updateData: (newData: Partial) => void; onNavigateToClass: (classId: string, studentId?: string) => void; } const Classes: React.FC = ({ data, updateData, onNavigateToClass }) => { const { showAlert, showConfirm } = useDialog(); const [isModalOpen, setIsModalOpen] = useState(false); const [isClosing, setIsClosing] = useState(false); const [editingClass, setEditingClass] = useState(null); const [isGeneratingPDF, setIsGeneratingPDF] = useState(null); const [scheduleClass, setScheduleClass] = useState(null); // For LessonSchedule component const [viewingStudentsClass, setViewingStudentsClass] = useState(null); // For student list modal 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 [classes, setClasses] = useState([]); const [courses, setCourses] = useState([]); // To display names correctly const [employees, setEmployees] = useState([]); // To display names const [isLoadingData, setIsLoadingData] = useState(true); const loadData = async () => { try { setIsLoadingData(true); const [clsRes, crsRes, empRes] = await Promise.all([ fetch('/api/turmas'), fetch('/api/cursos'), fetch('/api/funcionarios') ]); const clsData = await clsRes.json(); const crsData = await crsRes.json(); const empData = await empRes.json(); const mappedClasses = (clsData.turmas || []).map((t: any) => ({ id: t.id, name: t.nome, courseId: t.curso_id, teacher: t.professor, schedule: t.horario, scheduleDay: t.dia_semana, maxStudents: Number(t.max_alunos || 0), startDate: t.data_inicio ? t.data_inicio.substring(0, 10) : '', endDate: t.data_fim ? t.data_fim.substring(0, 10) : '', defaultStartTime: t.horario_inicio_padrao, defaultEndTime: t.horario_fim_padrao })); setClasses(mappedClasses); setCourses(crsData.cursos || []); setEmployees(empData.funcionarios || []); } catch (err) { console.error('Erro ao buscar turmas/cursos/funcionários:', err); showAlert('Erro', 'Falha ao carregar dados do servidor.', 'error'); } finally { setIsLoadingData(false); } }; React.useEffect(() => { loadData(); }, []); const [formData, setFormData] = useState>({ name: '', courseId: '', teacher: '', schedule: '', scheduleDay: '', maxStudents: 15, startDate: '', endDate: '', defaultStartTime: '', defaultEndTime: '' }); const DAY_NAMES = ['Domingo', 'Segunda-feira', 'Terça-feira', 'Quarta-feira', 'Quinta-feira', 'Sexta-feira', 'Sábado']; const [quickTimeClass, setQuickTimeClass] = useState(null); const [quickStartTime, setQuickStartTime] = useState(''); const [quickEndTime, setQuickEndTime] = useState(''); // Auto-calculate end date based on course durationMonths React.useEffect(() => { if (formData.courseId && formData.startDate) { const course = courses.find((c: any) => c.id === formData.courseId); if (course && course.duracao_meses) { const start = new Date(formData.startDate + 'T12:00:00Z'); const end = new Date(start); end.setUTCMonth(end.getUTCMonth() + course.duracao_meses); const endString = end.toISOString().split('T')[0]; setFormData(prev => ({ ...prev, endDate: endString })); } } }, [formData.courseId, formData.startDate, courses]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!formData.name || !formData.courseId || !formData.teacher) { showAlert('Atenção', '⚠️ Por favor, preencha todos os campos obrigatórios.', 'warning'); return; } const todayStr = new Date().toISOString().split('T')[0]; // Removido bloqueio de data retroativa para permitir planejamento histórico const newClassId = editingClass ? editingClass.id : crypto.randomUUID(); const resolvedScheduleName = formData.scheduleDay ? DAY_NAMES[parseInt(formData.scheduleDay)] : formData.schedule; const newClass: Class = { ...formData, id: newClassId, schedule: resolvedScheduleName }; let updatedLessons = [...(data.lessons || [])]; // Gerar cronograma automaticamente if (newClass.startDate && newClass.endDate && newClass.scheduleDay && newClass.defaultStartTime && newClass.defaultEndTime) { let generationStartStr = newClass.startDate; if (editingClass) { // Ao editar, removemos apenas as aulas que coincidem ou são futuras em relação ao ponto de alteração // Mas o sistema agora permite gerar todo o período do curso (mesmo retroativo) se solicitado. updatedLessons = updatedLessons.filter(l => !(l.classId === newClass.id && l.date >= generationStartStr)); } const generatedLessons = []; let currentDate = new Date(generationStartStr + 'T12:00:00Z'); const endObject = new Date(newClass.endDate + 'T12:00:00Z'); const targetDay = parseInt(newClass.scheduleDay); // Avançar até o primeiro dia da semana alvo a partir da data de início (nunca para trás) while (currentDate.getUTCDay() !== targetDay) { currentDate.setUTCDate(currentDate.getUTCDate() + 1); } while (currentDate <= endObject) { const dateString = currentDate.toISOString().split('T')[0]; generatedLessons.push({ id: crypto.randomUUID(), classId: newClass.id, date: dateString, startTime: newClass.defaultStartTime, endTime: newClass.defaultEndTime, status: 'scheduled', type: 'regular' }); currentDate.setUTCDate(currentDate.getUTCDate() + 7); } updatedLessons = [...updatedLessons, ...generatedLessons]; } const payload = { nome: newClass.name, curso_id: newClass.courseId, professor: newClass.teacher, horario: newClass.schedule, dia_semana: newClass.scheduleDay, max_alunos: newClass.maxStudents, data_inicio: newClass.startDate, data_fim: newClass.endDate, horario_inicio_padrao: newClass.defaultStartTime, horario_fim_padrao: newClass.defaultEndTime }; const saveToServer = async () => { try { if (editingClass) { const res = await fetch(`/api/turmas/${editingClass.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!res.ok) throw new Error('Failed to update class'); } else { const res = await fetch('/api/turmas', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...payload, id: newClass.id }) }); if (!res.ok) throw new Error('Failed to create class'); } // Save lessons in both PostgreSQL and JSON for compatibility const classLessons = updatedLessons.filter(l => l.classId === newClass.id); if (classLessons.length > 0) { await fetch('/api/aulas/lote', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ aulas: classLessons }) }).catch(e => console.warn('Erro ao salvar aulas no SQL:', e)); } updateData({ lessons: updatedLessons }); dbService.saveData({ ...data, lessons: updatedLessons }); await loadData(); closeModal(); } catch (err) { showAlert('Erro', 'Falha ao salvar turma no banco.', 'error'); } }; saveToServer(); }; const closeModal = () => { setIsClosing(true); setTimeout(() => { setIsModalOpen(false); setIsClosing(false); setEditingClass(null); setFormData({ name: '', courseId: '', teacher: '', schedule: '', maxStudents: 15 }); }, 400); }; const handleEdit = (cls: Class) => { setEditingClass(cls); setFormData({ ...cls }); setIsModalOpen(true); }; const handleDelete = (id: string) => { showConfirm( 'Excluir Turma', '⚠️ Tem certeza que deseja excluir esta turma? Isso não removerá os alunos, mas eles ficarão sem turma.', async () => { try { const res = await fetch(`/api/turmas/${id}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Failed to delete'); await loadData(); } catch (err) { showAlert('Erro', 'Ocorreu um erro ao deletar a turma.', 'error'); } } ); }; const handleDownloadClassList = async (cls: Class) => { setIsGeneratingPDF(cls.id); try { await pdfService.generateClassListPDF(cls, data); } catch (error) { console.error('Error generating PDF:', error); } finally { setIsGeneratingPDF(null); } }; const handleQuickTimeSave = () => { if (!quickTimeClass || !quickStartTime || !quickEndTime) { showAlert('Atenção', 'Preencha início e término.', 'warning'); return; } if (quickStartTime >= quickEndTime) { showAlert('Atenção', 'Fim deve ser maior que início.', 'warning'); return; } // Save class default times const updatedClass = { ...quickTimeClass, defaultStartTime: quickStartTime, defaultEndTime: quickEndTime }; const updatedClasses = data.classes.map(c => c.id === quickTimeClass.id ? updatedClass : c); // Update all future scheduled lessons for this class const today = new Date().toISOString().split('T')[0]; const updatedLessons = (data.lessons || []).map(l => { if (l.classId === quickTimeClass.id && l.status === 'scheduled' && l.date >= today) { return { ...l, startTime: quickStartTime, endTime: quickEndTime }; } return l; }); updateData({ classes: updatedClasses, lessons: updatedLessons }); dbService.saveData({ ...data, classes: updatedClasses, lessons: updatedLessons }); setQuickTimeClass(null); showAlert('Sucesso', 'Horário alterado para a turma e todas as aulas futuras atualizadas!', 'success'); }; const calculateAge = (birthDate: string) => { if (!birthDate) return null; const today = new Date(); const birth = new Date(birthDate); let age = today.getFullYear() - birth.getFullYear(); const m = today.getMonth() - birth.getMonth(); if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) { age--; } return age; }; const inputClass = "w-full px-4 py-3 bg-white text-black border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all shadow-sm"; return (

Turmas PostgreSQL

Controle de horários e ocupação das salas.

{classes.map(cls => { const studentCount = data.students.filter(s => s.classId === cls.id).length; const occupancyPercent = Math.min(100, (studentCount / (cls.maxStudents || 30)) * 100); const course = courses.find((c: any) => c.id === cls.courseId); const now = new Date(); const clsLessons = (data.lessons || []).filter(l => l.classId === cls.id && l.status !== 'cancelled'); const isOngoing = clsLessons.some(l => { if (!l.startTime || !l.endTime) return false; const lDate = new Date(l.date + 'T12:00:00Z'); if (lDate.getDate() !== now.getDate() || lDate.getMonth() !== now.getMonth() || lDate.getFullYear() !== now.getFullYear()) return false; const [sh, sm] = l.startTime.split(':').map(Number); const lStart = new Date(now); lStart.setHours(sh, sm, 0, 0); const [eh, em] = l.endTime.split(':').map(Number); const lEnd = new Date(now); lEnd.setHours(eh, em, 0, 0); return now >= lStart && now <= lEnd; }); return (

{cls.name}

{isOngoing && ( Em andamento )}
{course?.nome || course?.name || 'Sem Curso Vinculado'} {cls.defaultStartTime && cls.defaultEndTime && (
{cls.defaultStartTime} - {cls.defaultEndTime}
)}

Professor

{cls.teacher}

Dias de Aula

{cls.scheduleDay ? DAY_NAMES[parseInt(cls.scheduleDay)] : cls.schedule} {cls.defaultStartTime && cls.defaultEndTime && ( {cls.defaultStartTime} às {cls.defaultEndTime} )}

{/* Contagem de Aulas */} {(() => { const now = new Date(); const totalLessons = clsLessons.length; const completedLessons = clsLessons.filter(l => { if (l.status === 'cancelled') return false; const lDate = new Date(l.date + 'T12:00:00Z'); if (!l.endTime) return lDate < now; const [eh, em] = l.endTime.split(':').map(Number); const lEnd = new Date(lDate); lEnd.setUTCHours(eh, em, 0, 0); return now > lEnd; }).length; const cancelledLessons = clsLessons.filter(l => l.status === 'cancelled').length; const remainingLessons = totalLessons - completedLessons - cancelledLessons; return totalLessons > 0 ? (
{totalLessons} Total {completedLessons} Concluídas {remainingLessons} Restantes
) : null; })()}
OCUPAÇÃO {studentCount} / {cls.maxStudents}
90 ? 'bg-red-500' : occupancyPercent > 50 ? 'bg-indigo-500' : 'bg-emerald-500' }`} style={{ width: `${occupancyPercent}%` }} />
); })} {classes.length === 0 && (

Nenhuma turma cadastrada ainda.

Vincule um curso a uma nova turma para começar.

)}
{isModalOpen && (
{/* Blue Top Bar */}

{editingClass ? 'Editar Turma' : 'Criar Turma'}

Selecione o curso e horários.

setFormData({...formData, name: e.target.value})} />
setFormData({...formData, startDate: e.target.value})} />
setFormData({...formData, endDate: e.target.value})} />
setFormData({...formData, defaultStartTime: e.target.value})} />
setFormData({...formData, defaultEndTime: e.target.value})} />
setFormData({...formData, maxStudents: parseInt(e.target.value) || 0})} />
)} {/* Lesson Schedule Modal */} {scheduleClass && ( setScheduleClass(null)} /> )} {/* Viewing Students Modal */} {viewingStudentsClass && (

Alunos da Turma

{viewingStudentsClass.name} • {data.students.filter(s => s.classId === viewingStudentsClass.id).length} alunos matriculados

{data.students .filter(s => s.classId === viewingStudentsClass.id) .sort((a,b) => (a.name || '').localeCompare(b.name || '')) .map(student => ( ))} {data.students.filter(s => s.classId === viewingStudentsClass.id).length === 0 && ( )}
Aluno Idade Ação
{student.photo ? ( {student.name} ) : (
)}

{student.name}

{student.enrollmentNumber || 'Sem matrícula'}

{calculateAge(student.birthDate) !== null ? `${calculateAge(student.birthDate)} anos` : '-'}
Nenhum aluno matriculado nesta turma.
)}
); }; export default Classes;