import React, { useState, useEffect, useRef } from 'react'; import { Bell, X, CheckCircle, Trash2, ShieldCheck, FileText, Paperclip, DollarSign, AlertTriangle, Info, TrendingUp, CreditCard, ClipboardList } from 'lucide-react'; import { SchoolData, Notification, View } from '../types'; import { dbService } from '../services/dbService'; interface Props { data: SchoolData; updateData: (newData: Partial) => void; setView: (view: View) => void; onNavigateToStudent?: (studentId: string) => void; } const AdminNotifications: React.FC = ({ data, updateData, setView, onNavigateToStudent }) => { const [isOpen, setIsOpen] = useState(false); const [viewingAttachment, setViewingAttachment] = useState(null); const [notifWithAttachment, setNotifWithAttachment] = useState(null); const [adminNotifs, setAdminNotifs] = useState([]); const prevCountRef = useRef(0); const audioRef = useRef(null); const handleDeleteAttachment = async () => { if (!notifWithAttachment) return; try { const resp = await fetch(`/api/notificacoes/remover-anexo/${notifWithAttachment.id}`, { method: 'PUT' }); if (resp.ok) { setAdminNotifs(prev => prev.map(n => n.id === notifWithAttachment.id ? { ...n, attachment: undefined } : n)); setViewingAttachment(null); setNotifWithAttachment(null); } } catch (e) { console.error('Erro ao excluir anexo:', e); } }; const fetchNotifications = async () => { try { const resp = await fetch('/api/notificacoes/admin'); if (resp.ok) { const d = await resp.json(); setAdminNotifs(d.notifications); } } catch (e) { console.error('Erro ao buscar notificações admin:', e); } }; useEffect(() => { fetchNotifications(); const interval = setInterval(fetchNotifications, 30000); // Polling 30s return () => clearInterval(interval); }, []); const unreadCount = adminNotifs.filter(n => !n.read).length; // Som de notificação quando chega uma nova useEffect(() => { if (unreadCount > prevCountRef.current && prevCountRef.current >= 0) { try { if (!audioRef.current) { const ctx = new (window.AudioContext || (window as any).webkitAudioContext)(); const oscillator = ctx.createOscillator(); const gainNode = ctx.createGain(); oscillator.connect(gainNode); gainNode.connect(ctx.destination); oscillator.type = 'sine'; oscillator.frequency.setValueAtTime(880, ctx.currentTime); oscillator.frequency.setValueAtTime(1100, ctx.currentTime + 0.1); oscillator.frequency.setValueAtTime(880, ctx.currentTime + 0.2); gainNode.gain.setValueAtTime(0.3, ctx.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.4); oscillator.start(ctx.currentTime); oscillator.stop(ctx.currentTime + 0.4); } } catch(e) { console.warn('Som de notificação indisponível', e); } } prevCountRef.current = unreadCount; }, [unreadCount]); const handleAction = (notif: Notification) => { if (!notif.read) handleMarkAsRead(notif.id); const isFinance = notif.title.toLowerCase().includes('pagamento') || notif.title.toLowerCase().includes('cobrança'); const isExam = notif.title.toLowerCase().includes('prova') || notif.title.toLowerCase().includes('atividade'); if (isFinance) { setView(View.Finance); setIsOpen(false); return; } if (isExam) { setView(View.ReportCard); setIsOpen(false); return; } if (notif.title.toLowerCase().includes('justificativa') || notif.message.toLowerCase().includes('justificativa')) { const targetId = (notif as any).fromStudentId || notif.studentId; if (onNavigateToStudent && targetId !== 'admin') { onNavigateToStudent(targetId); } else { setView(View.AttendanceQuery); } setIsOpen(false); } }; const handleMarkAsRead = async (id: string) => { try { const resp = await fetch(`/api/notificacoes/ler/${id}`, { method: 'PUT' }); if (resp.ok) { setAdminNotifs(prev => prev.map(n => n.id === id ? { ...n, read: true } : n)); } } catch (e) { console.error('Erro ao marcar como lida:', e); } }; const handleClearRead = async () => { try { const resp = await fetch('/api/notificacoes/limpar-lidas', { method: 'DELETE' }); if (resp.ok) { setAdminNotifs(prev => prev.filter(n => !n.read)); } } catch (e) { console.error('Erro ao limpar lidas:', e); } }; // Aceitar justificativa diretamente pela notificação const handleAcceptJustification = (notif: Notification) => { // Procura registros de falta pendentes de aceitação const pendingAbsences = (data.attendance || []).filter(a => a.type === 'absence' && a.justification && !a.justificationAccepted ); if (pendingAbsences.length > 0) { // Tenta achar pelo studentId mencionado ou associado à notificação const targetId = (notif as any).fromStudentId; const matchedAbsence = targetId ? pendingAbsences.find(a => a.studentId === targetId) || pendingAbsences[0] : pendingAbsences[0]; const updatedAttendance = (data.attendance || []).map(a => a.id === matchedAbsence.id ? { ...a, justificationAccepted: true } : a ); updateData({ attendance: updatedAttendance }); dbService.saveData({ ...data, attendance: updatedAttendance }); handleMarkAsRead(notif.id); } else { // Se não encontrou pendentes, apenas marca como lida handleMarkAsRead(notif.id); } }; return (
{isOpen && (

Central de Alertas {unreadCount > 0 && {unreadCount}}

{adminNotifs.length === 0 ? (

Nenhuma notificação

Sua caixa de entrada está limpa.

) : (
{adminNotifs.map(notif => { const isJustificativa = notif.title.toLowerCase().includes('justificativa') || notif.message.toLowerCase().includes('justificativa'); let displayMessage = notif.message; let justificationMotive = ''; let attachmentFromMessage = null; let metadata: any = {}; if (notif.message.startsWith('{')) { try { const parsed = JSON.parse(notif.message); displayMessage = parsed.text || parsed.motivo || displayMessage; justificationMotive = parsed.motivo || ''; attachmentFromMessage = parsed.arquivo || parsed.arquivo_base64 || null; } catch(e) {} } if (notif.anexo && notif.anexo.startsWith('{')) { try { metadata = JSON.parse(notif.anexo); } catch(e) {} } const finalAttachment = notif.attachment || attachmentFromMessage; const isFinance = metadata.type === 'finance' || notif.title.toLowerCase().includes('pagamento') || notif.title.toLowerCase().includes('cobrança'); const isExam = metadata.type === 'exam' || notif.title.toLowerCase().includes('prova') || notif.title.toLowerCase().includes('atividade'); // Configuração dinâmica de cores e ícones let icon = ; let colorClass = 'text-indigo-500'; let bgClass = 'bg-indigo-50'; let borderClass = 'border-indigo-100'; if (isFinance) { if (metadata.status === 'paid' || notif.title.includes('Confirmado')) { icon = ; colorClass = 'text-emerald-500'; bgClass = 'bg-emerald-50'; borderClass = 'border-emerald-100'; } else if (metadata.status === 'overdue' || notif.title.includes('Atraso')) { icon = ; colorClass = 'text-red-500'; bgClass = 'bg-red-50'; borderClass = 'border-red-100'; } else { icon = ; colorClass = 'text-blue-500'; bgClass = 'bg-blue-50'; borderClass = 'border-blue-100'; } } else if (isExam) { icon = ; colorClass = 'text-violet-600'; bgClass = 'bg-violet-50'; borderClass = 'border-violet-100'; } else if (isJustificativa) { icon = ; colorClass = 'text-amber-500'; bgClass = 'bg-amber-50'; borderClass = 'border-amber-100'; } return (
handleAction(notif)} className={`p-3 rounded-xl border transition-all cursor-pointer relative overflow-hidden group ${notif.read ? 'bg-slate-50 border-transparent opacity-70' : `bg-white ${borderClass} hover:shadow-md shadow-sm`}`}> {!notif.read &&
}
{icon}

{notif.title}

{new Date(notif.createdAt).toLocaleDateString('pt-BR')}

{displayMessage}

{isJustificativa && justificationMotive && (

Motivo enviado:

"{justificationMotive}"

)} {(!notif.read) && (
{isJustificativa && ( )} {isJustificativa && ( )} {isJustificativa && finalAttachment && !String(finalAttachment).startsWith('{') && ( )}
)}
); })}
)}
)} {viewingAttachment && (

Visualização do Documento

{viewingAttachment.startsWith('data:application/pdf') || viewingAttachment.includes('.pdf') ? (