// Notifications hook with Realtime subscription + unread count import { useEffect, useState, useCallback, useRef } from 'react'; import { supabase } from '../lib/supabase'; import type { RealtimeChannel } from '@supabase/supabase-js'; import type { Notification } from '../lib/notificationStore'; import { getNotifications, getUnreadCount, markAsRead as markAsReadApi, markAllAsRead as markAllAsReadApi, } from '../lib/notificationStore'; type Row = { [key: string]: unknown }; function rowToNotif(row: Row): Notification { return { id: row.id as string, userId: row.user_id as string, type: row.type as Notification['type'], title: row.title as string, body: row.body as string, linkTo: (row.link_to as string) ?? undefined, relatedApprovalId: (row.related_approval_id as string) ?? undefined, read: row.read as boolean, createdAt: row.created_at as string, }; } interface UseNotificationsResult { notifications: Notification[]; unreadCount: number; loading: boolean; markAsRead: (id: string) => Promise; markAllAsRead: () => Promise; refetch: () => Promise; } export function useNotifications(userId: string | undefined): UseNotificationsResult { const [notifications, setNotifications] = useState([]); const [unreadCount, setUnreadCount] = useState(0); const [loading, setLoading] = useState(true); const channelRef = useRef(null); const fetchAll = useCallback(async () => { if (!userId) return; setLoading(true); const [notifs, count] = await Promise.all([ getNotifications(userId), getUnreadCount(userId), ]); setNotifications(notifs); setUnreadCount(count); setLoading(false); }, [userId]); const markAsRead = useCallback(async (id: string) => { await markAsReadApi(id); setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n)), ); setUnreadCount((prev) => Math.max(0, prev - 1)); }, []); const markAllAsRead = useCallback(async () => { if (!userId) return; await markAllAsReadApi(userId); setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))); setUnreadCount(0); }, [userId]); useEffect(() => { if (!userId) return; void Promise.resolve().then(fetchAll); // Subscribe to notifications for this user const channel = supabase .channel(`notifications-${userId}`) .on( 'postgres_changes', { event: 'INSERT', schema: 'public', table: 'notifications', filter: `user_id=eq.${userId}`, }, (payload) => { const newNotif = rowToNotif(payload.new as Row); setNotifications((prev) => [newNotif, ...prev]); if (!newNotif.read) { setUnreadCount((prev) => prev + 1); } }, ) .on( 'postgres_changes', { event: 'UPDATE', schema: 'public', table: 'notifications', filter: `user_id=eq.${userId}`, }, (payload) => { const updated = rowToNotif(payload.new as Row); setNotifications((prev) => { const next = prev.map((n) => (n.id === updated.id ? updated : n)); setUnreadCount(next.filter((n) => !n.read).length); return next; }); }, ) .subscribe(); channelRef.current = channel; return () => { if (channelRef.current) { supabase.removeChannel(channelRef.current); channelRef.current = null; } }; }, [userId, fetchAll]); return { notifications, unreadCount, loading, markAsRead, markAllAsRead, refetch: fetchAll, }; }