From f505eeb7227e36b8977b1e77e224561b09d854ea Mon Sep 17 00:00:00 2001 From: John Mizerek Date: Mon, 29 Dec 2025 11:14:19 -0800 Subject: [PATCH] Implement complete cart management system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add full cart functionality with API integration: - Created Cart and OrderLineItem models with robust JSON parsing - Implemented cart API methods (getOrCreateCart, setLineItem, getCart, submitOrder) - Added cart state management to AppState with item count tracking - Built cart view screen with item display, quantity editing, and removal - Added cart badge to menu screen showing item count - Implemented real add-to-cart logic with recursive modifier handling - Added category name display in menu browsing - Fixed API response case sensitivity (ORDER/ORDERLINEITEMS) - Enhanced MenuItem model with categoryName field 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- lib/app/app_router.dart | 3 + lib/app/app_state.dart | 11 +- lib/models/cart.dart | 202 ++++++++++++ lib/models/menu_item.dart | 3 + lib/screens/cart_view_screen.dart | 462 ++++++++++++++++++++++++++++ lib/screens/menu_browse_screen.dart | 127 +++++++- lib/services/api.dart | 85 ++++- 7 files changed, 870 insertions(+), 23 deletions(-) create mode 100644 lib/models/cart.dart create mode 100644 lib/screens/cart_view_screen.dart diff --git a/lib/app/app_router.dart b/lib/app/app_router.dart index a6fad0d..c66b5ff 100644 --- a/lib/app/app_router.dart +++ b/lib/app/app_router.dart @@ -1,5 +1,6 @@ import "package:flutter/material.dart"; +import "../screens/cart_view_screen.dart"; import "../screens/login_screen.dart"; import "../screens/menu_browse_screen.dart"; import "../screens/restaurant_select_screen.dart"; @@ -12,6 +13,7 @@ class AppRoutes { static const String restaurantSelect = "/restaurants"; static const String servicePointSelect = "/service-points"; static const String menuBrowse = "/menu"; + static const String cartView = "/cart"; static Map get routes => { splash: (_) => const SplashScreen(), @@ -19,5 +21,6 @@ class AppRoutes { restaurantSelect: (_) => const RestaurantSelectScreen(), servicePointSelect: (_) => const ServicePointSelectScreen(), menuBrowse: (_) => const MenuBrowseScreen(), + cartView: (_) => const CartViewScreen(), }; } diff --git a/lib/app/app_state.dart b/lib/app/app_state.dart index 6e22e4e..f483f93 100644 --- a/lib/app/app_state.dart +++ b/lib/app/app_state.dart @@ -8,6 +8,7 @@ class AppState extends ChangeNotifier { int? _cartOrderId; String? _cartOrderUuid; + int _cartItemCount = 0; int? get selectedBusinessId => _selectedBusinessId; int? get selectedServicePointId => _selectedServicePointId; @@ -17,6 +18,7 @@ class AppState extends ChangeNotifier { int? get cartOrderId => _cartOrderId; String? get cartOrderUuid => _cartOrderUuid; + int get cartItemCount => _cartItemCount; bool get hasLocationSelection => _selectedBusinessId != null && _selectedServicePointId != null; @@ -50,15 +52,22 @@ class AppState extends ChangeNotifier { notifyListeners(); } - void setCartOrder({required int orderId, required String orderUuid}) { + void setCartOrder({required int orderId, required String orderUuid, int itemCount = 0}) { _cartOrderId = orderId; _cartOrderUuid = orderUuid; + _cartItemCount = itemCount; + notifyListeners(); + } + + void updateCartItemCount(int count) { + _cartItemCount = count; notifyListeners(); } void clearCart() { _cartOrderId = null; _cartOrderUuid = null; + _cartItemCount = 0; notifyListeners(); } diff --git a/lib/models/cart.dart b/lib/models/cart.dart new file mode 100644 index 0000000..cf86612 --- /dev/null +++ b/lib/models/cart.dart @@ -0,0 +1,202 @@ +class Cart { + final int orderId; + final String orderUuid; + final int userId; + final int businessId; + final double businessDeliveryMultiplier; + final int orderTypeId; + final double deliveryFee; + final int statusId; + final int? addressId; + final int? paymentId; + final String? remarks; + final DateTime addedOn; + final DateTime lastEditedOn; + final DateTime? submittedOn; + final int servicePointId; + final List lineItems; + + const Cart({ + required this.orderId, + required this.orderUuid, + required this.userId, + required this.businessId, + required this.businessDeliveryMultiplier, + required this.orderTypeId, + required this.deliveryFee, + required this.statusId, + this.addressId, + this.paymentId, + this.remarks, + required this.addedOn, + required this.lastEditedOn, + this.submittedOn, + required this.servicePointId, + required this.lineItems, + }); + + factory Cart.fromJson(Map json) { + final order = (json["ORDER"] ?? json["Order"]) as Map? ?? {}; + final lineItemsJson = (json["ORDERLINEITEMS"] ?? json["OrderLineItems"]) as List? ?? []; + + return Cart( + orderId: _parseInt(order["OrderID"]) ?? 0, + orderUuid: (order["OrderUUID"] as String?) ?? "", + userId: _parseInt(order["OrderUserID"]) ?? 0, + businessId: _parseInt(order["OrderBusinessID"]) ?? 0, + businessDeliveryMultiplier: _parseDouble(order["OrderBusinessDeliveryMultiplier"]) ?? 0.0, + orderTypeId: _parseInt(order["OrderTypeID"]) ?? 0, + deliveryFee: _parseDouble(order["OrderDeliveryFee"]) ?? 0.0, + statusId: _parseInt(order["OrderStatusID"]) ?? 0, + addressId: _parseInt(order["OrderAddressID"]), + paymentId: _parseInt(order["OrderPaymentID"]), + remarks: order["OrderRemarks"] as String?, + addedOn: _parseDateTime(order["OrderAddedOn"]), + lastEditedOn: _parseDateTime(order["OrderLastEditedOn"]), + submittedOn: _parseDateTime(order["OrderSubmittedOn"]), + servicePointId: _parseInt(order["OrderServicePointID"]) ?? 0, + lineItems: lineItemsJson + .map((item) => OrderLineItem.fromJson(item as Map)) + .toList(), + ); + } + + static int? _parseInt(dynamic value) { + if (value == null) return null; + if (value is int) return value; + if (value is num) return value.toInt(); + if (value is String) { + if (value.isEmpty) return null; + return int.tryParse(value); + } + return null; + } + + static double? _parseDouble(dynamic value) { + if (value == null) return null; + if (value is double) return value; + if (value is num) return value.toDouble(); + if (value is String) { + if (value.isEmpty) return null; + return double.tryParse(value); + } + return null; + } + + static DateTime _parseDateTime(dynamic value) { + if (value == null) return DateTime.now(); + if (value is DateTime) return value; + if (value is String) { + try { + return DateTime.parse(value); + } catch (e) { + return DateTime.now(); + } + } + return DateTime.now(); + } + + double get subtotal { + return lineItems + .where((item) => !item.isDeleted && item.parentOrderLineItemId == 0) + .fold(0.0, (sum, item) => sum + (item.price * item.quantity)); + } + + double get total => subtotal + deliveryFee; + + int get itemCount { + return lineItems + .where((item) => !item.isDeleted && item.parentOrderLineItemId == 0) + .fold(0, (sum, item) => sum + item.quantity); + } +} + +class OrderLineItem { + final int orderLineItemId; + final int parentOrderLineItemId; + final int orderId; + final int itemId; + final int statusId; + final double price; + final int quantity; + final String? remark; + final bool isDeleted; + final DateTime addedOn; + + const OrderLineItem({ + required this.orderLineItemId, + required this.parentOrderLineItemId, + required this.orderId, + required this.itemId, + required this.statusId, + required this.price, + required this.quantity, + this.remark, + required this.isDeleted, + required this.addedOn, + }); + + factory OrderLineItem.fromJson(Map json) { + return OrderLineItem( + orderLineItemId: _parseInt(json["OrderLineItemID"]) ?? 0, + parentOrderLineItemId: _parseInt(json["OrderLineItemParentOrderLineItemID"]) ?? 0, + orderId: _parseInt(json["OrderLineItemOrderID"]) ?? 0, + itemId: _parseInt(json["OrderLineItemItemID"]) ?? 0, + statusId: _parseInt(json["OrderLineItemStatusID"]) ?? 0, + price: _parseDouble(json["OrderLineItemPrice"]) ?? 0.0, + quantity: _parseInt(json["OrderLineItemQuantity"]) ?? 0, + remark: json["OrderLineItemRemark"] as String?, + isDeleted: _parseBool(json["OrderLineItemIsDeleted"]), + addedOn: _parseDateTime(json["OrderLineItemAddedOn"]), + ); + } + + static int? _parseInt(dynamic value) { + if (value == null) return null; + if (value is int) return value; + if (value is num) return value.toInt(); + if (value is String) { + if (value.isEmpty) return null; + return int.tryParse(value); + } + return null; + } + + static double? _parseDouble(dynamic value) { + if (value == null) return null; + if (value is double) return value; + if (value is num) return value.toDouble(); + if (value is String) { + if (value.isEmpty) return null; + return double.tryParse(value); + } + return null; + } + + static bool _parseBool(dynamic value) { + if (value == null) return false; + if (value is bool) return value; + if (value is num) return value != 0; + if (value is String) { + final lower = value.toLowerCase(); + return lower == "true" || lower == "1"; + } + return false; + } + + static DateTime _parseDateTime(dynamic value) { + if (value == null) return DateTime.now(); + if (value is DateTime) return value; + if (value is String) { + try { + return DateTime.parse(value); + } catch (e) { + return DateTime.now(); + } + } + return DateTime.now(); + } + + bool get isRootItem => parentOrderLineItemId == 0; + bool get isModifier => parentOrderLineItemId != 0; +} diff --git a/lib/models/menu_item.dart b/lib/models/menu_item.dart index 74a928e..e277f94 100644 --- a/lib/models/menu_item.dart +++ b/lib/models/menu_item.dart @@ -1,6 +1,7 @@ class MenuItem { final int itemId; final int categoryId; + final String categoryName; final String name; final String description; final int parentItemId; @@ -15,6 +16,7 @@ class MenuItem { const MenuItem({ required this.itemId, required this.categoryId, + required this.categoryName, required this.name, required this.description, required this.parentItemId, @@ -31,6 +33,7 @@ class MenuItem { return MenuItem( itemId: (json["ItemID"] as num).toInt(), categoryId: (json["ItemCategoryID"] as num).toInt(), + categoryName: (json["ItemCategoryName"] as String?) ?? "", name: (json["ItemName"] as String?) ?? "", description: (json["ItemDescription"] as String?) ?? "", parentItemId: (json["ItemParentItemID"] as num?)?.toInt() ?? 0, diff --git a/lib/screens/cart_view_screen.dart b/lib/screens/cart_view_screen.dart new file mode 100644 index 0000000..bfc8e05 --- /dev/null +++ b/lib/screens/cart_view_screen.dart @@ -0,0 +1,462 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../app/app_state.dart'; +import '../models/cart.dart'; +import '../models/menu_item.dart'; +import '../services/api.dart'; + +class CartViewScreen extends StatefulWidget { + const CartViewScreen({super.key}); + + @override + State createState() => _CartViewScreenState(); +} + +class _CartViewScreenState extends State { + Cart? _cart; + bool _isLoading = true; + String? _error; + Map _menuItemsById = {}; + + @override + void initState() { + super.initState(); + _loadCart(); + } + + Future _loadCart() async { + setState(() { + _isLoading = true; + _error = null; + }); + + try { + final appState = context.read(); + final cartOrderId = appState.cartOrderId; + + if (cartOrderId == null) { + setState(() { + _isLoading = false; + _cart = null; + }); + return; + } + + // Load cart + final cart = await Api.getCart(orderId: cartOrderId); + + // Load menu items to get names and prices + final businessId = appState.selectedBusinessId; + if (businessId != null) { + final menuItems = await Api.listMenuItems(businessId: businessId); + _menuItemsById = {for (var item in menuItems) item.itemId: item}; + } + + setState(() { + _cart = cart; + _isLoading = false; + }); + + // Update item count in app state + appState.updateCartItemCount(cart.itemCount); + } catch (e) { + setState(() { + _error = e.toString(); + _isLoading = false; + }); + } + } + + Future _removeLineItem(OrderLineItem lineItem) async { + try { + final appState = context.read(); + final cartOrderId = appState.cartOrderId; + if (cartOrderId == null) return; + + setState(() => _isLoading = true); + + // Set IsSelected=false to remove the item + await Api.setLineItem( + orderId: cartOrderId, + parentOrderLineItemId: lineItem.parentOrderLineItemId, + itemId: lineItem.itemId, + isSelected: false, + ); + + // Reload cart + await _loadCart(); + } catch (e) { + setState(() { + _error = e.toString(); + _isLoading = false; + }); + } + } + + Future _updateQuantity(OrderLineItem lineItem, int newQuantity) async { + if (newQuantity < 1) return; + + try { + final appState = context.read(); + final cartOrderId = appState.cartOrderId; + if (cartOrderId == null) return; + + setState(() => _isLoading = true); + + await Api.setLineItem( + orderId: cartOrderId, + parentOrderLineItemId: lineItem.parentOrderLineItemId, + itemId: lineItem.itemId, + isSelected: true, + quantity: newQuantity, + remark: lineItem.remark, + ); + + // Reload cart + await _loadCart(); + } catch (e) { + setState(() { + _error = e.toString(); + _isLoading = false; + }); + } + } + + Future _submitOrder() async { + try { + final appState = context.read(); + final cartOrderId = appState.cartOrderId; + if (cartOrderId == null) return; + + setState(() => _isLoading = true); + + await Api.submitOrder(orderId: cartOrderId); + + // Clear cart state + appState.clearCart(); + + if (!mounted) return; + + // Show success message + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Order submitted successfully!"), + backgroundColor: Colors.green, + ), + ); + + // Navigate back + Navigator.of(context).pop(); + } catch (e) { + setState(() { + _error = e.toString(); + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Cart"), + backgroundColor: Colors.black, + foregroundColor: Colors.white, + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _error != null + ? Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error, color: Colors.red, size: 48), + const SizedBox(height: 16), + Text( + "Error loading cart", + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 8), + Text(_error!, textAlign: TextAlign.center), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _loadCart, + child: const Text("Retry"), + ), + ], + ), + ), + ) + : _cart == null || _cart!.itemCount == 0 + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.shopping_cart_outlined, + size: 80, + color: Colors.grey, + ), + const SizedBox(height: 16), + Text( + "Your cart is empty", + style: Theme.of(context).textTheme.titleLarge, + ), + ], + ), + ) + : Column( + children: [ + Expanded( + child: ListView( + padding: const EdgeInsets.all(16), + children: _buildCartItems(), + ), + ), + _buildCartSummary(), + ], + ), + ); + } + + List _buildCartItems() { + if (_cart == null) return []; + + // Group line items by root items + final rootItems = _cart!.lineItems + .where((item) => item.parentOrderLineItemId == 0 && !item.isDeleted) + .toList(); + + final widgets = []; + + for (final rootItem in rootItems) { + widgets.add(_buildRootItemCard(rootItem)); + widgets.add(const SizedBox(height: 12)); + } + + return widgets; + } + + Widget _buildRootItemCard(OrderLineItem rootItem) { + final menuItem = _menuItemsById[rootItem.itemId]; + final itemName = menuItem?.name ?? "Item #${rootItem.itemId}"; + + // Find all modifiers for this root item + final modifiers = _cart!.lineItems + .where((item) => + item.parentOrderLineItemId == rootItem.orderLineItemId && + !item.isDeleted) + .toList(); + + return Card( + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + itemName, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + IconButton( + icon: const Icon(Icons.delete, color: Colors.red), + onPressed: () => _confirmRemoveItem(rootItem, itemName), + ), + ], + ), + if (modifiers.isNotEmpty) ...[ + const SizedBox(height: 8), + ...modifiers.map((mod) => _buildModifierRow(mod)), + ], + const SizedBox(height: 8), + Row( + children: [ + IconButton( + icon: const Icon(Icons.remove_circle_outline), + onPressed: rootItem.quantity > 1 + ? () => _updateQuantity(rootItem, rootItem.quantity - 1) + : null, + ), + Text( + "${rootItem.quantity}", + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + IconButton( + icon: const Icon(Icons.add_circle_outline), + onPressed: () => + _updateQuantity(rootItem, rootItem.quantity + 1), + ), + const Spacer(), + Text( + "\$${rootItem.price.toStringAsFixed(2)}", + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildModifierRow(OrderLineItem modifier) { + final menuItem = _menuItemsById[modifier.itemId]; + final modName = menuItem?.name ?? "Modifier #${modifier.itemId}"; + + return Padding( + padding: const EdgeInsets.only(left: 16, top: 4), + child: Row( + children: [ + const Icon(Icons.add, size: 12, color: Colors.grey), + const SizedBox(width: 4), + Expanded( + child: Text( + modName, + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + ), + if (modifier.price > 0) + Text( + "+\$${modifier.price.toStringAsFixed(2)}", + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + ], + ), + ); + } + + Widget _buildCartSummary() { + if (_cart == null) return const SizedBox.shrink(); + + return Container( + decoration: BoxDecoration( + color: Colors.grey[100], + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, -2), + ), + ], + ), + padding: const EdgeInsets.all(16), + child: SafeArea( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Subtotal", + style: TextStyle(fontSize: 16), + ), + Text( + "\$${_cart!.subtotal.toStringAsFixed(2)}", + style: const TextStyle(fontSize: 16), + ), + ], + ), + if (_cart!.deliveryFee > 0) ...[ + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Delivery Fee", + style: TextStyle(fontSize: 16), + ), + Text( + "\$${_cart!.deliveryFee.toStringAsFixed(2)}", + style: const TextStyle(fontSize: 16), + ), + ], + ), + ], + const Divider(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Total", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + "\$${_cart!.total.toStringAsFixed(2)}", + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _cart!.itemCount > 0 ? _submitOrder : null, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + ), + child: const Text( + "Submit Order", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + ), + ], + ), + ), + ); + } + + void _confirmRemoveItem(OrderLineItem item, String itemName) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Remove Item"), + content: Text("Remove $itemName from cart?"), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text("Cancel"), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + _removeLineItem(item); + }, + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: const Text("Remove"), + ), + ], + ), + ); + } +} diff --git a/lib/screens/menu_browse_screen.dart b/lib/screens/menu_browse_screen.dart index 386aa37..372c9f2 100644 --- a/lib/screens/menu_browse_screen.dart +++ b/lib/screens/menu_browse_screen.dart @@ -1,5 +1,9 @@ import "package:flutter/material.dart"; +import "package:provider/provider.dart"; +import "../app/app_router.dart"; +import "../app/app_state.dart"; +import "../models/cart.dart"; import "../models/menu_item.dart"; import "../services/api.dart"; @@ -98,16 +102,20 @@ class _MenuBrowseScreenState extends State { @override Widget build(BuildContext context) { + final appState = context.watch(); + return Scaffold( appBar: AppBar( title: const Text("Menu"), actions: [ IconButton( - icon: const Icon(Icons.shopping_cart), + icon: Badge( + label: Text("${appState.cartItemCount}"), + isLabelVisible: appState.cartItemCount > 0, + child: const Icon(Icons.shopping_cart), + ), onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Cart view coming soon")), - ); + Navigator.of(context).pushNamed(AppRoutes.cartView); }, ), ], @@ -154,6 +162,9 @@ class _MenuBrowseScreenState extends State { itemBuilder: (context, index) { final categoryId = categoryIds[index]; final items = _itemsByCategory[categoryId] ?? []; + final categoryName = items.isNotEmpty + ? items.first.categoryName + : "Category $categoryId"; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -161,7 +172,7 @@ class _MenuBrowseScreenState extends State { Padding( padding: const EdgeInsets.all(16), child: Text( - "Category $categoryId", + categoryName, style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), @@ -222,14 +233,106 @@ class _MenuBrowseScreenState extends State { ); } - void _addToCart(MenuItem item, Set selectedModifierIds) { - final message = selectedModifierIds.isEmpty - ? "Added ${item.name} to cart" - : "Added ${item.name} with ${selectedModifierIds.length} customizations"; + Future _addToCart(MenuItem item, Set selectedModifierIds) async { + if (_userId == null || _businessId == null || _servicePointId == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Missing required information")), + ); + return; + } - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message)), - ); + try { + final appState = context.read(); + + // Get or create cart + Cart cart; + if (appState.cartOrderId == null) { + cart = await Api.getOrCreateCart( + userId: _userId!, + businessId: _businessId!, + servicePointId: _servicePointId!, + orderTypeId: 1, // Dine-in + ); + // ignore: avoid_print + print("DEBUG: Created cart with orderId=${cart.orderId}"); + appState.setCartOrder( + orderId: cart.orderId, + orderUuid: cart.orderUuid, + itemCount: cart.itemCount, + ); + } else { + // We have an existing cart ID + cart = await Api.getCart(orderId: appState.cartOrderId!); + // ignore: avoid_print + print("DEBUG: Loaded existing cart with orderId=${cart.orderId}"); + } + + // ignore: avoid_print + print("DEBUG: About to add root item ${item.itemId} to cart ${cart.orderId}"); + + // Add root item + cart = await Api.setLineItem( + orderId: cart.orderId, + parentOrderLineItemId: 0, + itemId: item.itemId, + isSelected: true, + quantity: 1, + ); + + // ignore: avoid_print + print("DEBUG: Added root item, cart now has ${cart.lineItems.length} line items"); + + // Add all selected modifiers recursively + await _addModifiersRecursively( + cart.orderId, + item.itemId, + selectedModifierIds, + ); + + // Refresh cart to get final state + cart = await Api.getCart(orderId: cart.orderId); + appState.updateCartItemCount(cart.itemCount); + + if (!mounted) return; + + final message = selectedModifierIds.isEmpty + ? "Added ${item.name} to cart" + : "Added ${item.name} with ${selectedModifierIds.length} customizations"; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + } catch (e) { + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Error adding to cart: $e")), + ); + } + } + + Future _addModifiersRecursively( + int orderId, + int parentItemId, + Set selectedItemIds, + ) async { + final children = _itemsByParent[parentItemId] ?? []; + + for (final child in children) { + final isSelected = selectedItemIds.contains(child.itemId); + + await Api.setLineItem( + orderId: orderId, + parentOrderLineItemId: 0, // Will be handled by backend + itemId: child.itemId, + isSelected: isSelected, + ); + + // Recursively add grandchildren + if (isSelected) { + await _addModifiersRecursively(orderId, child.itemId, selectedItemIds); + } + } } } diff --git a/lib/services/api.dart b/lib/services/api.dart index a1af3d0..be131ff 100644 --- a/lib/services/api.dart +++ b/lib/services/api.dart @@ -1,6 +1,7 @@ import "dart:convert"; import "package:http/http.dart" as http; +import "../models/cart.dart"; import "../models/menu_item.dart"; import "../models/restaurant.dart"; import "../models/service_point.dart"; @@ -307,33 +308,97 @@ class Api { } // ------------------------- - // Ordering API (stubs referenced by OrderHomeScreen) + // Cart & Orders // ------------------------- - static Future getOrCreateCart({ + static Future getOrCreateCart({ required int userId, required int businessId, required int servicePointId, required int orderTypeId, }) async { - throw StateError("endpoint_not_implemented: Api.getOrCreateCart"); + final raw = await _postRaw( + "/orders/getOrCreateCart.cfm", + { + "OrderUserID": userId, + "BusinessID": businessId, + "OrderServicePointID": servicePointId, + "OrderTypeID": orderTypeId, + }, + businessIdOverride: businessId, + ); + + final j = _requireJson(raw, "GetOrCreateCart"); + + if (!_ok(j)) { + throw StateError( + "GetOrCreateCart API returned OK=false\nERROR: ${_err(j)}\nDETAIL: ${(j["MESSAGE"] ?? "").toString()}\nHTTP Status: ${raw.statusCode}", + ); + } + + return Cart.fromJson(j); } - static Future getCart({required int orderId}) async { - throw StateError("endpoint_not_implemented: Api.getCart"); + static Future getCart({required int orderId}) async { + final raw = await _postRaw( + "/orders/getCart.cfm", + {"OrderID": orderId}, + ); + + final j = _requireJson(raw, "GetCart"); + + if (!_ok(j)) { + throw StateError( + "GetCart API returned OK=false\nERROR: ${_err(j)}\nHTTP Status: ${raw.statusCode}", + ); + } + + return Cart.fromJson(j); } - static Future setLineItem({ + static Future setLineItem({ required int orderId, required int parentOrderLineItemId, required int itemId, - required int qty, - required List selectedChildItemIds, + required bool isSelected, + int quantity = 1, + String? remark, }) async { - throw StateError("endpoint_not_implemented: Api.setLineItem"); + final raw = await _postRaw( + "/orders/setLineItem.cfm", + { + "OrderID": orderId, + "ParentOrderLineItemID": parentOrderLineItemId, + "ItemID": itemId, + "IsSelected": isSelected, + "Quantity": quantity, + if (remark != null && remark.isNotEmpty) "Remark": remark, + }, + ); + + final j = _requireJson(raw, "SetLineItem"); + + if (!_ok(j)) { + throw StateError( + "SetLineItem API returned OK=false\nERROR: ${_err(j)}\nDETAIL: ${(j["MESSAGE"] ?? "").toString()}\nHTTP Status: ${raw.statusCode}", + ); + } + + return Cart.fromJson(j); } static Future submitOrder({required int orderId}) async { - throw StateError("endpoint_not_implemented: Api.submitOrder"); + final raw = await _postRaw( + "/orders/submit.cfm", + {"OrderID": orderId}, + ); + + final j = _requireJson(raw, "SubmitOrder"); + + if (!_ok(j)) { + throw StateError( + "SubmitOrder API returned OK=false\nERROR: ${_err(j)}\nHTTP Status: ${raw.statusCode}", + ); + } } }