From 70066c26e8ef1832a78ceaab1221f72c78624e2d Mon Sep 17 00:00:00 2001 From: llsajjad Date: Tue, 23 Sep 2025 15:01:42 +0330 Subject: [PATCH 1/3] Added markAll as read for notification --- .../repositories/notification_repository.dart | 7 +- lib/data/services/api/notification_api.dart | 2 +- lib/data/services/notification_service.dart | 17 +-- lib/view_models/notification_view_model.dart | 112 ++++++++++-------- .../pages/d_notification_page.dart | 15 ++- .../pages/m_notification_page.dart | 8 +- .../pages/t_notification_page.dart | 11 +- 7 files changed, 101 insertions(+), 71 deletions(-) diff --git a/lib/data/repositories/notification_repository.dart b/lib/data/repositories/notification_repository.dart index 78d94bc..e80f732 100644 --- a/lib/data/repositories/notification_repository.dart +++ b/lib/data/repositories/notification_repository.dart @@ -18,7 +18,8 @@ class NotificationsRepository { ); } - Future> markAllAsRead() async { - return _notificationsService.markAllAsRead(); - } +Future>> markAllAsRead() async { + return _notificationsService.markAllAsRead(); +} + } diff --git a/lib/data/services/api/notification_api.dart b/lib/data/services/api/notification_api.dart index d5a9273..e0b988a 100644 --- a/lib/data/services/api/notification_api.dart +++ b/lib/data/services/api/notification_api.dart @@ -7,7 +7,7 @@ class NotificationApi { - static const String markAllAsRead = '/api/v1/notifications'; + static const String markAllAsRead = '/api/v1/notifications/mark'; static const String markAsRead = '/api/v1/notifications/mark-as-read'; static const String tenderApprovalsTender = '/api/v1/notifications'; static String getTenderApprovalById({required String tenderId}) { diff --git a/lib/data/services/notification_service.dart b/lib/data/services/notification_service.dart index 8c15908..5d8e809 100644 --- a/lib/data/services/notification_service.dart +++ b/lib/data/services/notification_service.dart @@ -24,14 +24,15 @@ class NotificationsService { return result; } - Future> markAllAsRead() async { - final result = await _networkManager.makeRequest( - NotificationApi.markAllAsRead, - method: 'POST', - (json) => NotificationResponseModel.fromJson(json), - ); - return result; - } +Future>> markAllAsRead() async { + final result = await _networkManager.makeRequest( + NotificationApi.markAllAsRead, + method: 'GET', + (json) => json, + ); + return result; +} + Future> markAsRead({ required String notificationId, diff --git a/lib/view_models/notification_view_model.dart b/lib/view_models/notification_view_model.dart index a5d84d0..47a647f 100644 --- a/lib/view_models/notification_view_model.dart +++ b/lib/view_models/notification_view_model.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:tm_app/core/utils/app_toast.dart'; import 'package:tm_app/core/utils/result.dart'; import 'package:tm_app/data/repositories/notification_repository.dart'; import 'package:tm_app/data/services/model/notification__response/notification_response_model.dart'; @@ -30,49 +31,49 @@ class NotificationViewModel with ChangeNotifier { int get totalPages => _totalPages; /// Fetch notifications -Future getNotifications({int page = 1, bool isMobile = false}) async { - _isLoading = true; - notifyListeners(); + Future getNotifications({int page = 1, bool isMobile = false}) async { + _isLoading = true; + notifyListeners(); - final int offset = _pageSize * page; + final int offset = _pageSize * page; - final result = await _repository.getNotifications( - limit: _pageSize, - offset: offset, - ); + final result = await _repository.getNotifications( + limit: _pageSize, + offset: offset, + ); - switch (result) { - case Ok(): - if (isMobile && page > 1) { - final oldNotifications = - _notificationResponseModel?.data?.notifications ?? []; + switch (result) { + case Ok(): + if (isMobile && page > 1) { + final oldNotifications = + _notificationResponseModel?.data?.notifications ?? []; - final newNotifications = result.value.data?.notifications ?? []; + final newNotifications = result.value.data?.notifications ?? []; - _notificationResponseModel = result.value.copyWith( - data: result.value.data?.copyWith( - notifications: [...oldNotifications, ...newNotifications], - ), - ); - } else { - _notificationResponseModel = result.value; - } + _notificationResponseModel = result.value.copyWith( + data: result.value.data?.copyWith( + notifications: [...oldNotifications, ...newNotifications], + ), + ); + } else { + _notificationResponseModel = result.value; + } - currentPage = page; - _hasMoreData = - (result.value.data?.notifications?.length ?? 0) >= _pageSize; - _totalPages = result.value.data?.meta?.pages ?? 1; - break; + currentPage = page; + _hasMoreData = + (result.value.data?.notifications?.length ?? 0) >= _pageSize; + _totalPages = result.value.data?.meta?.pages ?? 1; + break; - case Error(): - _errorMessage = result.error.toString(); - break; + case Error(): + _errorMessage = result.error.toString(); + break; + } + + _isLoading = false; + notifyListeners(); } - _isLoading = false; - notifyListeners(); -} - /// Jump to page Future jumpToPage(int page) async { if (_totalPages == 0 || page < 1 || page > _totalPages) { @@ -82,33 +83,50 @@ Future getNotifications({int page = 1, bool isMobile = false}) async { } /// Mark all as read - Future markAllAsRead() async { + Future markAllAsRead(BuildContext context) async { + _isLoading = true; _errorMessage = null; notifyListeners(); final result = await _repository.markAllAsRead(); switch (result) { - case Ok(): - if (_notificationResponseModel?.data?.notifications != null) { - final updatedNotifications = - _notificationResponseModel!.data!.notifications! - .map((n) => n.copyWith(seen: true)) - .toList(); + case Ok>(): + final response = result.value; + final success = response['success'] as bool? ?? false; + final message = response['message'] as String? ?? 'Unknown response'; - _notificationResponseModel = _notificationResponseModel!.copyWith( - data: _notificationResponseModel!.data!.copyWith( - notifications: updatedNotifications, - ), - ); + if (success) { + if (_notificationResponseModel?.data?.notifications != null) { + final updatedNotifications = + _notificationResponseModel!.data!.notifications! + .map((n) => n.copyWith(seen: true)) + .toList(); + + _notificationResponseModel = _notificationResponseModel!.copyWith( + data: _notificationResponseModel!.data!.copyWith( + notifications: updatedNotifications, + ), + ); + } + + await getNotifications(page: 1); + + // toast success + AppToast.success(context, 'All notifications marked as read.'); + } else { + _errorMessage = message; + AppToast.error(context, message); } break; - case Error(): + case Error>(): _errorMessage = result.error.toString(); + AppToast.error(context, _errorMessage!); break; } + _isLoading = false; notifyListeners(); } diff --git a/lib/views/notification/pages/d_notification_page.dart b/lib/views/notification/pages/d_notification_page.dart index 06df345..e1e6cf5 100644 --- a/lib/views/notification/pages/d_notification_page.dart +++ b/lib/views/notification/pages/d_notification_page.dart @@ -4,12 +4,13 @@ import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/view_models/notification_view_model.dart'; import 'package:tm_app/views/notification/strings/notification_strings.dart'; -import 'package:tm_app/views/shared/page_selection_dialog.dart'; import 'package:tm_app/views/shared/desktop_navigation_widget.dart'; +import 'package:tm_app/views/shared/page_selection_dialog.dart'; + +import '../../shared/main_tab_bar.dart'; import '../widgets/notification_all_tab.dart'; import '../widgets/notification_important_tab.dart'; import '../widgets/notification_unread_tab.dart'; -import '../../shared/main_tab_bar.dart'; class DesktopNotificationPage extends StatefulWidget { const DesktopNotificationPage({super.key}); @@ -65,7 +66,9 @@ class _DesktopNotificationPageState extends State child: Align( alignment: Alignment.centerRight, child: InkWell( - onTap: viewModel.markAllAsRead, + onTap: () { + viewModel.markAllAsRead(context); + }, borderRadius: BorderRadius.circular(99), child: Container( width: 132.0.w(), @@ -116,8 +119,10 @@ class _DesktopNotificationPageState extends State onTap: () async { final selectedPage = await showDialog( context: context, - builder: (context) => PageSelectionDialog( - totalPages: viewModel.totalPages), + builder: + (context) => PageSelectionDialog( + totalPages: viewModel.totalPages, + ), ); if (selectedPage != null) { diff --git a/lib/views/notification/pages/m_notification_page.dart b/lib/views/notification/pages/m_notification_page.dart index cb8f40b..2256a26 100644 --- a/lib/views/notification/pages/m_notification_page.dart +++ b/lib/views/notification/pages/m_notification_page.dart @@ -36,7 +36,7 @@ class _MobileNotificationPageState extends State viewModel.hasMoreData) { viewModel.getNotifications( page: viewModel.currentPage + 1, - isMobile: true, + isMobile: true, ); } }); @@ -51,7 +51,7 @@ class _MobileNotificationPageState extends State @override Widget build(BuildContext context) { - final viewModel = context.watch(); // ✅ وصل به ویومدل + final viewModel = context.watch(); return Scaffold( backgroundColor: AppColors.backgroundColor, @@ -72,7 +72,9 @@ class _MobileNotificationPageState extends State child: Align( alignment: Alignment.centerRight, child: InkWell( - onTap: viewModel.markAllAsRead, // ✅ مثل دسکتاپ/تبلت + onTap: () { + viewModel.markAllAsRead(context); + }, borderRadius: BorderRadius.circular(99), child: Container( width: 132.0.w(), diff --git a/lib/views/notification/pages/t_notification_page.dart b/lib/views/notification/pages/t_notification_page.dart index e2ba926..dada67b 100644 --- a/lib/views/notification/pages/t_notification_page.dart +++ b/lib/views/notification/pages/t_notification_page.dart @@ -70,7 +70,9 @@ class _TabletNotificationPageState extends State child: Align( alignment: Alignment.centerRight, child: InkWell( - onTap: viewModel.markAllAsRead, + onTap: () { + viewModel.markAllAsRead(context); + }, borderRadius: BorderRadius.circular(99), child: Container( width: 132.0.w(), @@ -121,9 +123,10 @@ class _TabletNotificationPageState extends State onTap: () async { final selectedPage = await showDialog( context: context, - builder: (context) => PageSelectionDialog( - totalPages: viewModel.totalPages, - ), + builder: + (context) => PageSelectionDialog( + totalPages: viewModel.totalPages, + ), ); if (selectedPage != null) { From 1f60954c53be832823d050990183b96886e6058a Mon Sep 17 00:00:00 2001 From: llsajjad Date: Tue, 23 Sep 2025 15:49:30 +0330 Subject: [PATCH 2/3] Added unread list api in notification --- .../repositories/notification_repository.dart | 20 +++++++---- lib/data/services/api/notification_api.dart | 5 +-- lib/data/services/notification_service.dart | 34 ++++++++++++------ lib/view_models/notification_view_model.dart | 36 ++++++++++++++++++- .../widgets/notification_unread_tab.dart | 13 ++++++- 5 files changed, 86 insertions(+), 22 deletions(-) diff --git a/lib/data/repositories/notification_repository.dart b/lib/data/repositories/notification_repository.dart index e80f732..fc0075c 100644 --- a/lib/data/repositories/notification_repository.dart +++ b/lib/data/repositories/notification_repository.dart @@ -4,7 +4,7 @@ import 'package:tm_app/data/services/notification_service.dart'; class NotificationsRepository { NotificationsRepository({required NotificationsService notificationsService}) - : _notificationsService = notificationsService; + : _notificationsService = notificationsService; final NotificationsService _notificationsService; @@ -12,14 +12,20 @@ class NotificationsRepository { required int limit, required int offset, }) async { - return _notificationsService.getNotifications( + return _notificationsService.getNotifications(limit: limit, offset: offset); + } + + Future>> markAllAsRead() async { + return _notificationsService.markAllAsRead(); + } + + Future> getUnreadNotifications({ + required int limit, + required int offset, + }) { + return _notificationsService.getUnreadNotifications( limit: limit, offset: offset, ); } - -Future>> markAllAsRead() async { - return _notificationsService.markAllAsRead(); -} - } diff --git a/lib/data/services/api/notification_api.dart b/lib/data/services/api/notification_api.dart index e0b988a..a2d0af3 100644 --- a/lib/data/services/api/notification_api.dart +++ b/lib/data/services/api/notification_api.dart @@ -10,7 +10,8 @@ class NotificationApi { static const String markAllAsRead = '/api/v1/notifications/mark'; static const String markAsRead = '/api/v1/notifications/mark-as-read'; static const String tenderApprovalsTender = '/api/v1/notifications'; - static String getTenderApprovalById({required String tenderId}) { - return '$tenderApprovalsTender/$tenderId'; + static const String unreadNotifications = '/api/v1/notifications?seen=false'; + static String getUnreadNotifications({required int limit, required int offset}) { + return '$unreadNotifications?limit=$limit&offset=$offset'; } } diff --git a/lib/data/services/notification_service.dart b/lib/data/services/notification_service.dart index 5d8e809..90f415a 100644 --- a/lib/data/services/notification_service.dart +++ b/lib/data/services/notification_service.dart @@ -1,14 +1,15 @@ // notification_service.dart import 'dart:convert'; + +import 'package:tm_app/core/utils/result.dart'; import 'package:tm_app/data/services/api/notification_api.dart'; import 'package:tm_app/data/services/model/notification__response/notification_response_model.dart'; -import 'package:tm_app/core/utils/result.dart'; import '../../core/network/network_manager.dart'; class NotificationsService { NotificationsService({required NetworkManager networkManager}) - : _networkManager = networkManager; + : _networkManager = networkManager; final NetworkManager _networkManager; Future> getNotifications({ @@ -24,15 +25,14 @@ class NotificationsService { return result; } -Future>> markAllAsRead() async { - final result = await _networkManager.makeRequest( - NotificationApi.markAllAsRead, - method: 'GET', - (json) => json, - ); - return result; -} - + Future>> markAllAsRead() async { + final result = await _networkManager.makeRequest( + NotificationApi.markAllAsRead, + method: 'GET', + (json) => json, + ); + return result; + } Future> markAsRead({ required String notificationId, @@ -46,4 +46,16 @@ Future>> markAllAsRead() async { ); return result; } + + Future> getUnreadNotifications({ + required int limit, + required int offset, + }) async { + final result = await _networkManager.makeRequest( + NotificationApi.unreadNotifications, + method: 'GET', + (json) => NotificationResponseModel.fromJson(json), + ); + return result; + } } diff --git a/lib/view_models/notification_view_model.dart b/lib/view_models/notification_view_model.dart index 47a647f..21bf51b 100644 --- a/lib/view_models/notification_view_model.dart +++ b/lib/view_models/notification_view_model.dart @@ -11,6 +11,7 @@ class NotificationViewModel with ChangeNotifier { : _repository = repositoryViewModel { WidgetsBinding.instance.addPostFrameCallback((_) { getNotifications(); + getUnreadNotifications(); }); } @@ -30,6 +31,13 @@ class NotificationViewModel with ChangeNotifier { bool get hasMoreData => _hasMoreData; int get totalPages => _totalPages; + NotificationResponseModel? _unreadNotificationResponse; + bool _isUnreadLoading = false; + + NotificationResponseModel? get unreadNotificationResponse => + _unreadNotificationResponse; + bool get isUnreadLoading => _isUnreadLoading; + /// Fetch notifications Future getNotifications({int page = 1, bool isMobile = false}) async { _isLoading = true; @@ -110,7 +118,9 @@ class NotificationViewModel with ChangeNotifier { ); } - await getNotifications(page: 1); + await getNotifications(page: currentPage, ); + await getUnreadNotifications(); + // toast success AppToast.success(context, 'All notifications marked as read.'); @@ -147,4 +157,28 @@ class NotificationViewModel with ChangeNotifier { return '${difference.inDays} days ago'; } } + + Future getUnreadNotifications({int page = 1}) async { + _isUnreadLoading = true; + notifyListeners(); + + final int offset = _pageSize * page; + + final result = await _repository.getUnreadNotifications( + limit: _pageSize, + offset: offset, + ); + + switch (result) { + case Ok(): + _unreadNotificationResponse = result.value; + break; + case Error(): + _errorMessage = result.error.toString(); + break; + } + + _isUnreadLoading = false; + notifyListeners(); + } } diff --git a/lib/views/notification/widgets/notification_unread_tab.dart b/lib/views/notification/widgets/notification_unread_tab.dart index b5275d3..efb980e 100644 --- a/lib/views/notification/widgets/notification_unread_tab.dart +++ b/lib/views/notification/widgets/notification_unread_tab.dart @@ -12,9 +12,20 @@ class NotificationUnreadTab extends StatelessWidget { @override Widget build(BuildContext context) { final viewModel = context.watch(); + + if (viewModel.isUnreadLoading) { + return const Center(child: CircularProgressIndicator()); + } + final notifications = - viewModel.notificationResponse?.data?.notifications ?? []; + viewModel.unreadNotificationResponse?.data?.notifications ?? []; + + if (notifications.isEmpty) { + return const Center(child: Text('No unread notifications')); + } + return ListView.builder( + controller: scrollController, itemCount: notifications.length, itemBuilder: (context, index) { final notification = notifications[index]; From 1901b84b2d194a81196afeec295fee5979909f94 Mon Sep 17 00:00:00 2001 From: llsajjad Date: Tue, 23 Sep 2025 16:07:44 +0330 Subject: [PATCH 3/3] Fixed CircularProgressIndicator color and position show in notification --- lib/data/services/notification_service.dart | 2 +- .../pages/d_notification_page.dart | 29 +++++++++++------ .../pages/m_notification_page.dart | 32 +++++++++++-------- .../pages/t_notification_page.dart | 29 +++++++++++------ 4 files changed, 60 insertions(+), 32 deletions(-) diff --git a/lib/data/services/notification_service.dart b/lib/data/services/notification_service.dart index 90f415a..45e0b25 100644 --- a/lib/data/services/notification_service.dart +++ b/lib/data/services/notification_service.dart @@ -43,7 +43,7 @@ class NotificationsService { method: 'POST', (json) => NotificationResponseModel.fromJson(json), data: data, - ); + ); return result; } diff --git a/lib/views/notification/pages/d_notification_page.dart b/lib/views/notification/pages/d_notification_page.dart index e1e6cf5..5e3417e 100644 --- a/lib/views/notification/pages/d_notification_page.dart +++ b/lib/views/notification/pages/d_notification_page.dart @@ -92,16 +92,27 @@ class _DesktopNotificationPageState extends State ), ), SizedBox(height: 12.0.h()), - Expanded( - child: TabBarView( - controller: controller, - children: const [ - NotificationAllTab(), - NotificationUnreadTab(), - NotificationImportantTab(), - ], + viewModel.isLoading + ? Container() + : Expanded( + child: TabBarView( + controller: controller, + children: const [ + NotificationAllTab(), + NotificationUnreadTab(), + NotificationImportantTab(), + ], + ), + ), + if (viewModel.isLoading) ...[ + const Spacer(), + const Center( + child: CircularProgressIndicator( + color: AppColors.secondary50, + ), ), - ), + const Spacer(), + ], SizedBox(height: 10.0.h()), Row( children: [ diff --git a/lib/views/notification/pages/m_notification_page.dart b/lib/views/notification/pages/m_notification_page.dart index 2256a26..2d0a879 100644 --- a/lib/views/notification/pages/m_notification_page.dart +++ b/lib/views/notification/pages/m_notification_page.dart @@ -98,20 +98,26 @@ class _MobileNotificationPageState extends State ), ), SizedBox(height: 12.0.h()), - Expanded( - child: TabBarView( - controller: controller, - children: [ - NotificationAllTab(scrollController: _scrollController), - NotificationUnreadTab(scrollController: _scrollController), - NotificationImportantTab(scrollController: _scrollController), - ], - ), - ), + viewModel.isLoading + ? Container() + : Expanded( + child: TabBarView( + controller: controller, + children: [ + NotificationAllTab(scrollController: _scrollController), + NotificationUnreadTab(scrollController: _scrollController), + NotificationImportantTab( + scrollController: _scrollController, + ), + ], + ), + ), if (viewModel.isLoading) ...[ - SizedBox(height: 10.0.h()), - const Center(child: CircularProgressIndicator()), - SizedBox(height: 10.0.h()), + const Spacer(), + const Center( + child: CircularProgressIndicator(color: AppColors.secondary50), + ), + const Spacer(), ], ], ), diff --git a/lib/views/notification/pages/t_notification_page.dart b/lib/views/notification/pages/t_notification_page.dart index dada67b..ce73d68 100644 --- a/lib/views/notification/pages/t_notification_page.dart +++ b/lib/views/notification/pages/t_notification_page.dart @@ -96,16 +96,27 @@ class _TabletNotificationPageState extends State ), ), SizedBox(height: 12.0.h()), - Expanded( - child: TabBarView( - controller: controller, - children: const [ - NotificationAllTab(), - NotificationUnreadTab(), - NotificationImportantTab(), - ], + viewModel.isLoading + ? Container() + : Expanded( + child: TabBarView( + controller: controller, + children: const [ + NotificationAllTab(), + NotificationUnreadTab(), + NotificationImportantTab(), + ], + ), + ), + if (viewModel.isLoading) ...[ + const Spacer(), + const Center( + child: CircularProgressIndicator( + color: AppColors.secondary50, + ), ), - ), + const Spacer(), + ], SizedBox(height: 10.0.h()), Row( children: [