diff --git a/lib/app/app_state.dart b/lib/app/app_state.dart index 638f9af..6cf3392 100644 --- a/lib/app/app_state.dart +++ b/lib/app/app_state.dart @@ -12,6 +12,8 @@ class AppState extends ChangeNotifier { String? _cartOrderUuid; int _cartItemCount = 0; + int? _activeOrderId; + int? _activeOrderStatusId; int? get selectedBusinessId => _selectedBusinessId; String? get selectedBusinessName => _selectedBusinessName; @@ -25,6 +27,9 @@ class AppState extends ChangeNotifier { String? get cartOrderUuid => _cartOrderUuid; int get cartItemCount => _cartItemCount; + int? get activeOrderId => _activeOrderId; + int? get activeOrderStatusId => _activeOrderStatusId; + bool get hasLocationSelection => _selectedBusinessId != null && _selectedServicePointId != null; @@ -94,6 +99,22 @@ class AppState extends ChangeNotifier { notifyListeners(); } + void setActiveOrder({required int orderId, required int statusId}) { + _activeOrderId = orderId; + _activeOrderStatusId = statusId; + notifyListeners(); + } + + void updateActiveOrderStatus(int statusId) { + _activeOrderStatusId = statusId; + notifyListeners(); + } + + void clearActiveOrder() { + _activeOrderId = null; + _activeOrderStatusId = null; + notifyListeners(); + } void clearAll() { _selectedBusinessId = null; @@ -102,6 +123,7 @@ class AppState extends ChangeNotifier { _cartOrderId = null; _cartOrderUuid = null; - + _activeOrderId = null; + _activeOrderStatusId = null; } } diff --git a/lib/screens/cart_view_screen.dart b/lib/screens/cart_view_screen.dart index bfc8e05..b19d221 100644 --- a/lib/screens/cart_view_screen.dart +++ b/lib/screens/cart_view_screen.dart @@ -5,6 +5,7 @@ import '../app/app_state.dart'; import '../models/cart.dart'; import '../models/menu_item.dart'; import '../services/api.dart'; +import '../services/order_polling_service.dart'; class CartViewScreen extends StatefulWidget { const CartViewScreen({super.key}); @@ -133,6 +134,31 @@ class _CartViewScreenState extends State { 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 + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(update.message), + backgroundColor: _getStatusColor(update.statusId), + duration: const Duration(seconds: 5), + behavior: SnackBarBehavior.floating, + ), + ); + } + }, + ); + // Clear cart state appState.clearCart(); @@ -141,8 +167,9 @@ class _CartViewScreenState extends State { // Show success message ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text("Order submitted successfully!"), + content: Text("Order submitted successfully! You'll receive notifications as your order is prepared."), backgroundColor: Colors.green, + duration: Duration(seconds: 5), ), ); @@ -156,6 +183,21 @@ class _CartViewScreenState extends State { } } + Color _getStatusColor(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( diff --git a/lib/services/order_polling_service.dart b/lib/services/order_polling_service.dart new file mode 100644 index 0000000..48fcd9f --- /dev/null +++ b/lib/services/order_polling_service.dart @@ -0,0 +1,119 @@ +import 'dart:async'; +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 +class OrderPollingService { + static Timer? _pollingTimer; + static int? _currentOrderId; + static int? _lastKnownStatusId; + static Function(OrderStatusUpdate)? _onStatusUpdate; + + /// Start polling for order status updates + static void startPolling({ + required int orderId, + required int initialStatusId, + required Function(OrderStatusUpdate) onStatusUpdate, + }) { + print('[OrderPolling] 🔄 Starting polling for OrderID=$orderId, Status=$initialStatusId'); + + // Stop any existing polling + stopPolling(); + + _currentOrderId = orderId; + _lastKnownStatusId = initialStatusId; + _onStatusUpdate = onStatusUpdate; + + // Poll every 30 seconds + _pollingTimer = Timer.periodic(const Duration(seconds: 30), (_) { + _checkForUpdate(); + }); + + // Also check immediately + _checkForUpdate(); + } + + /// Stop polling + static void stopPolling() { + print('[OrderPolling] âšī¸ Stopping polling'); + _pollingTimer?.cancel(); + _pollingTimer = null; + _currentOrderId = null; + _lastKnownStatusId = null; + _onStatusUpdate = null; + } + + /// Check if currently polling + static bool get isPolling => _pollingTimer != null && _pollingTimer!.isActive; + + /// Get current order ID being polled + static int? get currentOrderId => _currentOrderId; + + /// Internal method to check for updates + static Future _checkForUpdate() async { + if (_currentOrderId == null || _lastKnownStatusId == null) { + print('[OrderPolling] âš ī¸ No order to poll, stopping'); + stopPolling(); + return; + } + + try { + print('[OrderPolling] 🔍 Checking status for OrderID=$_currentOrderId'); + + final response = await Api.post( + '/orders/checkStatusUpdate.cfm', + body: { + 'OrderID': _currentOrderId, + 'LastKnownStatusID': _lastKnownStatusId, + }, + ); + + final data = response.data; + + if (data['OK'] == true && data['HAS_UPDATE'] == true) { + final orderStatus = data['ORDER_STATUS']; + final newStatusId = orderStatus['StatusID'] as int; + final statusName = orderStatus['StatusName'] as String; + final message = orderStatus['Message'] as String; + + print('[OrderPolling] ✅ Status update detected: $statusName (ID=$newStatusId)'); + + // Update last known status + _lastKnownStatusId = newStatusId; + + // Notify listener + if (_onStatusUpdate != null) { + _onStatusUpdate!(OrderStatusUpdate( + orderId: _currentOrderId!, + statusId: newStatusId, + statusName: statusName, + message: message, + )); + } + } else { + print('[OrderPolling] â„šī¸ No status update'); + } + } catch (e) { + print('[OrderPolling] ❌ Error checking status: $e'); + } + } +} + +/// Data class representing an order status update +class OrderStatusUpdate { + final int orderId; + final int statusId; + final String statusName; + final String message; + + const OrderStatusUpdate({ + required this.orderId, + required this.statusId, + required this.statusName, + required this.message, + }); + + @override + String toString() => 'OrderStatusUpdate(orderId: $orderId, statusId: $statusId, statusName: $statusName, message: $message)'; +} diff --git a/lib/widgets/order_status_notification.dart b/lib/widgets/order_status_notification.dart new file mode 100644 index 0000000..939bc37 --- /dev/null +++ b/lib/widgets/order_status_notification.dart @@ -0,0 +1,179 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../app/app_state.dart'; +import '../services/order_polling_service.dart'; + +/// In-app notification banner that shows order status updates +class OrderStatusNotification extends StatefulWidget { + const OrderStatusNotification({super.key}); + + @override + State createState() => _OrderStatusNotificationState(); +} + +class _OrderStatusNotificationState extends State with SingleTickerProviderStateMixin { + OrderStatusUpdate? _latestUpdate; + bool _isVisible = false; + AnimationController? _animationController; + Animation? _slideAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + ); + _slideAnimation = CurvedAnimation( + parent: _animationController!, + curve: Curves.easeOut, + ); + } + + @override + void dispose() { + _animationController?.dispose(); + super.dispose(); + } + + void _showNotification(OrderStatusUpdate update) { + setState(() { + _latestUpdate = update; + _isVisible = true; + }); + _animationController?.forward(); + + // Auto-hide after 5 seconds + Future.delayed(const Duration(seconds: 5), () { + _hideNotification(); + }); + } + + void _hideNotification() { + _animationController?.reverse().then((_) { + if (mounted) { + setState(() { + _isVisible = false; + _latestUpdate = null; + }); + } + }); + } + + Color _getStatusColor(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; + } + } + + IconData _getStatusIcon(int statusId) { + switch (statusId) { + case 1: // Submitted + return Icons.receipt_long; + case 2: // Preparing + return Icons.restaurant; + case 3: // Ready + return Icons.notifications_active; + case 4: // Completed + return Icons.check_circle; + default: + return Icons.info; + } + } + + @override + Widget build(BuildContext context) { + // Listen for status updates + context.select((state) => state.activeOrderStatusId); + + if (!_isVisible || _latestUpdate == null) { + return const SizedBox.shrink(); + } + + final statusColor = _getStatusColor(_latestUpdate!.statusId); + final statusIcon = _getStatusIcon(_latestUpdate!.statusId); + + return SlideTransition( + position: Tween( + begin: const Offset(0, -1), + end: Offset.zero, + ).animate(_slideAnimation!), + child: Material( + elevation: 8, + color: Colors.transparent, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: statusColor, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: SafeArea( + child: Row( + children: [ + Icon( + statusIcon, + color: Colors.white, + size: 28, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _latestUpdate!.statusName, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + _latestUpdate!.message, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + ), + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: _hideNotification, + ), + ], + ), + ), + ), + ), + ); + } + + /// Static method to show notification from anywhere + static void show(BuildContext context, OrderStatusUpdate update) { + final state = context.findAncestorStateOfType<_OrderStatusNotificationState>(); + state?._showNotification(update); + } +} + +/// Global key to access notification from anywhere +final GlobalKey<_OrderStatusNotificationState> orderNotificationKey = GlobalKey();