Phase 2: scaffold Waiter PWA — React+Vite, PWA manifest, all pages and components

This commit is contained in:
2026-04-20 12:03:26 +03:00
parent 803358e52c
commit 36cc67dbbc
30 changed files with 8129 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
export default function ConnectionBanner() {
return (
<div style={{
background: '#7f1d1d',
color: '#fca5a5',
textAlign: 'center',
padding: '8px 16px',
fontSize: 13,
fontWeight: 600,
}}>
Cannot reach the system check your WiFi
</div>
)
}

View File

@@ -0,0 +1,97 @@
import { useState } from 'react'
export default function ItemOptionsModal({ product, onAdd, onClose }) {
const [selectedOptions, setSelectedOptions] = useState([])
const [removedIngredients, setRemovedIngredients] = useState([])
const [notes, setNotes] = useState('')
const [quantity, setQuantity] = useState(1)
const options = product.options || []
const ingredients = product.ingredients || []
function toggleOption(opt) {
setSelectedOptions(prev => {
const exists = prev.find(o => o.id === opt.id)
if (exists) return prev.filter(o => o.id !== opt.id)
return [...prev, { id: opt.id, name: opt.name, price_delta: opt.price_delta }]
})
}
function toggleIngredient(ing) {
setRemovedIngredients(prev =>
prev.includes(ing.name) ? prev.filter(n => n !== ing.name) : [...prev, ing.name]
)
}
const extraPrice = selectedOptions.reduce((s, o) => s + (o.price_delta || 0), 0)
const totalPrice = (product.base_price + extraPrice) * quantity
function handleAdd() {
onAdd({ product_id: product.id, quantity, selected_options: selectedOptions, removed_ingredients: removedIngredients, notes })
onClose()
}
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-sheet" onClick={e => e.stopPropagation()}>
<div className="modal-handle" />
<h2 className="modal-title">{product.name}</h2>
<p className="modal-price">{Number(totalPrice).toFixed(2)} </p>
{options.length > 0 && (
<section className="modal-section">
<h3>Επιλογές</h3>
{options.map(opt => (
<label key={opt.id} className="modal-option">
<input
type="checkbox"
checked={!!selectedOptions.find(o => o.id === opt.id)}
onChange={() => toggleOption(opt)}
/>
<span>{opt.name}</span>
{opt.price_delta > 0 && <span className="option-price">+{Number(opt.price_delta).toFixed(2)} </span>}
</label>
))}
</section>
)}
{ingredients.length > 0 && (
<section className="modal-section">
<h3>Αφαίρεση υλικών</h3>
{ingredients.map(ing => (
<label key={ing.id} className="modal-option modal-option--remove">
<input
type="checkbox"
checked={removedIngredients.includes(ing.name)}
onChange={() => toggleIngredient(ing)}
/>
<span>χωρίς {ing.name}</span>
</label>
))}
</section>
)}
<section className="modal-section">
<h3>Σημείωση</h3>
<textarea
className="modal-notes"
placeholder="π.χ. χωρίς αλάτι..."
value={notes}
onChange={e => setNotes(e.target.value)}
rows={2}
/>
</section>
<div className="modal-qty">
<button className="qty-btn" onClick={() => setQuantity(q => Math.max(1, q - 1))}></button>
<span className="qty-value">{quantity}</span>
<button className="qty-btn" onClick={() => setQuantity(q => q + 1)}>+</button>
</div>
<button className="btn btn--primary btn--lg" onClick={handleAdd} style={{ width: '100%', marginTop: 16 }}>
Προσθήκη στην παραγγελία
</button>
</div>
</div>
)
}

View File

@@ -0,0 +1,61 @@
function fmtPrice(v) {
return Number(v).toFixed(2) + ' €'
}
function ItemRow({ item, selectable, selected, onToggle }) {
const isPaid = item.status === 'paid'
const isCancelled = item.status === 'cancelled'
let opts = []
try { opts = item.selected_options ? JSON.parse(item.selected_options) : [] } catch {}
let removed = []
try { removed = item.removed_ingredients ? JSON.parse(item.removed_ingredients) : [] } catch {}
return (
<div
className={`order-item ${isPaid ? 'order-item--paid' : ''} ${isCancelled ? 'order-item--cancelled' : ''} ${selectable && selected ? 'order-item--selected' : ''}`}
onClick={selectable && !isPaid && !isCancelled ? () => onToggle(item.id) : undefined}
style={{ cursor: selectable && !isPaid && !isCancelled ? 'pointer' : 'default' }}
>
<div className="order-item__row">
{selectable && !isPaid && !isCancelled && (
<span style={{ marginRight: 8, color: selected ? '#f59e0b' : '#475569' }}>
{selected ? '☑' : '☐'}
</span>
)}
<span className="order-item__name">{item.product?.name || `#${item.product_id}`}</span>
<span className="order-item__qty">×{item.quantity}</span>
<span className="order-item__price">{fmtPrice(item.unit_price * item.quantity)}</span>
{isPaid && <span className="badge badge--paid">Πληρωμένο</span>}
{isCancelled && <span className="badge badge--cancelled">Ακυρώθηκε</span>}
</div>
{opts.map((o, i) => <div key={i} className="order-item__modifier">+ {o.name} {o.price_delta > 0 ? `(+${fmtPrice(o.price_delta)})` : ''}</div>)}
{removed.map((r, i) => <div key={i} className="order-item__modifier">- {r}</div>)}
{item.notes && <div className="order-item__modifier">📝 {item.notes}</div>}
</div>
)
}
export default function OrderSummary({ order, selectable = false, selectedIds = [], onToggle }) {
const activeItems = order.items?.filter(i => i.status !== 'cancelled') || []
const total = activeItems.reduce((s, i) => s + i.unit_price * i.quantity, 0)
return (
<div className="order-summary">
{activeItems.length === 0 && <p style={{ color: '#64748b', textAlign: 'center' }}>Δεν υπάρχουν αντικείμενα</p>}
{activeItems.map(item => (
<ItemRow
key={item.id}
item={item}
selectable={selectable}
selected={selectedIds.includes(item.id)}
onToggle={onToggle}
/>
))}
<div className="order-summary__total">
<span>Σύνολο</span>
<span>{fmtPrice(total)}</span>
</div>
</div>
)
}

View File

@@ -0,0 +1,37 @@
import { useState } from 'react'
export default function PinPad({ onSubmit, loading }) {
const [pin, setPin] = useState('')
function press(digit) {
if (pin.length < 8) setPin(p => p + digit)
}
function backspace() {
setPin(p => p.slice(0, -1))
}
function submit() {
if (pin.length > 0 && !loading) onSubmit(pin)
}
const dots = Array.from({ length: 8 }, (_, i) => (
<span key={i} style={{ fontSize: 20, color: i < pin.length ? '#f59e0b' : '#334155' }}></span>
))
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16 }}>
<div style={{ display: 'flex', gap: 10, marginBottom: 8 }}>{dots}</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 10 }}>
{[1,2,3,4,5,6,7,8,9].map(d => (
<button key={d} onClick={() => press(String(d))} className="pin-btn">{d}</button>
))}
<button onClick={backspace} className="pin-btn pin-btn--secondary"></button>
<button onClick={() => press('0')} className="pin-btn">0</button>
<button onClick={submit} className="pin-btn pin-btn--confirm" disabled={loading || pin.length === 0}>
{loading ? '…' : '✓'}
</button>
</div>
</div>
)
}

View File

@@ -0,0 +1,47 @@
import { useState } from 'react'
import ItemOptionsModal from './ItemOptionsModal'
export default function ProductPicker({ categories, products, onAdd }) {
const [activeCat, setActiveCat] = useState(categories[0]?.id ?? null)
const [selectedProduct, setSelectedProduct] = useState(null)
const filtered = products.filter(p => p.category_id === activeCat)
return (
<div className="product-picker">
<div className="category-tabs">
{categories.map(cat => (
<button
key={cat.id}
className={`cat-tab ${activeCat === cat.id ? 'cat-tab--active' : ''}`}
onClick={() => setActiveCat(cat.id)}
>
{cat.name}
</button>
))}
</div>
<div className="product-grid">
{filtered.map(product => (
<button key={product.id} className="product-btn" onClick={() => setSelectedProduct(product)}>
<span className="product-btn__name">{product.name}</span>
<span className="product-btn__price">{Number(product.base_price).toFixed(2)} </span>
</button>
))}
{filtered.length === 0 && (
<p style={{ color: '#64748b', gridColumn: '1/-1', textAlign: 'center', padding: 32 }}>
Δεν υπάρχουν προϊόντα
</p>
)}
</div>
{selectedProduct && (
<ItemOptionsModal
product={selectedProduct}
onAdd={onAdd}
onClose={() => setSelectedProduct(null)}
/>
)}
</div>
)
}

View File

@@ -0,0 +1,23 @@
export default function TableCard({ table, order, currentUserId, onClick }) {
const hasOrder = !!order
const isMyTable = hasOrder && order.waiters?.some(w => w.waiter_id === currentUserId)
let statusLabel = 'Ελεύθερο'
let cardClass = 'table-card table-card--free'
if (hasOrder && isMyTable) {
statusLabel = 'Δικό μου'
cardClass = 'table-card table-card--mine'
} else if (hasOrder) {
statusLabel = 'Ενεργό'
cardClass = 'table-card table-card--active'
}
return (
<button className={cardClass} onClick={onClick}>
<span className="table-card__number">{table.number}</span>
{table.name && <span className="table-card__name">{table.name}</span>}
<span className="table-card__status">{statusLabel}</span>
</button>
)
}