payfrit-app/lib/widgets/order_status_notification.dart
John Mizerek 008b6d45b2 Add polling service for order status notifications
Client-side implementation:
- New service: order_polling_service.dart - Timer-based polling every 30s
- Updated AppState: track active order ID and current status
- Cart integration: start polling after order submission
- In-app notifications: color-coded SnackBar for status changes
- Notification widget: animated banner (created for future use)

Status notification colors:
- Blue = Submitted (1)
- Orange = Preparing (2)
- Green = Ready (3)
- Purple = Completed (4)

Polling workflow:
1. User submits order → polling starts with status=1
2. Every 30s: check backend for status changes
3. On update: show notification + update AppState
4. Continues until order completed or app closed

Simple polling approach (Option 2) with planned migration to self-hosted WebSocket push for instant updates.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-31 16:07:23 -08:00

179 lines
5 KiB
Dart

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<OrderStatusNotification> createState() => _OrderStatusNotificationState();
}
class _OrderStatusNotificationState extends State<OrderStatusNotification> with SingleTickerProviderStateMixin {
OrderStatusUpdate? _latestUpdate;
bool _isVisible = false;
AnimationController? _animationController;
Animation<double>? _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<AppState, int?>((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<Offset>(
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();