import { useRef, useState } from 'react'
import useThemeStore from '../store/themeStore'
import useTableColourStore from '../store/tableColourStore'
const API_URL = import.meta.env.VITE_API_URL || ''
const STATUS_LABELS = {
free: 'ΕΛΕΥΘΕΡΟ',
open: 'ΑΝΟΙΧΤΟ',
mine: 'ΔΙΚΟ ΜΟΥ',
paid: 'ΠΛΗΡΩΜΕΝΟ',
partially_paid: 'ΜΕΡ. ΠΛHΡ.',
}
const DRAG_THRESHOLD = 8
const HOLD_MS = 480
// ─── Avatar helpers ───────────────────────────────────────────────────────────
const AVATAR_PALETTE = ['#3758c9', '#7a44c9', '#2f9e5e', '#d94b26', '#8a6d2b', '#0d7a8a', '#c93775', '#1d6f3a']
function avatarColor(name = '') {
let h = 0
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0
return AVATAR_PALETTE[h % AVATAR_PALETTE.length]
}
function WaiterAvatar({ waiter, size = 22, ring }) {
const displayName = waiter.nickname || waiter.full_name || waiter.username || '?'
const initials = displayName.trim().split(' ').map(p => p[0]).slice(0, 2).join('').toUpperCase()
const ringStyle = ring ? { boxShadow: `0 0 0 2px ${ring}` } : {}
if (waiter.avatar_url) {
return (
)
}
return (
{initials}
)
}
// Renders [icon] Name, [icon] Name inline. Falls back to icons + "X Waiters" if they don't fit
// (we approximate "don't fit" as > 2 waiters for the compact footer height).
function WaiterRow({ waiters, size = 22, cfg }) {
if (!waiters?.length) return null
const textColor = cfg.nameText
// ≤ 2 waiters: show icon + name pairs
if (waiters.length <= 2) {
return (
{waiters.map((w, i) => {
const name = w.nickname || w.full_name || w.username || '?'
return (
{i > 0 && · }
{name}
)
})}
)
}
// > 2 waiters: icons only + "X Waiters" label
return (
{waiters.slice(0, 3).map((w, i) => (
))}
{waiters.length > 3 && (
+{waiters.length - 3}
)}
{waiters.length} σερβιτόροι
)
}
// ─── Status pill ──────────────────────────────────────────────────────────────
function StatusPill({ label, badgeBg, badgeText, small }) {
return (
{label}
)
}
// ─── Flag dot ─────────────────────────────────────────────────────────────────
function FlagDot({ flag, size = 22 }) {
const textColor = flag.text_color || '#ffffff'
return (
{flag.emoji || '🏷️'}
)
}
// ─── Flag overflow row: show up to maxShow dots, then +N bubble ───────────────
function FlagDots({ flags, size, maxShow }) {
if (!flags.length) return null
const visible = flags.slice(0, maxShow)
const overflow = flags.length - maxShow
return (
{visible.map(f =>
)}
{overflow > 0 && (
+{overflow}
)}
)
}
// ─── Flag chip (icon + label) ─────────────────────────────────────────────────
function FlagChip({ flag }) {
const textColor = flag.text_color || '#ffffff'
return (
{flag.emoji || '🏷️'}
{flag.name}
)
}
// ─── Amount display ───────────────────────────────────────────────────────────
function Amount({ value, size = 22, color }) {
const s = Number(value || 0).toFixed(2)
const [whole, cents] = s.split('.')
const isNum = typeof size === 'number'
const centsSize = isNum ? size * 0.56 : `calc(${size} * 0.56)`
return (
{whole}
.{cents}€
)
}
// ─── Card variants ────────────────────────────────────────────────────────────
// 1x1 — square-ish, 4 per row. Badges top (up to 2 + +N), name center, status bottom.
function Card1x1({ table, order, flags, waiterObjects, cfg, statusKey }) {
return (
{/* top strip: badges up to 2, then +N */}
{/* center: name */}
{table.label || `T${table.number}`}
{/* bottom strip: status */}
{STATUS_LABELS[statusKey]}
)
}
// 2x1 — half width, compact horizontal. Name left, status + badges (up to 3 + +N) right.
function Card2x1({ table, order, flags, waiterObjects, cfg, statusKey }) {
return (
{table.label || `T${table.number}`}
{flags.length > 0 && (
)}
)
}
// 2x2 — current-style square. Name top-left, status (slightly smaller) below, amount bottom-left, flags right.
function Card2x2({ table, order, flags, waiterObjects, cfg, statusKey }) {
const isFree = !order
const total = order?.items?.filter(i => i.status === 'active').reduce((s, i) => s + i.unit_price * i.quantity, 0) ?? 0
const showAmount = !isFree
return (
{/* left column */}
{table.label || `T${table.number}`}
{/* right column: flags — show 2, then +N */}
{flags.length > 0 && (
)}
)
}
// 4x1 — full width horizontal. Name + amount left-center, badges (up to 3 + +N) + status right.
function Card4x1({ table, order, flags, waiterObjects, cfg, statusKey }) {
const isFree = !order
const total = order?.items?.filter(i => i.status === 'active').reduce((s, i) => s + i.unit_price * i.quantity, 0) ?? 0
const showAmount = !isFree
return (
{/* name */}
{table.label || `T${table.number}`}
{/* separator dot */}
·
{/* amount */}
{/* flags up to 3 + +N */}
{flags.length > 0 && (
)}
{/* status */}
)
}
// 4x2 — full width, tall. One main row: name+zone left, status center, amount+flags right. Flag chips below. Waiter footer.
function Card4x2({ table, order, flags, waiterObjects, groupName, cfg, statusKey }) {
const isFree = !order
const total = order?.items?.filter(i => i.status === 'active').reduce((s, i) => s + i.unit_price * i.quantity, 0) ?? 0
const showAmount = !isFree
const showWaiters = !isFree && waiterObjects.length > 0
return (
{/* main body */}
{/* top row: name LEFT | status CENTER | amount RIGHT — all top-aligned */}
{/* left: name + zone */}
{table.label || `T${table.number}`}
{groupName && (
{groupName}
)}
{/* center: status pill — top-aligned via paddingTop to optically align with name cap */}
{/* right: amount — top-aligned */}
{showAmount && (
)}
{/* flag chips row — right-aligned */}
{flags.length > 0 && (
{flags.slice(0, 4).map(f =>
)}
{flags.length > 4 && (
+{flags.length - 4}
)}
)}
{/* footer: waiters */}
{showWaiters
?
: —
}
)
}
// 4x3 — full width, two-column detail card. Left: name/zone/status/amount. Right: order items list. Footer: waiters.
function Card4x3({ table, order, flags, waiterObjects, groupName, cfg, statusKey }) {
const isFree = !order
const activeItems = order?.items?.filter(i => i.status === 'active') ?? []
const total = activeItems.reduce((s, i) => s + i.unit_price * i.quantity, 0)
const showWaiters = !isFree && waiterObjects.length > 0
return (
{/* left column: name, zone, amount, status, flags */}
{table.label || `T${table.number}`}
{groupName && (
{groupName}
)}
{flags.length > 0 && (
)}
{/* divider */}
{/* right column: order items */}
{isFree ? (
Ελεύθερο
) : activeItems.length === 0 ? (
Κανένα είδος
) : (
{activeItems.slice(0, 7).map(item => (
{item.quantity}×
{item.product?.name || `#${item.product_id}`}
{(item.unit_price * item.quantity).toFixed(2)}€
))}
{activeItems.length > 7 && (
+{activeItems.length - 7} ακόμα…
)}
)}
{/* footer: waiters */}
{showWaiters
?
: —
}
)
}
// ─── Main export ──────────────────────────────────────────────────────────────
export default function TableCard({
table,
order,
isMine,
flags = [],
groupName = '',
waiterObjects = [],
density = '2x2',
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]
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?.()
}
const cardProps = { table, order, flags, waiterObjects, groupName, cfg, statusKey }
const CardComponent = {
'1x1': Card1x1,
'2x1': Card2x1,
'2x2': Card2x2,
'4x1': Card4x1,
'4x2': Card4x2,
'4x3': Card4x3,
}[density] || Card2x2
return (
{showTip && flags.length > 0 && (
{flags.map(f => (
{f.emoji || '🏷️'}
{f.name}
))}
)}
)
}