notifications apis integrated

ui_ux_rollout_merge_notification_settings
WaseemAbbasi22 7 days ago
parent 65335dca87
commit 9ab4ed6964

@ -0,0 +1,5 @@
<svg width="14" height="9" viewBox="0 0 14 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.2983 1.2631C10.6276 1.09844 10.7611 0.697989 10.5965 0.36867C10.4318 0.0393513 10.0313 -0.0941316 9.70202 0.0705279C8.12823 0.857424 6.54486 2.42431 5.28996 3.87608C5.04918 4.15463 5.07981 4.57563 5.35836 4.8164C5.63691 5.05718 6.05791 5.02656 6.29868 4.74801C7.536 3.31658 8.9753 1.9246 10.2983 1.2631Z" fill="#3DA5E5"/>
<path d="M0.937592 4.94649C0.601136 4.79696 0.207161 4.94849 0.0576253 5.28494C-0.0904934 5.61821 0.0567727 6.00791 0.386557 6.16059C0.388304 6.16147 0.391922 6.16332 0.397355 6.16623C0.41272 6.17447 0.442691 6.19128 0.485999 6.21884C0.572533 6.2739 0.712934 6.3723 0.896879 6.53172C1.2645 6.85032 1.80902 7.41527 2.44547 8.36995C2.5726 8.56065 2.78878 8.67267 3.01789 8.66658C3.24698 8.66049 3.4572 8.53661 3.57403 8.33946L3.577 8.3345L3.5909 8.31156C3.60369 8.29058 3.62348 8.25844 3.65012 8.21608C3.70339 8.13135 3.78397 8.00585 3.89053 7.84726C4.09588 7.54165 4.01459 7.12744 3.70898 6.92209C3.49769 6.78012 3.23449 6.77516 3.02429 6.88662C2.5429 6.25821 2.11389 5.82206 1.77012 5.52413C1.5374 5.32244 1.34447 5.18472 1.20183 5.09395C1.0989 5.02536 0.982785 4.96707 0.937592 4.94649Z" fill="#3DA5E5"/>
<path d="M13.6316 1.2631C13.961 1.09844 14.0944 0.697989 13.9298 0.36867C13.7651 0.0393513 13.3647 -0.0941316 13.0354 0.0705279C11.2311 0.972653 9.40453 2.90685 8.06812 4.5356C7.38925 5.36297 6.81727 6.13626 6.41498 6.70264C6.38209 6.74895 6.35032 6.79389 6.31971 6.83738C5.8537 6.23534 5.43817 5.81422 5.10345 5.52413C4.87073 5.32244 4.6778 5.18472 4.53517 5.09395C4.45256 5.03938 4.32459 4.97291 4.27093 4.94649C3.93447 4.79696 3.54049 4.94849 3.39096 5.28494C3.24284 5.61821 3.39011 6.00791 3.71989 6.16059C3.72164 6.16147 3.72526 6.16332 3.73069 6.16623C3.74605 6.17447 3.77602 6.19128 3.81933 6.21884C3.90587 6.2739 4.04627 6.3723 4.23021 6.53172C4.59783 6.85032 5.14235 7.41527 5.7788 8.36995C5.90334 8.55676 6.11355 8.66833 6.33807 8.6668C6.56255 8.66526 6.7715 8.55034 6.89348 8.3619L6.89508 8.35944L6.9022 8.34852L6.93185 8.3034C6.95834 8.26328 6.9981 8.20348 7.05015 8.12634C7.15429 7.97205 7.30751 7.74858 7.50202 7.47474C7.8914 6.92653 8.44442 6.17899 9.09888 5.38136C10.4291 3.76011 12.1026 2.02764 13.6316 1.2631Z" fill="#3DA5E5"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@ -0,0 +1,4 @@
<svg width="21" height="23" viewBox="0 0 21 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.0125 1.44721C11.0932 1.60832 10.9314 1.8355 10.6079 2.28986C9.91025 3.26945 9.5 4.46755 9.5 5.76138C9.5 9.07218 12.1863 11.7561 15.5 11.7561C16.0799 11.7561 16.6405 11.6739 17.171 11.5206C17.7329 11.3581 18.0138 11.2769 18.1632 11.328C18.3126 11.3792 18.4902 11.6231 18.8454 12.111L18.8953 12.1794C19.0559 12.3997 19.2328 12.6424 19.3697 12.8561C19.6255 13.2556 19.8754 13.7373 19.9613 14.2985C20.2416 16.1297 18.9494 17.3153 17.6625 17.8466C16.8262 18.1919 15.931 18.4734 14.9988 18.6913C15.0049 18.7949 14.9948 18.9013 14.9667 19.0071C14.3896 21.1786 12.3697 22.7502 10.0002 22.7502C7.63084 22.7502 5.61087 21.1786 5.03379 19.0071C5.00568 18.9013 4.99562 18.795 5.00171 18.6914C4.06928 18.4735 3.17396 18.1919 2.33746 17.8466C1.05056 17.3153 -0.241638 16.1297 0.0386759 14.2985C0.124594 13.7373 0.374521 13.2556 0.630335 12.8561C0.775277 12.6298 0.966356 12.3685 1.13291 12.1407C1.76864 11.2713 1.92776 10.3598 1.92786 9.28573C1.92789 4.85418 5.54419 1.26583 10 1.26583C10.0497 1.26583 10.1105 1.26264 10.1775 1.25912C10.4766 1.24343 10.8993 1.22125 11.0125 1.44721ZM7.19327 19.079C9.04012 19.307 10.9604 19.307 12.8073 19.079C12.3003 20.0591 11.2441 20.7502 10.0002 20.7502C8.75643 20.7502 7.7002 20.0591 7.19327 19.079Z" fill="#767676"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.0001 1C17.0001 0.447715 16.5524 0 16.0001 0C15.4478 0 15.0001 0.447715 15.0001 1V1.39864C14.2597 1.56884 13.5904 1.92409 13.0489 2.41399L12.5409 2.08741C12.0763 1.78874 11.4576 1.92323 11.159 2.38779C10.8603 2.85236 10.9948 3.47107 11.4593 3.76974L11.9236 4.06817C11.7576 4.51495 11.6668 4.99727 11.6668 5.5C11.6668 6.00281 11.7577 6.48521 11.9236 6.93205L11.4598 7.23026C10.9952 7.52893 10.8607 8.14764 11.1594 8.61221C11.458 9.07677 12.0768 9.21126 12.5413 8.91259L13.0491 8.58617C13.5905 9.07598 14.2598 9.43118 15.0001 9.60137V10C15.0001 10.5523 15.4478 11 16.0001 11C16.5524 11 17.0001 10.5523 17.0001 10V9.60137C17.7404 9.43118 18.4097 9.07598 18.9512 8.58617L19.4589 8.91259C19.9235 9.21126 20.5422 9.07677 20.8409 8.61221C21.1395 8.14764 21.005 7.52893 20.5405 7.23026L20.0766 6.93205C20.2426 6.48521 20.3335 6.00281 20.3335 5.5C20.3335 4.99727 20.2426 4.51495 20.0767 4.06817L20.5409 3.76974C21.0055 3.47107 21.1399 2.85236 20.8413 2.38779C20.5426 1.92323 19.9239 1.78874 19.4593 2.08741L18.9513 2.41399C18.4099 1.92409 17.7405 1.56884 17.0001 1.39864V1ZM14.0483 4.28631C14.4615 3.68984 15.1753 3.28571 16.0001 3.28571C16.825 3.28571 17.5387 3.68984 17.9519 4.28631C18.1945 4.6366 18.3335 5.05259 18.3335 5.5C18.3335 5.94747 18.1945 6.36351 17.9518 6.71382C17.5386 7.31022 16.8249 7.71429 16.0001 7.71429C15.1753 7.71429 14.4616 7.31022 14.0484 6.71382C13.8057 6.36351 13.6668 5.94747 13.6668 5.5C13.6668 5.05259 13.8057 4.6366 14.0483 4.28631Z" fill="#767676"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@ -336,6 +336,46 @@ class ApiManager {
return response;
}
/// POST request with List/Array body (for endpoints that expect JSON arrays)
Future<http.Response> postList(String url, {Map<String, String>? headers, required List<dynamic> 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<String, dynamic>) {
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<http.Response> multiPart(String url, {Map<String, String>? headers, required Map<String, String> body, required List<Future<MultipartFile>> files}) async {
Map<String, String> headers0 = const {
'Content-Type': 'multipart/form-data',

@ -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
}

@ -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<int> getSystemNotifications({required User user, bool resetProvider = false}) async {
Future<int> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<SystemNotificationModel> 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<Map<String, dynamic>> markAllAsRead({required String userId}) async {
try {
final Map<String, dynamic> 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<bool> 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;
}
}
}
}

@ -556,5 +556,17 @@
"contactInfo": "معلومات الاتصال",
"destinationBuilding": "مبنى الوجهة",
"destinationFloor": "طابق الوجهة",
"destinationDepartment": "قسم الوجهة"
"destinationDepartment": "قسم الوجهة",
"notificationPreferenceCenter": "مركز تفضيلات الإشعارات",
"notificationModule": "وحدة الإشعار",
"notificationPriority": "أولوية الإشعار",
"channelPreference": "تفضيل القناة",
"enable": "تفعيل",
"disable": "تعطيل",
"noNotificationPreferences": "لا توجد تفضيلات إشعارات",
"noNotificationPreferencesDescription": "لا توجد تفضيلات إشعارات متاحة في الوقت الحالي.",
"preferencesSavedSuccessfully": "تم حفظ التفضيلات بنجاح",
"failedToSavePreferences": "فشل حفظ التفضيلات",
"error": "خطأ",
"retry": "إعادة المحاولة"
}

@ -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"
}

@ -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

@ -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 => 'إعادة المحاولة';
}

@ -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';
}

@ -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(),
},
),
),

@ -0,0 +1,66 @@
import 'package:test_sa/models/base.dart';
class ModuleWithSubModulesModel extends Base {
int? id;
String? name;
List<SubModuleModel>? subModules;
ModuleWithSubModulesModel({
this.id,
this.name,
this.subModules,
});
ModuleWithSubModulesModel.fromJson(Map<String, dynamic> json) {
id = json['id'];
name = json['name'];
if (json['subModules'] != null) {
subModules = <SubModuleModel>[];
json['subModules'].forEach((v) {
subModules!.add(SubModuleModel.fromJson(v));
});
}
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
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<String, dynamic> json) {
id = json['id'];
name = json['name'];
mainTableName = json['mainTableName'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id;
data['name'] = name;
data['mainTableName'] = mainTableName;
return data;
}
@override
String toString() => name ?? '';
}

@ -0,0 +1,67 @@
class NotificationFilterModel {
List<int>? priorityIds;
List<int>? channelIds;
List<int>? recipientIds;
List<int>? moduleIds;
List<int>? 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<String, dynamic> toApiBody() {
final Map<String, dynamic> 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;
}
}

@ -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<String, dynamic>? 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;
}
}
}

@ -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<String, dynamic> json) {
return NotificationChannel(
id: json['id'],
name: json['name'],
value: json['value'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'value': value,
};
}
}

@ -0,0 +1,167 @@
import 'package:test_sa/modules/notification_setting/models/notification_channel_model.dart';
class NotificationPreferenceResponse {
final List<NotificationTemplate>? 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<String, dynamic> 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<String, dynamic> 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<NotificationChannel>? templateChannels;
final List<NotificationChannel>? preferenceChannels;
// Local state for UI tracking
bool isModified;
List<int> 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<int>? selectedChannelIds,
}) : selectedChannelIds = selectedChannelIds ?? [];
factory NotificationTemplate.fromJson(Map<String, dynamic> json) {
final List<NotificationChannel>? templateChannelsList = json['templateChannels'] != null
? (json['templateChannels'] as List).map((e) => NotificationChannel.fromJson(e)).toList()
: null;
final List<NotificationChannel>? 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<int> 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<String, dynamic> 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<NotificationChannel>? templateChannels,
List<NotificationChannel>? preferenceChannels,
bool? isModified,
List<int>? 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,
);
}
}

@ -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<NotificationTemplate> _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<NotificationTemplate> 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<void> 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<void> 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<int> 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<bool> 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<void> retry() async {
reset();
await loadNotificationPreferences();
}
}

@ -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<NotificationSettingPage> createState() => _NotificationSettingPageState();
}
class _NotificationSettingPageState extends State<NotificationSettingPage> {
final ScrollController _scrollController = ScrollController();
late NotificationSettingProvider _provider;
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
WidgetsBinding.instance.addPostFrameCallback((_) {
_provider = Provider.of<NotificationSettingProvider>(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<NotificationSettingProvider>(
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<void> _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
}
}
}

@ -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<int> 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<String, int> 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);
}
},
),
);
}
}

@ -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":

@ -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),

@ -232,7 +232,7 @@ class _AppTextFormFieldState extends State<AppTextFormField> {
(widget.showSpeechToText && _speechToText.isListening) ? "Listening..." : widget.labelText ?? "",
),
// labelStyle: widget.labelStyle,
hintStyle: widget.labelStyle,
hintStyle: widget.hintStyle,
prefixIcon: widget.prefixIcon ??
(widget.prefixIconData == null
? null

@ -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<ModuleWithSubModulesModel> {
@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;
}
}
}

@ -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<Lookup> {
@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;
}
}
}

@ -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<Lookup> {
@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;
}
}
}

@ -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<Lookup> {
@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;
}
}
}

@ -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<NotificationFilterPage> createState() => _NotificationFilterPageState();
}
class _NotificationFilterPageState extends State<NotificationFilterPage> {
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<void> _loadProvidersAndRestoreValues() async {
// Get providers
final priorityProvider = context.read<NotificationPriorityLookupProvider>();
final channelProvider = context.read<NotificationChannelLookupProvider>();
final recipientProvider = context.read<NotificationRecipientLookupProvider>();
final moduleProvider = context.read<ModulesWithSubModulesProvider>();
// Load data from providers if not already loaded
final futures = <Future>[];
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<Lookup, NotificationPriorityLookupProvider>(
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<Lookup, NotificationChannelLookupProvider>(
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<Lookup, NotificationRecipientLookupProvider>(
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<ModuleWithSubModulesModel, ModulesWithSubModulesProvider>(
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<SubModuleModel, NullableLoadingProvider>(
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;
}
}

@ -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<SystemNotificationModel> 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<NotificationsProvider>(context, listen: false);
await provider.markOneAsRead(notificationId: notification.id!);
}
// Navigate to notification details
FirebaseNotificationManger.handleMessage(context, notification.toNotificationJson());
},
);
},
),
);
}
}
}

@ -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<NotificationsPage> 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<void> _loadNotifications() async {
final notificationsProvider = Provider.of<NotificationsProvider>(context, listen: false);
final userProvider = Provider.of<UserProvider>(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<SystemNotificationModel> 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<NotificationsProvider>(context);
_userProvider = Provider.of<UserProvider>(context);
_settingProvider = Provider.of<SettingProvider>(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<NotificationFilterModel>(
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),
),
);
}

@ -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,
),
],
),
),
);
}
}

@ -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,
),
),
),
);
}
}
Loading…
Cancel
Save