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:
parent
5107a9f434
commit
029c924f41
5 changed files with 93 additions and 32 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue