Skip to content

Architecture

How MealMind is put together: the app layers, the three entry points, and how data moves through a customer's visit.

High-level view

flowchart TB
    subgraph App["Flutter app (single codebase)"]
        direction TB
        Hub["Entry points<br/>main_user · main_chef · main_admin"]
        subgraph Customer["Customer side · lib/user"]
            Auth["Login / Signup<br/>lib/auth"]
            QR["QR table check-in"]
            Shell["UserMainWrapper<br/>bottom nav"]
            Menu["Menu · Item detail · Liked"]
            Cart["Cart · CartProvider"]
            Status["Order status · Bill"]
            Pay["Razorpay payment"]
        end
        subgraph Admin["Admin side · lib/admin"]
            ALogin["Admin login"]
            Queue["Order queue"]
            MenuAdm["Menu / Categories"]
            Users["Users & roles"]
            Stats["Analytics"]
        end
        Guard["AuthGuard<br/>lib/core/auth_guard.dart"]
    end

    subgraph Firebase
        FA[(Firebase Auth)]
        FS[(Cloud Firestore)]
        ST[(Storage)]
    end
    RZ[[Razorpay]]

    Hub --> Customer & Admin
    Guard -.protects.-> Shell & Admin
    Auth --> FA
    Customer --> FS
    Admin --> FS
    Pay --> RZ
    Menu --> ST

Layers

Layer Where Responsibility
Entry lib/main_user.dart, main_chef.dart, main_admin.dart One per app; each calls bootstrap() in lib/core/app_bootstrap.dart (Firebase init + CartProvider). lib/main.dart is the developer hub (DemoScreen in lib/dev/).
Core lib/core Shared startup, the AuthGuard route guard, the current-table global and snackbars.
Screens lib/user/screens, lib/admin/screens, lib/auth, lib/staff UI. Most screens subscribe to Firestore streams directly with StreamBuilder.
Services lib/user/services, lib/admin/services Multi-step operations: placing orders, menu CRUD, menu helpers.
State lib/user/state/cart_provider.dart, lib/core/table_state.dart Cart is mirrored from Firestore into a ChangeNotifier; the current table is a simple global.

Firestore is the single source of truth. Almost every screen listens to snapshots, so changes made by the kitchen show up on the customer's phone in real time without any custom sync code.

Three apps

App Entry Flavor / app ID Home
Customer main_user.dart user / com.example.demo UserHome: login → QRTableScanScreen
Chef main_chef.dart chef / com.example.demo.chef RoleGate(['chef','admin']) → AdminOrderQueueScreen
Admin main_admin.dart admin / com.example.demo.admin RoleGate(['admin']) → AdminShell

RoleGate (lib/staff/role_gate.dart) listens to authStateChanges(). Signed out, it shows StaffLoginScreen. Signed in, it reads users/{uid}.role and shows the app or an Access denied screen with a sign-out button.

google-services.json contains client entries for all three package names. The chef and admin entries currently reuse the customer app's Firebase app ID; register them as their own Firebase Android apps for proper analytics and Google Sign-In.

Authentication & roles

  1. Customers sign up with email/password (Signupscreen, lib/auth/signup_screen.dart) or sign in with Google (Loginscreen, lib/auth/login_screen.dart). A users/{uid} document stores profile data.
  2. Admins are ordinary Firebase users whose users/{uid}.role is "admin".
  3. AuthGuard(child: …, requiredRole: 'admin') wraps protected screens. It redirects to login when signed out and shows "Access denied: admins only." when the role doesn't match.

Enforce roles on the server too

Role checks in the app are a UX convenience. Enforce them in Firestore rules too (see Security rules).

Customer flow in detail

1. Table check-in, qr_table_scan_screen.dart

  • Requests camera permission and scans with mobile_scanner.
  • The QR payload can be plain text (5) or JSON ({"table":"5"}).
  • Looks up tableDetail where tableNumber == scanned. If it's free (or already held by this user) it sets isOccupied: true and currentCustomerId, then opens UserMainWrapper.
  • Customers can also pick from a list of free tables.

2. Browsing and cart

  • MenuScreen streams menu, filtered by category and sorted by price, likes or itemName.
  • Hearts write to users/{uid}/favorites.
  • CartProvider.updateCartItem adds, updates or deletes a cartItems doc; the provider's snapshot listener keeps the in-memory itemId → qty map in sync.

3. Placing an order, OrderService.placeOrderAndGoToStatus

  1. Reads all of the user's cartItems.
  2. Computes totalAmount = Σ price × quantity.
  3. Creates orders/order_<millis> with status: 'placed'.
  4. In one batch, copies each cart line into orders/{id}/items and deletes it from the cart.
  5. Navigates to OrderStatusScreen.

Customers can place several orders during one visit.

4. Kitchen, admin_order_screen.dart

  • Streams all orders newest-first into a grid with a stats bar.
  • The detail sheet advances placed → preparing → ready → completed (older received orders are treated like placed) and toggles isPaid.

5. Billing, order_status_screen.dart

When every active order is ready, the Generate bill button runs createBillAndCleanupAtomic: 1. Merges items from all ready orders (same itemId → quantities summed). 2. tax = 5% of subtotal, total = subtotal + tax − discount. 3. Gets the next bill number from counters/bill_counter in a transaction → bill_0001, … 4. Writes bill/{billId}, then deletes the merged orders and their items. 5. Opens BillScreen, which can render a PDF (pdf_generator.dart), share a screenshot, or start payment.

6. Payment, razorpay_payment_screen.dart

  • Opens Razorpay Checkout with the amount in paise.
  • On success: marks the order paid, sets bill.paidStatus = 'paid', and writes a payment record.
  • Errors and external wallets are reported in a bottom sheet.

See also: CI/CD pipeline · Testing · Project structure