From 9ab4ed69641153f26458195980900f428d8185a3 Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:27:25 +0300 Subject: [PATCH 01/35] notifications apis integrated --- assets/images/Icon_mark_read.svg | 5 + assets/images/notification_setting.svg | 4 + lib/controllers/api_routes/api_manager.dart | 40 ++ lib/controllers/api_routes/urls.dart | 26 +- .../providers/api/notifications_provider.dart | 96 ++++- lib/l10n/app_ar.arb | 14 +- lib/l10n/app_en.arb | 26 +- lib/l10n/app_localizations.dart | 72 ++++ lib/l10n/app_localizations_ar.dart | 37 ++ lib/l10n/app_localizations_en.dart | 37 ++ lib/main.dart | 14 + lib/models/module_with_submodules_model.dart | 66 ++++ lib/models/notification_filter_model.dart | 67 ++++ lib/models/system_notification_model.dart | 55 ++- .../models/notification_channel_model.dart | 28 ++ .../models/notification_preference_model.dart | 167 ++++++++ .../notification_setting_provider.dart | 236 +++++++++++ .../view/notification_setting_page.dart | 138 +++++++ .../widgets/notification_preference_card.dart | 186 +++++++++ lib/new_views/app_style/app_color.dart | 8 + lib/new_views/common_widgets/app_drawer.dart | 4 + .../common_widgets/app_text_form_field.dart | 2 +- .../modules_with_submodules_provider.dart | 37 ++ .../notification_channel_lookup_provider.dart | 36 ++ ...notification_priority_lookup_provider.dart | 36 ++ ...otification_recipient_lookup_provider.dart | 36 ++ .../notification_filter_page.dart | 340 ++++++++++++++++ .../notifications/notifications_list.dart | 46 ++- .../notifications/notifications_page.dart | 368 ++++++++++++++++-- .../widgets/notification_card_redesigned.dart | 131 +++++++ .../widgets/notification_tab_button.dart | 55 +++ 31 files changed, 2337 insertions(+), 76 deletions(-) create mode 100644 assets/images/Icon_mark_read.svg create mode 100644 assets/images/notification_setting.svg create mode 100644 lib/models/module_with_submodules_model.dart create mode 100644 lib/models/notification_filter_model.dart create mode 100644 lib/modules/notification_setting/models/notification_channel_model.dart create mode 100644 lib/modules/notification_setting/models/notification_preference_model.dart create mode 100644 lib/modules/notification_setting/provider/notification_setting_provider.dart create mode 100644 lib/modules/notification_setting/view/notification_setting_page.dart create mode 100644 lib/modules/notification_setting/view/widgets/notification_preference_card.dart create mode 100644 lib/providers/lookups/modules_with_submodules_provider.dart create mode 100644 lib/providers/lookups/notification_channel_lookup_provider.dart create mode 100644 lib/providers/lookups/notification_priority_lookup_provider.dart create mode 100644 lib/providers/lookups/notification_recipient_lookup_provider.dart create mode 100644 lib/views/pages/user/notifications/notification_filter_page.dart create mode 100644 lib/views/pages/user/notifications/widgets/notification_card_redesigned.dart create mode 100644 lib/views/pages/user/notifications/widgets/notification_tab_button.dart diff --git a/assets/images/Icon_mark_read.svg b/assets/images/Icon_mark_read.svg new file mode 100644 index 00000000..fd7ed9bd --- /dev/null +++ b/assets/images/Icon_mark_read.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/notification_setting.svg b/assets/images/notification_setting.svg new file mode 100644 index 00000000..d007a3db --- /dev/null +++ b/assets/images/notification_setting.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/controllers/api_routes/api_manager.dart b/lib/controllers/api_routes/api_manager.dart index 722e894e..ea8fcf06 100644 --- a/lib/controllers/api_routes/api_manager.dart +++ b/lib/controllers/api_routes/api_manager.dart @@ -336,6 +336,46 @@ class ApiManager { return response; } + /// POST request with List/Array body (for endpoints that expect JSON arrays) + Future postList(String url, {Map? headers, required List body, bool showToast = true}) async { + headers ??= {}; + + headers.addAll(_headers); + + Uri url0 = Uri.parse(url); + if (!kReleaseMode) { + print("URL:$url0"); + print("Headers:$headers"); + print("Body:$body"); + } + + // OPTIMIZATION: Use persistent HTTP client for connection pooling + var request = http.Request('POST', url0); + request.body = json.encode(body); + request.headers.addAll(headers); + + http.StreamedResponse streamedResponse = await _httpClient.send(request); + http.Response response = await http.Response.fromStream(streamedResponse); + + // OPTIMIZATION: Parse JSON once and reuse (was parsing twice before) + try { + if (response.statusCode == 401) { + showLoginDialog(); + } else { + // Parse response body only once + final responseBody = jsonDecode(response.body); + if (responseBody is Map) { + final message = responseBody["message"]; // Reuse parsed body + if (message != null && message.toString().isNotEmpty && showToast) { + Fluttertoast.showToast(msg: message, toastLength: Toast.LENGTH_LONG); + } + } + } + } catch (ex) {} + + return response; + } + Future multiPart(String url, {Map? headers, required Map body, required List> files}) async { Map headers0 = const { 'Content-Type': 'multipart/form-data', diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart index 36531408..77f8ad95 100644 --- a/lib/controllers/api_routes/urls.dart +++ b/lib/controllers/api_routes/urls.dart @@ -4,12 +4,12 @@ class URLs { static const String appReleaseBuildNumber = "42"; // static const host1 = "https://atomsm.hmg.com"; // production url - // static const host1 = "https://atomsmdev.hmg.com"; // local DEV url - static const host1 = "https://atomsmuat.hmg.com"; // local UAT url + static const host1 = "https://atomsmdev.hmg.com"; // local DEV url + // static const host1 = "https://atomsmuat.hmg.com"; // local UAT url - static final String _baseUrl = "$_host/mobile"; // host local UAT + // static final String _baseUrl = "$_host/mobile"; // host local UAT // static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis - // static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM + static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM // static final String _baseUrl = "$_host/v4/mobile"; // v4 for Demo module // static final String _baseUrl = "$_host/v5/mobile"; // v5 for data segregation // static final String _baseUrl = "$_host/v6/mobile"; // for asset delivery module @@ -236,7 +236,19 @@ class URLs { static get addSupplierEngineer => "$_baseUrl/Supplier/AddSupplierEngineer"; - static get getSystemNotifications => "$_baseUrl/SystemNotification/GetSystemNotifications"; // get + // static get getSystemNotifications => "$_baseUrl/SystemNotification/GetSystemNotifications"; // get (OLD - COMMENTED OUT) + static get getNotificationInbox => "$_baseUrl/NotificationNew/Inbox"; // get notifications with filters + static get markAllNotificationsAsRead => "$_baseUrl/NotificationNew/MarkAllRead"; // mark all as read + static get markOneNotificationAsRead => "$_baseUrl/NotificationNew/MarkAsReadOneNotification"; // mark one notification as read + + // Notification Filter Lookups + static get getNotificationPriorityLookup => "$_baseUrl/Lookups/GetLookup?lookupEnum=5022"; // notification priority filter + static get getNotificationChannelLookup => "$_baseUrl/Lookups/GetLookup?lookupEnum=5024"; // notification channel filter + static get getNotificationRecipientLookup => "$_baseUrl/Lookups/GetLookup?lookupEnum=5021"; // notification recipient filter + + // Modules and SubModules + static get getModulesWithSubModules => "$_baseUrl/Module/GetModulesWithSubModules"; // get modules with submodules for filters + static get getRecentNotifications => "$_baseUrl/return/user/recent/notification"; // get static get createRequest => "$_baseUrl/CallRequest/AddCallRequest"; // get static get CheckIfAssetHasAnotherServiceRequest => "$_baseUrl/ServiceRequest/CheckForPendingRequestForAsset"; // get @@ -565,5 +577,9 @@ class URLs { // Print/Report URLs static get generateReport => "$_baseUrl/ReportPDF/GenerateReport"; + // Notification Preference Center + static String get getNotificationPreferences => "$_baseUrl/NotificationPreferenceCenter/GetTemplateApplyOnCurrentUser"; + static String get saveNotificationPreferences => "$_baseUrl/NotificationPreferenceCenter/SaveNotificationPreferenceCenter"; + //4104 attachment type lookup } diff --git a/lib/controllers/providers/api/notifications_provider.dart b/lib/controllers/providers/api/notifications_provider.dart index 53d97eaa..49422655 100644 --- a/lib/controllers/providers/api/notifications_provider.dart +++ b/lib/controllers/providers/api/notifications_provider.dart @@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart'; import 'package:http/http.dart'; import 'package:test_sa/controllers/api_routes/api_manager.dart'; import 'package:test_sa/controllers/api_routes/urls.dart'; +import 'package:test_sa/models/notification_filter_model.dart'; import 'package:test_sa/models/system_notification_model.dart'; import 'package:test_sa/models/user.dart'; @@ -11,6 +12,9 @@ class NotificationsProvider extends ChangeNotifier { // number of items call in each request final pageItemNumber = 12; + // Store active filters + NotificationFilterModel? activeFilters; + //reset provider data void reset() { notifications = []; @@ -38,25 +42,49 @@ class NotificationsProvider extends ChangeNotifier { /// return state code if request complete may be 200, 404 or 403 /// for more details check http state manager /// lib\controllers\http_status_manger\http_status_manger.dart - Future getSystemNotifications({required User user, bool resetProvider = false}) async { + Future getSystemNotifications({ + required User user, + bool resetProvider = false, + NotificationFilterModel? filters, + }) async { if (isLoading == true) return -2; if (resetProvider) { reset(); } + + // Store filters for pagination + if (filters != null) { + activeFilters = filters; + } + isLoading = true; notifyListeners(); late Response response; try { - final Map body = {"pageSize": pageItemNumber, "pageNumber": notifications.length ~/ pageItemNumber + 1, "userId": user.userID}; - response = await ApiManager.instance.post(URLs.getSystemNotifications, body: body); + // Base request body with pagination + final Map body = { + "pageSize": pageItemNumber, + "pageNumber": notifications.length ~/ pageItemNumber + 1, + }; + + // Add filter parameters only if they have values + if (activeFilters != null) { + body.addAll(activeFilters!.toApiBody()); + } + // Don't add null values if no filters are active - just send pagination params only + + response = await ApiManager.instance.post(URLs.getNotificationInbox, body: body); + + // OLD API - Commented out but kept for reference + // final Map body = {"pageSize": pageItemNumber, "pageNumber": notifications.length ~/ pageItemNumber + 1, "userId": user.userID}; + // response = await ApiManager.instance.post(URLs.getSystemNotifications, body: body); stateCode = response.statusCode; if (response.statusCode >= 200 && response.statusCode < 300) { // client's request was successfully received List requestsListJson = json.decode(response.body)["data"]; List serviceRequestsPage = requestsListJson.map((request) => SystemNotificationModel.fromJson(request)).toList(); - notifications ??= []; notifications.addAll(serviceRequestsPage); // Update nextPage based on response length nextPage = serviceRequestsPage.length == pageItemNumber; @@ -73,6 +101,62 @@ class NotificationsProvider extends ChangeNotifier { } } + /// Mark all notifications as read + /// Returns a map with success status and message from API + Future> markAllAsRead({required String userId}) async { + try { + final Map body = {"userId": userId}; + final response = await ApiManager.instance.post(URLs.markAllNotificationsAsRead, body: body); + + if (response.statusCode >= 200 && response.statusCode < 300) { + // Update all notifications in the local list to read status + for (var notification in notifications) { + notification.readed = true; + } + notifyListeners(); + + // Parse response to get message + final responseData = json.decode(response.body); + final message = responseData['message'] ?? responseData['Message']; + return {'success': true, 'message': message}; + } else { + // Parse error message from response + try { + final responseData = json.decode(response.body); + final message = responseData['message'] ?? responseData['Message']; + return {'success': false, 'message': message}; + } catch (e) { + return {'success': false, 'message': null}; + } + } + } catch (error) { + print('Error marking all as read: $error'); + return {'success': false, 'message': null}; + } + } + + /// Mark one notification as read + /// Returns true if successful, false otherwise + Future markOneAsRead({required String notificationId}) async { + try { + final response = await ApiManager.instance.get("${URLs.markOneNotificationAsRead}?NotificationId=$notificationId"); + + if (response.statusCode >= 200 && response.statusCode < 300) { + // Update the specific notification in the local list to read status + final index = notifications.indexWhere((n) => n.id == notificationId); + if (index != -1) { + notifications[index].readed = true; + notifyListeners(); + } + return true; + } + return false; + } catch (error) { + print('Error marking notification as read: $error'); + return false; + } + } + /// return -2 if request in progress /// return -1 if error happen when sending request /// return state code if request complete may be 200, 404 or 403 @@ -86,7 +170,7 @@ class NotificationsProvider extends ChangeNotifier { //userId = 397.toString(); // testing id to view data try { - response = await get(Uri.parse("${host + URLs.getSystemNotifications}?uid=${user.id}&token=${user.token}"), headers: {"Content-Type": "application/json; charset=utf-8"}); + response = await get(Uri.parse("${host + URLs.getNotificationInbox}?uid=${user.id}&token=${user.token}"), headers: {"Content-Type": "application/json; charset=utf-8"}); stateCode = response.statusCode; if (response.statusCode >= 200 && response.statusCode < 300) { @@ -100,4 +184,4 @@ class NotificationsProvider extends ChangeNotifier { return null; } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 22934e7c..56efa294 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -556,5 +556,17 @@ "contactInfo": "معلومات الاتصال", "destinationBuilding": "مبنى الوجهة", "destinationFloor": "طابق الوجهة", - "destinationDepartment": "قسم الوجهة" + "destinationDepartment": "قسم الوجهة", + "notificationPreferenceCenter": "مركز تفضيلات الإشعارات", + "notificationModule": "وحدة الإشعار", + "notificationPriority": "أولوية الإشعار", + "channelPreference": "تفضيل القناة", + "enable": "تفعيل", + "disable": "تعطيل", + "noNotificationPreferences": "لا توجد تفضيلات إشعارات", + "noNotificationPreferencesDescription": "لا توجد تفضيلات إشعارات متاحة في الوقت الحالي.", + "preferencesSavedSuccessfully": "تم حفظ التفضيلات بنجاح", + "failedToSavePreferences": "فشل حفظ التفضيلات", + "error": "خطأ", + "retry": "إعادة المحاولة" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2e111a56..b3135a1e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -544,17 +544,29 @@ "createAssetTransferRequest": "Create Asset Transfer Request", "arrivedDate": "Arrived Date", "closedDate": "Closed Date", - "transferType": "Transfer Type", + "transferType": "Transfer Type", "since": "Since", "rejectionDetails": "Rejection Details", "part": "Part", "creationDate": "Creation Date", "createdBy": "Created BY", "trNumber": "TR Number", - "workOrderDetails": "Work Order Details", - "woNumber": "WO Number", - "contactInfo": "Contact Info", - "destinationBuilding": "Destination Building", - "destinationFloor": "Destination Floor", - "destinationDepartment": "Destination Department" + "workOrderDetails": "Work Order Details", + "woNumber": "WO Number", + "contactInfo": "Contact Info", + "destinationBuilding": "Destination Building", + "destinationFloor": "Destination Floor", + "destinationDepartment": "Destination Department", + "notificationPreferenceCenter": "Notification Preference Center", + "notificationModule": "Notification Module", + "notificationPriority": "Notification Priority", + "channelPreference": "Channel Preference", + "enable": "Enable", + "disable": "Disable", + "noNotificationPreferences": "No Notification Preferences", + "noNotificationPreferencesDescription": "There are no notification preferences available at this time.", + "preferencesSavedSuccessfully": "Preferences saved successfully", + "failedToSavePreferences": "Failed to save preferences", + "error": "Error", + "retry": "Retry" } \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 1001219e..4236c361 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3445,6 +3445,78 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Destination Department'** String get destinationDepartment; + + /// No description provided for @notificationPreferenceCenter. + /// + /// In en, this message translates to: + /// **'Notification Preference Center'** + String get notificationPreferenceCenter; + + /// No description provided for @notificationModule. + /// + /// In en, this message translates to: + /// **'Notification Module'** + String get notificationModule; + + /// No description provided for @notificationPriority. + /// + /// In en, this message translates to: + /// **'Notification Priority'** + String get notificationPriority; + + /// No description provided for @channelPreference. + /// + /// In en, this message translates to: + /// **'Channel Preference'** + String get channelPreference; + + /// No description provided for @enable. + /// + /// In en, this message translates to: + /// **'Enable'** + String get enable; + + /// No description provided for @disable. + /// + /// In en, this message translates to: + /// **'Disable'** + String get disable; + + /// No description provided for @noNotificationPreferences. + /// + /// In en, this message translates to: + /// **'No Notification Preferences'** + String get noNotificationPreferences; + + /// No description provided for @noNotificationPreferencesDescription. + /// + /// In en, this message translates to: + /// **'There are no notification preferences available at this time.'** + String get noNotificationPreferencesDescription; + + /// No description provided for @preferencesSavedSuccessfully. + /// + /// In en, this message translates to: + /// **'Preferences saved successfully'** + String get preferencesSavedSuccessfully; + + /// No description provided for @failedToSavePreferences. + /// + /// In en, this message translates to: + /// **'Failed to save preferences'** + String get failedToSavePreferences; + + /// No description provided for @error. + /// + /// In en, this message translates to: + /// **'Error'** + String get error; + + /// No description provided for @retry. + /// + /// In en, this message translates to: + /// **'Retry'** + String get retry; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 9f5681b3..deb87d12 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -1702,4 +1702,41 @@ class AppLocalizationsAr extends AppLocalizations { @override String get destinationDepartment => 'قسم الوجهة'; + + @override + String get notificationPreferenceCenter => 'مركز تفضيلات الإشعارات'; + + @override + String get notificationModule => 'وحدة الإشعار'; + + @override + String get notificationPriority => 'أولوية الإشعار'; + + @override + String get channelPreference => 'تفضيل القناة'; + + @override + String get enable => 'تفعيل'; + + @override + String get disable => 'تعطيل'; + + @override + String get noNotificationPreferences => 'لا توجد تفضيلات إشعارات'; + + @override + String get noNotificationPreferencesDescription => + 'لا توجد تفضيلات إشعارات متاحة في الوقت الحالي.'; + + @override + String get preferencesSavedSuccessfully => 'تم حفظ التفضيلات بنجاح'; + + @override + String get failedToSavePreferences => 'فشل حفظ التفضيلات'; + + @override + String get error => 'خطأ'; + + @override + String get retry => 'إعادة المحاولة'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index aedaaea0..7e6a52d4 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1709,4 +1709,41 @@ class AppLocalizationsEn extends AppLocalizations { @override String get destinationDepartment => 'Destination Department'; + + @override + String get notificationPreferenceCenter => 'Notification Preference Center'; + + @override + String get notificationModule => 'Notification Module'; + + @override + String get notificationPriority => 'Notification Priority'; + + @override + String get channelPreference => 'Channel Preference'; + + @override + String get enable => 'Enable'; + + @override + String get disable => 'Disable'; + + @override + String get noNotificationPreferences => 'No Notification Preferences'; + + @override + String get noNotificationPreferencesDescription => + 'There are no notification preferences available at this time.'; + + @override + String get preferencesSavedSuccessfully => 'Preferences saved successfully'; + + @override + String get failedToSavePreferences => 'Failed to save preferences'; + + @override + String get error => 'Error'; + + @override + String get retry => 'Retry'; } diff --git a/lib/main.dart b/lib/main.dart index 5c154b24..4bef6f92 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -55,6 +55,7 @@ import 'package:test_sa/modules/medical_gas_inspection/create_medical_gas_reques import 'package:test_sa/modules/medical_gas_inspection/providers/item_type_provider.dart'; import 'package:test_sa/modules/medical_gas_inspection/providers/medical_gas_inspection_provider.dart'; import 'package:test_sa/modules/medical_gas_inspection/providers/order_type_provider.dart'; +import 'package:test_sa/modules/notification_setting/provider/notification_setting_provider.dart'; import 'package:test_sa/modules/tm_module/gas_refill/create_gas_refill_request.dart'; import 'package:test_sa/modules/tm_module/gas_refill/update_gas_refill_request.dart'; import 'package:test_sa/modules/traf_module/create_traf_request_page.dart'; @@ -109,6 +110,7 @@ import 'package:test_sa/modules/export_module/provider/export_columns_provider.d import 'package:test_sa/modules/export_module/provider/export_format_provider.dart'; import 'package:test_sa/modules/export_module/provider/export_provider.dart'; import 'package:test_sa/modules/export_module/pages/export_form_view.dart'; +import 'package:test_sa/modules/notification_setting/view/notification_setting_page.dart'; import 'package:test_sa/views/pages/user/notifications/notifications_page.dart'; import 'package:test_sa/views/pages/user/profile_page.dart'; import 'package:test_sa/views/update_available_screen.dart'; @@ -141,6 +143,10 @@ import 'providers/lookups/customer_lookup_provider.dart'; import 'providers/lookups/department_lookup_provider.dart'; import 'providers/lookups/request_type_lookup_provider.dart'; import 'providers/lookups/yes_no_lookup_provider.dart'; +import 'providers/lookups/notification_priority_lookup_provider.dart'; +import 'providers/lookups/notification_channel_lookup_provider.dart'; +import 'providers/lookups/notification_recipient_lookup_provider.dart'; +import 'providers/lookups/modules_with_submodules_provider.dart'; import 'providers/service_request_providers/loan_availability_provider.dart'; import 'providers/service_request_providers/reject_reason_provider.dart'; import 'modules/cleaner_module/pages/cleaner_search_form_view.dart'; @@ -248,6 +254,10 @@ class MyApp extends StatelessWidget { ChangeNotifierProvider(create: (_) => IncidentPersonInvolvedLookupProvider(), lazy: true), ChangeNotifierProvider(create: (_) => ClinicalNonClinicalLookupProvider(), lazy: true), ChangeNotifierProvider(create: (_) => GenderLookupProvider(), lazy: true), + ChangeNotifierProvider(create: (_) => NotificationPriorityLookupProvider(), lazy: true), + ChangeNotifierProvider(create: (_) => NotificationChannelLookupProvider(), lazy: true), + ChangeNotifierProvider(create: (_) => NotificationRecipientLookupProvider(), lazy: true), + ChangeNotifierProvider(create: (_) => ModulesWithSubModulesProvider(), lazy: true), // GAS MODULE PROVIDERS (6) ChangeNotifierProvider(create: (_) => GasTypesProvider(), lazy: true), @@ -354,6 +364,9 @@ class MyApp extends StatelessWidget { // CX MODULE PROVIDERS (1) ChangeNotifierProvider(create: (_) => SurveyProvider(), lazy: true), + // NOTIFICATION MODULE PROVIDERS (1) + ChangeNotifierProvider(create: (_) => NotificationSettingProvider(), lazy: true), + // CLEANER MODULE PROVIDERS (1) ChangeNotifierProvider(create: (_) => CleanerProvider(), lazy: true), @@ -417,6 +430,7 @@ class MyApp extends StatelessWidget { CreateMedicalGasRequestPage.id: (_) => CreateMedicalGasRequestPage(), CreateDemoRequestPage.id: (_) => CreateDemoRequestPage(), CleanerSearchFormView.routeName: (_) => const CleanerSearchFormView(), + NotificationSettingPage.id: (_) => const NotificationSettingPage(), }, ), ), diff --git a/lib/models/module_with_submodules_model.dart b/lib/models/module_with_submodules_model.dart new file mode 100644 index 00000000..1b215c8b --- /dev/null +++ b/lib/models/module_with_submodules_model.dart @@ -0,0 +1,66 @@ +import 'package:test_sa/models/base.dart'; + +class ModuleWithSubModulesModel extends Base { + int? id; + String? name; + List? subModules; + + ModuleWithSubModulesModel({ + this.id, + this.name, + this.subModules, + }); + + ModuleWithSubModulesModel.fromJson(Map json) { + id = json['id']; + name = json['name']; + if (json['subModules'] != null) { + subModules = []; + json['subModules'].forEach((v) { + subModules!.add(SubModuleModel.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['name'] = name; + if (subModules != null) { + data['subModules'] = subModules!.map((v) => v.toJson()).toList(); + } + return data; + } + + @override + String toString() => name ?? ''; +} + +class SubModuleModel extends Base { + int? id; + String? name; + String? mainTableName; + + SubModuleModel({ + this.id, + this.name, + this.mainTableName, + }); + + SubModuleModel.fromJson(Map json) { + id = json['id']; + name = json['name']; + mainTableName = json['mainTableName']; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['name'] = name; + data['mainTableName'] = mainTableName; + return data; + } + + @override + String toString() => name ?? ''; +} diff --git a/lib/models/notification_filter_model.dart b/lib/models/notification_filter_model.dart new file mode 100644 index 00000000..3aad2c88 --- /dev/null +++ b/lib/models/notification_filter_model.dart @@ -0,0 +1,67 @@ +class NotificationFilterModel { + List? priorityIds; + List? channelIds; + List? recipientIds; + List? moduleIds; + List? subModuleIds; + DateTime? startDate; + DateTime? endDate; + String? searchText; + bool? isAscending; + int? isRead; // 0 = All, 1 = Unread, 2 = Read + + NotificationFilterModel({ + this.priorityIds, + this.channelIds, + this.recipientIds, + this.moduleIds, + this.subModuleIds, + this.startDate, + this.endDate, + this.searchText, + this.isAscending, + this.isRead, + }); + + // Check if any filters are active + bool get hasActiveFilters { + return (priorityIds?.isNotEmpty ?? false) || + (channelIds?.isNotEmpty ?? false) || + (recipientIds?.isNotEmpty ?? false) || + (moduleIds?.isNotEmpty ?? false) || + (subModuleIds?.isNotEmpty ?? false) || + startDate != null || + endDate != null; + } + + // Convert to API body (only include non-null/non-empty values) + Map toApiBody() { + final Map body = {}; + + if (priorityIds?.isNotEmpty ?? false) body['priorityIds'] = priorityIds; + if (channelIds?.isNotEmpty ?? false) body['channelIds'] = channelIds; + if (recipientIds?.isNotEmpty ?? false) body['recipientIds'] = recipientIds; + if (moduleIds?.isNotEmpty ?? false) body['moduleId'] = moduleIds!.first; // API expects single moduleId + if (subModuleIds?.isNotEmpty ?? false) body['subModuleIds'] = subModuleIds; + if (startDate != null) body['createdDateFrom'] = startDate!.toIso8601String(); + if (endDate != null) body['createdDateTo'] = endDate!.toIso8601String(); + if (searchText?.isNotEmpty ?? false) body['searchText'] = searchText; + if (isAscending != null) body['isAscending'] = isAscending; + if (isRead != null) body['isRead'] = isRead; + + return body; + } + + void clear() { + priorityIds?.clear(); + channelIds?.clear(); + recipientIds?.clear(); + moduleIds?.clear(); + subModuleIds?.clear(); + startDate = null; + endDate = null; + searchText = null; + isAscending = null; + isRead = null; + } +} diff --git a/lib/models/system_notification_model.dart b/lib/models/system_notification_model.dart index e2cf4b39..041a8780 100644 --- a/lib/models/system_notification_model.dart +++ b/lib/models/system_notification_model.dart @@ -8,12 +8,20 @@ class SystemNotificationModel { String? sourceName; bool? readed; String? readingDate; - int? id; + String? id; // Changed from int? to String? to match API response String? createdOn; String? modifiedOn; String? priorityName; String? statusName; int? transactionType; + String? moduleName; + String? subModuleName; + String? channelName; + + // New fields from the API + bool? sent; + String? sentDate; + int? noOfFailure; SystemNotificationModel( {this.userId, @@ -30,27 +38,42 @@ class SystemNotificationModel { this.modifiedOn, this.priorityName, this.transactionType, - this.statusName}); + this.statusName, + this.moduleName, + this.subModuleName, + this.channelName, + this.sent, + this.sentDate, + this.noOfFailure}); SystemNotificationModel.fromJson(Map? json) { // Allow json to be null if (json != null) { - // Add null check - userId = json['userId']; + // Fix: Handle id as String (API returns String like "6a3b8af78200480d80d8dbb8") + id = json['id']?.toString(); + userId = json['userId'] ?? json['userEmail']; userName = json['userName']; - title = json['title']; - text = json['text']; + title = json['title'] ?? json['notificationTitle']; + text = json['text'] ?? json['notificationBody']; referenceId = json['referenceId']; sourceId = json['sourceId']; - sourceName = json['sourceName']; - readed = json['readed']; + sourceName = json['sourceName'] ?? json['moduleName']; + readed = json['readed'] ?? (json['isRead'] == true); readingDate = json['readingDate']; - id = json['id']; - createdOn = json['createdOn']; + createdOn = json['createdDate'] ?? json['createdOn']; modifiedOn = json['modifiedOn']; priorityName = json['priorityName']; statusName = json['statusName']; transactionType = json['transactionType']; + moduleName = json['moduleName']; + subModuleName = json['subModuleName']; + channelName = json['channelName']; + + // Map new fields + sent = json['sent']; + sentDate = json['sentDate']; + noOfFailure = json['noOfFailure']; + } } @@ -72,6 +95,10 @@ class SystemNotificationModel { data['priority'] = priorityName; data['statusName'] = statusName; data['transactionType'] = transactionType; + data['channelName'] = channelName; + data['sent'] = sent; + data['sentDate'] = sentDate; + data['noOfFailure'] = noOfFailure; return data; } @@ -92,6 +119,12 @@ class SystemNotificationModel { data['priorityName'] = priorityName; data['statusName'] = statusName; data['transactionType'] = transactionType; + data['moduleName'] = moduleName; + data['subModuleName'] = subModuleName; + data['channelName'] = channelName; + data['sent'] = sent; + data['sentDate'] = sentDate; + data['noOfFailure'] = noOfFailure; return data; } -} +} \ No newline at end of file diff --git a/lib/modules/notification_setting/models/notification_channel_model.dart b/lib/modules/notification_setting/models/notification_channel_model.dart new file mode 100644 index 00000000..878108f7 --- /dev/null +++ b/lib/modules/notification_setting/models/notification_channel_model.dart @@ -0,0 +1,28 @@ +class NotificationChannel { + final int? id; + final String? name; + final int? value; + + NotificationChannel({ + this.id, + this.name, + this.value, + }); + + factory NotificationChannel.fromJson(Map json) { + return NotificationChannel( + id: json['id'], + name: json['name'], + value: json['value'], + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'value': value, + }; + } +} + diff --git a/lib/modules/notification_setting/models/notification_preference_model.dart b/lib/modules/notification_setting/models/notification_preference_model.dart new file mode 100644 index 00000000..1f3210ac --- /dev/null +++ b/lib/modules/notification_setting/models/notification_preference_model.dart @@ -0,0 +1,167 @@ +import 'package:test_sa/modules/notification_setting/models/notification_channel_model.dart'; + +class NotificationPreferenceResponse { + final List? data; + final int? totalRows; + final int? count; + final String? message; + final String? title; + final String? innerMessage; + final int? responseCode; + final bool? isSuccess; + + NotificationPreferenceResponse({ + this.data, + this.totalRows, + this.count, + this.message, + this.title, + this.innerMessage, + this.responseCode, + this.isSuccess, + }); + + factory NotificationPreferenceResponse.fromJson(Map json) { + return NotificationPreferenceResponse( + data: json['data'] != null + ? (json['data'] as List).map((e) => NotificationTemplate.fromJson(e)).toList() + : null, + totalRows: json['totalRows'], + count: json['count'], + message: json['message'], + title: json['title'], + innerMessage: json['innerMessage'], + responseCode: json['responseCode'], + isSuccess: json['isSuccess'], + ); + } + + Map toJson() { + return { + 'data': data?.map((e) => e.toJson()).toList(), + 'totalRows': totalRows, + 'count': count, + 'message': message, + 'title': title, + 'innerMessage': innerMessage, + 'responseCode': responseCode, + 'isSuccess': isSuccess, + }; + } +} + +class NotificationTemplate { + final int? notificationTemplateId; + final String? templateName; + final String? moduleName; + final String? subModuleName; + final bool? mandatory; + final bool? isDisabled; + final String? channelNames; + final bool? isCustom; + final List? templateChannels; + final List? preferenceChannels; + + // Local state for UI tracking + bool isModified; + List selectedChannelIds; + + NotificationTemplate({ + this.notificationTemplateId, + this.templateName, + this.moduleName, + this.subModuleName, + this.mandatory, + this.isDisabled, + this.channelNames, + this.isCustom, + this.templateChannels, + this.preferenceChannels, + this.isModified = false, + List? selectedChannelIds, + }) : selectedChannelIds = selectedChannelIds ?? []; + + factory NotificationTemplate.fromJson(Map json) { + final List? templateChannelsList = json['templateChannels'] != null + ? (json['templateChannels'] as List).map((e) => NotificationChannel.fromJson(e)).toList() + : null; + + final List? preferenceChannelsList = json['preferenceChannels'] != null + ? (json['preferenceChannels'] as List).map((e) => NotificationChannel.fromJson(e)).toList() + : null; + + // Initialize selectedChannelIds from preferenceChannels if available, otherwise from templateChannels + List initialSelectedIds = []; + if (preferenceChannelsList != null && preferenceChannelsList.isNotEmpty) { + initialSelectedIds = preferenceChannelsList + .where((e) => e.id != null) + .map((e) => e.id!) + .toList(); + } else if (templateChannelsList != null && templateChannelsList.isNotEmpty) { + initialSelectedIds = templateChannelsList + .where((e) => e.id != null) + .map((e) => e.id!) + .toList(); + } + + return NotificationTemplate( + notificationTemplateId: json['notificationTemplateId'], + templateName: json['templateName'], + moduleName: json['moduleName'], + subModuleName: json['subModuleName'], + mandatory: json['mandatory'], + isDisabled: json['isDisabled'], + channelNames: json['channelNames'], + isCustom: json['isCustom'], + templateChannels: templateChannelsList, + preferenceChannels: preferenceChannelsList, + selectedChannelIds: initialSelectedIds, + ); + } + + Map toJson() { + return { + 'notificationTemplateId': notificationTemplateId, + 'templateName': templateName, + 'moduleName': moduleName, + 'subModuleName': subModuleName, + 'mandatory': mandatory, + 'isDisabled': isDisabled, + 'channelNames': channelNames, + 'isCustom': isCustom, + 'templateChannels': templateChannels?.map((e) => e.toJson()).toList(), + 'preferenceChannels': preferenceChannels?.map((e) => e.toJson()).toList(), + }; + } + + // Create a copy with modified values + NotificationTemplate copyWith({ + int? notificationTemplateId, + String? templateName, + String? moduleName, + String? subModuleName, + bool? mandatory, + bool? isDisabled, + String? channelNames, + bool? isCustom, + List? templateChannels, + List? preferenceChannels, + bool? isModified, + List? selectedChannelIds, + }) { + return NotificationTemplate( + notificationTemplateId: notificationTemplateId ?? this.notificationTemplateId, + templateName: templateName ?? this.templateName, + moduleName: moduleName ?? this.moduleName, + subModuleName: subModuleName ?? this.subModuleName, + mandatory: mandatory ?? this.mandatory, + isDisabled: isDisabled ?? this.isDisabled, + channelNames: channelNames ?? this.channelNames, + isCustom: isCustom ?? this.isCustom, + templateChannels: templateChannels ?? this.templateChannels, + preferenceChannels: preferenceChannels ?? this.preferenceChannels, + isModified: isModified ?? this.isModified, + selectedChannelIds: selectedChannelIds ?? this.selectedChannelIds, + ); + } +} diff --git a/lib/modules/notification_setting/provider/notification_setting_provider.dart b/lib/modules/notification_setting/provider/notification_setting_provider.dart new file mode 100644 index 00000000..823427b8 --- /dev/null +++ b/lib/modules/notification_setting/provider/notification_setting_provider.dart @@ -0,0 +1,236 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:test_sa/controllers/api_routes/api_manager.dart'; +import 'package:test_sa/controllers/api_routes/urls.dart'; +import 'package:test_sa/modules/notification_setting/models/notification_preference_model.dart'; + +class NotificationSettingProvider extends ChangeNotifier { + // State management + bool _isLoading = false; + bool _isLoadingMore = false; + bool _isSaving = false; + String? _errorMessage; + List _templates = []; + + // Pagination + int _currentPage = 1; + final int _pageSize = 10; + int _totalRecords = 0; + bool _hasMoreData = true; + + // Getters + bool get isLoading => _isLoading; + bool get isLoadingMore => _isLoadingMore; + bool get isSaving => _isSaving; + String? get errorMessage => _errorMessage; + List get templates => _templates; + bool get hasMoreData => _hasMoreData; + bool get isEmpty => _templates.isEmpty && !_isLoading; + + // Check if there are any modified templates + bool get hasChanges => _templates.any((template) => template.isModified); + + /// Load initial notification preferences + Future loadNotificationPreferences({bool refresh = false}) async { + if (refresh) { + reset(); + } + + if (_isLoading || _isLoadingMore) return; + + _isLoading = _currentPage == 1; + _errorMessage = null; + notifyListeners(); + + try { + final body = { + 'pageNumber': _currentPage, + 'pageSize': _pageSize, + }; + + http.Response response = await ApiManager.instance.post( + URLs.getNotificationPreferences, + body: body, + showToast: false, + ); + + if (response.statusCode >= 200 && response.statusCode < 300) { + final jsonData = json.decode(response.body); + final notificationResponse = NotificationPreferenceResponse.fromJson(jsonData); + + if (notificationResponse.isSuccess == true && notificationResponse.data != null) { + _totalRecords = notificationResponse.totalRows ?? 0; + + if (notificationResponse.data!.isNotEmpty) { + _templates.addAll(notificationResponse.data!); + _currentPage++; + _hasMoreData = _templates.length < _totalRecords; + } else { + _hasMoreData = false; + } + } else { + _errorMessage = notificationResponse.message ?? 'Failed to load notification preferences'; + _hasMoreData = false; + } + } else { + _errorMessage = 'Failed to load notification preferences'; + _hasMoreData = false; + } + } catch (error) { + _errorMessage = 'An error occurred while loading data'; + print('Error in loadNotificationPreferences: $error'); + } finally { + _isLoading = false; + notifyListeners(); + } + } + + /// Load more data for pagination + Future loadMoreData() async { + if (_isLoadingMore || !_hasMoreData || _isLoading) return; + + _isLoadingMore = true; + notifyListeners(); + + try { + final body = { + 'pageNumber': _currentPage, + 'pageSize': _pageSize, + }; + + http.Response response = await ApiManager.instance.post( + URLs.getNotificationPreferences, + body: body, + showToast: false, + ); + + if (response.statusCode >= 200 && response.statusCode < 300) { + final jsonData = json.decode(response.body); + final notificationResponse = NotificationPreferenceResponse.fromJson(jsonData); + + if (notificationResponse.isSuccess == true && notificationResponse.data != null) { + if (notificationResponse.data!.isNotEmpty) { + _templates.addAll(notificationResponse.data!); + _currentPage++; + _hasMoreData = _templates.length < _totalRecords; + } else { + _hasMoreData = false; + } + } else { + _hasMoreData = false; + } + } else { + _hasMoreData = false; + } + } catch (error) { + print('Error in loadMoreData: $error'); + } finally { + _isLoadingMore = false; + notifyListeners(); + } + } + + /// Update enable/disable state for a template + void updateTemplateState(int index, bool isDisabled) { + if (index < 0 || index >= _templates.length) { + return; + } + + final template = _templates[index]; + + // Note: mandatory flag only applies to channel preferences, not to enable/disable toggle + // Users can always enable/disable notifications + + _templates[index] = template.copyWith( + isDisabled: isDisabled, + isModified: true, + ); + notifyListeners(); + } + + /// Update selected channels for a template + void updateTemplateChannels(int index, List channelIds) { + if (index < 0 || index >= _templates.length) return; + + final template = _templates[index]; + + _templates[index] = template.copyWith( + selectedChannelIds: channelIds, + isModified: true, + ); + _templates[index].selectedChannelIds = channelIds; + notifyListeners(); + } + + /// Save all modified preferences + Future savePreferences() async { + if (!hasChanges) return true; + + _isSaving = true; + _errorMessage = null; + notifyListeners(); + + try { + final preferences = _templates.where((t) => t.isModified).map((template) { + return { + 'notificationTemplateId': template.notificationTemplateId, + 'disabled': template.isDisabled ?? false, + 'channels': template.selectedChannelIds, + }; + }).toList(); + + if (preferences.isEmpty) { + _isSaving = false; + notifyListeners(); + return true; + } + + // Use ApiManager's postList method for array body + http.Response response = await ApiManager.instance.postList( + URLs.saveNotificationPreferences, + body: preferences, + showToast: true, + ); + + if (response.statusCode >= 200 && response.statusCode < 300) { + // Reset modification flags after successful save + for (var i = 0; i < _templates.length; i++) { + _templates[i] = _templates[i].copyWith(isModified: false); + } + + _isSaving = false; + notifyListeners(); + return true; + } else { + _errorMessage = 'Failed to save preferences'; + _isSaving = false; + notifyListeners(); + return false; + } + } catch (error) { + _errorMessage = 'An error occurred while saving'; + _isSaving = false; + notifyListeners(); + print('Error in savePreferences: $error'); + return false; + } + } + + /// Reset provider state + void reset() { + _templates.clear(); + _currentPage = 1; + _hasMoreData = true; + _totalRecords = 0; + _isLoading = false; + _isLoadingMore = false; + _errorMessage = null; + } + + /// Retry loading after error + Future retry() async { + reset(); + await loadNotificationPreferences(); + } +} diff --git a/lib/modules/notification_setting/view/notification_setting_page.dart b/lib/modules/notification_setting/view/notification_setting_page.dart new file mode 100644 index 00000000..773c8fa8 --- /dev/null +++ b/lib/modules/notification_setting/view/notification_setting_page.dart @@ -0,0 +1,138 @@ +import 'dart:developer'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:test_sa/extensions/context_extension.dart'; +import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/extensions/string_extensions.dart'; +import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart'; +import 'package:test_sa/modules/notification_setting/provider/notification_setting_provider.dart'; +import 'package:test_sa/modules/notification_setting/view/widgets/notification_preference_card.dart'; +import 'package:test_sa/new_views/common_widgets/app_filled_button.dart'; +import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart'; +import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; +import 'package:test_sa/views/widgets/loaders/app_loading.dart'; +import 'package:test_sa/views/widgets/loaders/no_data_found.dart'; + +class NotificationSettingPage extends StatefulWidget { + static const String id = "notification_setting_page"; + + const NotificationSettingPage({Key? key}) : super(key: key); + + @override + State createState() => _NotificationSettingPageState(); +} + +class _NotificationSettingPageState extends State { + final ScrollController _scrollController = ScrollController(); + late NotificationSettingProvider _provider; + + @override + void initState() { + super.initState(); + _scrollController.addListener(_onScroll); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _provider = Provider.of(context, listen: false); + _provider.loadNotificationPreferences(refresh: true); + }); + } + + @override + void dispose() { + _scrollController.removeListener(_onScroll); + _scrollController.dispose(); + super.dispose(); + } + + void _onScroll() { + if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { + _provider.loadMoreData(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: DefaultAppBar( + title: 'Notification Settings'.addTranslation, + ), + body: Consumer( + builder: (context, provider, child) { + _provider = provider; + if (provider.isLoading) { + return const Center(child: ALoading()); + } + if (provider.isEmpty || (provider.errorMessage != null && provider.templates.isEmpty)) { + return const Center(child: NoDataFound()); + } + return Column( + children: [ + Expanded( + child: RefreshIndicator( + onRefresh: () async { + await provider.loadNotificationPreferences(refresh: true); + }, + child: ListView.separated( + controller: _scrollController, + padding: EdgeInsets.all(16.toScreenWidth), + itemCount: provider.templates.length + (provider.isLoadingMore ? 1 : 0), + itemBuilder: (context, index) { + if (index == provider.templates.length) { + return const Center( + child: ALoading()); + } + + final template = provider.templates[index]; + return NotificationPreferenceCard( + key: ValueKey('${template.notificationTemplateId}_${template.isDisabled}'), + template: template, + onToggleChange: (isDisabled) { + provider.updateTemplateState(index, isDisabled); + }, + onChannelChange: (channelIds) { + provider.updateTemplateChannels(index, channelIds); + }, + ); + }, + separatorBuilder: (cxt, index) => 12.height, + ), + ), + ), + + // Save button - sticky at bottom + if (provider.hasChanges) + FooterActionButton.footerContainer( + context: context, + child: AppFilledButton( + onPressed: provider.isSaving ? null : _handleSave, + label: context.translation.save, + loading: provider.isSaving, + disableButton: provider.isSaving, + ), + ), + ], + ); + }, + ), + ); + } + + + Future _handleSave() async { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const Center(child: AppLazyLoading()), + ); + + final success = await _provider.savePreferences(); + // Navigator.pop(context); // Close loading dialog + + if (success) { + _provider.loadNotificationPreferences(refresh: true); + Navigator.pop(context); // Go back to previous screen + } + } +} + + diff --git a/lib/modules/notification_setting/view/widgets/notification_preference_card.dart b/lib/modules/notification_setting/view/widgets/notification_preference_card.dart new file mode 100644 index 00000000..c3dcdcb1 --- /dev/null +++ b/lib/modules/notification_setting/view/widgets/notification_preference_card.dart @@ -0,0 +1,186 @@ +import 'package:flutter/material.dart'; +import 'package:test_sa/extensions/context_extension.dart'; +import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/extensions/string_extensions.dart'; +import 'package:test_sa/extensions/text_extensions.dart'; +import 'package:test_sa/extensions/widget_extensions.dart'; +import 'package:test_sa/modules/notification_setting/models/notification_preference_model.dart'; +import 'package:test_sa/new_views/app_style/app_color.dart'; +import 'package:test_sa/new_views/app_style/app_text_style.dart'; +import 'package:test_sa/new_views/pages/land_page/widgets/static_filter_buttons.dart'; +import 'package:test_sa/views/widgets/item_views/info_header_widget.dart'; +import 'package:test_sa/views/widgets/item_views/info_text_widget.dart'; + +class NotificationPreferenceCard extends StatelessWidget { + final NotificationTemplate template; + final Function(bool isDisabled) onToggleChange; + final Function(List channelIds) onChannelChange; + + const NotificationPreferenceCard({ + Key? key, + required this.template, + required this.onToggleChange, + required this.onChannelChange, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final bool isEnabled = !(template.isDisabled ?? false); + final bool isMandatory = template.mandatory ?? false; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if(isMandatory) ...[ + 4.height, + Text( + 'You cannot update this notification as it is mandatory.'.addTranslation, + style: AppTextStyle.body2.copyWith( + color: AppColor.red30, + fontSize: 10.toScreenWidth, + ), + ), + ], + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: InfoHeader16Widget( + template.templateName ?? '', + ), + ), + 8.width, + _buildCompactToggle(context, isEnabled, isMandatory), + ], + ), + 4.height, + InfoTextWidget( + label: context.translation.notificationModule, + value: template.moduleName ?? '', + ), + 4.height, + InfoTextWidget( + label: 'Sub Module'.addTranslation, + value: template.subModuleName ?? '', + ), + 4.height, + if (template.templateChannels != null && template.templateChannels!.isNotEmpty) ...[ + InfoTextLabelWidget( + label: '${context.translation.channelPreference} :', + ), + 8.height, + _buildChannelChips(context, isMandatory), + + ], + ], + ).toShadowContainer(context,padding: 16); + } + + Widget _buildCompactToggle(BuildContext context, bool isEnabled, bool isMandatory) { + return Container( + width: 99.toScreenWidth, + height: 32.toScreenHeight, + padding: EdgeInsets.all(4.toScreenHeight), + decoration: BoxDecoration( + color: AppColor.toggleContainerBg(context), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: AppColor.border2Color(context)), + ), + child: IgnorePointer( + ignoring: isMandatory, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: () { + if(!isMandatory){ + onToggleChange(false); + } + }, + child: _buildToggleOption( + context: context, + label: context.translation.enable, + isActive: isEnabled, + activeColor: AppColor.toggleEnableBgColor(context), + inactiveColor: Colors.transparent, + textColor: isEnabled + ? AppColor.toggleEnableTextColor(context) + : AppColor.toggleDisableTextColor(context), + ), + ), + GestureDetector( + onTap: () { + if(!isMandatory){ + onToggleChange(true); + } + }, + child: _buildToggleOption( + context: context, + label: context.translation.disable, + isActive: !isEnabled, + activeColor: AppColor.toggleDisableBgColor(context), + inactiveColor: Colors.transparent, + textColor: !isEnabled + ? AppColor.toggleDisable2TextColor(context) + : AppColor.toggleDisableTextColor(context), + ), + ), + ], + ), + ), + ); + } + + Widget _buildToggleOption({ + required BuildContext context, + required String label, + required bool isActive, + required Color activeColor, + required Color inactiveColor, + required Color textColor, + }) { + return Container( + width: 44.toScreenWidth, + height: 22.toScreenHeight, + alignment: Alignment.center, + decoration: BoxDecoration( + color: isActive ? activeColor : inactiveColor, + borderRadius: BorderRadius.circular(3), + ), + child: Text( + label.toCamelCase, + style: AppTextStyle.body2.copyWith( + color: textColor, + fontSize: 10.toScreenWidth, + fontWeight: FontWeight.w600, + ), + ), + ); + } + + Widget _buildChannelChips(BuildContext context, bool isMandatory) { + final Map channelsMap = {}; + + if (template.templateChannels != null) { + for (var channel in template.templateChannels!) { + if (channel.name != null && channel.id != null) { + channelsMap[channel.name!] = channel.id!; + } + } + } + + return IgnorePointer( + ignoring: isMandatory, + child: RequestsTypeFilter( + items: channelsMap, + initialValues: template.selectedChannelIds, + onSelect: (selectedIds) { + if (!isMandatory) { + onChannelChange(selectedIds); + } + }, + ), + ); + } +} diff --git a/lib/new_views/app_style/app_color.dart b/lib/new_views/app_style/app_color.dart index 97a7dfcd..9933673e 100644 --- a/lib/new_views/app_style/app_color.dart +++ b/lib/new_views/app_style/app_color.dart @@ -201,6 +201,14 @@ class AppColor { static Color filterButtonTextColor(BuildContext context) => context.isDark ? neutral30 : neutral50; static Color addItemButtonColor(BuildContext context) => context.isDark ? AppColor.neutral60 : AppColor.white10; + // Toggle colors for notification settings + static Color toggleContainerBg(BuildContext context) => context.isDark ? containerBorderDark : white80; + static Color toggleEnableTextColor(BuildContext context) => context.isDark ? green70 : green15; + static Color toggleDisable2TextColor(BuildContext context) => context.isDark ? red30 : red30; + static Color toggleDisableTextColor(BuildContext context) => context.isDark ? white10 : white936; + static Color toggleEnableBgColor(BuildContext context) => context.isDark ? green70.withValues(alpha: .25) : green50.withValues(alpha: .25); + static Color toggleDisableBgColor(BuildContext context) => context.isDark ? red30.withValues(alpha: .25) : red30.withValues(alpha: .25); + static Color getRequestStatusTextColorByName(BuildContext context, String? status) { switch ((status ?? "").toLowerCase()) { case "open": diff --git a/lib/new_views/common_widgets/app_drawer.dart b/lib/new_views/common_widgets/app_drawer.dart index 93cabde5..bee95876 100644 --- a/lib/new_views/common_widgets/app_drawer.dart +++ b/lib/new_views/common_widgets/app_drawer.dart @@ -2,8 +2,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/extensions/string_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; +import 'package:test_sa/modules/notification_setting/view/notification_setting_page.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/pages/login_page.dart'; import 'package:test_sa/new_views/swipe_module/swipe_history_view.dart'; @@ -65,6 +67,8 @@ class AppDrawer extends StatelessWidget { children: [ drawerItem("drawer_notification", context.translation.notifications, context).onPress(() => Navigator.of(context).pushNamed(NotificationsPage.id)), 18.height, + drawerItem("notification_setting", 'Notification Settings'.addTranslation, context).onPress(() => Navigator.of(context).pushNamed(NotificationSettingPage.id)), + 18.height, // drawerItem("help_center", context.translation.helpCenter, context) /*.onPress(() => Navigator.of(context).pushNamed(HelpCenterPage.id))*/, // 18.height, // drawerItem("rate_us", context.translation.rateUs, context), diff --git a/lib/new_views/common_widgets/app_text_form_field.dart b/lib/new_views/common_widgets/app_text_form_field.dart index ad089ace..bfdedfd5 100644 --- a/lib/new_views/common_widgets/app_text_form_field.dart +++ b/lib/new_views/common_widgets/app_text_form_field.dart @@ -232,7 +232,7 @@ class _AppTextFormFieldState extends State { (widget.showSpeechToText && _speechToText.isListening) ? "Listening..." : widget.labelText ?? "", ), // labelStyle: widget.labelStyle, - hintStyle: widget.labelStyle, + hintStyle: widget.hintStyle, prefixIcon: widget.prefixIcon ?? (widget.prefixIconData == null ? null diff --git a/lib/providers/lookups/modules_with_submodules_provider.dart b/lib/providers/lookups/modules_with_submodules_provider.dart new file mode 100644 index 00000000..563ce35f --- /dev/null +++ b/lib/providers/lookups/modules_with_submodules_provider.dart @@ -0,0 +1,37 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:test_sa/controllers/api_routes/api_manager.dart'; +import 'package:test_sa/controllers/api_routes/urls.dart'; +import 'package:test_sa/models/base.dart'; +import 'package:test_sa/models/module_with_submodules_model.dart'; +import 'package:test_sa/providers/loading_list_notifier.dart'; + +class ModulesWithSubModulesProvider extends LoadingListNotifier { + @override + Future getData({int? id}) async { + if (loading == true) return -2; + loading = true; + notifyListeners(); + try { + Response response = await ApiManager.instance.get( + URLs.getModulesWithSubModules, + useCache: true, + enableToastMessage: false, + ); + stateCode = response.statusCode; + if (response.statusCode >= 200 && response.statusCode < 300) { + List modulesListJson = json.decode(response.body)["data"]; + items = modulesListJson.map((item) => ModuleWithSubModulesModel.fromJson(item)).toList(); + } + loading = false; + notifyListeners(); + return response.statusCode; + } catch (error) { + loading = false; + stateCode = -1; + notifyListeners(); + return -1; + } + } +} diff --git a/lib/providers/lookups/notification_channel_lookup_provider.dart b/lib/providers/lookups/notification_channel_lookup_provider.dart new file mode 100644 index 00000000..2f5bade0 --- /dev/null +++ b/lib/providers/lookups/notification_channel_lookup_provider.dart @@ -0,0 +1,36 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:test_sa/controllers/api_routes/api_manager.dart'; +import 'package:test_sa/controllers/api_routes/urls.dart'; +import 'package:test_sa/models/lookup.dart'; +import 'package:test_sa/providers/loading_list_notifier.dart'; + +class NotificationChannelLookupProvider extends LoadingListNotifier { + @override + Future getData({int? id}) async { + if (loading == true) return -2; + loading = true; + notifyListeners(); + try { + Response response = await ApiManager.instance.get( + URLs.getNotificationChannelLookup, + useCache: true, + enableToastMessage: false, + ); + stateCode = response.statusCode; + if (response.statusCode >= 200 && response.statusCode < 300) { + List categoriesListJson = json.decode(response.body)["data"]; + items = categoriesListJson.map((item) => Lookup.fromJson(item)).toList(); + } + loading = false; + notifyListeners(); + return response.statusCode; + } catch (error) { + loading = false; + stateCode = -1; + notifyListeners(); + return -1; + } + } +} diff --git a/lib/providers/lookups/notification_priority_lookup_provider.dart b/lib/providers/lookups/notification_priority_lookup_provider.dart new file mode 100644 index 00000000..120fb9a4 --- /dev/null +++ b/lib/providers/lookups/notification_priority_lookup_provider.dart @@ -0,0 +1,36 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:test_sa/controllers/api_routes/api_manager.dart'; +import 'package:test_sa/controllers/api_routes/urls.dart'; +import 'package:test_sa/models/lookup.dart'; +import 'package:test_sa/providers/loading_list_notifier.dart'; + +class NotificationPriorityLookupProvider extends LoadingListNotifier { + @override + Future getData({int? id}) async { + if (loading == true) return -2; + loading = true; + notifyListeners(); + try { + Response response = await ApiManager.instance.get( + URLs.getNotificationPriorityLookup, + useCache: true, + enableToastMessage: false, + ); + stateCode = response.statusCode; + if (response.statusCode >= 200 && response.statusCode < 300) { + List categoriesListJson = json.decode(response.body)["data"]; + items = categoriesListJson.map((item) => Lookup.fromJson(item)).toList(); + } + loading = false; + notifyListeners(); + return response.statusCode; + } catch (error) { + loading = false; + stateCode = -1; + notifyListeners(); + return -1; + } + } +} diff --git a/lib/providers/lookups/notification_recipient_lookup_provider.dart b/lib/providers/lookups/notification_recipient_lookup_provider.dart new file mode 100644 index 00000000..e6909835 --- /dev/null +++ b/lib/providers/lookups/notification_recipient_lookup_provider.dart @@ -0,0 +1,36 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:test_sa/controllers/api_routes/api_manager.dart'; +import 'package:test_sa/controllers/api_routes/urls.dart'; +import 'package:test_sa/models/lookup.dart'; +import 'package:test_sa/providers/loading_list_notifier.dart'; + +class NotificationRecipientLookupProvider extends LoadingListNotifier { + @override + Future getData({int? id}) async { + if (loading == true) return -2; + loading = true; + notifyListeners(); + try { + Response response = await ApiManager.instance.get( + URLs.getNotificationRecipientLookup, + useCache: true, + enableToastMessage: false, + ); + stateCode = response.statusCode; + if (response.statusCode >= 200 && response.statusCode < 300) { + List categoriesListJson = json.decode(response.body)["data"]; + items = categoriesListJson.map((item) => Lookup.fromJson(item)).toList(); + } + loading = false; + notifyListeners(); + return response.statusCode; + } catch (error) { + loading = false; + stateCode = -1; + notifyListeners(); + return -1; + } + } +} diff --git a/lib/views/pages/user/notifications/notification_filter_page.dart b/lib/views/pages/user/notifications/notification_filter_page.dart new file mode 100644 index 00000000..fe5e1cd3 --- /dev/null +++ b/lib/views/pages/user/notifications/notification_filter_page.dart @@ -0,0 +1,340 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:test_sa/extensions/context_extension.dart'; +import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/extensions/string_extensions.dart'; +import 'package:test_sa/extensions/text_extensions.dart'; +import 'package:test_sa/extensions/widget_extensions.dart'; +import 'package:test_sa/models/lookup.dart'; +import 'package:test_sa/models/module_with_submodules_model.dart'; +import 'package:test_sa/models/notification_filter_model.dart'; +import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart'; +import 'package:test_sa/new_views/app_style/app_color.dart'; +import 'package:test_sa/new_views/common_widgets/app_filled_button.dart'; +import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; +import 'package:test_sa/new_views/common_widgets/single_item_drop_down_menu.dart'; +import 'package:test_sa/providers/loading_list_notifier.dart'; +import 'package:test_sa/providers/lookups/modules_with_submodules_provider.dart'; +import 'package:test_sa/providers/lookups/notification_priority_lookup_provider.dart'; +import 'package:test_sa/providers/lookups/notification_channel_lookup_provider.dart'; +import 'package:test_sa/providers/lookups/notification_recipient_lookup_provider.dart'; +import 'package:test_sa/views/widgets/date_and_time/date_picker.dart'; +import 'package:test_sa/views/widgets/item_views/info_header_widget.dart'; + +class NotificationFilterPage extends StatefulWidget { + static const String id = "notification_filter_page"; + final NotificationFilterModel? data; + + const NotificationFilterPage({Key? key, this.data}) : super(key: key); + + @override + State createState() => _NotificationFilterPageState(); +} + +class _NotificationFilterPageState extends State { + NotificationFilterModel? filter; + Lookup? selectedPriority; + Lookup? selectedChannel; + Lookup? selectedRecipient; + ModuleWithSubModulesModel? selectedModule; + SubModuleModel? selectedSubModule; + + @override + void initState() { + super.initState(); + filter = widget.data ?? NotificationFilterModel(); + + // Load providers and restore selected values after first frame + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadProvidersAndRestoreValues(); + }); + } + + /// Load providers and restore previously selected filter values + Future _loadProvidersAndRestoreValues() async { + // Get providers + final priorityProvider = context.read(); + final channelProvider = context.read(); + final recipientProvider = context.read(); + final moduleProvider = context.read(); + + // Load data from providers if not already loaded + final futures = []; + if (priorityProvider.items.isEmpty) futures.add(priorityProvider.getData()); + if (channelProvider.items.isEmpty) futures.add(channelProvider.getData()); + if (recipientProvider.items.isEmpty) futures.add(recipientProvider.getData()); + if (moduleProvider.items.isEmpty) futures.add(moduleProvider.getData()); + + // Wait for all providers to load + await Future.wait(futures); + + if (!mounted) return; + + setState(() { + // Restore priority + if (filter?.priorityIds?.isNotEmpty ?? false) { + final priorityId = filter!.priorityIds!.first; + selectedPriority = priorityProvider.items.firstWhere( + (item) => item.id == priorityId, + orElse: () => Lookup(), + ); + if (selectedPriority?.id == null) selectedPriority = null; + } + + // Restore channel + if (filter?.channelIds?.isNotEmpty ?? false) { + final channelId = filter!.channelIds!.first; + selectedChannel = channelProvider.items.firstWhere( + (item) => item.id == channelId, + orElse: () => Lookup(), + ); + if (selectedChannel?.id == null) selectedChannel = null; + } + + // Restore recipient + if (filter?.recipientIds?.isNotEmpty ?? false) { + final recipientId = filter!.recipientIds!.first; + selectedRecipient = recipientProvider.items.firstWhere( + (item) => item.id == recipientId, + orElse: () => Lookup(), + ); + if (selectedRecipient?.id == null) selectedRecipient = null; + } + + // Restore module + if (filter?.moduleIds?.isNotEmpty ?? false) { + final moduleId = filter!.moduleIds!.first; + selectedModule = moduleProvider.items.firstWhere( + (item) => item.id == moduleId, + orElse: () => ModuleWithSubModulesModel(), + ); + if (selectedModule?.id == null) selectedModule = null; + + // Restore submodule if module is selected + if (selectedModule != null && (filter?.subModuleIds?.isNotEmpty ?? false)) { + final subModuleId = filter!.subModuleIds!.first; + selectedSubModule = selectedModule!.subModules?.firstWhere( + (item) => item.id == subModuleId, + orElse: () => SubModuleModel(), + ); + if (selectedSubModule?.id == null) selectedSubModule = null; + } + } + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColor.scaffold2Background(context), + appBar: DefaultAppBar( + title: context.translation.filter, + actions: [ + if (_showResetButton()) + TextButton( + onPressed: () { + setState(() { + filter?.clear(); + selectedPriority = null; + selectedChannel = null; + selectedRecipient = null; + selectedModule = null; + selectedSubModule = null; + }); + }, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: Text( + 'Clear Filters', + style: AppTextStyles.bodyText2.copyWith(color: AppColor.red30), + ), + ).paddingOnly(start: 16, end: 16) + ], + ), + body: Column( + children: [ + SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Filter by Priority + InfoHeader16Widget("Priority"), + 8.height, + SingleItemDropDownMenu( + context: context, + showAsBottomSheet: true, + showShadow: false, + title: "Select Priority", + initialValue: selectedPriority, + onSelect: (value) { + setState(() { + selectedPriority = value; + if (value != null) { + filter?.priorityIds = [value.id!]; + } else { + filter?.priorityIds?.clear(); + } + }); + }, + ), + + 16.height, + + // Filter by Channel + InfoHeader16Widget("Channel"), + 8.height, + SingleItemDropDownMenu( + context: context, + showAsBottomSheet: true, + showShadow: false, + title: "Select Channel", + initialValue: selectedChannel, + onSelect: (value) { + setState(() { + selectedChannel = value; + if (value != null) { + filter?.channelIds = [value.id!]; + } else { + filter?.channelIds?.clear(); + } + }); + }, + ), + + 16.height, + InfoHeader16Widget("Recipient"), + 8.height, + SingleItemDropDownMenu( + context: context, + showAsBottomSheet: true, + backgroundColor: AppColor.neutral100, + showShadow: false, + title: "Select Recipient", + initialValue: selectedRecipient, + onSelect: (value) { + setState(() { + selectedRecipient = value; + if (value != null) { + filter?.recipientIds = [value.id!]; + } else { + filter?.recipientIds?.clear(); + } + }); + }, + ), + + 16.height, + + // Filter by Module + InfoHeader16Widget("Module"), + 8.height, + SingleItemDropDownMenu( + context: context, + showAsBottomSheet: true, + showShadow: false, + title: "Select Module", + initialValue: selectedModule, + onSelect: (value) { + setState(() { + selectedModule = value; + selectedSubModule = null; + if (value != null) { + filter?.moduleIds = [value.id!]; + filter?.subModuleIds?.clear(); + } else { + filter?.moduleIds?.clear(); + filter?.subModuleIds?.clear(); + } + }); + }, + ), + + 16.height, + + // Filter by SubModule + InfoHeader16Widget( "SubModule"), + 8.height, + SingleItemDropDownMenu( + context: context, + showAsBottomSheet: true, + showShadow: false, + title: "Select SubModule", + initialValue: selectedSubModule, + enabled: selectedModule?.subModules?.isNotEmpty ?? false, + staticData: selectedModule?.subModules ?? [], + onSelect: (value) { + setState(() { + selectedSubModule = value; + if (value != null) { + filter?.subModuleIds = [value.id!]; + } else { + filter?.subModuleIds?.clear(); + } + }); + }, + ), + + 16.height, + + // Date Range Filter + context.translation.pickDate.heading5(context).custom(color: AppColor.textStyleColor(context)), + 8.height, + Row( + children: [ + ADatePicker( + label: context.translation.from, + from: DateTime(2000), + date: filter!.startDate, + onDatePicker: (date) { + setState(() { + filter!.startDate = date; + }); + }, + ).expanded, + 8.width, + ADatePicker( + label: context.translation.to, + from: DateTime(2000), + date: filter!.endDate, + onDatePicker: (date) { + if (filter!.startDate != null && date.isBefore(filter!.startDate!)) { + "End Date must be greater than start date".showToast; + return; + } + setState(() { + filter!.endDate = date; + }); + }, + ).expanded, + ], + ), + ], + ), + ).expanded, + 16.height, + FooterActionButton.footerContainer( + context: context, + child: AppFilledButton( + onPressed: _applyFilter, + label: context.translation.applyFilter, + ), + ) + ], + ), + ); + } + + void _applyFilter() { + if (filter?.startDate != null && filter?.endDate == null) { + filter?.endDate = DateTime.now(); + } + Navigator.of(context).pop(filter); + } + + bool _showResetButton() { + return filter?.hasActiveFilters ?? false; + } +} diff --git a/lib/views/pages/user/notifications/notifications_list.dart b/lib/views/pages/user/notifications/notifications_list.dart index db311261..b6549706 100644 --- a/lib/views/pages/user/notifications/notifications_list.dart +++ b/lib/views/pages/user/notifications/notifications_list.dart @@ -1,11 +1,15 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:test_sa/controllers/notification/firebase_notification_manger.dart'; +import 'package:test_sa/controllers/providers/api/notifications_provider.dart'; import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/models/system_notification_model.dart'; +import 'package:test_sa/views/pages/user/notifications/widgets/notification_card_redesigned.dart'; import 'package:test_sa/views/widgets/loaders/lazy_loading.dart'; import 'package:test_sa/views/widgets/loaders/no_data_found.dart'; -import 'package:test_sa/views/widgets/notifications/notification_item.dart'; class NotificationsList extends StatelessWidget { final List notifications; @@ -22,24 +26,28 @@ class NotificationsList extends StatelessWidget { return LazyLoading( nextPage: nextPage, onLazyLoad: onLazyLoad, - child: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: const EdgeInsets.all(0), - itemCount: notifications.length, - separatorBuilder: (context, itemIndex) => const Divider().defaultStyle(context).paddingOnly(top: 4, bottom: 4), - itemBuilder: (context, itemIndex) { - return NotificationItem( - notification: notifications[itemIndex], - onPressed: (notification) { - FirebaseNotificationManger.handleMessage(context, notification.toNotificationJson()); - }, - ); - }, - ).toShadowContainer(context), + child: ListView.separated( + // padding: const EdgeInsets.all(16), + itemCount: notifications.length, + separatorBuilder: (context, itemIndex) => const SizedBox(height: 12), + itemBuilder: (context, itemIndex) { + final notification = notifications[itemIndex]; + return NotificationCardRedesigned( + notification: notification, + onTap: () async { + // Mark notification as read when tapped + log('i am tapped with id ${notification.toJson()}'); + if (notification.id != null && !(notification.readed ?? false)) { + final provider = Provider.of(context, listen: false); + await provider.markOneAsRead(notificationId: notification.id!); + } + + // Navigate to notification details + FirebaseNotificationManger.handleMessage(context, notification.toNotificationJson()); + }, + ); + }, ), ); } -} +} \ No newline at end of file diff --git a/lib/views/pages/user/notifications/notifications_page.dart b/lib/views/pages/user/notifications/notifications_page.dart index 8c551b45..4ffc2c11 100644 --- a/lib/views/pages/user/notifications/notifications_page.dart +++ b/lib/views/pages/user/notifications/notifications_page.dart @@ -2,10 +2,20 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:test_sa/controllers/providers/api/notifications_provider.dart'; import 'package:test_sa/controllers/providers/api/user_provider.dart'; -import 'package:test_sa/controllers/providers/settings/setting_provider.dart'; import 'package:test_sa/extensions/context_extension.dart'; +import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/extensions/string_extensions.dart'; +import 'package:test_sa/extensions/text_extensions.dart'; +import 'package:test_sa/extensions/widget_extensions.dart'; +import 'package:test_sa/models/notification_filter_model.dart'; +import 'package:test_sa/models/system_notification_model.dart'; +import 'package:test_sa/new_views/app_style/app_color.dart'; +import 'package:test_sa/new_views/common_widgets/app_text_form_field.dart'; import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; +import 'package:test_sa/views/pages/user/notifications/notification_filter_page.dart'; import 'package:test_sa/views/pages/user/notifications/notifications_list.dart'; +import 'package:test_sa/views/pages/user/notifications/widgets/notification_tab_button.dart'; +import 'dart:async'; import '../../../widgets/loaders/loading_manager.dart'; @@ -21,40 +31,346 @@ class NotificationsPage extends StatefulWidget { class _NotificationsPageState extends State with TickerProviderStateMixin { late NotificationsProvider _notificationsProvider; late UserProvider _userProvider; - late SettingProvider _settingProvider; + + final TextEditingController _searchController = TextEditingController(); + int _selectedTabIndex = 0; // 0: All, 1: Unread, 2: Read + String _searchQuery = ''; + bool isAscending = false; + NotificationFilterModel? _activeFilters; + Timer? _debounceTimer; + + @override + void initState() { + super.initState(); + // Initialize filter model with default isRead = 0 (All) + _activeFilters = NotificationFilterModel(isRead: 0); + + // Load notifications when page loads + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadNotifications(); + }); + } + + @override + void dispose() { + _searchController.dispose(); + _debounceTimer?.cancel(); + super.dispose(); + } + + /// Load notifications from API + Future _loadNotifications() async { + final notificationsProvider = Provider.of(context, listen: false); + final userProvider = Provider.of(context, listen: false); + + // Update filter model with search, sort, and tab selection + _activeFilters ??= NotificationFilterModel(); + _activeFilters!.searchText = _searchQuery.isEmpty ? null : _searchQuery; + _activeFilters!.isAscending = isAscending; + _activeFilters!.isRead = _selectedTabIndex; // 0 = All, 1 = Unread, 2 = Read + + // Reset and fetch fresh data with filters + notificationsProvider.reset(); + await notificationsProvider.getSystemNotifications( + user: userProvider.user!, + resetProvider: true, + filters: _activeFilters, + ); + } + + /// Handle search with debounce + void _onSearchChanged(String value) { + setState(() { + _searchQuery = value; + }); + + // Cancel previous timer + _debounceTimer?.cancel(); + + // Start new timer (500ms debounce) + _debounceTimer = Timer(const Duration(milliseconds: 500), () { + _loadNotifications(); + }); + } + + /// Toggle sort order and reload data + void _toggleSort() { + setState(() { + isAscending = !isAscending; + }); + _loadNotifications(); + } + + List get _filteredNotifications { + // Tab filtering is now handled by API via isRead parameter + // No need for local filtering + return _notificationsProvider.notifications; + } + + int get _unreadCount { + return _notificationsProvider.notifications.where((n) => !(n.readed ?? false)).length; + } + + int get _allCount { + return _notificationsProvider.notifications.length; + } + + int get _readCount { + return _notificationsProvider.notifications.where((n) => n.readed ?? false).length; + } + + /// Get count of active filters + int get _activeFilterCount { + if (_activeFilters == null) return 0; + + int count = 0; + if (_activeFilters!.priorityIds?.isNotEmpty ?? false) count++; + if (_activeFilters!.channelIds?.isNotEmpty ?? false) count++; + if (_activeFilters!.recipientIds?.isNotEmpty ?? false) count++; + if (_activeFilters!.moduleIds?.isNotEmpty ?? false) count++; + if (_activeFilters!.subModuleIds?.isNotEmpty ?? false) count++; + if (_activeFilters!.startDate != null) count++; + if (_activeFilters!.endDate != null) count++; + + return count; + } @override Widget build(BuildContext context) { _notificationsProvider = Provider.of(context); _userProvider = Provider.of(context); - _settingProvider = Provider.of(context); + return Scaffold( - appBar: DefaultAppBar(title: context.translation.notifications), - body: SafeArea( - child: LoadingManager( - isLoading: (_notificationsProvider.isLoading ?? false) && _notificationsProvider.notifications.isEmpty, - isFailedLoading: _notificationsProvider.notifications == null, - stateCode: _notificationsProvider.stateCode, - onRefresh: () async { - _notificationsProvider.reset(); - await _notificationsProvider.getSystemNotifications(user: _userProvider.user!, resetProvider: true - - // host: _settingProvider.host, - // hospitalId: _userProvider.user.clientId, - ); - }, - child: NotificationsList( - nextPage: _notificationsProvider.nextPage, - onLazyLoad: () async { - await _notificationsProvider.getSystemNotifications( - user: _userProvider.user!, - // host: _settingProvider.host, - // hospitalId: _userProvider.user.clientId, + backgroundColor: AppColor.scaffold2Background(context), + appBar: DefaultAppBar( + title: context.translation.notifications, + actions: [ + // Mark All As Read Button + TextButton.icon( + onPressed: () async { + // Show loading dialog + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const Center( + child: CircularProgressIndicator(), + ), + ); + + // Call mark all as read API + final result = await _notificationsProvider.markAllAsRead( + userId: _userProvider.user!.userID!, ); + + // Close loading dialog + if (context.mounted) { + Navigator.of(context).pop(); + } + + // Show API response message only if provided + if (context.mounted && result['message'] != null) { + result['message'].addTranslation.showToast; + } }, - notifications: _notificationsProvider.notifications, + icon: 'Icon_mark_read'.toSvgAsset(width: 12, height: 12), + label: Text( + 'Mark All as Read', + style: AppTextStyles.bodyText2.copyWith( + color: AppColor.primary10, + fontWeight: FontWeight.w500, + ), + ), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + foregroundColor: AppColor.primary10, + ), ), - ), + ], + ), + body: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // Search Section + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 8.height, + AppTextFormField( + controller: _searchController, + labelText: context.translation.search, + showShadow: false, + showBorder: true, + prefixIconData: Icons.search, + hintText: 'Search by Title or content'.addTranslation, + hintStyle: AppTextStyles.tinyFont.copyWith(color: AppColor.labelTextStyleColor(context)), + contentPadding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16), + onChange: (value) { + _onSearchChanged(value); + }, + ), + ], + ), + + 16.height, + + // Filter Section (Placeholder for future filter/sort buttons) + Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + // TODO: Add Filter and Sort buttons here when needed + const Spacer(), + ], + ), + ), + + // Notification Tabs + Row( + children: [ + Expanded( + child: SizedBox( + height: 35.toScreenHeight, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: 3, + separatorBuilder: (context, index) => 8.width, + itemBuilder: (context, index) { + final labels = ['All Notifications', 'Read', 'Unread']; + final counts = [_allCount, _readCount, _unreadCount]; + return NotificationTabButton( + label: labels[index], + count: counts[index], + isSelected: _selectedTabIndex == index, + onTap: () { + setState(() { + _selectedTabIndex = index; + }); + // Reload notifications with new tab filter + _loadNotifications(); + }, + ); + }, + ), + ), + ), + 8.width, + // Sort & Filter Container + Container( + height: 35.toScreenHeight, + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: AppColor.background(context), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: AppColor.borderColor(context)), + ), + child: Row( + children: [ + // Sort Icon + Transform.flip( + flipY: isAscending, + child: 'dashboard/sort_descending' + .toSvgAsset( + width: 20, + height: 20, + color: AppColor.iconColor(context), + ) + .onPress(() { + _toggleSort(); + }), + ), + 14.width, + // Filter Icon + Stack( + clipBehavior: Clip.none, + children: [ + 'filter' + .toSvgAsset( + width: 20, + height: 20, + color: AppColor.iconColor(context), + ) + .onPress(() async { + // Open filter page + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NotificationFilterPage(data: _activeFilters), + ), + ); + + if (result != null) { + setState(() { + _activeFilters = result; + }); + _loadNotifications(); + } + }), + // Active Filters Badge + if (_activeFilterCount > 0) + Positioned( + right: -8, + top: -8, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), + decoration: BoxDecoration( + color: AppColor.red30, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: AppColor.white10, width: 1.5), + ), + constraints: const BoxConstraints( + minWidth: 16, + minHeight: 16, + ), + child: Text( + '$_activeFilterCount', + textAlign: TextAlign.center, + style: AppTextStyles.tinyFont2.copyWith( + color: AppColor.white10, + fontWeight: FontWeight.w600, + fontSize: 9, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + + 16.height, + + // Notifications List + Expanded( + child: LoadingManager( + isLoading: (_notificationsProvider.isLoading) && _notificationsProvider.notifications.isEmpty, + isFailedLoading: false, // Don't show error state, show no data found instead + stateCode: _notificationsProvider.stateCode, + onRefresh: () async { + _notificationsProvider.reset(); + await _notificationsProvider.getSystemNotifications( + user: _userProvider.user!, + resetProvider: true, + filters: _activeFilters, + ); + }, + child: NotificationsList( + nextPage: _notificationsProvider.nextPage, + onLazyLoad: () async { + await _notificationsProvider.getSystemNotifications( + user: _userProvider.user!, + filters: _activeFilters, + ); + }, + notifications: _filteredNotifications, + ), + ), + ), + ], + ).paddingOnly(start: 16, end: 16), ), ); } diff --git a/lib/views/pages/user/notifications/widgets/notification_card_redesigned.dart b/lib/views/pages/user/notifications/widgets/notification_card_redesigned.dart new file mode 100644 index 00000000..5ea5790b --- /dev/null +++ b/lib/views/pages/user/notifications/widgets/notification_card_redesigned.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/extensions/text_extensions.dart'; +import 'package:test_sa/models/system_notification_model.dart'; +import 'package:test_sa/new_views/app_style/app_color.dart'; +import 'package:test_sa/views/widgets/item_views/info_date_widget.dart'; +import 'package:test_sa/views/widgets/item_views/info_header_widget.dart'; +import 'package:test_sa/views/widgets/item_views/info_text_widget.dart'; + +class NotificationCardRedesigned extends StatelessWidget { + final SystemNotificationModel notification; + final VoidCallback onTap; + + const NotificationCardRedesigned({ + Key? key, + required this.notification, + required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final isUnread = !(notification.readed ?? false); + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColor.background(context), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: AppColor.borderColor(context)), + ), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Priority Badge & Date Row + Row( + children: [ + // Priority Badge + if (notification.priorityName != null && notification.priorityName!.isNotEmpty) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: AppColor.getRequestStatusColorByName( + context, + notification.priorityName!, + ), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + notification.priorityName!, + style: AppTextStyles.tinyFont2.copyWith( + color: AppColor.getRequestStatusTextColorByName( + context, + notification.priorityName!, + ), + fontWeight: FontWeight.w600, + ), + ), + ), + const Spacer(), + + // Date & Time - Top Right + if (notification.createdOn?.isNotEmpty ?? false) + InfoDateWidget(notification.createdOn ?? ""), + ], + ), + 8.height, + + // Title with Unread Dot + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Unread indicator (blue dot) - aligned with title + if (isUnread) + Padding( + padding: const EdgeInsets.only(top: 6, right: 8), + child: Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + color: AppColor.primary10, + shape: BoxShape.circle, + ), + ), + ), + + // Title + Expanded( + child: InfoHeader16Widget( + notification.title ?? '', + ), + ), + ], + ), + 8.height, + if (notification.sourceName?.isNotEmpty ?? false) + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: AppColor.primary10.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + notification.sourceName!, + style: AppTextStyles.tinyFont2.copyWith( + color: AppColor.primary10, + fontWeight: FontWeight.w500, + ), + ), + ), + if (notification.sourceName?.isNotEmpty ?? false) 8.height, + + // Message Preview + if (notification.text?.isNotEmpty ?? false) + InfoTextLabelWidget( + label: notification.text!, + // style: AppTextStyles.bodyText2.copyWith( + // color: AppColor.textColor(context), + // height: 1.4, + // ), + // maxLines: 3, + // overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ); + } + +} \ No newline at end of file diff --git a/lib/views/pages/user/notifications/widgets/notification_tab_button.dart b/lib/views/pages/user/notifications/widgets/notification_tab_button.dart new file mode 100644 index 00000000..754871af --- /dev/null +++ b/lib/views/pages/user/notifications/widgets/notification_tab_button.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:test_sa/extensions/text_extensions.dart'; +import 'package:test_sa/new_views/app_style/app_color.dart'; + +class NotificationTabButton extends StatelessWidget { + final String label; + final int? count; + final bool isSelected; + final VoidCallback onTap; + + const NotificationTabButton({ + Key? key, + required this.label, + this.count, + required this.isSelected, + required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: ShapeDecoration( + color: AppColor.background(context), + shape: RoundedRectangleBorder( + side: BorderSide( + width: 1, + color: isSelected ? AppColor.primary10 : AppColor.borderColor(context), + ), + borderRadius: BorderRadius.circular(6), + ), + shadows: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 14, + offset: const Offset(0, 0), + spreadRadius: 0, + ) + ], + ), + child: Text( + count != null ? '$label ($count)' : label, + style: AppTextStyles.tinyFont.copyWith( + height: 1, + color: isSelected ? AppColor.primary10 : AppColor.filterButtonTextColor(context), + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, + ), + ), + ), + ); + } +} From afc1e9c77a21ece158af7b04b6a42e822fa0b18d Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Thu, 25 Jun 2026 16:54:49 +0300 Subject: [PATCH 02/35] notification implementation completed --- lib/controllers/api_routes/urls.dart | 4 +- .../providers/api/notifications_provider.dart | 39 +++++++++- lib/models/module_with_submodules_model.dart | 1 + lib/models/system_notification_model.dart | 8 +- .../notification_filter_page.dart | 75 ------------------- .../notifications/notifications_page.dart | 74 +++++++----------- 6 files changed, 72 insertions(+), 129 deletions(-) diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart index 77f8ad95..b7e7e519 100644 --- a/lib/controllers/api_routes/urls.dart +++ b/lib/controllers/api_routes/urls.dart @@ -242,9 +242,9 @@ class URLs { static get markOneNotificationAsRead => "$_baseUrl/NotificationNew/MarkAsReadOneNotification"; // mark one notification as read // Notification Filter Lookups - static get getNotificationPriorityLookup => "$_baseUrl/Lookups/GetLookup?lookupEnum=5022"; // notification priority filter + static get getNotificationPriorityLookup => "$_baseUrl/Lookups/GetLookup?lookupEnum=5021"; // notification priority filter static get getNotificationChannelLookup => "$_baseUrl/Lookups/GetLookup?lookupEnum=5024"; // notification channel filter - static get getNotificationRecipientLookup => "$_baseUrl/Lookups/GetLookup?lookupEnum=5021"; // notification recipient filter + static get getNotificationRecipientLookup => "$_baseUrl/Lookups/GetLookup?lookupEnum=5022"; // notification recipient filter // Modules and SubModules static get getModulesWithSubModules => "$_baseUrl/Module/GetModulesWithSubModules"; // get modules with submodules for filters diff --git a/lib/controllers/providers/api/notifications_provider.dart b/lib/controllers/providers/api/notifications_provider.dart index 49422655..6da2120d 100644 --- a/lib/controllers/providers/api/notifications_provider.dart +++ b/lib/controllers/providers/api/notifications_provider.dart @@ -15,11 +15,21 @@ class NotificationsProvider extends ChangeNotifier { // Store active filters NotificationFilterModel? activeFilters; + // Store notification counts from API + int totalAllNotifications = 0; + int totalNotificationRead = 0; + int totalNotificationUnread = 0; + int totalRows = 0; // Total number of items available in the database + //reset provider data void reset() { notifications = []; nextPage = true; stateCode = null; + totalAllNotifications = 0; + totalNotificationRead = 0; + totalNotificationUnread = 0; + totalRows = 0; } // state code of current request to defied error message @@ -83,11 +93,20 @@ class NotificationsProvider extends ChangeNotifier { stateCode = response.statusCode; if (response.statusCode >= 200 && response.statusCode < 300) { // client's request was successfully received - List requestsListJson = json.decode(response.body)["data"]; + final responseData = json.decode(response.body); + List requestsListJson = responseData["data"]; List serviceRequestsPage = requestsListJson.map((request) => SystemNotificationModel.fromJson(request)).toList(); notifications.addAll(serviceRequestsPage); - // Update nextPage based on response length - nextPage = serviceRequestsPage.length == pageItemNumber; + + // Update notification counts from API response + totalAllNotifications = responseData["totalAllNotifications"] ?? 0; + totalNotificationRead = responseData["totalNotificationRead"] ?? 0; + totalNotificationUnread = responseData["totalNotificationUnread"] ?? 0; + totalRows = responseData["totalRows"] ?? 0; + + // Update nextPage based on totalRows comparison + // If we've loaded all available notifications, there's no next page + nextPage = notifications.length < totalRows; } isLoading = false; notifyListeners(); @@ -113,6 +132,11 @@ class NotificationsProvider extends ChangeNotifier { for (var notification in notifications) { notification.readed = true; } + + // Update counts: Move all unread to read + totalNotificationRead = totalNotificationRead + totalNotificationUnread; + totalNotificationUnread = 0; + notifyListeners(); // Parse response to get message @@ -144,8 +168,15 @@ class NotificationsProvider extends ChangeNotifier { if (response.statusCode >= 200 && response.statusCode < 300) { // Update the specific notification in the local list to read status final index = notifications.indexWhere((n) => n.id == notificationId); - if (index != -1) { + if (index != -1 && notifications[index].readed == false) { notifications[index].readed = true; + + // Update counts: Move one from unread to read + if (totalNotificationUnread > 0) { + totalNotificationUnread--; + totalNotificationRead++; + } + notifyListeners(); } return true; diff --git a/lib/models/module_with_submodules_model.dart b/lib/models/module_with_submodules_model.dart index 1b215c8b..24dc3808 100644 --- a/lib/models/module_with_submodules_model.dart +++ b/lib/models/module_with_submodules_model.dart @@ -13,6 +13,7 @@ class ModuleWithSubModulesModel extends Base { ModuleWithSubModulesModel.fromJson(Map json) { id = json['id']; + identifier = id?.toString(); name = json['name']; if (json['subModules'] != null) { subModules = []; diff --git a/lib/models/system_notification_model.dart b/lib/models/system_notification_model.dart index 041a8780..bb64637d 100644 --- a/lib/models/system_notification_model.dart +++ b/lib/models/system_notification_model.dart @@ -22,6 +22,7 @@ class SystemNotificationModel { bool? sent; String? sentDate; int? noOfFailure; + int? totalUnreadCount; // Added from API payload SystemNotificationModel( {this.userId, @@ -44,7 +45,8 @@ class SystemNotificationModel { this.channelName, this.sent, this.sentDate, - this.noOfFailure}); + this.noOfFailure, + this.totalUnreadCount}); SystemNotificationModel.fromJson(Map? json) { // Allow json to be null @@ -73,7 +75,7 @@ class SystemNotificationModel { sent = json['sent']; sentDate = json['sentDate']; noOfFailure = json['noOfFailure']; - + totalUnreadCount = json['totalUnreadCount']; } } @@ -99,6 +101,7 @@ class SystemNotificationModel { data['sent'] = sent; data['sentDate'] = sentDate; data['noOfFailure'] = noOfFailure; + data['totalUnreadCount'] = totalUnreadCount; return data; } @@ -125,6 +128,7 @@ class SystemNotificationModel { data['sent'] = sent; data['sentDate'] = sentDate; data['noOfFailure'] = noOfFailure; + data['totalUnreadCount'] = totalUnreadCount; return data; } } \ No newline at end of file diff --git a/lib/views/pages/user/notifications/notification_filter_page.dart b/lib/views/pages/user/notifications/notification_filter_page.dart index fe5e1cd3..334f94a6 100644 --- a/lib/views/pages/user/notifications/notification_filter_page.dart +++ b/lib/views/pages/user/notifications/notification_filter_page.dart @@ -16,8 +16,6 @@ import 'package:test_sa/new_views/common_widgets/single_item_drop_down_menu.dart import 'package:test_sa/providers/loading_list_notifier.dart'; import 'package:test_sa/providers/lookups/modules_with_submodules_provider.dart'; import 'package:test_sa/providers/lookups/notification_priority_lookup_provider.dart'; -import 'package:test_sa/providers/lookups/notification_channel_lookup_provider.dart'; -import 'package:test_sa/providers/lookups/notification_recipient_lookup_provider.dart'; import 'package:test_sa/views/widgets/date_and_time/date_picker.dart'; import 'package:test_sa/views/widgets/item_views/info_header_widget.dart'; @@ -34,8 +32,6 @@ class NotificationFilterPage extends StatefulWidget { class _NotificationFilterPageState extends State { NotificationFilterModel? filter; Lookup? selectedPriority; - Lookup? selectedChannel; - Lookup? selectedRecipient; ModuleWithSubModulesModel? selectedModule; SubModuleModel? selectedSubModule; @@ -54,15 +50,11 @@ class _NotificationFilterPageState extends State { Future _loadProvidersAndRestoreValues() async { // Get providers final priorityProvider = context.read(); - final channelProvider = context.read(); - final recipientProvider = context.read(); final moduleProvider = context.read(); // Load data from providers if not already loaded final futures = []; if (priorityProvider.items.isEmpty) futures.add(priorityProvider.getData()); - if (channelProvider.items.isEmpty) futures.add(channelProvider.getData()); - if (recipientProvider.items.isEmpty) futures.add(recipientProvider.getData()); if (moduleProvider.items.isEmpty) futures.add(moduleProvider.getData()); // Wait for all providers to load @@ -81,26 +73,6 @@ class _NotificationFilterPageState extends State { if (selectedPriority?.id == null) selectedPriority = null; } - // Restore channel - if (filter?.channelIds?.isNotEmpty ?? false) { - final channelId = filter!.channelIds!.first; - selectedChannel = channelProvider.items.firstWhere( - (item) => item.id == channelId, - orElse: () => Lookup(), - ); - if (selectedChannel?.id == null) selectedChannel = null; - } - - // Restore recipient - if (filter?.recipientIds?.isNotEmpty ?? false) { - final recipientId = filter!.recipientIds!.first; - selectedRecipient = recipientProvider.items.firstWhere( - (item) => item.id == recipientId, - orElse: () => Lookup(), - ); - if (selectedRecipient?.id == null) selectedRecipient = null; - } - // Restore module if (filter?.moduleIds?.isNotEmpty ?? false) { final moduleId = filter!.moduleIds!.first; @@ -136,8 +108,6 @@ class _NotificationFilterPageState extends State { setState(() { filter?.clear(); selectedPriority = null; - selectedChannel = null; - selectedRecipient = null; selectedModule = null; selectedSubModule = null; }); @@ -183,51 +153,6 @@ class _NotificationFilterPageState extends State { 16.height, - // Filter by Channel - InfoHeader16Widget("Channel"), - 8.height, - SingleItemDropDownMenu( - context: context, - showAsBottomSheet: true, - showShadow: false, - title: "Select Channel", - initialValue: selectedChannel, - onSelect: (value) { - setState(() { - selectedChannel = value; - if (value != null) { - filter?.channelIds = [value.id!]; - } else { - filter?.channelIds?.clear(); - } - }); - }, - ), - - 16.height, - InfoHeader16Widget("Recipient"), - 8.height, - SingleItemDropDownMenu( - context: context, - showAsBottomSheet: true, - backgroundColor: AppColor.neutral100, - showShadow: false, - title: "Select Recipient", - initialValue: selectedRecipient, - onSelect: (value) { - setState(() { - selectedRecipient = value; - if (value != null) { - filter?.recipientIds = [value.id!]; - } else { - filter?.recipientIds?.clear(); - } - }); - }, - ), - - 16.height, - // Filter by Module InfoHeader16Widget("Module"), 8.height, diff --git a/lib/views/pages/user/notifications/notifications_page.dart b/lib/views/pages/user/notifications/notifications_page.dart index 4ffc2c11..c1207f19 100644 --- a/lib/views/pages/user/notifications/notifications_page.dart +++ b/lib/views/pages/user/notifications/notifications_page.dart @@ -10,11 +10,13 @@ import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/models/notification_filter_model.dart'; import 'package:test_sa/models/system_notification_model.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; +import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart'; import 'package:test_sa/new_views/common_widgets/app_text_form_field.dart'; import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; import 'package:test_sa/views/pages/user/notifications/notification_filter_page.dart'; import 'package:test_sa/views/pages/user/notifications/notifications_list.dart'; import 'package:test_sa/views/pages/user/notifications/widgets/notification_tab_button.dart'; +import 'package:test_sa/views/widgets/loaders/app_loading.dart'; import 'dart:async'; import '../../../widgets/loaders/loading_manager.dart'; @@ -108,15 +110,15 @@ class _NotificationsPageState extends State with TickerProvid } int get _unreadCount { - return _notificationsProvider.notifications.where((n) => !(n.readed ?? false)).length; + return _notificationsProvider.totalNotificationUnread; } int get _allCount { - return _notificationsProvider.notifications.length; + return _notificationsProvider.totalAllNotifications; } int get _readCount { - return _notificationsProvider.notifications.where((n) => n.readed ?? false).length; + return _notificationsProvider.totalNotificationRead; } /// Get count of active filters @@ -125,8 +127,6 @@ class _NotificationsPageState extends State with TickerProvid int count = 0; if (_activeFilters!.priorityIds?.isNotEmpty ?? false) count++; - if (_activeFilters!.channelIds?.isNotEmpty ?? false) count++; - if (_activeFilters!.recipientIds?.isNotEmpty ?? false) count++; if (_activeFilters!.moduleIds?.isNotEmpty ?? false) count++; if (_activeFilters!.subModuleIds?.isNotEmpty ?? false) count++; if (_activeFilters!.startDate != null) count++; @@ -145,34 +145,26 @@ class _NotificationsPageState extends State with TickerProvid appBar: DefaultAppBar( title: context.translation.notifications, actions: [ - // Mark All As Read Button TextButton.icon( - onPressed: () async { - // Show loading dialog - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => const Center( - child: CircularProgressIndicator(), - ), - ); - - // Call mark all as read API - final result = await _notificationsProvider.markAllAsRead( - userId: _userProvider.user!.userID!, - ); - - // Close loading dialog - if (context.mounted) { - Navigator.of(context).pop(); - } - - // Show API response message only if provided - if (context.mounted && result['message'] != null) { - result['message'].addTranslation.showToast; - } - }, - icon: 'Icon_mark_read'.toSvgAsset(width: 12, height: 12), + onPressed: _unreadCount > 0 + ? () async { + showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading()); + final result = await _notificationsProvider.markAllAsRead( + userId: _userProvider.user!.userID!, + ); + if (context.mounted) { + Navigator.of(context).pop(); + } + if (context.mounted && result['message'] != null) { + '${result['message']}'.showToast; + } + } + : null, + icon: 'Icon_mark_read'.toSvgAsset( + width: 12, + height: 12, + color: AppColor.primary10, + ), label: Text( 'Mark All as Read', style: AppTextStyles.bodyText2.copyWith( @@ -182,7 +174,7 @@ class _NotificationsPageState extends State with TickerProvid ), style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - foregroundColor: AppColor.primary10, + foregroundColor: _unreadCount > 0 ? AppColor.primary10 : AppColor.neutral30, ), ), ], @@ -214,17 +206,6 @@ class _NotificationsPageState extends State with TickerProvid 16.height, - // Filter Section (Placeholder for future filter/sort buttons) - Container( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - // TODO: Add Filter and Sort buttons here when needed - const Spacer(), - ], - ), - ), - // Notification Tabs Row( children: [ @@ -310,7 +291,7 @@ class _NotificationsPageState extends State with TickerProvid if (_activeFilterCount > 0) Positioned( right: -8, - top: -8, + top: -12, child: Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), decoration: BoxDecoration( @@ -347,7 +328,8 @@ class _NotificationsPageState extends State with TickerProvid Expanded( child: LoadingManager( isLoading: (_notificationsProvider.isLoading) && _notificationsProvider.notifications.isEmpty, - isFailedLoading: false, // Don't show error state, show no data found instead + isFailedLoading: false, + // Don't show error state, show no data found instead stateCode: _notificationsProvider.stateCode, onRefresh: () async { _notificationsProvider.reset(); From 37a2be7b7866637f6d1d6bbb9fb703e2c80322c7 Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Wed, 1 Jul 2026 09:03:38 +0300 Subject: [PATCH 03/35] improvements --- .../providers/api/notifications_provider.dart | 4 +- lib/models/system_notification_model.dart | 5 + .../widgets/notification_preference_card.dart | 10 +- lib/new_views/app_style/app_color.dart | 36 +++++ .../notification_filter_page.dart | 28 ++-- .../notifications/notifications_list.dart | 8 +- .../notifications/notifications_page.dart | 54 +++---- .../widgets/notification_card_redesigned.dart | 144 ++++++------------ .../widgets/notification_tab_button.dart | 2 +- 9 files changed, 126 insertions(+), 165 deletions(-) diff --git a/lib/controllers/providers/api/notifications_provider.dart b/lib/controllers/providers/api/notifications_provider.dart index 6da2120d..5ddf1103 100644 --- a/lib/controllers/providers/api/notifications_provider.dart +++ b/lib/controllers/providers/api/notifications_provider.dart @@ -163,7 +163,7 @@ class NotificationsProvider extends ChangeNotifier { /// Returns true if successful, false otherwise Future markOneAsRead({required String notificationId}) async { try { - final response = await ApiManager.instance.get("${URLs.markOneNotificationAsRead}?NotificationId=$notificationId"); + final response = await ApiManager.instance.get("${URLs.markOneNotificationAsRead}?NotificationId=$notificationId", enableToastMessage: false); if (response.statusCode >= 200 && response.statusCode < 300) { // Update the specific notification in the local list to read status @@ -215,4 +215,4 @@ class NotificationsProvider extends ChangeNotifier { return null; } } -} \ No newline at end of file +} diff --git a/lib/models/system_notification_model.dart b/lib/models/system_notification_model.dart index bb64637d..492c3575 100644 --- a/lib/models/system_notification_model.dart +++ b/lib/models/system_notification_model.dart @@ -12,6 +12,7 @@ class SystemNotificationModel { String? createdOn; String? modifiedOn; String? priorityName; + int? priorityValue; String? statusName; int? transactionType; String? moduleName; @@ -38,6 +39,7 @@ class SystemNotificationModel { this.createdOn, this.modifiedOn, this.priorityName, + this.priorityValue, this.transactionType, this.statusName, this.moduleName, @@ -65,6 +67,7 @@ class SystemNotificationModel { createdOn = json['createdDate'] ?? json['createdOn']; modifiedOn = json['modifiedOn']; priorityName = json['priorityName']; + priorityValue = json['priorityValue']; statusName = json['statusName']; transactionType = json['transactionType']; moduleName = json['moduleName']; @@ -94,6 +97,7 @@ class SystemNotificationModel { data['createdOn'] = createdOn; data['modifiedOn'] = modifiedOn; data['priorityName'] = priorityName; + data['priorityValue'] = priorityValue; data['priority'] = priorityName; data['statusName'] = statusName; data['transactionType'] = transactionType; @@ -120,6 +124,7 @@ class SystemNotificationModel { data['createdOn'] = createdOn; data['modifiedOn'] = modifiedOn; data['priorityName'] = priorityName; + data['priorityValue'] = priorityValue; data['statusName'] = statusName; data['transactionType'] = transactionType; data['moduleName'] = moduleName; diff --git a/lib/modules/notification_setting/view/widgets/notification_preference_card.dart b/lib/modules/notification_setting/view/widgets/notification_preference_card.dart index c3dcdcb1..773eff4c 100644 --- a/lib/modules/notification_setting/view/widgets/notification_preference_card.dart +++ b/lib/modules/notification_setting/view/widgets/notification_preference_card.dart @@ -30,14 +30,14 @@ class NotificationPreferenceCard extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4.toScreenHeight, children: [ if(isMandatory) ...[ 4.height, Text( 'You cannot update this notification as it is mandatory.'.addTranslation, - style: AppTextStyle.body2.copyWith( + style: AppTextStyle.tiny.copyWith( color: AppColor.red30, - fontSize: 10.toScreenWidth, ), ), ], @@ -54,27 +54,23 @@ class NotificationPreferenceCard extends StatelessWidget { _buildCompactToggle(context, isEnabled, isMandatory), ], ), - 4.height, InfoTextWidget( label: context.translation.notificationModule, value: template.moduleName ?? '', ), - 4.height, InfoTextWidget( label: 'Sub Module'.addTranslation, value: template.subModuleName ?? '', ), - 4.height, if (template.templateChannels != null && template.templateChannels!.isNotEmpty) ...[ InfoTextLabelWidget( label: '${context.translation.channelPreference} :', ), - 8.height, _buildChannelChips(context, isMandatory), ], ], - ).toShadowContainer(context,padding: 16); + ).toShadowContainer(context); } Widget _buildCompactToggle(BuildContext context, bool isEnabled, bool isMandatory) { diff --git a/lib/new_views/app_style/app_color.dart b/lib/new_views/app_style/app_color.dart index 9933673e..396caf88 100644 --- a/lib/new_views/app_style/app_color.dart +++ b/lib/new_views/app_style/app_color.dart @@ -418,6 +418,42 @@ class AppColor { } } + //0 critical, 1 high, 2 low, 3 medium, + + static Color getNotificationPriorityStatusTextColor(BuildContext context, int id) { + switch (id) { + //low priority + case 2: + return greenStatusColor; + case 3: + return greenStatusTextColor; + case 0: + return redStatusTextColor; + case 1: + return redStatusTextColor; + default: + return defaultStatusTextColor(context); + } + } + + + static Color getNotificationPriorityStatusColor(BuildContext context, int id) { + switch (id) { + //low priority + case 2: + return greenStatusColor; + case 0: + return redStatusColor; + //high priority + case 1: + return redStatusColor; + case 3: + return greenStatusColor; + default: + return defaultStatusColor(context); + } + } + static Color getPriorityStatusColor(BuildContext context, int id) { switch (id) { //low priority diff --git a/lib/views/pages/user/notifications/notification_filter_page.dart b/lib/views/pages/user/notifications/notification_filter_page.dart index 334f94a6..ef48a331 100644 --- a/lib/views/pages/user/notifications/notification_filter_page.dart +++ b/lib/views/pages/user/notifications/notification_filter_page.dart @@ -98,7 +98,7 @@ class _NotificationFilterPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: AppColor.scaffold2Background(context), + // backgroundColor: AppColor.scaffold2Background(context), appBar: DefaultAppBar( title: context.translation.filter, actions: [ @@ -126,17 +126,16 @@ class _NotificationFilterPageState extends State { body: Column( children: [ SingleChildScrollView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Filter by Priority InfoHeader16Widget("Priority"), 8.height, SingleItemDropDownMenu( context: context, - showAsBottomSheet: true, - showShadow: false, + showAsFullScreenDialog: true, + showShadow: true, title: "Select Priority", initialValue: selectedPriority, onSelect: (value) { @@ -151,15 +150,15 @@ class _NotificationFilterPageState extends State { }, ), - 16.height, + 12.height, // Filter by Module InfoHeader16Widget("Module"), 8.height, SingleItemDropDownMenu( context: context, - showAsBottomSheet: true, - showShadow: false, + showAsFullScreenDialog: true, + showShadow: true, title: "Select Module", initialValue: selectedModule, onSelect: (value) { @@ -177,15 +176,15 @@ class _NotificationFilterPageState extends State { }, ), - 16.height, + 12.height, // Filter by SubModule - InfoHeader16Widget( "SubModule"), + InfoHeader16Widget("SubModule"), 8.height, SingleItemDropDownMenu( context: context, - showAsBottomSheet: true, - showShadow: false, + showAsFullScreenDialog: true, + showShadow: true, title: "Select SubModule", initialValue: selectedSubModule, enabled: selectedModule?.subModules?.isNotEmpty ?? false, @@ -202,10 +201,10 @@ class _NotificationFilterPageState extends State { }, ), - 16.height, + 12.height, // Date Range Filter - context.translation.pickDate.heading5(context).custom(color: AppColor.textStyleColor(context)), + InfoHeader16Widget(context.translation.pickDate), 8.height, Row( children: [ @@ -239,7 +238,6 @@ class _NotificationFilterPageState extends State { ], ), ).expanded, - 16.height, FooterActionButton.footerContainer( context: context, child: AppFilledButton( diff --git a/lib/views/pages/user/notifications/notifications_list.dart b/lib/views/pages/user/notifications/notifications_list.dart index b6549706..051593f4 100644 --- a/lib/views/pages/user/notifications/notifications_list.dart +++ b/lib/views/pages/user/notifications/notifications_list.dart @@ -5,6 +5,7 @@ import 'package:provider/provider.dart'; import 'package:test_sa/controllers/notification/firebase_notification_manger.dart'; import 'package:test_sa/controllers/providers/api/notifications_provider.dart'; import 'package:test_sa/extensions/context_extension.dart'; +import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/models/system_notification_model.dart'; import 'package:test_sa/views/pages/user/notifications/widgets/notification_card_redesigned.dart'; @@ -27,22 +28,17 @@ class NotificationsList extends StatelessWidget { nextPage: nextPage, onLazyLoad: onLazyLoad, child: ListView.separated( - // padding: const EdgeInsets.all(16), itemCount: notifications.length, - separatorBuilder: (context, itemIndex) => const SizedBox(height: 12), + separatorBuilder: (context, itemIndex) => 12.height, itemBuilder: (context, itemIndex) { final notification = notifications[itemIndex]; return NotificationCardRedesigned( notification: notification, onTap: () async { - // Mark notification as read when tapped - log('i am tapped with id ${notification.toJson()}'); if (notification.id != null && !(notification.readed ?? false)) { final provider = Provider.of(context, listen: false); await provider.markOneAsRead(notificationId: notification.id!); } - - // Navigate to notification details FirebaseNotificationManger.handleMessage(context, notification.toNotificationJson()); }, ); diff --git a/lib/views/pages/user/notifications/notifications_page.dart b/lib/views/pages/user/notifications/notifications_page.dart index c1207f19..8e4a68ea 100644 --- a/lib/views/pages/user/notifications/notifications_page.dart +++ b/lib/views/pages/user/notifications/notifications_page.dart @@ -16,7 +16,6 @@ import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; import 'package:test_sa/views/pages/user/notifications/notification_filter_page.dart'; import 'package:test_sa/views/pages/user/notifications/notifications_list.dart'; import 'package:test_sa/views/pages/user/notifications/widgets/notification_tab_button.dart'; -import 'package:test_sa/views/widgets/loaders/app_loading.dart'; import 'dart:async'; import '../../../widgets/loaders/loading_manager.dart'; @@ -80,7 +79,6 @@ class _NotificationsPageState extends State with TickerProvid ); } - /// Handle search with debounce void _onSearchChanged(String value) { setState(() { _searchQuery = value; @@ -141,7 +139,6 @@ class _NotificationsPageState extends State with TickerProvid _userProvider = Provider.of(context); return Scaffold( - backgroundColor: AppColor.scaffold2Background(context), appBar: DefaultAppBar( title: context.translation.notifications, actions: [ @@ -181,37 +178,26 @@ class _NotificationsPageState extends State with TickerProvid ), body: SafeArea( child: Column( + spacing: 12.toScreenHeight, crossAxisAlignment: CrossAxisAlignment.end, children: [ - // Search Section - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - 8.height, - AppTextFormField( - controller: _searchController, - labelText: context.translation.search, - showShadow: false, - showBorder: true, - prefixIconData: Icons.search, - hintText: 'Search by Title or content'.addTranslation, - hintStyle: AppTextStyles.tinyFont.copyWith(color: AppColor.labelTextStyleColor(context)), - contentPadding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16), - onChange: (value) { - _onSearchChanged(value); - }, - ), - ], + 12.height, + AppTextFormField( + controller: _searchController, + labelText: context.translation.search, + showShadow: false, + showBorder: true, + prefixIconData: Icons.search, + onChange: (value) { + _onSearchChanged(value); + }, ), - - 16.height, - - // Notification Tabs + // 8.height, Row( children: [ Expanded( child: SizedBox( - height: 35.toScreenHeight, + height: 30.toScreenHeight, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: 3, @@ -238,9 +224,10 @@ class _NotificationsPageState extends State with TickerProvid 8.width, // Sort & Filter Container Container( - height: 35.toScreenHeight, - padding: const EdgeInsets.symmetric(horizontal: 8), + height: 30.toScreenHeight, + padding: const EdgeInsets.symmetric(horizontal: 6), decoration: BoxDecoration( + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 14, offset: const Offset(0, 0), spreadRadius: 0)], color: AppColor.background(context), borderRadius: BorderRadius.circular(6), border: Border.all(color: AppColor.borderColor(context)), @@ -287,7 +274,7 @@ class _NotificationsPageState extends State with TickerProvid _loadNotifications(); } }), - // Active Filters Badge + // Active Filters Badge we are not using custom badge widget here as it need more customizations if (_activeFilterCount > 0) Positioned( right: -8, @@ -321,15 +308,12 @@ class _NotificationsPageState extends State with TickerProvid ), ], ), - - 16.height, - + // 8.height, // Notifications List Expanded( child: LoadingManager( isLoading: (_notificationsProvider.isLoading) && _notificationsProvider.notifications.isEmpty, isFailedLoading: false, - // Don't show error state, show no data found instead stateCode: _notificationsProvider.stateCode, onRefresh: () async { _notificationsProvider.reset(); @@ -352,7 +336,7 @@ class _NotificationsPageState extends State with TickerProvid ), ), ], - ).paddingOnly(start: 16, end: 16), + ).paddingOnly(start: 12, end: 12), ), ); } diff --git a/lib/views/pages/user/notifications/widgets/notification_card_redesigned.dart b/lib/views/pages/user/notifications/widgets/notification_card_redesigned.dart index 5ea5790b..36184dc9 100644 --- a/lib/views/pages/user/notifications/widgets/notification_card_redesigned.dart +++ b/lib/views/pages/user/notifications/widgets/notification_card_redesigned.dart @@ -1,11 +1,14 @@ import 'package:flutter/material.dart'; import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/extensions/string_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; +import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/models/system_notification_model.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/views/widgets/item_views/info_date_widget.dart'; import 'package:test_sa/views/widgets/item_views/info_header_widget.dart'; import 'package:test_sa/views/widgets/item_views/info_text_widget.dart'; +import 'package:test_sa/views/widgets/requests/request_status.dart'; class NotificationCardRedesigned extends StatelessWidget { final SystemNotificationModel notification; @@ -20,112 +23,55 @@ class NotificationCardRedesigned extends StatelessWidget { @override Widget build(BuildContext context) { final isUnread = !(notification.readed ?? false); - return Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: AppColor.background(context), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: AppColor.borderColor(context)), - ), - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(8), - child: Column( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4.toScreenHeight, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (notification.priorityName != null && notification.priorityValue != null) + StatusLabel( + label: notification.priorityName!, + radius: 4, + textColor: AppColor.getNotificationPriorityStatusTextColor(context, notification.priorityValue!), + backgroundColor: AppColor.getNotificationPriorityStatusColor(context, notification.priorityValue!), + ), + if (notification.createdOn?.isNotEmpty ?? false) InfoDateWidget(notification.createdOn ?? ""), + ], + ), + Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Priority Badge & Date Row - Row( - children: [ - // Priority Badge - if (notification.priorityName != null && notification.priorityName!.isNotEmpty) - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: AppColor.getRequestStatusColorByName( - context, - notification.priorityName!, - ), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - notification.priorityName!, - style: AppTextStyles.tinyFont2.copyWith( - color: AppColor.getRequestStatusTextColorByName( - context, - notification.priorityName!, - ), - fontWeight: FontWeight.w600, - ), - ), - ), - const Spacer(), - - // Date & Time - Top Right - if (notification.createdOn?.isNotEmpty ?? false) - InfoDateWidget(notification.createdOn ?? ""), - ], - ), - 8.height, - - // Title with Unread Dot - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Unread indicator (blue dot) - aligned with title - if (isUnread) - Padding( - padding: const EdgeInsets.only(top: 6, right: 8), - child: Container( - width: 8, - height: 8, - decoration: const BoxDecoration( - color: AppColor.primary10, - shape: BoxShape.circle, - ), - ), - ), - - // Title - Expanded( - child: InfoHeader16Widget( - notification.title ?? '', - ), - ), - ], - ), - 8.height, - if (notification.sourceName?.isNotEmpty ?? false) - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: AppColor.primary10.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - notification.sourceName!, - style: AppTextStyles.tinyFont2.copyWith( + if (isUnread) + Padding( + padding: const EdgeInsets.only(top: 6, right: 8), + child: Container( + width: 8, + height: 8, + decoration: const BoxDecoration( color: AppColor.primary10, - fontWeight: FontWeight.w500, + shape: BoxShape.circle, ), ), ), - if (notification.sourceName?.isNotEmpty ?? false) 8.height, - - // Message Preview - if (notification.text?.isNotEmpty ?? false) - InfoTextLabelWidget( - label: notification.text!, - // style: AppTextStyles.bodyText2.copyWith( - // color: AppColor.textColor(context), - // height: 1.4, - // ), - // maxLines: 3, - // overflow: TextOverflow.ellipsis, + Expanded( + child: InfoHeader16Widget( + notification.title ?? '', ), + ), ], ), - ), - ); + if (notification.sourceName?.isNotEmpty ?? false) + InfoTextWidget( + label: 'Source'.addTranslation, + value: notification.sourceName ?? '', + ), + if (notification.text?.isNotEmpty ?? false) + InfoTextLabelWidget( + label: notification.text!, + ), + ], + ).toShadowContainer(context).onPress(onTap); } - -} \ No newline at end of file +} diff --git a/lib/views/pages/user/notifications/widgets/notification_tab_button.dart b/lib/views/pages/user/notifications/widgets/notification_tab_button.dart index 754871af..64ecff9a 100644 --- a/lib/views/pages/user/notifications/widgets/notification_tab_button.dart +++ b/lib/views/pages/user/notifications/widgets/notification_tab_button.dart @@ -22,7 +22,7 @@ class NotificationTabButton extends StatelessWidget { onTap: onTap, child: Container( alignment: Alignment.center, - padding: const EdgeInsets.symmetric(horizontal: 12), + padding: const EdgeInsets.symmetric(horizontal: 6), decoration: ShapeDecoration( color: AppColor.background(context), shape: RoundedRectangleBorder( From cce4359d7a484cf0889099d517cc62d83b8028a0 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Wed, 1 Jul 2026 16:09:22 +0300 Subject: [PATCH 04/35] android ios notification bundling added. --- .../firebase_notification_manger.dart | 6 +- .../notification/notification_manger.dart | 130 +++++++++++++----- 2 files changed, 98 insertions(+), 38 deletions(-) diff --git a/lib/controllers/notification/firebase_notification_manger.dart b/lib/controllers/notification/firebase_notification_manger.dart index 9866be33..ea860ae0 100644 --- a/lib/controllers/notification/firebase_notification_manger.dart +++ b/lib/controllers/notification/firebase_notification_manger.dart @@ -253,7 +253,11 @@ class FirebaseNotificationManger { if (Platform.isAndroid) { if (message.data["notificationType"] != 'NurseConfirmArrive') { NotificationManger.showNotification( - title: message.notification?.title ?? "", subtext: message.notification?.body ?? "", hashcode: int.tryParse("1234" ?? "") ?? 1, payload: json.encode(message.data), context: context); + title: message.notification?.title ?? "", + subtext: message.notification?.body ?? "", + hashcode: DateTime.now().millisecondsSinceEpoch.hashCode, + payload: json.encode(message.data), + context: context); } } return; diff --git a/lib/controllers/notification/notification_manger.dart b/lib/controllers/notification/notification_manger.dart index ab1df81a..5e85c450 100644 --- a/lib/controllers/notification/notification_manger.dart +++ b/lib/controllers/notification/notification_manger.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; @@ -43,46 +44,71 @@ class NotificationManger { await localNotificationsPlugin.initialize(initializationSettings, onDidReceiveNotificationResponse: onNotificationPressed); } // push new notification - static const AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails( - 'com.hmg.atoms', - 'ATOMS', - channelDescription: 'Push notification service for ATOMS', - importance: Importance.max, - priority: Priority.max, - icon: "@drawable/ic_stat_name", - playSound: true, - channelShowBadge: true, - visibility: NotificationVisibility.public, - enableVibration: true, - groupKey: 'com.hmg.atoms', - ); - - static const DarwinNotificationDetails iosNotificationDetails = DarwinNotificationDetails( - categoryIdentifier: "atoms", - ); + static AndroidNotificationDetails androidNotificationDetails(String? groupId, String? channelId) => AndroidNotificationDetails( + channelId ?? 'com.hmg.atoms', + 'ATOMS', + channelDescription: 'Push notification service for ATOMS', + importance: Importance.max, + priority: Priority.max, + icon: "@drawable/ic_stat_name", + playSound: true, + channelShowBadge: true, + visibility: NotificationVisibility.public, + enableVibration: true, + groupKey: groupId ?? 'com.hmg.atoms', + ); + + static DarwinNotificationDetails iosNotificationDetails(String? threadId) => DarwinNotificationDetails(categoryIdentifier: "atoms", threadIdentifier: threadId); static Future showNotification({required BuildContext context, required String title, required String subtext, required int hashcode, String? payload}) async { - // const AndroidNotificationDetails androidChannel = AndroidNotificationDetails( - // 'com.hmg.atoms', - // 'ATOMS', - // channelDescription: 'Push notification service for ATOMS', - // importance: Importance.max, - // priority: Priority.max, - // icon: "@drawable/ic_stat_name", - // playSound: true, - // channelShowBadge: true, - // visibility: NotificationVisibility.public, - // enableVibration: true, - // groupKey: 'com.hmg.atoms', - // ); - - // const DarwinNotificationDetails iosNotificationDetails = DarwinNotificationDetails( - // categoryIdentifier: "atoms", - // ); - - const NotificationDetails platformChannel = NotificationDetails(android: androidNotificationDetails, iOS: iosNotificationDetails, macOS: iosNotificationDetails); + var dataPayload = json.decode(payload ?? "{}"); + String? groupId = dataPayload["group_id"]; + NotificationDetails platformChannel = NotificationDetails(android: androidNotificationDetails(groupId, groupId), iOS: iosNotificationDetails(groupId), macOS: iosNotificationDetails(groupId)); await localNotificationsPlugin.show(hashcode, title, subtext, platformChannel, payload: payload); + + // If there's a group_id, also show/update the summary notification + if (groupId != null && Platform.isAndroid) { + await _showGroupSummary(groupId); + } + } + + static final Map _groupCounts = {}; + + static Future _showGroupSummary(String groupId) async { + // Increment count for this group + _groupCounts[groupId] = (_groupCounts[groupId] ?? 0) + 1; + final count = _groupCounts[groupId]!; + + // Only show summary if we have 2+ notifications + if (count < 2) return; + + final AndroidNotificationDetails summaryDetails = AndroidNotificationDetails( + 'com.hmg.atoms', + 'ATOMS', + channelDescription: 'Push notification service for ATOMS', + importance: Importance.max, + priority: Priority.max, + icon: "@drawable/ic_stat_name", + groupKey: groupId, + setAsGroupSummary: true, + // THIS IS THE KEY + styleInformation: InboxStyleInformation( + [], + contentTitle: 'ATOMS', + summaryText: 'You have $count new notifications', + ), + ); + + // Use group_id's hashCode as the summary notification ID to ensure uniqueness + final int summaryId = groupId.hashCode; + + await localNotificationsPlugin.show( + summaryId, + 'ATOMS', + '$count new notifications', + NotificationDetails(android: summaryDetails), + ); } static Future scheduleNotification({int? id, String? title, String? body, String? payLoad, required DateTime scheduledNotificationDateTime}) async { @@ -95,7 +121,7 @@ class NotificationManger { scheduledNotificationDateTime, tz.local, ), - const NotificationDetails(android: androidNotificationDetails, iOS: iosNotificationDetails, macOS: iosNotificationDetails), + NotificationDetails(android: androidNotificationDetails(null, null), iOS: iosNotificationDetails(null), macOS: iosNotificationDetails(null)), androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime); } @@ -103,4 +129,34 @@ class NotificationManger { static Future cancelNotificationById(int notificationId) async { await localNotificationsPlugin.cancel(notificationId); } + + /// Show group summary notification (for background isolate) + static Future _showGroupSummaryBackground(String groupId, FlutterLocalNotificationsPlugin plugin) async { + final AndroidNotificationDetails summaryDetails = AndroidNotificationDetails( + 'com.hmg.atoms', + 'ATOMS', + channelDescription: 'Push notification service for ATOMS', + importance: Importance.max, + priority: Priority.max, + icon: "@drawable/ic_stat_name", + groupKey: groupId, + setAsGroupSummary: true, + autoCancel: true, + onlyAlertOnce: true, + styleInformation: InboxStyleInformation( + [], + contentTitle: 'ATOMS', + summaryText: 'New notifications', + ), + ); + + final int summaryId = groupId.hashCode; + + await plugin.show( + summaryId, + 'ATOMS', + 'New notifications', + NotificationDetails(android: summaryDetails), + ); + } } From 0e947cfcc6df805ef36283e3dd468458e4ec5546 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Mon, 6 Jul 2026 10:04:00 +0300 Subject: [PATCH 05/35] improvements --- .../user/notifications/notifications_page.dart | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/views/pages/user/notifications/notifications_page.dart b/lib/views/pages/user/notifications/notifications_page.dart index 8e4a68ea..938f6cae 100644 --- a/lib/views/pages/user/notifications/notifications_page.dart +++ b/lib/views/pages/user/notifications/notifications_page.dart @@ -181,23 +181,22 @@ class _NotificationsPageState extends State with TickerProvid spacing: 12.toScreenHeight, crossAxisAlignment: CrossAxisAlignment.end, children: [ - 12.height, AppTextFormField( controller: _searchController, labelText: context.translation.search, showShadow: false, showBorder: true, - prefixIconData: Icons.search, + textInputAction: TextInputAction.search, + suffixIcon: "search".toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)).paddingOnly(end: 12), onChange: (value) { _onSearchChanged(value); }, ), - // 8.height, Row( children: [ Expanded( child: SizedBox( - height: 30.toScreenHeight, + height: 34.toScreenHeight, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: 3, @@ -222,9 +221,8 @@ class _NotificationsPageState extends State with TickerProvid ), ), 8.width, - // Sort & Filter Container Container( - height: 30.toScreenHeight, + height: 34.toScreenHeight, padding: const EdgeInsets.symmetric(horizontal: 6), decoration: BoxDecoration( boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 14, offset: const Offset(0, 0), spreadRadius: 0)], @@ -234,7 +232,6 @@ class _NotificationsPageState extends State with TickerProvid ), child: Row( children: [ - // Sort Icon Transform.flip( flipY: isAscending, child: 'dashboard/sort_descending' @@ -247,8 +244,7 @@ class _NotificationsPageState extends State with TickerProvid _toggleSort(); }), ), - 14.width, - // Filter Icon + 12.width, Stack( clipBehavior: Clip.none, children: [ @@ -336,7 +332,7 @@ class _NotificationsPageState extends State with TickerProvid ), ), ], - ).paddingOnly(start: 12, end: 12), + ).paddingOnly(start: 16, end: 16, top: 16), ), ); } From c070bd48498f2dffea39c0e38c82c03858875e27 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Mon, 6 Jul 2026 10:09:20 +0300 Subject: [PATCH 06/35] improvements --- .../notifications/notification_filter_page.dart | 11 +---------- .../user/notifications/notifications_page.dart | 16 ++-------------- 2 files changed, 3 insertions(+), 24 deletions(-) diff --git a/lib/views/pages/user/notifications/notification_filter_page.dart b/lib/views/pages/user/notifications/notification_filter_page.dart index ef48a331..ca97a5c0 100644 --- a/lib/views/pages/user/notifications/notification_filter_page.dart +++ b/lib/views/pages/user/notifications/notification_filter_page.dart @@ -126,7 +126,7 @@ class _NotificationFilterPageState extends State { body: Column( children: [ SingleChildScrollView( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -149,10 +149,7 @@ class _NotificationFilterPageState extends State { }); }, ), - 12.height, - - // Filter by Module InfoHeader16Widget("Module"), 8.height, SingleItemDropDownMenu( @@ -175,10 +172,7 @@ class _NotificationFilterPageState extends State { }); }, ), - 12.height, - - // Filter by SubModule InfoHeader16Widget("SubModule"), 8.height, SingleItemDropDownMenu( @@ -200,10 +194,7 @@ class _NotificationFilterPageState extends State { }); }, ), - 12.height, - - // Date Range Filter InfoHeader16Widget(context.translation.pickDate), 8.height, Row( diff --git a/lib/views/pages/user/notifications/notifications_page.dart b/lib/views/pages/user/notifications/notifications_page.dart index 938f6cae..3d146705 100644 --- a/lib/views/pages/user/notifications/notifications_page.dart +++ b/lib/views/pages/user/notifications/notifications_page.dart @@ -234,13 +234,7 @@ class _NotificationsPageState extends State with TickerProvid children: [ Transform.flip( flipY: isAscending, - child: 'dashboard/sort_descending' - .toSvgAsset( - width: 20, - height: 20, - color: AppColor.iconColor(context), - ) - .onPress(() { + child: 'dashboard/sort_descending'.toSvgAsset(width: 20, height: 20, color: AppColor.iconColor(context)).onPress(() { _toggleSort(); }), ), @@ -248,13 +242,7 @@ class _NotificationsPageState extends State with TickerProvid Stack( clipBehavior: Clip.none, children: [ - 'filter' - .toSvgAsset( - width: 20, - height: 20, - color: AppColor.iconColor(context), - ) - .onPress(() async { + 'filter'.toSvgAsset(width: 20, height: 20, color: AppColor.iconColor(context)).onPress(() async { // Open filter page final result = await Navigator.push( context, From e08813bf661180fa2ef666bbef7f4e70e2de038b Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Mon, 6 Jul 2026 10:10:11 +0300 Subject: [PATCH 07/35] improvements --- .../pages/user/notifications/notification_filter_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/views/pages/user/notifications/notification_filter_page.dart b/lib/views/pages/user/notifications/notification_filter_page.dart index ca97a5c0..e10984c9 100644 --- a/lib/views/pages/user/notifications/notification_filter_page.dart +++ b/lib/views/pages/user/notifications/notification_filter_page.dart @@ -209,7 +209,7 @@ class _NotificationFilterPageState extends State { }); }, ).expanded, - 8.width, + 12.width, ADatePicker( label: context.translation.to, from: DateTime(2000), From 5744e6afbf02096329ad55f04252726903807639 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Mon, 6 Jul 2026 10:28:13 +0300 Subject: [PATCH 08/35] improvements --- .../user/notifications/widgets/notification_tab_button.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/views/pages/user/notifications/widgets/notification_tab_button.dart b/lib/views/pages/user/notifications/widgets/notification_tab_button.dart index 64ecff9a..f0cfe869 100644 --- a/lib/views/pages/user/notifications/widgets/notification_tab_button.dart +++ b/lib/views/pages/user/notifications/widgets/notification_tab_button.dart @@ -20,7 +20,8 @@ class NotificationTabButton extends StatelessWidget { Widget build(BuildContext context) { return GestureDetector( onTap: onTap, - child: Container( + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), alignment: Alignment.center, padding: const EdgeInsets.symmetric(horizontal: 6), decoration: ShapeDecoration( @@ -46,7 +47,7 @@ class NotificationTabButton extends StatelessWidget { style: AppTextStyles.tinyFont.copyWith( height: 1, color: isSelected ? AppColor.primary10 : AppColor.filterButtonTextColor(context), - fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, + fontWeight: FontWeight.w500, ), ), ), From 14a32d101ec5fe2e0f90faa02f14e9b19430b124 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Mon, 6 Jul 2026 10:49:42 +0300 Subject: [PATCH 09/35] improvements --- .../notification/firebase_notification_manger.dart | 11 ++++++++--- lib/controllers/notification/notification_manger.dart | 4 +++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/controllers/notification/firebase_notification_manger.dart b/lib/controllers/notification/firebase_notification_manger.dart index ea860ae0..76468734 100644 --- a/lib/controllers/notification/firebase_notification_manger.dart +++ b/lib/controllers/notification/firebase_notification_manger.dart @@ -241,7 +241,7 @@ class FirebaseNotificationManger { } await FirebaseMessaging.instance.setAutoInitEnabled(true); - await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(alert: true, badge: true, sound: true); + await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(alert: true, badge: false, sound: true); FirebaseMessaging.instance.getInitialMessage().then((initialMessage) { if (initialMessage != null) { @@ -252,9 +252,14 @@ class FirebaseNotificationManger { FirebaseMessaging.onMessage.listen((RemoteMessage message) { if (Platform.isAndroid) { if (message.data["notificationType"] != 'NurseConfirmArrive') { + // Extract title and body from notification field first, fallback to data field + final String title = message.notification?.title ?? message.data['title'] ?? 'ATOMS'; + final String body = message.notification?.body ?? message.data['body'] ?? 'New notification'; + + // Always show with our grouping logic NotificationManger.showNotification( - title: message.notification?.title ?? "", - subtext: message.notification?.body ?? "", + title: title, + subtext: body, hashcode: DateTime.now().millisecondsSinceEpoch.hashCode, payload: json.encode(message.data), context: context); diff --git a/lib/controllers/notification/notification_manger.dart b/lib/controllers/notification/notification_manger.dart index 5e85c450..94311ae9 100644 --- a/lib/controllers/notification/notification_manger.dart +++ b/lib/controllers/notification/notification_manger.dart @@ -56,6 +56,7 @@ class NotificationManger { visibility: NotificationVisibility.public, enableVibration: true, groupKey: groupId ?? 'com.hmg.atoms', + setAsGroupSummary: false, ); static DarwinNotificationDetails iosNotificationDetails(String? threadId) => DarwinNotificationDetails(categoryIdentifier: "atoms", threadIdentifier: threadId); @@ -92,7 +93,8 @@ class NotificationManger { icon: "@drawable/ic_stat_name", groupKey: groupId, setAsGroupSummary: true, - // THIS IS THE KEY + onlyAlertOnce: true, + autoCancel: true, styleInformation: InboxStyleInformation( [], contentTitle: 'ATOMS', From 50520d4eb6ec70f35eb66ad7ae1d356a87aad469 Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:40:33 +0300 Subject: [PATCH 10/35] improvements --- lib/controllers/api_routes/urls.dart | 8 ++++---- lib/new_views/common_widgets/app_drawer.dart | 8 ++++---- lib/new_views/pages/settings_page.dart | 15 +++++++++++++++ .../user/notifications/notifications_page.dart | 2 +- .../widgets/notification_card_redesigned.dart | 2 +- 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart index b7e7e519..efd747ca 100644 --- a/lib/controllers/api_routes/urls.dart +++ b/lib/controllers/api_routes/urls.dart @@ -4,12 +4,12 @@ class URLs { static const String appReleaseBuildNumber = "42"; // static const host1 = "https://atomsm.hmg.com"; // production url - static const host1 = "https://atomsmdev.hmg.com"; // local DEV url - // static const host1 = "https://atomsmuat.hmg.com"; // local UAT url + // static const host1 = "https://atomsmdev.hmg.com"; // local DEV url + static const host1 = "https://atomsmuat.hmg.com"; // local UAT url // static final String _baseUrl = "$_host/mobile"; // host local UAT - // static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis - static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM + static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis + // static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM // static final String _baseUrl = "$_host/v4/mobile"; // v4 for Demo module // static final String _baseUrl = "$_host/v5/mobile"; // v5 for data segregation // static final String _baseUrl = "$_host/v6/mobile"; // for asset delivery module diff --git a/lib/new_views/common_widgets/app_drawer.dart b/lib/new_views/common_widgets/app_drawer.dart index bee95876..9634e097 100644 --- a/lib/new_views/common_widgets/app_drawer.dart +++ b/lib/new_views/common_widgets/app_drawer.dart @@ -65,10 +65,10 @@ class AppDrawer extends StatelessWidget { ListView( padding: const EdgeInsets.only(top: 24), children: [ - drawerItem("drawer_notification", context.translation.notifications, context).onPress(() => Navigator.of(context).pushNamed(NotificationsPage.id)), - 18.height, - drawerItem("notification_setting", 'Notification Settings'.addTranslation, context).onPress(() => Navigator.of(context).pushNamed(NotificationSettingPage.id)), - 18.height, + // drawerItem("drawer_notification", context.translation.notifications, context).onPress(() => Navigator.of(context).pushNamed(NotificationsPage.id)), + // 18.height, + // drawerItem("notification_setting", 'Notification Settings'.addTranslation, context).onPress(() => Navigator.of(context).pushNamed(NotificationSettingPage.id)), + // 18.height, // drawerItem("help_center", context.translation.helpCenter, context) /*.onPress(() => Navigator.of(context).pushNamed(HelpCenterPage.id))*/, // 18.height, // drawerItem("rate_us", context.translation.rateUs, context), diff --git a/lib/new_views/pages/settings_page.dart b/lib/new_views/pages/settings_page.dart index 0222afe6..fcca8712 100644 --- a/lib/new_views/pages/settings_page.dart +++ b/lib/new_views/pages/settings_page.dart @@ -9,8 +9,10 @@ import 'package:provider/provider.dart'; import 'package:test_sa/controllers/providers/settings/setting_provider.dart'; import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/extensions/string_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; +import 'package:test_sa/modules/notification_setting/view/notification_setting_page.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/views/widgets/item_views/info_header_widget.dart'; @@ -168,6 +170,19 @@ class _SettingsPageState extends State { ], ); }), + + const Divider().defaultStyle(context), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + InfoHeader16Widget("Notification Settings".addTranslation), + //added this SizedBox to make it in center with the above switches. + SizedBox( + width: 42.toScreenWidth, + child: "arrow_right".toSvgAsset(width: 20, color: AppColor.primary10), + ) + ], + ).onPress(() => Navigator.of(context).pushNamed(NotificationSettingPage.id)), ], ).toShadowContainer(context, borderRadius: 16, padding: 16).paddingAll(16), ); diff --git a/lib/views/pages/user/notifications/notifications_page.dart b/lib/views/pages/user/notifications/notifications_page.dart index 3d146705..8025d0f0 100644 --- a/lib/views/pages/user/notifications/notifications_page.dart +++ b/lib/views/pages/user/notifications/notifications_page.dart @@ -178,7 +178,7 @@ class _NotificationsPageState extends State with TickerProvid ), body: SafeArea( child: Column( - spacing: 12.toScreenHeight, + spacing: 16.toScreenHeight, crossAxisAlignment: CrossAxisAlignment.end, children: [ AppTextFormField( diff --git a/lib/views/pages/user/notifications/widgets/notification_card_redesigned.dart b/lib/views/pages/user/notifications/widgets/notification_card_redesigned.dart index 36184dc9..b8d96c0e 100644 --- a/lib/views/pages/user/notifications/widgets/notification_card_redesigned.dart +++ b/lib/views/pages/user/notifications/widgets/notification_card_redesigned.dart @@ -25,7 +25,7 @@ class NotificationCardRedesigned extends StatelessWidget { final isUnread = !(notification.readed ?? false); return Column( crossAxisAlignment: CrossAxisAlignment.start, - spacing: 4.toScreenHeight, + spacing: 6.toScreenHeight, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, From c5eeacffb7b9ead01d48bdec4b601f275cb4a8a0 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Mon, 6 Jul 2026 14:45:40 +0300 Subject: [PATCH 11/35] improvements --- lib/controllers/api_routes/urls.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart index b7e7e519..efd747ca 100644 --- a/lib/controllers/api_routes/urls.dart +++ b/lib/controllers/api_routes/urls.dart @@ -4,12 +4,12 @@ class URLs { static const String appReleaseBuildNumber = "42"; // static const host1 = "https://atomsm.hmg.com"; // production url - static const host1 = "https://atomsmdev.hmg.com"; // local DEV url - // static const host1 = "https://atomsmuat.hmg.com"; // local UAT url + // static const host1 = "https://atomsmdev.hmg.com"; // local DEV url + static const host1 = "https://atomsmuat.hmg.com"; // local UAT url // static final String _baseUrl = "$_host/mobile"; // host local UAT - // static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis - static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM + static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis + // static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM // static final String _baseUrl = "$_host/v4/mobile"; // v4 for Demo module // static final String _baseUrl = "$_host/v5/mobile"; // v5 for data segregation // static final String _baseUrl = "$_host/v6/mobile"; // for asset delivery module From 5acfe5903029315c42b64b4687131cbcd39460b1 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Mon, 6 Jul 2026 15:06:48 +0300 Subject: [PATCH 12/35] improvements --- lib/modules/cm_module/cm_detail_page.dart | 2 +- lib/new_views/pages/settings_page.dart | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/modules/cm_module/cm_detail_page.dart b/lib/modules/cm_module/cm_detail_page.dart index 1bc46d8a..afa9ea03 100644 --- a/lib/modules/cm_module/cm_detail_page.dart +++ b/lib/modules/cm_module/cm_detail_page.dart @@ -121,7 +121,7 @@ class _CMDetailPageState extends State { }), if (isNurse) IconButton( - icon: 'qr'.toSvgAsset(height: 24, width: 24), + icon: 'qr'.toSvgAsset(height: 24, width: 24,color: AppColor.icon2Color(context)), onPressed: () { ServiceRequestBottomSheet.getQRCodeBottomSheet(context: context); }, diff --git a/lib/new_views/pages/settings_page.dart b/lib/new_views/pages/settings_page.dart index fcca8712..1c072029 100644 --- a/lib/new_views/pages/settings_page.dart +++ b/lib/new_views/pages/settings_page.dart @@ -176,11 +176,7 @@ class _SettingsPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ InfoHeader16Widget("Notification Settings".addTranslation), - //added this SizedBox to make it in center with the above switches. - SizedBox( - width: 42.toScreenWidth, - child: "arrow_right".toSvgAsset(width: 20, color: AppColor.primary10), - ) + "arrow_right".toSvgAsset(width: 18, color: AppColor.icon2Color(context)) ], ).onPress(() => Navigator.of(context).pushNamed(NotificationSettingPage.id)), ], From 335a29884378614993a35f2ddecca3d302583596 Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Mon, 20 Jul 2026 09:54:21 +0300 Subject: [PATCH 13/35] bug fixes --- lib/new_views/app_style/app_color.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/new_views/app_style/app_color.dart b/lib/new_views/app_style/app_color.dart index 396caf88..e482060c 100644 --- a/lib/new_views/app_style/app_color.dart +++ b/lib/new_views/app_style/app_color.dart @@ -424,9 +424,9 @@ class AppColor { switch (id) { //low priority case 2: - return greenStatusColor; - case 3: return greenStatusTextColor; + case 3: + return blueStatusTextColor; case 0: return redStatusTextColor; case 1: @@ -448,7 +448,7 @@ class AppColor { case 1: return redStatusColor; case 3: - return greenStatusColor; + return blueStatusColor; default: return defaultStatusColor(context); } From df100249754cf3083a849fb4665dd98b99748fac Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:23:31 +0300 Subject: [PATCH 14/35] bug fixes --- lib/controllers/api_routes/urls.dart | 1 + .../firebase_notification_manger.dart | 10 ++++ .../providers/api/notifications_provider.dart | 57 ++++++++++++++++--- .../widgets/app_bar_widget.dart | 30 +++++++++- .../notifications/notifications_page.dart | 6 +- 5 files changed, 91 insertions(+), 13 deletions(-) diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart index d427ca8d..501df4b8 100644 --- a/lib/controllers/api_routes/urls.dart +++ b/lib/controllers/api_routes/urls.dart @@ -247,6 +247,7 @@ class URLs { static get getNotificationInbox => "$_baseUrl/NotificationNew/Inbox"; // get notifications with filters static get markAllNotificationsAsRead => "$_baseUrl/NotificationNew/MarkAllRead"; // mark all as read static get markOneNotificationAsRead => "$_baseUrl/NotificationNew/MarkAsReadOneNotification"; // mark one notification as read + static get getUnreadNotificationCount => "$_baseUrl/NotificationNew/GetUnread"; // mark one notification as read // Notification Filter Lookups static get getNotificationPriorityLookup => "$_baseUrl/Lookups/GetLookup?lookupEnum=5021"; // notification priority filter diff --git a/lib/controllers/notification/firebase_notification_manger.dart b/lib/controllers/notification/firebase_notification_manger.dart index 76468734..361a29e1 100644 --- a/lib/controllers/notification/firebase_notification_manger.dart +++ b/lib/controllers/notification/firebase_notification_manger.dart @@ -6,7 +6,9 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:google_api_availability/google_api_availability.dart'; import 'package:huawei_push/huawei_push.dart' as h_push; +import 'package:provider/provider.dart'; import 'package:test_sa/controllers/notification/notification_manger.dart'; +import 'package:test_sa/controllers/providers/api/notifications_provider.dart'; import 'package:test_sa/models/device/device_transfer.dart'; import 'package:test_sa/models/new_models/gas_refill_model.dart'; import 'package:test_sa/modules/cm_module/cm_detail_page.dart'; @@ -263,6 +265,14 @@ class FirebaseNotificationManger { hashcode: DateTime.now().millisecondsSinceEpoch.hashCode, payload: json.encode(message.data), context: context); + + // Update unread notification count badge when new notification arrives + try { + final notificationsProvider = Provider.of(context, listen: false); + notificationsProvider.getUnreadCount(); + } catch (e) { + log('Error updating notification count: $e'); + } } } return; diff --git a/lib/controllers/providers/api/notifications_provider.dart b/lib/controllers/providers/api/notifications_provider.dart index 5ddf1103..7b9d5188 100644 --- a/lib/controllers/providers/api/notifications_provider.dart +++ b/lib/controllers/providers/api/notifications_provider.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:developer'; import 'package:flutter/cupertino.dart'; import 'package:http/http.dart'; @@ -120,8 +121,6 @@ class NotificationsProvider extends ChangeNotifier { } } - /// Mark all notifications as read - /// Returns a map with success status and message from API Future> markAllAsRead({required String userId}) async { try { final Map body = {"userId": userId}; @@ -159,8 +158,6 @@ class NotificationsProvider extends ChangeNotifier { } } - /// Mark one notification as read - /// Returns true if successful, false otherwise Future markOneAsRead({required String notificationId}) async { try { final response = await ApiManager.instance.get("${URLs.markOneNotificationAsRead}?NotificationId=$notificationId", enableToastMessage: false); @@ -188,11 +185,53 @@ class NotificationsProvider extends ChangeNotifier { } } - /// return -2 if request in progress - /// return -1 if error happen when sending request - /// return state code if request complete may be 200, 404 or 403 - /// for more details check http state manager - /// lib\controllers\http_status_manger\http_status_manger.dart + Future getUnreadCount() async { + int? count; + try { + final response = await ApiManager.instance.get(URLs.getUnreadNotificationCount, enableToastMessage: false); + if (response.statusCode >= 200 && response.statusCode < 300) { + final responseData = json.decode(response.body); + log('count response :: $responseData'); + count = responseData['data']; + // Update the totalNotificationUnread property + totalNotificationUnread = count ?? 0; + notifyListeners(); + } + return count; + } catch (error) { + print('Error getting unread notification count: $error'); + return count; + } + } + + /// Manually reset the unread count to zero + /// Useful when you want to clear the badge without making an API call + void resetUnreadCount() { + totalNotificationUnread = 0; + notifyListeners(); + } + + /// Update unread count manually (e.g., from push notification) + /// Useful when you receive a notification and want to increment the badge + void updateUnreadCount(int newCount) { + totalNotificationUnread = newCount; + notifyListeners(); + } + + /// Increment unread count by 1 (e.g., when receiving a new push notification) + void incrementUnreadCount() { + totalNotificationUnread++; + notifyListeners(); + } + + /// Decrement unread count by 1 (e.g., when marking one as read locally) + void decrementUnreadCount() { + if (totalNotificationUnread > 0) { + totalNotificationUnread--; + notifyListeners(); + } + } + Future?> getRecentNotifications({ required String host, required User user, diff --git a/lib/dashboard_latest/widgets/app_bar_widget.dart b/lib/dashboard_latest/widgets/app_bar_widget.dart index c2865647..78ffe1a0 100644 --- a/lib/dashboard_latest/widgets/app_bar_widget.dart +++ b/lib/dashboard_latest/widgets/app_bar_widget.dart @@ -1,7 +1,10 @@ +import 'dart:developer'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:test_sa/controllers/api_routes/api_manager.dart'; +import 'package:test_sa/controllers/providers/api/notifications_provider.dart'; import 'package:test_sa/controllers/providers/settings/setting_provider.dart'; import 'package:test_sa/dashboard_latest/dashboard_provider.dart'; import 'package:test_sa/extensions/context_extension.dart'; @@ -10,6 +13,7 @@ import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/helper/utils.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; +import 'package:test_sa/new_views/common_widgets/custom_badge.dart'; import 'package:test_sa/views/pages/user/notifications/notifications_page.dart'; import 'package:test_sa/views/pages/user/notifications/unread_chat_list.dart'; @@ -38,6 +42,8 @@ class _AppBarWidgetState extends State with TickerProviderStateMix super.initState(); // Profile section animation (left to right with fade) + getUnreadCount(); + _leftController = AnimationController( duration: const Duration(milliseconds: 600), vsync: this, @@ -95,6 +101,12 @@ class _AppBarWidgetState extends State with TickerProviderStateMix super.dispose(); } + Future getUnreadCount () async{ + NotificationsProvider notificationsProvider = Provider.of(context,listen: false); + log('i am initstate'); + await notificationsProvider.getUnreadCount(); + } + @override Widget build(BuildContext context) { return AppBar( @@ -253,9 +265,21 @@ class _AppBarWidgetState extends State with TickerProviderStateMix }), 16.width, ], - ("dashboard/notification").toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)).paddingOnly(top: 0, end: 0).onPress(() { - Navigator.of(context).pushNamed(NotificationsPage.id); - }), + // Notification icon with unread count badge + Consumer( + builder: (context, notificationsProvider, child) { + final unreadCount = notificationsProvider.totalNotificationUnread; + return CustomBadge( + width: 20, + height: 20, + positionT: -10, + value: unreadCount, + child: ("dashboard/notification").toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)).paddingOnly(top: 0, end: 0), + ).onPress(() { + Navigator.of(context).pushNamed(NotificationsPage.id); + }); + }, + ), ], ), ), diff --git a/lib/views/pages/user/notifications/notifications_page.dart b/lib/views/pages/user/notifications/notifications_page.dart index 8025d0f0..d2510879 100644 --- a/lib/views/pages/user/notifications/notifications_page.dart +++ b/lib/views/pages/user/notifications/notifications_page.dart @@ -36,7 +36,7 @@ class _NotificationsPageState extends State with TickerProvid final TextEditingController _searchController = TextEditingController(); int _selectedTabIndex = 0; // 0: All, 1: Unread, 2: Read String _searchQuery = ''; - bool isAscending = false; + bool isAscending = true; NotificationFilterModel? _activeFilters; Timer? _debounceTimer; @@ -77,6 +77,10 @@ class _NotificationsPageState extends State with TickerProvid resetProvider: true, filters: _activeFilters, ); + + // Refresh the unread count after loading notifications + // This updates the badge on the app bar + await notificationsProvider.getUnreadCount(); } void _onSearchChanged(String value) { From 4230a0dd3bec5543ce9fc4a3f5d0133a45f11022 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Wed, 22 Jul 2026 15:55:33 +0300 Subject: [PATCH 15/35] notification redirection improvements --- .../firebase_notification_manger.dart | 100 ++++++++++-------- 1 file changed, 56 insertions(+), 44 deletions(-) diff --git a/lib/controllers/notification/firebase_notification_manger.dart b/lib/controllers/notification/firebase_notification_manger.dart index 361a29e1..c80a0d8f 100644 --- a/lib/controllers/notification/firebase_notification_manger.dart +++ b/lib/controllers/notification/firebase_notification_manger.dart @@ -9,15 +9,23 @@ import 'package:huawei_push/huawei_push.dart' as h_push; import 'package:provider/provider.dart'; import 'package:test_sa/controllers/notification/notification_manger.dart'; import 'package:test_sa/controllers/providers/api/notifications_provider.dart'; +import 'package:test_sa/models/all_requests_and_count_model.dart'; import 'package:test_sa/models/device/device_transfer.dart'; import 'package:test_sa/models/new_models/gas_refill_model.dart'; import 'package:test_sa/modules/cm_module/cm_detail_page.dart'; import 'package:test_sa/modules/cx_module/survey/survey_page.dart'; +import 'package:test_sa/modules/demo_module/demo_detail_view_page.dart'; +import 'package:test_sa/modules/incident_module/incident_detail_page.dart'; +import 'package:test_sa/modules/internal_audit_module/pages/equipment_internal_audit/equipment_internal_audit_detail_page.dart'; +import 'package:test_sa/modules/internal_audit_module/pages/system_internal_audit/system_internal_audit_detail_page.dart'; +import 'package:test_sa/modules/loan_module/pages/loan_equipment_detail_page.dart'; +import 'package:test_sa/modules/medical_gas_inspection/medical_gas_inspection_detail_page.dart'; import 'package:test_sa/modules/pm_module/ppm_wo/ppm_details_page.dart'; import 'package:test_sa/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart'; import 'package:test_sa/modules/tm_module/device_transfer/device_transfer_details.dart'; import 'package:test_sa/modules/tm_module/tasks/task_request_detail_view.dart'; import 'package:test_sa/modules/tm_module/gas_refill/gas_refill_details.dart'; +import 'package:test_sa/modules/traf_module/traf_request_detail_page.dart'; import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; import 'package:test_sa/views/widgets/loaders/no_data_found.dart'; @@ -131,31 +139,11 @@ class FirebaseNotificationManger { return; } - // PPM=1, - // ServiceRequestEngineer = 3, - // AssetTransfer=7, - // SparePartTransaction= 8, - // GasRefill=9, - // TechnicalRetirmentWO = 11, - // Recurrent = 12, switch (transactionType) { case "1": - serviceClass = PpmDetailsPage(requestId: int.parse(messageData["requestNumber"].toString())); - break; - //these three request are same corrective maintenance.... - case "3": - serviceClass = CMDetailPage(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1); - break; - case "8": - serviceClass = CMDetailPage(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1); - break; - case "11": serviceClass = CMDetailPage(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1); break; - case "7": - serviceClass = DeviceTransferDetails(model: DeviceTransfer(id: int.parse(messageData["requestNumber"].toString())), moduleId: 3); - break; - case "9": + case "2": serviceClass = GasRefillDetailsPage( priority: messageData["priority"], date: messageData["createdOn"], @@ -163,37 +151,61 @@ class FirebaseNotificationManger { model: GasRefillModel(id: int.parse(messageData["requestNumber"].toString())), ); break; - case "12": + case "3": + serviceClass = DeviceTransferDetails(model: DeviceTransfer(id: int.parse(messageData["requestNumber"].toString())), moduleId: 3); + break; + case "4": + serviceClass = PpmDetailsPage(requestId: int.parse(messageData["requestNumber"].toString())); + break; + case "5": serviceClass = RecurrentWorkOrderView(taskId: int.parse(messageData["requestNumber"].toString())); + break; + case "6": + serviceClass = TaskRequestDetailsView( + taskId: int.parse(messageData["requestNumber"].toString()), + requestDetails: RequestsDetails(nameOfType: messageData["sourceName"], status: messageData["statusName"], priority: messageData["priorityName"], date: messageData["createdDate"]), + moduleId: 6); + break; + case "7": + serviceClass = TaskRequestDetailsView( + taskId: int.parse(messageData["requestNumber"].toString()), + requestDetails: RequestsDetails(nameOfType: messageData["sourceName"], status: messageData["statusName"], priority: messageData["priorityName"], date: messageData["createdDate"]), + moduleId: 7); + break; + // case "8": + // serviceClass = AssetInventoryDetailView(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1); + // break; + case "9": + serviceClass = TrafRequestDetailPage(trafId: int.parse(messageData["requestNumber"].toString())); + break; + case "10": + serviceClass = EquipmentInternalAuditDetailPage(auditId: int.parse(messageData["requestNumber"].toString())); + break; + case "11": + serviceClass = SystemInternalAuditDetailPage(auditId: int.parse(messageData["requestNumber"].toString())); + break; + case "12": + serviceClass = LoanEquipmentDetailPage(loanId: int.parse(messageData["requestNumber"].toString())); + break; + // case "13": + // serviceClass = AssetDeliveryPage(requestId: int.parse(messageData["requestNumber"].toString())); + // break; + case "14": + serviceClass = MedicalGasInspectionDetailPage(inspectionId: int.parse(messageData["requestNumber"].toString()), prority: messageData["priorityName"]); + break; + case "15": + serviceClass = DemoDetailViewPage(demoId: int.parse(messageData["requestNumber"].toString()), moduleId: 15); + break; + case "16": + serviceClass = IncidentDetailPage(incidentId: int.parse(messageData["requestNumber"].toString())); + break; case "17": serviceClass = SurveyPage(surveyId: int.parse(messageData["requestNumber"].toString())); - //Didn't handle task request yet... - // case 6: - // serviceClass = TaskRequestDetailsView( - // taskId: int.parse(messageData["requestNumber"].toString()), - // requestDetails: RequestsDetails(nameOfType: messageData["sourceName"], status: messageData["statusName"], priority: messageData["priorityName"], date: messageData["createdDate"])); - // return; default: serviceClass = const Scaffold(appBar: DefaultAppBar(), body: Center(child: NoDataFound())); } - // if (messageData["requestType"] == "Service request to engineer") { - // serviceClass = ServiceRequestDetailMain(requestId: messageData["requestNumber"] ?? ''); - // } else if (messageData["requestType"] == "Gas Refill") { - // serviceClass = GasRefillDetailsPage( - // priority: messageData["priority"], - // date: messageData["createdOn"], - // model: GasRefillModel(id: int.parse(messageData["requestNumber"].toString())), - // ); - // } else if (messageData["requestType"] == "Asset Transfer") { - // serviceClass = DeviceTransferDetails(model: DeviceTransfer(id: int.parse(messageData["requestNumber"].toString()))); - // } else if (messageData["requestType"] == "PPM") { - // serviceClass = PpmDetailsPage(requestId: int.parse(messageData["requestNumber"].toString())); - // } - // else if (data["requestType"] == "WorkOrder") { - // - // } Navigator.of(context).push(MaterialPageRoute(builder: (_) => serviceClass!)); } } From 59a9721fb1c48b39fc5e529ad0759b33c81ac5fb Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Wed, 22 Jul 2026 17:32:20 +0300 Subject: [PATCH 16/35] rename traf -> Assessment of Need & Technology --- lib/new_views/pages/land_page/my_request/my_requests_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/new_views/pages/land_page/my_request/my_requests_page.dart b/lib/new_views/pages/land_page/my_request/my_requests_page.dart index 227d46f1..65e635ea 100644 --- a/lib/new_views/pages/land_page/my_request/my_requests_page.dart +++ b/lib/new_views/pages/land_page/my_request/my_requests_page.dart @@ -606,7 +606,7 @@ class _MyRequestsPageState extends State with SingleTickerProvid case 8: return 'Asset Inventory'; case 9: - return 'TRAF'; + return 'Assessment of Need & Technology'; case 10: return 'Equipment Internal Audit'; case 11: From 1f65b2e0acab3f4220a0b69e2038bc14d10eba35 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Thu, 23 Jul 2026 09:16:39 +0300 Subject: [PATCH 17/35] improvements --- .../providers/api/notifications_provider.dart | 2 - lib/dashboard_latest/dashboard_view.dart | 2 +- .../widgets/app_bar_widget.dart | 37 +++++++------------ .../notifications/notifications_page.dart | 10 +---- 4 files changed, 15 insertions(+), 36 deletions(-) diff --git a/lib/controllers/providers/api/notifications_provider.dart b/lib/controllers/providers/api/notifications_provider.dart index 7b9d5188..2909c363 100644 --- a/lib/controllers/providers/api/notifications_provider.dart +++ b/lib/controllers/providers/api/notifications_provider.dart @@ -191,9 +191,7 @@ class NotificationsProvider extends ChangeNotifier { final response = await ApiManager.instance.get(URLs.getUnreadNotificationCount, enableToastMessage: false); if (response.statusCode >= 200 && response.statusCode < 300) { final responseData = json.decode(response.body); - log('count response :: $responseData'); count = responseData['data']; - // Update the totalNotificationUnread property totalNotificationUnread = count ?? 0; notifyListeners(); } diff --git a/lib/dashboard_latest/dashboard_view.dart b/lib/dashboard_latest/dashboard_view.dart index 6de6e88b..7b240bdb 100644 --- a/lib/dashboard_latest/dashboard_view.dart +++ b/lib/dashboard_latest/dashboard_view.dart @@ -60,7 +60,7 @@ class _DashboardViewState extends State { _dashBoardProvider.setTabs(userType: userProvider.user!.type!, context: context); _dashBoardProvider.getDashBoardCount(usersType: userProvider.user!.type!); _dashBoardProvider.resetRequestDataList(); - + notificationsProvider.getUnreadCount(); if (userProvider.isHouseKeeper) { _dashBoardProvider.currentListIndex = 0; _dashBoardProvider.getRequestDetail( diff --git a/lib/dashboard_latest/widgets/app_bar_widget.dart b/lib/dashboard_latest/widgets/app_bar_widget.dart index a87ec4cb..32968c33 100644 --- a/lib/dashboard_latest/widgets/app_bar_widget.dart +++ b/lib/dashboard_latest/widgets/app_bar_widget.dart @@ -41,9 +41,6 @@ class _AppBarWidgetState extends State with TickerProviderStateMix void initState() { super.initState(); - // Profile section animation (left to right with fade) - getUnreadCount(); - _leftController = AnimationController( duration: const Duration(milliseconds: 600), vsync: this, @@ -101,12 +98,6 @@ class _AppBarWidgetState extends State with TickerProviderStateMix super.dispose(); } - Future getUnreadCount () async{ - NotificationsProvider notificationsProvider = Provider.of(context,listen: false); - log('i am initstate'); - await notificationsProvider.getUnreadCount(); - } - @override Widget build(BuildContext context) { return AppBar( @@ -273,21 +264,19 @@ class _AppBarWidgetState extends State with TickerProviderStateMix }, ), ], - // Notification icon with unread count badge - Consumer( - builder: (context, notificationsProvider, child) { - final unreadCount = notificationsProvider.totalNotificationUnread; - return CustomBadge( - width: 20, - height: 20, - positionT: -10, - value: unreadCount, - child: ("dashboard/notification").toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)).paddingOnly(top: 0, end: 0), - ).onPress(() { - Navigator.of(context).pushNamed(NotificationsPage.id); - }); - }, - ), + Selector( + selector: (_, myModel) => myModel.totalNotificationUnread, // Selects only the userName + builder: (_, totalNotificationUnread, __) { + return CustomBadge( + width: 16, + height: 16, + positionT: -10, + value: totalNotificationUnread, + child: ("dashboard/notification").toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)).paddingOnly(top: 0, end: 0), + ).onPress(() { + Navigator.of(context).pushNamed(NotificationsPage.id); + }); + }).paddingOnly(end: 8, start: 4), ], ), ), diff --git a/lib/views/pages/user/notifications/notifications_page.dart b/lib/views/pages/user/notifications/notifications_page.dart index d2510879..7770164e 100644 --- a/lib/views/pages/user/notifications/notifications_page.dart +++ b/lib/views/pages/user/notifications/notifications_page.dart @@ -70,16 +70,8 @@ class _NotificationsPageState extends State with TickerProvid _activeFilters!.isAscending = isAscending; _activeFilters!.isRead = _selectedTabIndex; // 0 = All, 1 = Unread, 2 = Read - // Reset and fetch fresh data with filters notificationsProvider.reset(); - await notificationsProvider.getSystemNotifications( - user: userProvider.user!, - resetProvider: true, - filters: _activeFilters, - ); - - // Refresh the unread count after loading notifications - // This updates the badge on the app bar + await notificationsProvider.getSystemNotifications(user: userProvider.user!, resetProvider: true, filters: _activeFilters); await notificationsProvider.getUnreadCount(); } From 6cf85d54d4cafd2ec827744057e2b8aabe6dc504 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Thu, 23 Jul 2026 09:20:58 +0300 Subject: [PATCH 18/35] improvements --- lib/dashboard_latest/widgets/app_bar_widget.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/dashboard_latest/widgets/app_bar_widget.dart b/lib/dashboard_latest/widgets/app_bar_widget.dart index 32968c33..39a97f7a 100644 --- a/lib/dashboard_latest/widgets/app_bar_widget.dart +++ b/lib/dashboard_latest/widgets/app_bar_widget.dart @@ -270,7 +270,8 @@ class _AppBarWidgetState extends State with TickerProviderStateMix return CustomBadge( width: 16, height: 16, - positionT: -10, + positionT: -8, + positionR: -8, value: totalNotificationUnread, child: ("dashboard/notification").toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)).paddingOnly(top: 0, end: 0), ).onPress(() { From 3f214ba8a728186a6bc4966fd1e4db982b7d914a Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Thu, 23 Jul 2026 09:52:13 +0300 Subject: [PATCH 19/35] summary count fixed. --- .../notification/notification_manger.dart | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/controllers/notification/notification_manger.dart b/lib/controllers/notification/notification_manger.dart index 94311ae9..af91ccab 100644 --- a/lib/controllers/notification/notification_manger.dart +++ b/lib/controllers/notification/notification_manger.dart @@ -77,9 +77,24 @@ class NotificationManger { static final Map _groupCounts = {}; static Future _showGroupSummary(String groupId) async { - // Increment count for this group - _groupCounts[groupId] = (_groupCounts[groupId] ?? 0) + 1; - final count = _groupCounts[groupId]!; + // Query actual active notifications from the system + final androidPlugin = localNotificationsPlugin.resolvePlatformSpecificImplementation(); + + final List? activeNotifications = await androidPlugin?.getActiveNotifications(); + + // Count notifications that actually exist in this group (exclude summary itself) + int count = 0; + if (activeNotifications != null) { + count = activeNotifications.where((notification) { + return notification.groupKey == groupId && notification.id != groupId.hashCode; // Exclude summary notification + }).length; + } + + // Fallback to in-memory count if query fails (for older Android versions) + if (count == 0) { + _groupCounts[groupId] = (_groupCounts[groupId] ?? 0) + 1; + count = _groupCounts[groupId]!; + } // Only show summary if we have 2+ notifications if (count < 2) return; From 91003cc8a1eea8db63ce384f08af18d6f3dc0154 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Thu, 23 Jul 2026 10:00:52 +0300 Subject: [PATCH 20/35] apk provided to QA team. --- lib/controllers/api_routes/urls.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart index b7088f02..67c024a8 100644 --- a/lib/controllers/api_routes/urls.dart +++ b/lib/controllers/api_routes/urls.dart @@ -3,9 +3,9 @@ class URLs { static const String appReleaseBuildNumber = "47"; - static const host1 = "https://atomsm.hmg.com"; // production url + // static const host1 = "https://atomsm.hmg.com"; // production url // static const host1 = "https://atomsmdev.hmg.com"; // local DEV url - // static const host1 = "https://atomsmuat.hmg.com"; // local UAT url + static const host1 = "https://atomsmuat.hmg.com"; // local UAT url // static final String _baseUrl = "$_host/mobile"; // host local UAT static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis From d71ddcdf1a83f9517c6a5ae4f340609f191c9c0d Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Sun, 26 Jul 2026 16:19:10 +0300 Subject: [PATCH 21/35] ui/ux asset delivery improvements --- .../firebase_notification_manger.dart | 22 ++- .../pages/asset_delivery_item_view.dart | 8 +- .../pages/asset_delivery_line_card_view.dart | 59 ++------ .../pages/asset_delivery_page.dart | 16 +-- .../pages/asset_delivery_table_card_view.dart | 22 +-- .../delivery_inspection_form_view.dart | 126 ++++-------------- .../inpection_bottomsheet.dart | 11 +- .../oracle_user_autocomplete_field.dart | 16 ++- .../pages/history_log_view.dart | 4 +- .../asset_auto_complete_field.dart | 16 +-- .../widgets/date_and_time/date_picker.dart | 2 +- .../widgets/e_signature/e_signature.dart | 11 +- 12 files changed, 95 insertions(+), 218 deletions(-) diff --git a/lib/controllers/notification/firebase_notification_manger.dart b/lib/controllers/notification/firebase_notification_manger.dart index c80a0d8f..20a49c7a 100644 --- a/lib/controllers/notification/firebase_notification_manger.dart +++ b/lib/controllers/notification/firebase_notification_manger.dart @@ -12,6 +12,9 @@ import 'package:test_sa/controllers/providers/api/notifications_provider.dart'; import 'package:test_sa/models/all_requests_and_count_model.dart'; import 'package:test_sa/models/device/device_transfer.dart'; import 'package:test_sa/models/new_models/gas_refill_model.dart'; +import 'package:test_sa/modules/asset_delivery_module/pages/asset_delivery_page.dart'; +import 'package:test_sa/modules/asset_inventory_module/pages/asset_inventory_detail_view.dart'; +import 'package:test_sa/modules/asset_inventory_module/pages/asset_inventory_page.dart'; import 'package:test_sa/modules/cm_module/cm_detail_page.dart'; import 'package:test_sa/modules/cx_module/survey/survey_page.dart'; import 'package:test_sa/modules/demo_module/demo_detail_view_page.dart'; @@ -172,9 +175,9 @@ class FirebaseNotificationManger { requestDetails: RequestsDetails(nameOfType: messageData["sourceName"], status: messageData["statusName"], priority: messageData["priorityName"], date: messageData["createdDate"]), moduleId: 7); break; - // case "8": - // serviceClass = AssetInventoryDetailView(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1); - // break; + case "8": + serviceClass = AssetInventoryPage(sessionId: int.parse(messageData["requestNumber"].toString()), moduleId: int.parse(transactionType)); + break; case "9": serviceClass = TrafRequestDetailPage(trafId: int.parse(messageData["requestNumber"].toString())); break; @@ -187,9 +190,9 @@ class FirebaseNotificationManger { case "12": serviceClass = LoanEquipmentDetailPage(loanId: int.parse(messageData["requestNumber"].toString())); break; - // case "13": - // serviceClass = AssetDeliveryPage(requestId: int.parse(messageData["requestNumber"].toString())); - // break; + case "13": + serviceClass = AssetDeliveryPage(requestId: int.parse(messageData["requestNumber"].toString())); + break; case "14": serviceClass = MedicalGasInspectionDetailPage(inspectionId: int.parse(messageData["requestNumber"].toString()), prority: messageData["priorityName"]); break; @@ -271,12 +274,7 @@ class FirebaseNotificationManger { final String body = message.notification?.body ?? message.data['body'] ?? 'New notification'; // Always show with our grouping logic - NotificationManger.showNotification( - title: title, - subtext: body, - hashcode: DateTime.now().millisecondsSinceEpoch.hashCode, - payload: json.encode(message.data), - context: context); + NotificationManger.showNotification(title: title, subtext: body, hashcode: DateTime.now().millisecondsSinceEpoch.hashCode, payload: json.encode(message.data), context: context); // Update unread notification count badge when new notification arrives try { diff --git a/lib/modules/asset_delivery_module/pages/asset_delivery_item_view.dart b/lib/modules/asset_delivery_module/pages/asset_delivery_item_view.dart index 1fe8dc2b..df3c0dbc 100644 --- a/lib/modules/asset_delivery_module/pages/asset_delivery_item_view.dart +++ b/lib/modules/asset_delivery_module/pages/asset_delivery_item_view.dart @@ -35,8 +35,8 @@ class AssetDeliveryItemView extends StatelessWidget { Navigator.of(context).push(MaterialPageRoute( builder: (_) => AssetDeliveryPage( requestId: requestData!.id!, - poInternal: requestData?.poInternal, - poExternal: requestData?.poExternal, + // poInternal: requestData?.poInternal, + // poExternal: requestData?.poExternal, ))); }); } @@ -52,8 +52,8 @@ class AssetDeliveryItemView extends StatelessWidget { Navigator.of(context).push(MaterialPageRoute( builder: (_) => AssetDeliveryPage( requestId: requestDetails!.id!, - poInternal: requestDetails?.poInternal, - poExternal: requestDetails?.poExternal, + // poInternal: requestDetails?.poInternal, + // poExternal: requestDetails?.poExternal, ))); }); } diff --git a/lib/modules/asset_delivery_module/pages/asset_delivery_line_card_view.dart b/lib/modules/asset_delivery_module/pages/asset_delivery_line_card_view.dart index 5337f457..e953eb3d 100644 --- a/lib/modules/asset_delivery_module/pages/asset_delivery_line_card_view.dart +++ b/lib/modules/asset_delivery_module/pages/asset_delivery_line_card_view.dart @@ -4,6 +4,7 @@ import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/modules/asset_delivery_module/models/delivery_line_model.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; +import 'package:test_sa/views/widgets/item_views/info_text_widget.dart'; class AssetDeliveryLineCard extends StatelessWidget { DeliveryLineModel deliveryLineModel; @@ -28,59 +29,27 @@ class AssetDeliveryLineCard extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - 'Item Category: ${deliveryLineModel.itemCategory ?? '-'}', - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), - ), + InfoTextWidget(label: "Item Category", value: deliveryLineModel.itemCategory ?? '-'), if (!isViewOnly) - 'edit_icon'.toSvgAsset().onPress(() { + 'edit_icon'.toSvgAsset(width: 24, height: 24, color: AppColor.icon2Color(context)).onPress(() { if (editPress != null) { editPress!(); } }), ], ), - Text( - 'Item No: ${deliveryLineModel.itemNumber ?? '-'}', - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), - ), - Text( - 'Item Description: ${deliveryLineModel.itemDescription ?? '-'}', - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), - ), - Text( - 'Quantity: ${deliveryLineModel.qty ?? '-'}', - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), - ), - - if(!isTechnical)...[ - Text( - 'Remaining Quantity: ${deliveryLineModel.remainingQty ?? '-'}', - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), - ), - Text( - 'Quantity of this Delivery: ${deliveryLineModel.usingQtyInThisDelivery ?? '-'}', - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), - ), + InfoTextWidget(label: "Item No", value: deliveryLineModel.itemNumber ?? '-'), + InfoTextWidget(label: "Item Description", value: deliveryLineModel.itemDescription ?? '-'), + InfoTextWidget(label: "Quantity", value: deliveryLineModel.qty?.toString() ?? '-'), + if (!isTechnical) ...[ + InfoTextWidget(label: "Remaining Quantity", value: deliveryLineModel.remainingQty?.toString() ?? '-'), + InfoTextWidget(label: "Quantity of this Delivery", value: deliveryLineModel.usingQtyInThisDelivery?.toString() ?? '-'), ], - - Text( - 'Measure Unit: ${deliveryLineModel.unitOfMeasure ?? '-'}', - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), - ), - Text( - 'Unit price: ${deliveryLineModel.unitPrice ?? '-'}', - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), - ), - Text( - 'Line Type: ${deliveryLineModel.lineType ?? '-'}', - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), - ), - Text( - 'Total: ${deliveryLineModel.total ?? '-'}', - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), - ), + InfoTextWidget(label: "Measure Unit", value: deliveryLineModel.unitOfMeasure?.toString() ?? '-'), + InfoTextWidget(label: "Unit price", value: deliveryLineModel.unitPrice?.toString() ?? '-'), + InfoTextWidget(label: "Line Type", value: deliveryLineModel.lineType?.toString() ?? '-'), + InfoTextWidget(label: "Total", value: deliveryLineModel.total?.toString() ?? '-'), ], - ).toShadowContainer(context, borderRadius: 20, padding: 12); + ).toShadowContainer(context); } } diff --git a/lib/modules/asset_delivery_module/pages/asset_delivery_page.dart b/lib/modules/asset_delivery_module/pages/asset_delivery_page.dart index 52626bd9..664b4617 100644 --- a/lib/modules/asset_delivery_module/pages/asset_delivery_page.dart +++ b/lib/modules/asset_delivery_module/pages/asset_delivery_page.dart @@ -25,14 +25,14 @@ import 'package:test_sa/views/widgets/item_views/info_text_widget.dart'; class AssetDeliveryPage extends StatefulWidget { final int? requestId; - final String? poInternal; - final String? poExternal; + // final String? poInternal; + // final String? poExternal; const AssetDeliveryPage({ Key? key, this.requestId, - this.poInternal, - this.poExternal, + // this.poInternal, + // this.poExternal, }) : super(key: key); @override @@ -101,7 +101,7 @@ class _AssetDeliveryPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - requestDetailCard(context).toShadowContainer(context, borderRadius: 20, padding: 0), + requestDetailCard(context).toShadowContainer(context), 16.height, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -160,7 +160,7 @@ class _AssetDeliveryPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ InfoHeader16Widget('PO Details'.addTranslation), - InfoTextWidget(label: 'PO No. Internal'.addTranslation, value: widget.poInternal ?? '-'), + InfoTextWidget(label: 'PO No. Internal'.addTranslation, value: dataModel?.poNumber?.toString() ?? '-'), InfoTextWidget(label: 'Client Name'.addTranslation, value: dataModel?.clientName ?? '-'), // Row( // mainAxisSize: MainAxisSize.min, @@ -193,7 +193,7 @@ class _AssetDeliveryPageState extends State { InfoTextWidget(label: 'Total'.addTranslation, value: dataModel?.total != null ? dataModel!.total.toString() : '-'), InfoTextWidget(label: 'Total Invoice'.addTranslation, value: dataModel?.totalInvoice != null ? dataModel!.totalInvoice.toString() : '-'), ], - ).paddingAll(16); + ); } String getPOExternal() { @@ -218,7 +218,7 @@ class _AssetDeliveryPageState extends State { ? const NoDataFound().center : ListView.separated( itemCount: assetDeliveryTableList.length, - separatorBuilder: (_, __) => 8.height, + separatorBuilder: (_, __) => 12.height, padding: EdgeInsets.zero, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), diff --git a/lib/modules/asset_delivery_module/pages/asset_delivery_table_card_view.dart b/lib/modules/asset_delivery_module/pages/asset_delivery_table_card_view.dart index e7a41d96..59ed682a 100644 --- a/lib/modules/asset_delivery_module/pages/asset_delivery_table_card_view.dart +++ b/lib/modules/asset_delivery_module/pages/asset_delivery_table_card_view.dart @@ -7,6 +7,7 @@ import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/modules/asset_delivery_module/models/asset_delivery_data_model.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; +import 'package:test_sa/views/widgets/item_views/info_text_widget.dart'; import 'package:test_sa/views/widgets/requests/request_status.dart'; class AssetDeliveryTableCard extends StatelessWidget { @@ -54,24 +55,11 @@ class AssetDeliveryTableCard extends StatelessWidget { // ], // ), 8.height, - Text( - 'Delivery Number: ${deliveryTableModel.number ?? '-'}', - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), - ), - Text( - 'Delivery Amount: ${deliveryTableModel.deliveryAmount ?? '-'}', - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), - ), - Text( - 'Invoice Amount: ${deliveryTableModel.invoiceAmount ?? '-'}', - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), - ), - Align( - alignment: Alignment.centerRight, - child: Icon(Icons.arrow_forward, color: AppColor.blueStatus(context), size: 14), - ), + InfoTextWidget(label: "Delivery Number", value: deliveryTableModel.number ?? '-'), + InfoTextWidget(label: "Delivery Amount", value: deliveryTableModel.deliveryAmount?.toString() ?? '-'), + InfoTextWidget(label: "Invoice Amount", value: deliveryTableModel.invoiceAmount?.toString() ?? '-', showArrow: true), ], - ).toShadowContainer(context, borderRadius: 20).onPress(() { + ).toShadowContainer(context).onPress(() { onCardTap(); }); } diff --git a/lib/modules/asset_delivery_module/pages/delivery_inspection/delivery_inspection_form_view.dart b/lib/modules/asset_delivery_module/pages/delivery_inspection/delivery_inspection_form_view.dart index 39e097b4..f62cd62c 100644 --- a/lib/modules/asset_delivery_module/pages/delivery_inspection/delivery_inspection_form_view.dart +++ b/lib/modules/asset_delivery_module/pages/delivery_inspection/delivery_inspection_form_view.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:developer'; +import 'package:date_picker_timeline/extra/color.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -32,6 +33,8 @@ import 'package:test_sa/views/widgets/date_and_time/time_picker.dart'; import 'package:test_sa/new_views/common_widgets/single_item_drop_down_menu.dart'; import 'package:test_sa/new_views/swipe_module/dialoge/acknowledge_work_dialog.dart'; import 'package:test_sa/views/widgets/date_and_time/date_picker.dart'; +import 'package:test_sa/views/widgets/item_views/info_header_widget.dart'; +import 'package:test_sa/views/widgets/item_views/info_text_widget.dart'; import 'package:test_sa/views/widgets/loaders/no_data_found.dart'; import '../../../../new_views/common_widgets/app_filled_button.dart'; @@ -97,23 +100,19 @@ class _DeliveryInspectionFormViewState extends State return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: isLoading - ? CircularProgressIndicator(color: AppColor.loadingColor(context)).center + ? CircularProgressIndicator(color: AppColor.loadingColor(context)).center : Form( key: _formKey, child: Column( children: [ SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12, children: [ if (!viewOnly) ...[inspectionDetailsForm()] else ...[inspectionDetails()], - 20.height, - Text( - "Lines", - style: AppTextStyles.heading6.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.black10), - ), - 8.height, + InfoHeader16Widget("Lines"), linesList(context), ], )).expanded, @@ -178,12 +177,12 @@ class _DeliveryInspectionFormViewState extends State return Consumer(builder: (context, provider, child) { List linesList = provider.deliveryLinesList; return provider.isDeliveryLineListLoading - ? SizedBox.expand(child: CircularProgressIndicator(color: AppColor.loadingColor(context)).center) + ? SizedBox.expand(child: CircularProgressIndicator(color: AppColor.loadingColor(context)).center) : linesList.isEmpty ? const NoDataFound().center : ListView.separated( itemCount: linesList.length, - separatorBuilder: (_, __) => 8.height, + separatorBuilder: (_, __) => 12.height, padding: EdgeInsets.zero, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -234,6 +233,7 @@ class _DeliveryInspectionFormViewState extends State Widget inspectionDetailsForm() { return Column( crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12, children: [ _inspectionTile( title: 'Inspected by'.addTranslation, @@ -251,7 +251,6 @@ class _DeliveryInspectionFormViewState extends State } }, ), - 8.height, _inspectionTile( title: 'Approved by'.addTranslation, model: model.approvedByModel, @@ -269,7 +268,6 @@ class _DeliveryInspectionFormViewState extends State }, ), if (widget.requestModel?.paymentTerm?.value == 2) ...[ - 8.height, _inspectionTile( title: 'ThirdApprover'.addTranslation, model: model.thirdApprovalModel, @@ -287,7 +285,6 @@ class _DeliveryInspectionFormViewState extends State }, ), ], - 8.height, ADatePicker( label: 'Inspected Date/Time From'.addTranslation, hideShadow: true, @@ -314,12 +311,10 @@ class _DeliveryInspectionFormViewState extends State if (!_validateInspectionDates(from: fromDateTime, to: model.deliveryInspectionInspectedDateTo)) { return; // invalid, do not assign } - model.deliveryInspectionInspectedDateFrom = fromDateTime; setState(() {}); }, ), - 8.height, ADatePicker( label: 'Inspected Date/Time To'.addTranslation, hideShadow: true, @@ -356,18 +351,7 @@ class _DeliveryInspectionFormViewState extends State setState(() {}); }, ), - 8.height, - Text( - 'Total Time'.addTranslation, - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.black10), - ), - 8.height, - Text( - formatInspectionDuration(model.totalTime), - style: AppTextStyles.bodyText2.copyWith( - color: context.isDark ? AppColor.neutral30 : AppColor.neutral120, - ), - ), + InfoTextWidget(label: 'Total Time'.addTranslation, value: formatInspectionDuration(model.totalTime)), ], ).toShadowContainer(context, borderRadius: 20, padding: 12); } @@ -421,64 +405,18 @@ class _DeliveryInspectionFormViewState extends State return '${hours.toStringAsFixed(2)} hours'; } - Widget inspectionDetails() { return Column( crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4, children: [ - Text( - 'Inspected by'.addTranslation, - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.black10), - ), - 8.height, - Text( - model.deliveryInspectionInspectedBy ?? '-', - style: AppTextStyles.bodyText2.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral120), - ), - 8.height, - Text( - 'Approved by'.addTranslation, - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.black10), - ), - 8.height, - Text( - model.deliveryInspectionApprovedBy ?? '-', - style: AppTextStyles.bodyText2.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral120), - ), - 8.height, - Text( - 'Inspected Date/Time From'.addTranslation, - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.black10), - ), - 8.height, - Text( - model.deliveryInspectionInspectedDateFrom != null ? model.deliveryInspectionInspectedDateFrom!.toString().toServiceRequestDetailsFormat : '-', - style: AppTextStyles.bodyText2.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral120), - ), - 8.height, - Text( - 'Inspected Date/Time To'.addTranslation, - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.black10), - ), - 8.height, - Text( - model.deliveryInspectionInspectedDateTo != null ? model.deliveryInspectionInspectedDateTo!.toString().toServiceRequestDetailsFormat : '-', - style: AppTextStyles.bodyText2.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral120), - ), - 8.height, - Text( - 'Total Time'.addTranslation, - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.black10), - ), - 8.height, - Text( - formatInspectionDuration(model.totalTime), - style: AppTextStyles.bodyText2.copyWith( - color: context.isDark ? AppColor.neutral30 : AppColor.neutral120, - ), - ), + InfoTextWidget(label: "Inspected by", value: "\n${model.deliveryInspectionInspectedBy ?? '-'}"), + InfoTextWidget(label: "Approved by", value: "\n${model.deliveryInspectionApprovedBy ?? '-'}"), + InfoTextWidget(label: 'Inspected Date/Time From'.addTranslation, value: "\n${model.deliveryInspectionInspectedDateFrom?.toString().toServiceRequestDetailsFormat ?? '-'}"), + InfoTextWidget(label: 'Inspected Date/Time To'.addTranslation, value: "\n${model.deliveryInspectionInspectedDateTo?.toString().toServiceRequestDetailsFormat ?? '-'}"), + InfoTextWidget(label: 'Total Time'.addTranslation, value: "\n${formatInspectionDuration(model.totalTime) ?? '-'}"), ], - ).toShadowContainer(context, borderRadius: 20, padding: 12); + ).toShadowContainer(context); } Widget _inspectionTile({ @@ -493,8 +431,9 @@ class _DeliveryInspectionFormViewState extends State child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: AppColor.fieldBgColor(context), - borderRadius: BorderRadius.circular(12), + color: AppColor.background(context), + border: Border.all(color: AppColor.border2Color(context), width: 1), + borderRadius: BorderRadius.circular(16), ), child: Row( children: [ @@ -502,25 +441,9 @@ class _DeliveryInspectionFormViewState extends State crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text(title, style: AppTextStyles.textFieldLabelStyle), - 6.height, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - model?.name?.isNotEmpty == true ? model!.name! : 'Tap to add'.addTranslation, - style: AppTextStyles.bodyText, - ), - - ///Need to display other info as well - // Text( - // model?.email?.isNotEmpty == true - // ? model!.email! - // : '', - // style: AppTextStyles.bodyText, - // ), - ], - ), + InfoTextLabelWidget(label: title, textStyle: AppTextStyles.textFieldLabelStyle), + 4.height, + InfoTextLabelWidget(label: model?.name?.isNotEmpty == true ? model!.name! : 'Tap to add'.addTranslation, textStyle: AppTextStyles.bodyText), ], ), ], @@ -630,6 +553,7 @@ class _DeliveryInspectionFormViewState extends State return showModalBottomSheet( context: context, isScrollControlled: true, + backgroundColor: AppColor.bottomSheetColor(context), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), diff --git a/lib/modules/asset_delivery_module/pages/delivery_inspection/inpection_bottomsheet.dart b/lib/modules/asset_delivery_module/pages/delivery_inspection/inpection_bottomsheet.dart index 34aad6d3..130a2d6b 100644 --- a/lib/modules/asset_delivery_module/pages/delivery_inspection/inpection_bottomsheet.dart +++ b/lib/modules/asset_delivery_module/pages/delivery_inspection/inpection_bottomsheet.dart @@ -67,14 +67,14 @@ class _InspectionPersonBottomSheetState extends State { @override Widget build(BuildContext context) { assetDeliveryProvider ??= Provider.of(context); - final border = UnderlineInputBorder(borderSide: BorderSide.none, borderRadius: BorderRadius.circular(10)); + final border = UnderlineInputBorder(borderSide: BorderSide.none, borderRadius: BorderRadius.circular(16)); return Container( decoration: BoxDecoration( color: AppColor.background(context), - borderRadius: BorderRadius.circular(AppStyle.borderRadius * AppStyle.getScaleFactor(context)), + border: Border.all(color: AppColor.border2Color(context), width: 1), + borderRadius: BorderRadius.circular(16), // boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10)], ), child: Autocomplete( @@ -73,7 +74,7 @@ class _AutoCompletePartsFieldState extends State { return TextField( controller: fieldTextEditingController, focusNode: fieldFocusNode, - style: AppTextStyles.bodyText.copyWith(color: AppColor.black10), + style: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 14, color: AppColor.textStyleColor(context)), textAlign: TextAlign.start, decoration: InputDecoration( border: border, @@ -81,15 +82,16 @@ class _AutoCompletePartsFieldState extends State { focusedBorder: border, enabledBorder: border, errorBorder: border, - contentPadding: EdgeInsets.symmetric(vertical: 8.toScreenHeight, horizontal: 16.toScreenWidth), + contentPadding: EdgeInsets.symmetric(vertical: 12.toScreenHeight, horizontal: 16.toScreenWidth), constraints: const BoxConstraints(), suffixIconConstraints: const BoxConstraints(minWidth: 0), filled: true, - fillColor: AppColor.fieldBgColor(context), + fillColor: AppColor.dropDownBackground(context), errorStyle: AppTextStyle.tiny.copyWith(color: context.isDark ? AppColor.red50 : AppColor.red60), - floatingLabelStyle: AppTextStyle.body1.copyWith(fontWeight: FontWeight.w500, color: context.isDark ? null : AppColor.neutral20), labelText: widget.label, - labelStyle: AppTextStyles.tinyFont.copyWith(color: AppColor.textColor(context)), + floatingLabelStyle: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 12, color: AppColor.floatingTextStyleColor(context)), + labelStyle: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 12, color: AppColor.labelTextStyleColor(context)), + ), textInputAction: TextInputAction.search, onChanged: (text) { diff --git a/lib/modules/asset_delivery_module/pages/history_log_view.dart b/lib/modules/asset_delivery_module/pages/history_log_view.dart index 9ad8c1fc..01cc5f37 100644 --- a/lib/modules/asset_delivery_module/pages/history_log_view.dart +++ b/lib/modules/asset_delivery_module/pages/history_log_view.dart @@ -61,8 +61,8 @@ class _AssetDeliveryHistoryLogViewState extends State { @override Widget build(BuildContext context) { - final border = UnderlineInputBorder(borderSide: BorderSide.none, borderRadius: BorderRadius.circular(10)); + final border = UnderlineInputBorder(borderSide: BorderSide.none, borderRadius: BorderRadius.circular(16)); return Container( decoration: BoxDecoration( color: AppColor.background(context), - borderRadius: BorderRadius.circular(AppStyle.borderRadius * AppStyle.getScaleFactor(context)), - // boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10)], + border: Border.all(color: AppColor.border2Color(context), width: 1), + borderRadius: BorderRadius.circular(16), ), child: Autocomplete( optionsBuilder: (TextEditingValue textEditingValue) async { @@ -90,7 +90,7 @@ class _AssetAutoCompleteFieldState extends State { return TextField( controller: _controller, focusNode: fieldFocusNode, - style: AppTextStyles.bodyText.copyWith(color: AppColor.black10), + style: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 14, color: AppColor.textStyleColor(context)), textAlign: TextAlign.start, decoration: InputDecoration( border: border, @@ -98,15 +98,15 @@ class _AssetAutoCompleteFieldState extends State { focusedBorder: border, enabledBorder: border, errorBorder: border, - contentPadding: EdgeInsets.symmetric(vertical: 8.toScreenHeight, horizontal: 16.toScreenWidth), + contentPadding: EdgeInsets.symmetric(vertical: 12.toScreenHeight, horizontal: 16.toScreenWidth), constraints: const BoxConstraints(), suffixIconConstraints: const BoxConstraints(maxHeight: 24, maxWidth: 24 + 8), filled: true, - fillColor: AppColor.fieldBgColor(context), + fillColor: AppColor.dropDownBackground(context), errorStyle: AppTextStyle.tiny.copyWith(color: context.isDark ? AppColor.red50 : AppColor.red60), - floatingLabelStyle: AppTextStyle.body1.copyWith(fontWeight: FontWeight.w500, color: context.isDark ? null : AppColor.neutral20), + floatingLabelStyle: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 12, color: AppColor.floatingTextStyleColor(context)), + labelStyle: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 12, color: AppColor.labelTextStyleColor(context)), labelText: "Name/Oracle Code", - labelStyle: AppTextStyles.tinyFont.copyWith(color: AppColor.textColor(context)), suffixIcon: loading ? CircularProgressIndicator(color: AppColor.loadingColor(context), strokeWidth: 3.0).paddingOnly(end: 8) : null, ), textInputAction: TextInputAction.search, diff --git a/lib/views/widgets/date_and_time/date_picker.dart b/lib/views/widgets/date_and_time/date_picker.dart index 8fbb1264..f2439844 100644 --- a/lib/views/widgets/date_and_time/date_picker.dart +++ b/lib/views/widgets/date_and_time/date_picker.dart @@ -57,7 +57,7 @@ class ADatePicker extends StatelessWidget { children: [ Text( label, - style: AppTextStyles.tinyFont.copyWith(color: AppColor.floatingTextStyleColor(context)), + style: AppTextStyles.tinyFont2.copyWith(color: AppColor.floatingTextStyleColor(context)), ), 4.height, (date == null diff --git a/lib/views/widgets/e_signature/e_signature.dart b/lib/views/widgets/e_signature/e_signature.dart index 64d297e3..63615089 100644 --- a/lib/views/widgets/e_signature/e_signature.dart +++ b/lib/views/widgets/e_signature/e_signature.dart @@ -8,6 +8,7 @@ import 'package:test_sa/extensions/string_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; +import 'package:test_sa/views/widgets/item_views/info_text_widget.dart'; import '../loaders/image_loader.dart'; @@ -67,18 +68,16 @@ class _ESignatureState extends State { width: MediaQuery.of(context).size.width, padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth, vertical: 8.toScreenHeight), decoration: BoxDecoration( - color: widget.backgroundColor ?? (context.isDark ? AppColor.background(context) : Colors.white), - borderRadius: _editable ? const BorderRadius.only(topLeft: Radius.circular(10), topRight: Radius.circular(10)) : BorderRadius.circular(10), + color: AppColor.dropDownBackground(context), + border: Border.all(color: AppColor.border2Color(context), width: 1), + borderRadius: BorderRadius.circular(16), boxShadow: widget.showShadow ? [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10)] : null, ), child: widget.editable == false ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Signature'.addTranslation, - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.black10), - ), + InfoTextLabelWidget(label: "Signature", textStyle: AppTextStyles.bodyText), Container( height: 135.toScreenHeight, width: MediaQuery.of(context).size.width, From 27aee0b2b24e515fc82f9d0cc965cce4477d6633 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Mon, 27 Jul 2026 09:15:10 +0300 Subject: [PATCH 22/35] ui/ux asset delivery improvements --- .../cost_center_form_view.dart | 127 ++++++++---------- 1 file changed, 58 insertions(+), 69 deletions(-) diff --git a/lib/modules/asset_delivery_module/pages/delivery_inspection/cost_center_form_view.dart b/lib/modules/asset_delivery_module/pages/delivery_inspection/cost_center_form_view.dart index b272c886..79536d85 100644 --- a/lib/modules/asset_delivery_module/pages/delivery_inspection/cost_center_form_view.dart +++ b/lib/modules/asset_delivery_module/pages/delivery_inspection/cost_center_form_view.dart @@ -18,6 +18,7 @@ import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart'; import 'package:test_sa/new_views/common_widgets/app_text_form_field.dart'; import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; import 'package:test_sa/new_views/common_widgets/single_item_drop_down_menu.dart'; +import 'package:test_sa/views/widgets/item_views/info_text_widget.dart'; class CostCenterFormView extends StatefulWidget { final List? costCenterList; @@ -113,12 +114,19 @@ class _CostCenterFormViewState extends State { ), body: Consumer(builder: (context, provider, child) { return provider.isLoading - ? CircularProgressIndicator(color: AppColor.loadingColor(context)).center + ? CircularProgressIndicator(color: AppColor.loadingColor(context)).center : Column( children: [ - itemDetails(), - selectionButton(), - _selectedType == CostCenterType.costCenter ? _buildCostCenterList() : _buildSerialNumberList(), + ListView( + padding: const EdgeInsets.all(16), + children: [ + itemDetails(), + 12.height, + selectionButton(), + 12.height, + _selectedType == CostCenterType.costCenter ? _buildCostCenterList() : _buildSerialNumberList(), + ], + ).expanded, FooterActionButton.footerContainer( context: context, child: AppFilledButton( @@ -147,10 +155,12 @@ class _CostCenterFormViewState extends State { } Widget _buildCostCenterList() { - return ListView.builder( + return ListView.separated( itemCount: formModel.costCenters!.length + 1, shrinkWrap: true, - padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16, top: 8), + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (context, index) => 12.height, itemBuilder: (context, index) { if (index == formModel.costCenters!.length) { return AppFilledButton( @@ -184,10 +194,12 @@ class _CostCenterFormViewState extends State { } Widget _buildSerialNumberList() { - return ListView.builder( + return ListView.separated( itemCount: formModel.serialNos!.length + 1, shrinkWrap: true, - padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16, top: 8), + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (context, index) => 12.height, itemBuilder: (context, index) { if (index == formModel.serialNos!.length) { return AppFilledButton( @@ -217,36 +229,33 @@ class _CostCenterFormViewState extends State { } Widget selectionButton() { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - children: [ - Expanded( - child: _radioItem( - value: CostCenterType.costCenter, - groupValue: _selectedType, - title: 'Cost Center', - onTap: () { - setState(() { - _selectedType = CostCenterType.costCenter; - }); - }, - ), + return Row( + children: [ + Expanded( + child: _radioItem( + value: CostCenterType.costCenter, + groupValue: _selectedType, + title: 'Cost Center', + onTap: () { + setState(() { + _selectedType = CostCenterType.costCenter; + }); + }, ), - Expanded( - child: _radioItem( - value: CostCenterType.serialNumber, - groupValue: _selectedType, - title: 'Serial Number', - onTap: () { - setState(() { - _selectedType = CostCenterType.serialNumber; - }); - }, - ), + ), + Expanded( + child: _radioItem( + value: CostCenterType.serialNumber, + groupValue: _selectedType, + title: 'Serial Number', + onTap: () { + setState(() { + _selectedType = CostCenterType.serialNumber; + }); + }, ), - ], - ), + ), + ], ); } @@ -263,6 +272,13 @@ class _CostCenterFormViewState extends State { children: [ Radio( value: value, + activeColor: AppColor.primary10, + fillColor: WidgetStateColor.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return AppColor.primary10; // Thumb color when selected + } + return AppColor.icon2Color(context); // Thumb color when unselected (grey) + }), groupValue: groupValue, onChanged: (_) => onTap(), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, @@ -279,34 +295,12 @@ class _CostCenterFormViewState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, + spacing: 4, children: [ - Text( - 'Item No'.addTranslation, - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.black10), - ), - 8.height, - Text( - widget.itemNo ?? '-', - style: AppTextStyles.bodyText2.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral120), - ), - 8.height, - Text( - 'Item Description'.addTranslation, - style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.black10), - ), - 8.height, - Text( - widget.itemDescription ?? '-', - style: AppTextStyles.bodyText2.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral120), - ), + InfoTextWidget(label: "Item No", value: "\n${widget.itemNo ?? '-'}"), + InfoTextWidget(label: "Item Description", value: "\n${widget.itemDescription ?? '-'}"), ], - ).toShadowContainer( - context, - borderRadius: 20, - padding: 12, - showShadow: false, - margin: const EdgeInsets.only(left: 16, right: 16, top: 16, bottom: 8), - ); + ).toShadowContainer(context); } } @@ -402,7 +396,7 @@ class CostCenterItemCard extends StatelessWidget { ], ) ], - ).toShadowContainer(context, paddingObject: const EdgeInsets.symmetric(horizontal: 12, vertical: 12)).paddingOnly(bottom: 12); + ).toShadowContainer(context); } } @@ -447,11 +441,6 @@ class SerialNumberItemCard extends StatelessWidget { style: Theme.of(context).textTheme.titleMedium, ), ], - ) - .toShadowContainer( - context, - paddingObject: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), - ) - .paddingOnly(bottom: 12); + ).toShadowContainer(context); } } From 313df6da8b1a020e2624361f4a6f5b3e576568ea Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Mon, 17 Aug 2026 15:31:31 +0300 Subject: [PATCH 23/35] improvements --- .../providers/api/user_provider.dart | 8 +- lib/main.dart | 2 + .../incident_module/incident_detail_page.dart | 80 ++++++++++--------- .../traf_module/traf_request_detail_page.dart | 50 ++++++------ 4 files changed, 76 insertions(+), 64 deletions(-) diff --git a/lib/controllers/providers/api/user_provider.dart b/lib/controllers/providers/api/user_provider.dart index f1b395b6..d1844e35 100644 --- a/lib/controllers/providers/api/user_provider.dart +++ b/lib/controllers/providers/api/user_provider.dart @@ -68,7 +68,13 @@ class UserProvider extends ChangeNotifier { List get modulesPermissionsList => _modulesPermissionsModel; - ModulesPermissionsModel currentModelPermission(int moduleId) => ModulesPermissionsModel.fromJson(_modulesPermissionsModel.singleWhere((module) => module.module!.value == moduleId).toJson()); + ModulesPermissionsModel? currentModelPermission(int moduleId) { + try { + return ModulesPermissionsModel.fromJson(_modulesPermissionsModel.singleWhere((module) => module.module!.value == moduleId).toJson()); + } catch (ex) { + return null; + } + } set swipeHistory(List value) { _swipeHistory = value; diff --git a/lib/main.dart b/lib/main.dart index 50847d8f..29987432 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -68,6 +68,7 @@ import 'package:test_sa/modules/applicationDemo/providers/application_demo_reque import 'package:test_sa/modules/applicationDemo/screens/application_demo_request_form_screen.dart'; import 'package:test_sa/modules/tm_module/gas_refill/create_gas_refill_request.dart'; import 'package:test_sa/modules/tm_module/gas_refill/update_gas_refill_request.dart'; +import 'package:test_sa/modules/traf_module/traf_request_provider.dart'; import 'package:test_sa/new_views/app_style/app_themes.dart'; import 'package:test_sa/new_views/pages/help_center_page.dart'; import 'package:test_sa/new_views/pages/land_page/land_page.dart'; @@ -332,6 +333,7 @@ class MyApp extends StatelessWidget { ChangeNotifierProvider(create: (_) => LoanProvider(), lazy: true), ChangeNotifierProvider(create: (_) => LoanPeriodProvider(), lazy: true), ChangeNotifierProvider(create: (_) => DemoProvider(), lazy: true), + ChangeNotifierProvider(create: (_) => TrafRequestProvider(), lazy: true), // MEDICAL GAS INSPECTION PROVIDERS (4) ChangeNotifierProvider(create: (_) => MedicalGasOrderTypeProvider(), lazy: true), diff --git a/lib/modules/incident_module/incident_detail_page.dart b/lib/modules/incident_module/incident_detail_page.dart index 4f9ade6e..e9602f8c 100644 --- a/lib/modules/incident_module/incident_detail_page.dart +++ b/lib/modules/incident_module/incident_detail_page.dart @@ -33,49 +33,51 @@ class IncidentDetailPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: const DefaultAppBar(title: "Request Details"), - body: FutureBuilder( - future: Provider.of(context, listen: false).getIncidentById(incidentId), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) return CircularProgressIndicator(color: AppColor.loadingColor(context)).center; - if (snapshot.data == null) return const NoDataFound().center; + body: SafeArea( + child: FutureBuilder( + future: Provider.of(context, listen: false).getIncidentById(incidentId), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) return CircularProgressIndicator(color: AppColor.loadingColor(context)).center; + if (snapshot.data == null) return const NoDataFound().center; - List allAttachments = snapshot.data!.incidentAttachments!; + List allAttachments = snapshot.data!.incidentAttachments!; - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - StatusLabel( - label: snapshot.data!.incidentStatusName!, - textColor: AppColor.getRequestStatusTextColorByName(context, snapshot.data!.incidentStatusName!), - backgroundColor: AppColor.getRequestStatusColorByName(context, snapshot.data!.incidentStatusName!), - ), - 1.width.expanded, - InfoDateWidget(snapshot.data!.createdDate?.toString() ?? ""), - ], - ), - // 12.height, - ...requesterDetails(context, snapshot.data!), - const Divider().defaultStyle(context), - ...requestDetails(context, snapshot.data!), - if (snapshot.data!.ovrTicketNumber?.isNotEmpty ?? false) ovrInformation(context, snapshot.data!.ovrTicketNumber!), - const Divider().defaultStyle(context), - ...assetDetails(context, snapshot.data!), - const Divider().defaultStyle(context), - ...personInvolvedDetails(context, snapshot.data!), - if (allAttachments.isNotEmpty) ...[ + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StatusLabel( + label: snapshot.data!.incidentStatusName!, + textColor: AppColor.getRequestStatusTextColorByName(context, snapshot.data!.incidentStatusName!), + backgroundColor: AppColor.getRequestStatusColorByName(context, snapshot.data!.incidentStatusName!), + ), + 1.width.expanded, + InfoDateWidget(snapshot.data!.createdDate?.toString() ?? ""), + ], + ), + // 12.height, + ...requesterDetails(context, snapshot.data!), + const Divider().defaultStyle(context), + ...requestDetails(context, snapshot.data!), + if (snapshot.data!.ovrTicketNumber?.isNotEmpty ?? false) ovrInformation(context, snapshot.data!.ovrTicketNumber!), const Divider().defaultStyle(context), - InfoHeader16Widget("Attachments".addTranslation), - FilesList(images: allAttachments.map((e) => URLs.getFileUrl(e.attachmentName ?? '') ?? '').toList() ?? []), + ...assetDetails(context, snapshot.data!), + const Divider().defaultStyle(context), + ...personInvolvedDetails(context, snapshot.data!), + if (allAttachments.isNotEmpty) ...[ + const Divider().defaultStyle(context), + InfoHeader16Widget("Attachments".addTranslation), + FilesList(images: allAttachments.map((e) => URLs.getFileUrl(e.attachmentName ?? '') ?? '').toList() ?? []), + ], ], - ], - ).toShadowContainer(context), - ); - })); + ).toShadowContainer(context), + ); + }), + )); } List requestDetails(BuildContext context, IncidentDataModel incidentData) { diff --git a/lib/modules/traf_module/traf_request_detail_page.dart b/lib/modules/traf_module/traf_request_detail_page.dart index 00673a35..7f20e018 100644 --- a/lib/modules/traf_module/traf_request_detail_page.dart +++ b/lib/modules/traf_module/traf_request_detail_page.dart @@ -67,30 +67,32 @@ class _TrafRequestDetailPageState extends State { } return trafProvider.trafRequestDataModel == null ? const NoDataFound().center - : Column( - children: [ - ListView( - padding: const EdgeInsets.all(16), - children: [ - requesterInformation(trafProvider.trafRequestDataModel!), - 12.height, - requestInformation(trafProvider.trafRequestDataModel!), - 12.height, - assetInformation(trafProvider.trafRequestDataModel!), - ], - ).expanded, - // // if (isEngineer) - // FooterActionButton.footerContainer( - // context: context, - // child: AppFilledButton( - // buttonColor: AppColor.primary10, - // label: "Update", - // onPressed: () { - // Navigator.pushNamed(context, UpdateTrafRequestPage.id); - // }), - // ), - ], - ); + : SafeArea( + child: Column( + children: [ + ListView( + padding: const EdgeInsets.all(16), + children: [ + requesterInformation(trafProvider.trafRequestDataModel!), + 12.height, + requestInformation(trafProvider.trafRequestDataModel!), + 12.height, + assetInformation(trafProvider.trafRequestDataModel!), + ], + ).expanded, + // // if (isEngineer) + // FooterActionButton.footerContainer( + // context: context, + // child: AppFilledButton( + // buttonColor: AppColor.primary10, + // label: "Update", + // onPressed: () { + // Navigator.pushNamed(context, UpdateTrafRequestPage.id); + // }), + // ), + ], + ), + ); }, )); } From 09ecf4fab401e69c8cb9860303aaffa18a3c9d51 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Tue, 18 Aug 2026 11:58:52 +0300 Subject: [PATCH 24/35] traf added. --- lib/main.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index 29987432..acc81829 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -146,6 +146,8 @@ import 'modules/tm_module/device_transfer/device_filter_screen.dart'; import 'modules/tm_module/device_transfer/device_search_screen.dart'; import 'modules/tm_module/tasks/create_task_view.dart'; import 'modules/tm_module/gas_refill/gas_refill_request_form.dart'; +import 'modules/traf_module/create_traf_request_page.dart'; +import 'modules/traf_module/update_traf_request_page.dart'; import 'providers/lookups/classification_lookup_provider.dart'; import 'providers/lookups/customer_lookup_provider.dart'; import 'providers/lookups/department_lookup_provider.dart'; @@ -416,6 +418,8 @@ class MyApp extends StatelessWidget { LandPage.routeName: (_) => const LandPage(), GasRefillRequestForm.routeName: (_) => const GasRefillRequestForm(), CreateTaskView.id: (_) => const CreateTaskView(), + CreateTRAFRequestPage.id: (_) => CreateTRAFRequestPage(), + UpdateTrafRequestPage.id: (_) => UpdateTrafRequestPage(), CreateGasRefillRequest.id: (_) => const CreateGasRefillRequest(), CreateCMRequest.id: (_) => const CreateCMRequest(), MyAssetsPage.id: (_) => const MyAssetsPage(), From f413d467384535608a19697904bb1e8e51f99853 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Tue, 18 Aug 2026 15:55:18 +0300 Subject: [PATCH 25/35] improvements --- .../providers/api/all_requests_provider.dart | 15 ++++++++++----- .../my_request/all_requests_filter_page.dart | 5 +++++ .../land_page/my_request/my_requests_page.dart | 11 +++++++++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/lib/controllers/providers/api/all_requests_provider.dart b/lib/controllers/providers/api/all_requests_provider.dart index 0a93116b..45f62f80 100644 --- a/lib/controllers/providers/api/all_requests_provider.dart +++ b/lib/controllers/providers/api/all_requests_provider.dart @@ -137,7 +137,14 @@ class AllRequestsProvider extends ChangeNotifier { List list = [1, 2, 3, 4]; if (context.userProvider.isAssessor) { - list = [9, 15]; + list = [9]; + context.userProvider.modulesPermissionsList.forEach((module) { + if (module.module!.value! == 8 || module.module!.value! == 9 || module.module!.value! == 15) { + list.add(module.module!.value!); + } + }); + list.add(16); + list = list.toSet().toList(); return list; } if (context.userProvider.isQualityUser) { @@ -158,9 +165,8 @@ class AllRequestsProvider extends ChangeNotifier { list.add(7); // task mod } - //TODO uncommit this to enable TRAF in production. context.userProvider.modulesPermissionsList.forEach((module) { - if (module.module!.value! == 8 || module.module!.value! == 9) { + if (module.module!.value! == 8 || module.module!.value! == 9 || module.module!.value! == 15) { list.add(module.module!.value!); } }); @@ -184,11 +190,10 @@ class AllRequestsProvider extends ChangeNotifier { // list = [16]; list.add(16); } - if ( context.userProvider.isHouseKeeper) { + if (context.userProvider.isHouseKeeper) { list = [5]; } - return list; } diff --git a/lib/new_views/pages/land_page/my_request/all_requests_filter_page.dart b/lib/new_views/pages/land_page/my_request/all_requests_filter_page.dart index 0c4c61ab..68f27fad 100644 --- a/lib/new_views/pages/land_page/my_request/all_requests_filter_page.dart +++ b/lib/new_views/pages/land_page/my_request/all_requests_filter_page.dart @@ -141,6 +141,11 @@ class _AllRequestsFilterPageState extends State { 'Assessment of Need & Technology': 9, // 'Demo': 15, moved to permission api }; + context.userProvider.modulesPermissionsList.forEach((module) { + if (module.module!.value! == 8 || module.module!.value! == 9 || module.module!.value! == 15) { + types[module.module!.name!] = module.module!.value!; + } + }); } if (context.settingProvider.isUserFlowMedical && !context.userProvider.isNurse) { types['Incidents'] = 16; diff --git a/lib/new_views/pages/land_page/my_request/my_requests_page.dart b/lib/new_views/pages/land_page/my_request/my_requests_page.dart index 65e635ea..2dc66f86 100644 --- a/lib/new_views/pages/land_page/my_request/my_requests_page.dart +++ b/lib/new_views/pages/land_page/my_request/my_requests_page.dart @@ -130,6 +130,17 @@ class _MyRequestsPageState extends State with SingleTickerProvid Request(9, 'Assessment of Need & Technology'), // Request(15, 'Demo'), moved to permission api ]; + + context.userProvider.modulesPermissionsList.forEach((module) { + if (module.module!.value! == 8 || module.module!.value! == 9 || module.module!.value! == 15) { + requestsList.add(Request(module.module!.value!, module.module!.name!.addTranslation)); + } + }); + + final seenIds = {}; + requestsList = requestsList.where((req) { + return req.id == null ? true : seenIds.add(req.id!); + }).toList(); } if (context.userProvider.isCommissioningEngineer && context.settingProvider.isUserFlowMedical) { ///TODO need to show only allowed ids for all workOrder From f1f7f19ecc3982397d946f8c8e325b9b07040922 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Wed, 19 Aug 2026 16:18:21 +0300 Subject: [PATCH 26/35] keep old notification behavior, --- lib/controllers/api_routes/urls.dart | 4 +- .../firebase_notification_manger.dart | 164 +++++++++++------- lib/models/system_notification_model.dart | 4 + 3 files changed, 106 insertions(+), 66 deletions(-) diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart index f398bc51..7b81739f 100644 --- a/lib/controllers/api_routes/urls.dart +++ b/lib/controllers/api_routes/urls.dart @@ -7,8 +7,8 @@ class URLs { // static const host1 = "https://atomsmdev.hmg.com"; // local DEV url static const host1 = "https://atomsmuat.hmg.com"; // local UAT url - // static final String _baseUrl = "$_host/mobile"; // host local UAT - static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis + static final String _baseUrl = "$_host/mobile"; // host local UAT + // static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis // static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM // static final String _baseUrl = "$_host/v4/mobile"; // v4 for Demo module // static final String _baseUrl = "$_host/v5/mobile"; // v5 for data segregation diff --git a/lib/controllers/notification/firebase_notification_manger.dart b/lib/controllers/notification/firebase_notification_manger.dart index 20a49c7a..a39438cd 100644 --- a/lib/controllers/notification/firebase_notification_manger.dart +++ b/lib/controllers/notification/firebase_notification_manger.dart @@ -103,6 +103,7 @@ class FirebaseNotificationManger { Widget? serviceClass; String? transactionType = messageData["transactionType"]?.toString(); + bool IsOldNotification = messageData["IsOldNotification"]?.toString() == null; if (transactionType == null) { return; @@ -142,71 +143,106 @@ class FirebaseNotificationManger { return; } - switch (transactionType) { - case "1": - serviceClass = CMDetailPage(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1); - break; - case "2": - serviceClass = GasRefillDetailsPage( - priority: messageData["priority"], - date: messageData["createdOn"], - moduleId: 2, - model: GasRefillModel(id: int.parse(messageData["requestNumber"].toString())), - ); - break; - case "3": - serviceClass = DeviceTransferDetails(model: DeviceTransfer(id: int.parse(messageData["requestNumber"].toString())), moduleId: 3); - break; - case "4": - serviceClass = PpmDetailsPage(requestId: int.parse(messageData["requestNumber"].toString())); - break; - case "5": - serviceClass = RecurrentWorkOrderView(taskId: int.parse(messageData["requestNumber"].toString())); - break; - case "6": - serviceClass = TaskRequestDetailsView( - taskId: int.parse(messageData["requestNumber"].toString()), - requestDetails: RequestsDetails(nameOfType: messageData["sourceName"], status: messageData["statusName"], priority: messageData["priorityName"], date: messageData["createdDate"]), - moduleId: 6); - break; - case "7": - serviceClass = TaskRequestDetailsView( - taskId: int.parse(messageData["requestNumber"].toString()), - requestDetails: RequestsDetails(nameOfType: messageData["sourceName"], status: messageData["statusName"], priority: messageData["priorityName"], date: messageData["createdDate"]), - moduleId: 7); - break; - case "8": - serviceClass = AssetInventoryPage(sessionId: int.parse(messageData["requestNumber"].toString()), moduleId: int.parse(transactionType)); - break; - case "9": - serviceClass = TrafRequestDetailPage(trafId: int.parse(messageData["requestNumber"].toString())); - break; - case "10": - serviceClass = EquipmentInternalAuditDetailPage(auditId: int.parse(messageData["requestNumber"].toString())); - break; - case "11": - serviceClass = SystemInternalAuditDetailPage(auditId: int.parse(messageData["requestNumber"].toString())); - break; - case "12": - serviceClass = LoanEquipmentDetailPage(loanId: int.parse(messageData["requestNumber"].toString())); - break; - case "13": - serviceClass = AssetDeliveryPage(requestId: int.parse(messageData["requestNumber"].toString())); - break; - case "14": - serviceClass = MedicalGasInspectionDetailPage(inspectionId: int.parse(messageData["requestNumber"].toString()), prority: messageData["priorityName"]); - break; - case "15": - serviceClass = DemoDetailViewPage(demoId: int.parse(messageData["requestNumber"].toString()), moduleId: 15); - break; - case "16": - serviceClass = IncidentDetailPage(incidentId: int.parse(messageData["requestNumber"].toString())); - break; - case "17": - serviceClass = SurveyPage(surveyId: int.parse(messageData["requestNumber"].toString())); + if (IsOldNotification) { + switch (transactionType) { + case "1": + serviceClass = PpmDetailsPage(requestId: int.parse(messageData["requestNumber"].toString())); + break; + case "3": + serviceClass = CMDetailPage(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1); + break; + case "8": + serviceClass = CMDetailPage(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1); + break; + case "11": + serviceClass = CMDetailPage(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1); + break; + case "7": + serviceClass = DeviceTransferDetails(model: DeviceTransfer(id: int.parse(messageData["requestNumber"].toString())), moduleId: 3); + break; + case "9": + serviceClass = GasRefillDetailsPage( + priority: messageData["priority"], + date: messageData["createdOn"], + moduleId: 2, + model: GasRefillModel(id: int.parse(messageData["requestNumber"].toString())), + ); + break; + case "12": + serviceClass = RecurrentWorkOrderView(taskId: int.parse(messageData["requestNumber"].toString())); + case "17": + serviceClass = SurveyPage(surveyId: int.parse(messageData["requestNumber"].toString())); - default: - serviceClass = const Scaffold(appBar: DefaultAppBar(), body: Center(child: NoDataFound())); + default: + serviceClass = const Scaffold(appBar: DefaultAppBar(), body: Center(child: NoDataFound())); + } + } else { + switch (transactionType) { + case "1": + serviceClass = CMDetailPage(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1); + break; + case "2": + serviceClass = GasRefillDetailsPage( + priority: messageData["priority"], + date: messageData["createdOn"], + moduleId: 2, + model: GasRefillModel(id: int.parse(messageData["requestNumber"].toString())), + ); + break; + case "3": + serviceClass = DeviceTransferDetails(model: DeviceTransfer(id: int.parse(messageData["requestNumber"].toString())), moduleId: 3); + break; + case "4": + serviceClass = PpmDetailsPage(requestId: int.parse(messageData["requestNumber"].toString())); + break; + case "5": + serviceClass = RecurrentWorkOrderView(taskId: int.parse(messageData["requestNumber"].toString())); + break; + case "6": + serviceClass = TaskRequestDetailsView( + taskId: int.parse(messageData["requestNumber"].toString()), + requestDetails: RequestsDetails(nameOfType: messageData["sourceName"], status: messageData["statusName"], priority: messageData["priorityName"], date: messageData["createdDate"]), + moduleId: 6); + break; + case "7": + serviceClass = TaskRequestDetailsView( + taskId: int.parse(messageData["requestNumber"].toString()), + requestDetails: RequestsDetails(nameOfType: messageData["sourceName"], status: messageData["statusName"], priority: messageData["priorityName"], date: messageData["createdDate"]), + moduleId: 7); + break; + case "8": + serviceClass = AssetInventoryPage(sessionId: int.parse(messageData["requestNumber"].toString()), moduleId: int.parse(transactionType)); + break; + case "9": + serviceClass = TrafRequestDetailPage(trafId: int.parse(messageData["requestNumber"].toString())); + break; + case "10": + serviceClass = EquipmentInternalAuditDetailPage(auditId: int.parse(messageData["requestNumber"].toString())); + break; + case "11": + serviceClass = SystemInternalAuditDetailPage(auditId: int.parse(messageData["requestNumber"].toString())); + break; + case "12": + serviceClass = LoanEquipmentDetailPage(loanId: int.parse(messageData["requestNumber"].toString())); + break; + case "13": + serviceClass = AssetDeliveryPage(requestId: int.parse(messageData["requestNumber"].toString())); + break; + case "14": + serviceClass = MedicalGasInspectionDetailPage(inspectionId: int.parse(messageData["requestNumber"].toString()), prority: messageData["priorityName"]); + break; + case "15": + serviceClass = DemoDetailViewPage(demoId: int.parse(messageData["requestNumber"].toString()), moduleId: 15); + break; + case "16": + serviceClass = IncidentDetailPage(incidentId: int.parse(messageData["requestNumber"].toString())); + break; + case "17": + serviceClass = SurveyPage(surveyId: int.parse(messageData["requestNumber"].toString())); + + default: + serviceClass = const Scaffold(appBar: DefaultAppBar(), body: Center(child: NoDataFound())); + } } Navigator.of(context).push(MaterialPageRoute(builder: (_) => serviceClass!)); diff --git a/lib/models/system_notification_model.dart b/lib/models/system_notification_model.dart index 492c3575..21560a0b 100644 --- a/lib/models/system_notification_model.dart +++ b/lib/models/system_notification_model.dart @@ -24,6 +24,7 @@ class SystemNotificationModel { String? sentDate; int? noOfFailure; int? totalUnreadCount; // Added from API payload + bool? isOldNotification; SystemNotificationModel( {this.userId, @@ -48,6 +49,7 @@ class SystemNotificationModel { this.sent, this.sentDate, this.noOfFailure, + this.isOldNotification, this.totalUnreadCount}); SystemNotificationModel.fromJson(Map? json) { @@ -79,6 +81,7 @@ class SystemNotificationModel { sentDate = json['sentDate']; noOfFailure = json['noOfFailure']; totalUnreadCount = json['totalUnreadCount']; + isOldNotification = json['IsOldNotification']; } } @@ -106,6 +109,7 @@ class SystemNotificationModel { data['sentDate'] = sentDate; data['noOfFailure'] = noOfFailure; data['totalUnreadCount'] = totalUnreadCount; + data['IsOldNotification'] = isOldNotification; return data; } From b1ef110e87c8e4013ee9a9cbf0140032c88936a8 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Thu, 20 Aug 2026 10:04:54 +0300 Subject: [PATCH 27/35] improvement --- .../multiple_item_drop_down_menu.dart | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/lib/new_views/common_widgets/multiple_item_drop_down_menu.dart b/lib/new_views/common_widgets/multiple_item_drop_down_menu.dart index f9884d92..dd199c0c 100644 --- a/lib/new_views/common_widgets/multiple_item_drop_down_menu.dart +++ b/lib/new_views/common_widgets/multiple_item_drop_down_menu.dart @@ -80,8 +80,7 @@ class _MultipleItemDropDownMenuState Date: Thu, 20 Aug 2026 16:06:47 +0300 Subject: [PATCH 28/35] improvement --- lib/modules/demo_module/demo_request_item_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modules/demo_module/demo_request_item_view.dart b/lib/modules/demo_module/demo_request_item_view.dart index 6abc6d58..6d8dd30c 100644 --- a/lib/modules/demo_module/demo_request_item_view.dart +++ b/lib/modules/demo_module/demo_request_item_view.dart @@ -28,7 +28,7 @@ class DemoRequestItemView extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ StatusLabel( - label: requestData!.statusName!, + label: requestData!.statusName, textColor: AppColor.demoRequestStatusTextColor(context, requestData?.statusValue ?? 0), backgroundColor: AppColor.demoRequestStatus(context, requestData?.statusValue ?? 0), ), @@ -54,7 +54,7 @@ class DemoRequestItemView extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ StatusLabel( - label: requestDetails!.status!, + label: requestDetails!.status, textColor: AppColor.demoRequestStatusTextColor(context, requestDetails?.statusValue ?? 0), backgroundColor: AppColor.demoRequestStatus(context, requestDetails?.statusValue ?? 0), ), From a0180eef77e490835365ea0f5bbc171bfb638060 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Sun, 23 Aug 2026 07:19:46 +0300 Subject: [PATCH 29/35] v1.9.1+51 uploaded on stores. --- lib/controllers/api_routes/urls.dart | 10 +++++----- pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart index 7b81739f..ac1b7c1c 100644 --- a/lib/controllers/api_routes/urls.dart +++ b/lib/controllers/api_routes/urls.dart @@ -1,15 +1,15 @@ class URLs { URLs._(); - static const String appReleaseBuildNumber = "50"; + static const String appReleaseBuildNumber = "51"; - // static const host1 = "https://atomsm.hmg.com"; // production url + static const host1 = "https://atomsm.hmg.com"; // production url // static const host1 = "https://atomsmdev.hmg.com"; // local DEV url - static const host1 = "https://atomsmuat.hmg.com"; // local UAT url + // static const host1 = "https://atomsmuat.hmg.com"; // local UAT url - static final String _baseUrl = "$_host/mobile"; // host local UAT + // static final String _baseUrl = "$_host/mobile"; // host local UAT // static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis - // static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM + static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM // static final String _baseUrl = "$_host/v4/mobile"; // v4 for Demo module // static final String _baseUrl = "$_host/v5/mobile"; // v5 for data segregation // static final String _baseUrl = "$_host/v6/mobile"; // for asset delivery module diff --git a/pubspec.yaml b/pubspec.yaml index d1688a44..36416231 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.9.0+50 +version: 1.9.1+51 environment: sdk: ">=3.5.0 <4.0.0" From 83eec2c4d50871cbe569c57e7993c3108a888cdc Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Tue, 25 Aug 2026 11:40:19 +0300 Subject: [PATCH 30/35] bug fixes --- lib/controllers/api_routes/urls.dart | 8 ++++---- .../asset_delivery_module/helper_function.dart | 3 ++- .../pages/asset_delivery_page.dart | 12 ++++++++++-- .../pages/attachment_view.dart | 11 ++++++----- .../delivery_inspection_form_view.dart | 4 ++-- .../end_user_asset_detail_card.dart | 2 +- .../end_user_cost_center_list_view.dart | 2 +- .../update_end_user_cost_center_details_view.dart | 2 +- lib/views/widgets/e_signature/e_signature.dart | 13 +++---------- 9 files changed, 30 insertions(+), 27 deletions(-) diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart index ac1b7c1c..0a3ca470 100644 --- a/lib/controllers/api_routes/urls.dart +++ b/lib/controllers/api_routes/urls.dart @@ -3,13 +3,13 @@ class URLs { static const String appReleaseBuildNumber = "51"; - static const host1 = "https://atomsm.hmg.com"; // production url + // static const host1 = "https://atomsm.hmg.com"; // production url // static const host1 = "https://atomsmdev.hmg.com"; // local DEV url - // static const host1 = "https://atomsmuat.hmg.com"; // local UAT url + static const host1 = "https://atomsmuat.hmg.com"; // local UAT url - // static final String _baseUrl = "$_host/mobile"; // host local UAT + static final String _baseUrl = "$_host/mobile"; // host local UAT // static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis - static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM + // static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM // static final String _baseUrl = "$_host/v4/mobile"; // v4 for Demo module // static final String _baseUrl = "$_host/v5/mobile"; // v5 for data segregation // static final String _baseUrl = "$_host/v6/mobile"; // for asset delivery module diff --git a/lib/modules/asset_delivery_module/helper_function.dart b/lib/modules/asset_delivery_module/helper_function.dart index 747c34c6..c8948b6f 100644 --- a/lib/modules/asset_delivery_module/helper_function.dart +++ b/lib/modules/asset_delivery_module/helper_function.dart @@ -8,6 +8,7 @@ import 'package:test_sa/models/lookup.dart'; import 'package:test_sa/modules/asset_delivery_module/pages/attachment_view.dart'; import 'package:test_sa/modules/asset_delivery_module/provider/asset_delivery_provider.dart'; import 'package:test_sa/modules/asset_delivery_module/provider/attachment_type_provider.dart'; +import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart'; import 'package:test_sa/new_views/common_widgets/single_item_drop_down_menu.dart'; import 'package:test_sa/new_views/swipe_module/dialoge/acknowledge_work_dialog.dart'; @@ -67,7 +68,7 @@ class HelperFunction { static Future showAttachmentTypeBottomSheet(BuildContext context) { return showModalBottomSheet( context: context, - isScrollControlled: true, + isScrollControlled: true,backgroundColor: AppColor.bottomSheetColor(context), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), diff --git a/lib/modules/asset_delivery_module/pages/asset_delivery_page.dart b/lib/modules/asset_delivery_module/pages/asset_delivery_page.dart index 664b4617..43e5a516 100644 --- a/lib/modules/asset_delivery_module/pages/asset_delivery_page.dart +++ b/lib/modules/asset_delivery_module/pages/asset_delivery_page.dart @@ -25,6 +25,7 @@ import 'package:test_sa/views/widgets/item_views/info_text_widget.dart'; class AssetDeliveryPage extends StatefulWidget { final int? requestId; + // final String? poInternal; // final String? poExternal; @@ -144,7 +145,14 @@ class _AssetDeliveryPageState extends State { ], ), 16.height, - assetDeliveryTableList(context: context, assetDeliveryProvider: provider), + Selector( + selector: (_, provider) => provider.isDeliveryTableListLoading, + builder: (context, isLoading, child) { + return isLoading + ? SizedBox(height: 200.toScreenHeight, child: CircularProgressIndicator(color: AppColor.loadingColor(context)).center) + : assetDeliveryTableList(context: context, assetDeliveryProvider: provider); + }) + // assetDeliveryTableList(context: context, assetDeliveryProvider: provider), ], ), ); @@ -167,7 +175,7 @@ class _AssetDeliveryPageState extends State { // children: [ // Checkbox( // value: dataModel!.isHMG, - // activeColor: AppColor.neutral120, + // activeColor: AppColor.neutral120,x // materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, // visualDensity: const VisualDensity(horizontal: -4, vertical: -3), // onChanged: (value) {}, diff --git a/lib/modules/asset_delivery_module/pages/attachment_view.dart b/lib/modules/asset_delivery_module/pages/attachment_view.dart index 5759ebe8..57d9cecb 100644 --- a/lib/modules/asset_delivery_module/pages/attachment_view.dart +++ b/lib/modules/asset_delivery_module/pages/attachment_view.dart @@ -65,7 +65,6 @@ class _AssetDeliveryAttachmentViewState extends State : Row( children: [ AppFilledButton( - buttonColor: AppColor.white60, + buttonColor: AppColor.primary10, label: 'Attachments'.addTranslation, maxWidth: true, - textColor: AppColor.black10, + // textColor: AppColor.black10, onPressed: () { HelperFunction.attachmentTap(context: context, viewOnly: true, assetDeliveryProvider: assetDeliveryProvider!, deliveryTableItemId: widget.deliveryTableModel.id); }).expanded, diff --git a/lib/modules/asset_delivery_module/pages/end_user_acceptance/end_user_asset_detail_card.dart b/lib/modules/asset_delivery_module/pages/end_user_acceptance/end_user_asset_detail_card.dart index f023c938..20a55189 100644 --- a/lib/modules/asset_delivery_module/pages/end_user_acceptance/end_user_asset_detail_card.dart +++ b/lib/modules/asset_delivery_module/pages/end_user_acceptance/end_user_asset_detail_card.dart @@ -36,7 +36,7 @@ class EndUserAssetDetailCard extends StatelessWidget { ) : const SizedBox(), if (!isViewOnly) - 'edit_icon'.toSvgAsset().onPress(() { + 'edit_icon'.toSvgAsset(color: AppColor.icon2Color(context)).onPress(() { if (editPress != null) { editPress!(); } diff --git a/lib/modules/asset_delivery_module/pages/end_user_acceptance/end_user_cost_center_list_view.dart b/lib/modules/asset_delivery_module/pages/end_user_acceptance/end_user_cost_center_list_view.dart index dc81b8dc..523b51fe 100644 --- a/lib/modules/asset_delivery_module/pages/end_user_acceptance/end_user_cost_center_list_view.dart +++ b/lib/modules/asset_delivery_module/pages/end_user_acceptance/end_user_cost_center_list_view.dart @@ -111,7 +111,7 @@ class _EndUserCostCenterLIstViewState extends State { : FooterActionButton.footerContainer( context: context, child: AppFilledButton( - buttonColor: AppColor.white60, + buttonColor: AppColor.primary10, label: 'Attachments'.addTranslation, maxWidth: true, textColor: AppColor.black10, diff --git a/lib/modules/asset_delivery_module/pages/end_user_acceptance/update_end_user_cost_center_details_view.dart b/lib/modules/asset_delivery_module/pages/end_user_acceptance/update_end_user_cost_center_details_view.dart index ba82f75f..715395be 100644 --- a/lib/modules/asset_delivery_module/pages/end_user_acceptance/update_end_user_cost_center_details_view.dart +++ b/lib/modules/asset_delivery_module/pages/end_user_acceptance/update_end_user_cost_center_details_view.dart @@ -147,7 +147,7 @@ class _UpdateEndUserCostCenterDetailsViewState extends State 8.height, + separatorBuilder: (_, __) => 12.height, padding: EdgeInsets.zero, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), diff --git a/lib/views/widgets/e_signature/e_signature.dart b/lib/views/widgets/e_signature/e_signature.dart index 63615089..b8a1ead6 100644 --- a/lib/views/widgets/e_signature/e_signature.dart +++ b/lib/views/widgets/e_signature/e_signature.dart @@ -93,17 +93,10 @@ class _ESignatureState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - widget.title.tinyFont(context).paddingOnly(top: 8), + InfoTextLabelWidget(label: widget.title), (!_editable - ? "edit".toSvgAsset(width: 48).paddingOnly(top: 8) - : Container( - width: 48.toScreenWidth, - height: 48.toScreenWidth, - decoration: BoxDecoration(borderRadius: BorderRadius.circular(100), border: Border.all(color: AppColor.neutral30)), - padding: const EdgeInsets.all(11), - margin: const EdgeInsets.only(top: 8), - child: "done".toSvgAsset(width: 26), - )) + ? "edit_icon".toSvgAsset(color: AppColor.icon2Color(context)) + : "done".toSvgAsset(width: 24)) .onPress(() async { if (_editable) { signature = await _controller!.toPngBytes(); From 08dfc807fce6971fac40d989994fdff6ab78cccd Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:23:45 +0300 Subject: [PATCH 31/35] bug fixes attachment can be uploaded at any stage --- .../helper_function.dart | 7 +- .../pages/attachment_view.dart | 91 ++++++++++--------- .../delivery_inspection_form_view.dart | 2 +- .../technical_inspection_lines_list_view.dart | 6 +- 4 files changed, 55 insertions(+), 51 deletions(-) diff --git a/lib/modules/asset_delivery_module/helper_function.dart b/lib/modules/asset_delivery_module/helper_function.dart index c8948b6f..ba1c2584 100644 --- a/lib/modules/asset_delivery_module/helper_function.dart +++ b/lib/modules/asset_delivery_module/helper_function.dart @@ -35,14 +35,14 @@ class HelperFunction { builder: (context) => AssetDeliveryAttachmentView( tableItemId: deliveryTableItemId, attachmentType: result!.value, - viewOnly: viewOnly, + // viewOnly: viewOnly, attachmentList: list, ))); // } } } - static void cancelRequest({required BuildContext context, int ?itemId, int? requestId}) async { + static void cancelRequest({required BuildContext context, int? itemId, int? requestId}) async { AssetDeliveryProvider assetDeliveryProvider = Provider.of(context, listen: false); showDialog( context: context, @@ -68,7 +68,8 @@ class HelperFunction { static Future showAttachmentTypeBottomSheet(BuildContext context) { return showModalBottomSheet( context: context, - isScrollControlled: true,backgroundColor: AppColor.bottomSheetColor(context), + isScrollControlled: true, + backgroundColor: AppColor.bottomSheetColor(context), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), diff --git a/lib/modules/asset_delivery_module/pages/attachment_view.dart b/lib/modules/asset_delivery_module/pages/attachment_view.dart index 57d9cecb..fc9d8fbd 100644 --- a/lib/modules/asset_delivery_module/pages/attachment_view.dart +++ b/lib/modules/asset_delivery_module/pages/attachment_view.dart @@ -24,13 +24,13 @@ import 'package:test_sa/views/widgets/loaders/no_data_found.dart'; class AssetDeliveryAttachmentView extends StatefulWidget { final int? tableItemId; final int? attachmentType; - bool viewOnly = false; + // bool viewOnly = false; final List attachmentList; AssetDeliveryAttachmentView({ super.key, required this.attachmentList, - this.viewOnly = false, + // this.viewOnly = false, this.attachmentType, this.tableItemId, }); @@ -74,51 +74,54 @@ class _AssetDeliveryAttachmentViewState extends State URLs.getFileUrl(e.name ?? '') ?? '').toList() ?? []).toShadowContainer( - context, - borderRadius: 20, - padding: 12, - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - ), - ], - ), - ), - if (widget.attachmentType == 5 && !widget.viewOnly) - FooterActionButton.footerContainer( - context: context, - child: AppFilledButton( - buttonColor: AppColor.primary10, - label: 'Save'.addTranslation, - maxWidth: true, - onPressed: _onSavePressed, - ), - ), + // widget.attachmentType == 5 && !widget.viewOnly + widget.attachmentType == 5 //only add the attachment for current + ? AttachmentPicker( + label: context.translation.attachments, + attachment: _attachments, + buttonColor: AppColor.primary10, + onlyImages: false, + showAsListView: true, + buttonIcon: 'quotation_icon'.toSvgAsset(color: AppColor.primary10), + onChange: (attachment) {}, + ).toShadowContainer( + context, + borderRadius: 20, + padding: 12, + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ) + : _attachments.isNotEmpty + ? FilesList(images: _attachments.map((e) => URLs.getFileUrl(e.name ?? '') ?? '').toList() ?? []).toShadowContainer( + context, + borderRadius: 20, + padding: 12, + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ) + : const NoDataFound().center.expanded, ], ), + ), + if (widget.attachmentType == 5) + FooterActionButton.footerContainer( + context: context, + child: AppFilledButton( + buttonColor: AppColor.primary10, + label: 'Save'.addTranslation, + maxWidth: true, + onPressed: _onSavePressed, + ), + ), + ], + ), ); } diff --git a/lib/modules/asset_delivery_module/pages/delivery_inspection/delivery_inspection_form_view.dart b/lib/modules/asset_delivery_module/pages/delivery_inspection/delivery_inspection_form_view.dart index 14c70c87..8d5f31d8 100644 --- a/lib/modules/asset_delivery_module/pages/delivery_inspection/delivery_inspection_form_view.dart +++ b/lib/modules/asset_delivery_module/pages/delivery_inspection/delivery_inspection_form_view.dart @@ -160,7 +160,7 @@ class _DeliveryInspectionFormViewState extends State maxWidth: true, // textColor: AppColor.black10, onPressed: () { - HelperFunction.attachmentTap(context: context, viewOnly: true, assetDeliveryProvider: assetDeliveryProvider!, deliveryTableItemId: widget.deliveryTableModel.id); + HelperFunction.attachmentTap(context: context, viewOnly: viewOnly, assetDeliveryProvider: assetDeliveryProvider!, deliveryTableItemId: widget.deliveryTableModel.id); }).expanded, 8.width, AppFilledButton(buttonColor: AppColor.primary10, label: 'Next'.addTranslation, maxWidth: true, onPressed: _nextTap).expanded, diff --git a/lib/modules/asset_delivery_module/pages/technical_inpection/technical_inspection_lines_list_view.dart b/lib/modules/asset_delivery_module/pages/technical_inpection/technical_inspection_lines_list_view.dart index 082d9d09..aca0992d 100644 --- a/lib/modules/asset_delivery_module/pages/technical_inpection/technical_inspection_lines_list_view.dart +++ b/lib/modules/asset_delivery_module/pages/technical_inpection/technical_inspection_lines_list_view.dart @@ -161,13 +161,13 @@ class _TechnicalInspectionLinesListViewState extends State Date: Wed, 26 Aug 2026 13:24:16 +0300 Subject: [PATCH 32/35] improvements --- .../pages/delivery_inspection/cost_center_form_view.dart | 8 +++++--- .../delivery_inspection_form_view.dart | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/modules/asset_delivery_module/pages/delivery_inspection/cost_center_form_view.dart b/lib/modules/asset_delivery_module/pages/delivery_inspection/cost_center_form_view.dart index d7e7cb17..bc3cb5f3 100644 --- a/lib/modules/asset_delivery_module/pages/delivery_inspection/cost_center_form_view.dart +++ b/lib/modules/asset_delivery_module/pages/delivery_inspection/cost_center_form_view.dart @@ -81,7 +81,9 @@ class _CostCenterFormViewState extends State { await costCenterProvider.getData(id: widget.requestId); } } - setState(() {}); + if(mounted) { + setState(() {}); + } } void addItem(List list, T Function() creator) { @@ -190,7 +192,7 @@ class _CostCenterFormViewState extends State { }, ); }, - ).expanded; + ); } Widget _buildSerialNumberList() { @@ -225,7 +227,7 @@ class _CostCenterFormViewState extends State { }, ); }, - ).expanded; + ); } Widget selectionButton() { diff --git a/lib/modules/asset_delivery_module/pages/delivery_inspection/delivery_inspection_form_view.dart b/lib/modules/asset_delivery_module/pages/delivery_inspection/delivery_inspection_form_view.dart index 8d5f31d8..e2229969 100644 --- a/lib/modules/asset_delivery_module/pages/delivery_inspection/delivery_inspection_form_view.dart +++ b/lib/modules/asset_delivery_module/pages/delivery_inspection/delivery_inspection_form_view.dart @@ -288,7 +288,7 @@ class _DeliveryInspectionFormViewState extends State ADatePicker( label: 'Inspected Date/Time From'.addTranslation, hideShadow: true, - height: 56.toScreenHeight, + // height: 56.toScreenHeight, backgroundColor: AppColor.fieldBgColor(context), date: model.deliveryInspectionInspectedDateFrom, from: DateTime(DateTime.now().year, 1, 1), @@ -318,7 +318,7 @@ class _DeliveryInspectionFormViewState extends State ADatePicker( label: 'Inspected Date/Time To'.addTranslation, hideShadow: true, - height: 56.toScreenHeight, + // height: 56.toScreenHeight, backgroundColor: AppColor.fieldBgColor(context), date: model.deliveryInspectionInspectedDateTo, from: model.deliveryInspectionInspectedDateFrom, From 6acc21fc6a97c9bdf53eaa5205a914e99b338878 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Thu, 27 Aug 2026 14:21:05 +0300 Subject: [PATCH 33/35] improvements, hide swipe option for FM --- lib/dashboard_latest/dashboard_view.dart | 2 +- lib/new_views/common_widgets/app_drawer.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/dashboard_latest/dashboard_view.dart b/lib/dashboard_latest/dashboard_view.dart index 7b240bdb..91c30606 100644 --- a/lib/dashboard_latest/dashboard_view.dart +++ b/lib/dashboard_latest/dashboard_view.dart @@ -230,7 +230,7 @@ class _DashboardViewState extends State { ), ), ), - if (user != null && user.employeeIsHMG == false) + if (user != null && settingProvider.isUserFlowMedical == false && user.employeeIsHMG == false) Positioned( right: 20.toScreenWidth, bottom: 20.toScreenHeight, diff --git a/lib/new_views/common_widgets/app_drawer.dart b/lib/new_views/common_widgets/app_drawer.dart index 7ec66c40..0f6c0a9f 100644 --- a/lib/new_views/common_widgets/app_drawer.dart +++ b/lib/new_views/common_widgets/app_drawer.dart @@ -78,7 +78,7 @@ class AppDrawer extends StatelessWidget { 18.height, drawerItem("site_request_icon", "Site Request", context).onPress(() => Navigator.of(context).pushNamed(SiteRequestHistoryScreen.routeName)), 18.height, - if (userProvider.user != null && userProvider.user?.employeeIsHMG == false) + if (userProvider.user != null && settingProvider.isUserFlowMedical == false && userProvider.user?.employeeIsHMG == false) drawerItem("swipe", "Swipe History", context).onPress(() => Navigator.of(context).pushNamed(SwipeHistoryView.routeName)), // 18.height, // drawerItem("report", context.translation.reportBg, context) /*.onPress(() => Navigator.of(context).pushNamed(ReportBugPage.id))*/, From 071dd082b36f15be33485de976c5afe3180f820d Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Thu, 27 Aug 2026 15:13:19 +0300 Subject: [PATCH 34/35] external po & internal po added in search --- .../providers/api/all_requests_provider.dart | 2 ++ lib/dashboard_latest/dashboard_view.dart | 3 +- lib/models/search_all_requests_model.dart | 32 +++++++++++++++++++ .../my_request/all_requests_filter_page.dart | 4 +++ .../my_request/all_requests_search_page.dart | 15 ++++++++- .../my_request/my_requests_page.dart | 30 +++++++++++++++++ 6 files changed, 84 insertions(+), 2 deletions(-) diff --git a/lib/controllers/providers/api/all_requests_provider.dart b/lib/controllers/providers/api/all_requests_provider.dart index 45f62f80..fe447042 100644 --- a/lib/controllers/providers/api/all_requests_provider.dart +++ b/lib/controllers/providers/api/all_requests_provider.dart @@ -225,6 +225,8 @@ class AllRequestsProvider extends ChangeNotifier { if (search?.assetName?.value?.isNotEmpty ?? false) "assetName": search!.assetName!.value, if (search?.assetNo?.value?.isNotEmpty ?? false) "assetNumber": search!.assetNo!.value, if (search?.sn?.value?.isNotEmpty ?? false) "assetSerialNo": search!.sn!.value, + if (search?.externalPo?.value?.isNotEmpty ?? false) "externalPo": search!.externalPo!.value, + if (search?.internalPo?.value?.isNotEmpty ?? false) "internalPo": search!.internalPo!.value, if (search?.model?.value?.isNotEmpty ?? false) "model": search!.model!.value, if (search?.manufacture?.value?.isNotEmpty ?? false) "manufacturer": search!.manufacture!.value, if (search?.startDate != null) "from": search!.startDate!.toIso8601String(), diff --git a/lib/dashboard_latest/dashboard_view.dart b/lib/dashboard_latest/dashboard_view.dart index 91c30606..af51b0bb 100644 --- a/lib/dashboard_latest/dashboard_view.dart +++ b/lib/dashboard_latest/dashboard_view.dart @@ -198,6 +198,7 @@ class _DashboardViewState extends State { @override Widget build(BuildContext context) { final user = Provider.of(context, listen: false).user; + bool isUserFlowMedical = Provider.of(context, listen: false).isUserFlowMedical; bool isEngineer = (user?.type) == UsersTypes.engineer; ///Need to check which info need to show on dashboard for quality user. @@ -230,7 +231,7 @@ class _DashboardViewState extends State { ), ), ), - if (user != null && settingProvider.isUserFlowMedical == false && user.employeeIsHMG == false) + if (user != null && isUserFlowMedical == false && user.employeeIsHMG == false) Positioned( right: 20.toScreenWidth, bottom: 20.toScreenHeight, diff --git a/lib/models/search_all_requests_model.dart b/lib/models/search_all_requests_model.dart index f6d7b585..3adaef90 100644 --- a/lib/models/search_all_requests_model.dart +++ b/lib/models/search_all_requests_model.dart @@ -8,6 +8,8 @@ class SearchAllRequestsModel { SearchByManufactureModel? manufacture; // Now nullable SearchBySNModel? sn; // Now nullable SearchByRequestModel? model; // Now nullable + SearchByExternalPO? externalPo; // Now nullable + SearchByInternalPO? internalPo; // Now nullable SearchAllRequestsBaseModel? searchBySelectedValue; // Now nullable List? statuses; // Now nullable List? typeTransaction; // Now nullable @@ -21,6 +23,8 @@ class SearchAllRequestsModel { this.assetNo, this.manufacture, this.sn, + this.externalPo, + this.internalPo, this.model, this.statuses, this.typeTransaction, @@ -70,6 +74,16 @@ class SearchAllRequestsModel { sn?.value = null; } + if (field != 'externalPo') { + externalPo?.controller?.text = ""; + externalPo?.value = null; + } + + if (field != 'internalPo') { + internalPo?.controller?.text = ""; + internalPo?.value = null; + } + if (field != 'model') { model?.controller?.text = ""; model?.value = null; @@ -128,6 +142,14 @@ class SearchAllRequestsModel { json["assetSerialNo"] = sn!.value; } + if (externalPo?.value?.isNotEmpty ?? false) { + json["externalPo"] = externalPo!.value; + } + + if (internalPo?.value?.isNotEmpty ?? false) { + json["internalPo"] = internalPo!.value; + } + if (model?.value?.isNotEmpty ?? false) { json["model"] = model!.value; } @@ -187,3 +209,13 @@ class SearchByRequestModel extends SearchAllRequestsBaseModel { SearchByRequestModel(BuildContext context, {TextEditingController? controller, String? value}) // Parameters now nullable : super(context, controller: controller, label: context.translation.model, value: value); } + +class SearchByExternalPO extends SearchAllRequestsBaseModel { + SearchByExternalPO(BuildContext context, {TextEditingController? controller, String? value}) // Parameters now nullable + : super(context, controller: controller, label: "External PO", value: value); +} + +class SearchByInternalPO extends SearchAllRequestsBaseModel { + SearchByInternalPO(BuildContext context, {TextEditingController? controller, String? value}) // Parameters now nullable + : super(context, controller: controller, label: "Internal PO", value: value); +} diff --git a/lib/new_views/pages/land_page/my_request/all_requests_filter_page.dart b/lib/new_views/pages/land_page/my_request/all_requests_filter_page.dart index 68f27fad..93a22f8b 100644 --- a/lib/new_views/pages/land_page/my_request/all_requests_filter_page.dart +++ b/lib/new_views/pages/land_page/my_request/all_requests_filter_page.dart @@ -57,6 +57,8 @@ class _AllRequestsFilterPageState extends State { search!.assetNo?.controller?.text = search?.assetNo!.value ?? ""; search!.manufacture?.controller?.text = search?.manufacture!.value ?? ""; search!.sn?.controller?.text = search?.sn?.value ?? ""; + search!.externalPo?.controller?.text = search?.externalPo?.value ?? ""; + search!.internalPo?.controller?.text = search?.internalPo?.value ?? ""; search!.model?.controller?.text = search?.model?.value ?? ""; } super.initState(); @@ -74,6 +76,8 @@ class _AllRequestsFilterPageState extends State { manufacture: SearchByManufactureModel(context, controller: TextEditingController()), sn: SearchBySNModel(context, controller: TextEditingController()), model: SearchByRequestModel(context, controller: TextEditingController()), + externalPo: SearchByExternalPO(context, controller: TextEditingController()), + internalPo: SearchByInternalPO(context, controller: TextEditingController()), ); search!.searchBySelectedValue = search?.requestNumber; } diff --git a/lib/new_views/pages/land_page/my_request/all_requests_search_page.dart b/lib/new_views/pages/land_page/my_request/all_requests_search_page.dart index b49834b4..1f430cdb 100644 --- a/lib/new_views/pages/land_page/my_request/all_requests_search_page.dart +++ b/lib/new_views/pages/land_page/my_request/all_requests_search_page.dart @@ -58,6 +58,8 @@ class _AllRequestsSearchPageState extends State { search!.assetNo?.controller?.text = search?.assetNo?.value ?? ""; search!.manufacture?.controller?.text = search?.manufacture?.value ?? ""; search!.sn?.controller?.text = search?.sn?.value ?? ""; + search!.externalPo?.controller?.text = search?.externalPo?.value ?? ""; + search!.internalPo?.controller?.text = search?.internalPo?.value ?? ""; search!.model?.controller?.text = search?.model?.value ?? ""; } super.initState(); @@ -73,6 +75,8 @@ class _AllRequestsSearchPageState extends State { manufacture: SearchByManufactureModel(context, controller: TextEditingController()), sn: SearchBySNModel(context, controller: TextEditingController()), model: SearchByRequestModel(context, controller: TextEditingController()), + externalPo: SearchByExternalPO(context, controller: TextEditingController()), + internalPo: SearchByInternalPO(context, controller: TextEditingController()), ); search!.searchBySelectedValue = search?.requestNumber; } @@ -121,7 +125,16 @@ class _AllRequestsSearchPageState extends State { Text(context.translation.searchBy, style: AppTextStyles.heading5.copyWith(color: AppColor.labelTextStyleColor(context))), 8.height, RequestsSearchButtons( - items: [search!.requestNumber!, search!.assetName!, search!.assetNo!, search!.manufacture!, search!.sn!, search!.model!], + items: [ + search!.requestNumber!, + search!.assetName!, + search!.assetNo!, + search!.manufacture!, + search!.sn!, + search!.model!, + search!.externalPo!, + search!.internalPo!, + ], initialValue: search!.searchBySelectedValue!, onSelect: (value) { if (search!.searchBySelectedValue != value) { diff --git a/lib/new_views/pages/land_page/my_request/my_requests_page.dart b/lib/new_views/pages/land_page/my_request/my_requests_page.dart index 2dc66f86..402bbd04 100644 --- a/lib/new_views/pages/land_page/my_request/my_requests_page.dart +++ b/lib/new_views/pages/land_page/my_request/my_requests_page.dart @@ -353,6 +353,8 @@ class _MyRequestsPageState extends State with SingleTickerProvid (_search!.assetName?.value?.isNotEmpty ?? false) || (_search!.manufacture?.value?.isNotEmpty ?? false) || (_search!.sn?.value?.isNotEmpty ?? false) || + (_search!.externalPo?.value?.isNotEmpty ?? false) || + (_search!.internalPo?.value?.isNotEmpty ?? false) || (_search!.statuses?.isNotEmpty ?? false) || (_search!.typeTransaction?.isNotEmpty ?? false) || (_search!.startDate != null) || @@ -481,6 +483,30 @@ class _MyRequestsPageState extends State with SingleTickerProvid }, )); } + if (_search?.externalPo?.value?.isNotEmpty ?? false) { + chips.add(_buildFilterChip( + label: 'External PO: ${_search!.externalPo!.value}', + onDelete: () { + setState(() { + _search!.externalPo!.value = null; + _search!.externalPo!.controller?.clear(); + _refreshWithFilters(); + }); + }, + )); + } + if (_search?.internalPo?.value?.isNotEmpty ?? false) { + chips.add(_buildFilterChip( + label: 'Internal PO: ${_search!.internalPo!.value}', + onDelete: () { + setState(() { + _search!.internalPo!.value = null; + _search!.internalPo!.controller?.clear(); + _refreshWithFilters(); + }); + }, + )); + } // Model if (_search?.model?.value?.isNotEmpty ?? false) { @@ -664,6 +690,10 @@ class _MyRequestsPageState extends State with SingleTickerProvid _search?.manufacture?.controller?.clear(); _search?.sn?.value = null; _search?.sn?.controller?.clear(); + _search?.externalPo?.value = null; + _search?.externalPo?.controller?.clear(); + _search?.internalPo?.value = null; + _search?.internalPo?.controller?.clear(); _search?.model?.value = null; _search?.model?.controller?.clear(); _search?.statuses?.clear(); From 523ce2a98667663acc21a699a650ac795fb1077a Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Thu, 27 Aug 2026 16:32:49 +0300 Subject: [PATCH 35/35] v1.9.2+52 uploaded. --- lib/controllers/api_routes/urls.dart | 10 +++++----- pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart index 0a3ca470..de0033a9 100644 --- a/lib/controllers/api_routes/urls.dart +++ b/lib/controllers/api_routes/urls.dart @@ -1,15 +1,15 @@ class URLs { URLs._(); - static const String appReleaseBuildNumber = "51"; + static const String appReleaseBuildNumber = "52"; - // static const host1 = "https://atomsm.hmg.com"; // production url + static const host1 = "https://atomsm.hmg.com"; // production url // static const host1 = "https://atomsmdev.hmg.com"; // local DEV url - static const host1 = "https://atomsmuat.hmg.com"; // local UAT url + // static const host1 = "https://atomsmuat.hmg.com"; // local UAT url - static final String _baseUrl = "$_host/mobile"; // host local UAT + // static final String _baseUrl = "$_host/mobile"; // host local UAT // static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis - // static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM + static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM // static final String _baseUrl = "$_host/v4/mobile"; // v4 for Demo module // static final String _baseUrl = "$_host/v5/mobile"; // v5 for data segregation // static final String _baseUrl = "$_host/v6/mobile"; // for asset delivery module diff --git a/pubspec.yaml b/pubspec.yaml index 36416231..63478491 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.9.1+51 +version: 1.9.2+52 environment: sdk: ">=3.5.0 <4.0.0"