Compare commits

...

73 Commits

Author SHA1 Message Date
Sikander Saleem a9000de678 v1.9.5+55 uploaded on stores 3 days ago
Sikander Saleem 4218acb4b8 improvement 3 days ago
Sikander Saleem 494d9ba8d7 get site by user id replace in asset filters 3 days ago
Sikander Saleem 2ab2abd693 site are changed as per organization 3 days ago
Sikander Saleem e4a09fed1f improvement 3 days ago
Sikander Saleem 072953a3a7 role name added 4 days ago
Sikander Saleem 6ad8471e9b loading issue fixed in cm update spare part 2 weeks ago
Sikander Saleem fbd5d8167d v1.9.4+54 uploaded. 1 month ago
Sikander Saleem eb880893e2 release config 1 month ago
Sikander Saleem 2bb126c91f ios version upgraded to 15 1 month ago
Sikander Saleem d3e741e735 Merge remote-tracking branch 'refs/remotes/origin/main_production_site_api_change' into main_production_spare_part
# Conflicts:
#	lib/controllers/api_routes/urls.dart
1 month ago
Sikander Saleem 07131e9349 Merge branch 'refs/heads/main_production' into main_production_spare_part
# Conflicts:
#	lib/controllers/api_routes/urls.dart
1 month ago
WaseemAbbasi22 51d1f38083 user site api added 1 month ago
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. 2 months ago
Sikander Saleem 071dd082b3 external po & internal po added in search 2 months ago
Sikander Saleem 6acc21fc6a improvements, hide swipe option for FM 2 months ago
Sikander Saleem c4fb4a5908 improvements 2 months ago
WaseemAbbasi22 08dfc807fc bug fixes attachment can be uploaded at any stage 2 months ago
WaseemAbbasi22 83eec2c4d5 bug fixes 2 months ago
Sikander Saleem a0180eef77 v1.9.1+51 uploaded on stores. 2 months ago
Sikander Saleem cc9235eadd improvement 2 months ago
Sikander Saleem 5f91a195f4 improvement 2 months ago
Sikander Saleem 1977283646 Merge branch 'refs/heads/main_production' into main_production_spare_part 2 months 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 d7c8d9f3cf rename changes, apk provided to FM. 2 months ago
Sikander Saleem f413d46738 improvements 2 months ago
Sikander Saleem fb6716f3df improvement. 2 months ago
Sikander Saleem 09ecf4fab4 traf added. 2 months ago
Sikander Saleem 764814171b improvements 2 months ago
Sikander Saleem 30a69b1ca6 improvements-cont 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 d63f7d8939 improvements 2 months ago
WaseemAbbasi22 86aa0f67d8 improvements 2 months ago
WaseemAbbasi22 c88b5770c9 improvements 2 months ago
Sikander Saleem 75349c4724 improvements 2 months ago
Sikander Saleem da680fb210 Merge branch 'refs/heads/main_production' into main_production_spare_part 2 months ago
Sikander Saleem b9a354d2a2 complete status color fix. 2 months ago
Sikander Saleem e2f227a5e7 Merge branch 'refs/heads/main_production' into main_production_spare_part 2 months ago
Sikander Saleem af2528f041 dropdown text ui improvement 2 months ago
Sikander Saleem fff9181ec2 Merge remote-tracking branch 'refs/remotes/origin/main_production' into main_production_spare_part 2 months ago
WaseemAbbasi22 7f4a06b0db PM activity and cost flow implemented 2 months ago
Sikander Saleem 10d7aa4f0a bug fix. 2 months ago
WaseemAbbasi22 b56b998125 spare part ui implemented 2 months ago
Sikander Saleem 27aee0b2b2 ui/ux asset delivery improvements 3 months ago
Sikander Saleem d71ddcdf1a ui/ux asset delivery improvements 3 months ago
Sikander Saleem 91003cc8a1 apk provided to QA team. 3 months ago
Sikander Saleem 3f214ba8a7 summary count fixed. 3 months ago
Sikander Saleem 6cf85d54d4 improvements 3 months ago
Sikander Saleem 1f65b2e0ac improvements 3 months ago
Sikander Saleem 59a9721fb1 rename traf -> Assessment of Need & Technology 3 months ago
Sikander Saleem 4230a0dd3b notification redirection improvements 3 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
3 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. 4 months ago
WaseemAbbasi22 37a2be7b78 improvements 4 months ago
WaseemAbbasi22 afc1e9c77a notification implementation completed 4 months ago
WaseemAbbasi22 9ab4ed6964 notifications apis integrated 4 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

@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project # Uncomment this line to define a global platform for your project
platform :ios, '14.0' platform :ios, '15.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@ -40,7 +40,7 @@ post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target) flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config| target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0' config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
# Fix for Xcode 15+ DT_TOOLCHAIN_DIR issue # Fix for Xcode 15+ DT_TOOLCHAIN_DIR issue
if config.base_configuration_reference && File.exist?(config.base_configuration_reference.real_path) if config.base_configuration_reference && File.exist?(config.base_configuration_reference.real_path)
@ -55,7 +55,7 @@ post_install do |installer|
installer.generated_projects.each do |project| installer.generated_projects.each do |project|
project.targets.each do |target| project.targets.each do |target|
target.build_configurations.each do |config| target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0' config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)', '$(inherited)',
## dart: PermissionGroup.microphone ## dart: PermissionGroup.microphone

@ -379,7 +379,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@ -403,7 +403,7 @@
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Atoms; INFOPLIST_KEY_CFBundleDisplayName = Atoms;
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -534,7 +534,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -587,7 +587,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@ -613,7 +613,7 @@
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Atoms; INFOPLIST_KEY_CFBundleDisplayName = Atoms;
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -709,7 +709,7 @@
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Atoms; INFOPLIST_KEY_CFBundleDisplayName = Atoms;
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",

@ -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,7 +1,7 @@
class URLs { class URLs {
URLs._(); URLs._();
static const String appReleaseBuildNumber = "50"; static const String appReleaseBuildNumber = "55";
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
@ -68,6 +68,8 @@ class URLs {
static get getSiteRequests => "$_baseUrl/SiteRequest/GetSiteRequests"; // get site request history static get getSiteRequests => "$_baseUrl/SiteRequest/GetSiteRequests"; // get site request history
static get getDepartmentsBySites => "$_baseUrl/Customer/GetDepartmentsBySites"; // post get departments by sites static get getDepartmentsBySites => "$_baseUrl/Customer/GetDepartmentsBySites"; // post get departments by sites
static getUserSites(String userId) => "$_baseUrl/Customer/GetSitesForUser?userId=$userId"; // get user's current sites static getUserSites(String userId) => "$_baseUrl/Customer/GetSitesForUser?userId=$userId"; // get user's current sites
static getSitesNotAccessibleForUser(String userId) => "$_baseUrl/Customer/GetSitesNotAccessibleForUser?userId=$userId"; // get user's current sites
static get getCustomersAutoCompleteSameOrg => "$_baseUrl/Customer/GetCustomersAutoCompleteSameOrg"; // get user's current sites
// static get login => "$_baseUrl/MobileAuth/LoginIntegration"; // mobile login // static get login => "$_baseUrl/MobileAuth/LoginIntegration"; // mobile login
static get register => "$_baseUrl/handle/create/user"; // post static get register => "$_baseUrl/handle/create/user"; // post
@ -254,15 +256,33 @@ class URLs {
static get getRegularVisits => "$_baseUrl/Visit/GetVisits"; // get static get getRegularVisits => "$_baseUrl/Visit/GetVisits"; // get
static get updateRegularVisits => "$_baseUrl/Visit/UpdateVisits"; // get static get updateRegularVisits => "$_baseUrl/Visit/UpdateVisits"; // get
static get getPlanPreventiveVisitById => "$_baseUrl/PlanPreventiveVisit/GetPlanPreventiveVisitById"; // get static get getPlanPreventiveVisitById => "$_baseUrl/PlanPreventiveVisit/GetPlanPreventiveVisitById";
static get updateVisitByEngineer => "$_baseUrl/PlanPreventiveVisit/UpdateVisitByEngineer"; // get static get updateVisitByEngineer => "$_baseUrl/PlanPreventiveVisit/UpdateVisitByEngineer";
static get createPmActivitySparePart => "$_baseUrl/PlanPreventiveVisit/CreateActivitySparePart";
static get updatePmActivitySparePart => "$_baseUrl/PlanPreventiveVisit/UpdateActivitySparePart";
static get deletePmActivitySparePart => "$_baseUrl/PlanPreventiveVisit/DeleteActivitySparePart";
static get updatePmActivityCost => "$_baseUrl/PlanPreventiveVisit/EngineerUpdateCost";
static get getSingleServiceRequest => "$_baseUrl/return/call/information"; // get static get getSingleServiceRequest => "$_baseUrl/return/call/information"; // get
static get getSuppliersAutoComplete => "$_baseUrl/Supplier/GetSuppliersAutoComplete"; static get getSuppliersAutoComplete => "$_baseUrl/Supplier/GetSuppliersAutoComplete";
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 +611,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
@ -604,4 +628,8 @@ class URLs {
static get submitApplicationDemoRequest => "$_baseUrl/DemoUser/CreateDemoUser"; // post submit application demo request// get application demo request history static get submitApplicationDemoRequest => "$_baseUrl/DemoUser/CreateDemoUser"; // post submit application demo request// get application demo request history
static get sendDemoUserOtp => "$_baseUrl/DemoUser/EmailVerification"; // post send OTP to user static get sendDemoUserOtp => "$_baseUrl/DemoUser/EmailVerification"; // post send OTP to user
static get verifyDemoUserOtp => "$_baseUrl/DemoUser/VerifyOtp"; // post verify OTP static get verifyDemoUserOtp => "$_baseUrl/DemoUser/VerifyOtp"; // post verify OTP
// spare part request endpoint.
} }

@ -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,69 +143,108 @@ class FirebaseNotificationManger {
return; return;
} }
// PPM=1, if (IsOldNotification) {
// ServiceRequestEngineer = 3, switch (transactionType) {
// AssetTransfer=7, case "1":
// SparePartTransaction= 8, serviceClass = PpmDetailsPage(requestId: int.parse(messageData["requestNumber"].toString()));
// GasRefill=9, break;
// TechnicalRetirmentWO = 11, case "3":
// Recurrent = 12, serviceClass = CMDetailPage(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1);
switch (transactionType) { break;
case "1": case "8":
serviceClass = PpmDetailsPage(requestId: int.parse(messageData["requestNumber"].toString())); serviceClass = CMDetailPage(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1);
break; break;
//these three request are same corrective maintenance.... case "11":
case "3": serviceClass = CMDetailPage(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1);
serviceClass = CMDetailPage(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1); break;
break; case "7":
case "8": serviceClass = DeviceTransferDetails(model: DeviceTransfer(id: int.parse(messageData["requestNumber"].toString())), moduleId: 3);
serviceClass = CMDetailPage(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1); break;
break; case "9":
case "11": serviceClass = GasRefillDetailsPage(
serviceClass = CMDetailPage(requestId: int.parse(messageData["requestNumber"].toString()), moduleId: 1); priority: messageData["priority"],
break; date: messageData["createdOn"],
case "7": moduleId: 2,
serviceClass = DeviceTransferDetails(model: DeviceTransfer(id: int.parse(messageData["requestNumber"].toString())), moduleId: 3); model: GasRefillModel(id: int.parse(messageData["requestNumber"].toString())),
break; );
case "9": break;
serviceClass = GasRefillDetailsPage( case "12":
priority: messageData["priority"], serviceClass = RecurrentWorkOrderView(taskId: int.parse(messageData["requestNumber"].toString()));
date: messageData["createdOn"], case "17":
moduleId: 2, serviceClass = SurveyPage(surveyId: int.parse(messageData["requestNumber"].toString()));
model: GasRefillModel(id: int.parse(messageData["requestNumber"].toString())),
); default:
break; serviceClass = const Scaffold(appBar: DefaultAppBar(), body: Center(child: NoDataFound()));
case "12": }
serviceClass = RecurrentWorkOrderView(taskId: int.parse(messageData["requestNumber"].toString())); } else {
case "17": switch (transactionType) {
serviceClass = SurveyPage(surveyId: int.parse(messageData["requestNumber"].toString())); 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()));
//Didn't handle task request yet... default:
// case 6: serviceClass = const Scaffold(appBar: DefaultAppBar(), body: Center(child: NoDataFound()));
// serviceClass = TaskRequestDetailsView( }
// taskId: int.parse(messageData["requestNumber"].toString()),
// requestDetails: RequestsDetails(nameOfType: messageData["sourceName"], status: messageData["statusName"], priority: messageData["priorityName"], date: messageData["createdDate"]));
// return;
default:
serviceClass = const Scaffold(appBar: DefaultAppBar(), body: Center(child: NoDataFound()));
} }
// if (messageData["requestType"] == "Service request to engineer") {
// serviceClass = ServiceRequestDetailMain(requestId: messageData["requestNumber"] ?? '');
// } else if (messageData["requestType"] == "Gas Refill") {
// serviceClass = GasRefillDetailsPage(
// priority: messageData["priority"],
// date: messageData["createdOn"],
// model: GasRefillModel(id: int.parse(messageData["requestNumber"].toString())),
// );
// } else if (messageData["requestType"] == "Asset Transfer") {
// serviceClass = DeviceTransferDetails(model: DeviceTransfer(id: int.parse(messageData["requestNumber"].toString())));
// } else if (messageData["requestType"] == "PPM") {
// serviceClass = PpmDetailsPage(requestId: int.parse(messageData["requestNumber"].toString()));
// }
// else if (data["requestType"] == "WorkOrder") {
//
// }
Navigator.of(context).push(MaterialPageRoute(builder: (_) => serviceClass!)); 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,46 +44,88 @@ 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,
priority: Priority.max, priority: Priority.max,
icon: "@drawable/ic_stat_name", icon: "@drawable/ic_stat_name",
playSound: true, playSound: true,
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(
categoryIdentifier: "atoms", static DarwinNotificationDetails iosNotificationDetails(String? threadId) => DarwinNotificationDetails(categoryIdentifier: "atoms", threadIdentifier: threadId);
);
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!);
} }
}); });
@ -184,11 +190,10 @@ class AllRequestsProvider extends ChangeNotifier {
// list = [16]; // list = [16];
list.add(16); list.add(16);
} }
if ( context.userProvider.isHouseKeeper) { if (context.userProvider.isHouseKeeper) {
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) {

@ -88,32 +88,39 @@ class PartsProvider extends ChangeNotifier {
late Response response; late Response response;
try { try {
if (partNo != null) { if (partNo != null) {
response = await ApiManager.instance.post(URLs.getPartNumber, body: {"partNo": partNo, }); response = await ApiManager.instance.post(URLs.getPartNumber, body: {
"partNo": partNo,
});
} else { } else {
response = await ApiManager.instance.post(URLs.getPartNumber, body: {"partName": partName, }); response = await ApiManager.instance.post(URLs.getPartNumber, body: {
"partName": partName,
});
} }
List<SparePart> page = []; List<SparePart> page = [];
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 categoriesListJson = json.decode(response.body)["data"]; List categoriesListJson = json.decode(response.body)["data"];
page = categoriesListJson.map((part) => SparePart.fromJson(part,false)).toList(); page = categoriesListJson.map((part) => SparePart.fromJson(part, false)).toList();
} }
return page; return page;
} catch (error) { } catch (error) {
return []; return [];
} }
} }
// Implement this only for spare part request for now show and search on display name .... // Implement this only for spare part request for now show and search on display name ....
Future<List<SparePart>> getPartsListByDisplayName({num? assetId, String? displayName}) async { Future<List<SparePart>> getPartsListByDisplayName({num? assetId, String? displayName}) async {
late Response response; late Response response;
try { try {
response = await ApiManager.instance.post(URLs.getPartNumber, body: {"displayName": displayName, }); response = await ApiManager.instance.post(URLs.getPartNumber, body: {
"displayName": displayName,
});
List<SparePart> page = []; List<SparePart> page = [];
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 categoriesListJson = json.decode(response.body)["data"]; List categoriesListJson = json.decode(response.body)["data"];
page = categoriesListJson.map((part) => SparePart.fromJson(part,true)).toList(); page = categoriesListJson.map((part) => SparePart.fromJson(part, true)).toList();
} }
return page; return page;
} catch (error) { } catch (error) {

@ -9,9 +9,12 @@ 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/extensions/context_extension.dart'; import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/models/generic_attachment_model.dart'; import 'package:test_sa/models/generic_attachment_model.dart';
import 'package:test_sa/models/helper_data_models/spare_part/activity_spare_part_model.dart';
import 'package:test_sa/models/helper_data_models/workorder/work_order_helper_models.dart';
import 'package:test_sa/models/plan_preventive_visit/plan_preventive_visit_model.dart'; import 'package:test_sa/models/plan_preventive_visit/plan_preventive_visit_model.dart';
import 'package:test_sa/models/ppm/ppm.dart'; import 'package:test_sa/models/ppm/ppm.dart';
import 'package:test_sa/models/ppm/ppm_search.dart'; import 'package:test_sa/models/ppm/ppm_search.dart';
import 'package:test_sa/models/service_request/spare_parts.dart';
import 'package:test_sa/models/user.dart'; import 'package:test_sa/models/user.dart';
import '../../../new_views/common_widgets/app_lazy_loading.dart'; import '../../../new_views/common_widgets/app_lazy_loading.dart';
@ -20,6 +23,15 @@ class PpmProvider extends ChangeNotifier {
// number of items call in each request // number of items call in each request
final pageItemNumber = 12; final pageItemNumber = 12;
bool _isReadOnlyRequest = false;
bool get isReadOnlyRequest => _isReadOnlyRequest;
set isReadOnlyRequest(bool value) {
_isReadOnlyRequest = value;
notifyListeners();
}
PlanPreventiveVisit? _planPreventiveVisit; PlanPreventiveVisit? _planPreventiveVisit;
late int _totalTabs = 4; late int _totalTabs = 4;
TabController? tabController; TabController? tabController;
@ -61,6 +73,15 @@ class PpmProvider extends ChangeNotifier {
stateCode = null; stateCode = null;
} }
List<StoreAvailability> _storeAvailability = [];
List<StoreAvailability> get storeAvailability => _storeAvailability;
set storeAvailability(List<StoreAvailability> value) {
_storeAvailability = value;
notifyListeners();
}
// state code of current request to defied error message // state code of current request to defied error message
// like 400 customer request failed // like 400 customer request failed
// 500 service not available // 500 service not available
@ -78,6 +99,13 @@ class PpmProvider extends ChangeNotifier {
bool isLoading = false; bool isLoading = false;
PpmSearch visitsSearch = PpmSearch(); PpmSearch visitsSearch = PpmSearch();
WorkOrderCostModel? workOrderCostModel;
SparePartHelperModel? sparePartHelperModel;
void updateSparePartHelperModel(SparePartHelperModel? value) {
sparePartHelperModel = value;
notifyListeners();
}
/// return -2 if request in progress /// return -2 if request in progress
/// return -1 if error happen when sending request /// return -1 if error happen when sending request
@ -164,11 +192,22 @@ class PpmProvider extends ChangeNotifier {
if (response.statusCode >= 200 && response.statusCode < 300) { if (response.statusCode >= 200 && response.statusCode < 300) {
ppmPlanAttachments = []; ppmPlanAttachments = [];
planPreventiveVisit = PlanPreventiveVisit.fromJson(json.decode(response.body)["data"]); planPreventiveVisit = PlanPreventiveVisit.fromJson(json.decode(response.body)["data"]);
// Set isReadOnly based on visit status (1 or 3 means closed/completed)
if (planPreventiveVisit?.visitStatus?.value! == 1 || planPreventiveVisit?.visitStatus?.value == 3) { if (planPreventiveVisit?.visitStatus?.value! == 1 || planPreventiveVisit?.visitStatus?.value == 3) {
isReadOnly = true; isReadOnly = true;
} else { } else {
isReadOnly = false; isReadOnly = false;
} }
// Set isReadOnlyRequest based on visit status - same logic as CMDetailProvider
//Need to confirm this with backend Status values: 3 = Closed, 5 = Completed, 6 = Cancelled (adjust based on your PM status values)
if (planPreventiveVisit?.visitStatus?.value == 3 || planPreventiveVisit?.visitStatus?.value == 5 || planPreventiveVisit?.visitStatus?.value == 6) {
isReadOnlyRequest = true;
} else {
isReadOnlyRequest = false;
}
isLoading = false; isLoading = false;
notifyListeners(); notifyListeners();
return planPreventiveVisit; return planPreventiveVisit;
@ -364,4 +403,119 @@ class PpmProvider extends ChangeNotifier {
return true; return true;
} }
Future<bool> createActivitySparePart() async {
isLoading = true;
Response response;
try {
response = await ApiManager.instance.post(URLs.createPmActivitySparePart, body: sparePartHelperModel!.toPMJson());
stateCode = response.statusCode;
isLoading = false;
notifyListeners();
if (stateCode == 200) {
return true;
}
return false;
} catch (error) {
isLoading = false;
stateCode = -1;
notifyListeners();
return false;
}
}
Future<String> updateActivitySparePart() async {
Response response;
try {
response = await ApiManager.instance.post(URLs.updatePmActivitySparePart, body: sparePartHelperModel!.toPMJson(), showToast: false);
if (response.statusCode == 200) {
return "";
} else {
final responseBody = jsonDecode(response.body);
if (responseBody is Map<String, dynamic>) {
final message = responseBody["message"]; // Reuse parsed body
if (message != null && message.toString().isNotEmpty) {
return message;
}
}
return "Failed, Try Again";
}
} catch (error) {
return "Failed, Try Again";
}
}
Future<int> deleteActivitySparePart({required int id}) async {
isLoading = true;
notifyListeners();
try {
final response = await ApiManager.instance.delete('${URLs.deletePmActivitySparePart}?activitySparePartPPMId=$id', body: {});
stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) {}
isLoading = false;
notifyListeners();
return response.statusCode;
} catch (error) {
isLoading = false;
notifyListeners();
return -1;
}
}
Future<bool> engineerUpdateCost() async {
isLoading = true;
Response response;
try {
response = await ApiManager.instance.post(URLs.updatePmActivityCost, body: workOrderCostModel!.toPMCostJson());
stateCode = response.statusCode;
isLoading = false;
notifyListeners();
if (stateCode == 200) {
planPreventiveVisit = PlanPreventiveVisit.fromJson(json.decode(response.body)["data"]);
// Update isReadOnlyRequest after fetching updated data
if (planPreventiveVisit?.visitStatus?.value == 3 || planPreventiveVisit?.visitStatus?.value == 5 || planPreventiveVisit?.visitStatus?.value == 6) {
isReadOnlyRequest = true;
} else {
isReadOnlyRequest = false;
}
notifyListeners();
return true;
}
return false;
} catch (error) {
isLoading = false;
stateCode = -1;
notifyListeners();
return false;
}
}
Future<void> getStoreAvailabilityById({required num? partId}) async {
try {
isLoading = true;
notifyListeners();
final response = await ApiManager.instance.post(
URLs.getSparePartAvailableQuantity + "?PartId=$partId",
body: {},
);
stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) {
final List<dynamic> data = json.decode(response.body)["data"];
storeAvailability = data.map((item) => StoreAvailability.fromJson(item)).toList();
}
isLoading = false;
notifyListeners();
} catch (e) {
log("getStoreAvalivility [error] : $e");
isLoading = false;
notifyListeners();
}
}
} }

@ -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,
@ -139,18 +142,19 @@ class _AppBarWidgetState extends State<AppBarWidget> with TickerProviderStateMix
children: [ children: [
Text( Text(
snapshot.user?.username ?? "", snapshot.user?.username ?? "",
maxLines: 2,
style: AppTextStyles.heading6.copyWith(color: AppColor.headingTextColor(context), fontSize: context.isTablet() ? 14 : null), style: AppTextStyles.heading6.copyWith(color: AppColor.headingTextColor(context), fontSize: context.isTablet() ? 14 : null),
), ),
Text( // Text(
snapshot.user?.type?.name.toCamelCase ?? "", // snapshot.user?.roleName ?? "",
style: AppTextStyles.bodyText.copyWith(color: AppColor.lightTextColor(context), fontSize: context.isTablet() ? 12 : null), // style: AppTextStyles.bodyText.copyWith(color: AppColor.lightTextColor(context), fontSize: context.isTablet() ? 12 : null),
), // ),
], ],
), ),
), ),
), ),
), ),
const Spacer(), // const Spacer(),
// Action icons with right-to-left fade animation // Action icons with right-to-left fade animation
SlideTransition( SlideTransition(
position: _rightSlideAnimation, position: _rightSlideAnimation,
@ -261,15 +265,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,
Navigator.of(context).pushNamed(NotificationsPage.id); 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);
});
}).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": "إعادة المحاولة"
} }

@ -544,17 +544,29 @@
"createAssetTransferRequest": "Create Asset Transfer Request", "createAssetTransferRequest": "Create Asset Transfer Request",
"arrivedDate": "Arrived Date", "arrivedDate": "Arrived Date",
"closedDate": "Closed Date", "closedDate": "Closed Date",
"transferType": "Transfer Type", "transferType": "Transfer Type",
"since": "Since", "since": "Since",
"rejectionDetails": "Rejection Details", "rejectionDetails": "Rejection Details",
"part": "Part", "part": "Part",
"creationDate": "Creation Date", "creationDate": "Creation Date",
"createdBy": "Created BY", "createdBy": "Created BY",
"trNumber": "TR Number", "trNumber": "TR Number",
"workOrderDetails": "Work Order Details", "workOrderDetails": "Work Order Details",
"woNumber": "WO Number", "woNumber": "WO Number",
"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),
@ -343,6 +357,7 @@ class MyApp extends StatelessWidget {
ChangeNotifierProvider(create: (_) => DepartmentProvider(), lazy: true), ChangeNotifierProvider(create: (_) => DepartmentProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => DepartmentBySitesProvider(), lazy: true), ChangeNotifierProvider(create: (_) => DepartmentBySitesProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => SiteProvider(), lazy: true), ChangeNotifierProvider(create: (_) => SiteProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => SameOrgSiteProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => MappedSiteProvider(), lazy: true), ChangeNotifierProvider(create: (_) => MappedSiteProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => MedicalDepartmentProvider(), lazy: true), ChangeNotifierProvider(create: (_) => MedicalDepartmentProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => FlowMedicalDepartmentProvider(), lazy: true), ChangeNotifierProvider(create: (_) => FlowMedicalDepartmentProvider(), lazy: true),
@ -376,6 +391,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 +432,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 +459,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(),
}, },
), ),

@ -4,6 +4,7 @@ import 'package:test_sa/models/service_request/spare_parts.dart';
class SparePartHelperModel { class SparePartHelperModel {
int? id; int? id;
int? workOrderId; int? workOrderId;
String? pmVisitId;
int? partCatalogItemId; int? partCatalogItemId;
int? activityStatusId; int? activityStatusId;
SparePart? sparePart; SparePart? sparePart;
@ -12,11 +13,13 @@ class SparePartHelperModel {
num? installQty; num? installQty;
num? returnQty; num? returnQty;
String? comment; String? comment;
List<SparePartAttachments>? sparePartAttachments;
List<SparePartAttachments> sparePartAttachments;
SparePartHelperModel({ SparePartHelperModel({
this.id, this.id,
this.workOrderId, this.workOrderId,
this.pmVisitId,
this.partCatalogItemId, this.partCatalogItemId,
this.quantity, this.quantity,
this.installQty, this.installQty,
@ -24,9 +27,9 @@ class SparePartHelperModel {
this.sparePart, this.sparePart,
this.activityStatusId, this.activityStatusId,
this.activityStatus, this.activityStatus,
this.comment,this.sparePartAttachments, this.comment,
}); List<SparePartAttachments>? sparePartAttachments,
}) : sparePartAttachments = sparePartAttachments ?? [];
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{}; final Map<String, dynamic> data = <String, dynamic>{};
@ -36,13 +39,44 @@ class SparePartHelperModel {
data['quantity'] = quantity; data['quantity'] = quantity;
data['installQty'] = installQty; data['installQty'] = installQty;
data['returnQty'] = returnQty; data['returnQty'] = returnQty;
if(activityStatus!=null){ if (activityStatus != null) {
data['activityStatusId'] = activityStatus?.id; data['activityStatusId'] = activityStatus?.id;
} }
data['comment'] = comment; data['comment'] = comment;
if (sparePartAttachments != null) { if (sparePartAttachments.isNotEmpty) {
data['acitiySparePartAttachments'] = data['acitiySparePartAttachments'] = sparePartAttachments!.map((v) => v.toJson()).toList();
sparePartAttachments!.map((v) => v.toJson()).toList(); }
return data;
}
//{
// "id": 0,
// "planPreventiveVisitId": 0,
// "partCatalogItemId": 0,
// "quantity": 0,
// "comment": "string",
// "acitiySparePartAttachments": [
// {
// "id": 0,
// "name": "string",
// "originalName": "string",
// "attachmentTypeId": 0,
// "attachmentName": "string",
// "attachmentURL": "string"
// }
// ]
// }
Map<String, dynamic> toPMJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id;
data['planPreventiveVisitId'] = pmVisitId;
data['partCatalogItemId'] = sparePart?.id;
data['quantity'] = quantity;
data['comment'] = comment;
data['installQty'] = installQty;
data['returnQty'] = returnQty;
if (sparePartAttachments.isNotEmpty) {
data['acitiySparePartAttachments'] = sparePartAttachments!.map((v) => v.toJson()).toList();
} }
return data; return data;
} }

@ -178,4 +178,7 @@ class WorkOrderCostModel {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return {'workOrderId': workOrderId, 'sparePartCost': sparePartCost, 'laborCost': labourCost, 'travelCost': travelCost, 'qAmount': qAmount, 'prNo': prNo, 'poNo': poNo,'mrNo':mrNo,'exchangeCost':exchangeCost}; return {'workOrderId': workOrderId, 'sparePartCost': sparePartCost, 'laborCost': labourCost, 'travelCost': travelCost, 'qAmount': qAmount, 'prNo': prNo, 'poNo': poNo,'mrNo':mrNo,'exchangeCost':exchangeCost};
} }
Map<String, dynamic> toPMCostJson() {
return {'planPreventiveVisitId': workOrderId, 'sparePartCost': sparePartCost, 'laborCost': labourCost, 'travelCost': travelCost, 'qAmount': qAmount, 'prNo': prNo, 'poNo': poNo,'mrNo':mrNo,'exchangeCost':exchangeCost};
}
} }

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

@ -1,9 +1,11 @@
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/models/device/asset.dart'; import 'package:test_sa/models/device/asset.dart';
import 'package:test_sa/models/helper_data_models/spare_part/activity_spare_part_model.dart';
import 'package:test_sa/models/lookup.dart'; import 'package:test_sa/models/lookup.dart';
import 'package:test_sa/models/service_request/supplier_details.dart'; import 'package:test_sa/models/service_request/supplier_details.dart';
import 'package:test_sa/models/timer_model.dart'; import 'package:test_sa/models/timer_model.dart';
@ -49,10 +51,20 @@ class PlanPreventiveVisit {
List<PreventiveVisitKits>? preventiveVisitKits; List<PreventiveVisitKits>? preventiveVisitKits;
List<PreventiveVisitTimers>? preventiveVisitTimers; List<PreventiveVisitTimers>? preventiveVisitTimers;
List<PreventiveVisitSuppliers>? preventiveVisitSuppliers; List<PreventiveVisitSuppliers>? preventiveVisitSuppliers;
List<PMSparePartActivityModel>? activities;
num? sparePartCost;
num? labourCost;
num? travelCost;
num? qAmount;
String? prNo;
String? poNo;
String? mrNo;
num? exchangeCost;
TimerModel? tbsTimer = TimerModel(); TimerModel? tbsTimer = TimerModel();
List<TimerModel>? timerModelList = []; List<TimerModel>? timerModelList = [];
TimerModel? ppMTimePicker; TimerModel? ppMTimePicker;
PlanPreventiveVisit( PlanPreventiveVisit(
{this.id, {this.id,
this.visitNo, this.visitNo,
@ -95,6 +107,15 @@ class PlanPreventiveVisit {
this.preventiveVisitTimers, this.preventiveVisitTimers,
this.timerModelList, this.timerModelList,
this.ppMTimePicker, this.ppMTimePicker,
this.activities,
this.labourCost,
this.sparePartCost,
this.travelCost,
this.qAmount,
this.prNo,
this.poNo,
this.mrNo,
this.exchangeCost,
this.preventiveVisitSuppliers}); this.preventiveVisitSuppliers});
PlanPreventiveVisit.fromJson(Map<String, dynamic> json) { PlanPreventiveVisit.fromJson(Map<String, dynamic> json) {
@ -131,6 +152,14 @@ class PlanPreventiveVisit {
safety = json['safety'] != null ? Lookup.fromJson(json['safety']) : null; safety = json['safety'] != null ? Lookup.fromJson(json['safety']) : null;
engSignature = json['engSignature']; engSignature = json['engSignature'];
nurseSignature = json['nurseSignature']; nurseSignature = json['nurseSignature'];
travelCost= json['travelCost'];
sparePartCost= json['sparePartCost'];
labourCost =json['laborCost'];
qAmount= json['qAmount'];
prNo= json['prNo'];
poNo= json['poNo'];
mrNo= json['mrNo'];
exchangeCost= json['exchangeCost'];
if (json['preventiveVisitAttachments'] != null) { if (json['preventiveVisitAttachments'] != null) {
preventiveVisitAttachments = <PreventiveVisitAttachments>[]; preventiveVisitAttachments = <PreventiveVisitAttachments>[];
json['preventiveVisitAttachments'].forEach((v) { json['preventiveVisitAttachments'].forEach((v) {
@ -167,6 +196,12 @@ class PlanPreventiveVisit {
preventiveVisitSuppliers!.add(PreventiveVisitSuppliers.fromJson(v)); preventiveVisitSuppliers!.add(PreventiveVisitSuppliers.fromJson(v));
}); });
} }
if (json['activitySparePartsPPM'] != null) {
activities = <PMSparePartActivityModel>[];
json['activitySparePartsPPM'].forEach((v) {
activities!.add(PMSparePartActivityModel.fromJson(v));
});
}
} }
Map<String, dynamic> toJson({required int status}) { Map<String, dynamic> toJson({required int status}) {
@ -183,50 +218,6 @@ class PlanPreventiveVisit {
data['safetyId'] = safety?.id; data['safetyId'] = safety?.id;
data['engSignature'] = engSignature; data['engSignature'] = engSignature;
data['nurseSignature'] = nurseSignature; data['nurseSignature'] = nurseSignature;
//
// if (asset != null) {
// data['asset'] = asset!.toJson();
// }
// data['visitNo'] = visitNo;
// data['planNo'] = planNo;
// data['planName'] = planName;
// data['nextPMDate'] = nextPMDate;
// data['assetName'] = assetName;
// data['model'] = model;
// data['manufacturer'] = manufacturer;
// data['supplierName'] = supplierName;
// data['siteName'] = siteName;
// data['buildingName'] = buildingName;
// data['floorName'] = floorName;
// data['departmentName'] = departmentName;
// data['roomName'] = roomName;
// data['fromDate'] = fromDate;
// data['toDate'] = toDate;
// if (assignedEmployee != null) {
// data['assignedEmployee'] = assignedEmployee!.toJson();
// }
// data['acutalDateOfVisit'] = acutalDateOfVisit;
// if (typeOfService != null) {
// data['typeOfService'] = typeOfService!.toJson();
// }
// if (visitStatus != null) {
// data['visitStatus'] = visitStatus!.toJson();
// }
// data['travelingHours'] = travelingHours;
// data['comments'] = comments;
// data['executionTimeFrame'] = executionTimeFrame;
// if (taskStatus != null) {
// data['taskStatus'] = taskStatus!.toJson();
// }
// if (deviceStatus != null) {
// data['deviceStatus'] = deviceStatus!.toJson();
// }
// data['assetAvailability'] = assetAvailability;
// if (safety != null) {
// data['safety'] = safety!.toJson();
// }
// data['engSignature'] = engSignature;
// data['nurseSignature'] = nurseSignature;
if (preventiveVisitAttachments != null) { if (preventiveVisitAttachments != null) {
data['preventiveVisitAttachments'] = preventiveVisitAttachments!.map((v) => v.toJson()).toList(); data['preventiveVisitAttachments'] = preventiveVisitAttachments!.map((v) => v.toJson()).toList();
@ -246,14 +237,12 @@ class PlanPreventiveVisit {
if (preventiveVisitSuppliers != null) { if (preventiveVisitSuppliers != null) {
data['preventiveVisitSuppliers'] = preventiveVisitSuppliers!.map((v) => v.toJson()).toList(); data['preventiveVisitSuppliers'] = preventiveVisitSuppliers!.map((v) => v.toJson()).toList();
} }
if (activities != null) {
data['activitySparePartsPPM'] = activities!.map((v) => v.toJson()).toList();
}
return data; return data;
} }
bool _isLocalUrl(String url) {
if (url.isEmpty != false) return false;
return url.startsWith("/") || url.startsWith("file://") || url.substring(1).startsWith(':\\');
}
Future<bool> validate(BuildContext context) async { Future<bool> validate(BuildContext context) async {
if (visitStatus?.id == null) { if (visitStatus?.id == null) {
await Fluttertoast.showToast(msg: "${context.translation.youHaveToSelect} ${context.translation.status}"); await Fluttertoast.showToast(msg: "${context.translation.youHaveToSelect} ${context.translation.status}");
@ -290,276 +279,6 @@ class PlanPreventiveVisit {
} }
} }
// class PlanPreventiveVisit {
// String? id;
// String? visitNo;
// Asset? asset;
// String? planNo;
// String? planName;
// String? nextPMDate;
// String? assetName;
// String? model;
// String? manufacturer;
// String? supplierName;
// String? siteName;
// String? buildingName;
// String? floorName;
// String? departmentName;
// String? roomName;
// String? fromDate;
// String? toDate;
// AssignedEmployee? assignedEmployee;
// String? acutalDateOfVisit;
// TypeOfService? typeOfService;
// VisitStatus? visitStatus;
// String? travelingHours;
// String? comments;
// int? executionTimeFrame;
// Lookup? taskStatus;
// String? deviceStatus;
// Lookup? assetAvailability;
// Lookup? safety;
// String? engSignature;
// String? nurseSignature;
// List<PreventiveVisitChecklists>? preventiveVisitAttachments;
// List<PreventiveVisitChecklists>? preventiveVisitCalibrations;
// List<PreventiveVisitChecklists>? preventiveVisitChecklists;
// List<PreventiveVisitChecklists>? preventiveVisitKits;
// List<PreventiveVisitTimers>? preventiveVisitTimers;
// List<PreventiveVisitChecklists>? preventiveVisitSuppliers;
// TimerModel? tbsTimer = TimerModel();
//
// PlanPreventiveVisit(
// {this.id,
// this.visitNo,
// this.asset,
// this.planNo,
// this.planName,
// this.nextPMDate,
// this.assetName,
// this.model,
// this.manufacturer,
// this.supplierName,
// this.siteName,
// this.buildingName,
// this.floorName,
// this.departmentName,
// this.roomName,
// this.fromDate,
// this.toDate,
// this.assignedEmployee,
// this.acutalDateOfVisit,
// this.typeOfService,
// this.visitStatus,
// this.travelingHours,
// this.comments,
// this.executionTimeFrame,
// this.taskStatus,
// this.deviceStatus,
// this.assetAvailability,
// this.safety,
// this.engSignature,
// this.nurseSignature,
// this.preventiveVisitAttachments,
// this.preventiveVisitCalibrations,
// this.preventiveVisitChecklists,
// this.preventiveVisitKits,
// this.preventiveVisitTimers,
// this.preventiveVisitSuppliers});
//
// PlanPreventiveVisit.fromJson(Map<String, dynamic> json) {
// id = json['id'];
// visitNo = json['visitNo'];
// asset = json['asset'] != null ? Asset.fromJson(json['asset']) : null;
// planNo = json['planNo'];
// planName = json['planName'];
// nextPMDate = json['nextPMDate'];
// assetName = json['assetName'];
// model = json['model'];
// manufacturer = json['manufacturer'];
// supplierName = json['supplierName'];
// siteName = json['siteName'];
// buildingName = json['buildingName'];
// floorName = json['floorName'];
// departmentName = json['departmentName'];
// roomName = json['roomName'];
// fromDate = json['fromDate'];
// toDate = json['toDate'];
// assignedEmployee = json['assignedEmployee'] != null ? AssignedEmployee.fromJson(json['assignedEmployee']) : null;
// acutalDateOfVisit = json['acutalDateOfVisit'];
// typeOfService = json['typeOfService'] != null ? TypeOfService.fromJson(json['typeOfService']) : null;
// visitStatus = json['visitStatus'] != null ? VisitStatus.fromJson(json['visitStatus']) : null;
// travelingHours = json['travelingHours'];
// comments = json['comments'];
// executionTimeFrame = json['executionTimeFrame'];
// taskStatus = json['taskStatus'];
// deviceStatus = json['deviceStatus'];
// assetAvailability = json['assetAvailability'];
// safety = json['safety'];
// engSignature = json['engSignature'];
// nurseSignature = json['nurseSignature'];
// if (json['preventiveVisitAttachments'] != null) {
// preventiveVisitAttachments = <PreventiveVisitChecklists>[];
// json['preventiveVisitAttachments'].forEach((v) {
// preventiveVisitAttachments!.add(PreventiveVisitChecklists.fromJson(v));
// });
// }
// if (json['preventiveVisitCalibrations'] != null) {
// preventiveVisitCalibrations = <PreventiveVisitChecklists>[];
// json['preventiveVisitCalibrations'].forEach((v) {
// preventiveVisitCalibrations!.add( PreventiveVisitChecklists.fromJson(v));
// });
// }
// if (json['preventiveVisitChecklists'] != null) {
// preventiveVisitChecklists = <PreventiveVisitChecklists>[];
// json['preventiveVisitChecklists'].forEach((v) {
// preventiveVisitChecklists!.add( PreventiveVisitChecklists.fromJson(v));
// });
// }
// if (json['preventiveVisitKits'] != null) {
// preventiveVisitKits = <PreventiveVisitChecklists>[];
// json['preventiveVisitKits'].forEach((v) {
// preventiveVisitKits!.add( PreventiveVisitChecklists.fromJson(v));
// });
// }
// if (json['preventiveVisitTimers'] != null) {
// preventiveVisitTimers = <PreventiveVisitTimers>[];
// json['preventiveVisitTimers'].forEach((v) {
// preventiveVisitTimers!.add( PreventiveVisitTimers.fromJson(v));
// });
// }
// if (json['preventiveVisitSuppliers'] != null) {
// preventiveVisitSuppliers = <PreventiveVisitChecklists>[];
// json['preventiveVisitSuppliers'].forEach((v) {
// preventiveVisitSuppliers!.add( PreventiveVisitChecklists.fromJson(v));
// });
// }
// }
//
// Map<String, dynamic> toJson() {
// final Map<String, dynamic> data = Map<String, dynamic>();
// data['id'] = this.id;
// data['visitNo'] = this.visitNo;
// if (this.asset != null) {
// data['asset'] = this.asset!.toJson();
// }
// data['planNo'] = this.planNo;
// data['planName'] = this.planName;
// data['nextPMDate'] = this.nextPMDate;
// data['assetName'] = this.assetName;
// data['model'] = this.model;
// data['manufacturer'] = this.manufacturer;
// data['supplierName'] = this.supplierName;
// data['siteName'] = this.siteName;
// data['buildingName'] = this.buildingName;
// data['floorName'] = this.floorName;
// data['departmentName'] = this.departmentName;
// data['roomName'] = this.roomName;
// data['fromDate'] = this.fromDate;
// data['toDate'] = this.toDate;
// if (this.assignedEmployee != null) {
// data['assignedEmployee'] = this.assignedEmployee!.toJson();
// }
// data['acutalDateOfVisit'] = this.acutalDateOfVisit;
// if (this.typeOfService != null) {
// data['typeOfService'] = this.typeOfService!.toJson();
// }
// if (this.visitStatus != null) {
// data['visitStatus'] = this.visitStatus!.toJson();
// }
// data['travelingHours'] = this.travelingHours;
// data['comments'] = this.comments;
// data['executionTimeFrame'] = this.executionTimeFrame;
// data['taskStatus'] = this.taskStatus;
// data['deviceStatus'] = this.deviceStatus;
// data['assetAvailability'] = this.assetAvailability;
// data['safety'] = this.safety;
// data['engSignature'] = this.engSignature;
// data['nurseSignature'] = this.nurseSignature;
// if (this.preventiveVisitAttachments != null) {
// data['preventiveVisitAttachments'] = this.preventiveVisitAttachments!.map((v) => v.toJson()).toList();
// }
// if (this.preventiveVisitCalibrations != null) {
// data['preventiveVisitCalibrations'] = this.preventiveVisitCalibrations!.map((v) => v.toJson()).toList();
// }
// if (this.preventiveVisitChecklists != null) {
// data['preventiveVisitChecklists'] = this.preventiveVisitChecklists!.map((v) => v.toJson()).toList();
// }
// if (this.preventiveVisitKits != null) {
// data['preventiveVisitKits'] = this.preventiveVisitKits!.map((v) => v.toJson()).toList();
// }
// if (this.preventiveVisitTimers != null) {
// data['preventiveVisitTimers'] = this.preventiveVisitTimers!.map((v) => v.toJson()).toList();
// }
// if (this.preventiveVisitSuppliers != null) {
// data['preventiveVisitSuppliers'] = this.preventiveVisitSuppliers!.map((v) => v.toJson()).toList();
// }
// return data;
// }
//
// bool _isLocalUrl(String url) {
// if (url.isEmpty != false) return false;
// return url.startsWith("/") || url.startsWith("file://") || url.substring(1).startsWith(':\\');
// }
//
// Future<bool> validate(BuildContext context) async {
// if (visitStatus?.id == null) {
// await Fluttertoast.showToast(msg: "${context.translation.youHaveToSelect} ${context.translation.status}");
// return false;
// }
// if (acutalDateOfVisit == null) {
// await Fluttertoast.showToast(msg: "${context.translation.youHaveToSelect} ${context.translation.actualDate}");
// return false;
// }
// // if (expectedDate == null) {
// // await Fluttertoast.showToast(msg: "${context.translation.youHaveToSelect} ${context.translation.visitDate}");
// // return false;
// // }
// if (tbsTimer?.startAt == null) {
// await Fluttertoast.showToast(msg: "Working Hours Required");
// return false;
// }
// if (tbsTimer?.endAt == null) {
// await Fluttertoast.showToast(msg: "Please Stop The Timer");
// return false;
// }
//
// // if (externalEngineerTimer?.startAt != null && externalEngineerTimer?.endAt == null) {
// // await Fluttertoast.showToast(msg: "Please Stop External Engineer Timer");
// // return false;
// // }
//
// return true;
// }
//
// void removeEmptyObjects() {
// // if (vCalibrationTools?.isNotEmpty ?? false) vCalibrationTools!.removeWhere((element) => element.assetId == null && element.calibrationDateOfTesters == null);
// // if (vKits?.isNotEmpty ?? false) vKits!.removeWhere((element) => element.partName == null && element.partNumber == null);
// }
// }
// class Asset {
// num? id;
// String? assetNumber;
// String? assetSerialNo;
//
// Asset({this.id, this.assetNumber, this.assetSerialNo});
//
// Asset.fromJson(Map<String, dynamic> json) {
// id = json['id'];
// assetNumber = json['assetNumber'];
// assetSerialNo = json['assetSerialNo'];
// }
//
// Map<String, dynamic> toJson() {
// final Map<String, dynamic> data = Map<String, dynamic>();
// data['id'] = this.id;
// data['assetNumber'] = this.assetNumber;
// data['assetSerialNo'] = this.assetSerialNo;
// return data;
// }
// }
class AssignedEmployee { class AssignedEmployee {
String? userId; String? userId;
String? userName; String? userName;
@ -588,50 +307,6 @@ class AssignedEmployee {
} }
} }
// class Lookup {
// int? id;
// String? name;
// int? value;
//
// Lookup({this.id, this.name, this.value});
//
// Lookup.fromJson(Map<String, dynamic> json) {
// id = json['id'];
// name = json['name'];
// value = json['value'];
// }
//
// Map<String, dynamic> toJson() {
// final Map<String, dynamic> data = Map<String, dynamic>();
// data['id'] = this.id;
// data['name'] = this.name;
// data['value'] = this.value;
// return data;
// }
// }
// class VisitStatus {
// int? id;
// String? name;
// int? value;
//
// VisitStatus({this.id, this.name, this.value});
//
// VisitStatus.fromJson(Map<String, dynamic> json) {
// id = json['id'];
// name = json['name'];
// value = json['value'];
// }
//
// Map<String, dynamic> toJson() {
// final Map<String, dynamic> data = Map<String, dynamic>();
// data['id'] = this.id;
// data['name'] = this.name;
// data['value'] = this.value;
// return data;
// }
// }
class PreventiveVisitChecklists { class PreventiveVisitChecklists {
int? id; int? id;
Lookup? taskStatus; Lookup? taskStatus;
@ -788,171 +463,6 @@ class PreventiveVisitSuppliers {
} }
} }
// class Supplier {
// int? id;
// String? suppliername;
// String? name;
// String? website;
// String? email;
// String? code;
// int? suppNo;
// String? suppStatusId;
// String? cityId;
// String? person;
// String? comment;
// String? zipcode;
// String? contact;
// List<String>? telephones;
// List<String>? faxes;
// List<String>? addresses;
// List<String>? attachments;
// List<SuppPersons>? suppPersons;
// List<String>? suppTCodes;
//
// Supplier(
// {this.id,
// this.suppliername,
// this.name,
// this.website,
// this.email,
// this.code,
// this.suppNo,
// this.suppStatusId,
// this.cityId,
// this.person,
// this.comment,
// this.zipcode,
// this.contact,
// this.telephones,
// this.faxes,
// this.addresses,
// this.attachments,
// this.suppPersons,
// this.suppTCodes});
//
// Supplier.fromJson(Map<String, dynamic> json) {
// id = json['id'];
// suppliername = json['suppliername'];
// name = json['name'];
// website = json['website'];
// email = json['email'];
// code = json['code'];
// suppNo = json['suppNo'];
// suppStatusId = json['suppStatusId'];
// cityId = json['cityId'];
// person = json['person'];
// comment = json['comment'];
// zipcode = json['zipcode'];
// contact = json['contact'];
// if (json['telephones'] != null) {
// telephones = <String>[];
// json['telephones'].forEach((v) {
// telephones!.add(v);
// });
// }
// if (json['faxes'] != null) {
// faxes = <String>[];
// json['faxes'].forEach((v) {
// faxes!.add(v);
// });
// }
// if (json['addresses'] != null) {
// addresses = <String>[];
// json['addresses'].forEach((v) {
// addresses!.add(v);
// });
// }
// if (json['attachments'] != null) {
// attachments = <String>[];
// json['attachments'].forEach((v) {
// attachments!.add(v);
// });
// }
// if (json['suppPersons'] != null) {
// suppPersons = <SuppPersons>[];
// json['suppPersons'].forEach((v) {
// suppPersons!.add(SuppPersons.fromJson(v));
// });
// }
// if (json['suppTCodes'] != null) {
// suppTCodes = <String>[];
// json['suppTCodes'].forEach((v) {
// suppTCodes!.add(v);
// });
// }
// }
//
// Map<String, dynamic> toJson() {
// final Map<String, dynamic> data = <String, dynamic>{};
// data['id'] = id;
// data['suppliername'] = suppliername;
// data['name'] = name;
// data['website'] = website;
// data['email'] = email;
// data['code'] = code;
// data['suppNo'] = suppNo;
// data['suppStatusId'] = suppStatusId;
// data['cityId'] = cityId;
// data['person'] = person;
// data['comment'] = comment;
// data['zipcode'] = zipcode;
// data['contact'] = contact;
// if (telephones != null) {
// data['telephones'] = telephones!.map((v) => v).toList();
// }
// if (faxes != null) {
// data['faxes'] = faxes!.map((v) => v).toList();
// }
// if (addresses != null) {
// data['addresses'] = addresses!.map((v) => v).toList();
// }
// if (attachments != null) {
// data['attachments'] = attachments!.map((v) => v).toList();
// }
// if (suppPersons != null) {
// data['suppPersons'] = suppPersons!.map((v) => v.toJson()).toList();
// }
// if (suppTCodes != null) {
// data['suppTCodes'] = suppTCodes!.map((v) => v).toList();
// }
// return data;
// }
// }
// class SuppPersons {
// int? id;
// int? supplierId;
// String? personName;
// int? personRoleId;
// String? contact;
// String? externalEngCode;
// String? email;
//
// SuppPersons({this.id, this.supplierId, this.personName, this.personRoleId, this.contact, this.externalEngCode, this.email});
//
// SuppPersons.fromJson(Map<String, dynamic> json) {
// id = json['id'];
// supplierId = json['supplierId'];
// personName = json['personName'];
// personRoleId = json['personRoleId'];
// contact = json['contact'];
// externalEngCode = json['externalEngCode'];
// email = json['email'];
// }
//
// Map<String, dynamic> toJson() {
// final Map<String, dynamic> data = Map<String, dynamic>();
// data['id'] = this.id;
// data['supplierId'] = this.supplierId;
// data['personName'] = this.personName;
// data['personRoleId'] = this.personRoleId;
// data['contact'] = this.contact;
// data['externalEngCode'] = this.externalEngCode;
// data['email'] = this.email;
// return data;
// }
// }
class PreventiveVisitKits { class PreventiveVisitKits {
int? id; int? id;
PartCatalogItem? partCatalogItem; PartCatalogItem? partCatalogItem;
@ -999,3 +509,80 @@ class PartCatalogItem {
return data; return data;
} }
} }
class PMSparePartActivityModel {
int? id;
PartCatalogItem? partCatalogItem;
Lookup? activityStatus;
double? quantity;
double? installQty;
double? returnQty;
String? comment;
dynamic price;
List<SparePartAttachments> activitySparePartPPMAttachments;
PMSparePartActivityModel({
this.id,
this.partCatalogItem,
this.activityStatus,
this.quantity,
this.installQty,
this.returnQty,
this.comment,
this.price,
List<SparePartAttachments>? activitySparePartPPMAttachments,
}) : activitySparePartPPMAttachments = activitySparePartPPMAttachments ?? [];
PMSparePartActivityModel.fromJson(Map<String, dynamic> json) : activitySparePartPPMAttachments = [] {
id = json['id'];
partCatalogItem = json['partCatalogItem'] != null ? PartCatalogItem.fromJson(json['partCatalogItem']) : null;
activityStatus = json['activityStatus'] != null ? Lookup.fromJson(json['activityStatus']) : null;
quantity = json['quantity']?.toDouble();
installQty = json['installQty']?.toDouble();
returnQty = json['returnQty']?.toDouble();
comment = json['comment'];
price = json['price'];
// Handle typo in API response: "acitiySparePartPPMAttachments" instead of "activitySparePartPPMAttachments"
final attachmentsKey = json.containsKey('acitiySparePartPPMAttachments')
? 'acitiySparePartPPMAttachments'
: 'activitySparePartPPMAttachments';
activitySparePartPPMAttachments = json[attachmentsKey] == null
? []
: List.from(json[attachmentsKey]).map((e) => SparePartAttachments.fromJson(e)).toList();
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = {};
data['id'] = id;
if (partCatalogItem != null) {
data['partCatalogItem'] = partCatalogItem!.toJson();
}
if (activityStatus != null) {
data['activityStatus'] = activityStatus!.toJson();
}
data['quantity'] = quantity;
data['installQty'] = installQty;
data['returnQty'] = returnQty;
data['comment'] = comment;
data['price'] = price;
data['activitySparePartPPMAttachments'] = activitySparePartPPMAttachments
.map(
(v) => v.toJson(),
)
.toList();
return data;
}
}

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

@ -100,6 +100,8 @@ class User {
return {"username": userName, "password": password, "fireBaseToken": FirebaseNotificationManger?.token ?? "", "notificationType": notificationType}; return {"username": userName, "password": password, "fireBaseToken": FirebaseNotificationManger?.token ?? "", "notificationType": notificationType};
} }
String get roleName => userRoles?.first.name ?? "";
UsersTypes? get type { UsersTypes? get type {
switch (userRoles?.first.value) { switch (userRoles?.first.value) {
case "R-6": case "R-6":

@ -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,14 +35,14 @@ 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,
))); )));
// } // }
} }
} }
static void cancelRequest({required BuildContext context, int ?itemId, int? requestId}) async { static void cancelRequest({required BuildContext context, int? itemId, int? requestId}) async {
AssetDeliveryProvider assetDeliveryProvider = Provider.of<AssetDeliveryProvider>(context, listen: false); AssetDeliveryProvider assetDeliveryProvider = Provider.of<AssetDeliveryProvider>(context, listen: false);
showDialog( showDialog(
context: context, context: context,
@ -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,50 +73,55 @@ 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: [
Flexible(
fit: FlexFit.loose,
child: Column(
children: [ children: [
Flexible( // widget.attachmentType == 5 && !widget.viewOnly
fit: FlexFit.loose, // ✅ expands only as much as needed widget.attachmentType == 5 //only add the attachment for current
child: Column( ? AttachmentPicker(
children: [ label: context.translation.attachments,
widget.attachmentType == 5 && !widget.viewOnly attachment: _attachments,
? AttachmentPicker( buttonColor: AppColor.primary10,
label: context.translation.attachments, onlyImages: false,
attachment: _attachments, showAsListView: true,
buttonColor: AppColor.primary10, buttonIcon: 'quotation_icon'.toSvgAsset(color: AppColor.primary10),
onlyImages: false, onChange: (attachment) {},
showAsListView: true, ).toShadowContainer(
buttonIcon: 'quotation_icon'.toSvgAsset(color: AppColor.primary10), context,
onChange: (attachment) {}, borderRadius: 20,
).toShadowContainer( padding: 12,
context, margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
borderRadius: 20, )
padding: 12, : _attachments.isNotEmpty
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), ? FilesList(images: _attachments.map((e) => URLs.getFileUrl(e.name ?? '') ?? '').toList() ?? []).toShadowContainer(
) context,
: FilesList(images: _attachments.map((e) => URLs.getFileUrl(e.name ?? '') ?? '').toList() ?? []).toShadowContainer( borderRadius: 20,
context, padding: 12,
borderRadius: 20, margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: 12, )
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), : const NoDataFound().center.expanded,
),
],
),
),
if (widget.attachmentType == 5 && !widget.viewOnly)
FooterActionButton.footerContainer(
context: context,
child: AppFilledButton(
buttonColor: AppColor.primary10,
label: 'Save'.addTranslation,
maxWidth: true,
onPressed: _onSavePressed,
),
),
], ],
), ),
),
if (widget.attachmentType == 5)
FooterActionButton.footerContainer(
context: context,
child: AppFilledButton(
buttonColor: AppColor.primary10,
label: 'Save'.addTranslation,
maxWidth: true,
onPressed: _onSavePressed,
),
),
],
),
); );
} }

@ -81,7 +81,9 @@ class _CostCenterFormViewState extends State<CostCenterFormView> {
await costCenterProvider.getData(id: widget.requestId); await costCenterProvider.getData(id: widget.requestId);
} }
} }
setState(() {}); if(mounted) {
setState(() {});
}
} }
void addItem<T>(List<T> list, T Function() creator) { void addItem<T>(List<T> list, T Function() creator) {
@ -116,40 +118,40 @@ class _CostCenterFormViewState extends State<CostCenterFormView> {
return provider.isLoading return provider.isLoading
? CircularProgressIndicator(color: AppColor.loadingColor(context)).center ? CircularProgressIndicator(color: AppColor.loadingColor(context)).center
: Column( : Column(
children: [ children: [
ListView( ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
children: [ children: [
itemDetails(), itemDetails(),
12.height, 12.height,
selectionButton(), selectionButton(),
12.height, 12.height,
_selectedType == CostCenterType.costCenter ? _buildCostCenterList() : _buildSerialNumberList(), _selectedType == CostCenterType.costCenter ? _buildCostCenterList() : _buildSerialNumberList(),
], ],
).expanded, ).expanded,
FooterActionButton.footerContainer( FooterActionButton.footerContainer(
context: context, context: context,
child: AppFilledButton( child: AppFilledButton(
buttonColor: AppColor.primary10, buttonColor: AppColor.primary10,
label: 'Save'.addTranslation, label: 'Save'.addTranslation,
maxWidth: true, maxWidth: true,
onPressed: () async { onPressed: () async {
showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading()); showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading());
await assetDeliveryProvider await assetDeliveryProvider
.saveCostCenterToLineDeliveryInspection(tableItemId: widget.tableItemId, lineId: widget.lineId, list: formModel.costCenters, serialNoList: formModel.serialNos) .saveCostCenterToLineDeliveryInspection(tableItemId: widget.tableItemId, lineId: widget.lineId, list: formModel.costCenters, serialNoList: formModel.serialNos)
.then((status) async { .then((status) async {
Navigator.pop(context); Navigator.pop(context);
if (status) { if (status) {
Navigator.pop(context); Navigator.pop(context);
assetDeliveryProvider.getDeliveryLinesListById( assetDeliveryProvider.getDeliveryLinesListById(
itemId: widget.tableItemId, itemId: widget.tableItemId,
); );
} }
}); });
}), }),
), ),
], ],
); );
}), }),
); );
} }
@ -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,

@ -747,23 +747,16 @@ class CMDetailProvider extends ChangeNotifier {
} }
} }
Future<bool> updateActivitySparePart() async { Future<String> updateActivitySparePart() async {
isLoading = true;
Response response; Response response;
try { try {
response = await ApiManager.instance.post(URLs.updateActivitySparePartUrl, body: sparePartHelperModel!.toJson()); response = await ApiManager.instance.post(URLs.updateActivitySparePartUrl, body: sparePartHelperModel!.toJson());
stateCode = response.statusCode;
isLoading = false;
notifyListeners();
if (stateCode == 200) { if (stateCode == 200) {
return true; return "";
} }
return false; return "Failed, Try Again";
} catch (error) { } catch (error) {
isLoading = false; return "Failed, Try Again";
stateCode = -1;
notifyListeners();
return false;
} }
} }

@ -251,7 +251,6 @@ class _ActivitiesListViewState extends State<ActivitiesListView> {
label: context.translation.assistantEmployee, label: context.translation.assistantEmployee,
value: activity.activityMaintenance?.assistantEmployees?.first.user?.userName, value: activity.activityMaintenance?.assistantEmployees?.first.user?.userName,
), ),
] else ...[ ] else ...[
InfoTextWidget( InfoTextWidget(
label: context.translation.assignedEmployee, label: context.translation.assignedEmployee,
@ -344,27 +343,31 @@ class _ActivitiesListViewState extends State<ActivitiesListView> {
} }
void editSparePartRequest({required BuildContext context, required CMDetailProvider requestDetailProvider, required Activities activity}) async { void editSparePartRequest({required BuildContext context, required CMDetailProvider requestDetailProvider, required Activities activity}) async {
requestDetailProvider.sparePartHelperModel = SparePartHelperModel( final model = SparePartHelperModel(
id: activity.id, id: activity.id,
workOrderId: requestDetailProvider.currentWorkOrder?.data?.requestId, workOrderId: requestDetailProvider.currentWorkOrder?.data?.requestId,
comment: activity.activitySparePart?.comment, comment: activity.activitySparePart?.comment,
sparePartAttachments: activity.activitySparePart?.acitiySparePartAttachments ?? [], sparePartAttachments: activity.activitySparePart?.acitiySparePartAttachments ?? [],
activityStatus: activity.activityStatus, activityStatus: activity.activityStatus,
sparePart: SparePart( sparePart: SparePart(
id: activity.activitySparePart?.partCatalogItem?.id, id: activity.activitySparePart?.partCatalogItem?.id,
partName: activity.activitySparePart?.partCatalogItem?.partName, partName: activity.activitySparePart?.partCatalogItem?.partName,
partNo: activity.activitySparePart?.partCatalogItem?.partNumber, partNo: activity.activitySparePart?.partCatalogItem?.partNumber,
oracleCode: activity.activitySparePart?.partCatalogItem?.oracleCode), oracleCode: activity.activitySparePart?.partCatalogItem?.oracleCode,
),
quantity: activity.activitySparePart?.quantity, quantity: activity.activitySparePart?.quantity,
installQty: activity.activitySparePart?.installQty, installQty: activity.activitySparePart?.installQty,
returnQty: activity.activitySparePart?.returnQty, returnQty: activity.activitySparePart?.returnQty,
activityStatusId: activity.activityStatus?.id, activityStatusId: activity.activityStatus?.id,
); );
requestDetailProvider.updateSparePartHelperModel(requestDetailProvider.sparePartHelperModel); // Navigate and let SparePartRequest._initializeModel() handle provider setup
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).push(MaterialPageRoute(
builder: (_) => const SparePartRequest(), builder: (_) => SparePartRequest(
model: model,
provider: requestDetailProvider,
fromCm: true,
),
)); ));
} }
} }

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:test_sa/app_strings/app_asset.dart'; import 'package:test_sa/app_strings/app_asset.dart';
import 'package:test_sa/controllers/providers/api/ppm_provider.dart';
import 'package:test_sa/controllers/providers/api/service_requests_provider.dart'; import 'package:test_sa/controllers/providers/api/service_requests_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';
@ -668,16 +669,26 @@ class ServiceRequestBottomSheet {
} }
void onItemTap({required int index, required BuildContext context}) { void onItemTap({required int index, required BuildContext context}) {
CMDetailProvider requestDetailProvider = Provider.of<CMDetailProvider>(context, listen: false);
switch (index) { switch (index) {
case 0: case 0:
Provider.of<CMDetailProvider>(context, listen: false).sparePartHelperModel = SparePartHelperModel(); // Create fresh model - SparePartRequest will handle setting it in provider
final model = SparePartHelperModel(
id: 0,
workOrderId: requestDetailProvider.currentWorkOrder?.data?.requestId,
);
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute(builder: (context) => const SparePartRequest()), MaterialPageRoute(
builder: (context) => SparePartRequest(
model: model,
provider: requestDetailProvider,
fromCm: true,
),
),
); );
break; break;
case 1: case 1:
CMDetailProvider requestDetailProvider = Provider.of<CMDetailProvider>(context, listen: false);
requestDetailProvider.activityMaintenanceHelperModel = ActivityMaintenanceHelperModel( requestDetailProvider.activityMaintenanceHelperModel = ActivityMaintenanceHelperModel(
workOrderId: requestDetailProvider.currentWorkOrder?.data?.requestId, workOrderId: requestDetailProvider.currentWorkOrder?.data?.requestId,
id: 0, id: 0,
@ -768,7 +779,7 @@ class ServiceRequestBottomSheet {
})); }));
} }
static Future addAnotherSpareRequestBottomSheet({required BuildContext context}) { static Future addAnotherSpareRequestBottomSheet({required BuildContext context, bool fromCm = false}) {
return buildBottomSheetParent( return buildBottomSheetParent(
context: context, context: context,
childWidget: Consumer<ServiceRequestsProvider>( childWidget: Consumer<ServiceRequestsProvider>(
@ -804,11 +815,23 @@ class ServiceRequestBottomSheet {
buttonColor: AppColor.primary10, buttonColor: AppColor.primary10,
loading: false, loading: false,
onPressed: () { onPressed: () {
dynamic provider = fromCm ? Provider.of<CMDetailProvider>(context, listen: false) : Provider.of<PpmProvider>(context, listen: false);
Navigator.pop(context); Navigator.pop(context);
Provider.of<CMDetailProvider>(context, listen: false).sparePartHelperModel = SparePartHelperModel(); final model = SparePartHelperModel(
id: 0,
workOrderId: fromCm? provider.currentWorkOrder?.data?.requestId:null,
pmVisitId: !fromCm? provider.planPreventiveVisit?.id:null,
);
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute(builder: (context) => const SparePartRequest()), MaterialPageRoute(
builder: (context) => SparePartRequest(
model: model,
provider: provider,
fromCm: fromCm,
),
),
); );
}, },
).expanded, ).expanded,

@ -338,8 +338,26 @@ class _ServiceRequestDetailViewState extends State<ServiceRequestDetailView> {
const Spacer(), const Spacer(),
if (!provider.isReadOnlyRequest) if (!provider.isReadOnlyRequest)
"edit_icon".toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)).onPress(() async { "edit_icon".toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)).onPress(() async {
WorkOrderData currentWorkOrderData = provider.currentWorkOrder!.data!;
provider.workOrderCostModel = WorkOrderCostModel(
workOrderId: currentWorkOrderData.requestId,
labourCost: currentWorkOrderData.labourCost,
sparePartCost: currentWorkOrderData.sparePartCost,
travelCost: currentWorkOrderData.travelCost,
qAmount: currentWorkOrderData.qAmount,
prNo: currentWorkOrderData.prNo,
poNo: currentWorkOrderData.poNo,
mrNo: currentWorkOrderData.mrNo,
exchangeCost: currentWorkOrderData.exchangeCost,
);
provider.refreshTimer = false; provider.refreshTimer = false;
await Navigator.push(context, MaterialPageRoute(builder: (context) => CostDetailFormScreen(isEdit: true))); await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CostDetailFormScreen(
model: provider.workOrderCostModel,
provider: provider,
)));
provider.refreshTimer = true; provider.refreshTimer = true;
}), }),
], ],

@ -1,273 +1,216 @@
import 'dart:developer';
import 'package:flutter/material.dart'; 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/int_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/models/helper_data_models/workorder/work_order_helper_models.dart'; import 'package:test_sa/models/helper_data_models/workorder/work_order_helper_models.dart';
import 'package:test_sa/models/new_models/work_order_detail_model.dart';
import 'package:test_sa/modules/cm_module/cm_detail_provider.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.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/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';
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/app_text_form_field.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/providers/service_request_providers/equipment_status_provider.dart';
class CostDetailFormScreen extends StatefulWidget { class CostDetailFormScreen extends StatefulWidget {
static const String id = "/cost-detail-form-screen"; static const String id = "/cost-detail-form-screen";
bool isEdit = false; final WorkOrderCostModel? model;
final dynamic provider; // CMDetailProvider or PpmProvider
CostDetailFormScreen({Key? key, required this.isEdit}) : super(key: key); const CostDetailFormScreen({
Key? key,
required this.model,
required this.provider,
}) : super(key: key);
@override @override
_CostDetailFormScreenState createState() => _CostDetailFormScreenState(); _CostDetailFormScreenState createState() => _CostDetailFormScreenState();
} }
class _CostDetailFormScreenState extends State<CostDetailFormScreen> with TickerProviderStateMixin { class _CostDetailFormScreenState extends State<CostDetailFormScreen> {
CMDetailProvider? _requestDetailProvider; late WorkOrderCostModel _localModel;
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
} _localModel = WorkOrderCostModel(
workOrderId: widget.model?.workOrderId,
void assignValues() { labourCost: widget.model?.labourCost,
// ServiceRequestDetailProvider requestDetailProvider = Provider.of<ServiceRequestDetailProvider>(context, listen: false); sparePartCost: widget.model?.sparePartCost,
// _requestDetailProvider = Provider.of<ServiceRequestDetailProvider>(context, listen: false); travelCost: widget.model?.travelCost,
WorkOrderData currentWorkOrderData = _requestDetailProvider!.currentWorkOrder!.data!; qAmount: widget.model?.qAmount,
_requestDetailProvider!.workOrderCostModel = WorkOrderCostModel( prNo: widget.model?.prNo,
workOrderId: currentWorkOrderData.requestId, poNo: widget.model?.poNo,
labourCost: currentWorkOrderData.labourCost, mrNo: widget.model?.mrNo,
sparePartCost: currentWorkOrderData.sparePartCost, exchangeCost: widget.model?.exchangeCost,
travelCost: currentWorkOrderData.travelCost,
qAmount: currentWorkOrderData.qAmount,
prNo: currentWorkOrderData.prNo,
poNo: currentWorkOrderData.poNo,
mrNo: currentWorkOrderData.mrNo,
exchangeCost: currentWorkOrderData.exchangeCost,
); );
log('Initialized Cost Model: ${_localModel.toJson()}');
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_requestDetailProvider == null) {
_requestDetailProvider = Provider.of<CMDetailProvider>(context, listen: false);
assignValues();
}
return Scaffold( return Scaffold(
key: _scaffoldKey,
appBar: const DefaultAppBar(title: "Cost Details"), appBar: const DefaultAppBar(title: "Cost Details"),
body: Consumer<CMDetailProvider>(builder: (context, CMDetailProvider requestDetailProvider, child) { body: ListenableBuilder(
return Column( listenable: widget.provider,
crossAxisAlignment: CrossAxisAlignment.start, builder: (context, child) {
mainAxisAlignment: MainAxisAlignment.spaceBetween, return Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
SingleChildScrollView( mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, Expanded(
mainAxisSize: MainAxisSize.min, child: SingleChildScrollView(
children: [ child: Column(
AppTextFormField( crossAxisAlignment: CrossAxisAlignment.start,
labelText: "Spare Part Cost", mainAxisSize: MainAxisSize.min,
backgroundColor: AppColor.neutral100, children: [
initialValue: requestDetailProvider.workOrderCostModel?.sparePartCost?.toString(), AppTextFormField(
textAlign: TextAlign.center, labelText: "Spare Part Cost",
enable: false, backgroundColor: AppColor.neutral100,
labelStyle: AppTextStyles.textFieldLabelStyle, initialValue: _localModel.sparePartCost?.toString() ?? '0',
textInputType: TextInputType.number, textAlign: TextAlign.center,
showShadow: false, enable: false,
onChange: (value) { labelStyle: AppTextStyles.textFieldLabelStyle,
requestDetailProvider.workOrderCostModel?.sparePartCost = num.parse(value); textInputType: TextInputType.number,
}, showShadow: false,
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
8.height, 8.height,
AppTextFormField( AppTextFormField(
initialValue: requestDetailProvider.workOrderCostModel?.labourCost?.toString(), initialValue: _localModel.labourCost?.toString() ?? '',
textAlign: TextAlign.center, textAlign: TextAlign.center,
labelText: "Labor Cost", labelText: "Labor Cost",
backgroundColor: AppColor.fieldBgColor(context), backgroundColor: AppColor.fieldBgColor(context),
labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)),
textInputType: const TextInputType.numberWithOptions(decimal: true), textInputType: const TextInputType.numberWithOptions(decimal: true),
showShadow: false, showShadow: false,
onChange: (value) { onChange: (value) {
requestDetailProvider.workOrderCostModel?.labourCost = num.parse(value); _localModel.labourCost = num.tryParse(value);
}, },
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
8.height, 8.height,
AppTextFormField( AppTextFormField(
labelText: "Travel Cost", labelText: "Travel Cost",
backgroundColor: AppColor.fieldBgColor(context), backgroundColor: AppColor.fieldBgColor(context),
labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)),
initialValue: requestDetailProvider.workOrderCostModel?.travelCost?.toString(), initialValue: _localModel.travelCost?.toString() ?? '',
textAlign: TextAlign.center, textAlign: TextAlign.center,
textInputType: const TextInputType.numberWithOptions(decimal: true), textInputType: const TextInputType.numberWithOptions(decimal: true),
showShadow: false, showShadow: false,
onChange: (value) { onChange: (value) {
requestDetailProvider.workOrderCostModel?.travelCost = num.parse(value); _localModel.travelCost = num.tryParse(value);
}, },
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
8.height, 8.height,
AppTextFormField( AppTextFormField(
labelText: "Quot Amount", labelText: "Quot Amount",
backgroundColor: AppColor.fieldBgColor(context), backgroundColor: AppColor.fieldBgColor(context),
labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)),
initialValue: requestDetailProvider.workOrderCostModel?.qAmount?.toString(), initialValue: _localModel.qAmount?.toString() ?? '',
textAlign: TextAlign.center, textAlign: TextAlign.center,
textInputType: const TextInputType.numberWithOptions(decimal: true), textInputType: const TextInputType.numberWithOptions(decimal: true),
showShadow: false, showShadow: false,
onChange: (value) { onChange: (value) {
requestDetailProvider.workOrderCostModel?.qAmount = num.parse(value); _localModel.qAmount = num.tryParse(value);
}, },
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
8.height, 8.height,
AppTextFormField( AppTextFormField(
labelText: "PR No", labelText: "PR No",
backgroundColor: AppColor.fieldBgColor(context), backgroundColor: AppColor.fieldBgColor(context),
labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)),
initialValue: requestDetailProvider.workOrderCostModel?.prNo, initialValue: _localModel.prNo ?? '',
textAlign: TextAlign.center, textAlign: TextAlign.center,
textInputType: const TextInputType.numberWithOptions(decimal: true), textInputType: TextInputType.text,
showShadow: false, showShadow: false,
onChange: (value) { onChange: (value) {
requestDetailProvider.workOrderCostModel?.prNo = value; _localModel.prNo = value.isEmpty ? null : value;
}, },
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
8.height, 8.height,
AppTextFormField( AppTextFormField(
labelText: "PO No", labelText: "PO No",
backgroundColor: AppColor.fieldBgColor(context), backgroundColor: AppColor.fieldBgColor(context),
labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)),
initialValue: requestDetailProvider.workOrderCostModel?.poNo, initialValue: _localModel.poNo ?? '',
textAlign: TextAlign.center, textAlign: TextAlign.center,
textInputType: const TextInputType.numberWithOptions(decimal: true), textInputType: TextInputType.text,
showShadow: false, showShadow: false,
onChange: (value) { onChange: (value) {
requestDetailProvider.workOrderCostModel?.poNo = value; _localModel.poNo = value.isEmpty ? null : value;
}, },
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
8.height, 8.height,
AppTextFormField( AppTextFormField(
labelText: "MR No", labelText: "MR No",
backgroundColor: AppColor.fieldBgColor(context), backgroundColor: AppColor.fieldBgColor(context),
labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)),
initialValue: requestDetailProvider.workOrderCostModel?.mrNo, initialValue: _localModel.mrNo ?? '',
textAlign: TextAlign.center, textAlign: TextAlign.center,
textInputType: const TextInputType.numberWithOptions(decimal: true), textInputType: TextInputType.text,
showShadow: false, showShadow: false,
onChange: (value) { onChange: (value) {
requestDetailProvider.workOrderCostModel?.mrNo = value; _localModel.mrNo = value.isEmpty ? null : value;
}, },
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
8.height, 8.height,
AppTextFormField( AppTextFormField(
labelText: "Exchange Cost", labelText: "Exchange Cost",
backgroundColor: AppColor.fieldBgColor(context), backgroundColor: AppColor.fieldBgColor(context),
labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)),
initialValue: requestDetailProvider.workOrderCostModel?.exchangeCost != null ? requestDetailProvider.workOrderCostModel?.exchangeCost.toString() : '', initialValue: _localModel.exchangeCost?.toString() ?? '',
textAlign: TextAlign.center, textAlign: TextAlign.center,
textInputType: const TextInputType.numberWithOptions(decimal: true), textInputType: const TextInputType.numberWithOptions(decimal: true),
showShadow: false, showShadow: false,
onChange: (value) { onChange: (value) {
requestDetailProvider.workOrderCostModel?.exchangeCost = num.parse(value); _localModel.exchangeCost = num.tryParse(value);
}, },
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
], ],
).toShadowContainer(context).paddingAll(16), ).toShadowContainer(context).paddingAll(16),
).expanded, ),
FooterActionButton.footerContainer(
context: context,
child: AppFilledButton(
label: "Update Cost Details",
buttonColor: AppColor.primary10,
onPressed: () async {
if (validateForm(requestDetailProvider: requestDetailProvider)) {
showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading());
await requestDetailProvider.engineerUpdateCost().then((success) {
Navigator.pop(context);
if (success) {
Navigator.pop(context);
}
});
}
},
), ),
), FooterActionButton.footerContainer(
], context: context,
); child: AppFilledButton(
}), label: "Update Cost Details",
buttonColor: AppColor.primary10,
onPressed: () => _onSubmit(),
),
),
],
);
},
),
); );
} }
bool validateForm({required CMDetailProvider requestDetailProvider}) { Future<void> _onSubmit() async {
bool isValid = true; widget.provider.workOrderCostModel = _localModel;
if (!widget.isEdit) { showDialog(
return true; context: context,
} barrierDismissible: false,
// if (requestDetailProvider.engineerUpdateWorkOrderHelperModel!.equipmentStatus != null) { builder: (context) => const AppLazyLoading(),
// if (requestDetailProvider.engineerUpdateWorkOrderHelperModel!.equipmentStatus!.value == 1 || requestDetailProvider.engineerUpdateWorkOrderHelperModel!.equipmentStatus!.value == 2) { );
// if (requestDetailProvider.engineerUpdateWorkOrderHelperModel!.returnToService == null) {
// Fluttertoast.showToast(msg: "Return to service is required ", toastLength: Toast.LENGTH_LONG);
// return false;
// }
// }
// }
return isValid;
}
Widget assetStatusWidget({required BuildContext context, required EngineerUpdateWorkOrderHelperModel workOrderData}) { try {
return Consumer<EquipmentStatusProvider>(builder: (cxt, snapshot, _) { final success = await widget.provider.engineerUpdateCost();
return Column(
crossAxisAlignment: CrossAxisAlignment.start, if (mounted) Navigator.pop(context); // Close loading
mainAxisSize: MainAxisSize.min,
children: [ if (success && mounted) {
context.translation.assetStatus.bodyText(context).custom(color: AppColor.black20), Navigator.pop(context);
8.height, }
Wrap( } catch (e) {
runSpacing: 8, if (mounted) Navigator.pop(context);
spacing: 8, log('Error updating cost: $e');
children: [ }
for (var element in snapshot.items)
Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 24,
height: 24,
child: Radio(
value: element,
activeColor: Colors.red,
fillColor: WidgetStateColor.resolveWith((states) {
if (states.contains(WidgetState.selected)) return AppColor.primary10;
return AppColor.neutral130;
}),
groupValue: workOrderData.equipmentStatus,
onChanged: (state) {
setState(() {
workOrderData.equipmentStatus = element;
});
}),
),
8.width,
Text(element.name ?? '', style: AppTextStyles.tinyFont.copyWith(color: AppColor.neutral120)),
],
)
],
).toShimmer(isShow: snapshot.loading, context: context),
],
);
});
} }
} }

@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:developer';
import 'dart:io'; import 'dart:io';
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/providers/api/parts_provider.dart'; import 'package:test_sa/controllers/providers/api/parts_provider.dart';
@ -16,7 +15,6 @@ import 'package:test_sa/models/helper_data_models/spare_part/activity_spare_part
import 'package:test_sa/models/lookup.dart'; import 'package:test_sa/models/lookup.dart';
import 'package:test_sa/models/service_request/spare_parts.dart'; import 'package:test_sa/models/service_request/spare_parts.dart';
import 'package:test_sa/models/size_config.dart'; import 'package:test_sa/models/size_config.dart';
import 'package:test_sa/modules/cm_module/cm_detail_provider.dart';
import 'package:test_sa/modules/cm_module/cm_request_utils.dart'; import 'package:test_sa/modules/cm_module/cm_request_utils.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart'; import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/modules/cm_module/views/components/bottom_sheets/service_request_bottomsheet.dart'; import 'package:test_sa/modules/cm_module/views/components/bottom_sheets/service_request_bottomsheet.dart';
@ -28,385 +26,442 @@ 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/new_views/common_widgets/single_item_drop_down_menu.dart';
import 'package:test_sa/providers/loading_list_notifier.dart'; import 'package:test_sa/providers/loading_list_notifier.dart';
import 'package:test_sa/views/widgets/images/multi_image_picker.dart'; import 'package:test_sa/views/widgets/images/multi_image_picker.dart';
import 'package:test_sa/views/widgets/item_views/info_header_widget.dart';
import 'package:test_sa/views/widgets/item_views/info_text_widget.dart'; import 'package:test_sa/views/widgets/item_views/info_text_widget.dart';
import 'package:test_sa/views/widgets/loaders/no_data_found.dart'; import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
class SparePartRequest extends StatefulWidget { class SparePartRequest extends StatefulWidget {
static const String id = "/spare-part-request"; static const String id = "/spare-part-request";
final SparePartHelperModel? model;
final dynamic provider;
final bool fromCm;
const SparePartRequest({Key? key}) : super(key: key); const SparePartRequest({
Key? key,
required this.model,
required this.provider,
this.fromCm = false,
}) : super(key: key);
@override @override
_SparePartRequestState createState() => _SparePartRequestState(); _SparePartRequestState createState() => _SparePartRequestState();
} }
class _SparePartRequestState extends State<SparePartRequest> with TickerProviderStateMixin { class _SparePartRequestState extends State<SparePartRequest> {
CMDetailProvider? _requestDetailProvider;
PartsProvider? _partsProvider; PartsProvider? _partsProvider;
bool _isLoading = false; bool _isLoading = false;
List<SparePart> _spareParts = []; List<SparePart> _spareParts = [];
List<GenericAttachmentModel> _attachments = [];
List<GenericAttachmentModel> attachments = [];
final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final TextEditingController _partQtyController = TextEditingController();
final TextEditingController _installQtyController = TextEditingController();
final TextEditingController _returnQtyController = TextEditingController();
final TextEditingController _oracleNoController = TextEditingController();
final TextEditingController _descriptionController = TextEditingController();
Lookup statusLookup = Lookup.fromJson({"id": 5619, "name": "New", "value": 1}); Lookup statusLookup = Lookup.fromJson({"id": 5619, "name": "New", "value": 1});
int? activityStatus;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_partsProvider = Provider.of<PartsProvider>(context, listen: false); _partsProvider = Provider.of<PartsProvider>(context, listen: false);
_requestDetailProvider = Provider.of<CMDetailProvider>(context, listen: false); _initializeModel();
_requestDetailProvider?.sparePartHelperModel = SparePartHelperModel( _loadInitialData();
id: _requestDetailProvider?.sparePartHelperModel?.id ?? 0,
workOrderId: _requestDetailProvider?.sparePartHelperModel?.workOrderId ?? _requestDetailProvider?.currentWorkOrder?.data?.requestId,
sparePartAttachments: _requestDetailProvider?.sparePartHelperModel?.sparePartAttachments ?? [],
sparePart: _requestDetailProvider?.sparePartHelperModel?.sparePart ?? SparePart(),
quantity: _requestDetailProvider?.sparePartHelperModel?.quantity,
installQty: _requestDetailProvider?.sparePartHelperModel?.installQty,
returnQty: _requestDetailProvider?.sparePartHelperModel?.returnQty,
activityStatusId: _requestDetailProvider?.sparePartHelperModel?.activityStatusId ?? statusLookup.id,
activityStatus: _requestDetailProvider?.sparePartHelperModel?.activityStatus ?? statusLookup,
comment: _requestDetailProvider?.sparePartHelperModel?.comment ?? '',
);
_partQtyController.text = _requestDetailProvider?.sparePartHelperModel?.quantity != null ? _requestDetailProvider!.sparePartHelperModel!.quantity!.round().toString() : '';
_installQtyController.text = _requestDetailProvider?.sparePartHelperModel?.installQty != null ? _requestDetailProvider!.sparePartHelperModel!.installQty!.round().toString() : '';
_returnQtyController.text = _requestDetailProvider?.sparePartHelperModel?.returnQty != null ? _requestDetailProvider!.sparePartHelperModel!.returnQty!.round().toString() : '';
_oracleNoController.text = _requestDetailProvider?.sparePartHelperModel?.sparePart?.oracleCode != null ? _requestDetailProvider!.sparePartHelperModel!.sparePart!.oracleCode! : '';
_descriptionController.text = _requestDetailProvider?.sparePartHelperModel?.comment != null ? _requestDetailProvider!.sparePartHelperModel!.comment! : '';
activityStatus = _requestDetailProvider?.sparePartHelperModel?.activityStatus?.value;
scheduleMicrotask(() async {
_isLoading = true;
attachments = _requestDetailProvider?.sparePartHelperModel?.sparePartAttachments?.map((e) => GenericAttachmentModel(id: e.id!, name: e.name ?? '')).toList() ?? [];
setState(() {});
_spareParts = await _partsProvider!.getPartsListByDisplayName(assetId: _requestDetailProvider?.currentWorkOrder?.data?.asset?.id);
_isLoading = false;
setState(() {});
});
} }
void restValues({required CMDetailProvider requestDetailProvider}) { void _initializeModel() {
requestDetailProvider.sparePartHelperModel = SparePartHelperModel(id: 0, workOrderId: _requestDetailProvider?.currentWorkOrder?.data?.requestId, sparePartAttachments: []); final bool isNewActivity = widget.model?.id == null || widget.model?.id == 0;
_partQtyController.clear(); if (isNewActivity) {
_installQtyController.clear(); widget.provider.sparePartHelperModel = SparePartHelperModel(
_returnQtyController.clear(); id: 0,
_oracleNoController.clear(); workOrderId: widget.fromCm ? widget.provider.currentWorkOrder?.data?.requestId : null,
_descriptionController.clear(); pmVisitId: !widget.fromCm ? widget.provider.planPreventiveVisit?.id : null,
attachments = []; sparePartAttachments: [],
sparePart: SparePart(),
quantity: null,
installQty: null,
returnQty: null,
activityStatusId: statusLookup.id,
activityStatus: statusLookup,
comment: '',
);
} else if (widget.provider.sparePartHelperModel == null || widget.provider.sparePartHelperModel?.id != widget.model?.id) {
widget.provider.sparePartHelperModel = SparePartHelperModel(
id: widget.model?.id ?? 0,
workOrderId: widget.fromCm ? widget.model?.workOrderId ?? widget.provider.currentWorkOrder?.data?.requestId : null,
pmVisitId: !widget.fromCm ? widget.model?.pmVisitId : null,
sparePartAttachments: widget.model?.sparePartAttachments ?? [],
sparePart: widget.model?.sparePart ?? SparePart(),
quantity: widget.model?.quantity,
installQty: widget.model?.installQty,
returnQty: widget.model?.returnQty,
activityStatusId: widget.model?.activityStatusId ?? statusLookup.id,
activityStatus: widget.model?.activityStatus ?? statusLookup,
comment: widget.model?.comment ?? '',
);
}
log('Initialized SparePartHelperModel: ${widget.provider.sparePartHelperModel?.toJson()}');
} }
@override Future<void> _loadInitialData() async {
void dispose() { setState(() => _isLoading = true);
_partQtyController.dispose(); _spareParts = await _partsProvider!.getPartsListByDisplayName(
_installQtyController.dispose(); assetId: widget.fromCm ? widget.provider.currentWorkOrder?.data?.asset?.id : widget.provider.planPreventiveVisit.asset.id,
_returnQtyController.dispose(); );
_oracleNoController.dispose(); final bool isNewActivity = widget.model?.id == null || widget.model?.id == 0;
_descriptionController.dispose(); _attachments = [];
super.dispose(); if (!isNewActivity) {
} try {
final attachmentsList = widget.model?.sparePartAttachments;
if (attachmentsList != null && attachmentsList.isNotEmpty) {
for (var item in attachmentsList) {
_attachments.add(GenericAttachmentModel(
id: item.id ?? 0,
name: item.name ?? '',
));
}
}
log('Loaded ${_attachments.length} attachments for existing activity');
} catch (e) {
log('Error loading attachments: $e');
_attachments = [];
}
} else {
log('New activity - starting with empty attachments');
}
bool _isLocalUrl(String url) { if (mounted) setState(() => _isLoading = false);
if (url.isEmpty != false) return false;
return url.startsWith("/") || url.startsWith("file://") || url.substring(1).startsWith(':\\');
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool showInstallAndReturnQty = activityStatus == 3 || activityStatus == 4;
return Scaffold( return Scaffold(
key: _scaffoldKey,
appBar: DefaultAppBar(title: context.translation.sparePartRequest), appBar: DefaultAppBar(title: context.translation.sparePartRequest),
body: Consumer<CMDetailProvider>(builder: (context, CMDetailProvider requestDetailProvider, child) { body: ListenableBuilder(
return Form( listenable: widget.provider,
key: _formKey, builder: (context, _) {
child: Column( final provider = widget.provider;
children: [ final model = provider.sparePartHelperModel;
SingleChildScrollView( final bool showInstallAndReturnQty = (model?.activityStatus?.value == 3 || model?.activityStatus?.value == 4);
padding: const EdgeInsets.all(16), final bool isReadOnly = provider.isReadOnlyRequest ?? false;
child: IgnorePointer(
ignoring: _requestDetailProvider!.isReadOnlyRequest, return Form(
child: Column( key: _formKey,
crossAxisAlignment: CrossAxisAlignment.stretch, child: Column(
children: [ children: [
Column( Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: IgnorePointer(
ignoring: isReadOnly,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
context.translation.sparePartDetails.heading5(context).custom(color: AppColor.textStyleColor(context)), _buildFormContent(
// 12.height, context,
// SingleItemDropDownMenu<Lookup, ActivityStatusProvider>( model,
// context: context, provider,
// height: 56.toScreenHeight, showInstallAndReturnQty,
// title: context.translation.activityStatus,
// showShadow: false,
// backgroundColor: AppColor.neutral100,
// initialValue: requestDetailProvider.sparePartHelperModel?.activityStatus,
// onSelect: (status) {
// requestDetailProvider.sparePartHelperModel?.activityStatus = status;
// },
// ),
12.height,
SingleItemDropDownMenu<SparePart, NullableLoadingProvider>(
context: context,
title: context.translation.part,
staticData: _spareParts,
showShadow: false,
showAsBottomSheet: true,
loading: _isLoading,
initialValue: requestDetailProvider.sparePartHelperModel?.sparePart,
// backgroundColor: context.isDark ? AppColor.neutral20 : AppColor.neutral90,
onSelect: (part) async {
if (part != null) {
requestDetailProvider.sparePartHelperModel?.sparePart = part;
_oracleNoController.text = part.oracleCode ?? '';
requestDetailProvider.updateSparePartHelperModel(_requestDetailProvider?.sparePartHelperModel);
}
},
),
if (requestDetailProvider.sparePartHelperModel?.sparePart?.id != null) ...[
12.height,
SizedBox(
height: 30.toScreenHeight,
child: Text('View Parts Availability',
style: TextStyle(
fontSize: 12.toScreenWidth,
fontWeight: FontWeight.w500,
fontStyle: FontStyle.normal,
color: AppColor.primary10,
decorationColor: AppColor.primary10,
decoration: TextDecoration.underline,
)).onPress(
() async {
requestDetailProvider.getStoreAvailabilityById(partId: requestDetailProvider.sparePartHelperModel?.sparePart?.id);
showModalBottomSheet(
context: context,
useSafeArea: true,
isScrollControlled: true,
isDismissible: true,
backgroundColor: AppColor.bottomSheetColor(context),
builder: (BuildContext context) => PartDetailBottomSheetSheet().bottomSheetContainer(context),
);
},
),
),
],
12.height,
AppTextFormField(
controller: _partQtyController,
labelText: context.translation.quantity,
textInputType: TextInputType.number,
labelStyle: AppTextStyles.textFieldLabelStyle,
// showWithoutDecoration: true,
// backgroundColor: context.isDark ? AppColor.neutral20 : AppColor.neutral90,
enable: requestDetailProvider.sparePartHelperModel?.sparePart?.id != null,
validator: (value) => value == null || value.isEmpty
? context.translation.requiredField
: Validator.isNumeric(value)
? null
: context.translation.onlyNumbers,
onChange: (value) {
requestDetailProvider.sparePartHelperModel?.quantity = num.tryParse(value ?? "");
},
onSaved: (text) {
requestDetailProvider.sparePartHelperModel?.quantity = num.tryParse(text ?? "");
},
),
12.height,
if (showInstallAndReturnQty) ...[
AppTextFormField(
controller: _installQtyController,
labelText: context.translation.installedQty,
textInputType: const TextInputType.numberWithOptions(decimal: true),
labelStyle: AppTextStyles.textFieldLabelStyle,
// showWithoutDecoration: true,
// backgroundColor: context.isDark ? AppColor.neutral20 : AppColor.neutral90,
enable: requestDetailProvider.sparePartHelperModel?.sparePart?.id != null,
validator: (value) => value == null || value.isEmpty
? context.translation.requiredField
: Validator.isNumeric(value)
? null
: context.translation.onlyNumbers,
onChange: (value) {
requestDetailProvider.sparePartHelperModel?.installQty = num.tryParse(value ?? "");
},
onSaved: (text) {
requestDetailProvider.sparePartHelperModel?.installQty = num.tryParse(text ?? "");
},
),
12.height,
AppTextFormField(
controller: _returnQtyController,
labelText: context.translation.returnQty,
textInputType: TextInputType.number,
labelStyle: AppTextStyles.textFieldLabelStyle,
// showWithoutDecoration: true,
// backgroundColor: context.isDark ? AppColor.neutral20 : AppColor.neutral90,
enable: requestDetailProvider.sparePartHelperModel?.sparePart?.id != null,
validator: (value) => value == null || value.isEmpty
? context.translation.requiredField
: Validator.isNumeric(value)
? null
: context.translation.onlyNumbers,
onChange: (value) {
requestDetailProvider.sparePartHelperModel?.returnQty = num.tryParse(value ?? "");
},
onSaved: (text) {
requestDetailProvider.sparePartHelperModel?.returnQty = num.tryParse(text ?? "");
},
),
12.height,
],
AppTextFormField(
labelStyle: AppTextStyles.textFieldLabelStyle,
controller: _oracleNoController,
labelText: context.translation.oracleNo,
textInputType: TextInputType.number,
// showWithoutDecoration: true,
backgroundColor: AppColor.disableFieldBackground(context),
enable: false,
),
12.height,
AppTextFormField(
initialValue: requestDetailProvider.sparePartHelperModel?.comment,
labelStyle: AppTextStyles.textFieldLabelStyle,
controller: _descriptionController,
showSpeechToText: true,
// backgroundColor: context.isDark ? AppColor.neutral20 : AppColor.neutral90,
labelText: "Engineer Comment",
alignLabelWithHint: true,
// showWithoutDecoration: true,
textInputType: TextInputType.multiline,
onChange: (value) {
requestDetailProvider.sparePartHelperModel?.comment = value;
},
onSaved: (value) {
requestDetailProvider.sparePartHelperModel?.comment = value;
},
),
12.height,
AttachmentPicker(
label: context.translation.attachQuotation,
attachment: attachments,
buttonIcon: 'quotation_icon'.toSvgAsset(color: AppColor.primary10),
buttonColor: AppColor.primary10,
), ),
], ],
).toShadowContainer(context), ),
8.height, ),
],
), ),
), ),
).expanded, if (!isReadOnly) _buildSubmitButton(context, provider),
if (!requestDetailProvider.isReadOnlyRequest) ],
FooterActionButton.footerContainer( ),
context: context, );
child: AppFilledButton( },
label: _requestDetailProvider?.sparePartHelperModel?.id == 0 ? context.translation.addSparePartActivity : context.translation.updateSparePartActivity, ),
buttonColor: AppColor.green70, );
onPressed: () { }
_onSubmit(requestDetailProvider: requestDetailProvider);
}, Widget _buildFormContent(
)), BuildContext context,
], SparePartHelperModel? model,
dynamic provider,
bool showInstallAndReturnQty,
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 12,
children: [
InfoHeader16Widget(context.translation.sparePartDetails),
SingleItemDropDownMenu<SparePart, NullableLoadingProvider>(
context: context,
title: context.translation.part,
staticData: _spareParts,
showShadow: false,
showAsBottomSheet: true,
loading: _isLoading,
initialValue: model?.sparePart,
onSelect: (part) {
if (part != null) {
model?.sparePart = part;
provider.updateSparePartHelperModel(model);
}
},
),
if (model?.sparePart?.id != null) ...[
SizedBox(
// height: 30.toScreenHeight,
child: Text(
'View Parts Availability',
style: TextStyle(
fontSize: 12.toScreenWidth,
fontWeight: FontWeight.w500,
color: AppColor.primary10,
decorationColor: AppColor.primary10,
decoration: TextDecoration.underline,
),
).onPress(() {
provider.getStoreAvailabilityById(partId: model?.sparePart?.id);
showModalBottomSheet(
context: context,
useSafeArea: true,
isScrollControlled: true,
isDismissible: true,
backgroundColor: AppColor.bottomSheetColor(context),
builder: (ctx) => PartDetailBottomSheetSheet(provider: provider).bottomSheetContainer(ctx),
);
}),
), ),
); ],
}), AppTextFormField(
key: ValueKey('quantity_${model?.quantity}'),
initialValue: model?.quantity?.round().toString() ?? '',
labelText: context.translation.quantity,
textInputType: TextInputType.number,
labelStyle: AppTextStyles.textFieldLabelStyle,
enable: model?.sparePart?.id != null,
validator: (value) => value == null || value.isEmpty
? context.translation.requiredField
: Validator.isNumeric(value)
? null
: context.translation.onlyNumbers,
onChange: (value) {
model?.quantity = num.tryParse(value);
},
onSaved: (text) {
model?.quantity = num.tryParse(text ?? '');
},
),
if (showInstallAndReturnQty) ...[
AppTextFormField(
key: ValueKey('installQty_${model?.installQty}'),
initialValue: model?.installQty?.round().toString() ?? '',
labelText: context.translation.installedQty,
textInputType: const TextInputType.numberWithOptions(decimal: true),
labelStyle: AppTextStyles.textFieldLabelStyle,
enable: model?.sparePart?.id != null,
validator: (value) => value == null || value.isEmpty
? context.translation.requiredField
: Validator.isNumeric(value)
? null
: context.translation.onlyNumbers,
onChange: (value) {
model?.installQty = num.tryParse(value);
},
onSaved: (text) {
model?.installQty = num.tryParse(text ?? '');
},
),
AppTextFormField(
key: ValueKey('returnQty_${model?.returnQty}'),
initialValue: model?.returnQty?.round().toString() ?? '',
labelText: context.translation.returnQty,
textInputType: TextInputType.number,
labelStyle: AppTextStyles.textFieldLabelStyle,
enable: model?.sparePart?.id != null,
validator: (value) => value == null || value.isEmpty
? context.translation.requiredField
: Validator.isNumeric(value)
? null
: context.translation.onlyNumbers,
onChange: (value) {
model?.returnQty = num.tryParse(value);
},
onSaved: (text) {
model?.returnQty = num.tryParse(text ?? '');
},
),
],
AppTextFormField(
key: ValueKey('oracle_${model?.sparePart?.oracleCode}'),
initialValue: model?.sparePart?.oracleCode ?? '',
labelStyle: AppTextStyles.textFieldLabelStyle,
labelText: context.translation.oracleNo,
textInputType: TextInputType.number,
backgroundColor: AppColor.disableFieldBackground(context),
enable: false,
),
AppTextFormField(
key: ValueKey('comment_${model?.comment}'),
initialValue: model?.comment ?? '',
labelStyle: AppTextStyles.textFieldLabelStyle,
showSpeechToText: true,
labelText: "Engineer Comment",
alignLabelWithHint: true,
textInputType: TextInputType.multiline,
onChange: (value) {
model?.comment = value;
},
onSaved: (value) {
model?.comment = value;
},
),
AttachmentPicker(
label: context.translation.attachQuotation,
attachment: _attachments,
buttonIcon: 'quotation_icon'.toSvgAsset(color: AppColor.primary10),
buttonColor: AppColor.primary10,
),
],
).toShadowContainer(context);
}
Widget _buildSubmitButton(BuildContext context, dynamic provider) {
return FooterActionButton.footerContainer(
context: context,
child: AppFilledButton(
label: provider.sparePartHelperModel?.id == 0 ? context.translation.addSparePartActivity : context.translation.updateSparePartActivity,
buttonColor: AppColor.green70,
onPressed: () => _onSubmit(provider),
),
); );
} }
_onSubmit({required CMDetailProvider requestDetailProvider}) async { Future<void> _onSubmit(dynamic provider) async {
if (requestDetailProvider.sparePartHelperModel?.sparePart?.id == null) { final model = provider.sparePartHelperModel;
if (model?.sparePart?.id == null) {
"Please select spare part".showToast; "Please select spare part".showToast;
return; return;
} else if (requestDetailProvider.sparePartHelperModel?.quantity == null) { } else if (model?.quantity == null) {
"Please enter quantity".showToast; "Please enter quantity".showToast;
return; return;
} }
requestDetailProvider.sparePartHelperModel?.sparePartAttachments?.clear(); model?.sparePartAttachments?.clear();
for (var item in _attachments) {
for (var item in attachments) {
String fileName = CMRequestUtils.isLocalUrl(item.name ?? '') ? ("${item.name ?? ''.split("/").last}|${base64Encode(File(item.name ?? '').readAsBytesSync())}") : item.name ?? ''; String fileName = CMRequestUtils.isLocalUrl(item.name ?? '') ? ("${item.name ?? ''.split("/").last}|${base64Encode(File(item.name ?? '').readAsBytesSync())}") : item.name ?? '';
requestDetailProvider.sparePartHelperModel?.sparePartAttachments?.add(
model?.sparePartAttachments?.add(
SparePartAttachments(id: item.id, name: fileName), SparePartAttachments(id: item.id, name: fileName),
); );
} }
// Show loading dialog // Show loading
showDialog( showDialog(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (context) => const AppLazyLoading(), builder: (context) => const AppLazyLoading(),
); );
// Submit
bool success = false; bool success = false;
if (_requestDetailProvider?.sparePartHelperModel?.id == 0) { String message = "";
success = await requestDetailProvider.createActivitySparePart(); if (model?.id == 0) {
success = await provider.createActivitySparePart();
} else { } else {
success = await requestDetailProvider.updateActivitySparePart(); message = await provider.updateActivitySparePart();
success = message.isEmpty;
} }
if (mounted) Navigator.pop(context);
if (mounted) Navigator.pop(context); // Close loading
if (success) { if (success) {
requestDetailProvider.getWorkOrderById( if (widget.fromCm) {
id: requestDetailProvider.currentWorkOrder!.data!.requestId!, provider.getWorkOrderById(
); id: provider.currentWorkOrder!.data!.requestId!,
if (mounted) Navigator.pop(context); );
if (_requestDetailProvider?.sparePartHelperModel?.id == 0) { } else {
ServiceRequestBottomSheet.addAnotherSpareRequestBottomSheet(context: context); provider.getPlanPreventiveVisitById(
int.tryParse(provider.planPreventiveVisit?.id ?? ''),
);
}
if (mounted) Navigator.pop(context); // Close form
if (model?.id == 0 && widget.fromCm) {
ServiceRequestBottomSheet.addAnotherSpareRequestBottomSheet(context: context, fromCm: widget.fromCm);
const SizedBox().flushBar( const SizedBox().flushBar(
context: context, context: context,
title: context.translation.sparePartActivitySuccess, title: context.translation.sparePartActivitySuccess,
message: '', message: '',
); );
} }
} else {
if(message.isNotEmpty) {
context.showInfoDialog(message, title: "Error");
}
} }
} }
} }
/// Bottom Sheet for showing parts availability
class PartDetailBottomSheetSheet extends StatelessWidget { class PartDetailBottomSheetSheet extends StatelessWidget {
PartDetailBottomSheetSheet({Key? key}) : super(key: key); final dynamic provider;
const PartDetailBottomSheetSheet({
Key? key,
required this.provider,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
height: SizeConfig.screenHeight! / 2.2, height: SizeConfig.screenHeight! / 2.2,
width: SizeConfig.screenWidth, width: SizeConfig.screenWidth,
child: Column( child: SafeArea(
mainAxisSize: MainAxisSize.min, child: Column(
crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min,
children: [ crossAxisAlignment: CrossAxisAlignment.start,
8.height, children: [
'Parts Availability'.heading6(context).paddingOnly(top: 12, start: 12), 12.height,
Consumer<CMDetailProvider>(builder: (context, requestDetailProvider, child) { 'Parts Availability'.heading4(context),
return requestDetailProvider.isLoading Expanded(
? CircularProgressIndicator(color: AppColor.loadingColor(context)).center child: ListenableBuilder(
: requestDetailProvider.storeAvailability.isEmpty listenable: provider,
? const NoDataFound().center builder: (context, _) {
: ListView.separated( return provider.isLoading
padding: const EdgeInsets.only(top: 12), ? CircularProgressIndicator(
itemCount: requestDetailProvider.storeAvailability.length, color: AppColor.loadingColor(context),
separatorBuilder: (czt, index) => 12.height, ).center
itemBuilder: (context, index) { : provider.storeAvailability.isEmpty
StoreAvailability model = requestDetailProvider.storeAvailability[index]; ? const NoDataFound().center
return partAvailableQuantityCard(context: context, model: model); : ListView.separated(
}, padding: const EdgeInsets.only(top: 12, bottom: 12),
); itemCount: provider.storeAvailability.length,
}).expanded, separatorBuilder: (czt, index) => 12.height,
], itemBuilder: (context, index) {
StoreAvailability model = provider.storeAvailability[index];
return _partAvailableQuantityCard(context: context, model: model);
},
);
},
),
),
],
),
), ),
); );
} }
Widget partAvailableQuantityCard({required StoreAvailability model, required BuildContext context}) { Widget _partAvailableQuantityCard({required StoreAvailability model, required BuildContext context}) {
return Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ return Column(
InfoTextWidget(label: "Site Name:".addTranslation, value: model.siteName ?? ''), mainAxisSize: MainAxisSize.min,
InfoTextWidget(label: "Store Name:".addTranslation, value: model.storeName ?? ''), crossAxisAlignment: CrossAxisAlignment.start,
InfoTextWidget(label: "Available Quantity:".addTranslation, value: model.availablityQty != null ? model.availablityQty.toString() : ''), children: [
]).toShadowContainer(context, padding: 12, showShadow: false,); InfoTextWidget(
label: "Site Name".addTranslation,
value: model.siteName ?? '',
),
InfoTextWidget(
label: "Store Name".addTranslation,
value: model.storeName ?? '',
),
InfoTextWidget(
label: "Available Quantity".addTranslation,
value: model.availablityQty?.toString() ?? '',
),
],
).toShadowContainer(context);
} }
} }

@ -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,49 +33,51 @@ 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(
future: Provider.of<IncidentProvider>(context, listen: false).getIncidentById(incidentId), child: FutureBuilder<IncidentDataModel?>(
builder: (BuildContext context, AsyncSnapshot<IncidentDataModel?> snapshot) { future: Provider.of<IncidentProvider>(context, listen: false).getIncidentById(incidentId),
if (snapshot.connectionState == ConnectionState.waiting) return CircularProgressIndicator(color: AppColor.loadingColor(context)).center; builder: (BuildContext context, AsyncSnapshot<IncidentDataModel?> snapshot) {
if (snapshot.data == null) return const NoDataFound().center; if (snapshot.connectionState == ConnectionState.waiting) return CircularProgressIndicator(color: AppColor.loadingColor(context)).center;
if (snapshot.data == null) return const NoDataFound().center;
List<IncidentAttachments> allAttachments = snapshot.data!.incidentAttachments!; List<IncidentAttachments> allAttachments = snapshot.data!.incidentAttachments!;
return SingleChildScrollView( return SingleChildScrollView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
StatusLabel( StatusLabel(
label: snapshot.data!.incidentStatusName!, label: snapshot.data!.incidentStatusName!,
textColor: AppColor.getRequestStatusTextColorByName(context, snapshot.data!.incidentStatusName!), textColor: AppColor.getRequestStatusTextColorByName(context, snapshot.data!.incidentStatusName!),
backgroundColor: AppColor.getRequestStatusColorByName(context, snapshot.data!.incidentStatusName!), backgroundColor: AppColor.getRequestStatusColorByName(context, snapshot.data!.incidentStatusName!),
), ),
1.width.expanded, 1.width.expanded,
InfoDateWidget(snapshot.data!.createdDate?.toString() ?? ""), InfoDateWidget(snapshot.data!.createdDate?.toString() ?? ""),
], ],
), ),
// 12.height, // 12.height,
...requesterDetails(context, snapshot.data!), ...requesterDetails(context, snapshot.data!),
const Divider().defaultStyle(context), const Divider().defaultStyle(context),
...requestDetails(context, snapshot.data!), ...requestDetails(context, snapshot.data!),
if (snapshot.data!.ovrTicketNumber?.isNotEmpty ?? false) ovrInformation(context, snapshot.data!.ovrTicketNumber!), if (snapshot.data!.ovrTicketNumber?.isNotEmpty ?? false) ovrInformation(context, snapshot.data!.ovrTicketNumber!),
const Divider().defaultStyle(context),
...assetDetails(context, snapshot.data!),
const Divider().defaultStyle(context),
...personInvolvedDetails(context, snapshot.data!),
if (allAttachments.isNotEmpty) ...[
const Divider().defaultStyle(context), const Divider().defaultStyle(context),
InfoHeader16Widget("Attachments".addTranslation), ...assetDetails(context, snapshot.data!),
FilesList(images: allAttachments.map((e) => URLs.getFileUrl(e.attachmentName ?? '') ?? '').toList() ?? []), const Divider().defaultStyle(context),
...personInvolvedDetails(context, snapshot.data!),
if (allAttachments.isNotEmpty) ...[
const Divider().defaultStyle(context),
InfoHeader16Widget("Attachments".addTranslation),
FilesList(images: allAttachments.map((e) => URLs.getFileUrl(e.attachmentName ?? '') ?? '').toList() ?? []),
],
], ],
], ).toShadowContainer(context),
).toShadowContainer(context), );
); }),
})); ));
} }
List<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);
}
},
),
);
}
}

@ -0,0 +1,228 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/controllers/providers/api/ppm_provider.dart';
import 'package:test_sa/controllers/providers/api/user_provider.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/enums/user_types.dart';
import 'package:test_sa/models/helper_data_models/spare_part/activity_spare_part_model.dart';
import 'package:test_sa/models/plan_preventive_visit/plan_preventive_visit_model.dart';
import 'package:test_sa/models/service_request/spare_parts.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/modules/cm_module/views/forms/spare_part/spare_part_request.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_lazy_loading.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/views/widgets/images/files_list.dart';
import 'package:test_sa/views/widgets/item_views/info_text_widget.dart';
import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
import 'package:test_sa/views/widgets/requests/request_status.dart';
class PmActivitiesListView extends StatefulWidget {
static const String id = "/activities-list";
const PmActivitiesListView({Key? key}) : super(key: key);
@override
_PmActivitiesListViewState createState() {
return _PmActivitiesListViewState();
}
}
class _PmActivitiesListViewState extends State<PmActivitiesListView> {
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
List<PMSparePartActivityModel> activities = [];
bool disableNewActivity = false;
@override
Widget build(BuildContext context) {
UserProvider userProvider = Provider.of<UserProvider>(context);
return Scaffold(
appBar: const DefaultAppBar(title: "Spare Part Requests"),
body: Consumer<PpmProvider>(builder: (context, PpmProvider pmProvider, child) {
activities = pmProvider.planPreventiveVisit?.activities ?? [];
if (activities.isNotEmpty && (activities.last.activityStatus!.value == 14 || activities.last.activityStatus!.value == 19)) {
disableNewActivity = true;
} else {
disableNewActivity = false;
}
return Column(
children: [
(activities.isEmpty)
? NoDataFound(message: context.translation.noDataFound).center.expanded
: ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: activities.length,
separatorBuilder: (czt, index) => 12.height,
itemBuilder: (context, index) {
return sparePartActivityCard(pmProvider: pmProvider, userProvider: userProvider, context: context, activity: activities[index]);
},
).expanded,
if (userProvider.user!.type == UsersTypes.engineer && (pmProvider.planPreventiveVisit?.visitStatus?.value != 5 && pmProvider.planPreventiveVisit?.visitStatus?.value != 3))
FooterActionButton.footerContainer(
context: context,
child: AppFilledButton(
label: "Create Spare Part Request",
maxWidth: true,
buttonColor: AppColor.primary10,
loading: pmProvider.isLoading,
disableButton: disableNewActivity,
onPressed: () async {
final model = SparePartHelperModel(
id: 0,
pmVisitId: pmProvider.planPreventiveVisit?.id,
);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SparePartRequest(
model: model,
provider: pmProvider,
fromCm: false,
),
),
);
},
))
],
);
}),
);
}
Widget sparePartActivityCard({required PpmProvider pmProvider, required UserProvider userProvider, required BuildContext context, required PMSparePartActivityModel activity}) {
bool showActionButtons = userProvider.user!.userID == pmProvider.planPreventiveVisit?.assignedEmployee?.userId &&
userProvider.user!.type == UsersTypes.engineer &&
pmProvider.planPreventiveVisit?.visitStatus?.value != 5 &&
pmProvider.planPreventiveVisit?.visitStatus?.value != 3;
bool canDelete = showActionButtons && activity.activityStatus?.value == 1;
bool canEdit = showActionButtons && (canDelete || activity.activityStatus!.value! == 3);
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
StatusLabel(
label: activity.activityStatus?.name,
textColor: AppColor.white10,
backgroundColor: AppColor.primary10,
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 12,
children: [
if (canEdit)
"edit_icon".toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)).onPress(() {
editSparePartRequest(context: context, pmProvider: pmProvider, activity: activity);
}),
if (canDelete)
"delete_icon".toSvgAsset(height: 24, width: 24).onPress(() async {
showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading());
int status = await pmProvider.deleteActivitySparePart(id: activity.id!);
if (status == 200) {
await pmProvider.getPlanPreventiveVisitById((int.tryParse(pmProvider.planPreventiveVisit?.id ?? '')) as int);
Navigator.pop(context);
} else {
Navigator.pop(context);
}
}),
],
)
],
),
6.height,
InfoTextWidget(
label: context.translation.partName,
value: activity.partCatalogItem?.partName,
),
InfoTextWidget(
label: context.translation.partNo,
value: activity.partCatalogItem?.partNumber,
),
InfoTextWidget(
label: context.translation.oracleCode,
value: activity.partCatalogItem?.oracleCode,
),
if (activity.quantity != null && activity.quantity! > 0)
InfoTextWidget(
label: context.translation.quantity,
value: activity.quantity.toString(),
),
if (activity.installQty != null && activity.installQty! > 0)
InfoTextWidget(
label: context.translation.installedQty,
value: activity.installQty.toString(),
),
if (activity.returnQty != null && activity.returnQty! > 0)
InfoTextWidget(
label: context.translation.returnQty,
value: activity.returnQty.toString(),
),
if (activity.comment?.isNotEmpty ?? false) ...[
const Divider().defaultStyle(context),
InfoTextLabelWidget(label: activity.comment ?? ""),
],
if (activity.activitySparePartPPMAttachments.isNotEmpty) ...[
const Divider().defaultStyle(context),
FilesList(images: activity.activitySparePartPPMAttachments.map((toElement) => URLs.getFileUrl(toElement.name ?? '') ?? '').toList()),
],
],
).toShadowContainer(context, showShadow: false).onPress(() {
if (showActionButtons) return;
if (pmProvider.isReadOnlyRequest) {
editSparePartRequest(context: context, pmProvider: pmProvider, activity: activity);
} else if (userProvider.user!.userID == pmProvider.planPreventiveVisit?.assignedEmployee?.userId) {
pmProvider.isReadOnlyRequest = true;
editSparePartRequest(context: context, pmProvider: pmProvider, activity: activity);
}
});
}
void editSparePartRequest({required BuildContext context, required PpmProvider pmProvider, required PMSparePartActivityModel activity}) async {
final model = SparePartHelperModel(
id: activity.id,
pmVisitId: pmProvider.planPreventiveVisit?.id,
comment: activity.comment,
sparePartAttachments: activity.activitySparePartPPMAttachments,
activityStatus: activity.activityStatus,
sparePart: SparePart(
id: activity.partCatalogItem?.id,
partName: activity.partCatalogItem?.partName,
partNo: activity.partCatalogItem?.partNumber,
oracleCode: activity.partCatalogItem?.oracleCode,
),
quantity: activity.quantity,
installQty: activity.installQty,
returnQty: activity.returnQty,
activityStatusId: activity.activityStatus?.id,
);
Navigator.of(context).push(MaterialPageRoute(
builder: (_) => SparePartRequest(
model: model,
provider: pmProvider,
fromCm: false, // This is PM, not CM
),
));
}
}

@ -8,6 +8,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/models/plan_preventive_visit/plan_preventive_visit_model.dart'; import 'package:test_sa/models/plan_preventive_visit/plan_preventive_visit_model.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart'; import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/modules/pm_module/ppm_wo/activities/pm_activities_list_view.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/item_views/info_header_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/item_views/info_text_widget.dart';
@ -82,6 +83,20 @@ class _PpmDetailsPageState extends State<PpmDetailsPage> {
textColor: AppColor.getRequestStatusTextColorByName(context, planPreventiveVisit.visitStatus!.name!), textColor: AppColor.getRequestStatusTextColorByName(context, planPreventiveVisit.visitStatus!.name!),
backgroundColor: AppColor.getRequestStatusColorByName(context, planPreventiveVisit.visitStatus!.name!), backgroundColor: AppColor.getRequestStatusColorByName(context, planPreventiveVisit.visitStatus!.name!),
), ),
// "edit_icon".toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)).onPress(() {
// if(userProvider!.user!.type == UsersTypes.engineer && (!ppmProvider.isReadOnly)){
// FooterActionButton.footerContainer(
// child: AppFilledButton(
// onPressed: () async {
// await Navigator.of(context).push(MaterialPageRoute(builder: (_) => UpdatePpm(ppm: null, planPreventiveVisit: planPreventiveVisit)));
// getVisitData();
// },
// label: context.translation.updateWorkOrder,
// ),
// context: context,
// );
// }
// })
], ],
), ),
InfoHeader16Widget(planPreventiveVisit.assetName?.cleanupWhitespace.capitalizeFirstOfEach ?? ''), InfoHeader16Widget(planPreventiveVisit.assetName?.cleanupWhitespace.capitalizeFirstOfEach ?? ''),
@ -102,29 +117,23 @@ class _PpmDetailsPageState extends State<PpmDetailsPage> {
], ],
).toShadowContainer(context), ).toShadowContainer(context),
).expanded, ).expanded,
if (userProvider!.user!.type == UsersTypes.engineer && (!ppmProvider.isReadOnly)) ...[ // if (userProvider!.user!.type == UsersTypes.engineer && (!ppmProvider.isReadOnly)) ...[
FooterActionButton.footerContainer( FooterActionButton.footerContainer(
child: AppFilledButton( child: Column(
onPressed: () async { children: [
await Navigator.of(context).push(MaterialPageRoute(builder: (_) => UpdatePpm(ppm: null, planPreventiveVisit: planPreventiveVisit))); AppFilledButton(
getVisitData(); onPressed: () async {
}, await Navigator.of(context).push(MaterialPageRoute(builder: (_) => UpdatePpm(ppm: null, planPreventiveVisit: planPreventiveVisit)));
label: context.translation.updateWorkOrder, getVisitData();
},
label: userProvider!.user!.type == UsersTypes.engineer && (!ppmProvider.isReadOnly)? context.translation.updateWorkOrder:context.translation.viewDetails,
),
],
), ),
context: context, context: context,
) )
] else ...[ // ]
// FooterActionButton.footerContainer(
// context: context,
// child: AppFilledButton(
// onPressed: () async {
// await Navigator.of(context).push(MaterialPageRoute(builder: (_) => UpdatePpm(ppm: null, planPreventiveVisit: planPreventiveVisit)));
// getVisitData();
// },
// label: context.translation.viewDetails,
// ),
// )
]
]) ])
: const Center(child: NoDataFound()); : const Center(child: NoDataFound());
}), }),

@ -14,6 +14,7 @@ import 'package:test_sa/models/plan_preventive_visit/plan_preventive_visit_model
import 'package:test_sa/models/ppm/ppm.dart'; import 'package:test_sa/models/ppm/ppm.dart';
import 'package:test_sa/modules/cm_module/cm_request_utils.dart'; import 'package:test_sa/modules/cm_module/cm_request_utils.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart'; import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/modules/pm_module/ppm_wo/activities/pm_activities_list_view.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';
import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart'; import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart';
@ -44,7 +45,6 @@ class _UpdatePpmState extends State<UpdatePpm> with TickerProviderStateMixin {
TabController? _tabController; TabController? _tabController;
late PpmProvider ppmProvider; late PpmProvider ppmProvider;
_onSubmit({required int status}) async { _onSubmit({required int status}) async {
if (ppmProvider.validate()) { if (ppmProvider.validate()) {
showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading()); showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading());
@ -94,27 +94,33 @@ class _UpdatePpmState extends State<UpdatePpm> with TickerProviderStateMixin {
@override @override
void initState() { void initState() {
super.initState();
ppmProvider = Provider.of<PpmProvider>(context, listen: false); ppmProvider = Provider.of<PpmProvider>(context, listen: false);
_planPreventiveVisit = widget.planPreventiveVisit; _planPreventiveVisit = widget.planPreventiveVisit;
// WidgetsBinding.instance.addPostFrameCallback((_) { int initialTabs = (_planPreventiveVisit.typeOfService?.value == 2) ? 4 : 3;
initTabs(_planPreventiveVisit.typeOfService); _tabController = TabController(length: initialTabs, vsync: this);
// }); WidgetsBinding.instance.addPostFrameCallback((_) {
super.initState(); ppmProvider.totalTabs = initialTabs;
});
} }
void initTabs(typeOfService) { void initTabs(typeOfService) {
if (typeOfService == null) { if (typeOfService == null) {
return; return;
} }
if (typeOfService?.value == 2) {
ppmProvider.totalTabs = 4; int newTabCount = (typeOfService?.value == 2) ? 4 : 3;
} else {
ppmProvider.totalTabs = 3; // Only recreate TabController if tab count has changed
if (_tabController?.length != newTabCount) {
_tabController?.dispose();
_tabController = TabController(length: newTabCount, vsync: this);
// Update provider safely after current build
WidgetsBinding.instance.addPostFrameCallback((_) {
ppmProvider.totalTabs = newTabCount;
});
} }
_tabController?.dispose;
_tabController = TabController(length: ppmProvider.totalTabs, vsync: this);
} }
int tabIndex = 0; int tabIndex = 0;
@ -208,70 +214,82 @@ class _UpdatePpmState extends State<UpdatePpm> with TickerProviderStateMixin {
).expanded, ).expanded,
FooterActionButton.footerContainer( FooterActionButton.footerContainer(
context: context, context: context,
child: Row( child: Column(
children: [ children: [
if (tabIndex == 1) ...[ Row(
AppFilledButton( children: [
buttonColor: _tabController!.index == 0 ? null : AppColor.background(context), if (tabIndex == 1) ...[
// buttonColor: _tabController!.index == 0 ? null : AppColor.background(context), AppFilledButton(
textColor: _tabController!.index == 0 ? null : AppColor.blueStatus(context), buttonColor: _tabController!.index == 0 ? null : AppColor.background(context),
showBorder: true, // buttonColor: _tabController!.index == 0 ? null : AppColor.background(context),
disableButton: _tabController!.index == 0, textColor: _tabController!.index == 0 ? null : AppColor.blueStatus(context),
onPressed: () { showBorder: true,
if (_tabController!.index == 0) { disableButton: _tabController!.index == 0,
return; onPressed: () {
} if (_tabController!.index == 0) {
_tabController?.animateTo(_tabController!.index - 1); return;
setState(() {}); }
return; _tabController?.animateTo(_tabController!.index - 1);
}, setState(() {});
label: context.translation.back,
).expanded,
8.width,
],
if (!ppmProvider.isReadOnly) ...[
AppFilledButton(
// buttonColor: AppColor.white60,
// textColor: AppColor.neutral50,
buttonColor: context.isDark ? AppColor.neutral50 : AppColor.white60,
textColor: context.isDark ? Colors.white : AppColor.neutral50,
onPressed: () {
_onSubmit(status: 0);
},
label: context.translation.save,
).expanded,
8.width,
AppFilledButton(
onPressed: () {
if (tabIndex == 0) {
_onSubmit(status: 1);
return;
}
if (ppmProvider.totalTabs == _tabController!.index + 1) {
_onSubmit(status: 1);
} else {
_tabController?.animateTo(_tabController!.index + 1);
setState(() {});
}
},
label: tabIndex == 0 ? "Complete".addTranslation : ((ppmProvider.totalTabs == _tabController!.index + 1) ? "Complete".addTranslation : context.translation.next),
).expanded,
] else ...[
Visibility(
visible: tabIndex != 0,
child: AppFilledButton(
onPressed: () {
if (ppmProvider.totalTabs == _tabController!.index + 1) {
Navigator.pop(context);
return; return;
} },
_tabController?.animateTo(_tabController!.index + 1); label: context.translation.back,
setState(() {}); ).expanded,
}, 8.width,
label: ppmProvider.totalTabs == _tabController!.index + 1 ? context.translation.close : context.translation.next, ],
).expanded, if (!ppmProvider.isReadOnly) ...[
), AppFilledButton(
] // buttonColor: AppColor.white60,
// textColor: AppColor.neutral50,
buttonColor: context.isDark ? AppColor.neutral50 : AppColor.white60,
textColor: context.isDark ? Colors.white : AppColor.neutral50,
onPressed: () {
_onSubmit(status: 0);
},
label: context.translation.save,
).expanded,
8.width,
AppFilledButton(
onPressed: () {
if (tabIndex == 0) {
_onSubmit(status: 1);
return;
}
if (ppmProvider.totalTabs == _tabController!.index + 1) {
_onSubmit(status: 1);
} else {
_tabController?.animateTo(_tabController!.index + 1);
setState(() {});
}
},
label: tabIndex == 0 ? "Complete".addTranslation : ((ppmProvider.totalTabs == _tabController!.index + 1) ? "Complete".addTranslation : context.translation.next),
).expanded,
] else ...[
Visibility(
visible: tabIndex != 0,
child: AppFilledButton(
onPressed: () {
if (ppmProvider.totalTabs == _tabController!.index + 1) {
Navigator.pop(context);
return;
}
_tabController?.animateTo(_tabController!.index + 1);
setState(() {});
},
label: ppmProvider.totalTabs == _tabController!.index + 1 ? context.translation.close : context.translation.next,
).expanded,
),
]
],
),
12.height,
AppFilledButton(
onPressed: () async {
Navigator.of(context).push(MaterialPageRoute(builder: (_) => const PmActivitiesListView()));
},
buttonColor: context.isDark ? AppColor.primary10 : AppColor.neutral50,
label: "Spare Parts",
),
], ],
), ),
), ),

@ -13,6 +13,7 @@ import 'package:test_sa/models/device/asset.dart';
import 'package:test_sa/models/device/model_definition.dart'; import 'package:test_sa/models/device/model_definition.dart';
import 'package:test_sa/models/device/supplier.dart'; import 'package:test_sa/models/device/supplier.dart';
import 'package:test_sa/models/generic_attachment_model.dart'; import 'package:test_sa/models/generic_attachment_model.dart';
import 'package:test_sa/models/helper_data_models/workorder/work_order_helper_models.dart';
import 'package:test_sa/models/lookup.dart'; import 'package:test_sa/models/lookup.dart';
import 'package:test_sa/models/new_models/building.dart'; import 'package:test_sa/models/new_models/building.dart';
import 'package:test_sa/models/new_models/department.dart'; import 'package:test_sa/models/new_models/department.dart';
@ -21,6 +22,7 @@ import 'package:test_sa/models/new_models/room_model.dart';
import 'package:test_sa/models/new_models/site.dart'; import 'package:test_sa/models/new_models/site.dart';
import 'package:test_sa/models/plan_preventive_visit/plan_preventive_visit_model.dart'; import 'package:test_sa/models/plan_preventive_visit/plan_preventive_visit_model.dart';
import 'package:test_sa/modules/cm_module/cm_request_utils.dart'; import 'package:test_sa/modules/cm_module/cm_request_utils.dart';
import 'package:test_sa/modules/cm_module/views/forms/cost/cost_detail_form_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/common_widgets/app_text_form_field.dart'; import 'package:test_sa/new_views/common_widgets/app_text_form_field.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';
@ -104,6 +106,7 @@ class _WoInfoFormState extends State<WoInfoForm> {
IgnorePointer( IgnorePointer(
ignoring: ppmProvider.isReadOnly, ignoring: ppmProvider.isReadOnly,
child: Column( child: Column(
spacing: 12,
children: [ children: [
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -145,7 +148,6 @@ class _WoInfoFormState extends State<WoInfoForm> {
// '${context.translation.executionTimeFrame}: ${widget.planPreventiveVisit.executionTimeFrame ?? ""} days'.bodyText2(context).custom(color: AppColor.neutral120), // '${context.translation.executionTimeFrame}: ${widget.planPreventiveVisit.executionTimeFrame ?? ""} days'.bodyText2(context).custom(color: AppColor.neutral120),
], ],
).toShadowContainer(context), ).toShadowContainer(context),
12.height,
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -219,9 +221,10 @@ class _WoInfoFormState extends State<WoInfoForm> {
// } // }
// }, // },
// ), // ),
8.height, // 8.height,//
Column( Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
spacing: 12,
children: [ children: [
SingleItemDropDownMenu<Lookup, PpmTaskStatusProvider>( SingleItemDropDownMenu<Lookup, PpmTaskStatusProvider>(
context: context, context: context,
@ -236,7 +239,6 @@ class _WoInfoFormState extends State<WoInfoForm> {
} }
}, },
), ),
8.height,
ADatePicker( ADatePicker(
label: context.translation.actualVisit, label: context.translation.actualVisit,
hideShadow: true, hideShadow: true,
@ -283,8 +285,6 @@ class _WoInfoFormState extends State<WoInfoForm> {
// } // }
// }, // },
// ), // ),
8.height,
SingleItemDropDownMenu<Lookup, PpmElectricalSafetyProvider>( SingleItemDropDownMenu<Lookup, PpmElectricalSafetyProvider>(
context: context, context: context,
backgroundColor: AppColor.fieldBgColor(context), backgroundColor: AppColor.fieldBgColor(context),
@ -297,7 +297,6 @@ class _WoInfoFormState extends State<WoInfoForm> {
} }
}, },
), ),
8.height,
SingleItemDropDownMenu<Lookup, PpmAssetAvailabilityProvider>( SingleItemDropDownMenu<Lookup, PpmAssetAvailabilityProvider>(
context: context, context: context,
backgroundColor: AppColor.fieldBgColor(context), backgroundColor: AppColor.fieldBgColor(context),
@ -312,7 +311,6 @@ class _WoInfoFormState extends State<WoInfoForm> {
} }
}, },
), ),
8.height,
SingleItemDropDownMenu<Lookup, PpmServiceProvider>( SingleItemDropDownMenu<Lookup, PpmServiceProvider>(
context: context, context: context,
backgroundColor: AppColor.fieldBgColor(context), backgroundColor: AppColor.fieldBgColor(context),
@ -333,9 +331,7 @@ class _WoInfoFormState extends State<WoInfoForm> {
} }
}, },
), ),
8.height,
_timerWidget(context, totalWorkingHours), _timerWidget(context, totalWorkingHours),
8.height,
AppTextFormField( AppTextFormField(
labelText: context.translation.comment, labelText: context.translation.comment,
backgroundColor: AppColor.fieldBgColor(context), backgroundColor: AppColor.fieldBgColor(context),
@ -347,7 +343,6 @@ class _WoInfoFormState extends State<WoInfoForm> {
widget.planPreventiveVisit.comments = value; widget.planPreventiveVisit.comments = value;
}, },
), ),
16.height,
AttachmentPicker( AttachmentPicker(
label: context.translation.attachments, label: context.translation.attachments,
attachment: ppmProvider.ppmPlanAttachments, attachment: ppmProvider.ppmPlanAttachments,
@ -357,6 +352,7 @@ class _WoInfoFormState extends State<WoInfoForm> {
), ),
], ],
).toShadowContainer(context), ).toShadowContainer(context),
costCard(context, ppmProvider),
], ],
), ),
), ),
@ -413,6 +409,7 @@ class _WoInfoFormState extends State<WoInfoForm> {
Widget _timerWidget(BuildContext context, double totalWorkingHours) { Widget _timerWidget(BuildContext context, double totalWorkingHours) {
return Column( return Column(
spacing: 12,
children: [ children: [
AppTimer( AppTimer(
label: context.translation.workingHours, label: context.translation.workingHours,
@ -434,7 +431,6 @@ class _WoInfoFormState extends State<WoInfoForm> {
return true; return true;
}, },
), ),
11.height,
if (totalWorkingHours > 0.0) ...[ if (totalWorkingHours > 0.0) ...[
WorkingTimeTile( WorkingTimeTile(
timerList: timerList, timerList: timerList,
@ -454,6 +450,79 @@ class _WoInfoFormState extends State<WoInfoForm> {
); );
} }
Widget costCard(BuildContext context, PpmProvider provider) {
///Need to update this when actual flow decided...
PlanPreventiveVisit? model = provider.planPreventiveVisit;
final costModel = WorkOrderCostModel(
workOrderId: int.tryParse(model?.id ?? ''),
labourCost: model?.labourCost,
sparePartCost: model?.sparePartCost,
travelCost: model?.travelCost,
qAmount: model?.qAmount,
prNo: model?.prNo,
poNo: model?.poNo,
mrNo: model?.mrNo,
exchangeCost: model?.exchangeCost,
);
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
InfoHeader16Widget("Cost"),
const Spacer(),
if (!provider.isReadOnlyRequest)
"edit_icon".toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)).onPress(() async {
provider.workOrderCostModel = costModel;
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CostDetailFormScreen(
model: provider.workOrderCostModel,
provider: provider,
),
),
);
}),
],
),
InfoTextWidget(
label: 'Spare Part Cost'.addTranslation,
value: costModel.sparePartCost?.toString() ?? '-',
),
InfoTextWidget(
label: 'Labor Cost'.addTranslation,
value: costModel.labourCost?.toString() ?? '-',
),
InfoTextWidget(
label: 'Travel Cost'.addTranslation,
value: costModel.travelCost?.toString() ?? '-',
),
InfoTextWidget(
label: 'Quot Amount'.addTranslation,
value: costModel.qAmount?.toString() ?? '-',
),
InfoTextWidget(
label: 'PR No'.addTranslation,
value: costModel.prNo?.toString() ?? '-',
),
InfoTextWidget(
label: 'PO No'.addTranslation,
value: costModel.poNo?.toString() ?? '-',
),
InfoTextWidget(
label: 'MR No'.addTranslation,
value: costModel.mrNo?.toString() ?? '-',
),
InfoTextWidget(
label: 'Exchange Cost'.addTranslation,
value: costModel.exchangeCost?.toString() ?? '-',
),
],
).toShadowContainer(context);
}
// String formatDuration1(int seconds) { // String formatDuration1(int seconds) {
// int hours = seconds ~/ 3600; // int hours = seconds ~/ 3600;
// int minutes = (seconds % 3600) ~/ 60; // int minutes = (seconds % 3600) ~/ 60;

@ -27,6 +27,7 @@ class SiteRequestProvider extends ChangeNotifier {
// Data // Data
List<RequestTypeModel> _requestTypes = []; List<RequestTypeModel> _requestTypes = [];
List<SiteRequestSiteModel> _userCurrentSites = []; List<SiteRequestSiteModel> _userCurrentSites = [];
List<SiteRequestSiteModel> userFilterSites = [];
List<SiteRequestModel> _requestHistory = []; List<SiteRequestModel> _requestHistory = [];
// Pagination // Pagination
@ -106,6 +107,24 @@ class SiteRequestProvider extends ChangeNotifier {
); );
} }
/// Fetches user's current sites for Change/Remove Site operations
Future<List<SiteRequestSiteModel>> getSitesByUserId(String userId) async {
List<SiteRequestSiteModel> usersSite = [];
try {
Response response = await ApiManager.instance.post(URLs.getUserSites(userId), body: {},showToast: false);
if (response.statusCode >= 200 && response.statusCode < 300) {
List categoriesListJson = json.decode(response.body)["data"];
usersSite = categoriesListJson
.map((item) => SiteRequestSiteModel(
id: item['siteId']?.toString() ?? '',
name: item['siteName'] ?? '',
))
.toList();
}
} catch (ex) {}
return usersSite;
}
/// Fetches user's current sites for Change/Remove Site operations /// Fetches user's current sites for Change/Remove Site operations
Future<int> fetchUserCurrentSites(String userId) async { Future<int> fetchUserCurrentSites(String userId) async {
if (_loading) return -2; if (_loading) return -2;
@ -125,6 +144,24 @@ class SiteRequestProvider extends ChangeNotifier {
); );
} }
Future<int> fetchUserFilteredSites(String userId) async {
if (_loading) return -2;
return _performApiCall(
apiCall: () => ApiManager.instance.post(URLs.getSitesNotAccessibleForUser(userId), showToast: false, body: {}),
onSuccess: (responseData) {
List listJson = responseData['data'] ?? [];
userFilterSites = listJson
.map((item) => SiteRequestSiteModel(
id: item['siteId']?.toString() ?? '',
name: item['siteName'] ?? '',
))
.toList();
notifyListeners();
},
errorMessage: 'Failed to fetch user sites',
);
}
/// Submits a site request to the server /// Submits a site request to the server
Future<bool> submitSiteRequest(SiteRequestModel request, UserProvider userProvider) async { Future<bool> submitSiteRequest(SiteRequestModel request, UserProvider userProvider) async {
loading = true; loading = true;

@ -175,11 +175,12 @@ class _SiteRequestFormScreenState extends State<SiteRequestFormScreen> {
_siteReplacements.clear(); _siteReplacements.clear();
_loadedDepartmentSiteIds.clear(); // Reset loaded department site IDs _loadedDepartmentSiteIds.clear(); // Reset loaded department site IDs
}); });
final userId = _provider.getCurrentUserUserId(_userProvider);
// Fetch user's current sites if Change Site or Remove Site is selected // Fetch user's current sites if Change Site or Remove Site is selected
if ((_isChangeSiteRequest() || _isRemoveSiteRequest()) && value != null) { if ((_isChangeSiteRequest() || _isRemoveSiteRequest()) && value != null) {
final userId = _provider.getCurrentUserUserId(_userProvider);
await _provider.fetchUserCurrentSites(userId); await _provider.fetchUserCurrentSites(userId);
}else {
await _provider.fetchUserFilteredSites(userId);
} }
// Fetch all sites if Change Site is selected (needed for new site dropdown) // Fetch all sites if Change Site is selected (needed for new site dropdown)
@ -318,6 +319,40 @@ class _SiteRequestFormScreenState extends State<SiteRequestFormScreen> {
} }
Widget _buildSiteMultiSelect() { Widget _buildSiteMultiSelect() {
return MultipleItemDropDownMenu<SiteRequestSiteModel, NullableLoadingProvider>(
context: context,
title: context.translation.site,
initialValue: _selectedSites
.map((s) => SiteRequestSiteModel(
id: s.identifier ?? '',
name: s.name ?? '',
))
.toList(),
staticData: _provider.userFilterSites,
showAsFullScreenDialog: true,
backgroundColor: context.isDark ? AppColor.neutral60 : Colors.white,
showShadow: true,
height: 56.toScreenHeight,
showCancel: true,
onSelect: (value) {
if (value != null) {
setState(() {
_selectedSites = value.map((s) {
final site = Site(
id: int.tryParse(s.identifier ?? '0'),
custName: s.name,
);
return site;
}).toList();
final departmentProvider = Provider.of<DepartmentBySitesProvider>(context, listen: false);
final siteIds = value.map((site) => int.tryParse(site.identifier ?? '0') ?? 0).toList();
departmentProvider.setSiteIds(siteIds);
departmentProvider.getData();
});
}
},
);
return MultipleItemDropDownMenu<Site, SiteProvider>( return MultipleItemDropDownMenu<Site, SiteProvider>(
context: context, context: context,
title: context.translation.site, title: context.translation.site,

@ -155,8 +155,9 @@ class _CreateDeviceTransferRequestState extends State<CreateDeviceTransferReques
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
SingleItemDropDownMenu<Site, SiteProvider>( SingleItemDropDownMenu<Site, SameOrgSiteProvider>(
context: context, context: context,
requestById: context.userProvider.user?.clientId,
title: context.translation.site, title: context.translation.site,
initialValue: _assetDestination.site, initialValue: _assetDestination.site,
disableValue: isInternal ? null : _pickedAsset?.site, disableValue: isInternal ? null : _pickedAsset?.site,

@ -5,17 +5,17 @@ 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/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/helper/utils.dart';
import 'package:test_sa/models/lookup.dart'; import 'package:test_sa/models/lookup.dart';
import 'package:test_sa/models/new_models/site.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart'; import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/modules/site_request_module/models/dropdown_models.dart';
import 'package:test_sa/modules/site_request_module/providers/site_request_provider.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';
import 'package:test_sa/new_views/common_widgets/multiple_item_drop_down_menu.dart'; import 'package:test_sa/new_views/common_widgets/multiple_item_drop_down_menu.dart';
import 'package:test_sa/providers/department_provider.dart'; import 'package:test_sa/providers/department_provider.dart';
import 'package:test_sa/providers/gas_request_providers/site_provider.dart'; import 'package:test_sa/providers/loading_list_notifier.dart';
import 'package:test_sa/views/widgets/date_and_time/date_picker.dart'; import 'package:test_sa/views/widgets/date_and_time/date_picker.dart';
import 'package:test_sa/views/widgets/equipment/asset_status_buttons.dart';
import '../../../models/device/asset_search.dart'; import '../../../models/device/asset_search.dart';
import '../../../new_views/common_widgets/app_lazy_loading.dart'; import '../../../new_views/common_widgets/app_lazy_loading.dart';
import '../../../new_views/common_widgets/default_app_bar.dart'; import '../../../new_views/common_widgets/default_app_bar.dart';
@ -33,11 +33,13 @@ class DeviceFilterScreen extends StatefulWidget {
class _DeviceFilterScreenState extends State<DeviceFilterScreen> { class _DeviceFilterScreenState extends State<DeviceFilterScreen> {
late DeviceSearch filter; late DeviceSearch filter;
Lookup? _commissioningStatus; Lookup? _commissioningStatus;
List<Site> _selectedSites = []; List<SiteRequestSiteModel> _selectedSites = [];
List<Lookup> _selectedDepartments = []; List<Lookup> _selectedDepartments = [];
String? startDate, endDate; String? startDate, endDate;
bool loading = false; bool loading = false;
List<SiteRequestSiteModel> userSites = [];
@override @override
void initState() { void initState() {
filter = widget.data ?? DeviceSearch(); filter = widget.data ?? DeviceSearch();
@ -47,11 +49,15 @@ class _DeviceFilterScreenState extends State<DeviceFilterScreen> {
startDate = filter.supplyDateFrom; startDate = filter.supplyDateFrom;
endDate = filter.supplyDateTo; endDate = filter.supplyDateTo;
_commissioningStatus = filter.assetStatus; _commissioningStatus = filter.assetStatus;
final departmentProvider = Provider.of<DepartmentProvider>(context, listen: false); final departmentProvider = Provider.of<DepartmentProvider>(context, listen: false);
final siteProvider = Provider.of<SiteProvider>(context, listen: false); Utils.showLoading(context);
if (departmentProvider.items.isEmpty ?? true) await departmentProvider.getData(); if (departmentProvider.items.isEmpty ?? true) await departmentProvider.getData();
if (siteProvider.items.isEmpty ?? true) await siteProvider.getData(); if (userSites.isEmpty) {
userSites = await Provider.of<SiteRequestProvider>(context, listen: false).getSitesByUserId(context.userProvider.user?.userID ?? "");
}
await Future.delayed(const Duration(milliseconds: 100));
Utils.hideLoading(context);
// Initialize selected departments from filter // Initialize selected departments from filter
if (filter.departments != null && filter.departments!.isNotEmpty) { if (filter.departments != null && filter.departments!.isNotEmpty) {
_selectedDepartments = []; _selectedDepartments = [];
@ -70,11 +76,11 @@ class _DeviceFilterScreenState extends State<DeviceFilterScreen> {
if (filter.sites != null && filter.sites!.isNotEmpty) { if (filter.sites != null && filter.sites!.isNotEmpty) {
_selectedSites = []; _selectedSites = [];
for (var siteName in filter.sites!) { for (var siteName in filter.sites!) {
final match = siteProvider.items.firstWhere( final match = userSites.firstWhere(
(site) => site.name == siteName, (site) => site.name == siteName,
orElse: () => Site(), orElse: () => SiteRequestSiteModel(id: '', name: ''),
); );
if (match.id != null) { if (match.identifier != null) {
_selectedSites.add(match); _selectedSites.add(match);
} }
} }
@ -117,14 +123,15 @@ class _DeviceFilterScreenState extends State<DeviceFilterScreen> {
ListView( ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
children: [ children: [
AssetStatusButtons( // AssetStatusButtons(
initialValue: _commissioningStatus, // initialValue: _commissioningStatus,
onSelect: (value) { // onSelect: (value) {
if (value != null) { // if (value != null) {
_commissioningStatus = value; // _commissioningStatus = value;
} // }
}, // },
), // ),
context.translation.filter.heading5(context).custom(color: AppColor.textStyleColor(context)),
16.height, 16.height,
MultipleItemDropDownMenu<Lookup, DepartmentProvider>( MultipleItemDropDownMenu<Lookup, DepartmentProvider>(
context: context, context: context,
@ -145,11 +152,12 @@ class _DeviceFilterScreenState extends State<DeviceFilterScreen> {
}, },
), ),
12.height, 12.height,
MultipleItemDropDownMenu<Site, SiteProvider>( MultipleItemDropDownMenu<SiteRequestSiteModel, NullableLoadingProvider>(
context: context, context: context,
title: context.translation.bySite, title: context.translation.bySite,
initialValue: _selectedSites, initialValue: _selectedSites,
showAsBottomSheet: true, showAsBottomSheet: true,
staticData: userSites,
// backgroundColor: AppColor.fieldBgColor(context), // backgroundColor: AppColor.fieldBgColor(context),
backgroundColor: context.isDark ? AppColor.neutral60 : Colors.white, backgroundColor: context.isDark ? AppColor.neutral60 : Colors.white,
showShadow: true, showShadow: true,

@ -67,30 +67,32 @@ class _TrafRequestDetailPageState extends State<TrafRequestDetailPage> {
} }
return trafProvider.trafRequestDataModel == null return trafProvider.trafRequestDataModel == null
? const NoDataFound().center ? const NoDataFound().center
: Column( : SafeArea(
children: [ child: Column(
ListView( children: [
padding: const EdgeInsets.all(16), ListView(
children: [ padding: const EdgeInsets.all(16),
requesterInformation(trafProvider.trafRequestDataModel!), children: [
12.height, requesterInformation(trafProvider.trafRequestDataModel!),
requestInformation(trafProvider.trafRequestDataModel!), 12.height,
12.height, requestInformation(trafProvider.trafRequestDataModel!),
assetInformation(trafProvider.trafRequestDataModel!), 12.height,
], assetInformation(trafProvider.trafRequestDataModel!),
).expanded, ],
// // if (isEngineer) ).expanded,
// FooterActionButton.footerContainer( // // if (isEngineer)
// context: context, // FooterActionButton.footerContainer(
// child: AppFilledButton( // context: context,
// buttonColor: AppColor.primary10, // child: AppFilledButton(
// label: "Update", // buttonColor: AppColor.primary10,
// onPressed: () { // label: "Update",
// Navigator.pushNamed(context, UpdateTrafRequestPage.id); // onPressed: () {
// }), // Navigator.pushNamed(context, UpdateTrafRequestPage.id);
// ), // }),
], // ),
); ],
),
);
}, },
)); ));
} }

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

@ -41,7 +41,7 @@ class InfoDialog extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Expanded(
child: InfoHeader16Widget("Information").paddingOnly(top: 16), child: InfoHeader16Widget(title ?? "Information").paddingOnly(top: 16),
// Text( // Text(
// title ?? "Information", // title ?? "Information",
// style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: AppColor.headingTextColor(context), height: 35 / 24, letterSpacing: -0.96), // style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: AppColor.headingTextColor(context), height: 35 / 24, letterSpacing: -0.96),

@ -33,6 +33,31 @@ class SiteProvider extends LoadingListNotifier<Site> {
} }
} }
class SameOrgSiteProvider extends LoadingListNotifier<Site> {
@override
Future getData({int? id}) async {
if (loading ?? false) return -2;
loading = true;
notifyListeners();
try {
Response response = await ApiManager.instance.get(URLs.getCustomersAutoCompleteSameOrg + "?senderSiteId=$id");
stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) {
List categoriesListJson = json.decode(response.body)["data"];
items = categoriesListJson.map((item) => Site.fromJson(item)).toList();
}
loading = false;
notifyListeners();
return response.statusCode;
} catch (error) {
loading = false;
stateCode = -1;
notifyListeners();
return -1;
}
}
}
class MappedSiteProvider extends LoadingListNotifier<MappedSite> { class MappedSiteProvider extends LoadingListNotifier<MappedSite> {
@override @override
Future getData({int? id}) async { Future getData({int? id}) async {

@ -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( child: ListView.separated(
padding: const EdgeInsets.all(16), itemCount: notifications.length,
child: ListView.separated( separatorBuilder: (context, itemIndex) => 12.height,
shrinkWrap: true, itemBuilder: (context, itemIndex) {
physics: const NeverScrollableScrollPhysics(), final notification = notifications[itemIndex];
padding: const EdgeInsets.all(0), return NotificationCardRedesigned(
itemCount: notifications.length, notification: notification,
separatorBuilder: (context, itemIndex) => const Divider().defaultStyle(context).paddingOnly(top: 4, bottom: 4), onTap: () async {
itemBuilder: (context, itemIndex) { if (notification.id != null && !(notification.readed ?? false)) {
return NotificationItem( final provider = Provider.of<NotificationsProvider>(context, listen: false);
notification: notifications[itemIndex], await provider.markOneAsRead(notificationId: notification.id!);
onPressed: (notification) { }
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,40 +32,291 @@ 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(
body: SafeArea( title: context.translation.notifications,
child: LoadingManager( actions: [
isLoading: (_notificationsProvider.isLoading ?? false) && _notificationsProvider.notifications.isEmpty, TextButton.icon(
isFailedLoading: _notificationsProvider.notifications == null, onPressed: _unreadCount > 0
stateCode: _notificationsProvider.stateCode, ? () async {
onRefresh: () async { showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading());
_notificationsProvider.reset(); final result = await _notificationsProvider.markAllAsRead(
await _notificationsProvider.getSystemNotifications(user: _userProvider.user!, resetProvider: true userId: _userProvider.user!.userID!,
);
// host: _settingProvider.host, if (context.mounted) {
// hospitalId: _userProvider.user.clientId, Navigator.of(context).pop();
); }
}, if (context.mounted && result['message'] != null) {
child: NotificationsList( '${result['message']}'.showToast;
nextPage: _notificationsProvider.nextPage, }
onLazyLoad: () async { }
await _notificationsProvider.getSystemNotifications( : null,
user: _userProvider.user!, icon: 'Icon_mark_read'.toSvgAsset(
// host: _settingProvider.host, width: 12,
// hospitalId: _userProvider.user.clientId, height: 12,
); color: AppColor.primary10,
}, ),
notifications: _notificationsProvider.notifications, 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(
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(
isLoading: (_notificationsProvider.isLoading) && _notificationsProvider.notifications.isEmpty,
isFailedLoading: false,
stateCode: _notificationsProvider.stateCode,
onRefresh: () async {
_notificationsProvider.reset();
await _notificationsProvider.getSystemNotifications(
user: _userProvider.user!,
resetProvider: true,
filters: _activeFilters,
);
},
child: NotificationsList(
nextPage: _notificationsProvider.nextPage,
onLazyLoad: () async {
await _notificationsProvider.getSystemNotifications(
user: _userProvider.user!,
filters: _activeFilters,
);
},
notifications: _filteredNotifications,
),
),
),
],
).paddingOnly(start: 16, end: 16, 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();

@ -1,5 +1,6 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
class ImageLoader extends StatelessWidget { class ImageLoader extends StatelessWidget {
final String? url; final String? url;
@ -20,7 +21,7 @@ class ImageLoader extends StatelessWidget {
alignment: Alignment.center, alignment: Alignment.center,
width: width, width: width,
height: height, height: height,
placeholder: (context, url) => const Center(child: CircularProgressIndicator()), placeholder: (context, url) => Center(child: CircularProgressIndicator(color: AppColor.loadingColor(context))),
errorWidget: (context, url, error) => Icon(showDefaultIcon ? Icons.image_outlined : Icons.broken_image_rounded), errorWidget: (context, url, error) => Icon(showDefaultIcon ? Icons.image_outlined : Icons.broken_image_rounded),
); );
} }

@ -158,6 +158,7 @@ class _AppTimerState extends State<AppTimer> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
spacing: 12,
children: [ children: [
if (canPickTime) ...[ if (canPickTime) ...[
Row( Row(
@ -228,7 +229,6 @@ class _AppTimerState extends State<AppTimer> {
], ],
), ),
if (_tempPickerTimer != null) ...[ if (_tempPickerTimer != null) ...[
8.height,
Row( Row(
children: [ children: [
Column( Column(
@ -248,7 +248,7 @@ class _AppTimerState extends State<AppTimer> {
], ],
).toShadowContainer(context) ).toShadowContainer(context)
], ],
8.height, // 8.height,
], ],
if (widget.showTimer) if (widget.showTimer)
Container( Container(

@ -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.5+55
environment: environment:
sdk: ">=3.5.0 <4.0.0" sdk: ">=3.5.0 <4.0.0"

Loading…
Cancel
Save