Files
clube67_newwhats.local/base-scoreodonto/scoreodonto/components/NotificationCenter.tsx
T
VPS 4 Deploy Agent 5ec6bd6354
continuous-integration/webhook Falha no deploy de clube67_newwhats.local (VPS 4)
chore(ops): migrate clube67 to newwhats.clube67.com directory
2026-05-18 03:26:41 +02:00

165 lines
10 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { Bell, X, Calendar, DollarSign, AlertCircle, UserCheck, MessageSquare, ShieldAlert } from 'lucide-react';
import { HybridBackend } from '../services/backend.ts';
import { Notificacao } from '../types.ts';
export const NotificationCenter: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
const [notificacoes, setNotificacoes] = useState<Notificacao[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
const fetch = async () => {
try {
const data = await HybridBackend.getNotifications();
setNotificacoes(data);
setUnreadCount(data.filter(n => !n.lida).length);
} catch (err) {
console.error("Erro ao carregar notificações");
}
};
useEffect(() => {
fetch();
const interval = setInterval(fetch, 30000); // Polling cada 30s
return () => clearInterval(interval);
}, []);
const toggleOpen = () => setIsOpen(!isOpen);
const markAsRead = async (id: string) => {
await HybridBackend.markNotificationRead(id);
setNotificacoes(prev => prev.map(n => n.id === id ? { ...n, lida: true } : n));
setUnreadCount(prev => Math.max(0, prev - 1));
};
const getIcon = (tipo: string) => {
switch (tipo) {
case 'agenda': return <Calendar size={18} className="text-blue-500" />;
case 'financeiro': return <DollarSign size={18} className="text-emerald-500" />;
case 'lead': return <UserCheck size={18} className="text-amber-500" />;
case 'sistema': return <ShieldAlert size={18} className="text-red-500" />;
default: return <MessageSquare size={18} className="text-gray-500" />;
}
};
const getTypeLabel = (tipo: string) => {
switch (tipo) {
case 'agenda': return 'Agenda';
case 'financeiro': return 'Financeiro';
case 'lead': return 'Novo Lead';
case 'sistema': return 'Sistema';
default: return 'Geral';
}
};
return (
<div className="relative z-[1002]">
<div className="relative">
<button
onClick={toggleOpen}
className="group relative w-11 h-11 flex items-center justify-center rounded-xl bg-white/80 backdrop-blur-md border border-white/20 shadow-[0_8px_32px_rgba(0,0,0,0.06)] hover:shadow-[0_8px_32px_rgba(59,130,246,0.15)] hover:border-blue-400/50 transition-all duration-300"
>
<Bell size={22} className="text-slate-600 group-hover:text-blue-600 transition-colors" />
{unreadCount > 0 && (
<span className="absolute -top-1.5 -right-1.5 flex h-5 w-5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-5 w-5 bg-red-500 text-[10px] font-bold text-white items-center justify-center shadow-lg">
{unreadCount}
</span>
</span>
)}
</button>
{isOpen && (
<>
<div className="fixed inset-0" onClick={toggleOpen} />
<div className="absolute right-0 mt-4 w-[380px] bg-white/95 backdrop-blur-lg rounded-2xl shadow-[0_20px_50px_rgba(0,0,0,0.15)] border border-white/20 overflow-hidden animate-in fade-in zoom-in-95 slide-in-from-top-4 duration-300 transform origin-top-right">
<div className="px-6 py-5 bg-gradient-to-r from-slate-50 to-white border-b border-slate-100 flex justify-between items-center">
<div>
<h3 className="font-bold text-slate-800 text-lg tracking-tight">Centro de Notificações</h3>
<p className="text-[11px] text-slate-400 font-medium uppercase tracking-wider">Você tem {unreadCount} novas mensagens</p>
</div>
<button
onClick={toggleOpen}
className="p-2 hover:bg-slate-100 rounded-full transition-colors"
>
<X size={18} className="text-slate-400" />
</button>
</div>
<div className="max-h-[450px] overflow-y-auto custom-scrollbar bg-slate-50/30">
{notificacoes.length === 0 ? (
<div className="py-20 flex flex-col items-center justify-center text-center px-8">
<div className="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center mb-4">
<Bell size={32} className="text-slate-300" />
</div>
<p className="text-slate-500 font-medium">Tudo limpo por aqui!</p>
<p className="text-xs text-slate-400 mt-1">Não notificações para mostrar no momento.</p>
</div>
) : (
<div className="divide-y divide-slate-100">
{notificacoes.map(notif => (
<div
key={notif.id}
className={`group p-5 hover:bg-white transition-all duration-200 cursor-default ${!notif.lida ? 'bg-blue-50/40 relative' : ''}`}
>
{!notif.lida && (
<div className="absolute left-0 top-0 bottom-0 w-1 bg-blue-500 rounded-r-full" />
)}
<div className="flex gap-4">
<div className={`mt-1 h-10 w-10 flex-shrink-0 flex items-center justify-center rounded-xl ${notif.tipo === 'agenda' ? 'bg-blue-100 text-blue-600' :
notif.tipo === 'financeiro' ? 'bg-emerald-100 text-emerald-600' :
notif.tipo === 'lead' ? 'bg-amber-100 text-amber-600' :
'bg-red-100 text-red-600'
}`}>
{getIcon(notif.tipo)}
</div>
<div className="flex-1">
<div className="flex justify-between items-center mb-1">
<span className="text-[11px] font-bold text-slate-400 uppercase tracking-wide">
{getTypeLabel(notif.tipo)}
</span>
<span className="text-[10px] text-slate-400">
{new Date(notif.data).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
<h4 className={`text-sm font-bold leading-tight ${!notif.lida ? 'text-slate-900' : 'text-slate-600'}`}>
{notif.titulo}
</h4>
<p className="text-xs text-slate-500 mt-1 line-clamp-2 leading-relaxed">
{notif.mensagem}
</p>
{!notif.lida && (
<button
onClick={() => markAsRead(notif.id)}
className="text-[11px] text-blue-600 font-bold mt-3 opacity-0 group-hover:opacity-100 transition-opacity hover:underline"
>
MARCAR COMO LIDA
</button>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
<div className="p-4 bg-white border-t border-slate-100 text-center">
<button
onClick={() => {
window.location.hash = '/notificacoes';
setIsOpen(false);
}}
className="text-xs font-bold text-slate-500 hover:text-blue-600 transition-colors tracking-widest uppercase"
>
Histórico Completo
</button>
</div>
</div>
</>
)}
</div>
</div>
);
};