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,167 @@
import { useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import OrderSummary from '../components/OrderSummary'
import useAuthStore from '../store/authStore'
import client from '../api/client'
export default function TableDetailPage() {
const { tableId } = useParams()
const { user } = useAuthStore()
const navigate = useNavigate()
const [table, setTable] = useState(null)
const [order, setOrder] = useState(null)
const [loading, setLoading] = useState(true)
const [paying, setPaying] = useState(false)
const [selectedIds, setSelectedIds] = useState([])
const [confirmClose, setConfirmClose] = useState(false)
const [error, setError] = useState('')
async function load() {
setLoading(true)
try {
const { data } = await client.get(`/api/tables/${tableId}/status`)
setTable(data.table)
if (data.active_order_id) {
const { data: o } = await client.get(`/api/orders/${data.active_order_id}`)
setOrder(o)
} else {
setOrder(null)
}
} catch (err) {
setError('Σφάλμα φόρτωσης')
} finally {
setLoading(false)
}
}
useEffect(() => { load() }, [tableId])
const isMyOrder = order && (
order.opened_by === user?.id || order.waiters?.some(w => w.waiter_id === user?.id)
)
async function openOrder() {
try {
await client.post('/api/orders/', { table_id: Number(tableId) })
await load()
} catch (err) {
setError(err.response?.data?.detail || 'Σφάλμα ανοίγματος')
}
}
async function paySelected() {
if (selectedIds.length === 0) return
setPaying(true)
try {
await client.post(`/api/orders/${order.id}/pay`, { item_ids: selectedIds })
setSelectedIds([])
await load()
} catch {
setError('Σφάλμα πληρωμής')
} finally {
setPaying(false)
}
}
async function closeOrder() {
try {
await client.post(`/api/orders/${order.id}/close`)
setConfirmClose(false)
navigate('/tables')
} catch (err) {
setError(err.response?.data?.detail || 'Σφάλμα κλεισίματος')
}
}
function toggleItem(id) {
setSelectedIds(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id])
}
const allPaid = order && order.items?.filter(i => i.status === 'active').length === 0
&& order.items?.some(i => i.status === 'paid')
if (loading) return <div className="page page--centered"><p style={{ color: '#94a3b8' }}>Φόρτωση</p></div>
return (
<div className="page">
<header className="top-bar">
<button className="icon-btn" onClick={() => navigate('/tables')}></button>
<span className="top-bar__title">Τραπέζι {table?.number}</span>
<button className="icon-btn" onClick={load}></button>
</header>
{error && <p className="error-msg" style={{ margin: 12 }}>{error}</p>}
{!order && (
<div className="page page--centered">
<p style={{ color: '#94a3b8', marginBottom: 24 }}>Δεν υπάρχει ενεργή παραγγελία</p>
<button className="btn btn--primary btn--lg" onClick={openOrder}>
Άνοιγμα Παραγγελίας
</button>
</div>
)}
{order && (
<div className="detail-body">
<OrderSummary
order={order}
selectable={isMyOrder && !paying}
selectedIds={selectedIds}
onToggle={toggleItem}
/>
{isMyOrder && (
<div className="action-bar">
<button className="btn btn--accent" onClick={() => navigate(`/tables/${tableId}/add`)}>
+ Προσθήκη
</button>
<button
className="btn btn--success"
onClick={paySelected}
disabled={selectedIds.length === 0 || paying}
>
{paying ? '…' : `Πληρωμή (${selectedIds.length})`}
</button>
<button
className="btn btn--danger"
onClick={() => setConfirmClose(true)}
disabled={!allPaid}
>
Κλείσιμο
</button>
</div>
)}
{!isMyOrder && (
<p style={{ textAlign: 'center', color: '#64748b', padding: 16 }}>
Ανάγνωση μόνο άλλος σερβιτόρος
</p>
)}
</div>
)}
{confirmClose && (
<div className="modal-overlay" onClick={() => setConfirmClose(false)}>
<div className="modal-sheet" onClick={e => e.stopPropagation()}>
<div className="modal-handle" />
<h2 className="modal-title">Κλείσιμο παραγγελίας;</h2>
<p style={{ color: '#94a3b8', textAlign: 'center', marginBottom: 24 }}>
Αυτή η ενέργεια δεν αναιρείται.
</p>
<div style={{ display: 'flex', gap: 12 }}>
<button className="btn btn--secondary" style={{ flex: 1 }} onClick={() => setConfirmClose(false)}>
Άκυρο
</button>
<button className="btn btn--danger" style={{ flex: 1 }} onClick={closeOrder}>
Κλείσιμο
</button>
</div>
</div>
</div>
)}
</div>
)
}