Waiter PWA fixes, and extra feautures. Also added Emergency Mode, search etc
This commit is contained in:
@@ -2,6 +2,8 @@ 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: 'ΑΝΟΙΧΤΟ',
|
||||
@@ -13,7 +15,555 @@ const STATUS_LABELS = {
|
||||
const DRAG_THRESHOLD = 8
|
||||
const HOLD_MS = 480
|
||||
|
||||
export default function TableCard({ table, order, isMine, flags = [], groupName = '', onClick, onLongPress }) {
|
||||
// ─── 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 (
|
||||
<img
|
||||
src={API_URL + waiter.avatar_url}
|
||||
alt={displayName}
|
||||
style={{
|
||||
width: size, height: size, borderRadius: '50%',
|
||||
objectFit: 'cover', flexShrink: 0,
|
||||
...ringStyle,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div style={{
|
||||
width: size, height: size, borderRadius: '50%',
|
||||
background: avatarColor(displayName),
|
||||
color: 'white', fontSize: size * 0.4, fontWeight: 700,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
...ringStyle,
|
||||
}}>{initials}</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'nowrap', overflow: 'hidden', minWidth: 0 }}>
|
||||
{waiters.map((w, i) => {
|
||||
const name = w.nickname || w.full_name || w.username || '?'
|
||||
return (
|
||||
<div key={w.id} style={{ display: 'flex', alignItems: 'center', gap: 5, minWidth: 0, overflow: 'hidden' }}>
|
||||
{i > 0 && <span style={{ color: textColor, opacity: 0.3, fontSize: 14, flexShrink: 0 }}>·</span>}
|
||||
<WaiterAvatar waiter={w} size={size} />
|
||||
<span style={{
|
||||
fontSize: 12, fontWeight: 600, color: textColor, opacity: 0.85,
|
||||
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||
}}>{name}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// > 2 waiters: icons only + "X Waiters" label
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
{waiters.slice(0, 3).map((w, i) => (
|
||||
<div key={w.id} style={{ marginLeft: i === 0 ? 0 : -(size * 0.28) }}>
|
||||
<WaiterAvatar waiter={w} size={size} ring={cfg.cardBg} />
|
||||
</div>
|
||||
))}
|
||||
{waiters.length > 3 && (
|
||||
<div style={{
|
||||
marginLeft: -(size * 0.28), height: size, padding: '0 6px',
|
||||
borderRadius: size, background: `${cfg.nameText}20`,
|
||||
color: cfg.nameText, fontSize: 10, fontWeight: 700,
|
||||
display: 'flex', alignItems: 'center',
|
||||
}}>+{waiters.length - 3}</div>
|
||||
)}
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: textColor, opacity: 0.7, marginLeft: 4 }}>
|
||||
{waiters.length} σερβιτόροι
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Status pill ──────────────────────────────────────────────────────────────
|
||||
|
||||
function StatusPill({ label, badgeBg, badgeText, small }) {
|
||||
return (
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center',
|
||||
height: small ? 18 : 20,
|
||||
padding: small ? '0 6px' : '0 8px',
|
||||
borderRadius: 4,
|
||||
background: badgeBg,
|
||||
color: badgeText,
|
||||
fontSize: small ? 9 : 10,
|
||||
fontWeight: 800,
|
||||
letterSpacing: 0.4,
|
||||
whiteSpace: 'nowrap',
|
||||
}}>{label}</span>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Flag dot ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function FlagDot({ flag, size = 22 }) {
|
||||
const textColor = flag.text_color || '#ffffff'
|
||||
return (
|
||||
<div
|
||||
title={flag.name}
|
||||
style={{
|
||||
width: size, height: size, borderRadius: '50%',
|
||||
background: flag.color || '#6295F3',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: size * 0.55,
|
||||
flexShrink: 0,
|
||||
color: textColor,
|
||||
}}
|
||||
>
|
||||
{flag.emoji || '🏷️'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── 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 (
|
||||
<div style={{ display: 'flex', gap: 3, alignItems: 'center' }}>
|
||||
{visible.map(f => <FlagDot key={f.id} flag={f} size={size} />)}
|
||||
{overflow > 0 && (
|
||||
<div style={{
|
||||
width: size, height: size, borderRadius: '50%',
|
||||
background: 'rgba(0,0,0,0.18)',
|
||||
color: '#fff', fontSize: size * 0.44, fontWeight: 800,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}>+{overflow}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Flag chip (icon + label) ─────────────────────────────────────────────────
|
||||
|
||||
function FlagChip({ flag }) {
|
||||
const textColor = flag.text_color || '#ffffff'
|
||||
return (
|
||||
<div
|
||||
title={flag.name}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 5,
|
||||
height: 26, padding: '0 9px',
|
||||
borderRadius: 13,
|
||||
background: flag.color || '#6295F3',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 13, lineHeight: 1 }}>{flag.emoji || '🏷️'}</span>
|
||||
<span style={{ fontSize: 11, fontWeight: 700, color: textColor, whiteSpace: 'nowrap' }}>
|
||||
{flag.name}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── 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 (
|
||||
<div style={{ lineHeight: 1, color: color || 'inherit' }}>
|
||||
<span style={{ fontSize: size, fontWeight: 800, letterSpacing: -0.5 }}>{whole}</span>
|
||||
<span style={{ fontSize: centsSize, fontWeight: 800, opacity: 0.8 }}>.{cents}€</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── 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 (
|
||||
<div style={{
|
||||
width: '100%', aspectRatio: '1 / 1.05',
|
||||
background: cfg.cardBg, borderRadius: 14,
|
||||
position: 'relative', overflow: 'hidden',
|
||||
display: 'flex', flexDirection: 'column',
|
||||
padding: 8,
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.12)',
|
||||
}}>
|
||||
{/* top strip: badges up to 2, then +N */}
|
||||
<div style={{ height: '20%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 3 }}>
|
||||
<FlagDots flags={flags} size={16} maxShow={2} />
|
||||
</div>
|
||||
|
||||
{/* center: name */}
|
||||
<div style={{
|
||||
flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontWeight: 800, fontSize: 'clamp(18px, 5vw, 26px)',
|
||||
letterSpacing: -0.5, color: cfg.nameText, lineHeight: 1,
|
||||
}}>
|
||||
{table.label || `T${table.number}`}
|
||||
</div>
|
||||
|
||||
{/* bottom strip: status */}
|
||||
<div style={{ height: '20%', display: 'flex', alignItems: 'flex-end', justifyContent: 'center' }}>
|
||||
<span style={{
|
||||
fontSize: 7, fontWeight: 800, letterSpacing: 0.3,
|
||||
color: cfg.badgeText, textTransform: 'uppercase',
|
||||
background: cfg.badgeBg, borderRadius: 3,
|
||||
padding: '1px 4px', whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{STATUS_LABELS[statusKey]}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 2x1 — half width, compact horizontal. Name left, status + badges (up to 3 + +N) right.
|
||||
function Card2x1({ table, order, flags, waiterObjects, cfg, statusKey }) {
|
||||
return (
|
||||
<div style={{
|
||||
width: '100%', height: 64,
|
||||
background: cfg.cardBg, borderRadius: 14,
|
||||
padding: '10px 12px',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
gap: 10, overflow: 'hidden',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.12)',
|
||||
}}>
|
||||
<div style={{
|
||||
fontWeight: 800, fontSize: 'clamp(18px, 4.5vw, 24px)',
|
||||
letterSpacing: -0.5, color: cfg.nameText, lineHeight: 1, flexShrink: 0,
|
||||
}}>
|
||||
{table.label || `T${table.number}`}
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column',
|
||||
alignItems: 'flex-end', justifyContent: 'center', gap: 4,
|
||||
}}>
|
||||
<StatusPill label={STATUS_LABELS[statusKey]} badgeBg={cfg.badgeBg} badgeText={cfg.badgeText} small />
|
||||
{flags.length > 0 && (
|
||||
<FlagDots flags={flags} size={18} maxShow={3} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div style={{
|
||||
width: '100%', minHeight: 116,
|
||||
background: cfg.cardBg, borderRadius: 16,
|
||||
padding: '12px 12px 12px',
|
||||
display: 'flex', gap: 8, overflow: 'hidden',
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.12)',
|
||||
}}>
|
||||
{/* left column */}
|
||||
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }}>
|
||||
<span style={{
|
||||
fontSize: 'clamp(22px, 5.5vw, 36px)', fontWeight: 800,
|
||||
lineHeight: 1.05, color: cfg.nameText, letterSpacing: -0.5,
|
||||
}}>
|
||||
{table.label || `T${table.number}`}
|
||||
</span>
|
||||
<div style={{ marginTop: 5 }}>
|
||||
<StatusPill label={STATUS_LABELS[statusKey]} badgeBg={cfg.badgeBg} badgeText={cfg.badgeText} small />
|
||||
</div>
|
||||
<div style={{ marginTop: 'auto', paddingTop: 8, minHeight: 28 }}>
|
||||
{showAmount && <Amount value={total} size={'clamp(22px, 5.5vw, 36px)'} color={cfg.nameText} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* right column: flags — show 2, then +N */}
|
||||
{flags.length > 0 && (
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column-reverse',
|
||||
gap: 4, alignItems: 'flex-end', justifyContent: 'flex-start',
|
||||
}}>
|
||||
<FlagDots flags={flags} size={26} maxShow={2} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div style={{
|
||||
width: '100%', height: 68,
|
||||
background: cfg.cardBg, borderRadius: 14,
|
||||
padding: '12px 14px',
|
||||
display: 'flex', alignItems: 'center', gap: 14, overflow: 'hidden',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.12)',
|
||||
}}>
|
||||
{/* name */}
|
||||
<div style={{
|
||||
fontWeight: 800, fontSize: 'clamp(20px, 4.5vw, 28px)',
|
||||
letterSpacing: -0.5, color: cfg.nameText, lineHeight: 1, flexShrink: 0,
|
||||
}}>
|
||||
{table.label || `T${table.number}`}
|
||||
</div>
|
||||
|
||||
{/* separator dot */}
|
||||
<span style={{ color: cfg.nameText, opacity: 0.3, fontSize: 20, lineHeight: 1, flexShrink: 0 }}>·</span>
|
||||
|
||||
{/* amount */}
|
||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center' }}>
|
||||
{showAmount && <Amount value={total} size={'clamp(20px, 4.5vw, 28px)'} color={cfg.nameText} />}
|
||||
</div>
|
||||
|
||||
{/* flags up to 3 + +N */}
|
||||
{flags.length > 0 && (
|
||||
<FlagDots flags={flags} size={24} maxShow={3} />
|
||||
)}
|
||||
|
||||
{/* status */}
|
||||
<StatusPill label={STATUS_LABELS[statusKey]} badgeBg={cfg.badgeBg} badgeText={cfg.badgeText} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div style={{
|
||||
width: '100%',
|
||||
background: cfg.cardBg, borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.12)',
|
||||
display: 'flex', flexDirection: 'column',
|
||||
}}>
|
||||
{/* main body */}
|
||||
<div style={{ padding: '14px 14px 12px', display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{/* top row: name LEFT | status CENTER | amount RIGHT — all top-aligned */}
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 10 }}>
|
||||
{/* left: name + zone */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{
|
||||
fontWeight: 800, fontSize: 'clamp(30px, 7vw, 44px)',
|
||||
letterSpacing: -1.5, lineHeight: 1, color: cfg.nameText,
|
||||
}}>
|
||||
{table.label || `T${table.number}`}
|
||||
</div>
|
||||
{groupName && (
|
||||
<div style={{
|
||||
fontSize: 10, fontWeight: 700, letterSpacing: 0.8,
|
||||
color: cfg.nameText, opacity: 0.6,
|
||||
textTransform: 'uppercase', marginTop: 3,
|
||||
}}>
|
||||
{groupName}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* center: status pill — top-aligned via paddingTop to optically align with name cap */}
|
||||
<div style={{ paddingTop: 4, flexShrink: 0 }}>
|
||||
<StatusPill label={STATUS_LABELS[statusKey]} badgeBg={cfg.badgeBg} badgeText={cfg.badgeText} />
|
||||
</div>
|
||||
|
||||
{/* right: amount — top-aligned */}
|
||||
{showAmount && (
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
<Amount value={total} size={'clamp(30px, 7vw, 44px)'} color={cfg.nameText} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* flag chips row — right-aligned */}
|
||||
{flags.length > 0 && (
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', flexWrap: 'wrap', gap: 6 }}>
|
||||
{flags.slice(0, 4).map(f => <FlagChip key={f.id} flag={f} />)}
|
||||
{flags.length > 4 && (
|
||||
<div style={{
|
||||
height: 26, padding: '0 9px', borderRadius: 13,
|
||||
background: 'rgba(0,0,0,0.18)', color: '#fff',
|
||||
fontSize: 11, fontWeight: 800,
|
||||
display: 'flex', alignItems: 'center',
|
||||
}}>+{flags.length - 4}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* footer: waiters */}
|
||||
<div style={{
|
||||
borderTop: `1px solid ${cfg.nameText}22`,
|
||||
padding: '10px 14px', minHeight: 40,
|
||||
display: 'flex', alignItems: 'center',
|
||||
}}>
|
||||
{showWaiters
|
||||
? <WaiterRow waiters={waiterObjects} size={24} cfg={cfg} />
|
||||
: <span style={{ fontSize: 12, color: cfg.nameText, opacity: 0.45 }}>—</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div style={{
|
||||
width: '100%',
|
||||
background: cfg.cardBg, borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.12)',
|
||||
display: 'flex', flexDirection: 'column',
|
||||
}}>
|
||||
<div style={{ display: 'flex', padding: '14px 14px 10px', gap: 14, minWidth: 0, overflow: 'hidden' }}>
|
||||
{/* left column: name, zone, amount, status, flags */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', minWidth: 100, flexShrink: 0, justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<div style={{
|
||||
fontWeight: 800, fontSize: 'clamp(28px, 6vw, 40px)',
|
||||
letterSpacing: -1.5, lineHeight: 1, color: cfg.nameText,
|
||||
}}>
|
||||
{table.label || `T${table.number}`}
|
||||
</div>
|
||||
{groupName && (
|
||||
<div style={{
|
||||
fontSize: 10, fontWeight: 700, letterSpacing: 0.8,
|
||||
color: cfg.nameText, opacity: 0.6,
|
||||
textTransform: 'uppercase', marginTop: 3,
|
||||
}}>
|
||||
{groupName}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 10 }}>
|
||||
{!isFree && <Amount value={total} size={'clamp(22px, 5vw, 32px)'} color={cfg.nameText} />}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<StatusPill label={STATUS_LABELS[statusKey]} badgeBg={cfg.badgeBg} badgeText={cfg.badgeText} small />
|
||||
</div>
|
||||
|
||||
{flags.length > 0 && (
|
||||
<div style={{ marginTop: 8, display: 'flex', flexWrap: 'wrap', gap: 4 }}>
|
||||
<FlagDots flags={flags} size={22} maxShow={3} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* divider */}
|
||||
<div style={{ width: 1, background: `${cfg.nameText}20`, alignSelf: 'stretch', flexShrink: 0 }} />
|
||||
|
||||
{/* right column: order items */}
|
||||
<div style={{ flex: 1, minWidth: 0, overflow: 'hidden' }}>
|
||||
{isFree ? (
|
||||
<div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<span style={{ fontSize: 12, color: cfg.nameText, opacity: 0.35 }}>Ελεύθερο</span>
|
||||
</div>
|
||||
) : activeItems.length === 0 ? (
|
||||
<div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<span style={{ fontSize: 12, color: cfg.nameText, opacity: 0.35 }}>Κανένα είδος</span>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 3, minWidth: 0 }}>
|
||||
{activeItems.slice(0, 7).map(item => (
|
||||
<div key={item.id} style={{ display: 'flex', alignItems: 'baseline', gap: 5, overflow: 'hidden', minWidth: 0 }}>
|
||||
<span style={{
|
||||
fontSize: 11, fontWeight: 700, color: cfg.nameText,
|
||||
background: `${cfg.nameText}18`, borderRadius: 3,
|
||||
padding: '1px 5px', flexShrink: 0,
|
||||
}}>{item.quantity}×</span>
|
||||
<span style={{
|
||||
fontSize: 12, fontWeight: 500, color: cfg.nameText,
|
||||
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1,
|
||||
}}>{item.product?.name || `#${item.product_id}`}</span>
|
||||
<span style={{ fontSize: 11, fontWeight: 700, color: cfg.nameText, opacity: 0.7, flexShrink: 0 }}>
|
||||
{(item.unit_price * item.quantity).toFixed(2)}€
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{activeItems.length > 7 && (
|
||||
<div style={{ fontSize: 11, color: cfg.nameText, opacity: 0.5, marginTop: 2 }}>
|
||||
+{activeItems.length - 7} ακόμα…
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* footer: waiters */}
|
||||
<div style={{
|
||||
borderTop: `1px solid ${cfg.nameText}22`,
|
||||
padding: '10px 14px', minHeight: 38,
|
||||
display: 'flex', alignItems: 'center',
|
||||
}}>
|
||||
{showWaiters
|
||||
? <WaiterRow waiters={waiterObjects} size={22} cfg={cfg} />
|
||||
: <span style={{ fontSize: 12, color: cfg.nameText, opacity: 0.45 }}>—</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── 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)
|
||||
@@ -31,8 +581,6 @@ export default function TableCard({ table, order, isMine, flags = [], groupName
|
||||
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
|
||||
@@ -57,10 +605,7 @@ export default function TableCard({ table, order, isMine, flags = [], groupName
|
||||
if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) cancel()
|
||||
}
|
||||
|
||||
function onTouchEnd() {
|
||||
cancel()
|
||||
setShowTip(false)
|
||||
}
|
||||
function onTouchEnd() { cancel(); setShowTip(false) }
|
||||
|
||||
function onMouseDown(e) {
|
||||
startPos.current = { x: e.clientX, y: e.clientY }
|
||||
@@ -85,11 +630,21 @@ export default function TableCard({ table, order, isMine, flags = [], groupName
|
||||
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 (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={{ position: 'relative', minWidth: 0, overflow: 'hidden' }}>
|
||||
<button
|
||||
className="table-card-v2"
|
||||
style={{ background: cfg.cardBg }}
|
||||
style={{ display: 'block', width: '100%', background: 'none', border: 'none', padding: 0, cursor: 'pointer', textAlign: 'left' }}
|
||||
onClick={handleClick}
|
||||
onTouchStart={onTouchStart}
|
||||
onTouchMove={onTouchMove}
|
||||
@@ -99,89 +654,16 @@ export default function TableCard({ table, order, isMine, flags = [], groupName
|
||||
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>
|
||||
)}
|
||||
<CardComponent {...cardProps} />
|
||||
</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',
|
||||
minWidth: 160, pointerEvents: 'none',
|
||||
}}>
|
||||
{flags.map(f => (
|
||||
<div key={f.id} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '4px 0' }}>
|
||||
|
||||
Reference in New Issue
Block a user