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 ( {displayName} ) } 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}`}
{showAmount && }
{/* 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 */}
{showAmount && }
{/* 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}
)}
{!isFree && }
{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}
))}
)}
) }