Files
simple-pos-system/PLANS AND STRATEGIES/02_WAITER_PWA.md

7.0 KiB
Raw Blame History

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

{
  "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:

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)

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 (09) + 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

{
  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