diff --git a/lib/main.dart b/lib/main.dart index 4721c4a..2752b8f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,10 @@ import "package:provider/provider.dart"; import "app/app_router.dart" show AppRoutes; import "app/app_state.dart" show AppState; +/// Global key for showing snackbars from anywhere in the app +final GlobalKey rootScaffoldMessengerKey = + GlobalKey(); + void main() { WidgetsFlutterBinding.ensureInitialized(); runApp(const PayfritApp()); @@ -19,6 +23,7 @@ class PayfritApp extends StatelessWidget { ChangeNotifierProvider(create: (_) => AppState()), ], child: MaterialApp( + scaffoldMessengerKey: rootScaffoldMessengerKey, debugShowCheckedModeBanner: false, title: "Payfrit", initialRoute: AppRoutes.splash, diff --git a/lib/screens/cart_view_screen.dart b/lib/screens/cart_view_screen.dart index e81e9d1..99580a5 100644 --- a/lib/screens/cart_view_screen.dart +++ b/lib/screens/cart_view_screen.dart @@ -2,6 +2,7 @@ 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'; @@ -166,17 +167,16 @@ class _CartViewScreenState extends State { // Update app state appState.updateActiveOrderStatus(update.statusId); - // Show notification - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(update.message), - backgroundColor: _getStatusColor(update.statusId), - duration: const Duration(seconds: 5), - behavior: SnackBarBehavior.floating, - ), - ); - } + // Show notification using global scaffold messenger key + // This works even after the cart screen is popped + rootScaffoldMessengerKey.currentState?.showSnackBar( + SnackBar( + content: Text(update.message), + backgroundColor: _getStatusColorStatic(update.statusId), + duration: const Duration(seconds: 5), + behavior: SnackBarBehavior.floating, + ), + ); }, ); @@ -204,7 +204,9 @@ class _CartViewScreenState extends State { } } - Color _getStatusColor(int statusId) { + Color _getStatusColor(int statusId) => _getStatusColorStatic(statusId); + + static Color _getStatusColorStatic(int statusId) { switch (statusId) { case 1: // Submitted return Colors.blue; diff --git a/lib/screens/menu_browse_screen.dart b/lib/screens/menu_browse_screen.dart index 5ea97e6..5324792 100644 --- a/lib/screens/menu_browse_screen.dart +++ b/lib/screens/menu_browse_screen.dart @@ -308,12 +308,12 @@ class _MenuBrowseScreenState extends State { ); }, ), - // Top edge gradient (sharp, short fade) + // Top edge gradient Positioned( top: 0, left: 0, right: 0, - height: 20, + height: 16, child: Container( decoration: BoxDecoration( gradient: LinearGradient( @@ -327,12 +327,12 @@ class _MenuBrowseScreenState extends State { ), ), ), - // Bottom edge gradient (sharp, short fade) + // Bottom edge gradient Positioned( bottom: 0, left: 0, right: 0, - height: 28, + height: 16, child: Container( decoration: BoxDecoration( gradient: LinearGradient( @@ -435,12 +435,12 @@ class _MenuBrowseScreenState extends State { ); }, ), - // Top edge gradient (sharp, short fade) + // Top edge gradient Positioned( top: 0, left: 0, right: 0, - height: 14, + height: 16, child: Container( decoration: BoxDecoration( gradient: LinearGradient( @@ -454,12 +454,12 @@ class _MenuBrowseScreenState extends State { ), ), ), - // Bottom edge gradient (sharp, short fade) + // Bottom edge gradient Positioned( bottom: 0, left: 0, right: 0, - height: 24, + height: 16, child: Container( decoration: BoxDecoration( gradient: LinearGradient( @@ -726,6 +726,7 @@ class _MenuBrowseScreenState extends State { ) async { final children = _itemsByParent[parentItemId] ?? []; print('[MenuBrowse] _addModifiersRecursively: parentItemId=$parentItemId has ${children.length} children'); + print('[MenuBrowse] selectedItemIds passed in: $selectedItemIds'); for (final child in children) { final isSelected = selectedItemIds.contains(child.itemId); @@ -735,9 +736,11 @@ class _MenuBrowseScreenState extends State { print('[MenuBrowse] Child ${child.name} (ItemID=${child.itemId}): selected=$isSelected, hasChildren=$hasGrandchildren, hasSelectedDescendants=$hasSelectedDescendants'); - // Add this item if it's selected OR if it has selected descendants (to maintain hierarchy) - if (isSelected || hasSelectedDescendants) { - print('[MenuBrowse] Adding ${isSelected ? "selected" : "container"} item ${child.name} with ParentOrderLineItemID=$parentOrderLineItemId'); + // Only add this item if it's explicitly selected + // Container items (parents) should only be added if they themselves are in selectedItemIds + // This prevents default items from being submitted when the user hasn't modified them + if (isSelected) { + print('[MenuBrowse] ADDING selected item ${child.name} with ParentOrderLineItemID=$parentOrderLineItemId'); final cart = await Api.setLineItem( orderId: orderId, parentOrderLineItemId: parentOrderLineItemId, @@ -760,6 +763,32 @@ class _MenuBrowseScreenState extends State { selectedItemIds, ); } + } else if (hasSelectedDescendants) { + // This item itself is not selected, but it has selected descendants + // We need to add it as a container to maintain hierarchy + print('[MenuBrowse] ADDING container item ${child.name} (has selected descendants) with ParentOrderLineItemID=$parentOrderLineItemId'); + final cart = await Api.setLineItem( + orderId: orderId, + parentOrderLineItemId: parentOrderLineItemId, + itemId: child.itemId, + isSelected: true, + ); + + // Find the OrderLineItemID of this item we just added + final childLineItem = cart.lineItems.lastWhere( + (li) => li.itemId == child.itemId && li.parentOrderLineItemId == parentOrderLineItemId && !li.isDeleted, + orElse: () => throw StateError('Child line item not found for ItemID=${child.itemId}'), + ); + + // Recursively add children with this item as the new parent + await _addModifiersRecursively( + orderId, + childLineItem.orderLineItemId, + child.itemId, + selectedItemIds, + ); + } else { + print('[MenuBrowse] SKIPPING ${child.name} (not selected, no selected descendants)'); } } } @@ -810,10 +839,13 @@ class _ItemCustomizationSheetState extends State<_ItemCustomizationSheet> { /// Recursively initialize default selections void _initializeDefaults(int parentId) { final children = widget.itemsByParent[parentId] ?? []; + print('[Customization] _initializeDefaults for parentId=$parentId, found ${children.length} children'); for (final child in children) { + print('[Customization] Child ${child.name} (ID=${child.itemId}): isCheckedByDefault=${child.isCheckedByDefault}'); if (child.isCheckedByDefault) { _selectedItemIds.add(child.itemId); _defaultItemIds.add(child.itemId); // Remember this was a default + print('[Customization] -> Added to defaults and selected'); _initializeDefaults(child.itemId); } } @@ -877,20 +909,30 @@ class _ItemCustomizationSheetState extends State<_ItemCustomizationSheet> { // Filter out default items in groups that user never modified final itemsToSubmit = {}; + print('[Customization] ========== FILTERING LOGIC =========='); + print('[Customization] All selected items: $_selectedItemIds'); + print('[Customization] Default items: $_defaultItemIds'); + print('[Customization] User-modified groups: $_userModifiedGroups'); + for (final itemId in _selectedItemIds) { // Find which parent group this item belongs to final parentId = _findParentId(itemId); + final isDefault = _defaultItemIds.contains(itemId); + final groupWasModified = parentId != null && _userModifiedGroups.contains(parentId); + + print('[Customization] Item $itemId: isDefault=$isDefault, parentId=$parentId, groupWasModified=$groupWasModified'); // Include if: not a default, OR user modified this group - if (!_defaultItemIds.contains(itemId) || _userModifiedGroups.contains(parentId)) { + if (!isDefault || groupWasModified) { + print('[Customization] -> INCLUDED (not default or group was modified)'); itemsToSubmit.add(itemId); + } else { + print('[Customization] -> EXCLUDED (is default and group was not modified)'); } } - print('[Customization] Selected: $_selectedItemIds'); - print('[Customization] Defaults: $_defaultItemIds'); - print('[Customization] Modified groups: $_userModifiedGroups'); - print('[Customization] Submitting: $itemsToSubmit'); + print('[Customization] Final items to submit: $itemsToSubmit'); + print('[Customization] ====================================='); widget.onAdd(itemsToSubmit); } diff --git a/lib/services/api.dart b/lib/services/api.dart index 3a800b4..ce398dd 100644 --- a/lib/services/api.dart +++ b/lib/services/api.dart @@ -90,7 +90,19 @@ class Api { try { final decoded = jsonDecode(body); if (decoded is Map) return decoded; - } catch (_) {} + } catch (_) { + // If JSON parsing fails, try to extract JSON from the body + // (handles cases where debug output is appended after JSON) + final jsonStart = body.indexOf('{'); + final jsonEnd = body.lastIndexOf('}'); + if (jsonStart >= 0 && jsonEnd > jsonStart) { + try { + final jsonPart = body.substring(jsonStart, jsonEnd + 1); + final decoded = jsonDecode(jsonPart); + if (decoded is Map) return decoded; + } catch (_) {} + } + } return null; } diff --git a/lib/services/order_polling_service.dart b/lib/services/order_polling_service.dart index 9644af5..fc95aff 100644 --- a/lib/services/order_polling_service.dart +++ b/lib/services/order_polling_service.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'api.dart'; /// Service that polls the backend API for order status updates -/// Uses a simple polling approach (Option 2) with 30-second intervals +/// Uses a simple polling approach with 6-second intervals for responsive updates class OrderPollingService { static Timer? _pollingTimer; static int? _currentOrderId; @@ -25,8 +25,8 @@ class OrderPollingService { _lastKnownStatusId = initialStatusId; _onStatusUpdate = onStatusUpdate; - // Poll every 30 seconds - _pollingTimer = Timer.periodic(const Duration(seconds: 30), (_) { + // Poll every 6 seconds for responsive updates + _pollingTimer = Timer.periodic(const Duration(seconds: 6), (_) { _checkForUpdate(); });