Fix order status polling and improve notifications

- Reduce polling interval from 30s to 6s for faster updates
- Add global ScaffoldMessengerKey for app-wide snackbar notifications
- Fix notifications showing after cart screen is dismissed
- Add JSON parsing fallback to handle server debug output
- Standardize all gradient heights to 16px

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
John Mizerek 2026-01-01 19:48:42 -08:00
parent 5107a9f434
commit 029c924f41
5 changed files with 93 additions and 32 deletions

View file

@ -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<ScaffoldMessengerState> rootScaffoldMessengerKey =
GlobalKey<ScaffoldMessengerState>();
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const PayfritApp());
@ -19,6 +23,7 @@ class PayfritApp extends StatelessWidget {
ChangeNotifierProvider<AppState>(create: (_) => AppState()),
],
child: MaterialApp(
scaffoldMessengerKey: rootScaffoldMessengerKey,
debugShowCheckedModeBanner: false,
title: "Payfrit",
initialRoute: AppRoutes.splash,

View file

@ -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<CartViewScreen> {
// Update app state
appState.updateActiveOrderStatus(update.statusId);
// Show notification
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
// 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: _getStatusColor(update.statusId),
backgroundColor: _getStatusColorStatic(update.statusId),
duration: const Duration(seconds: 5),
behavior: SnackBarBehavior.floating,
),
);
}
},
);
@ -204,7 +204,9 @@ class _CartViewScreenState extends State<CartViewScreen> {
}
}
Color _getStatusColor(int statusId) {
Color _getStatusColor(int statusId) => _getStatusColorStatic(statusId);
static Color _getStatusColorStatic(int statusId) {
switch (statusId) {
case 1: // Submitted
return Colors.blue;

View file

@ -308,12 +308,12 @@ class _MenuBrowseScreenState extends State<MenuBrowseScreen> {
);
},
),
// 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<MenuBrowseScreen> {
),
),
),
// 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<MenuBrowseScreen> {
);
},
),
// 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<MenuBrowseScreen> {
),
),
),
// 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<MenuBrowseScreen> {
) 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<MenuBrowseScreen> {
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<MenuBrowseScreen> {
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 = <int>{};
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);
}

View file

@ -90,7 +90,19 @@ class Api {
try {
final decoded = jsonDecode(body);
if (decoded is Map<String, dynamic>) return decoded;
} 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<String, dynamic>) return decoded;
} catch (_) {}
}
}
return null;
}

View file

@ -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();
});