import { useEffect, useState } from 'react'; import { useAuth } from '../context/AuthContext'; import { CreditCard, CalendarCheck, BookOpen, Clock, TrendingUp, AlertTriangle, CalendarClock } from 'lucide-react'; import type { Payment, Attendance, Class, Course, Lesson } from '../types'; import { getLessonTimeStatus, getNormalizedDate, parseLessonDateTime } from '../lib/lessonUtils'; import { useRealTimeDate } from '../hooks/useRealTimeDate'; interface DashboardData { payments: Payment[]; attendance: Attendance[]; lessons: Lesson[]; studentClass: Class | null; course: Course | null; } export default function Dashboard() { const { student, token } = useAuth(); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); // Real-time update every 10s const now = useRealTimeDate(10000); useEffect(() => { const fetchAll = async () => { try { const headers = { Authorization: `Bearer ${token}` }; const [finRes, freqRes, meRes, aulasRes] = await Promise.all([ fetch('/api/portal/financeiro', { headers }), fetch('/api/portal/frequencia', { headers }), fetch('/api/portal/me', { headers }), fetch('/api/portal/aulas', { headers }), ]); const finData = await finRes.json(); const freqData = await freqRes.json(); const meData = await meRes.json(); const aulasData = await aulasRes.json(); setData({ payments: finData.payments || [], attendance: freqData.attendance || [], lessons: aulasData.lessons || [], studentClass: meData.class || null, course: meData.course || null, }); } catch (err) { console.error(err); } finally { setLoading(false); } }; if (token) fetchAll(); }, [token]); if (loading) { return (
{[1, 2, 3, 4].map(i => (
))}
); } const pendingPayments = data?.payments?.filter(p => p.status === 'pending' || p.status === 'overdue') || []; const overduePayments = data?.payments?.filter(p => p.status === 'overdue') || []; const totalPending = pendingPayments.reduce((s, p) => s + (p.amount - (p.discount || 0)), 0); // Synchronized Frequency Calculation (Matches Frequencia.tsx & Manager) let presencesCount = 0; let validLessonsCount = 0; if (data?.lessons && data?.attendance) { const deduplicatedLessons = data.lessons.filter((lesson, index, self) => index === self.findIndex((t) => ( t.date === lesson.date && t.startTime === lesson.startTime )) ); deduplicatedLessons.forEach(lesson => { if (lesson.status === 'cancelled') return; validLessonsCount++; // Construir janela de tempo EXATAMENTE como o Manager faz const lessonStart = new Date(lesson.date + 'T' + (lesson.startTime || '00:00') + ':00'); const lessonEnd = new Date(lesson.date + 'T' + (lesson.endTime || '23:59') + ':00'); const presenceStartWindow = new Date(lessonStart.getTime() - 30 * 60 * 1000); const att = data.attendance.find(a => { if (!a.date || typeof a.date !== 'string') return false; if ((a as any).lessonId === lesson.id) return true; if (a.date === `${lesson.date}T${lesson.startTime || '00:00'}:00`) return true; const recordTime = new Date(a.date); return recordTime >= presenceStartWindow && recordTime <= lessonEnd; }); const isPresent = att && (att.type === 'presence' || (!att.type && !(att as any).isVirtual) || att.verified === true); if (isPresent) presencesCount++; }); } const totalAttendance = data?.attendance?.length || 0; const frequencyPercent = validLessonsCount > 0 ? Math.round((presencesCount / validLessonsCount) * 100) : 0; const nextDue = pendingPayments .sort((a, b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime())[0]; const formatCurrency = (val: number) => val.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }); const formatDate = (d: string) => { if (!d) return '—'; const ms = parseLessonDateTime(d, '12:00', 12); if (isNaN(ms)) return d; return new Date(ms).toLocaleDateString('pt-BR'); }; const greeting = () => { const hour = new Date().getHours(); if (hour < 12) return 'Bom dia'; if (hour < 18) return 'Boa tarde'; return 'Boa noite'; }; const getNext7DaysReplacements = () => { if (!data?.lessons) return []; const today = new Date(); today.setHours(0, 0, 0, 0); const in7Days = new Date(today); in7Days.setDate(in7Days.getDate() + 7); return data.lessons.filter(l => { if (l.status === 'cancelled') return false; const parsedMs = parseLessonDateTime(l.date, '00:00', 0); if (isNaN(parsedMs)) return false; const classDate = new Date(parsedMs); classDate.setHours(0, 0, 0, 0); return l.type === 'reposicao' && classDate >= today && classDate <= in7Days; }); }; const getNextOrCurrentClass = (): { lesson: Lesson; isInProgress: boolean } | null => { if (!data?.lessons || data.lessons.length === 0) return null; const activeLessons = data.lessons.filter(l => l.status !== 'cancelled'); // Normalize "now" date const nowNorm = getNormalizedDate(now.toISOString()); // 1. First, priority: anything strictly "In Progress" RIGHT NOW const currentlyPlaying = activeLessons.find(l => { const { isInProgress } = getLessonTimeStatus(l, now); return isInProgress; }); if (currentlyPlaying) return { lesson: currentlyPlaying, isInProgress: true }; // 2. Secondary: If it's today and not completed yet const today = new Date(now); today.setHours(0, 0, 0, 0); const lessonsRemainingToday = activeLessons .filter(l => { const lessonMs = parseLessonDateTime(l.date, '12:00', 12); const lessonDate = new Date(lessonMs); lessonDate.setHours(0, 0, 0, 0); const { isCompleted } = getLessonTimeStatus(l, now); return lessonDate.getTime() === today.getTime() && !isCompleted; }) .sort((a, b) => { const timeA = parseLessonDateTime(a.date, a.startTime || (a as any).start_time, 0); const timeB = parseLessonDateTime(b.date, b.startTime || (b as any).start_time, 0); return timeA - timeB; }); if (lessonsRemainingToday[0]) { const { isInProgress } = getLessonTimeStatus(lessonsRemainingToday[0], now); return { lesson: lessonsRemainingToday[0], isInProgress }; } // 3. Last resort: Next future lesson const nextFuture = activeLessons .filter(l => { const { isCompleted } = getLessonTimeStatus(l, now); return !isCompleted; }) .sort((a, b) => { const dateA = parseLessonDateTime(a.date, a.startTime || (a as any).start_time, 0); const dateB = parseLessonDateTime(b.date, b.startTime || (b as any).start_time, 0); return dateA - dateB; }); if (nextFuture[0]) { const { isInProgress } = getLessonTimeStatus(nextFuture[0], now); return { lesson: nextFuture[0], isInProgress }; } return null; }; const formatTime = (t?: string) => (t && typeof t === 'string') ? t.substring(0, 5) : ''; const replacements = getNext7DaysReplacements(); const nextClassInfo = getNextOrCurrentClass(); const nextClass = nextClassInfo?.lesson || null; const isCurrentlyInProgress = nextClassInfo?.isInProgress || false; return (
{/* Greeting */}

{greeting()}, {student?.name.split(' ')[0]}! 👋

Aqui está um resumo da sua vida acadêmica.

{replacements.map(rep => (

🗓️ Aviso: Você tem uma reposição agendada para o dia {formatDate(rep.date || '')} {rep.startTime ? ` às ${formatTime(rep.startTime)}` : ''}.

))}
{/* Cards Grid */}
{/* Turma Card */}

MINHA TURMA

{data?.studentClass?.name || 'Não vinculado'}

{data?.course?.name || '—'}

{data?.studentClass?.schedule && (

{data.studentClass.schedule}

)} {data?.studentClass?.teacher && (

Professor Responsável

{data.studentClass.teacher}

)}
{/* Próxima Aula Card */}
{isCurrentlyInProgress ? ( ) : ( )}

{isCurrentlyInProgress ? ( <> AULA EM ANDAMENTO ) : ( 'PRÓXIMA AULA' )}

{nextClass ? ( <>

{nextClass.status === 'rescheduled' ? 'Reagendada' : (nextClass.type === 'reposicao' ? 'Reposição' : (nextClass.type === 'extra' ? 'Aula Extra' : 'Aula Regular'))}

{formatDate(nextClass.date || '')}

{(nextClass.startTime || nextClass.endTime) && (

{formatTime(nextClass.startTime)} {nextClass.endTime && `às ${formatTime(nextClass.endTime)}`}

)} {isCurrentlyInProgress && ( • AULA EM ANDAMENTO )} ) : ( <>

Nenhuma aula

Você não possui próximas aulas

)}
{/* Financeiro Card */}
0 ? 'var(--bg-danger-alpha)' : 'var(--bg-success-alpha)', display: 'flex', alignItems: 'center', justifyContent: 'center', }}> 0 ? 'var(--color-danger)' : 'var(--color-success)'} />

FINANCEIRO

{formatCurrency(totalPending)}

{pendingPayments.length} parcela{pendingPayments.length !== 1 ? 's' : ''} pendente{pendingPayments.length !== 1 ? 's' : ''}

{overduePayments.length > 0 && (

{overduePayments.length} atrasada{overduePayments.length !== 1 ? 's' : ''}

)}
{/* Frequência Card */}
= 75 ? 'var(--bg-accent-alpha)' : 'var(--bg-warning-alpha)', display: 'flex', alignItems: 'center', justifyContent: 'center', }}> = 75 ? 'var(--color-accent-light)' : 'var(--color-warning)'} />

FREQUÊNCIA

{frequencyPercent}%

{presencesCount} presença{presencesCount !== 1 ? 's' : ''} de {validLessonsCount} aula{validLessonsCount !== 1 ? 's' : ''} do curso

= 75 ? 'var(--gradient-primary)' : 'var(--color-warning)', transition: 'width 1s ease', }} />
{/* Próximo Vencimento Card */}

PRÓXIMO VENCIMENTO

{nextDue ? ( <>

{formatCurrency(nextDue.amount - (nextDue.discount || 0))}

Vence em {formatDate(nextDue.dueDate)}

{nextDue.description && (

{nextDue.description}

)} ) : ( <>

Em dia! ✅

Nenhuma parcela pendente

)}
); }