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¶
- Customers sign up with email/password (
Signupscreen,lib/auth/signup_screen.dart) or sign in with Google (Loginscreen,lib/auth/login_screen.dart). Ausers/{uid}document stores profile data. - Admins are ordinary Firebase users whose
users/{uid}.roleis"admin". 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
tableDetailwheretableNumber == scanned. If it's free (or already held by this user) it setsisOccupied: trueandcurrentCustomerId, then opensUserMainWrapper. - Customers can also pick from a list of free tables.
2. Browsing and cart¶
MenuScreenstreamsmenu, filtered bycategoryand sorted byprice,likesoritemName.- Hearts write to
users/{uid}/favorites. CartProvider.updateCartItemadds, updates or deletes acartItemsdoc; the provider's snapshot listener keeps the in-memoryitemId → qtymap in sync.
3. Placing an order, OrderService.placeOrderAndGoToStatus¶
- Reads all of the user's
cartItems. - Computes
totalAmount = Σ price × quantity. - Creates
orders/order_<millis>withstatus: 'placed'. - In one batch, copies each cart line into
orders/{id}/itemsand deletes it from the cart. - Navigates to
OrderStatusScreen.
Customers can place several orders during one visit.
4. Kitchen, admin_order_screen.dart¶
- Streams all
ordersnewest-first into a grid with a stats bar. - The detail sheet advances
placed → preparing → ready → completed(olderreceivedorders are treated likeplaced) and togglesisPaid.
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 apaymentrecord. - Errors and external wallets are reported in a bottom sheet.
See also: CI/CD pipeline · Testing · Project structure