Waiter PWA: sub-choices in modal, zone filter, login JSON fix
ItemOptionsModal:
- Supports inline sub-choices on both preference choices and checkbox options;
sub-choices appear indented when the parent is selected
- Supports shared_subset at the preference-set level (shown unless the selected
choice has disables_subset)
- Pre-selects default choices and their default sub-choices on open
- Add button disabled + red validation hints until all required selections made
- Price total reflects sub-choice extra_cost
TableListPage:
- Zone filter dropdown with multi-select; filters by table group_id
(fetches /api/tables/groups alongside tables and orders)
- Fixed 'mine' and 'free' filters to compose correctly with zone filter
LoginPage:
- Switch to JSON body { username, pin } to match updated /api/auth/login
- Read user fields from data.user.* instead of flat response
vite.config.js: enable SW devOptions so PWA works in dev mode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import TableCard from '../components/TableCard'
|
||||
import ConnectionBanner from '../components/ConnectionBanner'
|
||||
@@ -11,9 +11,13 @@ const FILTER_LABELS = { all: 'Όλα', mine: 'Δικά μου', free: 'Ελεύ
|
||||
export default function TableListPage() {
|
||||
const { user, logout } = useAuthStore()
|
||||
const [tables, setTables] = useState([])
|
||||
const [groups, setGroups] = useState([])
|
||||
const [orders, setOrders] = useState([])
|
||||
const [filter, setFilter] = useState('all')
|
||||
const [offline, setOffline] = useState(false)
|
||||
const [zoneOpen, setZoneOpen] = useState(false)
|
||||
const [selectedZones, setSelectedZones] = useState(new Set())
|
||||
const zoneRef = useRef(null)
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -22,14 +26,25 @@ export default function TableListPage() {
|
||||
return () => window.removeEventListener('backend-offline', handler)
|
||||
}, [])
|
||||
|
||||
// Close zone dropdown on outside click
|
||||
useEffect(() => {
|
||||
function onClick(e) {
|
||||
if (zoneRef.current && !zoneRef.current.contains(e.target)) setZoneOpen(false)
|
||||
}
|
||||
document.addEventListener('mousedown', onClick)
|
||||
return () => document.removeEventListener('mousedown', onClick)
|
||||
}, [])
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const [tablesRes, ordersRes] = await Promise.all([
|
||||
const [tablesRes, ordersRes, groupsRes] = await Promise.all([
|
||||
client.get('/api/tables/'),
|
||||
client.get('/api/orders/my'),
|
||||
client.get('/api/tables/groups'),
|
||||
])
|
||||
setTables(tablesRes.data)
|
||||
setOrders(ordersRes.data)
|
||||
setGroups(groupsRes.data)
|
||||
setOffline(false)
|
||||
} catch {}
|
||||
}
|
||||
@@ -40,10 +55,19 @@ export default function TableListPage() {
|
||||
return orders.find(o => o.table_id === tableId && ['open', 'partially_paid'].includes(o.status))
|
||||
}
|
||||
|
||||
function toggleZone(id) {
|
||||
setSelectedZones(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(id)) next.delete(id); else next.add(id)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const filtered = tables.filter(t => {
|
||||
const order = getOrder(t.id)
|
||||
if (filter === 'free') return !order
|
||||
if (filter === 'mine') return order && order.waiters?.some(w => w.waiter_id === user?.id)
|
||||
if (filter === 'free' && order) return false
|
||||
if (filter === 'mine' && !(order && order.waiters?.some(w => w.waiter_id === user?.id))) return false
|
||||
if (selectedZones.size > 0 && !selectedZones.has(t.group_id ?? 'none')) return false
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -52,6 +76,8 @@ export default function TableListPage() {
|
||||
navigate('/login')
|
||||
}
|
||||
|
||||
const zoneActive = selectedZones.size > 0
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<header className="top-bar">
|
||||
@@ -68,6 +94,66 @@ export default function TableListPage() {
|
||||
{FILTER_LABELS[f]}
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Zone filter */}
|
||||
<div ref={zoneRef} style={{ position: 'relative' }}>
|
||||
<button
|
||||
className={`filter-tab ${zoneActive ? 'filter-tab--active' : ''}`}
|
||||
onClick={() => setZoneOpen(o => !o)}
|
||||
>
|
||||
Ζώνη{zoneActive ? ` (${selectedZones.size})` : ''}
|
||||
</button>
|
||||
{zoneOpen && (
|
||||
<div style={{
|
||||
position: 'absolute', top: '110%', right: 0, zIndex: 100,
|
||||
background: '#fff', border: '1px solid #e2e8f0', borderRadius: 12,
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,0.12)', minWidth: 180, padding: 8,
|
||||
}}>
|
||||
<button
|
||||
onClick={() => setSelectedZones(new Set())}
|
||||
style={{
|
||||
display: 'block', width: '100%', textAlign: 'left',
|
||||
padding: '8px 12px', borderRadius: 8, fontSize: 14,
|
||||
color: selectedZones.size === 0 ? '#fff' : '#374151',
|
||||
background: selectedZones.size === 0 ? '#4f46e5' : 'transparent',
|
||||
border: 'none', cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Όλες οι ζώνες
|
||||
</button>
|
||||
{groups.map(g => (
|
||||
<button
|
||||
key={g.id}
|
||||
onClick={() => toggleZone(g.id)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8, width: '100%',
|
||||
textAlign: 'left', padding: '8px 12px', borderRadius: 8, fontSize: 14,
|
||||
color: selectedZones.has(g.id) ? '#fff' : '#374151',
|
||||
background: selectedZones.has(g.id) ? '#4f46e5' : 'transparent',
|
||||
border: 'none', cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{g.color && <span style={{ width: 10, height: 10, borderRadius: '50%', background: g.color, display: 'inline-block', flexShrink: 0 }} />}
|
||||
{g.name}
|
||||
</button>
|
||||
))}
|
||||
{tables.some(t => !t.group_id) && (
|
||||
<button
|
||||
onClick={() => toggleZone('none')}
|
||||
style={{
|
||||
display: 'block', width: '100%', textAlign: 'left',
|
||||
padding: '8px 12px', borderRadius: 8, fontSize: 14,
|
||||
color: selectedZones.has('none') ? '#fff' : '#374151',
|
||||
background: selectedZones.has('none') ? '#4f46e5' : 'transparent',
|
||||
border: 'none', cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Χωρίς ζώνη
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="table-grid">
|
||||
|
||||
Reference in New Issue
Block a user