diff --git a/ios/Podfile b/ios/Podfile index 5596a08a..72d600e0 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # 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. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -40,7 +40,7 @@ post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) 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 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| project.targets.each do |target| 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'] ||= [ '$(inherited)', ## dart: PermissionGroup.microphone diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 3c09fbf5..e0fcb9d1 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -379,7 +379,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -403,7 +403,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Atoms; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -534,7 +534,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -587,7 +587,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -613,7 +613,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Atoms; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -709,7 +709,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Atoms; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart index 85a70b8e..040259cc 100644 --- a/lib/controllers/api_routes/urls.dart +++ b/lib/controllers/api_routes/urls.dart @@ -1,7 +1,7 @@ class URLs { URLs._(); - static const String appReleaseBuildNumber = "53"; + static const String appReleaseBuildNumber = "55"; // static const host1 = "https://atomsm.hmg.com"; // production 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 getDepartmentsBySites => "$_baseUrl/Customer/GetDepartmentsBySites"; // post get departments by 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 register => "$_baseUrl/handle/create/user"; // post @@ -254,8 +256,13 @@ class URLs { static get getRegularVisits => "$_baseUrl/Visit/GetVisits"; // get static get updateRegularVisits => "$_baseUrl/Visit/UpdateVisits"; // get - static get getPlanPreventiveVisitById => "$_baseUrl/PlanPreventiveVisit/GetPlanPreventiveVisitById"; // get - static get updateVisitByEngineer => "$_baseUrl/PlanPreventiveVisit/UpdateVisitByEngineer"; // get + static get getPlanPreventiveVisitById => "$_baseUrl/PlanPreventiveVisit/GetPlanPreventiveVisitById"; + 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 getSuppliersAutoComplete => "$_baseUrl/Supplier/GetSuppliersAutoComplete"; @@ -625,4 +632,8 @@ class URLs { 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 verifyDemoUserOtp => "$_baseUrl/DemoUser/VerifyOtp"; // post verify OTP + +// spare part request endpoint. + + } diff --git a/lib/controllers/providers/api/parts_provider.dart b/lib/controllers/providers/api/parts_provider.dart index fc2829b8..e50b7a7b 100644 --- a/lib/controllers/providers/api/parts_provider.dart +++ b/lib/controllers/providers/api/parts_provider.dart @@ -88,32 +88,39 @@ class PartsProvider extends ChangeNotifier { late Response response; try { if (partNo != null) { - response = await ApiManager.instance.post(URLs.getPartNumber, body: {"partNo": partNo, }); + response = await ApiManager.instance.post(URLs.getPartNumber, body: { + "partNo": partNo, + }); } else { - response = await ApiManager.instance.post(URLs.getPartNumber, body: {"partName": partName, }); + response = await ApiManager.instance.post(URLs.getPartNumber, body: { + "partName": partName, + }); } List page = []; if (response.statusCode >= 200 && response.statusCode < 300) { // client's request was successfully received 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; } catch (error) { return []; } } + // Implement this only for spare part request for now show and search on display name .... Future> getPartsListByDisplayName({num? assetId, String? displayName}) async { late Response response; try { - response = await ApiManager.instance.post(URLs.getPartNumber, body: {"displayName": displayName, }); + response = await ApiManager.instance.post(URLs.getPartNumber, body: { + "displayName": displayName, + }); List page = []; if (response.statusCode >= 200 && response.statusCode < 300) { // client's request was successfully received 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; } catch (error) { diff --git a/lib/controllers/providers/api/ppm_provider.dart b/lib/controllers/providers/api/ppm_provider.dart index 81a7e066..7861b5b1 100644 --- a/lib/controllers/providers/api/ppm_provider.dart +++ b/lib/controllers/providers/api/ppm_provider.dart @@ -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/extensions/context_extension.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/ppm/ppm.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 '../../../new_views/common_widgets/app_lazy_loading.dart'; @@ -20,6 +23,15 @@ class PpmProvider extends ChangeNotifier { // number of items call in each request final pageItemNumber = 12; + bool _isReadOnlyRequest = false; + + bool get isReadOnlyRequest => _isReadOnlyRequest; + + set isReadOnlyRequest(bool value) { + _isReadOnlyRequest = value; + notifyListeners(); + } + PlanPreventiveVisit? _planPreventiveVisit; late int _totalTabs = 4; TabController? tabController; @@ -61,6 +73,15 @@ class PpmProvider extends ChangeNotifier { stateCode = null; } + List _storeAvailability = []; + + List get storeAvailability => _storeAvailability; + + set storeAvailability(List value) { + _storeAvailability = value; + notifyListeners(); + } + // state code of current request to defied error message // like 400 customer request failed // 500 service not available @@ -78,6 +99,13 @@ class PpmProvider extends ChangeNotifier { bool isLoading = false; PpmSearch visitsSearch = PpmSearch(); + WorkOrderCostModel? workOrderCostModel; + SparePartHelperModel? sparePartHelperModel; + + void updateSparePartHelperModel(SparePartHelperModel? value) { + sparePartHelperModel = value; + notifyListeners(); + } /// return -2 if request in progress /// return -1 if error happen when sending request @@ -164,11 +192,22 @@ class PpmProvider extends ChangeNotifier { if (response.statusCode >= 200 && response.statusCode < 300) { ppmPlanAttachments = []; 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) { isReadOnly = true; } else { 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; notifyListeners(); return planPreventiveVisit; @@ -364,4 +403,119 @@ class PpmProvider extends ChangeNotifier { return true; } + + Future 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 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) { + 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 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 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 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 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(); + } + } } diff --git a/lib/dashboard_latest/widgets/app_bar_widget.dart b/lib/dashboard_latest/widgets/app_bar_widget.dart index 39a97f7a..8dcc73b2 100644 --- a/lib/dashboard_latest/widgets/app_bar_widget.dart +++ b/lib/dashboard_latest/widgets/app_bar_widget.dart @@ -142,18 +142,19 @@ class _AppBarWidgetState extends State with TickerProviderStateMix children: [ Text( snapshot.user?.username ?? "", + maxLines: 2, style: AppTextStyles.heading6.copyWith(color: AppColor.headingTextColor(context), fontSize: context.isTablet() ? 14 : null), ), - Text( - snapshot.user?.type?.name.toCamelCase ?? "", - style: AppTextStyles.bodyText.copyWith(color: AppColor.lightTextColor(context), fontSize: context.isTablet() ? 12 : null), - ), + // Text( + // snapshot.user?.roleName ?? "", + // 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 SlideTransition( position: _rightSlideAnimation, diff --git a/lib/main.dart b/lib/main.dart index 544e12c5..16d39c62 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -358,6 +358,7 @@ class MyApp extends StatelessWidget { ChangeNotifierProvider(create: (_) => DepartmentProvider(), lazy: true), ChangeNotifierProvider(create: (_) => DepartmentBySitesProvider(), lazy: true), ChangeNotifierProvider(create: (_) => SiteProvider(), lazy: true), + ChangeNotifierProvider(create: (_) => SameOrgSiteProvider(), lazy: true), ChangeNotifierProvider(create: (_) => MappedSiteProvider(), lazy: true), ChangeNotifierProvider(create: (_) => MedicalDepartmentProvider(), lazy: true), ChangeNotifierProvider(create: (_) => FlowMedicalDepartmentProvider(), lazy: true), diff --git a/lib/models/helper_data_models/spare_part/activity_spare_part_model.dart b/lib/models/helper_data_models/spare_part/activity_spare_part_model.dart index 74a1049a..6e051b66 100644 --- a/lib/models/helper_data_models/spare_part/activity_spare_part_model.dart +++ b/lib/models/helper_data_models/spare_part/activity_spare_part_model.dart @@ -5,6 +5,7 @@ import 'package:test_sa/models/service_request/spare_parts.dart'; class SparePartHelperModel { int? id; int? workOrderId; + String? pmVisitId; int? partCatalogItemId; int? activityStatusId; SparePart? sparePart; @@ -18,6 +19,7 @@ class SparePartHelperModel { SparePartHelperModel({ this.id, this.workOrderId, + this.pmVisitId, this.partCatalogItemId, this.quantity, this.installQty, @@ -25,9 +27,9 @@ class SparePartHelperModel { this.sparePart, this.activityStatusId, this.activityStatus, - this.comment,this.sparePartAttachments, - }); - + this.comment, + List? sparePartAttachments, + }) : sparePartAttachments = sparePartAttachments ?? []; Map toJson() { final Map data = {}; @@ -37,7 +39,7 @@ class SparePartHelperModel { data['quantity'] = quantity; data['installQty'] = installQty; data['returnQty'] = returnQty; - if(activityStatus!=null){ + if (activityStatus != null) { data['activityStatusId'] = activityStatus?.id; } data['comment'] = comment; @@ -47,6 +49,18 @@ class SparePartHelperModel { } return data; } + Map toPMJson() { + final Map data = {}; + 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; + } } - - diff --git a/lib/models/helper_data_models/workorder/work_order_helper_models.dart b/lib/models/helper_data_models/workorder/work_order_helper_models.dart index 21bf4ef9..cd2c9141 100644 --- a/lib/models/helper_data_models/workorder/work_order_helper_models.dart +++ b/lib/models/helper_data_models/workorder/work_order_helper_models.dart @@ -168,4 +168,7 @@ class WorkOrderCostModel { 'exchangeCost': exchangeCost }; } + Map toPMCostJson() { + return {'planPreventiveVisitId': workOrderId, 'sparePartCost': sparePartCost, 'laborCost': labourCost, 'travelCost': travelCost, 'qAmount': qAmount, 'prNo': prNo, 'poNo': poNo,'mrNo':mrNo,'exchangeCost':exchangeCost}; + } } diff --git a/lib/models/plan_preventive_visit/plan_preventive_visit_model.dart b/lib/models/plan_preventive_visit/plan_preventive_visit_model.dart index 6af49ca6..d2471a6b 100644 --- a/lib/models/plan_preventive_visit/plan_preventive_visit_model.dart +++ b/lib/models/plan_preventive_visit/plan_preventive_visit_model.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'dart:io'; import 'package:flutter/widgets.dart'; @@ -5,6 +6,7 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/models/device/asset.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/lookup.dart'; import 'package:test_sa/models/service_request/supplier_details.dart'; import 'package:test_sa/models/timer_model.dart'; @@ -50,10 +52,20 @@ class PlanPreventiveVisit { List? preventiveVisitKits; List? preventiveVisitTimers; List? preventiveVisitSuppliers; + List? activities; + num? sparePartCost; + num? labourCost; + num? travelCost; + num? qAmount; + String? prNo; + String? poNo; + String? mrNo; + num? exchangeCost; TimerModel? tbsTimer = TimerModel(); List? timerModelList = []; TimerModel? ppMTimePicker; + PlanPreventiveVisit( {this.id, this.visitNo, @@ -96,6 +108,15 @@ class PlanPreventiveVisit { this.preventiveVisitTimers, this.timerModelList, this.ppMTimePicker, + this.activities, + this.labourCost, + this.sparePartCost, + this.travelCost, + this.qAmount, + this.prNo, + this.poNo, + this.mrNo, + this.exchangeCost, this.preventiveVisitSuppliers}); PlanPreventiveVisit.fromJson(Map json) { @@ -132,6 +153,14 @@ class PlanPreventiveVisit { safety = json['safety'] != null ? Lookup.fromJson(json['safety']) : null; engSignature = json['engSignature']; 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) { preventiveVisitAttachments = []; json['preventiveVisitAttachments'].forEach((v) { @@ -168,6 +197,12 @@ class PlanPreventiveVisit { preventiveVisitSuppliers!.add(PreventiveVisitSuppliers.fromJson(v)); }); } + if (json['activitySparePartsPPM'] != null) { + activities = []; + json['activitySparePartsPPM'].forEach((v) { + activities!.add(PMSparePartActivityModel.fromJson(v)); + }); + } } Map toJson({required int status}) { @@ -203,10 +238,12 @@ class PlanPreventiveVisit { if (preventiveVisitSuppliers != null) { data['preventiveVisitSuppliers'] = preventiveVisitSuppliers!.map((v) => v.toJson()).toList(); } + if (activities != null) { + data['activitySparePartsPPM'] = activities!.map((v) => v.toJson()).toList(); + } return data; } - Future validate(BuildContext context) async { if (visitStatus?.id == null) { await Fluttertoast.showToast(msg: "${context.translation.youHaveToSelect} ${context.translation.status}"); @@ -234,7 +271,6 @@ class PlanPreventiveVisit { } } - class AssignedEmployee { String? userId; String? userName; @@ -263,7 +299,6 @@ class AssignedEmployee { } } - class PreventiveVisitChecklists { int? id; Lookup? taskStatus; @@ -395,7 +430,6 @@ class PreventiveVisitSuppliers { } } - class PreventiveVisitKits { int? id; PartCatalogItem? partCatalogItem; @@ -442,3 +476,80 @@ class PartCatalogItem { return data; } } + +class PMSparePartActivityModel { + int? id; + PartCatalogItem? partCatalogItem; + Lookup? activityStatus; + double? quantity; + double? installQty; + double? returnQty; + String? comment; + dynamic price; + + List activitySparePartPPMAttachments; + + PMSparePartActivityModel({ + this.id, + this.partCatalogItem, + this.activityStatus, + this.quantity, + this.installQty, + this.returnQty, + this.comment, + this.price, + List? activitySparePartPPMAttachments, + }) : activitySparePartPPMAttachments = activitySparePartPPMAttachments ?? []; + + PMSparePartActivityModel.fromJson(Map 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 toJson() { + final Map 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; + } +} diff --git a/lib/models/user.dart b/lib/models/user.dart index aa3389dc..608e0989 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -100,6 +100,8 @@ class User { return {"username": userName, "password": password, "fireBaseToken": FirebaseNotificationManger?.token ?? "", "notificationType": notificationType}; } + String get roleName => userRoles?.first.name ?? ""; + UsersTypes? get type { switch (userRoles?.first.value) { case "R-6": diff --git a/lib/modules/cm_module/cm_detail_provider.dart b/lib/modules/cm_module/cm_detail_provider.dart index fae159c2..8f7ea659 100644 --- a/lib/modules/cm_module/cm_detail_provider.dart +++ b/lib/modules/cm_module/cm_detail_provider.dart @@ -747,23 +747,16 @@ class CMDetailProvider extends ChangeNotifier { } } - Future updateActivitySparePart() async { - isLoading = true; + Future updateActivitySparePart() async { Response response; try { response = await ApiManager.instance.post(URLs.updateActivitySparePartUrl, body: sparePartHelperModel!.toJson()); - stateCode = response.statusCode; - isLoading = false; - notifyListeners(); if (stateCode == 200) { - return true; + return ""; } - return false; + return "Failed, Try Again"; } catch (error) { - isLoading = false; - stateCode = -1; - notifyListeners(); - return false; + return "Failed, Try Again"; } } diff --git a/lib/modules/cm_module/views/components/activities_list_view.dart b/lib/modules/cm_module/views/components/activities_list_view.dart index 1a4821c6..882ae77c 100644 --- a/lib/modules/cm_module/views/components/activities_list_view.dart +++ b/lib/modules/cm_module/views/components/activities_list_view.dart @@ -357,27 +357,31 @@ class _ActivitiesListViewState extends State { } void editSparePartRequest({required BuildContext context, required CMDetailProvider requestDetailProvider, required Activities activity}) async { - requestDetailProvider.sparePartHelperModel = SparePartHelperModel( + final model = SparePartHelperModel( id: activity.id, workOrderId: requestDetailProvider.currentWorkOrder?.data?.requestId, comment: activity.activitySparePart?.comment, sparePartAttachments: activity.activitySparePart?.acitiySparePartAttachments ?? [], activityStatus: activity.activityStatus, sparePart: SparePart( - id: activity.activitySparePart?.partCatalogItem?.id, - partName: activity.activitySparePart?.partCatalogItem?.partName, - partNo: activity.activitySparePart?.partCatalogItem?.partNumber, - oracleCode: activity.activitySparePart?.partCatalogItem?.oracleCode), + id: activity.activitySparePart?.partCatalogItem?.id, + partName: activity.activitySparePart?.partCatalogItem?.partName, + partNo: activity.activitySparePart?.partCatalogItem?.partNumber, + oracleCode: activity.activitySparePart?.partCatalogItem?.oracleCode, + ), quantity: activity.activitySparePart?.quantity, installQty: activity.activitySparePart?.installQty, returnQty: activity.activitySparePart?.returnQty, activityStatusId: activity.activityStatus?.id, ); - requestDetailProvider.updateSparePartHelperModel(requestDetailProvider.sparePartHelperModel); - + // Navigate and let SparePartRequest._initializeModel() handle provider setup Navigator.of(context).push(MaterialPageRoute( - builder: (_) => const SparePartRequest(), + builder: (_) => SparePartRequest( + model: model, + provider: requestDetailProvider, + fromCm: true, + ), )); } } diff --git a/lib/modules/cm_module/views/components/bottom_sheets/service_request_bottomsheet.dart b/lib/modules/cm_module/views/components/bottom_sheets/service_request_bottomsheet.dart index 04fdd0ce..2272212a 100644 --- a/lib/modules/cm_module/views/components/bottom_sheets/service_request_bottomsheet.dart +++ b/lib/modules/cm_module/views/components/bottom_sheets/service_request_bottomsheet.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.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/extensions/context_extension.dart'; import 'package:test_sa/extensions/int_extensions.dart'; @@ -668,16 +669,26 @@ class ServiceRequestBottomSheet { } void onItemTap({required int index, required BuildContext context}) { + CMDetailProvider requestDetailProvider = Provider.of(context, listen: false); switch (index) { case 0: - Provider.of(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( context, - MaterialPageRoute(builder: (context) => const SparePartRequest()), + MaterialPageRoute( + builder: (context) => SparePartRequest( + model: model, + provider: requestDetailProvider, + fromCm: true, + ), + ), ); break; case 1: - CMDetailProvider requestDetailProvider = Provider.of(context, listen: false); requestDetailProvider.activityMaintenanceHelperModel = ActivityMaintenanceHelperModel( workOrderId: requestDetailProvider.currentWorkOrder?.data?.requestId, 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( context: context, childWidget: Consumer( @@ -804,11 +815,23 @@ class ServiceRequestBottomSheet { buttonColor: AppColor.primary10, loading: false, onPressed: () { + dynamic provider = fromCm ? Provider.of(context, listen: false) : Provider.of(context, listen: false); Navigator.pop(context); - Provider.of(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( context, - MaterialPageRoute(builder: (context) => const SparePartRequest()), + MaterialPageRoute( + builder: (context) => SparePartRequest( + model: model, + provider: provider, + fromCm: fromCm, + ), + ), ); }, ).expanded, diff --git a/lib/modules/cm_module/views/components/service_request_detail_view.dart b/lib/modules/cm_module/views/components/service_request_detail_view.dart index e6055eec..9f49fa8a 100644 --- a/lib/modules/cm_module/views/components/service_request_detail_view.dart +++ b/lib/modules/cm_module/views/components/service_request_detail_view.dart @@ -367,8 +367,26 @@ class _ServiceRequestDetailViewState extends State { const Spacer(), if (!provider.isReadOnlyRequest) "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; - 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; }), ], diff --git a/lib/modules/cm_module/views/forms/cost/cost_detail_form_screen.dart b/lib/modules/cm_module/views/forms/cost/cost_detail_form_screen.dart index f7be5cab..94b7f842 100644 --- a/lib/modules/cm_module/views/forms/cost/cost_detail_form_screen.dart +++ b/lib/modules/cm_module/views/forms/cost/cost_detail_form_screen.dart @@ -1,273 +1,216 @@ +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/text_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/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/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/app_text_form_field.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 { 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 _CostDetailFormScreenState createState() => _CostDetailFormScreenState(); } -class _CostDetailFormScreenState extends State with TickerProviderStateMixin { - CMDetailProvider? _requestDetailProvider; - - final GlobalKey _scaffoldKey = GlobalKey(); +class _CostDetailFormScreenState extends State { + late WorkOrderCostModel _localModel; @override void initState() { super.initState(); - } - - void assignValues() { - // ServiceRequestDetailProvider requestDetailProvider = Provider.of(context, listen: false); - // _requestDetailProvider = Provider.of(context, listen: false); - WorkOrderData currentWorkOrderData = _requestDetailProvider!.currentWorkOrder!.data!; - _requestDetailProvider!.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, + _localModel = WorkOrderCostModel( + workOrderId: widget.model?.workOrderId, + labourCost: widget.model?.labourCost, + sparePartCost: widget.model?.sparePartCost, + travelCost: widget.model?.travelCost, + qAmount: widget.model?.qAmount, + prNo: widget.model?.prNo, + poNo: widget.model?.poNo, + mrNo: widget.model?.mrNo, + exchangeCost: widget.model?.exchangeCost, ); + log('Initialized Cost Model: ${_localModel.toJson()}'); } @override Widget build(BuildContext context) { - if (_requestDetailProvider == null) { - _requestDetailProvider = Provider.of(context, listen: false); - assignValues(); - } - return Scaffold( - key: _scaffoldKey, appBar: const DefaultAppBar(title: "Cost Details"), - body: Consumer(builder: (context, CMDetailProvider requestDetailProvider, child) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - AppTextFormField( - labelText: "Spare Part Cost", - backgroundColor: AppColor.neutral100, - initialValue: requestDetailProvider.workOrderCostModel?.sparePartCost?.toString(), - textAlign: TextAlign.center, - enable: false, - labelStyle: AppTextStyles.textFieldLabelStyle, - textInputType: TextInputType.number, - showShadow: false, - onChange: (value) { - requestDetailProvider.workOrderCostModel?.sparePartCost = num.parse(value); - }, - style: Theme.of(context).textTheme.titleMedium, - ), - 8.height, - AppTextFormField( - initialValue: requestDetailProvider.workOrderCostModel?.labourCost?.toString(), - textAlign: TextAlign.center, - labelText: "Labor Cost", - backgroundColor: AppColor.fieldBgColor(context), - labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), - textInputType: const TextInputType.numberWithOptions(decimal: true), - showShadow: false, - onChange: (value) { - requestDetailProvider.workOrderCostModel?.labourCost = num.parse(value); - }, - style: Theme.of(context).textTheme.titleMedium, - ), - 8.height, - AppTextFormField( - labelText: "Travel Cost", - backgroundColor: AppColor.fieldBgColor(context), - labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), - initialValue: requestDetailProvider.workOrderCostModel?.travelCost?.toString(), - textAlign: TextAlign.center, - textInputType: const TextInputType.numberWithOptions(decimal: true), - showShadow: false, - onChange: (value) { - requestDetailProvider.workOrderCostModel?.travelCost = num.parse(value); - }, - style: Theme.of(context).textTheme.titleMedium, - ), - 8.height, - AppTextFormField( - labelText: "Quot Amount", - backgroundColor: AppColor.fieldBgColor(context), - labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), - initialValue: requestDetailProvider.workOrderCostModel?.qAmount?.toString(), - textAlign: TextAlign.center, - textInputType: const TextInputType.numberWithOptions(decimal: true), - showShadow: false, - onChange: (value) { - requestDetailProvider.workOrderCostModel?.qAmount = num.parse(value); - }, - style: Theme.of(context).textTheme.titleMedium, - ), - 8.height, - AppTextFormField( - labelText: "PR No", - backgroundColor: AppColor.fieldBgColor(context), - labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), - initialValue: requestDetailProvider.workOrderCostModel?.prNo, - textAlign: TextAlign.center, - textInputType: const TextInputType.numberWithOptions(decimal: true), - showShadow: false, - onChange: (value) { - requestDetailProvider.workOrderCostModel?.prNo = value; - }, - style: Theme.of(context).textTheme.titleMedium, - ), - 8.height, - AppTextFormField( - labelText: "PO No", - backgroundColor: AppColor.fieldBgColor(context), - labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), - initialValue: requestDetailProvider.workOrderCostModel?.poNo, - textAlign: TextAlign.center, - textInputType: const TextInputType.numberWithOptions(decimal: true), - showShadow: false, - onChange: (value) { - requestDetailProvider.workOrderCostModel?.poNo = value; - }, - style: Theme.of(context).textTheme.titleMedium, - ), - 8.height, - AppTextFormField( - labelText: "MR No", - backgroundColor: AppColor.fieldBgColor(context), - labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), - initialValue: requestDetailProvider.workOrderCostModel?.mrNo, - textAlign: TextAlign.center, - textInputType: const TextInputType.numberWithOptions(decimal: true), - showShadow: false, - onChange: (value) { - requestDetailProvider.workOrderCostModel?.mrNo = value; - }, - style: Theme.of(context).textTheme.titleMedium, - ), - 8.height, - AppTextFormField( - labelText: "Exchange Cost", - backgroundColor: AppColor.fieldBgColor(context), - labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), - initialValue: requestDetailProvider.workOrderCostModel?.exchangeCost != null ? requestDetailProvider.workOrderCostModel?.exchangeCost.toString() : '', - textAlign: TextAlign.center, - textInputType: const TextInputType.numberWithOptions(decimal: true), - showShadow: false, - onChange: (value) { - requestDetailProvider.workOrderCostModel?.exchangeCost = num.parse(value); - }, - style: Theme.of(context).textTheme.titleMedium, - ), - ], - ).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); - } - }); - } - }, + body: ListenableBuilder( + listenable: widget.provider, + builder: (context, child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + AppTextFormField( + labelText: "Spare Part Cost", + backgroundColor: AppColor.neutral100, + initialValue: _localModel.sparePartCost?.toString() ?? '0', + textAlign: TextAlign.center, + enable: false, + labelStyle: AppTextStyles.textFieldLabelStyle, + textInputType: TextInputType.number, + showShadow: false, + style: Theme.of(context).textTheme.titleMedium, + ), + 8.height, + AppTextFormField( + initialValue: _localModel.labourCost?.toString() ?? '', + textAlign: TextAlign.center, + labelText: "Labor Cost", + backgroundColor: AppColor.fieldBgColor(context), + labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), + textInputType: const TextInputType.numberWithOptions(decimal: true), + showShadow: false, + onChange: (value) { + _localModel.labourCost = num.tryParse(value); + }, + style: Theme.of(context).textTheme.titleMedium, + ), + 8.height, + AppTextFormField( + labelText: "Travel Cost", + backgroundColor: AppColor.fieldBgColor(context), + labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), + initialValue: _localModel.travelCost?.toString() ?? '', + textAlign: TextAlign.center, + textInputType: const TextInputType.numberWithOptions(decimal: true), + showShadow: false, + onChange: (value) { + _localModel.travelCost = num.tryParse(value); + }, + style: Theme.of(context).textTheme.titleMedium, + ), + 8.height, + AppTextFormField( + labelText: "Quot Amount", + backgroundColor: AppColor.fieldBgColor(context), + labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), + initialValue: _localModel.qAmount?.toString() ?? '', + textAlign: TextAlign.center, + textInputType: const TextInputType.numberWithOptions(decimal: true), + showShadow: false, + onChange: (value) { + _localModel.qAmount = num.tryParse(value); + }, + style: Theme.of(context).textTheme.titleMedium, + ), + 8.height, + AppTextFormField( + labelText: "PR No", + backgroundColor: AppColor.fieldBgColor(context), + labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), + initialValue: _localModel.prNo ?? '', + textAlign: TextAlign.center, + textInputType: TextInputType.text, + showShadow: false, + onChange: (value) { + _localModel.prNo = value.isEmpty ? null : value; + }, + style: Theme.of(context).textTheme.titleMedium, + ), + 8.height, + AppTextFormField( + labelText: "PO No", + backgroundColor: AppColor.fieldBgColor(context), + labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), + initialValue: _localModel.poNo ?? '', + textAlign: TextAlign.center, + textInputType: TextInputType.text, + showShadow: false, + onChange: (value) { + _localModel.poNo = value.isEmpty ? null : value; + }, + style: Theme.of(context).textTheme.titleMedium, + ), + 8.height, + AppTextFormField( + labelText: "MR No", + backgroundColor: AppColor.fieldBgColor(context), + labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), + initialValue: _localModel.mrNo ?? '', + textAlign: TextAlign.center, + textInputType: TextInputType.text, + showShadow: false, + onChange: (value) { + _localModel.mrNo = value.isEmpty ? null : value; + }, + style: Theme.of(context).textTheme.titleMedium, + ), + 8.height, + AppTextFormField( + labelText: "Exchange Cost", + backgroundColor: AppColor.fieldBgColor(context), + labelStyle: AppTextStyles.textFieldLabelStyle.copyWith(color: AppColor.textColor(context)), + initialValue: _localModel.exchangeCost?.toString() ?? '', + textAlign: TextAlign.center, + textInputType: const TextInputType.numberWithOptions(decimal: true), + showShadow: false, + onChange: (value) { + _localModel.exchangeCost = num.tryParse(value); + }, + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ).toShadowContainer(context).paddingAll(16), + ), ), - ), - ], - ); - }), + FooterActionButton.footerContainer( + context: context, + child: AppFilledButton( + label: "Update Cost Details", + buttonColor: AppColor.primary10, + onPressed: () => _onSubmit(), + ), + ), + ], + ); + }, + ), ); } - bool validateForm({required CMDetailProvider requestDetailProvider}) { - bool isValid = true; - if (!widget.isEdit) { - return true; - } - // if (requestDetailProvider.engineerUpdateWorkOrderHelperModel!.equipmentStatus != null) { - // 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; - } + Future _onSubmit() async { + widget.provider.workOrderCostModel = _localModel; + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const AppLazyLoading(), + ); - Widget assetStatusWidget({required BuildContext context, required EngineerUpdateWorkOrderHelperModel workOrderData}) { - return Consumer(builder: (cxt, snapshot, _) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - context.translation.assetStatus.bodyText(context).custom(color: AppColor.black20), - 8.height, - Wrap( - runSpacing: 8, - spacing: 8, - 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), - ], - ); - }); + try { + final success = await widget.provider.engineerUpdateCost(); + + if (mounted) Navigator.pop(context); // Close loading + + if (success && mounted) { + Navigator.pop(context); + } + } catch (e) { + if (mounted) Navigator.pop(context); + log('Error updating cost: $e'); + } } } diff --git a/lib/modules/cm_module/views/forms/spare_part/spare_part_request.dart b/lib/modules/cm_module/views/forms/spare_part/spare_part_request.dart index cac36002..85388772 100644 --- a/lib/modules/cm_module/views/forms/spare_part/spare_part_request.dart +++ b/lib/modules/cm_module/views/forms/spare_part/spare_part_request.dart @@ -1,7 +1,6 @@ -import 'dart:async'; import 'dart:convert'; +import 'dart:developer'; import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:test_sa/controllers/providers/api/parts_provider.dart'; @@ -17,7 +16,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/service_request/spare_parts.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/views/components/action_button/footer_action_button.dart'; import 'package:test_sa/modules/cm_module/views/components/bottom_sheets/service_request_bottomsheet.dart'; @@ -29,387 +27,445 @@ 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/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/loaders/no_data_found.dart'; class SparePartRequest extends StatefulWidget { 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 _SparePartRequestState createState() => _SparePartRequestState(); } -class _SparePartRequestState extends State with TickerProviderStateMixin { - CMDetailProvider? _requestDetailProvider; +class _SparePartRequestState extends State { PartsProvider? _partsProvider; bool _isLoading = false; List _spareParts = []; + List _attachments = []; - List attachments = []; final GlobalKey _formKey = GlobalKey(); - final GlobalKey _scaffoldKey = GlobalKey(); - 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}); - int? activityStatus; @override void initState() { super.initState(); _partsProvider = Provider.of(context, listen: false); - _requestDetailProvider = Provider.of(context, listen: false); - _requestDetailProvider?.sparePartHelperModel = SparePartHelperModel( - 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?.where((e) => e?.id != null).map((e) => GenericAttachmentModel(id: e!.id!, name: e.name ?? '')).toList() ?? []; - setState(() {}); - _spareParts = await _partsProvider!.getPartsListByDisplayName(assetId: _requestDetailProvider?.currentWorkOrder?.data?.asset?.id); - _isLoading = false; - setState(() {}); - }); + _initializeModel(); + _loadInitialData(); } - void restValues({required CMDetailProvider requestDetailProvider}) { - requestDetailProvider.sparePartHelperModel = SparePartHelperModel(id: 0, workOrderId: _requestDetailProvider?.currentWorkOrder?.data?.requestId, sparePartAttachments: []); - _partQtyController.clear(); - _installQtyController.clear(); - _returnQtyController.clear(); - _oracleNoController.clear(); - _descriptionController.clear(); - attachments = []; + void _initializeModel() { + final bool isNewActivity = widget.model?.id == null || widget.model?.id == 0; + if (isNewActivity) { + widget.provider.sparePartHelperModel = SparePartHelperModel( + id: 0, + workOrderId: widget.fromCm ? widget.provider.currentWorkOrder?.data?.requestId : null, + pmVisitId: !widget.fromCm ? widget.provider.planPreventiveVisit?.id : null, + 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 - void dispose() { - _partQtyController.dispose(); - _installQtyController.dispose(); - _returnQtyController.dispose(); - _oracleNoController.dispose(); - _descriptionController.dispose(); - super.dispose(); - } + Future _loadInitialData() async { + setState(() => _isLoading = true); + _spareParts = await _partsProvider!.getPartsListByDisplayName( + assetId: widget.fromCm ? widget.provider.currentWorkOrder?.data?.asset?.id : widget.provider.planPreventiveVisit.asset.id, + ); + final bool isNewActivity = widget.model?.id == null || widget.model?.id == 0; + _attachments = []; + 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 (url.isEmpty != false) return false; - return url.startsWith("/") || url.startsWith("file://") || url.substring(1).startsWith(':\\'); + if (mounted) setState(() => _isLoading = false); } @override Widget build(BuildContext context) { - bool showInstallAndReturnQty = activityStatus == 3 || activityStatus == 4; return Scaffold( - key: _scaffoldKey, appBar: DefaultAppBar(title: context.translation.sparePartRequest), - body: Consumer(builder: (context, CMDetailProvider requestDetailProvider, child) { - return Form( - key: _formKey, - child: Column( - children: [ - SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: IgnorePointer( - ignoring: _requestDetailProvider!.isReadOnlyRequest, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Column( + body: ListenableBuilder( + listenable: widget.provider, + builder: (context, _) { + final provider = widget.provider; + final model = provider.sparePartHelperModel; + final bool showInstallAndReturnQty = (model?.activityStatus?.value == 3 || model?.activityStatus?.value == 4); + final bool isReadOnly = provider.isReadOnlyRequest ?? false; + + return Form( + key: _formKey, + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: IgnorePointer( + ignoring: isReadOnly, + child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - context.translation.sparePartDetails.heading5(context).custom(color: AppColor.textStyleColor(context)), - // 12.height, - // SingleItemDropDownMenu( - // context: context, - // height: 56.toScreenHeight, - // title: context.translation.activityStatus, - // showShadow: false, - // backgroundColor: AppColor.neutral100, - // initialValue: requestDetailProvider.sparePartHelperModel?.activityStatus, - // onSelect: (status) { - // requestDetailProvider.sparePartHelperModel?.activityStatus = status; - // }, - // ), - 12.height, - SingleItemDropDownMenu( - 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, - showAsListView: true, - moduleId: RequestAttachmentTypeEnum.sparePartActivity.id, - attachment: attachments, - buttonIcon: 'quotation_icon'.toSvgAsset(color: AppColor.primary10), - // buttonColor: AppColor.primary10, + _buildFormContent( + context, + model, + provider, + showInstallAndReturnQty, ), ], - ).toShadowContainer(context), - 8.height, - ], + ), + ), ), ), - ).expanded, - 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); - }, - )), - ], + if (!isReadOnly) _buildSubmitButton(context, provider), + ], + ), + ); + }, + ), + ); + } + + Widget _buildFormContent( + BuildContext context, + SparePartHelperModel? model, + dynamic provider, + bool showInstallAndReturnQty, + ) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 12, + children: [ + InfoHeader16Widget(context.translation.sparePartDetails), + SingleItemDropDownMenu( + 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, + showAsListView: true, + moduleId: RequestAttachmentTypeEnum.sparePartActivity.id, + 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 { - if (requestDetailProvider.sparePartHelperModel?.sparePart?.id == null) { + Future _onSubmit(dynamic provider) async { + final model = provider.sparePartHelperModel; + + if (model?.sparePart?.id == null) { "Please select spare part".showToast; return; - } else if (requestDetailProvider.sparePartHelperModel?.quantity == null) { + } else if (model?.quantity == null) { "Please enter quantity".showToast; return; } - requestDetailProvider.sparePartHelperModel?.sparePartAttachments?.clear(); - - for (var item in attachments) { + model?.sparePartAttachments?.clear(); + for (var item in _attachments) { String fileName = CMRequestUtils.isLocalUrl(item.name ?? '') ? ("${item.name ?? ''.split("/").last}|${base64Encode(File(item.name ?? '').readAsBytesSync())}") : item.name ?? ''; - requestDetailProvider.sparePartHelperModel?.sparePartAttachments?.add( + + model?.sparePartAttachments?.add( GenericAttachmentModel(id: item.id, name: fileName, documentType: item.documentType), + // SparePartAttachments(id: item.id, name: fileName), ); } - // Show loading dialog + // Show loading showDialog( context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading(), ); + + // Submit bool success = false; - if (_requestDetailProvider?.sparePartHelperModel?.id == 0) { - success = await requestDetailProvider.createActivitySparePart(); + String message = ""; + if (model?.id == 0) { + success = await provider.createActivitySparePart(); } 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) { - requestDetailProvider.getWorkOrderById( - id: requestDetailProvider.currentWorkOrder!.data!.requestId!, - ); - if (mounted) Navigator.pop(context); - if (_requestDetailProvider?.sparePartHelperModel?.id == 0) { - ServiceRequestBottomSheet.addAnotherSpareRequestBottomSheet(context: context); + if (widget.fromCm) { + provider.getWorkOrderById( + id: provider.currentWorkOrder!.data!.requestId!, + ); + } else { + 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( context: context, title: context.translation.sparePartActivitySuccess, message: '', ); } + } else { + if(message.isNotEmpty) { + context.showInfoDialog(message, title: "Error"); + } } } } +/// Bottom Sheet for showing parts availability class PartDetailBottomSheetSheet extends StatelessWidget { - PartDetailBottomSheetSheet({Key? key}) : super(key: key); + final dynamic provider; + + const PartDetailBottomSheetSheet({ + Key? key, + required this.provider, + }) : super(key: key); @override Widget build(BuildContext context) { return SizedBox( height: SizeConfig.screenHeight! / 2.2, width: SizeConfig.screenWidth, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - 8.height, - 'Parts Availability'.heading6(context).paddingOnly(top: 12, start: 12), - Consumer(builder: (context, requestDetailProvider, child) { - return requestDetailProvider.isLoading - ? CircularProgressIndicator(color: AppColor.loadingColor(context)).center - : requestDetailProvider.storeAvailability.isEmpty - ? const NoDataFound().center - : ListView.separated( - padding: const EdgeInsets.only(top: 12), - itemCount: requestDetailProvider.storeAvailability.length, - separatorBuilder: (czt, index) => 12.height, - itemBuilder: (context, index) { - StoreAvailability model = requestDetailProvider.storeAvailability[index]; - return partAvailableQuantityCard(context: context, model: model); - }, - ); - }).expanded, - ], + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 12.height, + 'Parts Availability'.heading4(context), + Expanded( + child: ListenableBuilder( + listenable: provider, + builder: (context, _) { + return provider.isLoading + ? CircularProgressIndicator( + color: AppColor.loadingColor(context), + ).center + : provider.storeAvailability.isEmpty + ? const NoDataFound().center + : ListView.separated( + padding: const EdgeInsets.only(top: 12, bottom: 12), + itemCount: provider.storeAvailability.length, + 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}) { - return Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - InfoTextWidget(label: "Site Name:".addTranslation, value: model.siteName ?? ''), - InfoTextWidget(label: "Store Name:".addTranslation, value: model.storeName ?? ''), - InfoTextWidget(label: "Available Quantity:".addTranslation, value: model.availablityQty != null ? model.availablityQty.toString() : ''), - ]).toShadowContainer(context, padding: 12, showShadow: false,); + Widget _partAvailableQuantityCard({required StoreAvailability model, required BuildContext context}) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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); } } diff --git a/lib/modules/pm_module/ppm_wo/activities/pm_activities_list_view.dart b/lib/modules/pm_module/ppm_wo/activities/pm_activities_list_view.dart new file mode 100644 index 00000000..a9a1542f --- /dev/null +++ b/lib/modules/pm_module/ppm_wo/activities/pm_activities_list_view.dart @@ -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 { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + List activities = []; + + bool disableNewActivity = false; + + @override + Widget build(BuildContext context) { + UserProvider userProvider = Provider.of(context); + return Scaffold( + appBar: const DefaultAppBar(title: "Spare Part Requests"), + body: Consumer(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 + ), + )); + } +} diff --git a/lib/modules/pm_module/ppm_wo/ppm_details_page.dart b/lib/modules/pm_module/ppm_wo/ppm_details_page.dart index bf64bf9a..ffcc3679 100644 --- a/lib/modules/pm_module/ppm_wo/ppm_details_page.dart +++ b/lib/modules/pm_module/ppm_wo/ppm_details_page.dart @@ -8,6 +8,7 @@ import 'package:test_sa/extensions/text_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/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/views/widgets/item_views/info_header_widget.dart'; import 'package:test_sa/views/widgets/item_views/info_text_widget.dart'; @@ -82,6 +83,20 @@ class _PpmDetailsPageState extends State { textColor: AppColor.getRequestStatusTextColorByName(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 ?? ''), @@ -102,29 +117,23 @@ class _PpmDetailsPageState extends State { ], ).toShadowContainer(context), ).expanded, - if (userProvider!.user!.type == UsersTypes.engineer && (!ppmProvider.isReadOnly)) ...[ + // 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, + child: Column( + children: [ + AppFilledButton( + onPressed: () async { + await Navigator.of(context).push(MaterialPageRoute(builder: (_) => UpdatePpm(ppm: null, planPreventiveVisit: planPreventiveVisit))); + getVisitData(); + }, + label: userProvider!.user!.type == UsersTypes.engineer && (!ppmProvider.isReadOnly)? context.translation.updateWorkOrder:context.translation.viewDetails, + ), + + ], ), 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()); }), diff --git a/lib/modules/pm_module/ppm_wo/update_ppm/update_ppm.dart b/lib/modules/pm_module/ppm_wo/update_ppm/update_ppm.dart index e6301bf6..6886920a 100644 --- a/lib/modules/pm_module/ppm_wo/update_ppm/update_ppm.dart +++ b/lib/modules/pm_module/ppm_wo/update_ppm/update_ppm.dart @@ -15,6 +15,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/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/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/common_widgets/app_filled_button.dart'; import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart'; @@ -45,7 +46,6 @@ class _UpdatePpmState extends State with TickerProviderStateMixin { TabController? _tabController; late PpmProvider ppmProvider; - _onSubmit({required int status}) async { if (ppmProvider.validate()) { showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading()); @@ -99,27 +99,33 @@ class _UpdatePpmState extends State with TickerProviderStateMixin { @override void initState() { + super.initState(); ppmProvider = Provider.of(context, listen: false); _planPreventiveVisit = widget.planPreventiveVisit; - // WidgetsBinding.instance.addPostFrameCallback((_) { - initTabs(_planPreventiveVisit.typeOfService); - // }); - super.initState(); + int initialTabs = (_planPreventiveVisit.typeOfService?.value == 2) ? 4 : 3; + _tabController = TabController(length: initialTabs, vsync: this); + WidgetsBinding.instance.addPostFrameCallback((_) { + ppmProvider.totalTabs = initialTabs; + }); } - - void initTabs(typeOfService) { if (typeOfService == null) { return; } - if (typeOfService?.value == 2) { - ppmProvider.totalTabs = 4; - } else { - ppmProvider.totalTabs = 3; + + int newTabCount = (typeOfService?.value == 2) ? 4 : 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; @@ -213,70 +219,82 @@ class _UpdatePpmState extends State with TickerProviderStateMixin { ).expanded, FooterActionButton.footerContainer( context: context, - child: Row( + child: Column( children: [ - if (tabIndex == 1) ...[ - AppFilledButton( - buttonColor: _tabController!.index == 0 ? null : AppColor.background(context), - // buttonColor: _tabController!.index == 0 ? null : AppColor.background(context), - textColor: _tabController!.index == 0 ? null : AppColor.blueStatus(context), - showBorder: true, - disableButton: _tabController!.index == 0, - onPressed: () { - if (_tabController!.index == 0) { - return; - } - _tabController?.animateTo(_tabController!.index - 1); - setState(() {}); - return; - }, - 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); + Row( + children: [ + if (tabIndex == 1) ...[ + AppFilledButton( + buttonColor: _tabController!.index == 0 ? null : AppColor.background(context), + // buttonColor: _tabController!.index == 0 ? null : AppColor.background(context), + textColor: _tabController!.index == 0 ? null : AppColor.blueStatus(context), + showBorder: true, + disableButton: _tabController!.index == 0, + onPressed: () { + if (_tabController!.index == 0) { + return; + } + _tabController?.animateTo(_tabController!.index - 1); + setState(() {}); return; - } - _tabController?.animateTo(_tabController!.index + 1); - setState(() {}); - }, - label: ppmProvider.totalTabs == _tabController!.index + 1 ? context.translation.close : context.translation.next, - ).expanded, - ), - ] + }, + 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; + } + _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", + ), ], ), ), diff --git a/lib/modules/pm_module/ppm_wo/update_ppm/wo_info_form.dart b/lib/modules/pm_module/ppm_wo/update_ppm/wo_info_form.dart index e5d8ec05..8d2e7726 100644 --- a/lib/modules/pm_module/ppm_wo/update_ppm/wo_info_form.dart +++ b/lib/modules/pm_module/ppm_wo/update_ppm/wo_info_form.dart @@ -15,6 +15,7 @@ import 'package:test_sa/models/device/model_definition.dart'; import 'package:test_sa/models/device/supplier.dart'; import 'package:test_sa/models/enums/request_attachment_type_enum.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/new_models/building.dart'; import 'package:test_sa/models/new_models/department.dart'; @@ -23,6 +24,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/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/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/common_widgets/app_text_form_field.dart'; import 'package:test_sa/new_views/common_widgets/single_item_drop_down_menu.dart'; @@ -115,6 +117,7 @@ class _WoInfoFormState extends State { IgnorePointer( ignoring: ppmProvider.isReadOnly, child: Column( + spacing: 12, children: [ Column( mainAxisSize: MainAxisSize.min, @@ -156,7 +159,6 @@ class _WoInfoFormState extends State { // '${context.translation.executionTimeFrame}: ${widget.planPreventiveVisit.executionTimeFrame ?? ""} days'.bodyText2(context).custom(color: AppColor.neutral120), ], ).toShadowContainer(context), - 12.height, Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -216,9 +218,10 @@ class _WoInfoFormState extends State { // } // }, // ), - 8.height, + // 8.height,// Column( mainAxisAlignment: MainAxisAlignment.start, + spacing: 12, children: [ SingleItemDropDownMenu( context: context, @@ -233,7 +236,6 @@ class _WoInfoFormState extends State { } }, ), - 8.height, ADatePicker( label: context.translation.actualVisit, hideShadow: true, @@ -280,8 +282,6 @@ class _WoInfoFormState extends State { // } // }, // ), - - 8.height, SingleItemDropDownMenu( context: context, backgroundColor: AppColor.fieldBgColor(context), @@ -294,7 +294,6 @@ class _WoInfoFormState extends State { } }, ), - 8.height, SingleItemDropDownMenu( context: context, backgroundColor: AppColor.fieldBgColor(context), @@ -309,7 +308,6 @@ class _WoInfoFormState extends State { } }, ), - 8.height, SingleItemDropDownMenu( context: context, backgroundColor: AppColor.fieldBgColor(context), @@ -330,9 +328,7 @@ class _WoInfoFormState extends State { } }, ), - 8.height, _timerWidget(context, totalWorkingHours), - 8.height, AppTextFormField( labelText: context.translation.comment, backgroundColor: AppColor.fieldBgColor(context), @@ -344,7 +340,6 @@ class _WoInfoFormState extends State { widget.planPreventiveVisit.comments = value; }, ), - 16.height, AttachmentPicker( label: context.translation.attachments, showAsListView: true, @@ -356,6 +351,7 @@ class _WoInfoFormState extends State { ), ], ).toShadowContainer(context), + costCard(context, ppmProvider), ], ), ), @@ -412,6 +408,7 @@ class _WoInfoFormState extends State { Widget _timerWidget(BuildContext context, double totalWorkingHours) { return Column( + spacing: 12, children: [ AppTimer( label: context.translation.workingHours, @@ -433,7 +430,6 @@ class _WoInfoFormState extends State { return true; }, ), - 11.height, if (totalWorkingHours > 0.0) ...[ WorkingTimeTile( timerList: timerList, @@ -453,6 +449,79 @@ class _WoInfoFormState extends State { ); } + 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) { // int hours = seconds ~/ 3600; // int minutes = (seconds % 3600) ~/ 60; diff --git a/lib/modules/site_request_module/providers/site_request_provider.dart b/lib/modules/site_request_module/providers/site_request_provider.dart index d97b5b7d..375c4d86 100644 --- a/lib/modules/site_request_module/providers/site_request_provider.dart +++ b/lib/modules/site_request_module/providers/site_request_provider.dart @@ -27,6 +27,7 @@ class SiteRequestProvider extends ChangeNotifier { // Data List _requestTypes = []; List _userCurrentSites = []; + List userFilterSites = []; List _requestHistory = []; // Pagination @@ -106,6 +107,24 @@ class SiteRequestProvider extends ChangeNotifier { ); } + /// Fetches user's current sites for Change/Remove Site operations + Future> getSitesByUserId(String userId) async { + List 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 Future fetchUserCurrentSites(String userId) async { if (_loading) return -2; @@ -125,6 +144,24 @@ class SiteRequestProvider extends ChangeNotifier { ); } + Future 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 Future submitSiteRequest(SiteRequestModel request, UserProvider userProvider) async { loading = true; diff --git a/lib/modules/site_request_module/screens/site_request_form_screen.dart b/lib/modules/site_request_module/screens/site_request_form_screen.dart index 1dde072a..86b905d5 100644 --- a/lib/modules/site_request_module/screens/site_request_form_screen.dart +++ b/lib/modules/site_request_module/screens/site_request_form_screen.dart @@ -175,11 +175,12 @@ class _SiteRequestFormScreenState extends State { _siteReplacements.clear(); _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 if ((_isChangeSiteRequest() || _isRemoveSiteRequest()) && value != null) { - final userId = _provider.getCurrentUserUserId(_userProvider); await _provider.fetchUserCurrentSites(userId); + }else { + await _provider.fetchUserFilteredSites(userId); } // Fetch all sites if Change Site is selected (needed for new site dropdown) @@ -318,6 +319,40 @@ class _SiteRequestFormScreenState extends State { } Widget _buildSiteMultiSelect() { + return MultipleItemDropDownMenu( + 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(context, listen: false); + final siteIds = value.map((site) => int.tryParse(site.identifier ?? '0') ?? 0).toList(); + departmentProvider.setSiteIds(siteIds); + departmentProvider.getData(); + }); + + } + }, + ); return MultipleItemDropDownMenu( context: context, title: context.translation.site, diff --git a/lib/modules/tm_module/device_transfer/create_device_transfer_request.dart b/lib/modules/tm_module/device_transfer/create_device_transfer_request.dart index 24494964..4a582ffc 100644 --- a/lib/modules/tm_module/device_transfer/create_device_transfer_request.dart +++ b/lib/modules/tm_module/device_transfer/create_device_transfer_request.dart @@ -156,8 +156,9 @@ class _CreateDeviceTransferRequestState extends State( + SingleItemDropDownMenu( context: context, + requestById: context.userProvider.user?.clientId, title: context.translation.site, initialValue: _assetDestination.site, disableValue: isInternal ? null : _pickedAsset?.site, diff --git a/lib/modules/tm_module/device_transfer/device_filter_screen.dart b/lib/modules/tm_module/device_transfer/device_filter_screen.dart index 04802a5e..cdd65c2d 100644 --- a/lib/modules/tm_module/device_transfer/device_filter_screen.dart +++ b/lib/modules/tm_module/device_transfer/device_filter_screen.dart @@ -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/text_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/new_models/site.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/common_widgets/app_filled_button.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/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/equipment/asset_status_buttons.dart'; - import '../../../models/device/asset_search.dart'; import '../../../new_views/common_widgets/app_lazy_loading.dart'; import '../../../new_views/common_widgets/default_app_bar.dart'; @@ -33,11 +33,13 @@ class DeviceFilterScreen extends StatefulWidget { class _DeviceFilterScreenState extends State { late DeviceSearch filter; Lookup? _commissioningStatus; - List _selectedSites = []; + List _selectedSites = []; List _selectedDepartments = []; String? startDate, endDate; bool loading = false; + List userSites = []; + @override void initState() { filter = widget.data ?? DeviceSearch(); @@ -47,11 +49,15 @@ class _DeviceFilterScreenState extends State { startDate = filter.supplyDateFrom; endDate = filter.supplyDateTo; _commissioningStatus = filter.assetStatus; + final departmentProvider = Provider.of(context, listen: false); - final siteProvider = Provider.of(context, listen: false); + Utils.showLoading(context); if (departmentProvider.items.isEmpty ?? true) await departmentProvider.getData(); - if (siteProvider.items.isEmpty ?? true) await siteProvider.getData(); - + if (userSites.isEmpty) { + userSites = await Provider.of(context, listen: false).getSitesByUserId(context.userProvider.user?.userID ?? ""); + } + await Future.delayed(const Duration(milliseconds: 100)); + Utils.hideLoading(context); // Initialize selected departments from filter if (filter.departments != null && filter.departments!.isNotEmpty) { _selectedDepartments = []; @@ -70,11 +76,11 @@ class _DeviceFilterScreenState extends State { if (filter.sites != null && filter.sites!.isNotEmpty) { _selectedSites = []; for (var siteName in filter.sites!) { - final match = siteProvider.items.firstWhere( + final match = userSites.firstWhere( (site) => site.name == siteName, - orElse: () => Site(), + orElse: () => SiteRequestSiteModel(id: '', name: ''), ); - if (match.id != null) { + if (match.identifier != null) { _selectedSites.add(match); } } @@ -117,14 +123,15 @@ class _DeviceFilterScreenState extends State { ListView( padding: const EdgeInsets.all(16), children: [ - AssetStatusButtons( - initialValue: _commissioningStatus, - onSelect: (value) { - if (value != null) { - _commissioningStatus = value; - } - }, - ), + // AssetStatusButtons( + // initialValue: _commissioningStatus, + // onSelect: (value) { + // if (value != null) { + // _commissioningStatus = value; + // } + // }, + // ), + context.translation.filter.heading5(context).custom(color: AppColor.textStyleColor(context)), 16.height, MultipleItemDropDownMenu( context: context, @@ -145,11 +152,12 @@ class _DeviceFilterScreenState extends State { }, ), 12.height, - MultipleItemDropDownMenu( + MultipleItemDropDownMenu( context: context, title: context.translation.bySite, initialValue: _selectedSites, showAsBottomSheet: true, + staticData: userSites, // backgroundColor: AppColor.fieldBgColor(context), backgroundColor: context.isDark ? AppColor.neutral60 : Colors.white, showShadow: true, diff --git a/lib/new_views/swipe_module/dialoge/info_dialog.dart b/lib/new_views/swipe_module/dialoge/info_dialog.dart index 5809ab6d..8dcc0cb7 100644 --- a/lib/new_views/swipe_module/dialoge/info_dialog.dart +++ b/lib/new_views/swipe_module/dialoge/info_dialog.dart @@ -41,7 +41,7 @@ class InfoDialog extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: InfoHeader16Widget("Information").paddingOnly(top: 16), + child: InfoHeader16Widget(title ?? "Information").paddingOnly(top: 16), // Text( // title ?? "Information", // style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: AppColor.headingTextColor(context), height: 35 / 24, letterSpacing: -0.96), diff --git a/lib/providers/gas_request_providers/site_provider.dart b/lib/providers/gas_request_providers/site_provider.dart index d497a88d..c2ce34f4 100644 --- a/lib/providers/gas_request_providers/site_provider.dart +++ b/lib/providers/gas_request_providers/site_provider.dart @@ -33,6 +33,31 @@ class SiteProvider extends LoadingListNotifier { } } +class SameOrgSiteProvider extends LoadingListNotifier { + @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 { @override Future getData({int? id}) async { diff --git a/lib/views/widgets/loaders/image_loader.dart b/lib/views/widgets/loaders/image_loader.dart index eed5d473..6197e620 100644 --- a/lib/views/widgets/loaders/image_loader.dart +++ b/lib/views/widgets/loaders/image_loader.dart @@ -1,5 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:test_sa/new_views/app_style/app_color.dart'; class ImageLoader extends StatelessWidget { final String? url; @@ -20,7 +21,7 @@ class ImageLoader extends StatelessWidget { alignment: Alignment.center, width: width, 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), ); } diff --git a/lib/views/widgets/timer/app_timer.dart b/lib/views/widgets/timer/app_timer.dart index d33b0274..759a9c17 100644 --- a/lib/views/widgets/timer/app_timer.dart +++ b/lib/views/widgets/timer/app_timer.dart @@ -158,6 +158,7 @@ class _AppTimerState extends State { @override Widget build(BuildContext context) { return Column( + spacing: 12, children: [ if (canPickTime) ...[ Row( @@ -228,7 +229,6 @@ class _AppTimerState extends State { ], ), if (_tempPickerTimer != null) ...[ - 8.height, Row( children: [ Column( @@ -248,7 +248,7 @@ class _AppTimerState extends State { ], ).toShadowContainer(context) ], - 8.height, + // 8.height, ], if (widget.showTimer) Container( diff --git a/pubspec.yaml b/pubspec.yaml index ccf2f535..b2e61e4a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.9.3+53 +version: 1.9.5+55 environment: sdk: ">=3.5.0 <4.0.0"