# Guide 02 — Waiter PWA (React + Vite) ## Overview A Progressive Web App installed on waiters' personal phones. Minimal, fast, touch-optimized. Works only when connected to the restaurant LAN. No offline functionality needed — show a clear error if backend is unreachable. --- ## Project Structure ``` waiter_pwa/ ├── public/ │ ├── manifest.json # PWA manifest (name, icons, display: standalone) │ └── icons/ # App icons (192x192, 512x512) ├── src/ │ ├── main.jsx │ ├── App.jsx │ ├── api/ │ │ └── client.js # Axios instance pointed at LOCAL backend IP │ ├── store/ │ │ └── authStore.js # Zustand store for auth state │ ├── pages/ │ │ ├── LoginPage.jsx │ │ ├── TableListPage.jsx │ │ ├── TableDetailPage.jsx │ │ ├── AddItemsPage.jsx │ │ └── OfflinePage.jsx │ ├── components/ │ │ ├── PinPad.jsx │ │ ├── TableCard.jsx │ │ ├── OrderSummary.jsx │ │ ├── ProductPicker.jsx │ │ ├── ItemOptionsModal.jsx │ │ └── ConnectionBanner.jsx │ └── service-worker.js # Minimal SW — caches app shell only ├── vite.config.js └── package.json ``` --- ## PWA Setup ### manifest.json ```json { "name": "TableServe", "short_name": "TableServe", "start_url": "/", "display": "standalone", "background_color": "#0f172a", "theme_color": "#0f172a", "icons": [ { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" } ] } ``` ### vite.config.js Use `vite-plugin-pwa` for service worker generation: ```js import { VitePWA } from 'vite-plugin-pwa' export default { plugins: [ VitePWA({ registerType: 'autoUpdate', workbox: { globPatterns: ['**/*.{js,css,html,ico,png,svg}'] // Cache app shell only — no API responses cached } }) ] } ``` --- ## API Client (client.js) ```js import axios from 'axios' // This base URL must point to the LOCAL backend static IP // It should be configurable — read from an env variable at build time const BASE_URL = import.meta.env.VITE_API_URL || 'http://192.168.1.10:8000' const client = axios.create({ baseURL: BASE_URL }) // Attach token to every request client.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) config.headers.Authorization = `Bearer ${token}` return config }) // On 402/423 — show license error screen // On network error — show offline screen client.interceptors.response.use( res => res, err => { if (!err.response) { // Network error = backend unreachable window.dispatchEvent(new Event('backend-offline')) } return Promise.reject(err) } ) export default client ``` --- ## Auth Flow ### First-time login (no saved user) 1. Show username input + PIN pad 2. POST `/api/auth/login` → store token + username in localStorage 3. Redirect to Table List ### Returning user (username saved) 1. Show "Welcome back, [Name]" + PIN pad only 2. Same login flow 3. "Not you?" link → clears saved username → shows full login ### PIN Pad Component - 10 digit buttons (0–9) + backspace + confirm - Large touch targets (minimum 64px) - Dots display (●●●●) as PIN is entered - No keyboard shown — native keyboard is clunky for PINs --- ## Pages ### LoginPage - Logo / app name at top - If username saved: greeting + PIN pad - If no username: username text input + PIN pad - "Logout / Switch User" link at bottom ### TableListPage - Header: waiter's name + logout icon - `ConnectionBanner` shown if backend unreachable - Grid of `TableCard` components - Each card shows: table number, status badge (Free / Active / Your Order) - Filter tabs: All | My Tables | Free Tables - Tap a table → `TableDetailPage` ### TableDetailPage - Header: "Table #X" + back button - If no active order: large "Open Order" button - If active order exists and waiter is assigned: - Order summary (list of items, quantities, prices) - Total at bottom - "Add Items" button - "Mark as Paid" button (can select specific items for partial payment) - "Close Order" button (only enabled when all items are paid) - If active order exists but waiter is NOT assigned: read-only view, no actions ### AddItemsPage - Accessed from TableDetailPage → "Add Items" - Category tabs at top (scrollable horizontal) - Product grid/list below - Tap product → `ItemOptionsModal` (select options, remove ingredients, add note, set quantity) - Staging area at bottom: items added so far (like a cart) - "Send Order" button — submits entire batch to backend, triggers printing ### ItemOptionsModal (bottom sheet) - Product name + base price - Options list (radio or checkbox depending on type) with price adjustments shown - Ingredients list with toggle to remove each - Freetext note input - Quantity stepper (+/-) - "Add to Order" button ### OfflinePage - Shown when backend is unreachable - Simple message: "Cannot reach the system. Please check your WiFi connection." - Retry button that pings `/api/system/health` --- ## UI Design Direction - **Theme**: Dark. Deep navy/slate background (`#0f172a`). This is a working tool used in dim restaurant lighting. - **Accent**: Warm amber or teal — something that reads clearly as "action" on dark backgrounds. - **Typography**: Clean, highly legible. Large touch targets. No tiny text. - **Table cards**: Color-coded by status. Free = subtle/muted. Active = accented. Your table = highlighted. - **Touch targets**: All interactive elements minimum 48px height. Prefer 64px for primary actions. - **Transitions**: Subtle slide transitions between pages. No heavy animations — this is a tool, not a showcase. --- ## State Management (Zustand) ### authStore ```js { user: null, // { id, username, role } token: null, savedUsername: null, // persisted in localStorage login(user, token), logout(), } ``` ### No complex global state needed beyond auth. - Table list: fetched on mount, local component state - Active order: fetched when opening TableDetailPage - AddItems cart: local component state, discarded on submit or back navigation --- ## Key UX Rules 1. Every destructive or confirm action requires a **second tap** (e.g. "Close Order" → confirmation sheet) 2. After "Send Order" succeeds, immediately navigate back to TableDetailPage and show updated order 3. If "Send Order" fails (network), show error toast — do NOT clear the staged items 4. Loading states on all async actions — buttons disabled while in-flight 5. "Add Items" should show a badge count of staged items so waiter doesn't lose track --- ## Installation Instructions (for deployment) Include a simple printed QR code at the restaurant pointing to `http://[LOCAL_IP]:5173` - iOS: Open in Safari → Share → Add to Home Screen - Android: Open in Chrome → Menu → Add to Home Screen