From 4e6ce8595355d8e7fad566268caca252fc61a206 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Sun, 14 Jun 2026 16:08:16 +0700 Subject: [PATCH] feat: scope notification storage by partner ID and implement order history view --- lib/screens/account_screen.dart | 3 - lib/screens/main_shell.dart | 14 +- lib/screens/notifications_screen.dart | 13 +- lib/screens/orders_screen.dart | 196 +++++++++++++++++++++----- lib/screens/settings_screen.dart | 3 - lib/services/background_service.dart | 14 +- 6 files changed, 185 insertions(+), 58 deletions(-) diff --git a/lib/screens/account_screen.dart b/lib/screens/account_screen.dart index 824c103..06c0b36 100644 --- a/lib/screens/account_screen.dart +++ b/lib/screens/account_screen.dart @@ -70,9 +70,6 @@ class _AccountScreenState extends State { void _logout() async { final prefs = await SharedPreferences.getInstance(); await prefs.remove('odoo_session'); - await prefs.remove('last_seen_notification_id'); - await prefs.remove('last_device_notified_id'); - await prefs.remove('read_notification_ids'); await NotificationService().clearBadge(); if (mounted) { Navigator.of(context).pushAndRemoveUntil( diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index 265ffd9..debe62e 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -24,8 +24,6 @@ class _MainShellState extends State { int _unreadNotificationCount = 0; Timer? _notificationTimer; - static const _kLastNotified = 'last_device_notified_id'; - late final List _pages; @override @@ -69,13 +67,17 @@ class _MainShellState extends State { if (response != null && response['status'] == 'success') { final List notifs = response['data'] ?? []; final prefs = await SharedPreferences.getInstance(); - final lastNotifiedId = prefs.getInt(_kLastNotified) ?? 0; - final readIds = prefs.getStringList('read_notification_ids'); + final partnerId = widget.partnerId; + final keyLastNotified = 'last_device_notified_id_$partnerId'; + final keyReadNotificationIds = 'read_notification_ids_$partnerId'; + + final lastNotifiedId = prefs.getInt(keyLastNotified) ?? 0; + final readIds = prefs.getStringList(keyReadNotificationIds); 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); + await prefs.setStringList(keyReadNotificationIds, initialRead); unreadCount = 0; } else { unreadCount = notifs @@ -102,7 +104,7 @@ class _MainShellState extends State { body: notif['body'] ?? '', ); } - await prefs.setInt(_kLastNotified, highestNewId); + await prefs.setInt(keyLastNotified, highestNewId); } await NotificationService().setBadge(unreadCount); diff --git a/lib/screens/notifications_screen.dart b/lib/screens/notifications_screen.dart index 3ce64ca..44485ee 100644 --- a/lib/screens/notifications_screen.dart +++ b/lib/screens/notifications_screen.dart @@ -37,7 +37,9 @@ class _NotificationsScreenState extends State { if (response != null && response['status'] == 'success') { final List fetched = response['data'] ?? []; final prefs = await SharedPreferences.getInstance(); - final readIds = prefs.getStringList('read_notification_ids') ?? []; + final partnerId = OdooService().client?.sessionId?.partnerId ?? 0; + final keyReadNotificationIds = 'read_notification_ids_$partnerId'; + final readIds = prefs.getStringList(keyReadNotificationIds) ?? []; if (mounted) { setState(() { @@ -100,11 +102,13 @@ class _NotificationsScreenState extends State { isUnread: isUnread, onTap: () async { final prefs = await SharedPreferences.getInstance(); - final readIds = prefs.getStringList('read_notification_ids') ?? []; + final partnerId = OdooService().client?.sessionId?.partnerId ?? 0; + final keyReadNotificationIds = 'read_notification_ids_$partnerId'; + final readIds = prefs.getStringList(keyReadNotificationIds) ?? []; final notifIdStr = (notif['id'] as int? ?? 0).toString(); if (!readIds.contains(notifIdStr)) { readIds.add(notifIdStr); - await prefs.setStringList('read_notification_ids', readIds); + await prefs.setStringList(keyReadNotificationIds, readIds); // Recalculate and update system badge count final unreadCount = _notifications @@ -113,7 +117,7 @@ class _NotificationsScreenState extends State { await NotificationService().setBadge(unreadCount); } - if (!mounted) return; + if (!context.mounted) return; await Navigator.push( context, MaterialPageRoute( @@ -121,6 +125,7 @@ class _NotificationsScreenState extends State { NotificationDetailScreen(notif: notif), ), ); + if (!context.mounted) return; _fetchNotifications(); }, ); diff --git a/lib/screens/orders_screen.dart b/lib/screens/orders_screen.dart index fbfc715..cac5f66 100644 --- a/lib/screens/orders_screen.dart +++ b/lib/screens/orders_screen.dart @@ -1,47 +1,171 @@ import 'package:flutter/material.dart'; +import '../services/odoo_service.dart'; import '../theme/app_theme.dart'; -/// Orders tab — placeholder screen for future ordering features. -class OrdersScreen extends StatelessWidget { +class OrdersScreen extends StatefulWidget { const OrdersScreen({super.key}); + @override + State createState() => _OrdersScreenState(); +} + +class _OrdersScreenState extends State { + List _history = []; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _fetchHistory(); + } + + Future _fetchHistory() async { + if (mounted) setState(() => _isLoading = true); + try { + final history = await OdooService().getOrderHistory(); + if (mounted) { + setState(() { + _history = history; + _isLoading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() => _isLoading = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error loading history: $e')), + ); + } + } + } + + String _formatDate(String rawDate) { + if (rawDate.isEmpty) return ''; + try { + final parsed = DateTime.parse(rawDate); + final months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ]; + final year = parsed.year; + final month = months[parsed.month - 1]; + final day = parsed.day.toString().padLeft(2, '0'); + final hour = parsed.hour.toString().padLeft(2, '0'); + final minute = parsed.minute.toString().padLeft(2, '0'); + return '$day $month $year, $hour:$minute'; + } catch (_) { + return rawDate; + } + } + @override Widget build(BuildContext context) { - return Center( - child: Padding( - padding: const EdgeInsets.all(40), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.all(28), - decoration: BoxDecoration( - color: AppTheme.surfaceContainerLow, - shape: BoxShape.circle, - ), - child: Icon( - Icons.receipt_long_rounded, - size: 56, - color: AppTheme.secondary, - ), - ), - const SizedBox(height: 28), - Text( - 'Orders', - style: Theme.of(context).textTheme.headlineMedium, - ), - const SizedBox(height: 12), - Text( - 'Online ordering is coming soon!\nStay tuned for updates.', - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: AppTheme.onSurfaceVariant, - height: 1.6, - ), - textAlign: TextAlign.center, - ), - ], - ), + final theme = Theme.of(context); + + if (_isLoading) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + + return Scaffold( + appBar: AppBar( + title: const Text('Order & Points History'), ), + body: _history.isEmpty + ? Center( + child: Padding( + padding: const EdgeInsets.all(40), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(28), + decoration: const BoxDecoration( + color: AppTheme.surfaceContainerLow, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.receipt_long_rounded, + size: 56, + color: AppTheme.secondary, + ), + ), + const SizedBox(height: 28), + Text( + 'No Orders Yet', + style: theme.textTheme.headlineMedium, + ), + const SizedBox(height: 12), + Text( + 'Your order history and points transactions will show up here after you make purchases.', + style: theme.textTheme.bodyLarge?.copyWith( + color: AppTheme.onSurfaceVariant, + height: 1.6, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ) + : RefreshIndicator( + onRefresh: _fetchHistory, + child: ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 16), + itemCount: _history.length, + itemBuilder: (context, index) { + final item = _history[index]; + final isEarn = item['type'] == 'earn'; + final orderRef = item['order_ref'] as String? ?? ''; + final rawDate = item['date'] as String? ?? ''; + + return Card( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + color: AppTheme.surfaceContainerLow, + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + leading: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: (isEarn ? const Color(0xFF2E7D32) : const Color(0xFFC62828)).withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: Icon( + isEarn ? Icons.add_card_rounded : Icons.payment_rounded, + color: isEarn ? const Color(0xFF2E7D32) : const Color(0xFFC62828), + size: 24, + ), + ), + title: Text( + orderRef.isNotEmpty ? orderRef : 'Point Adjustment', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: AppTheme.onSurface, + ), + ), + subtitle: Padding( + padding: const EdgeInsets.only(top: 6.0), + child: Text( + _formatDate(rawDate), + style: theme.textTheme.bodySmall?.copyWith( + color: AppTheme.onSurfaceVariant, + ), + ), + ), + trailing: Text( + '${isEarn ? '+' : ''}${item['points']} pts', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: isEarn ? const Color(0xFF2E7D32) : const Color(0xFFC62828), + fontSize: 16, + ), + ), + ), + ); + }, + ), + ), ); } } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index f862073..2ae23f4 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -181,9 +181,6 @@ class _SettingsScreenState extends State { void _logout() async { final prefs = await SharedPreferences.getInstance(); await prefs.remove('odoo_session'); - await prefs.remove('last_seen_notification_id'); - await prefs.remove('last_device_notified_id'); - await prefs.remove('read_notification_ids'); await NotificationService().clearBadge(); if (mounted) { Navigator.of(context).pushAndRemoveUntil( diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart index 1b8b8fb..3982bba 100644 --- a/lib/services/background_service.dart +++ b/lib/services/background_service.dart @@ -6,7 +6,6 @@ import 'notification_service.dart'; // NOTE: This key tracks what IDs have been shown as device tray notifications. // It is separate from 'last_seen_notification_id' (which tracks what the user READ in-app). -const String _kLastDeviceNotifiedId = 'last_device_notified_id'; @pragma('vm:entry-point') void callbackDispatcher() { @@ -20,11 +19,13 @@ void callbackDispatcher() { return Future.value(true); // Not logged in, nothing to do } - final lastDeviceNotifiedId = prefs.getInt(_kLastDeviceNotifiedId) ?? 0; - final sessionArgs = json.decode(sessionStr); final session = OdooSession.fromJson( Map.from(sessionArgs as Map)); + final partnerId = session.partnerId; + final keyLastDeviceNotifiedId = 'last_device_notified_id_$partnerId'; + final keyReadNotificationIds = 'read_notification_ids_$partnerId'; + final client = OdooClient(url, sessionId: session); final response = await client.callRPC( @@ -40,6 +41,7 @@ void callbackDispatcher() { List.from(response['data'] ?? []); // Filter to only truly new ones not yet shown on device tray + final lastDeviceNotifiedId = prefs.getInt(keyLastDeviceNotifiedId) ?? 0; final newNotifs = notifications .where((n) => (n['id'] as int? ?? 0) > lastDeviceNotifiedId) .toList(); @@ -60,15 +62,15 @@ void callbackDispatcher() { ); } - await prefs.setInt(_kLastDeviceNotifiedId, highestId); + await prefs.setInt(keyLastDeviceNotifiedId, highestId); } // Always compute badge count based on read_notification_ids - final readIds = prefs.getStringList('read_notification_ids'); + final readIds = prefs.getStringList(keyReadNotificationIds); if (readIds == null) { // Initialize read list with all currently fetched notifications on first install/run final initialRead = notifications.map((n) => (n['id'] as int? ?? 0).toString()).toList(); - await prefs.setStringList('read_notification_ids', initialRead); + await prefs.setStringList(keyReadNotificationIds, initialRead); await notifService.setBadge(0); } else { final unreadCount = notifications