Files
xenia-pos-local/waiter_pwa/src/components/UserMenu.jsx
bonamin 34ae328b0d feat: bump client-services (accumulated feature work + deploy fixes)
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>
2026-07-19 10:00:14 +03:00

188 lines
6.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 { 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>
)
}