// Histórico de Notificações — a sineta só mostra as 30 últimas e some. // Aqui dá pra procurar depois: "quando foi que o fulano abriu aquele // orçamento?". Busca por texto, filtro por tipo e por lidas/não lidas. const NOTIF_TIPOS = { budget_viewed: { label: 'Visualização de orçamento', cor: '#8B5CF6', icon: 'eye' }, budget_approved: { label: 'Aprovação do cliente', cor: '#10B981', icon: 'check' }, budget_ajuste: { label: 'Ajuste solicitado', cor: '#F59E0B', icon: 'alert' }, approval_pending: { label: 'Aprovação aguardando', cor: '#F59E0B', icon: 'clock' }, operation_update: { label: 'Atualização da operação', cor: '#3B82F6', icon: 'zap' }, system: { label: 'Sistema', cor: '#64748B', icon: 'bell' }, }; const NOTIF_POR_PAGINA = 25; function Notificacoes({ onNavigate }) { const [notifs, setNotifs] = React.useState([]); const [total, setTotal] = React.useState(0); const [loading, setLoading] = React.useState(true); const [pagina, setPagina] = React.useState(0); const [busca, setBusca] = React.useState(''); const [buscaAtiva, setBuscaAtiva] = React.useState(''); const [tipo, setTipo] = React.useState('todos'); const [lidas, setLidas] = React.useState('todas'); const [toast, setToast] = React.useState(null); const showToast = (msg, type = 'success') => { setToast({ msg, type }); setTimeout(() => setToast(null), 3000); }; // Debounce da busca pra não bater no banco a cada tecla React.useEffect(() => { const t = setTimeout(() => { setBuscaAtiva(busca.trim()); setPagina(0); }, 300); return () => clearTimeout(t); }, [busca]); const carregar = React.useCallback(async () => { setLoading(true); let q = sb.from('notifications').select('*', { count: 'exact' }); if (tipo !== 'todos') q = q.eq('type', tipo); if (lidas === 'nao') q = q.eq('read', false); if (lidas === 'sim') q = q.eq('read', true); if (buscaAtiva) { const esc = buscaAtiva.replace(/[%_,]/g, ''); q = q.or(`title.ilike.%${esc}%,message.ilike.%${esc}%`); } const de = pagina * NOTIF_POR_PAGINA; const { data, count } = await q.order('created_at', { ascending: false }).range(de, de + NOTIF_POR_PAGINA - 1); setNotifs(data || []); setTotal(count || 0); setLoading(false); }, [tipo, lidas, buscaAtiva, pagina]); React.useEffect(() => { carregar(); }, [carregar]); const naoLidas = notifs.filter(n => !n.read); const marcarUma = async (n) => { if (n.read) return; await markNotificationRead(n.id); setNotifs(prev => prev.map(x => x.id === n.id ? { ...x, read: true, read_at: new Date().toISOString() } : x)); }; const marcarTodasDaPagina = async () => { if (!naoLidas.length) return; await markAllNotificationsRead(naoLidas.map(n => n.id)); showToast(`${naoLidas.length} marcada${naoLidas.length !== 1 ? 's' : ''} como lida${naoLidas.length !== 1 ? 's' : ''}.`); carregar(); }; const abrir = async (n) => { await marcarUma(n); if (n.link && onNavigate) onNavigate(n.link); }; const totalPaginas = Math.max(1, Math.ceil(total / NOTIF_POR_PAGINA)); const temFiltro = tipo !== 'todos' || lidas !== 'todas' || !!buscaAtiva; const limparFiltros = () => { setTipo('todos'); setLidas('todas'); setBusca(''); setPagina(0); }; const fmtData = (iso) => { const d = new Date(iso); const hoje = new Date(); hoje.setHours(0, 0, 0, 0); const dia = new Date(d); dia.setHours(0, 0, 0, 0); const difDias = Math.round((hoje - dia) / 86400000); const hora = d.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }); if (difDias === 0) return `Hoje, ${hora}`; if (difDias === 1) return `Ontem, ${hora}`; return `${d.toLocaleDateString('pt-BR', { day: '2-digit', month: 'short', year: 'numeric' })} · ${hora}`; }; return (
Tudo que já passou pela sineta — inclusive o que você não viu na hora.