| |
|
|
| 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<void>; |
| markAllAsRead: () => Promise<void>; |
| refetch: () => Promise<void>; |
| } |
|
|
| export function useNotifications(userId: string | undefined): UseNotificationsResult { |
| const [notifications, setNotifications] = useState<Notification[]>([]); |
| const [unreadCount, setUnreadCount] = useState(0); |
| const [loading, setLoading] = useState(true); |
| const channelRef = useRef<RealtimeChannel | null>(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); |
|
|
| |
| 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, |
| }; |
| } |
|
|