Skip to content

RuumiDev/wz-web-app

Repository files navigation

WingZone Web App — Meru Branch POS

A full-stack food-ordering and point-of-sale (POS) web application for WingZone Meru Branch.
Customers order online; kitchen staff receive real-time tickets and print ESC/POS receipts — no proprietary hardware required.

Live URL: https://wingzone-web-app.web.app


Table of Contents

  1. Abstract
  2. Problem Statement
  3. Solution Overview
  4. Tech Stack
  5. System Architecture
  6. Firestore Database Schema
  7. Payment Flow (ToyyibPay FPX)
  8. User Flow — Customer
  9. User Flow — Admin / Staff
  10. Project Structure
  11. Environment Variables
  12. Getting Started
  13. Deployment
  14. QZ Tray Thermal Printing
  15. Event Menu System
  16. Firestore Security Rules
  17. Known Constraints & Limitations
  18. Completed Features
  19. Future Improvements

Abstract

WingZone Web App is a Progressive Web App (PWA-ready) that replaces paper-based and verbal ordering at WingZone Meru Branch. It provides a customer-facing digital menu with real-time order placement and tracking, and an admin-facing operations dashboard for order management, receipt printing, menu administration, and sales reporting — all hosted on Firebase with zero ongoing server costs on the Spark plan and minimal cost on Blaze (Cloud Functions used only for signing QZ Tray print requests).

The system is purpose-built for a single branch but designed with multi-branch expansion in mind. It handles the complete ordering lifecycle: menu browsing → customisation → FPX payment → kitchen receipt print → order status pipeline → customer notification.


Problem Statement

WingZone Meru Branch faced several operational pain points:

Pain Point Impact
No digital ordering — orders taken verbally or by paper slip Error-prone, slow during peak hours
No real-time visibility of order queue for kitchen Staff had to walk to counter repeatedly
Thermal receipt printing required expensive proprietary POS terminals High capital cost
No data on daily/weekly sales without manual tallying No business intelligence
Special events required a completely separate manual menu Confusing for both staff and customers
No customer loyalty or voucher mechanism Missed retention opportunity

Solution Overview

A hosted web app that:

  • Customers access from any smartphone browser — no app install required. They browse a live menu, customise items (flavours, sides, beverages, bone type, dipping sauces), pay via FPX bank transfer (ToyyibPay), and track their order status in real time.
  • Kitchen / Cashier receive instant Firestore push updates on the admin dashboard. A browser tab running on the cashier PC auto-plays an alert tone, auto-prints an ESC/POS kitchen receipt via QZ Tray, and advances the order to Preparing — no manual intervention needed for confirmed orders.
  • Admin manages the full menu CRUD, store availability, event menus, discount vouchers, user accounts, and reviews from the same dashboard.

Target Users

Role Route Auth Required
Customer / No (anonymous checkout supported)
Registered Customer /profile, /order/[id] Firebase Auth (Google)
Admin / Staff /admin/* Firebase Auth + Firestore admin record

Tech Stack

Layer Technology Notes
Framework Next.js 14 (App Router) Server Components + Client Components
Language TypeScript Strict mode
Styling Tailwind CSS Custom brand tokens (primary, deep-red, wz-text)
Animations Framer Motion Page transitions, card stagger, sidebar
Database Firebase Firestore Real-time listeners on orders + menu
Auth Firebase Authentication Google OAuth; admin gate via Firestore record
File Storage Firebase Storage Menu item images; proxy-uploaded via /api/upload
Hosting Firebase Hosting CDN-backed static + SSR via Cloud Functions Gen 2
Thermal Printing QZ Tray 2.x + ESC/POS Local desktop bridge; signed via RSA-SHA512
Payments ToyyibPay (FPX) Malaysian FPX bank transfer; webhook confirmation
Admin SDK firebase-admin Singleton on webhook route; Cloud Run Node.js runtime
UI Lucide React, React Hot Toast, SweetAlert2 Icons, toasts, confirmation dialogs
Excel SheetJS (xlsx) Menu bulk import + daily sales export
Image Crop react-image-crop v11 4:3 crop modal; canvas output capped at 600 px WebP 60%
PDF jsPDF E-receipt PDF generation (pre-generated on mount, iOS Share Sheet safe)
Charts Planned

System Architecture

Browser (Customer / Admin)
        │
        │  HTTPS
        ▼
┌─────────────────────────────────────┐
│         Firebase Hosting            │
│   (Next.js 14 — App Router SSR)     │
│                                     │
│  ┌─────────────────────────────┐   │
│  │  Next.js API Routes          │   │
│  │  /api/checkout               │   │  ──► ToyyibPay API (create bill)
│  │  /api/payment/callback        │   │  ◄── ToyyibPay webhook (payment result)
│  │  /api/qz-sign                │   │  ──► RSA-SHA512 signed response
│  │  /api/upload                 │   │  ──► Firebase Storage (REST)
│  └─────────────────────────────┘   │
└─────────────────────────────────────┘
        │                    │
        │  Firestore SDK      │  Firebase Admin SDK
        ▼                    ▼
┌──────────────┐    ┌────────────────┐
│  Firestore   │    │ Firebase Admin │
│  (real-time) │    │ (webhook only) │
└──────────────┘    └────────────────┘
        │
        │  WebSocket (localhost)
        ▼
┌──────────────┐
│   QZ Tray    │  (running on cashier PC)
│  (2.x local) │
└──────────────┘
        │  USB / Serial
        ▼
┌──────────────┐
│ Thermal POS  │
│   Printer    │
└──────────────┘

Key design decisions:

  • Image uploads go through /api/upload (server-side proxy) to avoid CORS issues with Firebase Storage from localhost during development.
  • QZ Tray signing is done server-side (/api/qz-sign) so the RSA private key never reaches the browser.
  • ToyyibPay webhook uses multipart/form-data — parsed with a regex field extractor since Next.js request.formData() handles it natively.

Firestore Database Schema

Collections

menuItems/{id}

{
  name: string
  type: 'combo' | 'alacarte' | 'promotion' | 'pack'
  category: MenuCategory          // 'Combo Meals' | 'Wings' | 'Tenders' | ...
  basePrice: number               // RM
  description: string
  imageUrl?: string               // Firebase Storage URL
  displayOrder?: number           // Lower = shown first
  isAvailable: boolean
  kitchenIngredients: KitchenIngredient[]
  customizationRequirements: CustomizationRequirements
  availableFlavors: string[]
  components: ComponentCategory[]
  defaultSide?: string
  basketUpcharge?: number         // RM upcharge for basket add-on
  voucherAmount?: number          // Fixed RM deduction (promotion type only)
  isTimeRestricted?: boolean
  startTime?: string              // 'HH:mm' MYT
  endTime?: string                // 'HH:mm' MYT
  menuContext?: 'outlet' | 'event'
  createdAt: Timestamp
  updatedAt: Timestamp
}

orders/{id}

{
  orderId: string                 // Human-readable e.g. "WZ-1714123456"
  recipientName: string
  items: OrderItem[]
  kitchenSummary: {               // Pre-aggregated for thermal receipt
    Mains: Record<string, number>
    Sides: Record<string, number>
    Drinks: Record<string, number>
    Dippings: Record<string, number>
  }
  totalPrice: number              // Final charged amount
  subtotal?: number               // Pre-discount total
  discountPct?: number            // App exclusive discount %
  discountAmount?: number         // RM saved from app discount
  totalVoucher?: number           // RM saved from promotion items
  customDiscountAmt?: number      // RM saved from voucher code
  voucherCode?: string            // e.g. "RAYA2026"
  totalSavings?: number           // Sum of all savings
  orderStatus: OrderStatus        // See status pipeline below
  paymentMethod: 'cash' | 'online' | 'fpx'
  pickupTime?: string
  isGroupOrder: boolean
  customerEmail?: string
  phone?: string
  hasPrinted?: boolean
  hasRated?: boolean
  rating?: number
  ratingFeedback?: string
  menuContext?: 'outlet' | 'event'
  eventName?: string
  createdAt: Timestamp
  updatedAt: Timestamp
}

Order Status Pipeline:

awaitingPayment ──(ToyyibPay webhook)──► confirmed
                                              │
                                    (auto 30 s via useOrderNotifier)
                                              │
                                           preparing
                                              │
                                        (admin action)
                                              │
                                            ready
                                              │
                                        (admin action)
                                              │
                                          delivered

Cash orders enter at pending and are manually confirmed by admin.

settings/discount

{ discountPct: number }   // App-exclusive % discount applied at checkout (public read)

settings/availability

{ GlobalAvailability }    // Per-flavour, per-side, per-beverage availability toggles

settings/store

{ isOpen: boolean, storeName: string, hours: StoreHours, ... }

app_config/event_settings

{
  isEventModeEnabled: boolean
  eventName: string
  startDate: string    // ISO datetime
  endDate: string      // ISO datetime
}

users/{uid}

{ name: string, phone: string, email: string, createdAt: Timestamp }

admins/{uid}

{ email: string }    // Presence = admin access granted

vouchers/{id}

{
  code: string              // Uppercase e.g. "LAUNCH5"
  discountPercentage: number
  isActive: boolean
  singleUsePerAccount: boolean
  usedBy: string[]          // UIDs that have redeemed this code
  createdAt: Timestamp
}

reviews/{id}

Auto-populated from order rating submissions.


Payment Flow (ToyyibPay FPX)

Customer taps "Pay with FPX"
        │
        ▼
POST /api/checkout
  ├── Validates env vars (API key, category code, app URL)
  ├── Converts amount to cents (RM → toyyibpay integer)
  ├── Creates bill via ToyyibPay REST API
  │     billReturnUrl  = /payment/success
  │     billCallbackUrl = /api/payment/callback
  └── Returns { checkoutUrl }
        │
        ▼
Browser redirects to toyyibpay.com/{BillCode}
Customer completes FPX bank transfer
        │
        ├── ToyyibPay POSTs to /api/payment/callback (multipart/form-data)
        │     billcode, status_id, order_id, msg, transaction_id
        │     status_id = 1 → Payment successful
        │         ├── Firestore: order.orderStatus = 'confirmed'
        │         └── useOrderNotifier picks up → ding + auto-print + 30s auto-advance
        │
        └── Browser redirects to /payment/success
              Shows order confirmation + link to tracking page

Error resilience:

  • If ToyyibPay returns HTML (maintenance mode) instead of JSON, /api/checkout parses raw text first and returns a 502 with a user-friendly message.
  • Webhook is idempotent — if called twice for the same order, the update is a no-op.

User Flow — Customer

1. Open https://wingzone-web-app.web.app
       │
       ├── [Event Mode active?] ──YES──► MenuSelectionModal
       │                                  Customer picks Outlet or Event menu
       │                                  Choice saved to sessionStorage
       │
       ▼
2. Browse menu
   ├── Category pills (sticky, smooth-scroll, active pill auto-scrolls into view)
   ├── Item cards with image (blur placeholder → loaded → animate-pulse removed)
   ├── Sold Out overlay on unavailable items
   └── Promotion badge ("Available" / time remaining) on time-restricted items

3. Tap item → CustomisationModal
   ├── Select bone type, flavour(s), side, dipping, beverage
   ├── Optional: basket add-on, extra dipping
   ├── Special instructions
   └── Real-time price update → "Add to Order" button

4. Floating Order Widget (bottom of screen)
   ├── Item count badge
   ├── Expand to see full cart
   └── "Checkout" button

5. Checkout sheet
   ├── Name / phone / pickup time
   ├── Voucher code input (requires login)
   ├── Pricing breakdown: Subtotal → App Exclusive → Promo → Voucher → Total
   ├── Payment method: FPX (online) or Cash
   └── Place Order

6. FPX path → redirected to ToyyibPay → bank transfer
   Cash path → order saved as 'pending' immediately

7. /payment/success → confirmation screen

8. /order/[id] — real-time tracking
   ├── 4-step animated pipeline: Pending → Preparing → Ready → Delivered
   ├── Save / Share E-Receipt button (jsPDF pre-generated on mount; iOS Share Sheet safe)
   ├── "I'm Here!" button when status = Ready (triggers Delivered)
   └── Post-delivery star rating widget

User Flow — Admin / Staff

1. /admin/login — Google sign-in
   Firestore checks admins/{uid} → redirect to /admin on success

2. /admin — Dashboard
   ├── Today's order count, revenue, pending orders
   ├── Daily Sales Report table with Gateway Fee + Net Banked columns
   └── Excel export

3. /admin/orders — Live Order Feed
   ├── Firestore onSnapshot listener — new orders appear instantly
   ├── "Advance" button: pending→preparing, preparing→ready, ready→delivered
   ├── Print Receipt button → QZ Tray ESC/POS (if QZ Tray running)
   ├── Cancel Order button
   └── useOrderNotifier (background):
         ├── Listens for status=confirmed + hasPrinted=false
         ├── Plays ding.mp3 alert
         ├── Auto-prints kitchen receipt via QZ Tray
         ├── Marks hasPrinted=true
         └── After 30 s → advances to 'preparing'

4. /admin/menu — Menu Management
   ├── Filter pills: All / Outlet / Event
   ├── Add / Edit / Delete items
   ├── Image upload (crop modal → 600px WebP 60% → /api/upload → Firebase Storage)
   ├── Toggle availability per item
   └── Set menuContext (Outlet / Event)

5. /admin/availability — Global Availability
   Toggles for bone types, flavours, beverages, sides, dipping sauces

6. /admin/event-settings — Event Menu Control
   ├── Enable / disable Event Mode
   ├── Set event name
   └── Set optional start/end datetime window

7. /admin/vouchers — Voucher Codes
   ├── Create voucher (code, discount %, single-use toggle)
   └── Activate / deactivate

8. /admin/users — Customer Accounts
9. /admin/reviews — Star Ratings + Feedback
10. /admin/import — Bulk Excel Menu Import
11. /admin/settings — Branch info, operating hours, notification preferences

Project Structure

WzWebApp/
├── app/
│   ├── page.tsx                    # Customer menu — main entry point
│   ├── layout.tsx                  # Root layout + CartContext + GlobalLoader
│   ├── loading.tsx                 # Slot-machine GlobalLoader
│   ├── not-found.tsx               # Branded 404 with mascot
│   ├── globals.css                 # Tailwind base + custom form utilities
│   ├── about/page.tsx
│   ├── profile/page.tsx            # Customer profile (name, phone, edit form)
│   ├── order/[id]/page.tsx         # Real-time order tracking + e-receipt PDF
│   ├── payment/success/page.tsx    # Post-FPX landing page
│   ├── admin/
│   │   ├── layout.tsx              # Admin shell — sidebar, topbar, logout confirm
│   │   ├── page.tsx                # Dashboard — sales summary + report
│   │   ├── login/page.tsx          # Google sign-in gate
│   │   ├── orders/page.tsx         # Live order feed + receipt printing
│   │   ├── menu/page.tsx           # Menu CRUD + context filter
│   │   ├── availability/page.tsx   # Global ingredient availability
│   │   ├── event-settings/page.tsx # Event Mode toggle + config
│   │   ├── settings/page.tsx       # Branch settings + notification prefs
│   │   ├── users/page.tsx          # Customer account list
│   │   ├── reviews/page.tsx        # Rating + feedback viewer
│   │   ├── vouchers/page.tsx       # Voucher code management
│   │   └── import/page.tsx         # Bulk Excel menu import
│   └── api/
│       ├── checkout/route.ts       # ToyyibPay bill creation
│       ├── payment/callback/       # ToyyibPay webhook (multipart parser)
│       ├── qz-sign/route.ts        # RSA-SHA512 QZ Tray signing
│       └── upload/route.ts         # Image proxy upload → Firebase Storage
│
├── components/
│   ├── CustomerHeader.tsx          # Sticky header (logo + nav + sign-out)
│   ├── StoreStatusBanner.tsx       # Open/closed banner (dismissible)
│   ├── FloatingOrderWidget.tsx     # Persistent cart bottom widget
│   ├── ExclusivityToast.tsx        # App-exclusive discount toast
│   ├── MenuSelectionModal.tsx      # Outlet vs Event menu chooser
│   ├── admin/
│   │   ├── MenuItemModal.tsx       # Add/edit item modal (crop + upload)
│   │   └── OrderListener.tsx       # Firestore order listener component
│   └── ui/
│       ├── Loader.tsx              # GlobalLoader + ButtonSpinner
│       ├── PageWrapper.tsx         # Framer Motion page fade wrapper
│       └── reveal-loader.tsx       # Slot-machine word animation
│
├── context/
│   └── CartContext.tsx             # Global cart state (items, totals, helpers)
│
├── hooks/
│   ├── useScrollSpy.ts             # Scroll-event category spy (V4, rAF debounced)
│   ├── useOrderNotifier.ts         # Auto-ding + auto-print + 30s auto-advance
│   ├── useEventGatekeeper.ts       # Firestore listener → MenuSelectionModal control
│   └── usePromoTime.ts             # Time-restricted promotion active/inactive state
│
├── lib/
│   ├── firebase.ts                 # Firebase client SDK init
│   ├── firebase-admin.ts           # Firebase Admin SDK singleton
│   ├── qzService.ts                # QZ Tray connection + signing + ESC/POS helpers
│   └── qz-tray.ts                  # QZ Tray JS library loader
│
├── types/
│   └── index.ts                    # All shared TypeScript interfaces + enums
│
├── public/
│   ├── certs/digital-certificate.txt  # QZ Tray self-signed cert (served publicly)
│   ├── WingzoneLogo.png
│   ├── wingmascot.png
│   ├── mascotComingSoon.png        # Fallback menu image
│   ├── ding.mp3                    # Order alert tone
│   └── flavors/                    # Flavour artwork (PNG)
│
├── private/                        # .gitignored
│   └── private-key.pem             # RSA key for QZ Tray signing
│
├── firestore.rules
├── storage.rules
├── firebase.json
├── next.config.mjs
├── tailwind.config.ts
└── package.json

Environment Variables

Copy .env.local.example.env.local. Never commit .env.local.

# Firebase client SDK
NEXT_PUBLIC_FIREBASE_API_KEY=
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
NEXT_PUBLIC_FIREBASE_PROJECT_ID=
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
NEXT_PUBLIC_FIREBASE_APP_ID=

# App base URL (used for ToyyibPay return/callback URLs)
NEXT_PUBLIC_APP_URL=https://wingzone-web-app.web.app

# QZ Tray — RSA private key (single-line, \n-escaped)
QZ_PRIVATE_KEY=

# ToyyibPay
TOYYIBPAY_SECRET_KEY=
TOYYIBPAY_CATEGORY_CODE=

For Firebase production, store QZ_PRIVATE_KEY as a secret:

firebase functions:secrets:set QZ_PRIVATE_KEY

Getting Started

# Install dependencies
npm install

# Run development server
npm run dev

Deployment

# Build + deploy to Firebase Hosting (Next.js SSR via Cloud Functions Gen 2)
firebase deploy --only hosting

Requires:

  • firebase login
  • .firebaserc linked to your project
  • QZ_PRIVATE_KEY secret set in Firebase (firebase functions:secrets:set QZ_PRIVATE_KEY)

QZ Tray Thermal Printing

How it works

  1. The cashier PC runs QZ Tray 2.x as a background system service.
  2. On a confirmed order (or manual print click), the web app:
    • Fetches the signing cert from /certs/digital-certificate.txt
    • POST /api/qz-sign — server signs the request with the RSA-SHA512 private key
    • Sends signed ESC/POS commands to QZ Tray over wss://localhost:8181
  3. QZ Tray forwards commands to the connected USB/Serial thermal printer.

First-time setup (new cashier PC)

  1. Install QZ Tray 2.x
  2. Open the web app and trigger a print
  3. In the QZ trust dialog: check "Remember this decision" → click Allow
  4. Done — cert is served from the live URL, no local file setup

Auto-print on confirmed orders (useOrderNotifier)

  • Subscribes to Firestore orders where orderStatus === 'confirmed' and hasPrinted === false
  • Plays ding.mp3 alert
  • Prints kitchen receipt immediately
  • Sets hasPrinted = true
  • After 30 seconds, advances order to preparing automatically

Event Menu System

Allows the store to activate a separate curated menu for special events (pop-ups, catering, limited-time drops) without replacing the regular Outlet Menu.

Activation flow

  1. Admin → Event Settings: toggle Event Mode on, set event name, optionally set start/end datetime.
  2. Firestore app_config/event_settings is updated.
  3. useEventGatekeeper on the customer page detects the change in real time.
  4. If no session choice exists, MenuSelectionModal appears — full-screen overlay asking the customer to choose Outlet or [Event Name] menu.
  5. Choice is saved to sessionStorage for the tab's lifetime.
  6. An event escape hatch banner appears below the header when viewing the Event Menu — one tap returns to the Outlet Menu (cart cleared after SweetAlert2 confirmation).

Per-item setup

Each menu item has menuContext: 'outlet' | 'event' set via the Menu Context segmented control in the item editor. The admin Menu page has All / Outlet / Event filter pills for quick sorting.


Firestore Security Rules

menuItems      — public read, auth-required write
settings/*     — public read (menu + discount), auth-required write
orders         — public create (anonymous checkout), auth-required read/update/delete
everything else — auth-required read + write

Orders allow unauthenticated create so guests can place orders. All sensitive operations (reading order lists, updating status, managing menu) require Firebase Authentication.


Known Constraints & Limitations

Constraint Detail
QZ Tray dependency Must be running on the cashier PC for auto-print to work. It is a local desktop app — not cloud-based.
Admin auth Auth is a Firestore admins/{uid} presence check, not Firebase Custom Claims. The admin record must be seeded manually.
ToyyibPay webhook format ToyyibPay POSTs multipart/form-data. If they change the payload format, the field parser in the webhook route needs updating.
Canvas re-encoding Image uploads go through a <canvas> crop step, which always decompresses → re-encodes. Output is capped at 600 px WebP 60% to keep sizes below ~80 KB.
jsPDF e-receipt PDF is text-only (no image of the page). Complex layouts are approximated with manual y-coordinate tracking.
Single branch The data model has no branch scoping — adding a second branch would require a significant schema change.
No offline support Firestore persistence is enabled but the app has no Service Worker / offline fallback shell.
ToyyibPay HTML error responses On gateway maintenance, ToyyibPay returns HTML instead of JSON. The checkout route handles this with a raw text → safe JSON.parse guard and returns a user-friendly 502.
iOS Share Sheet navigator.share({ files }) only works from a user-initiated synchronous click. The e-receipt PDF is pre-generated on useEffect mount and stored in state so the button handler has zero await before calling navigator.share.

Completed Features

  • Full customer menu with live Firestore data, category navigation (scroll-spy V4), and customisation modal
  • Cart context, floating order widget, and order submission
  • Real-time order tracking page with animated 4-step pipeline
  • User profile management with unsaved-changes detection
  • Sign-out confirmation (SweetAlert2) on both customer profile and admin sidebar
  • All admin pages: orders, menu CRUD, availability, settings, users, reviews, daily report, import, vouchers, event settings
  • ESC/POS receipt printing via QZ Tray with server-side RSA-SHA512 signing
  • useOrderNotifier — ding + auto-print + 30 s auto-advance on confirmed orders
  • ToyyibPay FPX payment integration — bill creation, redirect, webhook confirmation
  • Webhook correctly parses multipart/form-data; handles HTML error responses gracefully
  • Image upload pipeline: browser crop (600 px WebP 60%) → /api/upload proxy → Firebase Storage
  • E-receipt PDF (jsPDF, pre-generated on mount, iOS Share Sheet + desktop download fallback)
  • App-exclusive discount: Firestore settings/discount, shown in checkout breakdown and e-receipt
  • Voucher codes: per-account single-use enforcement, displayed in checkout + e-receipt
  • Event Menu System: dual-menu modal, per-item menuContext, escape hatch banner
  • ExclusivityToast — discount notification (only shown after discount data loads)
  • Framer Motion animations: page fade, menu card stagger, sidebar slide, banner dismiss
  • GlobalLoader (slot-machine word animation) + ButtonSpinner
  • Branded 404 page and mascotComingSoon.png fallback for items without images
  • Menu image blur placeholder (neutral gray 1×1 PNG) + animate-pulse skeleton + priority on first 6 cards
  • Category pill bar: sticky, auto-scrolls active pill into view on mobile, centered on desktop
  • Daily Sales Report: Gateway Fee + Net Banked columns; full Excel export
  • Admin menu context filter pills (All / Outlet / Event)
  • Time-restricted promotions: "Available" badge during active window
  • Phone input min-w-0 fix (prevents bleed on mobile edit profile)
  • Firebase production deployment (Hosting + Cloud Functions Gen 2)

Future Improvements

Priority Feature Notes
High Push notifications (FCM) Notify customers when order is Ready; notify staff on new orders without needing a browser tab open
High ToyyibPay webhook polling fallback In case webhook delivery fails, poll ToyyibPay bill status every 30 s as a safety net
Medium Customer order history Paginated list on /profile of all past orders with re-order shortcut
Medium Multi-branch support Scope Firestore documents by branchId; branch selector on admin login
Medium PWA manifest + Service Worker Full offline shell so the menu loads without network; install-to-home-screen on mobile
Medium Firebase Custom Claims for admin Replace Firestore admins collection check with proper Firebase Auth custom claims
Low Automated CI GitHub Actions: next build + tsc --noEmit + eslint on every PR
Low Sales analytics charts Week/month revenue graphs on the admin dashboard
Low Event Menu date/time auto-activation Cloud Function scheduled trigger to flip isEventModeEnabled at the configured datetime, instead of relying on the admin to toggle manually
Low Loyalty / points system Award points per order, redeemable as discount
Low Table QR codes Per-table QR that pre-fills table number at checkout

Target Users

Role Access
Customer Menu browsing, ordering, order tracking, profile management
Admin / Staff Orders dashboard, menu management, store settings, reporting

Features

Customer-Facing

  • Live Menu with category navigation (smooth scroll + sticky category pill bar)
  • Item Customisation Modal — flavour, side, and beverage selections with real-time price calculation
  • Floating Order Widget — persistent cart summary visible across the menu
  • Real-time Order Tracking — order status updates pushed via Firestore listeners
  • User Profile — display name, phone, address management with unsaved-changes detection
  • Store Status Banner — shows open/closed status, dismissible with animation
  • Branded 404 Page — custom not-found page with mascot
  • Fallback Item ImagemascotComingSoon.png shown for items without uploaded photos
  • Image Loading Overlay — fullscreen overlay held until all menu card images resolve (no flash of missing images)

Payments

  • ToyyibPay FPX Online Payment — customers pay via FPX bank transfer at checkout
  • Background Webhook (/api/payment/callback) — ToyyibPay POSTs multipart/form-data after every payment attempt; webhook verifies status and advances order to confirmed
  • Firebase Admin SDK on the webhook route — direct Firestore write without going through the client SDK

Admin Panel (/admin)

  • Orders Dashboard — live order feed with Firestore real-time listener, advance status button with spinner feedback
  • Payment Status FlowAwaiting Payment → (webhook) → Confirmed → (auto 30 s) → Preparing → (admin) → Ready → (admin) → Delivered
  • Auto-print + ding on ConfirmeduseOrderNotifier listens for new confirmed + hasPrinted=false orders, plays alert tone, auto-prints ESC/POS receipt via QZ Tray, then advances to Preparing after 30 s
  • Menu Management — add/edit/delete menu items, upload item images (Firebase Storage), toggle availability
  • Store Availability — toggle store open/closed status
  • Store Settings — update branch info, operating hours
  • User Management — view all registered customer accounts
  • Reviews — view customer ratings and feedback
  • Daily Sales Report — aggregate order totals by day with Gateway Fee (RM) and Net Banked (RM) columns; Excel export includes all columns
  • Menu Import — bulk import menu items via Excel (.xlsx)
  • Event Settings — toggle Event Mode on/off, set event name and active date/time window; live status banner shows current state
  • Vouchers — manage discount voucher codes
  • Admin Login — dedicated /admin/login route with Firebase Authentication

Printing

  • ESC/POS Receipt Printing via QZ Tray 2.x
  • Server-side SHA-512 RSA request signing (/api/qz-sign)
  • Certificate served from /certs/digital-certificate.txt — no per-device file setup needed

Event Menu System

  • Dual-menu selection modal (MenuSelectionModal) — full-screen overlay lets customers choose between the regular Outlet Menu and a named Event Menu before browsing; choice is persisted in sessionStorage
  • useEventGatekeeper hook — subscribes to app_config/event_settings in Firestore; shows the selection modal only when Event Mode is active and the session has no stored choice
  • Per-item menuContext — each menu item carries menuContext: 'outlet' | 'event'; the customer page filters items to the active context
  • Event escape hatch banner — sticky banner rendered between the header and category pills when viewing the Event Menu; one-tap switch back to Outlet Menu (with cart-clear confirmation)
  • ExclusivityToast — bottom-anchored toast that slides up to confirm the active menu context

UX / Polish

  • Framer Motion animations: page fade-in, stagger on menu cards, hamburger sidebar slide, banner dismiss collapse, save button reveal
  • GlobalLoader — full-screen slot-machine word animation on route loading
  • ButtonSpinner — inline loading indicator on async action buttons
  • Cart/Checkout totals — digital receipt–style breakdown with line-item subtotals, savings callout, and grand total row

Tech Stack

Layer Technology
Framework Next.js 14 (App Router)
Language TypeScript
Styling Tailwind CSS
Animations Framer Motion
Backend / DB Firebase Firestore
Auth Firebase Authentication
File Storage Firebase Storage
Hosting Firebase Hosting + Cloud Functions Gen 2 (Blaze)
Printing QZ Tray 2.2.4 + ESC/POS
Payments ToyyibPay (FPX)
Admin SDK firebase-admin (modular, Cloud Run Node.js runtime)
UI Utilities Lucide React, React Hot Toast, SweetAlert2
Excel Import SheetJS (xlsx)
Image Cropping react-image-crop

Project Structure

app/
  page.tsx                  # Customer menu (main page) — dual-menu gatekeeper, event escape hatch
  layout.tsx                # Root layout + CartContext provider
  loading.tsx               # GlobalLoader shown on route transitions
  not-found.tsx             # Branded 404 page
  about/                    # About page
  profile/                  # Customer profile management
  order/[id]/               # Real-time order tracking
  admin/
    layout.tsx              # Admin shell layout + sidebar navigation
    page.tsx                # Admin dashboard home
    login/                  # Admin login page (Firebase Auth)
    orders/                 # Orders management + receipt printing
    menu/                   # Menu item CRUD (with menuContext filter pills)
    availability/           # Store open/closed toggle
    event-settings/         # Event Mode toggle, event name, active date/time window
    settings/               # Branch settings
    users/                  # Customer accounts list
    reviews/                # Customer reviews
    vouchers/               # Discount voucher management
    import/                 # Bulk Excel menu import
  api/
    qz-sign/route.ts        # Server-side QZ Tray request signing
    upload/route.ts         # Image upload handler
    checkout/route.ts       # Creates ToyyibPay bill + order in Firestore
    payment/callback/       # ToyyibPay background webhook (multipart/form-data parser)
  payment/
    success/                # Post-payment landing page

components/
  CustomerHeader.tsx        # Unified sticky header (logo + nav) — h-20, shrink-safe
  StoreStatusBanner.tsx     # Sticky store open/closed banner (dismissible)
  FloatingOrderWidget.tsx   # Persistent cart summary
  MenuSelectionModal.tsx    # Dual-menu selection overlay (Outlet vs Event); persists choice to sessionStorage
  ExclusivityToast.tsx      # Bottom-anchored context confirmation toast
  admin/
    MenuItemModal.tsx       # Add/edit menu item modal (includes menuContext segmented control)
    OrderListener.tsx       # Firestore real-time order listener
  ui/
    Loader.tsx              # GlobalLoader + ButtonSpinner
    PageWrapper.tsx         # Framer Motion page fade-in wrapper

context/
  CartContext.tsx            # Global cart state

hooks/
  useOrderNotifier.ts       # Listens for confirmed + unprinted orders → ding + print + auto-advance
  useEventGatekeeper.ts     # Subscribes to app_config/event_settings; controls MenuSelectionModal visibility
  usePromoTime.ts           # Promo/time-based display helpers

lib/
  firebase.ts               # Firebase client SDK initialisation
  firebase-admin.ts         # Firebase Admin SDK singleton (try/catch init pattern)
  qzService.ts              # QZ Tray connection, signing, and print helpers
  qz-tray.ts                # QZ Tray JS library loader

types/
  index.ts                  # Shared TypeScript types (MenuItem with menuContext, EventConfig, etc.)

public/
  certs/digital-certificate.txt  # QZ Tray signing certificate (self-signed)
  wingmascot.png            # Brand mascot
  404mascot.png             # 404 page mascot
  mascotComingSoon.png      # Fallback image for menu items without photos
  WingzoneLogo.png
  ding.mp3                  # Alert tone played when a confirmed order arrives

private/                    # .gitignored — RSA private key for QZ signing
  private-key.pem

Environment Variables

Copy .env.local.example to .env.local and fill in values. Never commit .env.local.

# Firebase client config
NEXT_PUBLIC_FIREBASE_API_KEY=
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
NEXT_PUBLIC_FIREBASE_PROJECT_ID=
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
NEXT_PUBLIC_FIREBASE_APP_ID=

# QZ Tray signing — RSA private key (single-line, \n-escaped)
QZ_PRIVATE_KEY=

# ToyyibPay
TOYYIBPAY_API_KEY=
TOYYIBPAY_CATEGORY_CODE=
NEXT_PUBLIC_TOYYIBPAY_URL=https://toyyibpay.com

For production (Firebase), set QZ_PRIVATE_KEY as a Firebase secret:

firebase functions:secrets:set QZ_PRIVATE_KEY

Getting Started

# Install dependencies
npm install

# Start development server
npm run dev

Open http://localhost:3000.

Admin panel is at http://localhost:3000/admin — requires a Firebase account with admin custom claims set in Firestore.


Deployment

# Build and deploy to Firebase Hosting + Cloud Functions
firebase deploy --only hosting

Requires Firebase CLI logged in (firebase login) and project linked (.firebaserc).


Event Menu System

The Event Menu System lets the store activate a separate curated menu for special events (e.g. pop-ups, catering, limited-time drops) without displacing the regular Outlet Menu.

How it works

  1. An admin navigates to Event Settings and toggles Event Mode on, sets an event name, and optionally configures a start/end date-time window.
  2. The settings are written to app_config/event_settings in Firestore.
  3. On the customer page, useEventGatekeeper listens to that document in real time. When Event Mode is active and no session choice is stored, MenuSelectionModal is shown.
  4. The customer picks Outlet Menu or the named Event Menu — the choice is saved to sessionStorage and persists for the tab's lifetime.
  5. The category pill bar, cart, and checkout all operate on the filtered item set for the chosen context.
  6. A sticky event escape hatch banner appears below the header when viewing the Event Menu, allowing one-tap return to the Outlet Menu (cart is cleared after confirmation).

Admin controls

  • Master toggle — enables/disables Event Mode globally
  • Event name — displayed in the selection modal and the escape hatch banner
  • Date/time window — optional; event auto-activates and auto-deactivates based on the window
  • Per-item context — each menu item has a menuContext field set via the Menu Context segmented control in the item editor (outlet | event)
  • Context filter pills — the admin Menu page has All / Outlet / Event filter pills to quickly view items by context

QZ Tray Thermal Printing

How it works

  1. The cashier PC runs QZ Tray 2.x as a local background service.
  2. When an admin clicks Print Receipt, the web app:
    • Fetches the signing cert from /certs/digital-certificate.txt
    • Calls POST /api/qz-sign with the request string — the server signs it with the RSA private key
    • Sends signed ESC/POS commands to QZ Tray via WebSocket
  3. QZ Tray forwards the commands to the connected USB/Serial thermal printer.

First-time setup on a new cashier PC

  1. Install QZ Tray 2.x
  2. Open the web app and trigger any print
  3. When the trust dialog appears: check "Remember this decision" → click Allow
  4. Done — no manual cert file installation needed (the cert is served from the live URL)

Certificate

The signing certificate at public/certs/digital-certificate.txt is a self-signed certificate issued for this deployment. QZ Tray allows permanent trust for user-generated certificates (unlike the QZ demo cert which intentionally blocks persistent trust).


Current Progress

Completed ✅

  • Full customer menu with live Firestore data, category navigation, and customisation modal
  • Cart context, floating order widget, and order submission
  • Real-time order tracking page
  • User profile management with unsaved-changes detection
  • All admin pages: orders, menu CRUD, availability, settings, users, reviews, daily report, import
  • ESC/POS receipt printing via QZ Tray with server-side RSA signing
  • Animated UI: page transitions, menu card stagger, sidebar, store banner, save button
  • GlobalLoader (slot-machine animation) and ButtonSpinner
  • Branded 404 page and fallback menu item image
  • Firebase production deployment (Hosting + Cloud Functions Gen 2)
  • QZ Tray trust resolved — permanent "Remember this decision" works with self-signed cert
  • GitHub repository set up with secrets excluded from version control
  • ToyyibPay FPX payment integration — checkout creates bill, webhook confirms payment
  • Background webhook correctly parses ToyyibPay multipart/form-data payload via regex
  • Firebase Admin SDK on webhook route — serverComponentsExternalPackages + try/catch singleton
  • useOrderNotifier — ding + auto-print + 30 s auto-advance to Preparing on confirmed orders
  • Unified CustomerHeader — sticky header with correct height (h-20) and shrink-safe logo
  • Image loading overlay — held until all menu images resolve
  • Cart/Checkout totals redesign — digital receipt–style breakdown with savings callout
  • Daily Sales Report — Gateway Fee (RM) and Net Banked (RM) columns added to table and Excel export
  • Event Menu System — dual-menu gatekeeper (useEventGatekeeper), MenuSelectionModal, per-item menuContext, event escape hatch banner, ExclusivityToast
  • Admin: Event Settings page — master toggle, event name, start/end datetime window, live status banner
  • Admin: Vouchers page — discount voucher management
  • Admin: Login page — dedicated /admin/login route
  • Category pill bleed fixshrink-0 on all pills + trailing spacer prevents last pill clipping on mobile
  • Admin Menu context filter — All / Outlet / Event pills in the menu admin page

Pending / Future Improvements

  • Push notifications for order status updates (Firebase Cloud Messaging)
  • Customer-facing order history page
  • Voucher redemption on the customer checkout flow
  • Multiple branch support
  • PWA manifest + offline support
  • Automated ESLint/type-check CI on GitHub Actions
  • ToyyibPay payment status polling fallback (for webhook delivery failures)
  • Event Menu date/time window auto-activation without page reload (scheduled Firestore rule or Cloud Function)

Known Limitations

  • QZ Tray must be running on the cashier PC for printing to work — it is a local desktop service, not cloud-based.
  • Admin auth currently relies on a Firestore admins collection check — no Firebase custom claims. Ensure the admins document is correctly seeded.
  • .env.local must be manually configured on each development machine — it is intentionally excluded from version control.
  • ToyyibPay webhook is multipart/form-data (not x-www-form-urlencoded) — the webhook uses a regex-based field extractor; if ToyyibPay changes their payload format this will need updating.
  • QZ Tray must be running on the cashier PC for auto-print on confirmed orders to work.

About

A full-stack POS and customer food-ordering web application. Features real-time kitchen tracking, FPX payments (ToyyibPay), dual-menu event system, and automated ESC/POS thermal receipt printing via QZ Tray.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages