Snapshot of in-progress work across local_backend, manager_dashboard, and waiter_pwa (pricing, chat, fiscal, prep zones, recovery codes, CRM, inventory, permissions), plus the nginx/docker-compose deploy fixes for the Unraid + NPM reverse-proxy setup. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
188 lines
6.8 KiB
JavaScript
188 lines
6.8 KiB
JavaScript
import { useEffect, useRef, useState } from 'react'
|
||
import { useNavigate } from 'react-router-dom'
|
||
import useAuthStore from '../store/authStore'
|
||
import useShiftStore from '../store/shiftStore'
|
||
import useThemeStore from '../store/themeStore'
|
||
import client from '../api/client'
|
||
|
||
function formatTime(iso) {
|
||
if (!iso) return ''
|
||
return new Date(iso).toLocaleTimeString('el-GR', { hour: '2-digit', minute: '2-digit' })
|
||
}
|
||
|
||
function formatDuration(iso) {
|
||
if (!iso) return ''
|
||
const mins = Math.floor((Date.now() - new Date(iso).getTime()) / 60000)
|
||
if (mins < 60) return `${mins}λ`
|
||
const h = Math.floor(mins / 60)
|
||
const m = mins % 60
|
||
return m === 0 ? `${h}ω` : `${h}ω ${m}λ`
|
||
}
|
||
|
||
export default function UserMenu() {
|
||
const [open, setOpen] = useState(false)
|
||
const [busy, setBusy] = useState(false)
|
||
const ref = useRef(null)
|
||
const navigate = useNavigate()
|
||
const { user, logout } = useAuthStore()
|
||
const { dark, toggle } = useThemeStore()
|
||
const {
|
||
shift, selfEndAllowed,
|
||
setShift, clearShift,
|
||
} = useShiftStore()
|
||
|
||
useEffect(() => {
|
||
function onClick(e) {
|
||
if (ref.current && !ref.current.contains(e.target)) setOpen(false)
|
||
}
|
||
document.addEventListener('mousedown', onClick)
|
||
return () => document.removeEventListener('mousedown', onClick)
|
||
}, [])
|
||
|
||
function handleLogout() {
|
||
setOpen(false)
|
||
logout()
|
||
navigate('/login')
|
||
}
|
||
|
||
const activeBreak = shift?.breaks?.find(b => !b.ended_at)
|
||
const isWaiter = user?.role === 'waiter'
|
||
|
||
async function handleEndShift() {
|
||
if (!window.confirm('Να τελειώσει η βάρδια σου;')) return
|
||
setBusy(true)
|
||
try {
|
||
await client.post('/api/shifts/end', {})
|
||
clearShift()
|
||
setOpen(false)
|
||
} catch {
|
||
// ignore — gate will re-check
|
||
} finally {
|
||
setBusy(false)
|
||
}
|
||
}
|
||
|
||
async function handleBreak() {
|
||
setBusy(true)
|
||
try {
|
||
if (activeBreak) {
|
||
await client.post(`/api/shifts/${shift.id}/break/end`)
|
||
} else {
|
||
await client.post(`/api/shifts/${shift.id}/break/start`)
|
||
}
|
||
const res = await client.get('/api/shifts/my')
|
||
setShift(res.data)
|
||
} catch {
|
||
// ignore
|
||
} finally {
|
||
setBusy(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div ref={ref} style={{ position: 'relative' }}>
|
||
<button
|
||
onClick={() => setOpen(o => !o)}
|
||
title="Μενού χρήστη"
|
||
style={{
|
||
display: 'flex', alignItems: 'center', gap: 6,
|
||
padding: '8px 14px', borderRadius: 20, border: '2px solid transparent',
|
||
background: open ? 'var(--accent)' : 'var(--bg3)',
|
||
color: open ? 'var(--accent-fg)' : 'var(--text)',
|
||
fontSize: 14, fontWeight: 600, cursor: 'pointer', lineHeight: 1,
|
||
transition: 'background 0.12s, color 0.12s',
|
||
whiteSpace: 'nowrap',
|
||
}}
|
||
>
|
||
{activeBreak && (
|
||
<span style={{
|
||
width: 7, height: 7, borderRadius: '50%',
|
||
background: open ? 'var(--accent-fg)' : 'var(--accent)', flexShrink: 0,
|
||
animation: 'tab-pulse 1.5s ease-in-out infinite',
|
||
}} />
|
||
)}
|
||
<span>{user?.username}</span>
|
||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0, opacity: 0.6 }}>
|
||
<path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||
</svg>
|
||
</button>
|
||
|
||
{open && (
|
||
<div className="user-menu-dropdown">
|
||
{/* ── Shift info (waiters only) ─────────────────────── */}
|
||
{isWaiter && shift && (
|
||
<>
|
||
<div style={{
|
||
padding: '12px 16px',
|
||
background: 'var(--bg3)',
|
||
borderRadius: 10,
|
||
margin: '4px 8px 2px',
|
||
}}>
|
||
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: 0.6, marginBottom: 4 }}>
|
||
Βάρδια ενεργή
|
||
</div>
|
||
<div style={{ fontSize: 13, color: 'var(--text)', display: 'flex', justifyContent: 'space-between' }}>
|
||
<span>Από {formatTime(shift.started_at)}</span>
|
||
<span style={{ color: 'var(--muted)' }}>{formatDuration(shift.started_at)}</span>
|
||
</div>
|
||
{shift.starting_cash != null && (
|
||
<div style={{ fontSize: 12, color: 'var(--muted)', marginTop: 2 }}>
|
||
Αρχικά: €{shift.starting_cash.toFixed(2)}
|
||
</div>
|
||
)}
|
||
{activeBreak && (
|
||
<div style={{ fontSize: 12, color: 'var(--accent)', marginTop: 4, fontWeight: 600 }}>
|
||
☕ Σε διάλειμμα από {formatTime(activeBreak.started_at)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Break button */}
|
||
<button
|
||
className={`user-menu-item ${busy ? 'user-menu-item--disabled' : ''}`}
|
||
onClick={handleBreak}
|
||
disabled={busy}
|
||
>
|
||
<span className="user-menu-item__icon">{activeBreak ? '▶' : '☕'}</span>
|
||
<span>{activeBreak ? 'Τέλος Διαλείμματος' : 'Διάλειμμα'}</span>
|
||
</button>
|
||
|
||
{/* End shift button */}
|
||
{selfEndAllowed ? (
|
||
<button
|
||
className={`user-menu-item ${busy ? 'user-menu-item--disabled' : ''}`}
|
||
onClick={handleEndShift}
|
||
disabled={busy}
|
||
style={{ color: 'var(--danger)' }}
|
||
>
|
||
<span className="user-menu-item__icon">⏹</span>
|
||
<span>Τέλος Βάρδιας</span>
|
||
</button>
|
||
) : (
|
||
<div style={{ padding: '8px 16px', fontSize: 12, color: 'var(--muted)', fontStyle: 'italic' }}>
|
||
Ζητήστε από τον διαχειριστή να κλείσει τη βάρδια
|
||
</div>
|
||
)}
|
||
|
||
<div className="user-menu-divider" />
|
||
</>
|
||
)}
|
||
|
||
{/* ── Theme toggle ──────────────────────────────────── */}
|
||
<button className="user-menu-item" onClick={() => { toggle(); setOpen(false) }}>
|
||
<span className="user-menu-item__icon">{dark ? '☀️' : '🌙'}</span>
|
||
<span>{dark ? 'Φωτεινό θέμα' : 'Σκοτεινό θέμα'}</span>
|
||
</button>
|
||
|
||
<div className="user-menu-divider" />
|
||
|
||
<button className="user-menu-item user-menu-item--danger" onClick={handleLogout}>
|
||
<span className="user-menu-item__icon">⏏</span>
|
||
<span>Αποσύνδεση</span>
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|