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

@@ -1,24 +1,196 @@
export default function TableCard({ table, order, currentUserId, onClick }) {
const hasOrder = !!order
const isMyTable = hasOrder && order.waiters?.some(w => w.waiter_id === currentUserId)
import { useRef, useState } from 'react'
import useThemeStore from '../store/themeStore'
import useTableColourStore from '../store/tableColourStore'
let statusLabel = 'Ελεύθερο'
let cardClass = 'table-card table-card--free'
const STATUS_LABELS = {
free: 'ΕΛΕΥΘΕΡΟ',
open: 'ΑΝΟΙΧΤΟ',
mine: 'ΔΙΚΟ ΜΟΥ',
paid: 'ΠΛΗΡΩΜΕΝΟ',
partially_paid: 'ΜΕΡ. ΠΛHΡ.',
}
if (hasOrder && isMyTable) {
statusLabel = 'Δικό μου'
cardClass = 'table-card table-card--mine'
} else if (hasOrder) {
statusLabel = 'Ενεργό'
cardClass = 'table-card table-card--active'
}
const DRAG_THRESHOLD = 8
const HOLD_MS = 480
export default function TableCard({ table, order, isMine, flags = [], groupName = '', onClick, onLongPress }) {
const holdTimer = useRef(null)
const startPos = useRef({ x: 0, y: 0 })
const didFire = useRef(false)
const [showTip, setShowTip] = useState(false)
const dark = useThemeStore(s => s.dark)
const colours = useTableColourStore(s => s.colours)
let statusKey = 'free'
if (order?.status === 'paid') statusKey = 'paid'
else if (order?.status === 'partially_paid') statusKey = 'partially_paid'
else if (order && isMine) statusKey = 'mine'
else if (order) statusKey = 'open'
const mode = dark ? 'dark' : 'light'
const cfg = colours[mode][statusKey]
const displayName = table.label || `T${table.number}`
function cancel() {
clearTimeout(holdTimer.current)
holdTimer.current = null
}
function onTouchStart(e) {
const t = e.touches[0]
startPos.current = { x: t.clientX, y: t.clientY }
didFire.current = false
holdTimer.current = setTimeout(() => {
didFire.current = true
if (onLongPress) onLongPress()
else setShowTip(true)
}, HOLD_MS)
}
function onTouchMove(e) {
if (!holdTimer.current) return
const t = e.touches[0]
const dx = Math.abs(t.clientX - startPos.current.x)
const dy = Math.abs(t.clientY - startPos.current.y)
if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) cancel()
}
function onTouchEnd() {
cancel()
setShowTip(false)
}
function onMouseDown(e) {
startPos.current = { x: e.clientX, y: e.clientY }
didFire.current = false
holdTimer.current = setTimeout(() => {
didFire.current = true
if (onLongPress) onLongPress()
else setShowTip(true)
}, HOLD_MS)
}
function onMouseMove(e) {
if (!holdTimer.current) return
const dx = Math.abs(e.clientX - startPos.current.x)
const dy = Math.abs(e.clientY - startPos.current.y)
if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) cancel()
}
function onMouseUp() { cancel(); setShowTip(false) }
function onMouseLeave() { cancel(); setShowTip(false) }
function handleClick(e) {
if (didFire.current) { e.preventDefault(); return }
onClick?.()
}
return (
<button className={cardClass} onClick={onClick}>
<span className="table-card__number">{displayName}</span>
<span className="table-card__status">{statusLabel}</span>
</button>
<div style={{ position: 'relative' }}>
<button
className="table-card-v2"
style={{ background: cfg.cardBg }}
onClick={handleClick}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
onMouseUp={onMouseUp}
onMouseLeave={onMouseLeave}
>
{/* Top-left: table name + area */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', maxWidth: '65%' }}>
<span style={{
fontSize: 'clamp(22px, 5.5vw, 36px)',
fontWeight: 800,
lineHeight: 1.05,
color: cfg.nameText,
letterSpacing: -0.5,
}}>
{displayName}
</span>
{groupName && (
<span style={{
fontSize: 10,
fontWeight: 600,
letterSpacing: 0.8,
color: cfg.nameText + '80',
marginTop: 1,
textTransform: 'uppercase',
}}>
{groupName}
</span>
)}
</div>
{/* Bottom-left: status badge */}
<div style={{
position: 'absolute', bottom: 11, left: 11,
background: cfg.badgeBg,
borderRadius: 5,
padding: '2px 8px',
}}>
<span style={{
fontSize: 10,
fontWeight: 700,
letterSpacing: 0.5,
color: cfg.badgeText,
whiteSpace: 'nowrap',
}}>
{STATUS_LABELS[statusKey]}
</span>
</div>
{/* Bottom-right: flag circles, stacked, up to 3 visible */}
{flags.length > 0 && (
<div style={{
position: 'absolute', bottom: 8, right: 10,
display: 'flex', flexDirection: 'column-reverse', gap: 4,
}}>
{flags.slice(0, 3).map(f => (
<div key={f.id} style={{
width: 28, height: 28, borderRadius: '50%',
background: 'rgba(98,149,243,0.9)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 14,
boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
}}>
{f.emoji || '🏷️'}
</div>
))}
{flags.length > 3 && (
<div style={{
width: 28, height: 28, borderRadius: '50%',
background: 'rgba(98,149,243,0.9)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 10, fontWeight: 700, color: '#fff',
}}>
+{flags.length - 3}
</div>
)}
</div>
)}
</button>
{/* Flag name tooltip on long-press (only when no onLongPress handler) */}
{showTip && flags.length > 0 && (
<div style={{
position: 'absolute', bottom: 'calc(100% + 8px)', right: 0,
background: 'var(--bg2)', border: '1px solid var(--border)',
borderRadius: 10, padding: '8px 12px', zIndex: 50,
boxShadow: '0 4px 16px var(--shadow)',
minWidth: 160,
pointerEvents: 'none',
}}>
{flags.map(f => (
<div key={f.id} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '4px 0' }}>
<span style={{ fontSize: 15 }}>{f.emoji || '🏷️'}</span>
<span style={{ fontSize: 13, color: 'var(--text)' }}>{f.name}</span>
</div>
))}
</div>
)}
</div>
)
}