import 'dart:async'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../services/odoo_service.dart'; import '../services/notification_service.dart'; import '../theme/app_theme.dart'; import 'notifications_screen.dart'; import 'branches_screen.dart'; import 'settings_screen.dart'; class LoyaltyDashboard extends StatefulWidget { final int partnerId; const LoyaltyDashboard({super.key, required this.partnerId}); @override State createState() => _LoyaltyDashboardState(); } class _LoyaltyDashboardState extends State { List _loyaltyCards = []; bool _isLoading = true; int _unreadNotificationCount = 0; Timer? _notificationTimer; // Shared pref keys static const _kLastNotified = 'last_device_notified_id'; @override void initState() { super.initState(); _fetchLoyaltyData(); _fetchNotificationCount(); _notificationTimer = Timer.periodic(const Duration(seconds: 10), (_) { _fetchNotificationCount(); }); } @override void dispose() { _notificationTimer?.cancel(); super.dispose(); } Future _fetchNotificationCount() async { try { final client = OdooService().client; if (client == null) return; final response = await client.callRPC( '/api/loyalty/fetch_notifications', 'call', {'last_id': 0}, ); if (response != null && response['status'] == 'success') { final List notifs = response['data'] ?? []; final prefs = await SharedPreferences.getInstance(); final lastNotifiedId = prefs.getInt(_kLastNotified) ?? 0; // Check read list final readIds = prefs.getStringList('read_notification_ids'); int unreadCount = 0; if (readIds == null) { final initialRead = notifs.map((n) => (n['id'] as int? ?? 0).toString()).toList(); await prefs.setStringList('read_notification_ids', initialRead); unreadCount = 0; } else { unreadCount = notifs .where((n) => !readIds.contains((n['id'] as int? ?? 0).toString())) .length; } int highestNewId = lastNotifiedId; final List toNotify = []; for (var notif in notifs) { final id = notif['id'] as int? ?? 0; if (id > lastNotifiedId) { toNotify.add(notif); if (id > highestNewId) highestNewId = id; } } // Show device tray notifications for any not yet shown if (toNotify.isNotEmpty) { final notifService = NotificationService(); for (final notif in toNotify) { await notifService.showNotification( id: notif['id'] as int, title: notif['title'] ?? 'Mie Mapan', body: notif['body'] ?? '', ); } await prefs.setInt(_kLastNotified, highestNewId); } // Always update system badge to match the unread count await NotificationService().setBadge(unreadCount); if (mounted) { setState(() => _unreadNotificationCount = unreadCount); } } } catch (e) { // ignore } } Future _fetchLoyaltyData() async { try { final cards = await OdooService().getLoyaltyCards(widget.partnerId); await _fetchNotificationCount(); setState(() { _loyaltyCards = cards; _isLoading = false; }); } catch (e) { if (mounted) { setState(() => _isLoading = false); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error loading loyalty cards: $e'))); } } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('My Rewards'), actions: [ IconButton( icon: const Icon(Icons.storefront), onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const BranchesScreen())), ), Stack( clipBehavior: Clip.none, children: [ IconButton( icon: const Icon(Icons.notifications), onPressed: () async { await Navigator.push(context, MaterialPageRoute(builder: (_) => const NotificationsScreen())); _fetchNotificationCount(); }, ), if (_unreadNotificationCount > 0) Positioned( right: 8, top: 8, child: Container( padding: const EdgeInsets.all(4), decoration: const BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), constraints: const BoxConstraints( minWidth: 16, minHeight: 16, ), child: Text( '$_unreadNotificationCount', style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), ), ), ], ), IconButton( icon: const Icon(Icons.settings), onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsScreen())), ), const SizedBox(width: 8), ], ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : RefreshIndicator( onRefresh: _fetchLoyaltyData, child: _loyaltyCards.isEmpty ? Center( child: Text( 'No active rewards yet.', style: Theme.of(context).textTheme.titleLarge, ), ) : ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 32.0, vertical: 40.0), itemCount: _loyaltyCards.length, itemBuilder: (context, index) { final card = _loyaltyCards[index]; return Container( margin: const EdgeInsets.only(bottom: 40), padding: const EdgeInsets.all(32), decoration: const BoxDecoration( color: AppTheme.surfaceContainerHighest, // Soft Lift without shadow borderRadius: BorderRadius.zero, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( '${card['program_id']?[1] ?? 'Loyalty Program'}', style: Theme.of(context).textTheme.titleLarge, softWrap: true, ), ), const SizedBox(width: 16), Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: const BoxDecoration( color: AppTheme.secondaryContainer, borderRadius: BorderRadius.zero, // Editorial block ), child: Text( (() { final programName = (card['program_id']?[1] as String? ?? '').toLowerCase(); if (programName.contains('silver')) return 'Silver Member'; if (programName.contains('gold')) return 'Gold Member'; if (programName.contains('platinum')) return 'Platinum Member'; return 'Member'; })(), style: Theme.of(context).textTheme.labelLarge?.copyWith( color: AppTheme.onSecondaryContainer, fontWeight: FontWeight.bold, ), ), ), ], ), const SizedBox(height: 32), Text('Membership Code', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 4), Text('${card['code'] ?? 'N/A'}', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 24), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, children: [ Text('Available Points', style: Theme.of(context).textTheme.bodyMedium), Text( '${card['points'] ?? 0}', style: Theme.of(context).textTheme.displayMedium?.copyWith( color: AppTheme.primary, ), ), ], ), ], ), ); }, ), ), ); } }