Compare commits

...

45 Commits

Author SHA1 Message Date
Sikander Saleem 0863447e17 v1.9.3+53 uploaded 1 month ago
Sikander Saleem 43a657e68f medical gas request date added. 1 month ago
Sikander Saleem 523ce2a986 v1.9.2+52 uploaded. 1 month ago
Sikander Saleem 071dd082b3 external po & internal po added in search 1 month ago
Sikander Saleem 6acc21fc6a improvements, hide swipe option for FM 1 month ago
Sikander Saleem c4fb4a5908 improvements 1 month ago
WaseemAbbasi22 08dfc807fc bug fixes attachment can be uploaded at any stage 1 month ago
WaseemAbbasi22 83eec2c4d5 bug fixes 1 month ago
Sikander Saleem a0180eef77 v1.9.1+51 uploaded on stores. 1 month ago
Sikander Saleem cc9235eadd improvement 1 month ago
Sikander Saleem b1ef110e87 improvement 2 months ago
Sikander Saleem 3afcb220a7 Merge branch 'refs/heads/main_production' into ui_ux_rollout_merge_notification_settings 2 months ago
Sikander Saleem f1f7f19ecc keep old notification behavior, 2 months ago
Sikander Saleem f413d46738 improvements 2 months ago
Sikander Saleem 09ecf4fab4 traf added. 2 months ago
Sikander Saleem 313df6da8b improvements 2 months ago
Sikander Saleem b97f310d9d Merge branch 'refs/heads/main_production' into ui_ux_rollout_merge_notification_settings
# Conflicts:
#	lib/main.dart
#	lib/modules/asset_delivery_module/pages/delivery_inspection/cost_center_form_view.dart
#	lib/new_views/common_widgets/app_drawer.dart
#	lib/new_views/pages/settings_page.dart
2 months ago
Sikander Saleem b9a354d2a2 complete status color fix. 2 months ago
Sikander Saleem af2528f041 dropdown text ui improvement 2 months ago
Sikander Saleem 10d7aa4f0a bug fix. 2 months ago
Sikander Saleem 27aee0b2b2 ui/ux asset delivery improvements 2 months ago
Sikander Saleem d71ddcdf1a ui/ux asset delivery improvements 2 months ago
Sikander Saleem 91003cc8a1 apk provided to QA team. 2 months ago
Sikander Saleem 3f214ba8a7 summary count fixed. 2 months ago
Sikander Saleem 6cf85d54d4 improvements 2 months ago
Sikander Saleem 1f65b2e0ac improvements 2 months ago
Sikander Saleem 59a9721fb1 rename traf -> Assessment of Need & Technology 2 months ago
Sikander Saleem 4230a0dd3b notification redirection improvements 2 months ago
Sikander Saleem 6087f55cc8 Merge branch 'refs/heads/ui_ux_rollout_merge' into ui_ux_rollout_merge_notification_settings
# Conflicts:
#	lib/dashboard_latest/widgets/app_bar_widget.dart
#	lib/modules/cm_module/cm_detail_page.dart
2 months ago
WaseemAbbasi22 df10024975 bug fixes 3 months ago
WaseemAbbasi22 335a298843 bug fixes 3 months ago
Sikander Saleem 2a20b92c33 Merge remote-tracking branch 'refs/remotes/origin/ui_ux_rollout_merge' into ui_ux_rollout_merge_notification_settings 3 months ago
Sikander Saleem 5acfe59030 improvements 3 months ago
Sikander Saleem 2325943d16 Merge remote-tracking branch 'origin/ui_ux_rollout_merge_notification_settings' into ui_ux_rollout_merge_notification_settings 3 months ago
Sikander Saleem c5eeacffb7 improvements 3 months ago
WaseemAbbasi22 50520d4eb6 improvements 3 months ago
Sikander Saleem 14a32d101e improvements 3 months ago
Sikander Saleem 5744e6afbf improvements 3 months ago
Sikander Saleem e08813bf66 improvements 3 months ago
Sikander Saleem c070bd4849 improvements 3 months ago
Sikander Saleem 0e947cfcc6 improvements 3 months ago
Sikander Saleem cce4359d7a android ios notification bundling added. 3 months ago
WaseemAbbasi22 37a2be7b78 improvements 3 months ago
WaseemAbbasi22 afc1e9c77a notification implementation completed 3 months ago
WaseemAbbasi22 9ab4ed6964 notifications apis integrated 3 months ago

@ -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; 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 { 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 = _headers; Map<String, String> headers0 = _headers;

@ -1,15 +1,15 @@
class URLs { class URLs {
URLs._(); URLs._();
static const String appReleaseBuildNumber = "50"; static const String appReleaseBuildNumber = "53";
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://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/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/v4/mobile"; // v4 for Demo module
// static final String _baseUrl = "$_host/v5/mobile"; // v5 for data segregation // static final String _baseUrl = "$_host/v5/mobile"; // v5 for data segregation
// static final String _baseUrl = "$_host/v6/mobile"; // for asset delivery module // static final String _baseUrl = "$_host/v6/mobile"; // for asset delivery module
@ -262,7 +262,20 @@ class URLs {
static get addSupplierEngineer => "$_baseUrl/Supplier/AddSupplierEngineer"; 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
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
static get getNotificationChannelLookup => "$_baseUrl/Lookups/GetLookup?lookupEnum=5024"; // notification channel 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
static get getRecentNotifications => "$_baseUrl/return/user/recent/notification"; // get static get getRecentNotifications => "$_baseUrl/return/user/recent/notification"; // get
static get createRequest => "$_baseUrl/CallRequest/AddCallRequest"; // get static get createRequest => "$_baseUrl/CallRequest/AddCallRequest"; // get
static get CheckIfAssetHasAnotherServiceRequest => "$_baseUrl/ServiceRequest/CheckForPendingRequestForAsset"; // get static get CheckIfAssetHasAnotherServiceRequest => "$_baseUrl/ServiceRequest/CheckForPendingRequestForAsset"; // get
@ -591,6 +604,10 @@ class URLs {
// Print/Report URLs // Print/Report URLs
static get generateReport => "$_baseUrl/ReportPDF/GenerateReport"; 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 //4104 attachment type lookup
// Demo Request APIs // Demo Request APIs

@ -6,16 +6,29 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_api_availability/google_api_availability.dart'; import 'package:google_api_availability/google_api_availability.dart';
import 'package:huawei_push/huawei_push.dart' as h_push; 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/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/device/device_transfer.dart';
import 'package:test_sa/models/new_models/gas_refill_model.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/cm_module/cm_detail_page.dart';
import 'package:test_sa/modules/cx_module/survey/survey_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/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/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/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/tasks/task_request_detail_view.dart';
import 'package:test_sa/modules/tm_module/gas_refill/gas_refill_details.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/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/views/widgets/loaders/no_data_found.dart'; import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
@ -90,6 +103,7 @@ class FirebaseNotificationManger {
Widget? serviceClass; Widget? serviceClass;
String? transactionType = messageData["transactionType"]?.toString(); String? transactionType = messageData["transactionType"]?.toString();
bool IsOldNotification = messageData["IsOldNotification"]?.toString() == null;
if (transactionType == null) { if (transactionType == null) {
return; return;
@ -129,18 +143,11 @@ class FirebaseNotificationManger {
return; return;
} }
// PPM=1, if (IsOldNotification) {
// ServiceRequestEngineer = 3,
// AssetTransfer=7,
// SparePartTransaction= 8,
// GasRefill=9,
// TechnicalRetirmentWO = 11,
// Recurrent = 12,
switch (transactionType) { switch (transactionType) {
case "1": case "1":
serviceClass = PpmDetailsPage(requestId: int.parse(messageData["requestNumber"].toString())); serviceClass = PpmDetailsPage(requestId: int.parse(messageData["requestNumber"].toString()));
break; break;
//these three request are same corrective maintenance....
case "3": case "3":
serviceClass = CMDetailPage(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1); serviceClass = CMDetailPage(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1);
break; break;
@ -166,32 +173,78 @@ class FirebaseNotificationManger {
case "17": case "17":
serviceClass = SurveyPage(surveyId: int.parse(messageData["requestNumber"].toString())); 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: default:
serviceClass = const Scaffold(appBar: DefaultAppBar(), body: Center(child: NoDataFound())); 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()));
}
}
// 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!)); Navigator.of(context).push(MaterialPageRoute(builder: (_) => serviceClass!));
} }
} }
@ -241,7 +294,7 @@ class FirebaseNotificationManger {
} }
await FirebaseMessaging.instance.setAutoInitEnabled(true); 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) { FirebaseMessaging.instance.getInitialMessage().then((initialMessage) {
if (initialMessage != null) { if (initialMessage != null) {
@ -252,8 +305,20 @@ class FirebaseNotificationManger {
FirebaseMessaging.onMessage.listen((RemoteMessage message) { FirebaseMessaging.onMessage.listen((RemoteMessage message) {
if (Platform.isAndroid) { if (Platform.isAndroid) {
if (message.data["notificationType"] != 'NurseConfirmArrive') { if (message.data["notificationType"] != 'NurseConfirmArrive') {
NotificationManger.showNotification( // Extract title and body from notification field first, fallback to data field
title: message.notification?.title ?? "", subtext: message.notification?.body ?? "", hashcode: int.tryParse("1234" ?? "") ?? 1, payload: json.encode(message.data), context: context); 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: 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 {
final notificationsProvider = Provider.of<NotificationsProvider>(context, listen: false);
notificationsProvider.getUnreadCount();
} catch (e) {
log('Error updating notification count: $e');
}
} }
} }
return; return;

@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -43,8 +44,8 @@ class NotificationManger {
await localNotificationsPlugin.initialize(initializationSettings, onDidReceiveNotificationResponse: onNotificationPressed); await localNotificationsPlugin.initialize(initializationSettings, onDidReceiveNotificationResponse: onNotificationPressed);
} // push new notification } // push new notification
static const AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails( static AndroidNotificationDetails androidNotificationDetails(String? groupId, String? channelId) => AndroidNotificationDetails(
'com.hmg.atoms', channelId ?? 'com.hmg.atoms',
'ATOMS', 'ATOMS',
channelDescription: 'Push notification service for ATOMS', channelDescription: 'Push notification service for ATOMS',
importance: Importance.max, importance: Importance.max,
@ -54,35 +55,77 @@ class NotificationManger {
channelShowBadge: true, channelShowBadge: true,
visibility: NotificationVisibility.public, visibility: NotificationVisibility.public,
enableVibration: true, enableVibration: true,
groupKey: 'com.hmg.atoms', groupKey: groupId ?? 'com.hmg.atoms',
setAsGroupSummary: false,
); );
static const DarwinNotificationDetails iosNotificationDetails = DarwinNotificationDetails( static DarwinNotificationDetails iosNotificationDetails(String? threadId) => DarwinNotificationDetails(categoryIdentifier: "atoms", threadIdentifier: threadId);
categoryIdentifier: "atoms",
);
static Future<void> showNotification({required BuildContext context, required String title, required String subtext, required int hashcode, String? payload}) async { static Future<void> showNotification({required BuildContext context, required String title, required String subtext, required int hashcode, String? payload}) async {
// const AndroidNotificationDetails androidChannel = AndroidNotificationDetails( var dataPayload = json.decode(payload ?? "{}");
// 'com.hmg.atoms', String? groupId = dataPayload["group_id"];
// 'ATOMS', NotificationDetails platformChannel = NotificationDetails(android: androidNotificationDetails(groupId, groupId), iOS: iosNotificationDetails(groupId), macOS: iosNotificationDetails(groupId));
// 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);
await localNotificationsPlugin.show(hashcode, title, subtext, platformChannel, payload: payload); 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<String, int> _groupCounts = {};
static Future<void> _showGroupSummary(String groupId) async {
// Query actual active notifications from the system
final androidPlugin = localNotificationsPlugin.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
final List<ActiveNotification>? 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;
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,
onlyAlertOnce: true,
autoCancel: true,
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 { static Future scheduleNotification({int? id, String? title, String? body, String? payLoad, required DateTime scheduledNotificationDateTime}) async {
@ -95,7 +138,7 @@ class NotificationManger {
scheduledNotificationDateTime, scheduledNotificationDateTime,
tz.local, tz.local,
), ),
const NotificationDetails(android: androidNotificationDetails, iOS: iosNotificationDetails, macOS: iosNotificationDetails), NotificationDetails(android: androidNotificationDetails(null, null), iOS: iosNotificationDetails(null), macOS: iosNotificationDetails(null)),
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime); uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime);
} }
@ -103,4 +146,34 @@ class NotificationManger {
static Future<void> cancelNotificationById(int notificationId) async { static Future<void> cancelNotificationById(int notificationId) async {
await localNotificationsPlugin.cancel(notificationId); await localNotificationsPlugin.cancel(notificationId);
} }
/// Show group summary notification (for background isolate)
static Future<void> _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),
);
}
} }

@ -137,7 +137,14 @@ class AllRequestsProvider extends ChangeNotifier {
List<int> list = [1, 2, 3, 4]; List<int> list = [1, 2, 3, 4];
if (context.userProvider.isAssessor) { 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; return list;
} }
if (context.userProvider.isQualityUser) { if (context.userProvider.isQualityUser) {
@ -158,9 +165,8 @@ class AllRequestsProvider extends ChangeNotifier {
list.add(7); // task mod list.add(7); // task mod
} }
//TODO uncommit this to enable TRAF in production.
context.userProvider.modulesPermissionsList.forEach((module) { 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!); list.add(module.module!.value!);
} }
}); });
@ -188,7 +194,6 @@ class AllRequestsProvider extends ChangeNotifier {
list = [5]; list = [5];
} }
return list; return list;
} }
@ -220,6 +225,8 @@ class AllRequestsProvider extends ChangeNotifier {
if (search?.assetName?.value?.isNotEmpty ?? false) "assetName": search!.assetName!.value, if (search?.assetName?.value?.isNotEmpty ?? false) "assetName": search!.assetName!.value,
if (search?.assetNo?.value?.isNotEmpty ?? false) "assetNumber": search!.assetNo!.value, if (search?.assetNo?.value?.isNotEmpty ?? false) "assetNumber": search!.assetNo!.value,
if (search?.sn?.value?.isNotEmpty ?? false) "assetSerialNo": search!.sn!.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?.model?.value?.isNotEmpty ?? false) "model": search!.model!.value,
if (search?.manufacture?.value?.isNotEmpty ?? false) "manufacturer": search!.manufacture!.value, if (search?.manufacture?.value?.isNotEmpty ?? false) "manufacturer": search!.manufacture!.value,
if (search?.startDate != null) "from": search!.startDate!.toIso8601String(), if (search?.startDate != null) "from": search!.startDate!.toIso8601String(),

@ -1,9 +1,11 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:developer';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:test_sa/controllers/api_routes/api_manager.dart'; import 'package:test_sa/controllers/api_routes/api_manager.dart';
import 'package:test_sa/controllers/api_routes/urls.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/system_notification_model.dart';
import 'package:test_sa/models/user.dart'; import 'package:test_sa/models/user.dart';
@ -11,11 +13,24 @@ class NotificationsProvider extends ChangeNotifier {
// number of items call in each request // number of items call in each request
final pageItemNumber = 12; final pageItemNumber = 12;
// 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 //reset provider data
void reset() { void reset() {
notifications = []; notifications = [];
nextPage = true; nextPage = true;
stateCode = null; stateCode = null;
totalAllNotifications = 0;
totalNotificationRead = 0;
totalNotificationUnread = 0;
totalRows = 0;
} }
// state code of current request to defied error message // state code of current request to defied error message
@ -38,28 +53,61 @@ class NotificationsProvider extends ChangeNotifier {
/// return state code if request complete may be 200, 404 or 403 /// return state code if request complete may be 200, 404 or 403
/// for more details check http state manager /// for more details check http state manager
/// lib\controllers\http_status_manger\http_status_manger.dart /// 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 (isLoading == true) return -2;
if (resetProvider) { if (resetProvider) {
reset(); reset();
} }
// Store filters for pagination
if (filters != null) {
activeFilters = filters;
}
isLoading = true; isLoading = true;
notifyListeners(); notifyListeners();
late Response response; late Response response;
try { try {
final Map<String, dynamic> body = {"pageSize": pageItemNumber, "pageNumber": notifications.length ~/ pageItemNumber + 1, "userId": user.userID}; // Base request body with pagination
response = await ApiManager.instance.post(URLs.getSystemNotifications, body: body); 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; stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) { if (response.statusCode >= 200 && response.statusCode < 300) {
// client's request was successfully received // client's request was successfully received
List requestsListJson = json.decode(response.body)["data"]; final responseData = json.decode(response.body);
List requestsListJson = responseData["data"];
List<SystemNotificationModel> serviceRequestsPage = requestsListJson.map((request) => SystemNotificationModel.fromJson(request)).toList(); List<SystemNotificationModel> serviceRequestsPage = requestsListJson.map((request) => SystemNotificationModel.fromJson(request)).toList();
notifications ??= [];
notifications.addAll(serviceRequestsPage); 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; isLoading = false;
notifyListeners(); notifyListeners();
@ -73,11 +121,115 @@ class NotificationsProvider extends ChangeNotifier {
} }
} }
/// return -2 if request in progress Future<Map<String, dynamic>> markAllAsRead({required String userId}) async {
/// return -1 if error happen when sending request try {
/// return state code if request complete may be 200, 404 or 403 final Map<String, dynamic> body = {"userId": userId};
/// for more details check http state manager final response = await ApiManager.instance.post(URLs.markAllNotificationsAsRead, body: body);
/// lib\controllers\http_status_manger\http_status_manger.dart
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;
}
// Update counts: Move all unread to read
totalNotificationRead = totalNotificationRead + totalNotificationUnread;
totalNotificationUnread = 0;
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};
}
}
Future<bool> markOneAsRead({required String notificationId}) async {
try {
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
final index = notifications.indexWhere((n) => n.id == notificationId);
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;
}
return false;
} catch (error) {
print('Error marking notification as read: $error');
return false;
}
}
Future<int?> 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);
count = responseData['data'];
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<List<SystemNotificationModel>?> getRecentNotifications({ Future<List<SystemNotificationModel>?> getRecentNotifications({
required String host, required String host,
required User user, required User user,
@ -86,7 +238,7 @@ class NotificationsProvider extends ChangeNotifier {
//userId = 397.toString(); // testing id to view data //userId = 397.toString(); // testing id to view data
try { 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; stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) { if (response.statusCode >= 200 && response.statusCode < 300) {

@ -68,7 +68,13 @@ class UserProvider extends ChangeNotifier {
List<ModulesPermissionsModel> get modulesPermissionsList => _modulesPermissionsModel; List<ModulesPermissionsModel> 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<SwipeHistory> value) { set swipeHistory(List<SwipeHistory> value) {
_swipeHistory = value; _swipeHistory = value;

@ -60,7 +60,7 @@ class _DashboardViewState extends State<DashboardView> {
_dashBoardProvider.setTabs(userType: userProvider.user!.type!, context: context); _dashBoardProvider.setTabs(userType: userProvider.user!.type!, context: context);
_dashBoardProvider.getDashBoardCount(usersType: userProvider.user!.type!); _dashBoardProvider.getDashBoardCount(usersType: userProvider.user!.type!);
_dashBoardProvider.resetRequestDataList(); _dashBoardProvider.resetRequestDataList();
notificationsProvider.getUnreadCount();
if (userProvider.isHouseKeeper) { if (userProvider.isHouseKeeper) {
_dashBoardProvider.currentListIndex = 0; _dashBoardProvider.currentListIndex = 0;
_dashBoardProvider.getRequestDetail( _dashBoardProvider.getRequestDetail(
@ -198,6 +198,7 @@ class _DashboardViewState extends State<DashboardView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final user = Provider.of<UserProvider>(context, listen: false).user; final user = Provider.of<UserProvider>(context, listen: false).user;
bool isUserFlowMedical = Provider.of<SettingProvider>(context, listen: false).isUserFlowMedical;
bool isEngineer = (user?.type) == UsersTypes.engineer; bool isEngineer = (user?.type) == UsersTypes.engineer;
///Need to check which info need to show on dashboard for quality user. ///Need to check which info need to show on dashboard for quality user.
@ -230,7 +231,7 @@ class _DashboardViewState extends State<DashboardView> {
), ),
), ),
), ),
if (user != null && user.employeeIsHMG == false) if (user != null && isUserFlowMedical == false && user.employeeIsHMG == false)
Positioned( Positioned(
right: 20.toScreenWidth, right: 20.toScreenWidth,
bottom: 20.toScreenHeight, bottom: 20.toScreenHeight,

@ -1,7 +1,10 @@
import 'dart:developer';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:test_sa/controllers/api_routes/api_manager.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/controllers/providers/settings/setting_provider.dart';
import 'package:test_sa/dashboard_latest/dashboard_provider.dart'; import 'package:test_sa/dashboard_latest/dashboard_provider.dart';
import 'package:test_sa/extensions/context_extension.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/extensions/widget_extensions.dart';
import 'package:test_sa/helper/utils.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/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/notifications_page.dart';
import 'package:test_sa/views/pages/user/notifications/unread_chat_list.dart'; import 'package:test_sa/views/pages/user/notifications/unread_chat_list.dart';
@ -37,7 +41,6 @@ class _AppBarWidgetState extends State<AppBarWidget> with TickerProviderStateMix
void initState() { void initState() {
super.initState(); super.initState();
// Profile section animation (left to right with fade)
_leftController = AnimationController( _leftController = AnimationController(
duration: const Duration(milliseconds: 600), duration: const Duration(milliseconds: 600),
vsync: this, vsync: this,
@ -261,15 +264,20 @@ class _AppBarWidgetState extends State<AppBarWidget> with TickerProviderStateMix
}, },
), ),
], ],
IconButton( Selector<NotificationsProvider, int>(
icon: ("dashboard/notification").toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)), selector: (_, myModel) => myModel.totalNotificationUnread, // Selects only the userName
padding: EdgeInsets.zero, builder: (_, totalNotificationUnread, __) {
visualDensity: VisualDensity.compact, return CustomBadge(
tooltip: "Notifications", width: 16,
onPressed: () { height: 16,
positionT: -8,
positionR: -8,
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); Navigator.of(context).pushNamed(NotificationsPage.id);
}, });
), }).paddingOnly(end: 8, start: 4),
], ],
), ),
), ),

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

@ -556,5 +556,17 @@
"contactInfo": "Contact Info", "contactInfo": "Contact Info",
"destinationBuilding": "Destination Building", "destinationBuilding": "Destination Building",
"destinationFloor": "Destination Floor", "destinationFloor": "Destination Floor",
"destinationDepartment": "Destination Department" "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: /// In en, this message translates to:
/// **'Destination Department'** /// **'Destination Department'**
String get destinationDepartment; 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 class _AppLocalizationsDelegate

@ -1702,4 +1702,41 @@ class AppLocalizationsAr extends AppLocalizations {
@override @override
String get destinationDepartment => 'قسم الوجهة'; 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 @override
String get destinationDepartment => 'Destination Department'; 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/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/medical_gas_inspection_provider.dart';
import 'package:test_sa/modules/medical_gas_inspection/providers/order_type_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/signup/providers/company_provider.dart'; import 'package:test_sa/modules/signup/providers/company_provider.dart';
import 'package:test_sa/modules/signup/providers/group_provider.dart'; import 'package:test_sa/modules/signup/providers/group_provider.dart';
import 'package:test_sa/modules/signup/providers/signup_provider.dart'; import 'package:test_sa/modules/signup/providers/signup_provider.dart';
@ -68,6 +69,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/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/create_gas_refill_request.dart';
import 'package:test_sa/modules/tm_module/gas_refill/update_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/app_style/app_themes.dart';
import 'package:test_sa/new_views/pages/help_center_page.dart'; import 'package:test_sa/new_views/pages/help_center_page.dart';
import 'package:test_sa/new_views/pages/land_page/land_page.dart'; import 'package:test_sa/new_views/pages/land_page/land_page.dart';
@ -118,6 +120,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_format_provider.dart';
import 'package:test_sa/modules/export_module/provider/export_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/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/notifications/notifications_page.dart';
import 'package:test_sa/views/pages/user/profile_page.dart'; import 'package:test_sa/views/pages/user/profile_page.dart';
import 'package:test_sa/views/update_available_screen.dart'; import 'package:test_sa/views/update_available_screen.dart';
@ -145,11 +148,17 @@ import 'modules/tm_module/device_transfer/device_filter_screen.dart';
import 'modules/tm_module/device_transfer/device_search_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/tasks/create_task_view.dart';
import 'modules/tm_module/gas_refill/gas_refill_request_form.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/classification_lookup_provider.dart';
import 'providers/lookups/customer_lookup_provider.dart'; import 'providers/lookups/customer_lookup_provider.dart';
import 'providers/lookups/department_lookup_provider.dart'; import 'providers/lookups/department_lookup_provider.dart';
import 'providers/lookups/request_type_lookup_provider.dart'; import 'providers/lookups/request_type_lookup_provider.dart';
import 'providers/lookups/yes_no_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/loan_availability_provider.dart';
import 'providers/service_request_providers/reject_reason_provider.dart'; import 'providers/service_request_providers/reject_reason_provider.dart';
import 'modules/cleaner_module/pages/cleaner_search_form_view.dart'; import 'modules/cleaner_module/pages/cleaner_search_form_view.dart';
@ -257,6 +266,10 @@ class MyApp extends StatelessWidget {
ChangeNotifierProvider(create: (_) => IncidentPersonInvolvedLookupProvider(), lazy: true), ChangeNotifierProvider(create: (_) => IncidentPersonInvolvedLookupProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => ClinicalNonClinicalLookupProvider(), lazy: true), ChangeNotifierProvider(create: (_) => ClinicalNonClinicalLookupProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => GenderLookupProvider(), 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) // GAS MODULE PROVIDERS (6)
ChangeNotifierProvider(create: (_) => GasTypesProvider(), lazy: true), ChangeNotifierProvider(create: (_) => GasTypesProvider(), lazy: true),
@ -332,6 +345,7 @@ class MyApp extends StatelessWidget {
ChangeNotifierProvider(create: (_) => LoanProvider(), lazy: true), ChangeNotifierProvider(create: (_) => LoanProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => LoanPeriodProvider(), lazy: true), ChangeNotifierProvider(create: (_) => LoanPeriodProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => DemoProvider(), lazy: true), ChangeNotifierProvider(create: (_) => DemoProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => TrafRequestProvider(), lazy: true),
// MEDICAL GAS INSPECTION PROVIDERS (4) // MEDICAL GAS INSPECTION PROVIDERS (4)
ChangeNotifierProvider(create: (_) => MedicalGasOrderTypeProvider(), lazy: true), ChangeNotifierProvider(create: (_) => MedicalGasOrderTypeProvider(), lazy: true),
@ -376,6 +390,9 @@ class MyApp extends StatelessWidget {
// CX MODULE PROVIDERS (1) // CX MODULE PROVIDERS (1)
ChangeNotifierProvider(create: (_) => SurveyProvider(), lazy: true), ChangeNotifierProvider(create: (_) => SurveyProvider(), lazy: true),
// NOTIFICATION MODULE PROVIDERS (1)
ChangeNotifierProvider(create: (_) => NotificationSettingProvider(), lazy: true),
// CLEANER MODULE PROVIDERS (1) // CLEANER MODULE PROVIDERS (1)
ChangeNotifierProvider(create: (_) => CleanerProvider(), lazy: true), ChangeNotifierProvider(create: (_) => CleanerProvider(), lazy: true),
@ -414,6 +431,8 @@ class MyApp extends StatelessWidget {
LandPage.routeName: (_) => const LandPage(), LandPage.routeName: (_) => const LandPage(),
GasRefillRequestForm.routeName: (_) => const GasRefillRequestForm(), GasRefillRequestForm.routeName: (_) => const GasRefillRequestForm(),
CreateTaskView.id: (_) => const CreateTaskView(), CreateTaskView.id: (_) => const CreateTaskView(),
CreateTRAFRequestPage.id: (_) => CreateTRAFRequestPage(),
UpdateTrafRequestPage.id: (_) => UpdateTrafRequestPage(),
CreateGasRefillRequest.id: (_) => const CreateGasRefillRequest(), CreateGasRefillRequest.id: (_) => const CreateGasRefillRequest(),
CreateCMRequest.id: (_) => const CreateCMRequest(), CreateCMRequest.id: (_) => const CreateCMRequest(),
MyAssetsPage.id: (_) => const MyAssetsPage(), MyAssetsPage.id: (_) => const MyAssetsPage(),
@ -439,6 +458,7 @@ class MyApp extends StatelessWidget {
CreateMedicalGasRequestPage.id: (_) => CreateMedicalGasRequestPage(), CreateMedicalGasRequestPage.id: (_) => CreateMedicalGasRequestPage(),
CreateDemoRequestPage.id: (_) => CreateDemoRequestPage(), CreateDemoRequestPage.id: (_) => CreateDemoRequestPage(),
CleanerSearchFormView.routeName: (_) => const CleanerSearchFormView(), CleanerSearchFormView.routeName: (_) => const CleanerSearchFormView(),
NotificationSettingPage.id: (_) => const NotificationSettingPage(),
ApplicationDemoRequestFormScreen.routeName: (_) => const ApplicationDemoRequestFormScreen(), ApplicationDemoRequestFormScreen.routeName: (_) => const ApplicationDemoRequestFormScreen(),
}, },
), ),

@ -0,0 +1,67 @@
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'];
identifier = id?.toString();
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,6 +8,8 @@ class SearchAllRequestsModel {
SearchByManufactureModel? manufacture; // Now nullable SearchByManufactureModel? manufacture; // Now nullable
SearchBySNModel? sn; // Now nullable SearchBySNModel? sn; // Now nullable
SearchByRequestModel? model; // Now nullable SearchByRequestModel? model; // Now nullable
SearchByExternalPO? externalPo; // Now nullable
SearchByInternalPO? internalPo; // Now nullable
SearchAllRequestsBaseModel? searchBySelectedValue; // Now nullable SearchAllRequestsBaseModel? searchBySelectedValue; // Now nullable
List<int>? statuses; // Now nullable List<int>? statuses; // Now nullable
List<int>? typeTransaction; // Now nullable List<int>? typeTransaction; // Now nullable
@ -21,6 +23,8 @@ class SearchAllRequestsModel {
this.assetNo, this.assetNo,
this.manufacture, this.manufacture,
this.sn, this.sn,
this.externalPo,
this.internalPo,
this.model, this.model,
this.statuses, this.statuses,
this.typeTransaction, this.typeTransaction,
@ -70,6 +74,16 @@ class SearchAllRequestsModel {
sn?.value = null; sn?.value = null;
} }
if (field != 'externalPo') {
externalPo?.controller?.text = "";
externalPo?.value = null;
}
if (field != 'internalPo') {
internalPo?.controller?.text = "";
internalPo?.value = null;
}
if (field != 'model') { if (field != 'model') {
model?.controller?.text = ""; model?.controller?.text = "";
model?.value = null; model?.value = null;
@ -128,6 +142,14 @@ class SearchAllRequestsModel {
json["assetSerialNo"] = sn!.value; 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) { if (model?.value?.isNotEmpty ?? false) {
json["model"] = model!.value; json["model"] = model!.value;
} }
@ -187,3 +209,13 @@ class SearchByRequestModel extends SearchAllRequestsBaseModel {
SearchByRequestModel(BuildContext context, {TextEditingController? controller, String? value}) // Parameters now nullable SearchByRequestModel(BuildContext context, {TextEditingController? controller, String? value}) // Parameters now nullable
: super(context, controller: controller, label: context.translation.model, value: value); : 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);
}

@ -8,12 +8,23 @@ class SystemNotificationModel {
String? sourceName; String? sourceName;
bool? readed; bool? readed;
String? readingDate; String? readingDate;
int? id; String? id; // Changed from int? to String? to match API response
String? createdOn; String? createdOn;
String? modifiedOn; String? modifiedOn;
String? priorityName; String? priorityName;
int? priorityValue;
String? statusName; String? statusName;
int? transactionType; int? transactionType;
String? moduleName;
String? subModuleName;
String? channelName;
// New fields from the API
bool? sent;
String? sentDate;
int? noOfFailure;
int? totalUnreadCount; // Added from API payload
bool? isOldNotification;
SystemNotificationModel( SystemNotificationModel(
{this.userId, {this.userId,
@ -29,28 +40,48 @@ class SystemNotificationModel {
this.createdOn, this.createdOn,
this.modifiedOn, this.modifiedOn,
this.priorityName, this.priorityName,
this.priorityValue,
this.transactionType, this.transactionType,
this.statusName}); this.statusName,
this.moduleName,
this.subModuleName,
this.channelName,
this.sent,
this.sentDate,
this.noOfFailure,
this.isOldNotification,
this.totalUnreadCount});
SystemNotificationModel.fromJson(Map<String, dynamic>? json) { SystemNotificationModel.fromJson(Map<String, dynamic>? json) {
// Allow json to be null // Allow json to be null
if (json != null) { if (json != null) {
// Add null check // Fix: Handle id as String (API returns String like "6a3b8af78200480d80d8dbb8")
userId = json['userId']; id = json['id']?.toString();
userId = json['userId'] ?? json['userEmail'];
userName = json['userName']; userName = json['userName'];
title = json['title']; title = json['title'] ?? json['notificationTitle'];
text = json['text']; text = json['text'] ?? json['notificationBody'];
referenceId = json['referenceId']; referenceId = json['referenceId'];
sourceId = json['sourceId']; sourceId = json['sourceId'];
sourceName = json['sourceName']; sourceName = json['sourceName'] ?? json['moduleName'];
readed = json['readed']; readed = json['readed'] ?? (json['isRead'] == true);
readingDate = json['readingDate']; readingDate = json['readingDate'];
id = json['id']; createdOn = json['createdDate'] ?? json['createdOn'];
createdOn = json['createdOn'];
modifiedOn = json['modifiedOn']; modifiedOn = json['modifiedOn'];
priorityName = json['priorityName']; priorityName = json['priorityName'];
priorityValue = json['priorityValue'];
statusName = json['statusName']; statusName = json['statusName'];
transactionType = json['transactionType']; transactionType = json['transactionType'];
moduleName = json['moduleName'];
subModuleName = json['subModuleName'];
channelName = json['channelName'];
// Map new fields
sent = json['sent'];
sentDate = json['sentDate'];
noOfFailure = json['noOfFailure'];
totalUnreadCount = json['totalUnreadCount'];
isOldNotification = json['IsOldNotification'];
} }
} }
@ -69,9 +100,16 @@ class SystemNotificationModel {
data['createdOn'] = createdOn; data['createdOn'] = createdOn;
data['modifiedOn'] = modifiedOn; data['modifiedOn'] = modifiedOn;
data['priorityName'] = priorityName; data['priorityName'] = priorityName;
data['priorityValue'] = priorityValue;
data['priority'] = priorityName; data['priority'] = priorityName;
data['statusName'] = statusName; data['statusName'] = statusName;
data['transactionType'] = transactionType; data['transactionType'] = transactionType;
data['channelName'] = channelName;
data['sent'] = sent;
data['sentDate'] = sentDate;
data['noOfFailure'] = noOfFailure;
data['totalUnreadCount'] = totalUnreadCount;
data['IsOldNotification'] = isOldNotification;
return data; return data;
} }
@ -90,8 +128,16 @@ class SystemNotificationModel {
data['createdOn'] = createdOn; data['createdOn'] = createdOn;
data['modifiedOn'] = modifiedOn; data['modifiedOn'] = modifiedOn;
data['priorityName'] = priorityName; data['priorityName'] = priorityName;
data['priorityValue'] = priorityValue;
data['statusName'] = statusName; data['statusName'] = statusName;
data['transactionType'] = transactionType; data['transactionType'] = transactionType;
data['moduleName'] = moduleName;
data['subModuleName'] = subModuleName;
data['channelName'] = channelName;
data['sent'] = sent;
data['sentDate'] = sentDate;
data['noOfFailure'] = noOfFailure;
data['totalUnreadCount'] = totalUnreadCount;
return data; return data;
} }
} }

@ -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/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/asset_delivery_provider.dart';
import 'package:test_sa/modules/asset_delivery_module/provider/attachment_type_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/app_lazy_loading.dart';
import 'package:test_sa/new_views/common_widgets/single_item_drop_down_menu.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/new_views/swipe_module/dialoge/acknowledge_work_dialog.dart';
@ -34,7 +35,7 @@ class HelperFunction {
builder: (context) => AssetDeliveryAttachmentView( builder: (context) => AssetDeliveryAttachmentView(
tableItemId: deliveryTableItemId, tableItemId: deliveryTableItemId,
attachmentType: result!.value, attachmentType: result!.value,
viewOnly: viewOnly, // viewOnly: viewOnly,
attachmentList: list, attachmentList: list,
))); )));
// } // }
@ -68,6 +69,7 @@ class HelperFunction {
return showModalBottomSheet<Lookup?>( return showModalBottomSheet<Lookup?>(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
backgroundColor: AppColor.bottomSheetColor(context),
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)), borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
), ),

@ -35,8 +35,8 @@ class AssetDeliveryItemView extends StatelessWidget {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).push(MaterialPageRoute(
builder: (_) => AssetDeliveryPage( builder: (_) => AssetDeliveryPage(
requestId: requestData!.id!, requestId: requestData!.id!,
poInternal: requestData?.poInternal, // poInternal: requestData?.poInternal,
poExternal: requestData?.poExternal, // poExternal: requestData?.poExternal,
))); )));
}); });
} }
@ -52,8 +52,8 @@ class AssetDeliveryItemView extends StatelessWidget {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).push(MaterialPageRoute(
builder: (_) => AssetDeliveryPage( builder: (_) => AssetDeliveryPage(
requestId: requestDetails!.id!, requestId: requestDetails!.id!,
poInternal: requestDetails?.poInternal, // poInternal: requestDetails?.poInternal,
poExternal: requestDetails?.poExternal, // poExternal: requestDetails?.poExternal,
))); )));
}); });
} }

@ -25,14 +25,15 @@ import 'package:test_sa/views/widgets/item_views/info_text_widget.dart';
class AssetDeliveryPage extends StatefulWidget { class AssetDeliveryPage extends StatefulWidget {
final int? requestId; final int? requestId;
final String? poInternal;
final String? poExternal; // final String? poInternal;
// final String? poExternal;
const AssetDeliveryPage({ const AssetDeliveryPage({
Key? key, Key? key,
this.requestId, this.requestId,
this.poInternal, // this.poInternal,
this.poExternal, // this.poExternal,
}) : super(key: key); }) : super(key: key);
@override @override
@ -144,7 +145,14 @@ class _AssetDeliveryPageState extends State<AssetDeliveryPage> {
], ],
), ),
16.height, 16.height,
assetDeliveryTableList(context: context, assetDeliveryProvider: provider), Selector<AssetDeliveryProvider, bool>(
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<AssetDeliveryPage> {
// children: [ // children: [
// Checkbox( // Checkbox(
// value: dataModel!.isHMG, // value: dataModel!.isHMG,
// activeColor: AppColor.neutral120, // activeColor: AppColor.neutral120,x
// materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, // materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
// visualDensity: const VisualDensity(horizontal: -4, vertical: -3), // visualDensity: const VisualDensity(horizontal: -4, vertical: -3),
// onChanged: (value) {}, // onChanged: (value) {},

@ -24,13 +24,13 @@ import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
class AssetDeliveryAttachmentView extends StatefulWidget { class AssetDeliveryAttachmentView extends StatefulWidget {
final int? tableItemId; final int? tableItemId;
final int? attachmentType; final int? attachmentType;
bool viewOnly = false; // bool viewOnly = false;
final List<GenericAttachmentModel> attachmentList; final List<GenericAttachmentModel> attachmentList;
AssetDeliveryAttachmentView({ AssetDeliveryAttachmentView({
super.key, super.key,
required this.attachmentList, required this.attachmentList,
this.viewOnly = false, // this.viewOnly = false,
this.attachmentType, this.attachmentType,
this.tableItemId, this.tableItemId,
}); });
@ -65,7 +65,6 @@ class _AssetDeliveryAttachmentViewState extends State<AssetDeliveryAttachmentVie
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
log('value ${widget.attachmentType}');
return Scaffold( return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: DefaultAppBar( appBar: DefaultAppBar(
@ -74,15 +73,18 @@ class _AssetDeliveryAttachmentViewState extends State<AssetDeliveryAttachmentVie
Navigator.pop(context); Navigator.pop(context);
}, },
), ),
body: widget.attachmentList.isEmpty body:
? const NoDataFound().center // widget.attachmentList.isEmpty
: Column( // ? const NoDataFound().center
// :
Column(
children: [ children: [
Flexible( Flexible(
fit: FlexFit.loose, // ✅ expands only as much as needed fit: FlexFit.loose,
child: Column( child: Column(
children: [ children: [
widget.attachmentType == 5 && !widget.viewOnly // widget.attachmentType == 5 && !widget.viewOnly
widget.attachmentType == 5 //only add the attachment for current
? AttachmentPicker( ? AttachmentPicker(
label: context.translation.attachments, label: context.translation.attachments,
attachment: _attachments, attachment: _attachments,
@ -97,16 +99,18 @@ class _AssetDeliveryAttachmentViewState extends State<AssetDeliveryAttachmentVie
padding: 12, padding: 12,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
) )
: FilesList(images: _attachments.map((e) => URLs.getFileUrl(e.name ?? '') ?? '').toList() ?? []).toShadowContainer( : _attachments.isNotEmpty
? FilesList(images: _attachments.map((e) => URLs.getFileUrl(e.name ?? '') ?? '').toList() ?? []).toShadowContainer(
context, context,
borderRadius: 20, borderRadius: 20,
padding: 12, padding: 12,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
), )
: const NoDataFound().center.expanded,
], ],
), ),
), ),
if (widget.attachmentType == 5 && !widget.viewOnly) if (widget.attachmentType == 5)
FooterActionButton.footerContainer( FooterActionButton.footerContainer(
context: context, context: context,
child: AppFilledButton( child: AppFilledButton(

@ -81,8 +81,10 @@ class _CostCenterFormViewState extends State<CostCenterFormView> {
await costCenterProvider.getData(id: widget.requestId); await costCenterProvider.getData(id: widget.requestId);
} }
} }
if(mounted) {
setState(() {}); setState(() {});
} }
}
void addItem<T>(List<T> list, T Function() creator) { void addItem<T>(List<T> list, T Function() creator) {
setState(() { setState(() {
@ -190,7 +192,7 @@ class _CostCenterFormViewState extends State<CostCenterFormView> {
}, },
); );
}, },
).expanded; );
} }
Widget _buildSerialNumberList() { Widget _buildSerialNumberList() {
@ -225,7 +227,7 @@ class _CostCenterFormViewState extends State<CostCenterFormView> {
}, },
); );
}, },
).expanded; );
} }
Widget selectionButton() { Widget selectionButton() {

@ -155,12 +155,12 @@ class _DeliveryInspectionFormViewState extends State<DeliveryInspectionFormView>
: Row( : Row(
children: [ children: [
AppFilledButton( AppFilledButton(
buttonColor: AppColor.white60, buttonColor: AppColor.primary10,
label: 'Attachments'.addTranslation, label: 'Attachments'.addTranslation,
maxWidth: true, maxWidth: true,
textColor: AppColor.black10, // textColor: AppColor.black10,
onPressed: () { 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, }).expanded,
8.width, 8.width,
AppFilledButton(buttonColor: AppColor.primary10, label: 'Next'.addTranslation, maxWidth: true, onPressed: _nextTap).expanded, AppFilledButton(buttonColor: AppColor.primary10, label: 'Next'.addTranslation, maxWidth: true, onPressed: _nextTap).expanded,
@ -288,7 +288,7 @@ class _DeliveryInspectionFormViewState extends State<DeliveryInspectionFormView>
ADatePicker( ADatePicker(
label: 'Inspected Date/Time From'.addTranslation, label: 'Inspected Date/Time From'.addTranslation,
hideShadow: true, hideShadow: true,
height: 56.toScreenHeight, // height: 56.toScreenHeight,
backgroundColor: AppColor.fieldBgColor(context), backgroundColor: AppColor.fieldBgColor(context),
date: model.deliveryInspectionInspectedDateFrom, date: model.deliveryInspectionInspectedDateFrom,
from: DateTime(DateTime.now().year, 1, 1), from: DateTime(DateTime.now().year, 1, 1),
@ -318,7 +318,7 @@ class _DeliveryInspectionFormViewState extends State<DeliveryInspectionFormView>
ADatePicker( ADatePicker(
label: 'Inspected Date/Time To'.addTranslation, label: 'Inspected Date/Time To'.addTranslation,
hideShadow: true, hideShadow: true,
height: 56.toScreenHeight, // height: 56.toScreenHeight,
backgroundColor: AppColor.fieldBgColor(context), backgroundColor: AppColor.fieldBgColor(context),
date: model.deliveryInspectionInspectedDateTo, date: model.deliveryInspectionInspectedDateTo,
from: model.deliveryInspectionInspectedDateFrom, from: model.deliveryInspectionInspectedDateFrom,

@ -36,7 +36,7 @@ class EndUserAssetDetailCard extends StatelessWidget {
) )
: const SizedBox(), : const SizedBox(),
if (!isViewOnly) if (!isViewOnly)
'edit_icon'.toSvgAsset().onPress(() { 'edit_icon'.toSvgAsset(color: AppColor.icon2Color(context)).onPress(() {
if (editPress != null) { if (editPress != null) {
editPress!(); editPress!();
} }

@ -111,7 +111,7 @@ class _EndUserCostCenterLIstViewState extends State<EndUserCostCenterLIstView> {
: FooterActionButton.footerContainer( : FooterActionButton.footerContainer(
context: context, context: context,
child: AppFilledButton( child: AppFilledButton(
buttonColor: AppColor.white60, buttonColor: AppColor.primary10,
label: 'Attachments'.addTranslation, label: 'Attachments'.addTranslation,
maxWidth: true, maxWidth: true,
textColor: AppColor.black10, textColor: AppColor.black10,

@ -147,7 +147,7 @@ class _UpdateEndUserCostCenterDetailsViewState extends State<UpdateEndUserCostCe
? CircularProgressIndicator(color: AppColor.loadingColor(context)).center ? CircularProgressIndicator(color: AppColor.loadingColor(context)).center
: ListView.separated( : ListView.separated(
itemCount: assetList.length, itemCount: assetList.length,
separatorBuilder: (_, __) => 8.height, separatorBuilder: (_, __) => 12.height,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),

@ -161,13 +161,13 @@ class _TechnicalInspectionLinesListViewState extends State<TechnicalInspectionLi
: Row( : Row(
children: [ children: [
AppFilledButton( AppFilledButton(
buttonColor: AppColor.white60, buttonColor: AppColor.primary10,
label: 'Attachments'.addTranslation, label: 'Attachments'.addTranslation,
maxWidth: true, maxWidth: true,
textColor: AppColor.black10, // textColor: AppColor.black10,
onPressed: () { onPressed: () {
HelperFunction.attachmentTap( HelperFunction.attachmentTap(
context: context, viewOnly: true, assetDeliveryProvider: assetDeliveryProvider!, deliveryTableItemId: widget.deliveryTableModel.id); context: context, viewOnly: viewOnly, assetDeliveryProvider: assetDeliveryProvider!, deliveryTableItemId: widget.deliveryTableModel.id);
}).expanded, }).expanded,
8.width, 8.width,
AppFilledButton(buttonColor: AppColor.primary10, label: 'Next'.addTranslation, maxWidth: true, onPressed: _nextTap).expanded, AppFilledButton(buttonColor: AppColor.primary10, label: 'Next'.addTranslation, maxWidth: true, onPressed: _nextTap).expanded,

@ -28,7 +28,7 @@ class DemoRequestItemView extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
StatusLabel( StatusLabel(
label: requestData!.statusName!, label: requestData!.statusName,
textColor: AppColor.demoRequestStatusTextColor(context, requestData?.statusValue ?? 0), textColor: AppColor.demoRequestStatusTextColor(context, requestData?.statusValue ?? 0),
backgroundColor: AppColor.demoRequestStatus(context, requestData?.statusValue ?? 0), backgroundColor: AppColor.demoRequestStatus(context, requestData?.statusValue ?? 0),
), ),
@ -54,7 +54,7 @@ class DemoRequestItemView extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
StatusLabel( StatusLabel(
label: requestDetails!.status!, label: requestDetails!.status,
textColor: AppColor.demoRequestStatusTextColor(context, requestDetails?.statusValue ?? 0), textColor: AppColor.demoRequestStatusTextColor(context, requestDetails?.statusValue ?? 0),
backgroundColor: AppColor.demoRequestStatus(context, requestDetails?.statusValue ?? 0), backgroundColor: AppColor.demoRequestStatus(context, requestDetails?.statusValue ?? 0),
), ),

@ -33,7 +33,8 @@ class IncidentDetailPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: const DefaultAppBar(title: "Request Details"), appBar: const DefaultAppBar(title: "Request Details"),
body: FutureBuilder<IncidentDataModel?>( body: SafeArea(
child: FutureBuilder<IncidentDataModel?>(
future: Provider.of<IncidentProvider>(context, listen: false).getIncidentById(incidentId), future: Provider.of<IncidentProvider>(context, listen: false).getIncidentById(incidentId),
builder: (BuildContext context, AsyncSnapshot<IncidentDataModel?> snapshot) { builder: (BuildContext context, AsyncSnapshot<IncidentDataModel?> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) return CircularProgressIndicator(color: AppColor.loadingColor(context)).center; if (snapshot.connectionState == ConnectionState.waiting) return CircularProgressIndicator(color: AppColor.loadingColor(context)).center;
@ -75,7 +76,8 @@ class IncidentDetailPage extends StatelessWidget {
], ],
).toShadowContainer(context), ).toShadowContainer(context),
); );
})); }),
));
} }
List<Widget> requestDetails(BuildContext context, IncidentDataModel incidentData) { List<Widget> requestDetails(BuildContext context, IncidentDataModel incidentData) {

@ -3,6 +3,7 @@ import 'package:test_sa/modules/medical_gas_inspection/models/medical_gas_data_m
class DeliveryNotesFormModel { class DeliveryNotesFormModel {
int? id; int? id;
DateTime? requestDate;
DateTime? deliveredDate; DateTime? deliveredDate;
String? deliveryNoteNumber; String? deliveryNoteNumber;
bool? isPressureChecked; bool? isPressureChecked;
@ -15,6 +16,7 @@ class DeliveryNotesFormModel {
DeliveryNotesFormModel({ DeliveryNotesFormModel({
this.id, this.id,
this.requestDate,
this.deliveredDate, this.deliveredDate,
this.deliveryNoteNumber, this.deliveryNoteNumber,
this.isPressureChecked, this.isPressureChecked,
@ -31,6 +33,7 @@ class DeliveryNotesFormModel {
DeliveryNotesFormModel.fromJson(Map<String, dynamic> json) { DeliveryNotesFormModel.fromJson(Map<String, dynamic> json) {
id = json['id']; id = json['id'];
requestDate = json['requestDate'] != null ? DateTime.parse(json['requestDate']) : null;
deliveredDate = json['deliveredDate'] != null ? DateTime.parse(json['deliveredDate']) : null; deliveredDate = json['deliveredDate'] != null ? DateTime.parse(json['deliveredDate']) : null;
deliveryNoteNumber = json['deliveryNoteNumber']; deliveryNoteNumber = json['deliveryNoteNumber'];
isPressureChecked = json['isPressureChecked']; isPressureChecked = json['isPressureChecked'];
@ -54,6 +57,7 @@ class DeliveryNotesFormModel {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'id': id, 'id': id,
'requestDate': requestDate?.toIso8601String(),
'deliveredDate': deliveredDate?.toIso8601String(), 'deliveredDate': deliveredDate?.toIso8601String(),
'deliveryNoteNumber': deliveryNoteNumber, 'deliveryNoteNumber': deliveryNoteNumber,
'isPressureChecked': isPressureChecked, 'isPressureChecked': isPressureChecked,

@ -98,6 +98,7 @@ class _UpdateDeliveryNotesState extends State<UpdateDeliveryNotes> {
children: [ children: [
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12,
children: [ children: [
ADatePicker( ADatePicker(
label: "Delivered Date", label: "Delivered Date",
@ -118,7 +119,7 @@ class _UpdateDeliveryNotesState extends State<UpdateDeliveryNotes> {
}); });
}, },
), ),
16.height, // 16.height,
AppTextFormField( AppTextFormField(
initialValue: formModel.deliveryNoteNumber, initialValue: formModel.deliveryNoteNumber,
labelText: "Delivery Notes", labelText: "Delivery Notes",
@ -136,7 +137,25 @@ class _UpdateDeliveryNotesState extends State<UpdateDeliveryNotes> {
formModel.deliveryNoteNumber = value; formModel.deliveryNoteNumber = value;
}, },
), ),
16.height, ADatePicker(
label: "Requested Date",
hideShadow: true,
backgroundColor: AppColor.fieldBgColor(context),
from: DateTime.now().subtract(const Duration(days: 365 * 3)),
date: formModel.requestDate,
formatDateWithTime: true,
onDatePicker: (selectedDate) {
showThemedTimePicker(
context: context,
initialTime: TimeOfDay.now(),
).then((selectedTime) {
if (selectedTime != null) {
formModel.requestDate = DateTime(selectedDate.year, selectedDate.month, selectedDate.day, selectedTime.hour, selectedTime.minute);
setState(() {});
}
});
},
),
//Older design code //Older design code
// MultipleItemDropDownMenu<MedicalGasRequestDetailModel, NullableLoadingProvider>( // MultipleItemDropDownMenu<MedicalGasRequestDetailModel, NullableLoadingProvider>(
@ -194,7 +213,7 @@ class _UpdateDeliveryNotesState extends State<UpdateDeliveryNotes> {
left: 'Items'.addTranslation.bodyText(context).custom(color: AppColor.textColor(context)), left: 'Items'.addTranslation.bodyText(context).custom(color: AppColor.textColor(context)),
right: 'Delivered Qty'.addTranslation.bodyText(context).custom(color: AppColor.textColor(context)), right: 'Delivered Qty'.addTranslation.bodyText(context).custom(color: AppColor.textColor(context)),
), ),
8.height, // 8.height,
ListView.separated( ListView.separated(
separatorBuilder: (_, __) => 12.height, separatorBuilder: (_, __) => 12.height,
itemCount: formModel.deliveredItemList.length, itemCount: formModel.deliveredItemList.length,
@ -206,7 +225,7 @@ class _UpdateDeliveryNotesState extends State<UpdateDeliveryNotes> {
}, },
) )
], ],
16.height, // 16.height,
AttachmentPicker( AttachmentPicker(
label: 'Upload Attachment', label: 'Upload Attachment',
@ -216,7 +235,7 @@ class _UpdateDeliveryNotesState extends State<UpdateDeliveryNotes> {
onlyImages: false, onlyImages: false,
buttonIcon: 'attachment_icon'.toSvgAsset(color: AppColor.primary10), buttonIcon: 'attachment_icon'.toSvgAsset(color: AppColor.primary10),
), ),
16.height, // 16.height,
//should create a reusable widget. //should create a reusable widget.
Row( Row(
children: [ children: [
@ -238,7 +257,7 @@ class _UpdateDeliveryNotesState extends State<UpdateDeliveryNotes> {
.expanded, .expanded,
], ],
), ),
8.height, // 8.height,
Row( Row(
// mainAxisAlignment: MainAxisAlignment.start, // mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,

@ -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,182 @@
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,
spacing: 4.toScreenHeight,
children: [
if(isMandatory) ...[
4.height,
Text(
'You cannot update this notification as it is mandatory.'.addTranslation,
style: AppTextStyle.tiny.copyWith(
color: AppColor.red30,
),
),
],
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: InfoHeader16Widget(
template.templateName ?? '',
),
),
8.width,
_buildCompactToggle(context, isEnabled, isMandatory),
],
),
InfoTextWidget(
label: context.translation.notificationModule,
value: template.moduleName ?? '',
),
InfoTextWidget(
label: 'Sub Module'.addTranslation,
value: template.subModuleName ?? '',
),
if (template.templateChannels != null && template.templateChannels!.isNotEmpty) ...[
InfoTextLabelWidget(
label: '${context.translation.channelPreference} :',
),
_buildChannelChips(context, isMandatory),
],
],
).toShadowContainer(context);
}
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);
}
},
),
);
}
}

@ -67,7 +67,8 @@ class _TrafRequestDetailPageState extends State<TrafRequestDetailPage> {
} }
return trafProvider.trafRequestDataModel == null return trafProvider.trafRequestDataModel == null
? const NoDataFound().center ? const NoDataFound().center
: Column( : SafeArea(
child: Column(
children: [ children: [
ListView( ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@ -90,6 +91,7 @@ class _TrafRequestDetailPageState extends State<TrafRequestDetailPage> {
// }), // }),
// ), // ),
], ],
),
); );
}, },
)); ));

@ -201,6 +201,14 @@ class AppColor {
static Color filterButtonTextColor(BuildContext context) => context.isDark ? neutral30 : neutral50; static Color filterButtonTextColor(BuildContext context) => context.isDark ? neutral30 : neutral50;
static Color addItemButtonColor(BuildContext context) => context.isDark ? AppColor.neutral60 : AppColor.white10; 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) { static Color getRequestStatusTextColorByName(BuildContext context, String? status) {
switch ((status ?? "").toLowerCase()) { switch ((status ?? "").toLowerCase()) {
case "open": case "open":
@ -215,6 +223,8 @@ class AppColor {
return redStatusTextColor; return redStatusTextColor;
case "request completed": case "request completed":
return greenStatusTextColor; return greenStatusTextColor;
case "complete":
return greenStatusTextColor;
case "low priority": case "low priority":
return greenStatusTextColor; return greenStatusTextColor;
case "low": case "low":
@ -410,6 +420,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 greenStatusTextColor;
case 3:
return blueStatusTextColor;
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 blueStatusColor;
default:
return defaultStatusColor(context);
}
}
static Color getPriorityStatusColor(BuildContext context, int id) { static Color getPriorityStatusColor(BuildContext context, int id) {
switch (id) { switch (id) {
//low priority //low priority

@ -2,8 +2,10 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.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/text_extensions.dart';
import 'package:test_sa/extensions/widget_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/modules/site_request_module/screens/site_request_history_screen.dart'; import 'package:test_sa/modules/site_request_module/screens/site_request_history_screen.dart';
import 'package:test_sa/new_views/app_style/app_color.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/pages/login_page.dart';
@ -64,8 +66,10 @@ class AppDrawer extends StatelessWidget {
ListView( ListView(
padding: const EdgeInsets.only(top: 24), padding: const EdgeInsets.only(top: 24),
children: [ children: [
drawerItem("drawer_notification", context.translation.notifications, context).onPress(() => Navigator.of(context).pushNamed(NotificationsPage.id)), // drawerItem("drawer_notification", context.translation.notifications, context).onPress(() => Navigator.of(context).pushNamed(NotificationsPage.id)),
18.height, // 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))*/, // drawerItem("help_center", context.translation.helpCenter, context) /*.onPress(() => Navigator.of(context).pushNamed(HelpCenterPage.id))*/,
// 18.height, // 18.height,
// drawerItem("rate_us", context.translation.rateUs, context), // drawerItem("rate_us", context.translation.rateUs, context),
@ -74,7 +78,7 @@ class AppDrawer extends StatelessWidget {
18.height, 18.height,
drawerItem("site_request_icon", "Site Request", context).onPress(() => Navigator.of(context).pushNamed(SiteRequestHistoryScreen.routeName)), drawerItem("site_request_icon", "Site Request", context).onPress(() => Navigator.of(context).pushNamed(SiteRequestHistoryScreen.routeName)),
18.height, 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)), drawerItem("swipe", "Swipe History", context).onPress(() => Navigator.of(context).pushNamed(SwipeHistoryView.routeName)),
// 18.height, // 18.height,
// drawerItem("report", context.translation.reportBg, context) /*.onPress(() => Navigator.of(context).pushNamed(ReportBugPage.id))*/, // drawerItem("report", context.translation.reportBg, context) /*.onPress(() => Navigator.of(context).pushNamed(ReportBugPage.id))*/,

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

@ -80,8 +80,7 @@ class _MultipleItemDropDownMenuState<T extends Base, X extends LoadingListNotifi
return widget.initialValue!.any((init) { return widget.initialValue!.any((init) {
// Try Lookup-specific comparison first (id/value) // Try Lookup-specific comparison first (id/value)
if (element is Lookup && init is Lookup) { if (element is Lookup && init is Lookup) {
return (element.id != null && element.id == init.id) || return (element.id != null && element.id == init.id) || (element.value != null && element.value == init.value);
(element.value != null && element.value == init.value);
} }
// Fallback to identifier comparison // Fallback to identifier comparison
if (element.identifier != null && init.identifier != null) { if (element.identifier != null && init.identifier != null) {
@ -124,8 +123,7 @@ class _MultipleItemDropDownMenuState<T extends Base, X extends LoadingListNotifi
return widget.initialValue!.any((init) { return widget.initialValue!.any((init) {
// Try Lookup-specific comparison first (id/value) // Try Lookup-specific comparison first (id/value)
if (element is Lookup && init is Lookup) { if (element is Lookup && init is Lookup) {
return (element.id != null && element.id == init.id) || return (element.id != null && element.id == init.id) || (element.value != null && element.value == init.value);
(element.value != null && element.value == init.value);
} }
// Fallback to identifier comparison // Fallback to identifier comparison
if (element.identifier != null && init.identifier != null) { if (element.identifier != null && init.identifier != null) {
@ -166,8 +164,7 @@ class _MultipleItemDropDownMenuState<T extends Base, X extends LoadingListNotifi
final hasMatch = oldWidget.initialValue.any((old) { final hasMatch = oldWidget.initialValue.any((old) {
// Try Lookup-specific comparison first (id/value) // Try Lookup-specific comparison first (id/value)
if (old is Lookup && current is Lookup) { if (old is Lookup && current is Lookup) {
return (old.id != null && old.id == current.id) || return (old.id != null && old.id == current.id) || (old.value != null && old.value == current.value);
(old.value != null && old.value == current.value);
} }
// Fallback to identifier comparison // Fallback to identifier comparison
if (old.identifier != null && current.identifier != null) { if (old.identifier != null && current.identifier != null) {
@ -189,15 +186,11 @@ class _MultipleItemDropDownMenuState<T extends Base, X extends LoadingListNotifi
final isEmpty = (X == NullableLoadingProvider ? widget.staticData : provider?.items)?.isEmpty ?? true; final isEmpty = (X == NullableLoadingProvider ? widget.staticData : provider?.items)?.isEmpty ?? true;
// Determine the actual loading state - respect explicit loading prop and enabled state // Determine the actual loading state - respect explicit loading prop and enabled state
final bool isActuallyLoading = widget.enabled final bool isActuallyLoading = widget.enabled ? (widget.loading ?? ((X == NullableLoadingProvider) ? false : provider?.loading ?? false)) : false; // Never show loading when disabled
? (widget.loading ?? ((X == NullableLoadingProvider) ? false : provider?.loading ?? false))
: false; // Never show loading when disabled
// Provide a valid stateCode when disabled to prevent AppLoadingManager from auto-fetching // Provide a valid stateCode when disabled to prevent AppLoadingManager from auto-fetching
// When enabled, use the provider's actual stateCode // When enabled, use the provider's actual stateCode
final int? effectiveStateCode = widget.enabled final int? effectiveStateCode = widget.enabled ? ((X == NullableLoadingProvider) ? 200 : provider?.stateCode) : 200; // Return 200 when disabled to indicate "loaded" state (prevents auto-fetch)
? ((X == NullableLoadingProvider) ? 200 : provider?.stateCode)
: 200; // Return 200 when disabled to indicate "loaded" state (prevents auto-fetch)
return AppLoadingManager( return AppLoadingManager(
isLoading: isActuallyLoading, isLoading: isActuallyLoading,
@ -240,7 +233,7 @@ class _MultipleItemDropDownMenuState<T extends Base, X extends LoadingListNotifi
if (_selectedItem.isEmpty) if (_selectedItem.isEmpty)
Text( Text(
context.translation.select, context.translation.select,
style: Theme.of(context).textTheme.bodyLarge, style: AppTextStyles.bodyText.copyWith(color: AppColor.textStyleColor(context)),
) )
else else
Wrap( Wrap(

@ -263,9 +263,9 @@ class _SingleItemDropDownMenuState<T extends Base, X extends LoadingListNotifier
isExpanded: true, isExpanded: true,
hint: Text( hint: Text(
isEmpty || widget.enabled == false ? context.translation.noDataFound : context.translation.select, isEmpty || widget.enabled == false ? context.translation.noDataFound : context.translation.select,
style: AppTextStyles.heading5.copyWith(color: AppColor.textStyleColor(context)), style: AppTextStyles.bodyText.copyWith(color: AppColor.textStyleColor(context)),
), ),
style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 12), // style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 12),
underline: const SizedBox.shrink(), underline: const SizedBox.shrink(),
onChanged: (widget.enabled == false || widget.showAsBottomSheet || widget.showAsFullScreenDialog) onChanged: (widget.enabled == false || widget.showAsBottomSheet || widget.showAsFullScreenDialog)
? null ? null
@ -284,7 +284,7 @@ class _SingleItemDropDownMenuState<T extends Base, X extends LoadingListNotifier
child: Text(value.name ?? "", // Null-aware operator for value.name child: Text(value.name ?? "", // Null-aware operator for value.name
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: AppTextStyles.heading5.copyWith(color: AppColor.textStyleColor(context)) style: AppTextStyles.bodyText.copyWith(color: AppColor.textStyleColor(context))
// Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500, color: context.isDark ? AppColor.white10 : AppColor.black10), // Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500, color: context.isDark ? AppColor.white10 : AppColor.black10),
), ),
); );

@ -57,6 +57,8 @@ class _AllRequestsFilterPageState extends State<AllRequestsFilterPage> {
search!.assetNo?.controller?.text = search?.assetNo!.value ?? ""; search!.assetNo?.controller?.text = search?.assetNo!.value ?? "";
search!.manufacture?.controller?.text = search?.manufacture!.value ?? ""; search!.manufacture?.controller?.text = search?.manufacture!.value ?? "";
search!.sn?.controller?.text = search?.sn?.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 ?? ""; search!.model?.controller?.text = search?.model?.value ?? "";
} }
super.initState(); super.initState();
@ -74,6 +76,8 @@ class _AllRequestsFilterPageState extends State<AllRequestsFilterPage> {
manufacture: SearchByManufactureModel(context, controller: TextEditingController()), manufacture: SearchByManufactureModel(context, controller: TextEditingController()),
sn: SearchBySNModel(context, controller: TextEditingController()), sn: SearchBySNModel(context, controller: TextEditingController()),
model: SearchByRequestModel(context, controller: TextEditingController()), model: SearchByRequestModel(context, controller: TextEditingController()),
externalPo: SearchByExternalPO(context, controller: TextEditingController()),
internalPo: SearchByInternalPO(context, controller: TextEditingController()),
); );
search!.searchBySelectedValue = search?.requestNumber; search!.searchBySelectedValue = search?.requestNumber;
} }
@ -141,6 +145,11 @@ class _AllRequestsFilterPageState extends State<AllRequestsFilterPage> {
'Assessment of Need & Technology': 9, 'Assessment of Need & Technology': 9,
// 'Demo': 15, moved to permission api // '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) { if (context.settingProvider.isUserFlowMedical && !context.userProvider.isNurse) {
types['Incidents'] = 16; types['Incidents'] = 16;

@ -58,6 +58,8 @@ class _AllRequestsSearchPageState extends State<AllRequestsSearchPage> {
search!.assetNo?.controller?.text = search?.assetNo?.value ?? ""; search!.assetNo?.controller?.text = search?.assetNo?.value ?? "";
search!.manufacture?.controller?.text = search?.manufacture?.value ?? ""; search!.manufacture?.controller?.text = search?.manufacture?.value ?? "";
search!.sn?.controller?.text = search?.sn?.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 ?? ""; search!.model?.controller?.text = search?.model?.value ?? "";
} }
super.initState(); super.initState();
@ -73,6 +75,8 @@ class _AllRequestsSearchPageState extends State<AllRequestsSearchPage> {
manufacture: SearchByManufactureModel(context, controller: TextEditingController()), manufacture: SearchByManufactureModel(context, controller: TextEditingController()),
sn: SearchBySNModel(context, controller: TextEditingController()), sn: SearchBySNModel(context, controller: TextEditingController()),
model: SearchByRequestModel(context, controller: TextEditingController()), model: SearchByRequestModel(context, controller: TextEditingController()),
externalPo: SearchByExternalPO(context, controller: TextEditingController()),
internalPo: SearchByInternalPO(context, controller: TextEditingController()),
); );
search!.searchBySelectedValue = search?.requestNumber; search!.searchBySelectedValue = search?.requestNumber;
} }
@ -121,7 +125,16 @@ class _AllRequestsSearchPageState extends State<AllRequestsSearchPage> {
Text(context.translation.searchBy, style: AppTextStyles.heading5.copyWith(color: AppColor.labelTextStyleColor(context))), Text(context.translation.searchBy, style: AppTextStyles.heading5.copyWith(color: AppColor.labelTextStyleColor(context))),
8.height, 8.height,
RequestsSearchButtons( 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!, initialValue: search!.searchBySelectedValue!,
onSelect: (value) { onSelect: (value) {
if (search!.searchBySelectedValue != value) { if (search!.searchBySelectedValue != value) {

@ -130,6 +130,17 @@ class _MyRequestsPageState extends State<MyRequestsPage> with SingleTickerProvid
Request(9, 'Assessment of Need & Technology'), Request(9, 'Assessment of Need & Technology'),
// Request(15, 'Demo'), moved to permission api // 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 = <int>{};
requestsList = requestsList.where((req) {
return req.id == null ? true : seenIds.add(req.id!);
}).toList();
} }
if (context.userProvider.isCommissioningEngineer && context.settingProvider.isUserFlowMedical) { if (context.userProvider.isCommissioningEngineer && context.settingProvider.isUserFlowMedical) {
///TODO need to show only allowed ids for all workOrder ///TODO need to show only allowed ids for all workOrder
@ -342,6 +353,8 @@ class _MyRequestsPageState extends State<MyRequestsPage> with SingleTickerProvid
(_search!.assetName?.value?.isNotEmpty ?? false) || (_search!.assetName?.value?.isNotEmpty ?? false) ||
(_search!.manufacture?.value?.isNotEmpty ?? false) || (_search!.manufacture?.value?.isNotEmpty ?? false) ||
(_search!.sn?.value?.isNotEmpty ?? false) || (_search!.sn?.value?.isNotEmpty ?? false) ||
(_search!.externalPo?.value?.isNotEmpty ?? false) ||
(_search!.internalPo?.value?.isNotEmpty ?? false) ||
(_search!.statuses?.isNotEmpty ?? false) || (_search!.statuses?.isNotEmpty ?? false) ||
(_search!.typeTransaction?.isNotEmpty ?? false) || (_search!.typeTransaction?.isNotEmpty ?? false) ||
(_search!.startDate != null) || (_search!.startDate != null) ||
@ -470,6 +483,30 @@ class _MyRequestsPageState extends State<MyRequestsPage> 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 // Model
if (_search?.model?.value?.isNotEmpty ?? false) { if (_search?.model?.value?.isNotEmpty ?? false) {
@ -653,6 +690,10 @@ class _MyRequestsPageState extends State<MyRequestsPage> with SingleTickerProvid
_search?.manufacture?.controller?.clear(); _search?.manufacture?.controller?.clear();
_search?.sn?.value = null; _search?.sn?.value = null;
_search?.sn?.controller?.clear(); _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?.value = null;
_search?.model?.controller?.clear(); _search?.model?.controller?.clear();
_search?.statuses?.clear(); _search?.statuses?.clear();

@ -13,6 +13,7 @@ import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart'; import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_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/helper/utils.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/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/app_filled_button.dart';
@ -177,6 +178,14 @@ class _SettingsPageState extends State<SettingsPage> {
], ],
); );
}), }),
const Divider().defaultStyle(context),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InfoHeader16Widget("Notification Settings".addTranslation),
"arrow_right".toSvgAsset(width: 18, color: AppColor.icon2Color(context))
],
).onPress(() => Navigator.of(context).pushNamed(NotificationSettingPage.id)),
], ],
).toShadowContainer(context, padding: 16), ).toShadowContainer(context, padding: 16),
AppFilledButton( AppFilledButton(

@ -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,254 @@
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/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;
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 moduleProvider = context.read<ModulesWithSubModulesProvider>();
// Load data from providers if not already loaded
final futures = <Future>[];
if (priorityProvider.items.isEmpty) futures.add(priorityProvider.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 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;
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: [
InfoHeader16Widget("Priority"),
8.height,
SingleItemDropDownMenu<Lookup, NotificationPriorityLookupProvider>(
context: context,
showAsFullScreenDialog: true,
showShadow: true,
title: "Select Priority",
initialValue: selectedPriority,
onSelect: (value) {
setState(() {
selectedPriority = value;
if (value != null) {
filter?.priorityIds = [value.id!];
} else {
filter?.priorityIds?.clear();
}
});
},
),
12.height,
InfoHeader16Widget("Module"),
8.height,
SingleItemDropDownMenu<ModuleWithSubModulesModel, ModulesWithSubModulesProvider>(
context: context,
showAsFullScreenDialog: true,
showShadow: true,
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();
}
});
},
),
12.height,
InfoHeader16Widget("SubModule"),
8.height,
SingleItemDropDownMenu<SubModuleModel, NullableLoadingProvider>(
context: context,
showAsFullScreenDialog: true,
showShadow: true,
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();
}
});
},
),
12.height,
InfoHeader16Widget(context.translation.pickDate),
8.height,
Row(
children: [
ADatePicker(
label: context.translation.from,
from: DateTime(2000),
date: filter!.startDate,
onDatePicker: (date) {
setState(() {
filter!.startDate = date;
});
},
).expanded,
12.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,
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,16 @@
import 'dart:developer';
import 'package:flutter/material.dart'; 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/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/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/system_notification_model.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/lazy_loading.dart';
import 'package:test_sa/views/widgets/loaders/no_data_found.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 { class NotificationsList extends StatelessWidget {
final List<SystemNotificationModel> notifications; final List<SystemNotificationModel> notifications;
@ -22,23 +27,22 @@ class NotificationsList extends StatelessWidget {
return LazyLoading( return LazyLoading(
nextPage: nextPage, nextPage: nextPage,
onLazyLoad: onLazyLoad, onLazyLoad: onLazyLoad,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: ListView.separated( child: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(0),
itemCount: notifications.length, itemCount: notifications.length,
separatorBuilder: (context, itemIndex) => const Divider().defaultStyle(context).paddingOnly(top: 4, bottom: 4), separatorBuilder: (context, itemIndex) => 12.height,
itemBuilder: (context, itemIndex) { itemBuilder: (context, itemIndex) {
return NotificationItem( final notification = notifications[itemIndex];
notification: notifications[itemIndex], return NotificationCardRedesigned(
onPressed: (notification) { notification: notification,
onTap: () async {
if (notification.id != null && !(notification.readed ?? false)) {
final provider = Provider.of<NotificationsProvider>(context, listen: false);
await provider.markOneAsRead(notificationId: notification.id!);
}
FirebaseNotificationManger.handleMessage(context, notification.toNotificationJson()); FirebaseNotificationManger.handleMessage(context, notification.toNotificationJson());
}, },
); );
}, },
).toShadowContainer(context),
), ),
); );
} }

@ -2,10 +2,21 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:test_sa/controllers/providers/api/notifications_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/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/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_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/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/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'; import '../../../widgets/loaders/loading_manager.dart';
@ -21,26 +32,275 @@ class NotificationsPage extends StatefulWidget {
class _NotificationsPageState extends State<NotificationsPage> with TickerProviderStateMixin { class _NotificationsPageState extends State<NotificationsPage> with TickerProviderStateMixin {
late NotificationsProvider _notificationsProvider; late NotificationsProvider _notificationsProvider;
late UserProvider _userProvider; late UserProvider _userProvider;
late SettingProvider _settingProvider;
final TextEditingController _searchController = TextEditingController();
int _selectedTabIndex = 0; // 0: All, 1: Unread, 2: Read
String _searchQuery = '';
bool isAscending = true;
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
notificationsProvider.reset();
await notificationsProvider.getSystemNotifications(user: userProvider.user!, resetProvider: true, filters: _activeFilters);
await notificationsProvider.getUnreadCount();
}
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.totalNotificationUnread;
}
int get _allCount {
return _notificationsProvider.totalAllNotifications;
}
int get _readCount {
return _notificationsProvider.totalNotificationRead;
}
/// Get count of active filters
int get _activeFilterCount {
if (_activeFilters == null) return 0;
int count = 0;
if (_activeFilters!.priorityIds?.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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_notificationsProvider = Provider.of<NotificationsProvider>(context); _notificationsProvider = Provider.of<NotificationsProvider>(context);
_userProvider = Provider.of<UserProvider>(context); _userProvider = Provider.of<UserProvider>(context);
_settingProvider = Provider.of<SettingProvider>(context);
return Scaffold( return Scaffold(
appBar: DefaultAppBar(title: context.translation.notifications), appBar: DefaultAppBar(
title: context.translation.notifications,
actions: [
TextButton.icon(
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(
color: AppColor.primary10,
fontWeight: FontWeight.w500,
),
),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
foregroundColor: _unreadCount > 0 ? AppColor.primary10 : AppColor.neutral30,
),
),
],
),
body: SafeArea( body: SafeArea(
child: Column(
spacing: 16.toScreenHeight,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
AppTextFormField(
controller: _searchController,
labelText: context.translation.search,
showShadow: false,
showBorder: true,
textInputAction: TextInputAction.search,
suffixIcon: "search".toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)).paddingOnly(end: 12),
onChange: (value) {
_onSearchChanged(value);
},
),
Row(
children: [
Expanded(
child: SizedBox(
height: 34.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,
Container(
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)],
color: AppColor.background(context),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: AppColor.borderColor(context)),
),
child: Row(
children: [
Transform.flip(
flipY: isAscending,
child: 'dashboard/sort_descending'.toSvgAsset(width: 20, height: 20, color: AppColor.iconColor(context)).onPress(() {
_toggleSort();
}),
),
12.width,
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 we are not using custom badge widget here as it need more customizations
if (_activeFilterCount > 0)
Positioned(
right: -8,
top: -12,
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,
),
),
),
),
],
),
],
),
),
],
),
// 8.height,
// Notifications List
Expanded(
child: LoadingManager( child: LoadingManager(
isLoading: (_notificationsProvider.isLoading ?? false) && _notificationsProvider.notifications.isEmpty, isLoading: (_notificationsProvider.isLoading) && _notificationsProvider.notifications.isEmpty,
isFailedLoading: _notificationsProvider.notifications == null, isFailedLoading: false,
stateCode: _notificationsProvider.stateCode, stateCode: _notificationsProvider.stateCode,
onRefresh: () async { onRefresh: () async {
_notificationsProvider.reset(); _notificationsProvider.reset();
await _notificationsProvider.getSystemNotifications(user: _userProvider.user!, resetProvider: true await _notificationsProvider.getSystemNotifications(
user: _userProvider.user!,
// host: _settingProvider.host, resetProvider: true,
// hospitalId: _userProvider.user.clientId, filters: _activeFilters,
); );
}, },
child: NotificationsList( child: NotificationsList(
@ -48,13 +308,15 @@ class _NotificationsPageState extends State<NotificationsPage> with TickerProvid
onLazyLoad: () async { onLazyLoad: () async {
await _notificationsProvider.getSystemNotifications( await _notificationsProvider.getSystemNotifications(
user: _userProvider.user!, user: _userProvider.user!,
// host: _settingProvider.host, filters: _activeFilters,
// hospitalId: _userProvider.user.clientId,
); );
}, },
notifications: _notificationsProvider.notifications, notifications: _filteredNotifications,
),
), ),
), ),
],
).paddingOnly(start: 16, end: 16, top: 16),
), ),
); );
} }

@ -0,0 +1,77 @@
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;
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 Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 6.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: [
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,
),
),
),
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);
}
}

@ -0,0 +1,56 @@
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: AnimatedContainer(
duration: const Duration(milliseconds: 250),
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 6),
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: FontWeight.w500,
),
),
),
);
}
}

@ -140,10 +140,9 @@ class _ProfilePageState extends State<ProfilePage> {
}), }),
8.height, 8.height,
Text( Text(
"Work Order Status".addTranslation, (_user.username ?? ""),
style: AppTextStyles.heading3.copyWith(color: AppColor.headingTextColor(context)), style: AppTextStyles.heading3.copyWith(fontWeight: FontWeight.w600, color: AppColor.headingTextColor(context)),
), ),
(_user.username ?? "").heading3(context).custom(fontWeight: FontWeight.w600, color: context.isDark ? AppColor.neutral10 : AppColor.neutral50),
], ],
), ),
), ),

@ -93,17 +93,10 @@ class _ESignatureState extends State<ESignature> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
widget.title.tinyFont(context).paddingOnly(top: 8), InfoTextLabelWidget(label: widget.title),
(!_editable (!_editable
? "edit".toSvgAsset(width: 48).paddingOnly(top: 8) ? "edit_icon".toSvgAsset(color: AppColor.icon2Color(context))
: Container( : "done".toSvgAsset(width: 24))
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),
))
.onPress(() async { .onPress(() async {
if (_editable) { if (_editable) {
signature = await _controller!.toPngBytes(); signature = await _controller!.toPngBytes();

@ -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. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.9.0+50 version: 1.9.3+53
environment: environment:
sdk: ">=3.5.0 <4.0.0" sdk: ">=3.5.0 <4.0.0"

Loading…
Cancel
Save