Phase 2: scaffold Waiter PWA — React+Vite, PWA manifest, all pages and components
This commit is contained in:
167
waiter_pwa/src/pages/TableDetailPage.jsx
Normal file
167
waiter_pwa/src/pages/TableDetailPage.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user