import { createContext, useContext, useEffect, useRef, useState, useCallback } from 'react' import useAuthStore from '../store/authStore' import client from '../api/client' const NotificationContext = createContext(null) export function useNotifications() { return useContext(NotificationContext) } // ─── Persistent banner (one message at a time, stacked) ─────────────────────── function NotificationBanner({ message, onAck }) { const tableIds = (() => { try { return JSON.parse(message.table_ids || '[]') } catch { return [] } })() return (
📢
{message.sender_name && (
{message.sender_name}
)}
{message.body}
{tableIds.length > 0 && (
Τραπέζι{tableIds.length > 1 ? 'α' : ''}: {tableIds.join(', ')}
)}
) } export function NotificationProvider({ children }) { const { token, user } = useAuthStore() const [pendingMessages, setPendingMessages] = useState([]) const [recentMessages, setRecentMessages] = useState([]) const fetchUnread = useCallback(async () => { if (!token || !user) return try { const res = await client.get('/api/messages/unread') setPendingMessages(res.data) } catch { /* offline or unauthenticated — swallow */ } }, [token, user?.id]) const fetchRecent = useCallback(async () => { if (!token || !user) return try { const res = await client.get('/api/messages/recent?limit=10') setRecentMessages(res.data) } catch { } }, [token, user?.id]) // Initial load + 5s fallback poll (SSE is primary, poll is safety net) useEffect(() => { if (!token || !user) return fetchUnread() fetchRecent() const id = setInterval(fetchUnread, 5000) return () => clearInterval(id) }, [token, user?.id]) // SSE message_sent events → add to pending without polling useEffect(() => { function onSSEEvent(e) { const { type, data } = e.detail if (type !== 'message_sent') return if (!user) return // Check if this message targets us (empty = broadcast) const targets = data.target_waiter_ids || [] if (targets.length > 0 && !targets.includes(user.id)) return const msg = { id: data.id, sender_id: data.sender_id, sender_name: data.sender_name, body: data.body, table_ids: data.table_ids, created_at: data.created_at, acked_by: [], } setPendingMessages(prev => { if (prev.find(m => m.id === msg.id)) return prev return [msg, ...prev] }) setRecentMessages(prev => { if (prev.find(m => m.id === msg.id)) return prev return [msg, ...prev].slice(0, 10) }) } window.addEventListener('sse-event', onSSEEvent) return () => window.removeEventListener('sse-event', onSSEEvent) }, [user?.id]) // Fallback: re-fetch unread when SSE reconnects (catches any messages missed during gap) useEffect(() => { function onSSEConnect() { fetchUnread() fetchRecent() } // SSEProvider fires this via setOnline — we listen to the connection store indirectly // through the backend-coming-back-online signal that SSEProvider dispatches window.addEventListener('sse-reconnected', onSSEConnect) return () => window.removeEventListener('sse-reconnected', onSSEConnect) }, [fetchUnread, fetchRecent]) async function ackMessage(messageId) { try { await client.post(`/api/messages/${messageId}/ack`) setPendingMessages(prev => prev.filter(m => m.id !== messageId)) fetchRecent() } catch { } } const unreadCount = pendingMessages.length return ( {children} {/* Floating banner stack (max 3 visible) */} {pendingMessages.length > 0 && (
{pendingMessages.slice(0, 3).map(msg => (
))} {pendingMessages.length > 3 && (
+{pendingMessages.length - 3} ακόμα μηνύματα
)}
)}
) }