import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../app/app_state.dart'; import '../main.dart' show rootScaffoldMessengerKey; import '../models/cart.dart'; import '../models/menu_item.dart'; import '../services/api.dart'; import '../services/order_polling_service.dart'; import '../services/stripe_service.dart'; /// Helper class to store modifier breadcrumb paths class ModifierPath { final List names; final double price; const ModifierPath({ required this.names, required this.price, }); } class CartViewScreen extends StatefulWidget { const CartViewScreen({super.key}); @override State createState() => _CartViewScreenState(); } class _CartViewScreenState extends State { Cart? _cart; bool _isLoading = true; bool _isProcessingPayment = false; String? _error; Map _menuItemsById = {}; // Tip options as percentages static const List _tipPercentages = [0, 15, 18, 20, 25]; int _selectedTipIndex = 1; // Default to 15% double get _tipAmount { if (_cart == null) return 0.0; return _cart!.subtotal * (_tipPercentages[_selectedTipIndex] / 100); } FeeBreakdown get _feeBreakdown { if (_cart == null) { return const FeeBreakdown( subtotal: 0, tax: 0, tip: 0, payfritFee: 0, cardFee: 0, total: 0, ); } return StripeService.calculateFees( subtotal: _cart!.subtotal, tax: _cart!.tax, tip: _tipAmount, ); } @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) { // If cart not found (deleted or doesn't exist), clear it from app state if (e.toString().contains('not_found') || e.toString().contains('Order not found')) { final appState = context.read(); appState.clearCart(); setState(() { _cart = null; _isLoading = false; }); } else { 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 _processPaymentAndSubmit() async { if (_cart == null) return; final appState = context.read(); final cartOrderId = appState.cartOrderId; final businessId = appState.selectedBusinessId; if (cartOrderId == null || businessId == null) return; setState(() => _isProcessingPayment = true); try { // 1. Process payment with Stripe final paymentResult = await StripeService.processPayment( context: context, businessId: businessId, orderId: cartOrderId, subtotal: _cart!.subtotal, tax: _cart!.tax, tip: _tipAmount, ); if (!paymentResult.success) { if (!mounted) return; setState(() => _isProcessingPayment = false); if (paymentResult.error != 'Payment cancelled') { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ Icon(Icons.error, color: Colors.white), SizedBox(width: 8), Expanded(child: Text(paymentResult.error ?? 'Payment failed')), ], ), backgroundColor: Colors.red, behavior: SnackBarBehavior.floating, margin: const EdgeInsets.only(bottom: 80, left: 16, right: 16), ), ); } return; } // 2. Payment successful, now submit the order await Api.submitOrder(orderId: cartOrderId); // Set active order for polling (status 1 = submitted) appState.setActiveOrder(orderId: cartOrderId, statusId: 1); // Start polling for status updates OrderPollingService.startPolling( orderId: cartOrderId, initialStatusId: 1, onStatusUpdate: (update) { // Update app state appState.updateActiveOrderStatus(update.statusId); // Show notification using global scaffold messenger key // This works even after the cart screen is popped rootScaffoldMessengerKey.currentState?.showSnackBar( SnackBar( content: Row( children: [ Icon(Icons.notifications_active, color: Colors.white), SizedBox(width: 8), Expanded(child: Text(update.message)), ], ), backgroundColor: _getStatusColorStatic(update.statusId), duration: const Duration(seconds: 5), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.only(bottom: 80, left: 16, right: 16), ), ); }, ); // Clear cart state appState.clearCart(); if (!mounted) return; // Show success message ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Row( children: [ Icon(Icons.check_circle, color: Colors.white), SizedBox(width: 8), Expanded( child: Text( "Payment successful! Order placed. You'll receive notifications as your order is prepared.", ), ), ], ), backgroundColor: Colors.green, duration: const Duration(seconds: 5), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.only(bottom: 80, left: 16, right: 16), ), ); // Navigate back Navigator.of(context).pop(); } catch (e) { if (!mounted) return; setState(() => _isProcessingPayment = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ const Icon(Icons.error, color: Colors.white), const SizedBox(width: 8), Expanded(child: Text('Error: ${e.toString()}')), ], ), backgroundColor: Colors.red, behavior: SnackBarBehavior.floating, margin: const EdgeInsets.only(bottom: 80, left: 16, right: 16), ), ); } } Color _getStatusColor(int statusId) => _getStatusColorStatic(statusId); static Color _getStatusColorStatic(int statusId) { switch (statusId) { case 1: // Submitted return Colors.blue; case 2: // Preparing return Colors.orange; case 3: // Ready return Colors.green; case 4: // Completed return Colors.purple; default: return Colors.grey; } } @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}"; print('[Cart] Building card for root item: $itemName (OrderLineItemID=${rootItem.orderLineItemId})'); print('[Cart] Total line items in cart: ${_cart!.lineItems.length}'); // Find ALL modifiers (recursively) and build breadcrumb paths for leaf items only final modifierPaths = _buildModifierPaths(rootItem.orderLineItemId); // Calculate total price for this line item (root + all modifiers) final lineItemTotal = _calculateLineItemTotal(rootItem); print('[Cart] Found ${modifierPaths.length} modifier paths for this root item'); 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 (modifierPaths.isNotEmpty) ...[ const SizedBox(height: 8), ...modifierPaths.map((path) => _buildModifierPathRow(path)), ], 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( "\$${lineItemTotal.toStringAsFixed(2)}", style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ], ), ], ), ), ); } /// Calculate the total price for a root item including all its modifiers double _calculateLineItemTotal(OrderLineItem rootItem) { double total = rootItem.price * rootItem.quantity; total += _sumModifierPrices(rootItem.orderLineItemId, rootItem.quantity); return total; } /// Recursively sum modifier prices for a parent line item double _sumModifierPrices(int parentOrderLineItemId, int rootQuantity) { double total = 0.0; final children = _cart!.lineItems.where( (item) => !item.isDeleted && item.parentOrderLineItemId == parentOrderLineItemId ); for (final child in children) { // Modifier price is multiplied by root item quantity total += child.price * rootQuantity; // Recursively add grandchildren modifier prices total += _sumModifierPrices(child.orderLineItemId, rootQuantity); } return total; } /// Build breadcrumb paths for all leaf modifiers /// Excludes default items - they don't need to be shown in the cart List _buildModifierPaths(int rootOrderLineItemId) { final paths = []; // Get direct children of root final directChildren = _cart!.lineItems .where((item) => item.parentOrderLineItemId == rootOrderLineItemId && !item.isDeleted) .toList(); // Recursively collect leaf items with their paths void collectLeafPaths(OrderLineItem item, List currentPath) { final menuItem = _menuItemsById[item.itemId]; // Skip default items - they don't need to be repeated in the cart if (menuItem?.isCheckedByDefault == true) { return; } final children = _cart!.lineItems .where((child) => child.parentOrderLineItemId == item.orderLineItemId && !child.isDeleted) .toList(); final itemName = menuItem?.name ?? "Item #${item.itemId}"; if (children.isEmpty) { // This is a leaf - add its path paths.add(ModifierPath( names: [...currentPath, itemName], price: item.price, )); } else { // This has children - recurse into them for (final child in children) { collectLeafPaths(child, [...currentPath, itemName]); } } } for (final child in directChildren) { collectLeafPaths(child, []); } return paths; } Widget _buildModifierPathRow(ModifierPath path) { final displayText = path.names.join(' > '); 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( displayText, style: const TextStyle( fontSize: 14, color: Colors.grey, ), ), ), if (path.price > 0) Text( "+\$${path.price.toStringAsFixed(2)}", style: const TextStyle( fontSize: 14, color: Colors.grey, ), ), ], ), ); } Widget _buildCartSummary() { if (_cart == null) return const SizedBox.shrink(); final fees = _feeBreakdown; 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( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Tip Selection const Text( "Add a tip", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), const SizedBox(height: 8), Row( children: List.generate(_tipPercentages.length, (index) { final isSelected = _selectedTipIndex == index; final percent = _tipPercentages[index]; return Expanded( child: Padding( padding: EdgeInsets.only( left: index == 0 ? 0 : 4, right: index == _tipPercentages.length - 1 ? 0 : 4, ), child: GestureDetector( onTap: () => setState(() => _selectedTipIndex = index), child: Container( padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( color: isSelected ? Colors.black : Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all( color: isSelected ? Colors.black : Colors.grey.shade300, ), ), child: Center( child: Text( percent == 0 ? "No tip" : "$percent%", style: TextStyle( fontSize: 14, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, color: isSelected ? Colors.white : Colors.black, ), ), ), ), ), ), ); }), ), const SizedBox(height: 16), const Divider(height: 1), const SizedBox(height: 12), // Subtotal _buildSummaryRow("Subtotal", fees.subtotal), const SizedBox(height: 6), // Tax _buildSummaryRow("Tax (8.25%)", fees.tax), // Only show delivery fee for delivery orders (OrderTypeID = 3) if (_cart!.deliveryFee > 0 && _cart!.orderTypeId == 3) ...[ const SizedBox(height: 6), _buildSummaryRow("Delivery Fee", _cart!.deliveryFee), ], // Tip if (_tipAmount > 0) ...[ const SizedBox(height: 6), _buildSummaryRow("Tip", _tipAmount), ], const SizedBox(height: 6), // Payfrit fee _buildSummaryRow("Service Fee", fees.payfritFee, isGrey: true), const SizedBox(height: 6), // Card processing fee _buildSummaryRow("Card Processing", fees.cardFee, isGrey: true), const SizedBox(height: 12), const Divider(height: 1), const SizedBox(height: 12), // Total Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( "Total", style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), Text( "\$${fees.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 && !_isProcessingPayment) ? _processPaymentAndSubmit : null, style: ElevatedButton.styleFrom( backgroundColor: Colors.black, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), disabledBackgroundColor: Colors.grey, ), child: _isProcessingPayment ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : Text( "Pay \$${fees.total.toStringAsFixed(2)}", style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ), ), ], ), ), ); } Widget _buildSummaryRow(String label, double amount, {bool isGrey = false}) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: TextStyle( fontSize: 15, color: isGrey ? Colors.grey.shade600 : Colors.black, ), ), Text( "\$${amount.toStringAsFixed(2)}", style: TextStyle( fontSize: 15, color: isGrey ? Colors.grey.shade600 : Colors.black, ), ), ], ); } 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"), ), ], ), ); } }