"use client"; import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Bell, Check, CheckCheck, ClipboardList, DollarSign, Package, TrendingUp, AlertTriangle, MessageSquare, } from "lucide-react"; import { formatDistanceToNow } from "date-fns"; import { es } from "date-fns/locale"; interface Notification { id: string; tipo: string; titulo: string; mensaje: string; url: string | null; leida: boolean; createdAt: string; } const TIPO_ICONS: Record = { TAREA_ASIGNADA: , TAREA_COMPLETADA: , GASTO_PENDIENTE: , GASTO_APROBADO: , ORDEN_APROBADA: , AVANCE_REGISTRADO: , ALERTA_INVENTARIO: , GENERAL: , RECORDATORIO: , }; export function NotificationBell() { const [notifications, setNotifications] = useState([]); const [unreadCount, setUnreadCount] = useState(0); const [loading, setLoading] = useState(true); const [isOpen, setIsOpen] = useState(false); useEffect(() => { fetchNotifications(); // Polling cada 30 segundos const interval = setInterval(fetchNotifications, 30000); return () => clearInterval(interval); }, []); // Registrar service worker y suscribirse a push useEffect(() => { if ("serviceWorker" in navigator && "PushManager" in window) { registerServiceWorker(); } }, []); const registerServiceWorker = async () => { try { const registration = await navigator.serviceWorker.register("/sw.js"); console.log("Service Worker registrado:", registration); // Solicitar permiso para notificaciones const permission = await Notification.requestPermission(); if (permission === "granted") { await subscribeToPush(registration); } } catch (error) { console.error("Error registrando Service Worker:", error); } }; const subscribeToPush = async (registration: ServiceWorkerRegistration) => { try { const vapidPublicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY; if (!vapidPublicKey) { console.warn("VAPID public key not configured"); return; } const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(vapidPublicKey) as BufferSource, }); // Enviar suscripción al servidor const p256dhKey = subscription.getKey("p256dh"); const authKey = subscription.getKey("auth"); if (p256dhKey && authKey) { const p256dhArray = new Uint8Array(p256dhKey); const authArray = new Uint8Array(authKey); await fetch("/api/notifications/subscribe", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ endpoint: subscription.endpoint, keys: { p256dh: arrayBufferToBase64(p256dhArray), auth: arrayBufferToBase64(authArray), }, }), }); } console.log("Suscrito a push notifications"); } catch (error) { console.error("Error suscribiendo a push:", error); } }; const fetchNotifications = async () => { try { const res = await fetch("/api/notifications?limit=10"); if (res.ok) { const data = await res.json(); setNotifications(data.notificaciones); setUnreadCount(data.unreadCount); } } catch (error) { console.error("Error fetching notifications:", error); } finally { setLoading(false); } }; const markAsRead = async (notificationId: string) => { try { await fetch("/api/notifications", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ notificationIds: [notificationId] }), }); setNotifications((prev) => prev.map((n) => (n.id === notificationId ? { ...n, leida: true } : n)) ); setUnreadCount((prev) => Math.max(0, prev - 1)); } catch (error) { console.error("Error marking as read:", error); } }; const markAllAsRead = async () => { try { await fetch("/api/notifications", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ markAllRead: true }), }); setNotifications((prev) => prev.map((n) => ({ ...n, leida: true }))); setUnreadCount(0); } catch (error) { console.error("Error marking all as read:", error); } }; const handleNotificationClick = (notification: Notification) => { if (!notification.leida) { markAsRead(notification.id); } if (notification.url) { window.location.href = notification.url; } setIsOpen(false); }; return ( Notificaciones {unreadCount > 0 && ( )} {loading ? (
Cargando...
) : notifications.length === 0 ? (
No tienes notificaciones
) : ( notifications.map((notification) => ( handleNotificationClick(notification)} >
{TIPO_ICONS[notification.tipo] || TIPO_ICONS.GENERAL}

{notification.titulo}

{notification.mensaje}

{formatDistanceToNow(new Date(notification.createdAt), { addSuffix: true, locale: es, })}

{!notification.leida && (
)} )) )} ); } // Utilidad para convertir VAPID key function urlBase64ToUint8Array(base64String: string): Uint8Array { const padding = "=".repeat((4 - (base64String.length % 4)) % 4); const base64 = (base64String + padding) .replace(/-/g, "+") .replace(/_/g, "/"); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; } // Utilidad para convertir ArrayBuffer a Base64 function arrayBufferToBase64(buffer: Uint8Array): string { let binary = ""; for (let i = 0; i < buffer.byteLength; i++) { binary += String.fromCharCode(buffer[i]); } return btoa(binary); }