import React, { useState } from 'react'; import { SchoolData, Class, Lesson, Notification } from '../types'; import { useDialog } from '../DialogContext'; import { Calendar, Plus, X, AlertCircle, RefreshCw, Send, CheckCircle, Search, Clock, Trash2 } from 'lucide-react'; import { dbService } from '../services/dbService'; interface LessonScheduleProps { classObj: Class; data: SchoolData; updateData: (newData: Partial) => void; onClose: () => void; } const LessonSchedule: React.FC = ({ classObj, data, updateData, onClose }) => { const { showAlert, showConfirm } = useDialog(); const [dbClasses, setDbClasses] = useState(data?.classes || []); const [dbCourses, setDbCourses] = useState(data?.courses || []); React.useEffect(() => { Promise.all([ fetch('/api/turmas').catch(() => ({ ok: false, json: async () => ({}) })), fetch('/api/cursos').catch(() => ({ ok: false, json: async () => ({}) })), ]).then(async (responses) => { const [resT, resC] = responses; if (resT && resT.ok) { const jsonT = await resT.json(); if (jsonT.turmas) setDbClasses(jsonT.turmas.map((t: any) => ({ id: t.id, name: t.nome, courseId: t.curso_id, maxStudents: Number(t.max_alunos || 0) }))); } if (resC && resC.ok) { const jsonC = await resC.json(); if (jsonC.cursos) setDbCourses(jsonC.cursos.map((c: any) => ({ id: c.id, name: c.nome, monthlyFee: Number(c.mensalidade || 0), registrationFee: Number(c.taxa_matricula || 0) }))); } }).catch(console.error); }, []); const [showGenerateModal, setShowGenerateModal] = useState(false); const [showLessonDetail, setShowLessonDetail] = useState(null); const [isClosing, setIsClosing] = useState(false); // Form states for generation const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [dayOfWeek, setDayOfWeek] = useState('1'); const [startTime, setStartTime] = useState(''); const [endTime, setEndTime] = useState(''); const [extraCount, setExtraCount] = useState(''); React.useEffect(() => { if (extraCount && startDate && dayOfWeek) { let current = new Date(startDate + 'T12:00:00Z'); const day = parseInt(dayOfWeek, 10); while (current.getUTCDay() !== day) { current.setUTCDate(current.getUTCDate() + 1); } current.setUTCDate(current.getUTCDate() + (7 * (Number(extraCount) - 1))); setEndDate(current.toISOString().split('T')[0]); } }, [extraCount, startDate, dayOfWeek]); // Form states for cancellation const [cancelReason, setCancelReason] = useState(''); const [wantReplacement, setWantReplacement] = useState(false); const [replacementDate, setReplacementDate] = useState(''); const [replacementStartTime, setReplacementStartTime] = useState(''); const [replacementEndTime, setReplacementEndTime] = useState(''); const checkCollision = (date: string, start: string, end: string, ignoreLessonId?: string) => { return (data.lessons || []).find(l => { // Ignore if it's the lesson being replaced (if any) or if it's cancelled if (l.id === ignoreLessonId || l.status === 'cancelled') return false; if (l.date !== date) return false; if (!l.startTime || !l.endTime) return false; // Só dá conflito se for na mesma TURMA ou com o mesmo PROFESSOR const isSameClass = l.classId === classObj.id; const otherClass = dbClasses.find(c => c.id === l.classId); const isSameTeacher = otherClass && classObj.teacher && otherClass.teacher === classObj.teacher; if (!isSameClass && !isSameTeacher) return false; // Regra: NovoInicio < HorarioFimExistente AND NovoFim > HorarioInicioExistente return (start < l.endTime) && (end > l.startTime); }); }; const classLessons = (data.lessons || []) .filter(l => l.classId === classObj.id) .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); const handleGenerateLessons = () => { if (!startDate || !endDate || !dayOfWeek || !startTime || !endTime) { showAlert('Atenção', 'Preencha todos os campos, incluindo horários de início e término.', 'warning'); return; } if (startTime >= endTime) { showAlert('Atenção', 'O horário de término deve ser maior que o de início.', 'warning'); return; } // Bloqueio de retroatividade removido conforme solicitado. const start = new Date(startDate); const end = new Date(endDate); const day = parseInt(dayOfWeek, 10); const newLessons: Lesson[] = []; const ignoredDates: string[] = []; // Increment date until finding the exact day let current = new Date(start); while (current.getUTCDay() !== day) { current.setUTCDate(current.getUTCDate() + 1); } while (current <= end) { const dateStr = current.toISOString().split('T')[0]; // Validação de Choque de Horários if (checkCollision(dateStr, startTime, endTime)) { ignoredDates.push(new Date(dateStr + 'T12:00:00Z').toLocaleDateString('pt-BR')); } else { newLessons.push({ id: crypto.randomUUID(), classId: classObj.id, date: dateStr, startTime, endTime, status: 'scheduled', type: 'extra' }); } current.setUTCDate(current.getUTCDate() + 7); // advance one week } if (newLessons.length === 0 && ignoredDates.length === 0) { showAlert('Atenção', 'Nenhuma data encontrada nesse período para o dia da semana selecionado.', 'warning'); return; } if (newLessons.length === 0 && ignoredDates.length > 0) { showAlert('⚠️ Choque de Horários!', `Nenhuma aula gerada. Todas as datas pretendidas deram choque com horários existentes: ${ignoredDates.join(', ')}`, 'warning'); return; } const updatedLessons = [...(data.lessons || []), ...newLessons]; // Notificar alunos sobre novas aulas extras geradas const datesList = newLessons.map(l => new Date(l.date + 'T12:00:00Z').toLocaleDateString('pt-BR')).join(', '); const notifMsg = `Novas aulas extras foram agendadas para os dias: ${datesList} (${startTime} às ${endTime}).`; const waMsg = `📅 *Novas Aulas Extras Agendadas!*\n\nOlá, {nome}!\nInformamos que foram agendadas novas aulas extras para a turma *${classObj.name}*.\n\n*Datas:* ${datesList}\n*Horário:* ${startTime} às ${endTime}\n\nAguardamos você!`; const newNotifs = notifyLessonAction('Aulas Extras Agendadas', notifMsg, waMsg); const updatedNotifications = [...(data.notifications || []), ...newNotifs]; updateData({ lessons: updatedLessons, notifications: updatedNotifications }); dbService.saveData({ ...data, lessons: updatedLessons, notifications: updatedNotifications }); setShowGenerateModal(false); if (ignoredDates.length > 0) { showAlert('Aviso de Agendamento Parcial', `Aulas geradas, porém os dias ${ignoredDates.join(', ')} foram ignorados devido a choque de horário no mesmo intervalo (⚠️ Choque de Horários!).`, 'warning'); } else { showAlert('Sucesso', `${newLessons.length} aulas extras geradas e alunos notificados!`, 'success'); } }; const notifyLessonAction = (title: string, notificationMessage: string, waMessage: string) => { const students = data.students.filter(s => s.status === 'active' && s.classId === classObj.id); // Notificações Portal do Aluno const newNotifs: Notification[] = students.map(s => ({ id: crypto.randomUUID(), studentId: s.id, title, message: notificationMessage, read: false, createdAt: new Date().toISOString() })); // Mensagens WhatsApp try { const payloadAlunos = students.flatMap(student => { const birthDateStr = student.birthDate || ''; let age = 18; if (birthDateStr && birthDateStr.includes('-')) { const [year, month, day] = birthDateStr.split('-').map(Number); const birthDate = new Date(year, month - 1, day); const today = new Date(); age = today.getFullYear() - birthDate.getFullYear(); const m = today.getMonth() - birthDate.getMonth(); if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) age--; } const isMinor = age < 18; const targets = []; if (isMinor) { // 1. Envia para o Responsável if (student.guardianPhone?.trim()) { targets.push({ nome: student.guardianName?.trim() || 'Responsável', telefone: student.guardianPhone.trim(), nome_responsavel: student.guardianName, telefone_responsavel: student.guardianPhone }); } // 2. Envia para o Aluno (se ele tiver celular próprio) if (student.phone?.trim()) { targets.push({ nome: student.name || 'Aluno', telefone: student.phone.trim(), nome_responsavel: student.guardianName, telefone_responsavel: student.guardianPhone }); } } else { // 3. Regra Maior de Idade (inalterada) - O foco é o próprio aluno targets.push({ nome: student.name || 'Aluno', telefone: student.phone?.trim() || student.guardianPhone?.trim() || '', nome_responsavel: student.guardianName, telefone_responsavel: student.guardianPhone }); } // Remove possíveis contatos sem telefone para não bugar a API return targets.filter(t => t.telefone); }); fetch('/api/enviar-massa', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ alunos: payloadAlunos, mensagem: waMessage }) }).catch(e => console.warn(e)); } catch (e) { console.warn("Falha silenciosa api enviar-massa", e); } return newNotifs; }; const handleCancelLesson = async (lesson: Lesson) => { if (!cancelReason) { showAlert('Atenção', 'Informe o motivo do cancelamento.', 'warning'); return; } if (wantReplacement) { if (!replacementDate || !replacementStartTime || !replacementEndTime) { showAlert('Atenção', 'Informe a data e os horários da reposição.', 'warning'); return; } if (replacementStartTime >= replacementEndTime) { showAlert('Atenção', 'O horário de término da reposição deve ser maior que o de início.', 'warning'); return; } if (checkCollision(replacementDate, replacementStartTime, replacementEndTime, lesson.id)) { showAlert('⚠️ Choque de Horários!', 'Já existe uma aula marcada para este dia neste intervalo de tempo. Por favor, escolha outro horário.', 'warning'); return; } } setIsClosing(true); const updatedLessons: Lesson[] = (data.lessons || []).map(l => l.id === lesson.id ? { ...l, status: 'cancelled', cancelReason } : l ); let replacementStr = ''; if (wantReplacement && replacementDate) { updatedLessons.push({ id: crypto.randomUUID(), classId: classObj.id, date: replacementDate, startTime: replacementStartTime, endTime: replacementEndTime, status: 'scheduled', type: 'reposicao', originalLessonId: lesson.id }); replacementStr = `\n✅ *Reposição agendada:* ${new Date(replacementDate + 'T12:00:00Z').toLocaleDateString('pt-BR')}`; } const lessonDateStr = new Date(lesson.date + 'T12:00:00Z').toLocaleDateString('pt-BR'); const notifMsg = `A aula do dia ${lessonDateStr} foi cancelada. Motivo: ${cancelReason}. ${wantReplacement ? `Uma reposição foi agendada para o dia ${new Date(replacementDate + 'T12:00:00Z').toLocaleDateString('pt-BR')}.` : ''}`; const waMsg = `🚨 *Aviso Importante: Aula Cancelada*\n\nOlá, {nome}!\nInformamos que a aula da turma *${classObj.name}* do dia *${lessonDateStr}* foi cancelada.\n\n*Motivo:* ${cancelReason}${replacementStr}\n\nAgradecemos a compreensão.`; const newNotifs = notifyLessonAction('Aula Cancelada', notifMsg, waMsg); const updatedNotifications = [...(data.notifications || []), ...newNotifs]; updateData({ lessons: updatedLessons, notifications: updatedNotifications }); await dbService.saveData({ ...data, lessons: updatedLessons, notifications: updatedNotifications }); setTimeout(() => { setShowLessonDetail(null); setIsClosing(false); setCancelReason(''); setWantReplacement(false); setReplacementDate(''); setReplacementStartTime(''); setReplacementEndTime(''); showAlert('Sucesso', 'Aula cancelada e alunos notificados.', 'success'); }, 400); }; const handleUncancelLesson = async (lesson: Lesson) => { setIsClosing(true); const updatedLessons: Lesson[] = (data.lessons || []).map(l => l.id === lesson.id ? { ...l, status: 'scheduled', cancelReason: undefined } : l ); updateData({ lessons: updatedLessons }); await dbService.saveData({ ...data, lessons: updatedLessons }); setTimeout(() => { setShowLessonDetail(null); setIsClosing(false); showAlert('Sucesso', 'Aula reativada com sucesso.', 'success'); }, 400); }; const handleRescheduleLesson = async (lesson: Lesson) => { if (!replacementDate || !replacementStartTime || !replacementEndTime) { showAlert('Atenção', 'Informe nova data e horários.', 'warning'); return; } if (replacementStartTime >= replacementEndTime) { showAlert('Atenção', 'Horário de término deve ser maior que o de início.', 'warning'); return; } if (checkCollision(replacementDate, replacementStartTime, replacementEndTime, lesson.id)) { showAlert('⚠️ Choque de Horários!', 'Já existe uma aula marcada para este intervalo de tempo.', 'warning'); return; } if (!cancelReason) { showAlert('Atenção', 'Informe o motivo do reagendamento.', 'warning'); return; } setIsClosing(true); const updatedLessons: Lesson[] = (data.lessons || []).map(l => l.id === lesson.id ? { ...l, date: replacementDate, startTime: replacementStartTime, endTime: replacementEndTime, status: 'rescheduled', type: l.type, cancelReason: undefined } : l ); // Remove qualquer registro de falta (absence) gerado automaticamente para esta aula (Regra 21) const updatedAttendance = (data.attendance || []).filter( a => !(a.lessonId === lesson.id && a.type === 'absence') ); const oldDateStr = new Date(lesson.date + 'T12:00:00Z').toLocaleDateString('pt-BR'); const newDateStr = new Date(replacementDate + 'T12:00:00Z').toLocaleDateString('pt-BR'); const notifMsg = `A aula do dia ${oldDateStr} foi reagendada para ${newDateStr} (${replacementStartTime} às ${replacementEndTime}). Motivo: ${cancelReason}.`; const waMsg = `📅 *Aviso de Reagendamento*\n\nOlá, {nome}!\nInformamos que a aula da turma *${classObj.name}* originalmente do dia *${oldDateStr}* foi reagendada.\n\n*Nova Data:* ${newDateStr}\n*Novo Horário:* ${replacementStartTime} às ${replacementEndTime}\n*Motivo:* ${cancelReason}\n\nAgradecemos a compreensão!`; const newNotifs = notifyLessonAction('Aula Reagendada', notifMsg, waMsg); const updatedNotifications = [...(data.notifications || []), ...newNotifs]; updateData({ lessons: updatedLessons, notifications: updatedNotifications, attendance: updatedAttendance }); await dbService.saveData({ ...data, lessons: updatedLessons, notifications: updatedNotifications, attendance: updatedAttendance }); setTimeout(() => { setShowLessonDetail(null); setIsClosing(false); setReplacementDate(''); setReplacementStartTime(''); setReplacementEndTime(''); showAlert('Sucesso', 'Aula reagendada com sucesso.', 'success'); }, 400); }; const handleCancelAllFuture = () => { showConfirm('Cancelar Cronograma', 'Deseja realmente cancelar TODAS as aulas futuras não realizadas? Não haverá reposição e a ação atualizará todas para Cancelada.', async () => { const today = new Date().toISOString().split('T')[0]; const updatedLessons = (data.lessons || []).map(l => { if (l.classId === classObj.id && l.status === 'scheduled' && l.date >= today) { return { ...l, status: 'cancelled', cancelReason: 'Cancelamento Geral de Cronograma' }; } return l; }); updateData({ lessons: updatedLessons as Lesson[] }); await dbService.saveData({ ...data, lessons: updatedLessons as Lesson[] }); showAlert('Sucesso', 'Cronograma futuro cancelado.', 'success'); }); }; const handleUncancelAllFuture = () => { showConfirm('Reativar Cronograma', 'Deseja realmente reativar TODAS as aulas futuras que estavam canceladas?', async () => { const today = new Date().toISOString().split('T')[0]; const updatedLessons = (data.lessons || []).map(l => { if (l.classId === classObj.id && l.status === 'cancelled' && l.date >= today) { return { ...l, status: 'scheduled', cancelReason: undefined }; } return l; }); updateData({ lessons: updatedLessons as Lesson[] }); await dbService.saveData({ ...data, lessons: updatedLessons as Lesson[] }); showAlert('Sucesso', 'Cronograma futuro reativado com sucesso.', 'success'); }); }; const handleDeleteAllSchedule = () => { showConfirm('Excluir Cronograma Completo', '⚠️ Tem certeza? Isso removerá TODAS as aulas desta turma permanentemente (agendadas, canceladas e reposições). Esta ação NÃO pode ser desfeita.', async () => { const updatedLessons = (data.lessons || []).filter(l => l.classId !== classObj.id); updateData({ lessons: updatedLessons }); await dbService.saveData({ ...data, lessons: updatedLessons }); showAlert('Sucesso', 'Cronograma completo excluído.', 'success'); }); }; const closeLessonDetail = () => { setIsClosing(true); setTimeout(() => { setShowLessonDetail(null); setIsClosing(false); setCancelReason(''); setWantReplacement(false); setReplacementDate(''); setReplacementStartTime(''); setReplacementEndTime(''); }, 400); }; return (
{/* Header */}

Cronograma de Aulas

Turma: {classObj.name}

{/* Lesson Stats Bar */} {classLessons.length > 0 && (() => { const now = new Date(); const totalLessons = classLessons.length; const completedLessons = classLessons.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 = classLessons.filter(l => l.status === 'cancelled').length; const remainingLessons = totalLessons - completedLessons - cancelledLessons; const progressPercent = totalLessons > 0 ? Math.round((completedLessons / totalLessons) * 100) : 0; return (
{totalLessons} Total {completedLessons} Concluídas {remainingLessons} Restantes {cancelledLessons > 0 && ( {cancelledLessons} Canceladas )}
{progressPercent}%
); })()} {/* Content */}
{classLessons.length === 0 ? (

Nenhuma aula gerada ainda.

Clique em "Gerar Aulas do Ano" para preencher o cronograma.

) : (
{classLessons.map(lesson => { const dateObj = new Date(lesson.date); const displayDate = new Date(dateObj.getTime() + dateObj.getTimezoneOffset() * 60000); const now = new Date(); const [startH, startM] = (lesson.startTime || "00:00").split(':').map(Number); const [endH, endM] = (lesson.endTime || "23:59").split(':').map(Number); const lessonStart = new Date(displayDate); lessonStart.setHours(startH, startM, 0); const lessonEnd = new Date(displayDate); lessonEnd.setHours(endH, endM, 0); const isCancelled = lesson.status === 'cancelled'; const isRescheduled = lesson.status === 'rescheduled'; const isCompletedStatus = lesson.status === 'completed' || (now > lessonEnd && !isCancelled); const isInProgress = !isCancelled && now >= lessonStart && now <= lessonEnd; const isReposicao = lesson.type === 'reposicao'; const isExtra = lesson.type === 'extra'; const isPast = lessonEnd < now && !isInProgress; return (
setShowLessonDetail(lesson)} className={`p-4 rounded-xl border-2 cursor-pointer transition-all hover:scale-105 ${ isCancelled ? 'bg-red-50 border-red-200 opacity-80' : isInProgress ? 'bg-indigo-50 border-indigo-400 shadow-indigo-100 shadow-lg' : isPast || isCompletedStatus ? 'bg-slate-50 border-slate-200 opacity-60 grayscale-[0.3]' : isRescheduled ? 'bg-orange-50 border-orange-300 shadow-sm' : isExtra ? 'bg-purple-50 border-purple-200' : isReposicao ? 'bg-emerald-100 border-emerald-300' : 'bg-emerald-50 border-emerald-100 hover:border-emerald-300' }`} >

{displayDate.getDate().toString().padStart(2, '0')}

{displayDate.toLocaleString('pt-BR', { month: 'short' })} {displayDate.getFullYear()}

{lesson.startTime && lesson.endTime && (

{lesson.startTime} - {lesson.endTime}

)}
{isCancelled && ( Cancelada )} {isInProgress && ( Em andamento )} {isCompletedStatus && !isCancelled && ( Concluída )} {isRescheduled && !isCancelled && !isReposicao && !isInProgress && !isCompletedStatus && ( Reagendada )} {isExtra && !isCancelled && ( Aula Extra )} {isReposicao && !isCancelled && ( Reposição )}
); })}
)}
{/* Generate Lessons Modal */} {showGenerateModal && (

Adicionar Aula Extra

setExtraCount(parseInt(e.target.value) || '')} />
setStartDate(e.target.value)} />
setEndDate(e.target.value)} />
setStartTime(e.target.value)} />
setEndTime(e.target.value)} />
)} {/* Lesson Details & Cancellation Modal */} {showLessonDetail && (

Detalhes da Aula

Data Agendada

{new Date(showLessonDetail.date + 'T12:00:00Z').toLocaleDateString('pt-BR')}

{showLessonDetail.startTime && showLessonDetail.endTime && (

{showLessonDetail.startTime} às {showLessonDetail.endTime}

)} {!showLessonDetail.startTime &&
} {showLessonDetail.status === 'cancelled' ? (
Aula Cancelada

Motivo: {showLessonDetail.cancelReason}

{!wantReplacement ? (
) : (
setReplacementDate(e.target.value)} />
setReplacementStartTime(e.target.value)} />
setReplacementEndTime(e.target.value)} />