Phase 1: scaffold local backend — models, schemas, routers, printer service, Docker
This commit is contained in:
227
PLANS AND STRATEGIES/02_WAITER_PWA.md
Normal file
227
PLANS AND STRATEGIES/02_WAITER_PWA.md
Normal file
@@ -0,0 +1,227 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user