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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user