diff --git a/lib/data/repositories/notification_repository.dart b/lib/data/repositories/notification_repository.dart index 78d94bc..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,13 +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 d5a9273..a2d0af3 100644 --- a/lib/data/services/api/notification_api.dart +++ b/lib/data/services/api/notification_api.dart @@ -7,10 +7,11 @@ 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}) { - 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 8c15908..45e0b25 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,11 +25,11 @@ class NotificationsService { return result; } - Future> markAllAsRead() async { + Future>> markAllAsRead() async { final result = await _networkManager.makeRequest( NotificationApi.markAllAsRead, - method: 'POST', - (json) => NotificationResponseModel.fromJson(json), + method: 'GET', + (json) => json, ); return result; } @@ -42,6 +43,18 @@ class NotificationsService { method: 'POST', (json) => NotificationResponseModel.fromJson(json), data: data, + ); + 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 a5d84d0..21bf51b 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'; @@ -10,6 +11,7 @@ class NotificationViewModel with ChangeNotifier { : _repository = repositoryViewModel { WidgetsBinding.instance.addPostFrameCallback((_) { getNotifications(); + getUnreadNotifications(); }); } @@ -29,50 +31,57 @@ 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; - 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 +91,52 @@ 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: currentPage, ); + await getUnreadNotifications(); + + + // 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(); } @@ -129,4 +157,28 @@ Future getNotifications({int page = 1, bool isMobile = false}) async { 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/pages/d_notification_page.dart b/lib/views/notification/pages/d_notification_page.dart index 06df345..5e3417e 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(), @@ -89,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: [ @@ -116,8 +130,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..2d0a879 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(), @@ -96,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 e2ba926..ce73d68 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(), @@ -94,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: [ @@ -121,9 +134,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) { 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];