Frontend overhaul: manager dashboard restructure, waiter PWA rework, new order drawer and components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
123
waiter_pwa/src/context/NotificationContext.jsx
Normal file
123
waiter_pwa/src/context/NotificationContext.jsx
Normal file
@@ -0,0 +1,123 @@
|
||||
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([]) // unacked
|
||||
const [recentMessages, setRecentMessages] = useState([]) // last 10 (for history)
|
||||
const pollRef = useRef(null)
|
||||
|
||||
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])
|
||||
|
||||
useEffect(() => {
|
||||
if (!token || !user) return
|
||||
fetchUnread()
|
||||
fetchRecent()
|
||||
pollRef.current = setInterval(fetchUnread, 2000)
|
||||
return () => clearInterval(pollRef.current)
|
||||
}, [token, user?.id])
|
||||
|
||||
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 }}>
|
||||
{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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user