Merge branch 'refs/heads/main_production' into main_production_attahment_type

# Conflicts:
#	lib/controllers/api_routes/urls.dart
#	lib/models/helper_data_models/spare_part/activity_spare_part_model.dart
#	lib/models/plan_preventive_visit/plan_preventive_visit_model.dart
#	lib/modules/cm_module/views/forms/spare_part/spare_part_request.dart
main_production
Sikander Saleem 4 days ago
commit 7490544084

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

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

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

@ -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<SparePart> 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<List<SparePart>> 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<SparePart> 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) {

@ -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> _storeAvailability = [];
List<StoreAvailability> get storeAvailability => _storeAvailability;
set storeAvailability(List<StoreAvailability> 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<bool> createActivitySparePart() async {
isLoading = true;
Response response;
try {
response = await ApiManager.instance.post(URLs.createPmActivitySparePart, body: sparePartHelperModel!.toPMJson());
stateCode = response.statusCode;
isLoading = false;
notifyListeners();
if (stateCode == 200) {
return true;
}
return false;
} catch (error) {
isLoading = false;
stateCode = -1;
notifyListeners();
return false;
}
}
Future<String> updateActivitySparePart() async {
Response response;
try {
response = await ApiManager.instance.post(URLs.updatePmActivitySparePart, body: sparePartHelperModel!.toPMJson(), showToast: false);
if (response.statusCode == 200) {
return "";
} else {
final responseBody = jsonDecode(response.body);
if (responseBody is Map<String, dynamic>) {
final message = responseBody["message"]; // Reuse parsed body
if (message != null && message.toString().isNotEmpty) {
return message;
}
}
return "Failed, Try Again";
}
} catch (error) {
return "Failed, Try Again";
}
}
Future<int> deleteActivitySparePart({required int id}) async {
isLoading = true;
notifyListeners();
try {
final response = await ApiManager.instance.delete('${URLs.deletePmActivitySparePart}?activitySparePartPPMId=$id', body: {});
stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) {}
isLoading = false;
notifyListeners();
return response.statusCode;
} catch (error) {
isLoading = false;
notifyListeners();
return -1;
}
}
Future<bool> engineerUpdateCost() async {
isLoading = true;
Response response;
try {
response = await ApiManager.instance.post(URLs.updatePmActivityCost, body: workOrderCostModel!.toPMCostJson());
stateCode = response.statusCode;
isLoading = false;
notifyListeners();
if (stateCode == 200) {
planPreventiveVisit = PlanPreventiveVisit.fromJson(json.decode(response.body)["data"]);
// Update isReadOnlyRequest after fetching updated data
if (planPreventiveVisit?.visitStatus?.value == 3 || planPreventiveVisit?.visitStatus?.value == 5 || planPreventiveVisit?.visitStatus?.value == 6) {
isReadOnlyRequest = true;
} else {
isReadOnlyRequest = false;
}
notifyListeners();
return true;
}
return false;
} catch (error) {
isLoading = false;
stateCode = -1;
notifyListeners();
return false;
}
}
Future<void> getStoreAvailabilityById({required num? partId}) async {
try {
isLoading = true;
notifyListeners();
final response = await ApiManager.instance.post(
URLs.getSparePartAvailableQuantity + "?PartId=$partId",
body: {},
);
stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) {
final List<dynamic> data = json.decode(response.body)["data"];
storeAvailability = data.map((item) => StoreAvailability.fromJson(item)).toList();
}
isLoading = false;
notifyListeners();
} catch (e) {
log("getStoreAvalivility [error] : $e");
isLoading = false;
notifyListeners();
}
}
}

@ -142,18 +142,19 @@ class _AppBarWidgetState extends State<AppBarWidget> 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,

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

@ -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 = sparePartAttachments ?? [];
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
@ -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<String, dynamic> toPMJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id;
data['planPreventiveVisitId'] = pmVisitId;
data['partCatalogItemId'] = sparePart?.id;
data['quantity'] = quantity;
data['comment'] = comment;
data['installQty'] = installQty;
data['returnQty'] = returnQty;
if (sparePartAttachments.isNotEmpty) {
data['acitiySparePartAttachments'] = sparePartAttachments!.map((v) => v.toJson()).toList();
}
return data;
}
}

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

@ -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>? preventiveVisitKits;
List<PreventiveVisitTimers>? preventiveVisitTimers;
List<PreventiveVisitSuppliers>? preventiveVisitSuppliers;
List<PMSparePartActivityModel>? activities;
num? sparePartCost;
num? labourCost;
num? travelCost;
num? qAmount;
String? prNo;
String? poNo;
String? mrNo;
num? exchangeCost;
TimerModel? tbsTimer = TimerModel();
List<TimerModel>? 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<String, dynamic> 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 = <GenericAttachmentModel>[];
json['preventiveVisitAttachments'].forEach((v) {
@ -168,6 +197,12 @@ class PlanPreventiveVisit {
preventiveVisitSuppliers!.add(PreventiveVisitSuppliers.fromJson(v));
});
}
if (json['activitySparePartsPPM'] != null) {
activities = <PMSparePartActivityModel>[];
json['activitySparePartsPPM'].forEach((v) {
activities!.add(PMSparePartActivityModel.fromJson(v));
});
}
}
Map<String, dynamic> toJson({required int status}) {
@ -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<bool> 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<SparePartAttachments> activitySparePartPPMAttachments;
PMSparePartActivityModel({
this.id,
this.partCatalogItem,
this.activityStatus,
this.quantity,
this.installQty,
this.returnQty,
this.comment,
this.price,
List<SparePartAttachments>? activitySparePartPPMAttachments,
}) : activitySparePartPPMAttachments = activitySparePartPPMAttachments ?? [];
PMSparePartActivityModel.fromJson(Map<String, dynamic> json) : activitySparePartPPMAttachments = [] {
id = json['id'];
partCatalogItem = json['partCatalogItem'] != null ? PartCatalogItem.fromJson(json['partCatalogItem']) : null;
activityStatus = json['activityStatus'] != null ? Lookup.fromJson(json['activityStatus']) : null;
quantity = json['quantity']?.toDouble();
installQty = json['installQty']?.toDouble();
returnQty = json['returnQty']?.toDouble();
comment = json['comment'];
price = json['price'];
// Handle typo in API response: "acitiySparePartPPMAttachments" instead of "activitySparePartPPMAttachments"
final attachmentsKey = json.containsKey('acitiySparePartPPMAttachments')
? 'acitiySparePartPPMAttachments'
: 'activitySparePartPPMAttachments';
activitySparePartPPMAttachments = json[attachmentsKey] == null
? []
: List.from(json[attachmentsKey]).map((e) => SparePartAttachments.fromJson(e)).toList();
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = {};
data['id'] = id;
if (partCatalogItem != null) {
data['partCatalogItem'] = partCatalogItem!.toJson();
}
if (activityStatus != null) {
data['activityStatus'] = activityStatus!.toJson();
}
data['quantity'] = quantity;
data['installQty'] = installQty;
data['returnQty'] = returnQty;
data['comment'] = comment;
data['price'] = price;
data['activitySparePartPPMAttachments'] = activitySparePartPPMAttachments
.map(
(v) => v.toJson(),
)
.toList();
return data;
}
}

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

@ -747,23 +747,16 @@ class CMDetailProvider extends ChangeNotifier {
}
}
Future<bool> updateActivitySparePart() async {
isLoading = true;
Future<String> 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";
}
}

@ -357,27 +357,31 @@ class _ActivitiesListViewState extends State<ActivitiesListView> {
}
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,
),
));
}
}

@ -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<CMDetailProvider>(context, listen: false);
switch (index) {
case 0:
Provider.of<CMDetailProvider>(context, listen: false).sparePartHelperModel = SparePartHelperModel();
// Create fresh model - SparePartRequest will handle setting it in provider
final model = SparePartHelperModel(
id: 0,
workOrderId: requestDetailProvider.currentWorkOrder?.data?.requestId,
);
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const SparePartRequest()),
MaterialPageRoute(
builder: (context) => SparePartRequest(
model: model,
provider: requestDetailProvider,
fromCm: true,
),
),
);
break;
case 1:
CMDetailProvider requestDetailProvider = Provider.of<CMDetailProvider>(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<ServiceRequestsProvider>(
@ -804,11 +815,23 @@ class ServiceRequestBottomSheet {
buttonColor: AppColor.primary10,
loading: false,
onPressed: () {
dynamic provider = fromCm ? Provider.of<CMDetailProvider>(context, listen: false) : Provider.of<PpmProvider>(context, listen: false);
Navigator.pop(context);
Provider.of<CMDetailProvider>(context, listen: false).sparePartHelperModel = SparePartHelperModel();
final model = SparePartHelperModel(
id: 0,
workOrderId: fromCm? provider.currentWorkOrder?.data?.requestId:null,
pmVisitId: !fromCm? provider.planPreventiveVisit?.id:null,
);
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const SparePartRequest()),
MaterialPageRoute(
builder: (context) => SparePartRequest(
model: model,
provider: provider,
fromCm: fromCm,
),
),
);
},
).expanded,

@ -367,8 +367,26 @@ class _ServiceRequestDetailViewState extends State<ServiceRequestDetailView> {
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;
}),
],

@ -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<CostDetailFormScreen> with TickerProviderStateMixin {
CMDetailProvider? _requestDetailProvider;
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
class _CostDetailFormScreenState extends State<CostDetailFormScreen> {
late WorkOrderCostModel _localModel;
@override
void initState() {
super.initState();
}
void assignValues() {
// ServiceRequestDetailProvider requestDetailProvider = Provider.of<ServiceRequestDetailProvider>(context, listen: false);
// _requestDetailProvider = Provider.of<ServiceRequestDetailProvider>(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<CMDetailProvider>(context, listen: false);
assignValues();
}
return Scaffold(
key: _scaffoldKey,
appBar: const DefaultAppBar(title: "Cost Details"),
body: Consumer<CMDetailProvider>(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<void> _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<EquipmentStatusProvider>(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');
}
}
}

@ -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<SparePartRequest> with TickerProviderStateMixin {
CMDetailProvider? _requestDetailProvider;
class _SparePartRequestState extends State<SparePartRequest> {
PartsProvider? _partsProvider;
bool _isLoading = false;
List<SparePart> _spareParts = [];
List<GenericAttachmentModel> _attachments = [];
List<GenericAttachmentModel> attachments = [];
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final TextEditingController _partQtyController = TextEditingController();
final TextEditingController _installQtyController = TextEditingController();
final TextEditingController _returnQtyController = TextEditingController();
final TextEditingController _oracleNoController = TextEditingController();
final TextEditingController _descriptionController = TextEditingController();
Lookup statusLookup = Lookup.fromJson({"id": 5619, "name": "New", "value": 1});
int? activityStatus;
@override
void initState() {
super.initState();
_partsProvider = Provider.of<PartsProvider>(context, listen: false);
_requestDetailProvider = Provider.of<CMDetailProvider>(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<void> _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<CMDetailProvider>(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<Lookup, ActivityStatusProvider>(
// 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<SparePart, NullableLoadingProvider>(
context: context,
title: context.translation.part,
staticData: _spareParts,
showShadow: false,
showAsBottomSheet: true,
loading: _isLoading,
initialValue: requestDetailProvider.sparePartHelperModel?.sparePart,
// backgroundColor: context.isDark ? AppColor.neutral20 : AppColor.neutral90,
onSelect: (part) async {
if (part != null) {
requestDetailProvider.sparePartHelperModel?.sparePart = part;
_oracleNoController.text = part.oracleCode ?? '';
requestDetailProvider.updateSparePartHelperModel(_requestDetailProvider?.sparePartHelperModel);
}
},
),
if (requestDetailProvider.sparePartHelperModel?.sparePart?.id != null) ...[
12.height,
SizedBox(
height: 30.toScreenHeight,
child: Text('View Parts Availability',
style: TextStyle(
fontSize: 12.toScreenWidth,
fontWeight: FontWeight.w500,
fontStyle: FontStyle.normal,
color: AppColor.primary10,
decorationColor: AppColor.primary10,
decoration: TextDecoration.underline,
)).onPress(
() async {
requestDetailProvider.getStoreAvailabilityById(partId: requestDetailProvider.sparePartHelperModel?.sparePart?.id);
showModalBottomSheet(
context: context,
useSafeArea: true,
isScrollControlled: true,
isDismissible: true,
backgroundColor: AppColor.bottomSheetColor(context),
builder: (BuildContext context) => PartDetailBottomSheetSheet().bottomSheetContainer(context),
);
},
),
),
],
12.height,
AppTextFormField(
controller: _partQtyController,
labelText: context.translation.quantity,
textInputType: TextInputType.number,
labelStyle: AppTextStyles.textFieldLabelStyle,
// showWithoutDecoration: true,
// backgroundColor: context.isDark ? AppColor.neutral20 : AppColor.neutral90,
enable: requestDetailProvider.sparePartHelperModel?.sparePart?.id != null,
validator: (value) => value == null || value.isEmpty
? context.translation.requiredField
: Validator.isNumeric(value)
? null
: context.translation.onlyNumbers,
onChange: (value) {
requestDetailProvider.sparePartHelperModel?.quantity = num.tryParse(value ?? "");
},
onSaved: (text) {
requestDetailProvider.sparePartHelperModel?.quantity = num.tryParse(text ?? "");
},
),
12.height,
if (showInstallAndReturnQty) ...[
AppTextFormField(
controller: _installQtyController,
labelText: context.translation.installedQty,
textInputType: const TextInputType.numberWithOptions(decimal: true),
labelStyle: AppTextStyles.textFieldLabelStyle,
// showWithoutDecoration: true,
// backgroundColor: context.isDark ? AppColor.neutral20 : AppColor.neutral90,
enable: requestDetailProvider.sparePartHelperModel?.sparePart?.id != null,
validator: (value) => value == null || value.isEmpty
? context.translation.requiredField
: Validator.isNumeric(value)
? null
: context.translation.onlyNumbers,
onChange: (value) {
requestDetailProvider.sparePartHelperModel?.installQty = num.tryParse(value ?? "");
},
onSaved: (text) {
requestDetailProvider.sparePartHelperModel?.installQty = num.tryParse(text ?? "");
},
),
12.height,
AppTextFormField(
controller: _returnQtyController,
labelText: context.translation.returnQty,
textInputType: TextInputType.number,
labelStyle: AppTextStyles.textFieldLabelStyle,
// showWithoutDecoration: true,
// backgroundColor: context.isDark ? AppColor.neutral20 : AppColor.neutral90,
enable: requestDetailProvider.sparePartHelperModel?.sparePart?.id != null,
validator: (value) => value == null || value.isEmpty
? context.translation.requiredField
: Validator.isNumeric(value)
? null
: context.translation.onlyNumbers,
onChange: (value) {
requestDetailProvider.sparePartHelperModel?.returnQty = num.tryParse(value ?? "");
},
onSaved: (text) {
requestDetailProvider.sparePartHelperModel?.returnQty = num.tryParse(text ?? "");
},
),
12.height,
],
AppTextFormField(
labelStyle: AppTextStyles.textFieldLabelStyle,
controller: _oracleNoController,
labelText: context.translation.oracleNo,
textInputType: TextInputType.number,
// showWithoutDecoration: true,
backgroundColor: AppColor.disableFieldBackground(context),
enable: false,
),
12.height,
AppTextFormField(
initialValue: requestDetailProvider.sparePartHelperModel?.comment,
labelStyle: AppTextStyles.textFieldLabelStyle,
controller: _descriptionController,
showSpeechToText: true,
// backgroundColor: context.isDark ? AppColor.neutral20 : AppColor.neutral90,
labelText: "Engineer Comment",
alignLabelWithHint: true,
// showWithoutDecoration: true,
textInputType: TextInputType.multiline,
onChange: (value) {
requestDetailProvider.sparePartHelperModel?.comment = value;
},
onSaved: (value) {
requestDetailProvider.sparePartHelperModel?.comment = value;
},
),
12.height,
AttachmentPicker(
label: context.translation.attachQuotation,
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<SparePart, NullableLoadingProvider>(
context: context,
title: context.translation.part,
staticData: _spareParts,
showShadow: false,
showAsBottomSheet: true,
loading: _isLoading,
initialValue: model?.sparePart,
onSelect: (part) {
if (part != null) {
model?.sparePart = part;
provider.updateSparePartHelperModel(model);
}
},
),
if (model?.sparePart?.id != null) ...[
SizedBox(
// height: 30.toScreenHeight,
child: Text(
'View Parts Availability',
style: TextStyle(
fontSize: 12.toScreenWidth,
fontWeight: FontWeight.w500,
color: AppColor.primary10,
decorationColor: AppColor.primary10,
decoration: TextDecoration.underline,
),
).onPress(() {
provider.getStoreAvailabilityById(partId: model?.sparePart?.id);
showModalBottomSheet(
context: context,
useSafeArea: true,
isScrollControlled: true,
isDismissible: true,
backgroundColor: AppColor.bottomSheetColor(context),
builder: (ctx) => PartDetailBottomSheetSheet(provider: provider).bottomSheetContainer(ctx),
);
}),
),
);
}),
],
AppTextFormField(
key: ValueKey('quantity_${model?.quantity}'),
initialValue: model?.quantity?.round().toString() ?? '',
labelText: context.translation.quantity,
textInputType: TextInputType.number,
labelStyle: AppTextStyles.textFieldLabelStyle,
enable: model?.sparePart?.id != null,
validator: (value) => value == null || value.isEmpty
? context.translation.requiredField
: Validator.isNumeric(value)
? null
: context.translation.onlyNumbers,
onChange: (value) {
model?.quantity = num.tryParse(value);
},
onSaved: (text) {
model?.quantity = num.tryParse(text ?? '');
},
),
if (showInstallAndReturnQty) ...[
AppTextFormField(
key: ValueKey('installQty_${model?.installQty}'),
initialValue: model?.installQty?.round().toString() ?? '',
labelText: context.translation.installedQty,
textInputType: const TextInputType.numberWithOptions(decimal: true),
labelStyle: AppTextStyles.textFieldLabelStyle,
enable: model?.sparePart?.id != null,
validator: (value) => value == null || value.isEmpty
? context.translation.requiredField
: Validator.isNumeric(value)
? null
: context.translation.onlyNumbers,
onChange: (value) {
model?.installQty = num.tryParse(value);
},
onSaved: (text) {
model?.installQty = num.tryParse(text ?? '');
},
),
AppTextFormField(
key: ValueKey('returnQty_${model?.returnQty}'),
initialValue: model?.returnQty?.round().toString() ?? '',
labelText: context.translation.returnQty,
textInputType: TextInputType.number,
labelStyle: AppTextStyles.textFieldLabelStyle,
enable: model?.sparePart?.id != null,
validator: (value) => value == null || value.isEmpty
? context.translation.requiredField
: Validator.isNumeric(value)
? null
: context.translation.onlyNumbers,
onChange: (value) {
model?.returnQty = num.tryParse(value);
},
onSaved: (text) {
model?.returnQty = num.tryParse(text ?? '');
},
),
],
AppTextFormField(
key: ValueKey('oracle_${model?.sparePart?.oracleCode}'),
initialValue: model?.sparePart?.oracleCode ?? '',
labelStyle: AppTextStyles.textFieldLabelStyle,
labelText: context.translation.oracleNo,
textInputType: TextInputType.number,
backgroundColor: AppColor.disableFieldBackground(context),
enable: false,
),
AppTextFormField(
key: ValueKey('comment_${model?.comment}'),
initialValue: model?.comment ?? '',
labelStyle: AppTextStyles.textFieldLabelStyle,
showSpeechToText: true,
labelText: "Engineer Comment",
alignLabelWithHint: true,
textInputType: TextInputType.multiline,
onChange: (value) {
model?.comment = value;
},
onSaved: (value) {
model?.comment = value;
},
),
AttachmentPicker(
label: context.translation.attachQuotation,
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<void> _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<CMDetailProvider>(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);
}
}

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

@ -8,6 +8,7 @@ import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/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<PpmDetailsPage> {
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<PpmDetailsPage> {
],
).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());
}),

@ -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<UpdatePpm> 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<UpdatePpm> with TickerProviderStateMixin {
@override
void initState() {
super.initState();
ppmProvider = Provider.of<PpmProvider>(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<UpdatePpm> 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",
),
],
),
),

@ -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<WoInfoForm> {
IgnorePointer(
ignoring: ppmProvider.isReadOnly,
child: Column(
spacing: 12,
children: [
Column(
mainAxisSize: MainAxisSize.min,
@ -156,7 +159,6 @@ class _WoInfoFormState extends State<WoInfoForm> {
// '${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<WoInfoForm> {
// }
// },
// ),
8.height,
// 8.height,//
Column(
mainAxisAlignment: MainAxisAlignment.start,
spacing: 12,
children: [
SingleItemDropDownMenu<Lookup, PpmTaskStatusProvider>(
context: context,
@ -233,7 +236,6 @@ class _WoInfoFormState extends State<WoInfoForm> {
}
},
),
8.height,
ADatePicker(
label: context.translation.actualVisit,
hideShadow: true,
@ -280,8 +282,6 @@ class _WoInfoFormState extends State<WoInfoForm> {
// }
// },
// ),
8.height,
SingleItemDropDownMenu<Lookup, PpmElectricalSafetyProvider>(
context: context,
backgroundColor: AppColor.fieldBgColor(context),
@ -294,7 +294,6 @@ class _WoInfoFormState extends State<WoInfoForm> {
}
},
),
8.height,
SingleItemDropDownMenu<Lookup, PpmAssetAvailabilityProvider>(
context: context,
backgroundColor: AppColor.fieldBgColor(context),
@ -309,7 +308,6 @@ class _WoInfoFormState extends State<WoInfoForm> {
}
},
),
8.height,
SingleItemDropDownMenu<Lookup, PpmServiceProvider>(
context: context,
backgroundColor: AppColor.fieldBgColor(context),
@ -330,9 +328,7 @@ class _WoInfoFormState extends State<WoInfoForm> {
}
},
),
8.height,
_timerWidget(context, totalWorkingHours),
8.height,
AppTextFormField(
labelText: context.translation.comment,
backgroundColor: AppColor.fieldBgColor(context),
@ -344,7 +340,6 @@ class _WoInfoFormState extends State<WoInfoForm> {
widget.planPreventiveVisit.comments = value;
},
),
16.height,
AttachmentPicker(
label: context.translation.attachments,
showAsListView: true,
@ -356,6 +351,7 @@ class _WoInfoFormState extends State<WoInfoForm> {
),
],
).toShadowContainer(context),
costCard(context, ppmProvider),
],
),
),
@ -412,6 +408,7 @@ class _WoInfoFormState extends State<WoInfoForm> {
Widget _timerWidget(BuildContext context, double totalWorkingHours) {
return Column(
spacing: 12,
children: [
AppTimer(
label: context.translation.workingHours,
@ -433,7 +430,6 @@ class _WoInfoFormState extends State<WoInfoForm> {
return true;
},
),
11.height,
if (totalWorkingHours > 0.0) ...[
WorkingTimeTile(
timerList: timerList,
@ -453,6 +449,79 @@ class _WoInfoFormState extends State<WoInfoForm> {
);
}
Widget costCard(BuildContext context, PpmProvider provider) {
///Need to update this when actual flow decided...
PlanPreventiveVisit? model = provider.planPreventiveVisit;
final costModel = WorkOrderCostModel(
workOrderId: int.tryParse(model?.id ?? ''),
labourCost: model?.labourCost,
sparePartCost: model?.sparePartCost,
travelCost: model?.travelCost,
qAmount: model?.qAmount,
prNo: model?.prNo,
poNo: model?.poNo,
mrNo: model?.mrNo,
exchangeCost: model?.exchangeCost,
);
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
InfoHeader16Widget("Cost"),
const Spacer(),
if (!provider.isReadOnlyRequest)
"edit_icon".toSvgAsset(height: 24, width: 24, color: AppColor.icon2Color(context)).onPress(() async {
provider.workOrderCostModel = costModel;
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CostDetailFormScreen(
model: provider.workOrderCostModel,
provider: provider,
),
),
);
}),
],
),
InfoTextWidget(
label: 'Spare Part Cost'.addTranslation,
value: costModel.sparePartCost?.toString() ?? '-',
),
InfoTextWidget(
label: 'Labor Cost'.addTranslation,
value: costModel.labourCost?.toString() ?? '-',
),
InfoTextWidget(
label: 'Travel Cost'.addTranslation,
value: costModel.travelCost?.toString() ?? '-',
),
InfoTextWidget(
label: 'Quot Amount'.addTranslation,
value: costModel.qAmount?.toString() ?? '-',
),
InfoTextWidget(
label: 'PR No'.addTranslation,
value: costModel.prNo?.toString() ?? '-',
),
InfoTextWidget(
label: 'PO No'.addTranslation,
value: costModel.poNo?.toString() ?? '-',
),
InfoTextWidget(
label: 'MR No'.addTranslation,
value: costModel.mrNo?.toString() ?? '-',
),
InfoTextWidget(
label: 'Exchange Cost'.addTranslation,
value: costModel.exchangeCost?.toString() ?? '-',
),
],
).toShadowContainer(context);
}
// String formatDuration1(int seconds) {
// int hours = seconds ~/ 3600;
// int minutes = (seconds % 3600) ~/ 60;

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

@ -175,11 +175,12 @@ class _SiteRequestFormScreenState extends State<SiteRequestFormScreen> {
_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<SiteRequestFormScreen> {
}
Widget _buildSiteMultiSelect() {
return MultipleItemDropDownMenu<SiteRequestSiteModel, NullableLoadingProvider>(
context: context,
title: context.translation.site,
initialValue: _selectedSites
.map((s) => SiteRequestSiteModel(
id: s.identifier ?? '',
name: s.name ?? '',
))
.toList(),
staticData: _provider.userFilterSites,
showAsFullScreenDialog: true,
backgroundColor: context.isDark ? AppColor.neutral60 : Colors.white,
showShadow: true,
height: 56.toScreenHeight,
showCancel: true,
onSelect: (value) {
if (value != null) {
setState(() {
_selectedSites = value.map((s) {
final site = Site(
id: int.tryParse(s.identifier ?? '0'),
custName: s.name,
);
return site;
}).toList();
final departmentProvider = Provider.of<DepartmentBySitesProvider>(context, listen: false);
final siteIds = value.map((site) => int.tryParse(site.identifier ?? '0') ?? 0).toList();
departmentProvider.setSiteIds(siteIds);
departmentProvider.getData();
});
}
},
);
return MultipleItemDropDownMenu<Site, SiteProvider>(
context: context,
title: context.translation.site,

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

@ -5,17 +5,17 @@ import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/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<DeviceFilterScreen> {
late DeviceSearch filter;
Lookup? _commissioningStatus;
List<Site> _selectedSites = [];
List<SiteRequestSiteModel> _selectedSites = [];
List<Lookup> _selectedDepartments = [];
String? startDate, endDate;
bool loading = false;
List<SiteRequestSiteModel> userSites = [];
@override
void initState() {
filter = widget.data ?? DeviceSearch();
@ -47,11 +49,15 @@ class _DeviceFilterScreenState extends State<DeviceFilterScreen> {
startDate = filter.supplyDateFrom;
endDate = filter.supplyDateTo;
_commissioningStatus = filter.assetStatus;
final departmentProvider = Provider.of<DepartmentProvider>(context, listen: false);
final siteProvider = Provider.of<SiteProvider>(context, listen: false);
Utils.showLoading(context);
if (departmentProvider.items.isEmpty ?? true) await departmentProvider.getData();
if (siteProvider.items.isEmpty ?? true) await siteProvider.getData();
if (userSites.isEmpty) {
userSites = await Provider.of<SiteRequestProvider>(context, listen: false).getSitesByUserId(context.userProvider.user?.userID ?? "");
}
await Future.delayed(const Duration(milliseconds: 100));
Utils.hideLoading(context);
// Initialize selected departments from filter
if (filter.departments != null && filter.departments!.isNotEmpty) {
_selectedDepartments = [];
@ -70,11 +76,11 @@ class _DeviceFilterScreenState extends State<DeviceFilterScreen> {
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<DeviceFilterScreen> {
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<Lookup, DepartmentProvider>(
context: context,
@ -145,11 +152,12 @@ class _DeviceFilterScreenState extends State<DeviceFilterScreen> {
},
),
12.height,
MultipleItemDropDownMenu<Site, SiteProvider>(
MultipleItemDropDownMenu<SiteRequestSiteModel, NullableLoadingProvider>(
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,

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

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

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

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

@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# 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"

Loading…
Cancel
Save