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:
2026-04-29 12:12:23 +03:00
parent defc49f84f
commit bb39088464
78 changed files with 24370 additions and 1358 deletions

View 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>
)
}