Files
simple-pos-system/waiter_pwa/src/context/NotificationContext.jsx

171 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (
<div style={{
display: 'flex', alignItems: 'flex-start', gap: 12,
background: '#1e1b4b', border: '1px solid #6366f1',
borderRadius: 14, padding: '12px 14px',
boxShadow: '0 8px 24px rgba(0,0,0,0.4)',
animation: 'slideIn 0.25s ease',
}}>
<span style={{ fontSize: 22, flexShrink: 0 }}>📢</span>
<div style={{ flex: 1, minWidth: 0 }}>
{message.sender_name && (
<div style={{ fontSize: 11, fontWeight: 700, color: '#a5b4fc', marginBottom: 2, textTransform: 'uppercase', letterSpacing: 0.5 }}>
{message.sender_name}
</div>
)}
<div style={{ fontSize: 15, fontWeight: 600, color: '#e2e8f0', lineHeight: 1.4 }}>
{message.body}
</div>
{tableIds.length > 0 && (
<div style={{ fontSize: 12, color: '#94a3b8', marginTop: 4 }}>
Τραπέζι{tableIds.length > 1 ? 'α' : ''}: {tableIds.join(', ')}
</div>
)}
</div>
<button
onClick={() => onAck(message.id)}
style={{
flexShrink: 0, height: 32, padding: '0 12px',
borderRadius: 8, border: 'none',
background: '#4f46e5', color: 'white',
fontSize: 12, fontWeight: 700, cursor: 'pointer',
}}
>OK </button>
</div>
)
}
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 (
<NotificationContext.Provider value={{ pendingMessages, recentMessages, unreadCount, ackMessage, fetchRecent, fetchUnread }}>
{children}
{/* Floating banner stack (max 3 visible) */}
{pendingMessages.length > 0 && (
<div style={{
position: 'fixed', top: 64, left: 0, right: 0, zIndex: 9999,
padding: '0 12px',
display: 'flex', flexDirection: 'column', gap: 8,
pointerEvents: 'none',
}}>
<style>{`@keyframes slideIn { from { transform: translateY(-16px); opacity: 0 } to { transform: translateY(0); opacity: 1 } }`}</style>
{pendingMessages.slice(0, 3).map(msg => (
<div key={msg.id} style={{ pointerEvents: 'all' }}>
<NotificationBanner message={msg} onAck={ackMessage} />
</div>
))}
{pendingMessages.length > 3 && (
<div style={{
textAlign: 'center', fontSize: 12, color: '#94a3b8',
pointerEvents: 'all',
}}>
+{pendingMessages.length - 3} ακόμα μηνύματα
</div>
)}
</div>
)}
</NotificationContext.Provider>
)
}