Merge branch 'refs/heads/main_production_ai_enhancement_rollout_changes' into ui_ux_rollout_merge

# Conflicts:
#	lib/controllers/api_routes/urls.dart
#	lib/dashboard_latest/widgets/progress_fragment.dart
#	lib/dashboard_latest/widgets/request_category_fragment.dart
#	lib/models/new_models/dashboard_count.dart
#	lib/modules/cleaner_module/pages/cleaner_request_item_view.dart
#	lib/modules/cleaner_module/pages/cleaner_search_form_view.dart
#	lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart
#	lib/modules/pm_module/recurrent_wo/components/task_info_widget.dart
#	lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart
#	lib/modules/tm_module/device_transfer/device_filter_screen.dart
#	lib/modules/traf_module/traf_request_detail_page.dart
#	lib/modules/traf_module/traf_request_item_view.dart
#	lib/new_views/pages/land_page/create_request_type_bottom_sheet.dart
#	lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart
#	lib/new_views/pages/land_page/widgets/request_item_view_list.dart
ui_ux_rollout_merge
Sikander Saleem 1 week ago
commit d136fd990e

@ -1,14 +1,13 @@
class URLs { class URLs {
URLs._(); URLs._();
static const String appReleaseBuildNumber = "40"; static const String appReleaseBuildNumber = "42";
// static const host1 = "https://atomsm.hmg.com"; // production url // static const host1 = "https://atomsm.hmg.com"; // production url
// static const host1 = "https://atomsmdev.hmg.com"; // local DEV url // static const host1 = "https://atomsmdev.hmg.com"; // local DEV url
static const host1 = "https://atomsmuat.hmg.com"; // local UAT url static const host1 = "https://atomsmuat.hmg.com"; // local UAT url
// static final String _baseUrl = "$_host/mobile"; // host local UAT // static final String _baseUrl = "$_host/mobile"; // host local UAT
// static final String _baseUrl = "$_host/v1/mobile"; // new V1 apis
static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis
// static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM // static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM
// static final String _baseUrl = "$_host/v4/mobile"; // v4 for Demo module // static final String _baseUrl = "$_host/v4/mobile"; // v4 for Demo module
@ -73,8 +72,12 @@ class URLs {
static get searchPlanRecurrentTaskListSearch => "$_baseUrl/PlanRecurrentTasks/PlanRecurrentTaskListSearchDto"; // get static get searchPlanRecurrentTaskListSearch => "$_baseUrl/PlanRecurrentTasks/PlanRecurrentTaskListSearchDto"; // get
static get checkQRSameRoom => "$_baseUrl/PlanRecurrentTasks/CheckQRSameRoom"; // get static get checkQRSameRoom => "$_baseUrl/PlanRecurrentTasks/CheckQRSameRoom"; // get
// //One Time Task Urls cleaner module new flow urls ....
static get getTaskType => "$_baseUrl/OneTimeTasks/GetTaskTypes";
static get scanQRCode => "$_baseUrl/OneTimeTasks/ScanQRCode";
static get proceedRequest => "$_baseUrl/OneTimeTasks/Proceed";
static get completeRequest => "$_baseUrl/OneTimeTasks/Complete";
static get getRequestById => "$_baseUrl/OneTimeTasks/GetById";
//service request new flow urls. //service request new flow urls.
static get nurseDashboardCountUrl => '$_baseUrl/ServiceRequest/GetDashboardNurseCount'; static get nurseDashboardCountUrl => '$_baseUrl/ServiceRequest/GetDashboardNurseCount';
@ -369,6 +372,7 @@ class URLs {
static get getSiteContactInfo => "$_baseUrl/AssetGroupSiteContactInfo"; // add static get getSiteContactInfo => "$_baseUrl/AssetGroupSiteContactInfo"; // add
static get getDepartmentBasedOnSite => "$_baseUrl/TRAFDataSource/GetDepartmentBasedOnSite"; // add static get getDepartmentBasedOnSite => "$_baseUrl/TRAFDataSource/GetDepartmentBasedOnSite"; // add
static get getCustomersByUserId => "$_baseUrl/TRAFDataSource/GetCustomersByUserId"; // add
// asset inventory Urls. // asset inventory Urls.
static get getAssetInventoryById => '$_baseUrl/AssetInventory/GetAssetInventoryById'; static get getAssetInventoryById => '$_baseUrl/AssetInventory/GetAssetInventoryById';

@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:developer';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
@ -157,14 +158,20 @@ class AllRequestsProvider extends ChangeNotifier {
list.add(7); // task mod list.add(7); // task mod
} }
// switching asset inventory to users permissions //TODO uncommit this to enable TRAF in production.
context.userProvider.modulesPermissionsList.forEach((module) { context.userProvider.modulesPermissionsList.forEach((module) {
if (module.module!.value! == 8 || module.module!.value! == 9) { if (module.module!.value! == 8 || module.module!.value! == 9) {
list.add(module.module!.value!); list.add(module.module!.value!);
} }
}); });
//Todo commit this when passing to production.
// if (context.settingProvider.isUserFlowMedical && context.userProvider.isNurse) { // context.userProvider.modulesPermissionsList.forEach((module) {
// if (module.module!.value! == 8) {
// list.add(module.module!.value!);
// }
// });
//
// if (context.settingProvider.isUserFlowMedical && !context.userProvider.isAssessor && (context.userProvider.isNurse || context.userProvider.isEngineer)) {
// list.add(9); // // list.add(9); //
// } // }
list.add(12); list.add(12);
@ -177,6 +184,10 @@ class AllRequestsProvider extends ChangeNotifier {
// list = [16]; // list = [16];
list.add(16); list.add(16);
} }
if ( context.userProvider.isHouseKeeper) {
list = [5];
}
return list; return list;
} }
@ -274,23 +285,27 @@ class AllRequestsProvider extends ChangeNotifier {
} }
} }
Future<bool> updateRecurrentWo({required int status}) async { Future<Map<String, dynamic>> updateRecurrentWo({required int status, bool showToast = true}) async {
isLoading = true;
Response response; Response response;
try { try {
response = await ApiManager.instance.post(URLs.updateRecurrentPlanUrl, body: recurrentWoData!.toJson(status: status)); response = await ApiManager.instance.post(URLs.updateRecurrentPlanUrl, body: recurrentWoData!.toJson(status: status), showToast: showToast);
stateCode = response.statusCode; stateCode = response.statusCode;
isLoading = false;
notifyListeners();
if (stateCode == 200) { if (stateCode == 200) {
return true; return {'success': true, 'message': ''};
} else {
// Parse error message from response
try {
final responseBody = jsonDecode(response.body);
String errorMessage = responseBody['message'] ?? responseBody['title'] ?? 'Failed to update the task';
return {'success': false, 'message': errorMessage};
} catch (e) {
return {'success': false, 'message': 'Failed to update the task. Status code: $stateCode'};
}
} }
return false;
} catch (error) { } catch (error) {
isLoading = false;
stateCode = -1; stateCode = -1;
notifyListeners(); return {'success': false, 'message': error.toString()};
return false;
} }
} }

@ -51,6 +51,7 @@ class UserProvider extends ChangeNotifier {
bool get isQualityUser => user!.type == UsersTypes.qualityUser; bool get isQualityUser => user!.type == UsersTypes.qualityUser;
bool get isCommissioningEngineer => user!.type == UsersTypes.commissioningEngineer; bool get isCommissioningEngineer => user!.type == UsersTypes.commissioningEngineer;
bool get isHouseKeeper => user!.type == UsersTypes.houseKeeper;
VerifyOtpModel _verifyOtpModel = VerifyOtpModel(); VerifyOtpModel _verifyOtpModel = VerifyOtpModel();
SwipeTransaction _swipeTransactionModel = SwipeTransaction(); SwipeTransaction _swipeTransactionModel = SwipeTransaction();

@ -205,6 +205,7 @@ class DashBoardProvider extends ChangeNotifier {
UsersTypes.qualityUser: URLs.qaAuditorCount, UsersTypes.qualityUser: URLs.qaAuditorCount,
UsersTypes.commissioningEngineer: URLs.commissioningEngineerCount, UsersTypes.commissioningEngineer: URLs.commissioningEngineerCount,
UsersTypes.engineer: URLs.engineerDashboardCountUrl, UsersTypes.engineer: URLs.engineerDashboardCountUrl,
UsersTypes.houseKeeper: URLs.engineerDashboardCountUrl,
UsersTypes.assessor: URLs.engineerDashboardCountUrl, UsersTypes.assessor: URLs.engineerDashboardCountUrl,
UsersTypes.assessorTl: URLs.engineerDashboardCountUrl, UsersTypes.assessorTl: URLs.engineerDashboardCountUrl,
}; };
@ -407,6 +408,7 @@ class DashBoardProvider extends ChangeNotifier {
case UsersTypes.engineer: case UsersTypes.engineer:
case UsersTypes.assessor: case UsersTypes.assessor:
case UsersTypes.assessorTl: case UsersTypes.assessorTl:
case UsersTypes.houseKeeper:
if (status == 0) { if (status == 0) {
return URLs.engineerDashboardUpcoming; return URLs.engineerDashboardUpcoming;
} else if (status == 1 && tabId == 1) { } else if (status == 1 && tabId == 1) {
@ -574,6 +576,14 @@ class CategoryTabs {
return tabs; return tabs;
} }
if (userType == UsersTypes.houseKeeper) {
// tabs.add(CategoryTabs(label: 'Upcoming', tag: 0, id: 0));
// tabs.add(CategoryTabs('Open', 1));
// tabs.add(CategoryTabs(label: 'UnAssigned', tag: 1, id: 1));
tabs.add(CategoryTabs(label: 'In Progress', tag: 2, id: 2));
tabs.add(CategoryTabs(label: 'Completed', tag: 4, id: 3));
return tabs;
}
if (userType == UsersTypes.qualityUser) { if (userType == UsersTypes.qualityUser) {
tabs.add(CategoryTabs(label: 'Open', tag: 1, id: 2, requestType: RequestTypeEnum.open, icon: "dashboard/open_icon", iconColor: AppColor.openColor)); tabs.add(CategoryTabs(label: 'Open', tag: 1, id: 2, requestType: RequestTypeEnum.open, icon: "dashboard/open_icon", iconColor: AppColor.openColor));

@ -226,12 +226,19 @@ class _ProgressFragmentState extends State<ProgressFragment> with SingleTickerPr
ChartData('Acknowledged', snapshot.dashboardCount?.data?.countComplete?.toDouble() ?? 0.0, AppColor.completeChartColor, "dashboard/completed_icon"), ChartData('Acknowledged', snapshot.dashboardCount?.data?.countComplete?.toDouble() ?? 0.0, AppColor.completeChartColor, "dashboard/completed_icon"),
]; ];
} else if (_userProvider.isQualityUser) { } else if (_userProvider.isQualityUser) {
total = (snapshot.dashboardCount?.data?.countComplete ?? 0) + (snapshot.dashboardCount?.data?.countInprogress ?? 0) + (snapshot.dashboardCount?.data?.countOpen ?? 0); total = (snapshot.dashboardCount?.data?.countClose ?? 0) + (snapshot.dashboardCount?.data?.countInprogress ?? 0) + (snapshot.dashboardCount?.data?.countOpen ?? 0);
chartData = [ chartData = [
ChartData('Open', snapshot.dashboardCount?.data?.countOpen?.toDouble() ?? 0.0, AppColor.openChartColor, "dashboard/open_icon"), ChartData('Open', snapshot.dashboardCount?.data?.countOpen?.toDouble() ?? 0.0, AppColor.openChartColor, "dashboard/open_icon"),
ChartData('In Progress', snapshot.dashboardCount?.data?.countInprogress?.toDouble() ?? 0.0, AppColor.inProgressChartColor, "dashboard/inprogress_icon"), ChartData('In Progress', snapshot.dashboardCount?.data?.countInprogress?.toDouble() ?? 0.0, AppColor.inProgressChartColor, "dashboard/inprogress_icon"),
ChartData('Closed', snapshot.dashboardCount?.data?.countComplete?.toDouble() ?? 0.0, AppColor.completeChartColor, "dashboard/completed_icon"), ChartData('Closed', snapshot.dashboardCount?.data?.countComplete?.toDouble() ?? 0.0, AppColor.completeChartColor, "dashboard/completed_icon"),
]; ];
} else if (_userProvider.isHouseKeeper) {
total = (snapshot.dashboardCount?.data?.countClose ?? 0) + (snapshot.dashboardCount?.data?.countInprogress ?? 0) + (snapshot.dashboardCount?.data?.countOpen ?? 0);
chartData = [
ChartData('In Progress', snapshot.dashboardCount?.data?.countInprogress?.toDouble() ?? 0.0, AppColor.orange70),
ChartData('Completed', snapshot.dashboardCount?.data?.countClose?.toDouble() ?? 0.0, AppColor.green70),
// ChartData('Open', snapshot.dashboardCount?.data?.countOpen?.toDouble() ?? 0.0, AppColor.blueStatus(context)),
];
} else { } else {
total = (snapshot.dashboardCount?.data?.countComplete ?? 0) + (snapshot.dashboardCount?.data?.countInprogress ?? 0) + (snapshot.dashboardCount?.data?.countOpen ?? 0); total = (snapshot.dashboardCount?.data?.countComplete ?? 0) + (snapshot.dashboardCount?.data?.countInprogress ?? 0) + (snapshot.dashboardCount?.data?.countOpen ?? 0);
chartData = [ chartData = [

@ -7,6 +7,7 @@ import 'package:test_sa/dashboard_latest/widgets/request_category_list.dart';
import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/enum_extensions.dart'; import 'package:test_sa/extensions/enum_extensions.dart';
import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/enums/user_types.dart'; import 'package:test_sa/models/enums/user_types.dart';
@ -111,16 +112,51 @@ class _RequestCategoryFragmentState extends State<RequestCategoryFragment> with
final user = Provider.of<UserProvider>(context, listen: false).user; final user = Provider.of<UserProvider>(context, listen: false).user;
final settingProvider = Provider.of<SettingProvider>(context, listen: false); final settingProvider = Provider.of<SettingProvider>(context, listen: false);
// Check if user is FMS Engineer using SettingProvider // Check if user is houseKeeper
final bool isFMSEngineer = user?.type == UsersTypes.engineer && settingProvider.isUserFMS; final bool isHouseKeeper = user?.type == UsersTypes.houseKeeper;
return Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ return Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
!isNurse && dashboardProvider.tabs.isNotEmpty && dashboardProvider.tabs[dashboardProvider.currentListIndex].tag == 0
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
context.translation.allWorkOrder.heading4(context),
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// context.translation.allWorkOrder.heading4(context),
// GestureDetector(
// onTap: () {
// dashboardProvider.isShowDashBoardCalendar = !dashboardProvider.isShowDashBoardCalendar;
// },
// child: dashboardProvider.isShowDashBoardCalendar ? AppAsset.listIcon.toSvgAsset() : AppAsset.calendarIcon.toSvgAsset())
// ],
// ),
dashboardProvider.isShowDashBoardCalendar
? Padding(
padding: EdgeInsets.only(top: 12.toScreenHeight),
child: const WeeklyCalendarFragment(),
)
: const SizedBox(),
],
).paddingOnly(start: 14, end: 12)
: isHouseKeeper
? Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
'All Tasks'.addTranslation.heading4(context),
'add_icon'.toSvgAsset(width: 20, height: 20).onPress(() {
Navigator.of(context).pushNamed('/cleaner_search');
}),
],
).paddingOnly(start: 16, end: 16)
: context.translation.allWorkOrder.heading4(context).paddingOnly(start: 16, end: 16),
getTabs( getTabs(
requestsProvider: dashboardProvider, requestsProvider: dashboardProvider,
context: context, context: context,
userType: Provider.of<UserProvider>(context, listen: false).user?.type, userType: Provider.of<UserProvider>(context, listen: false).user?.type,
), ),
FadeTransition( FadeTransition(f
opacity: _headerFadeAnimation, opacity: _headerFadeAnimation,
child: SlideTransition( child: SlideTransition(
position: _headerSlideAnimation, position: _headerSlideAnimation,

@ -4,6 +4,7 @@ import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/new_models/dashboard_detail.dart'; import 'package:test_sa/models/new_models/dashboard_detail.dart';
import 'package:test_sa/modules/asset_delivery_module/pages/asset_delivery_item_view.dart'; import 'package:test_sa/modules/asset_delivery_module/pages/asset_delivery_item_view.dart';
import 'package:test_sa/modules/asset_inventory_module/pages/inventory_session_item_view.dart'; import 'package:test_sa/modules/asset_inventory_module/pages/inventory_session_item_view.dart';
import 'package:test_sa/modules/cleaner_module/widgets/one_time_task_item_view.dart';
import 'package:test_sa/modules/demo_module/demo_request_item_view.dart'; import 'package:test_sa/modules/demo_module/demo_request_item_view.dart';
import 'package:test_sa/modules/internal_audit_module/pages/equipment_internal_audit/equipment_internal_audit_item_view.dart'; import 'package:test_sa/modules/internal_audit_module/pages/equipment_internal_audit/equipment_internal_audit_item_view.dart';
import 'package:test_sa/modules/loan_module/pages/loan_equipment_item_view.dart'; import 'package:test_sa/modules/loan_module/pages/loan_equipment_item_view.dart';
@ -131,6 +132,8 @@ class _RequestCategoryListState extends State<RequestCategoryList> with SingleTi
return MedicalGasInspectionItemView(requestData: request); return MedicalGasInspectionItemView(requestData: request);
case 15: case 15:
return DemoRequestItemView(requestData: request); return DemoRequestItemView(requestData: request);
case 17:
return OneTimeTaskItemView(requestData: request);
default: default:
return Container( return Container(
height: 100, height: 100,

@ -137,13 +137,13 @@ import 'modules/tm_module/device_transfer/device_search_screen.dart';
import 'modules/tm_module/tasks/create_task_view.dart'; import 'modules/tm_module/tasks/create_task_view.dart';
import 'modules/tm_module/gas_refill/gas_refill_request_form.dart'; import 'modules/tm_module/gas_refill/gas_refill_request_form.dart';
import 'providers/lookups/classification_lookup_provider.dart'; import 'providers/lookups/classification_lookup_provider.dart';
import 'providers/lookups/customer_lookup_provider.dart';
import 'providers/lookups/department_lookup_provider.dart'; import 'providers/lookups/department_lookup_provider.dart';
import 'providers/lookups/request_type_lookup_provider.dart'; import 'providers/lookups/request_type_lookup_provider.dart';
import 'providers/lookups/yes_no_lookup_provider.dart'; import 'providers/lookups/yes_no_lookup_provider.dart';
import 'providers/service_request_providers/loan_availability_provider.dart'; import 'providers/service_request_providers/loan_availability_provider.dart';
import 'providers/service_request_providers/reject_reason_provider.dart'; import 'providers/service_request_providers/reject_reason_provider.dart';
import 'modules/cleaner_module/pages/cleaner_search_form_view.dart'; import 'modules/cleaner_module/pages/cleaner_search_form_view.dart';
import 'modules/cleaner_module/pages/cleaner_search_list_view.dart';
import 'modules/cleaner_module/provider/cleaner_provider.dart'; import 'modules/cleaner_module/provider/cleaner_provider.dart';
// class MyHttpOverrides extends HttpOverrides { // class MyHttpOverrides extends HttpOverrides {
@ -236,6 +236,7 @@ class MyApp extends StatelessWidget {
ChangeNotifierProvider(create: (_) => RequestTypeLookupProvider(), lazy: true), ChangeNotifierProvider(create: (_) => RequestTypeLookupProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => YesNoLookupProvider(), lazy: true), ChangeNotifierProvider(create: (_) => YesNoLookupProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => DepartmentLookupProvider(), lazy: true), ChangeNotifierProvider(create: (_) => DepartmentLookupProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => CustomerLookupProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => EndUserStatusLookupProvider(), lazy: true), ChangeNotifierProvider(create: (_) => EndUserStatusLookupProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => EndUserRejectionReasonLookupProvider(), lazy: true), ChangeNotifierProvider(create: (_) => EndUserRejectionReasonLookupProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => AttachmentTypeLookupProvider(), lazy: true), ChangeNotifierProvider(create: (_) => AttachmentTypeLookupProvider(), lazy: true),
@ -416,7 +417,6 @@ class MyApp extends StatelessWidget {
CreateMedicalGasRequestPage.id: (_) => CreateMedicalGasRequestPage(), CreateMedicalGasRequestPage.id: (_) => CreateMedicalGasRequestPage(),
CreateDemoRequestPage.id: (_) => CreateDemoRequestPage(), CreateDemoRequestPage.id: (_) => CreateDemoRequestPage(),
CleanerSearchFormView.routeName: (_) => const CleanerSearchFormView(), CleanerSearchFormView.routeName: (_) => const CleanerSearchFormView(),
CleanerSearchListView.routeName: (_) => const CleanerSearchListView(),
}, },
), ),
), ),

@ -134,36 +134,47 @@ class RequestsDetails {
int? transactionType; int? transactionType;
int? numberOfAssets; int? numberOfAssets;
int? numberOfSites; int? numberOfSites;
String? siteName;
String? buildingName;
String? departmentName;
String? floorName;
String? roomName;
RequestsDetails( RequestsDetails({
{this.id, this.id,
this.nameOfType, this.nameOfType,
this.priority, this.priority,
this.status, this.status,
this.statusValue, this.statusValue,
this.assetName, this.assetName,
this.assetNo, this.assetNo,
this.assetSN, this.assetSN,
this.model, this.model,
this.supplier, this.supplier,
this.manufacturer, this.manufacturer,
this.requestType, this.requestType,
this.requestNo, this.requestNo,
this.numberOfAssets, this.numberOfAssets,
this.numberOfSites, this.numberOfSites,
this.sessionType, this.sessionType,
this.gasType, this.gasType,
this.site, this.site,
this.statusReceiver, this.statusReceiver,
this.assetTransferFrom, this.assetTransferFrom,
this.assetTransferTo, this.assetTransferTo,
this.code, this.code,
this.siteTransferFrom, this.siteTransferFrom,
this.siteTransferTo, this.siteTransferTo,
this.poInternal, this.poInternal,
this.poExternal, this.poExternal,
this.date, this.date,
this.transactionType}); this.transactionType,
this.siteName,
this.buildingName,
this.departmentName,
this.floorName,
this.roomName,
});
RequestsDetails.fromJson(Map<String, dynamic> json) { RequestsDetails.fromJson(Map<String, dynamic> json) {
id = json['id']; id = json['id'];
@ -194,6 +205,11 @@ class RequestsDetails {
siteTransferFrom = json['siteTransferFrom']; siteTransferFrom = json['siteTransferFrom'];
siteTransferTo = json['siteTransferTo']; siteTransferTo = json['siteTransferTo'];
transactionType = json['transactionType']; transactionType = json['transactionType'];
siteName = json['siteName'];
buildingName = json['buildingName'];
departmentName = json['departmentName'];
floorName = json['floorName'];
roomName = json['roomName'];
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@ -224,6 +240,11 @@ class RequestsDetails {
data['siteTransferFrom'] = siteTransferFrom; data['siteTransferFrom'] = siteTransferFrom;
data['siteTransferTo'] = siteTransferTo; data['siteTransferTo'] = siteTransferTo;
data['transactionType'] = transactionType; data['transactionType'] = transactionType;
data['siteName'] = siteName;
data['buildingName'] = buildingName;
data['departmentName'] = departmentName;
data['floorName'] = floorName;
data['roomName'] = roomName;
return data; return data;
} }
} }

@ -5,5 +5,6 @@ enum UsersTypes {
assessor, assessor,
assessorTl, assessorTl,
qualityUser, qualityUser,
commissioningEngineer commissioningEngineer,
houseKeeper
} }

@ -63,6 +63,11 @@ class Data {
int? numberOfSites; int? numberOfSites;
int? numberOfAssets; int? numberOfAssets;
String? nameOfType; String? nameOfType;
String? siteName;
String? buildingName;
String? floorName;
String? departmentName;
String? roomName;
Data( Data(
{this.id, {this.id,
@ -80,10 +85,15 @@ class Data {
this.assetNumber, this.assetNumber,
this.requestTypeName, this.requestTypeName,
this.requestNo, this.requestNo,
this.requestType, this.requestType,
this.poExternal, this.poExternal,
this.poInternal, this.poInternal,
this.transactionNo}); this.transactionNo,
this.siteName,
this.departmentName,
this.buildingName,
this.floorName,
this.roomName});
Data.fromJson(Map<String, dynamic> json) { Data.fromJson(Map<String, dynamic> json) {
id = json['id']; id = json['id'];
@ -106,6 +116,11 @@ class Data {
poInternal = json['poInternal']; poInternal = json['poInternal'];
poExternal = json['poExternal']; poExternal = json['poExternal'];
nameOfType = json['nameOfType']; nameOfType = json['nameOfType'];
siteName = json['siteName'];
departmentName = json['departmentName'];
buildingName = json['buildingName'];
floorName = json['floorName'];
roomName = json['roomName'];
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@ -127,6 +142,11 @@ class Data {
data['numberOfAssets'] = numberOfAssets; data['numberOfAssets'] = numberOfAssets;
data['numberOfSites'] = numberOfSites; data['numberOfSites'] = numberOfSites;
data['nameOfType'] = nameOfType; data['nameOfType'] = nameOfType;
data['siteName'] = siteName;
data['buildingName'] = buildingName;
data['departmentName'] = departmentName;
data['floorName'] = floorName;
data['roomName'] = roomName;
return data; return data;
} }
} }

@ -0,0 +1,62 @@
import '../base.dart';
class TrafCustomer extends Base {
int? customerId;
int? langId;
String? nameValue;
String? customerName;
String? languageName;
int? id;
String? createdBy;
String? createdDate;
String? modifiedBy;
String? modifiedDate;
TrafCustomer({
this.customerId,
this.langId,
this.nameValue,
this.customerName,
this.languageName,
this.id,
this.createdBy,
this.createdDate,
this.modifiedBy,
this.modifiedDate,
}) : super(identifier: id?.toString() ?? '', name: nameValue ?? customerName);
factory TrafCustomer.fromJson(Map<String, dynamic> json) {
return TrafCustomer(
customerId: json['customerId'],
langId: json['langId'],
nameValue: json['nameValue'],
customerName: json['customerName'],
languageName: json['languageName'],
id: json['id'],
createdBy: json['createdBy'],
createdDate: json['createdDate'],
modifiedBy: json['modifiedBy'],
modifiedDate: json['modifiedDate'],
);
}
Map<String, dynamic> toJson() {
return {
'customerId': customerId,
'langId': langId,
'nameValue': nameValue,
'customerName': customerName,
'languageName': languageName,
'id': id,
'createdBy': createdBy,
'createdDate': createdDate,
'modifiedBy': modifiedBy,
'modifiedDate': modifiedDate,
};
}
@override
String toString() {
return nameValue ?? customerName ?? '';
}
}

@ -3,9 +3,9 @@ import 'package:test_sa/models/new_models/room_model.dart';
import '../base.dart'; import '../base.dart';
class TrafDepartment extends Base { class TrafDepartment extends Base {
TrafDepartment({ TrafDepartment(
this.departmentName, this.departmentCode, this.ntCode, this.costCenterNumber, this.costCenterName, this.name, this.id, this.createdBy, this.createdDate, this.modifiedBy, this.modifiedDate {this.departmentName, this.departmentCode, this.ntCode, this.costCenterNumber, this.costCenterName, this.name, this.id, this.createdBy, this.createdDate, this.modifiedBy, this.modifiedDate})
}) : super(identifier: id?.toString() ?? '', name: departmentName); // Handle potential null id : super(identifier: id?.toString() ?? '', name: departmentName); // Handle potential null id
TrafDepartment.fromJson(Map<String, dynamic> json) { TrafDepartment.fromJson(Map<String, dynamic> json) {
departmentName = json['departmentName'] ?? json['name']; departmentName = json['departmentName'] ?? json['name'];
@ -15,6 +15,7 @@ class TrafDepartment extends Base {
costCenterName = json['costCenterName']; costCenterName = json['costCenterName'];
name = json['name']; name = json['name'];
id = json['id']; id = json['id'];
identifier = id?.toString() ?? '';
createdBy = json['createdBy']; createdBy = json['createdBy'];
createdDate = json['createdDate']; createdDate = json['createdDate'];
modifiedBy = json['modifiedBy']; modifiedBy = json['modifiedBy'];
@ -33,24 +34,22 @@ class TrafDepartment extends Base {
String? modifiedBy; String? modifiedBy;
String? modifiedDate; String? modifiedDate;
// TrafDepartment copyWith({ // TrafDepartment copyWith({
// num? id, // Parameters are now nullable // num? id, // Parameters are now nullable
// String? departmentName, // String? departmentName,
// String? departmentCode, // String? departmentCode,
// String? departmentId, // String? departmentId,
// String? ntCode, // String? ntCode,
// List<Rooms>? rooms, // List<Rooms>? rooms,
// }) => // }) =>
// TrafDepartment( // TrafDepartment(
// id: id ?? this.id, // id: id ?? this.id,
// departmentName: departmentName ?? this.departmentName, // departmentName: departmentName ?? this.departmentName,
// departmentCode: departmentCode ?? this.departmentCode, // departmentCode: departmentCode ?? this.departmentCode,
// departmentId: departmentId ?? this.departmentId, // departmentId: departmentId ?? this.departmentId,
// ntCode: ntCode ?? this.ntCode, // ntCode: ntCode ?? this.ntCode,
// rooms: rooms ?? this.rooms, // rooms: rooms ?? this.rooms,
// ); // );
} }
// class TrafDepartment extends Base { // class TrafDepartment extends Base {

@ -117,6 +117,8 @@ class User {
return UsersTypes.qualityUser; return UsersTypes.qualityUser;
case "R-201": // Quality User case "R-201": // Quality User
return UsersTypes.commissioningEngineer; return UsersTypes.commissioningEngineer;
case "R-204": // Quality User
return UsersTypes.houseKeeper;
default: default:
return null; return null;
} }

@ -1,24 +0,0 @@
// Cleaner request search model for mock/static data
class CleanerRequest {
final String priority;
final String date;
final String time;
final String roomNo;
final String site;
final String building;
final String floor;
final String clinic;
final String? taskNo;
CleanerRequest({
required this.priority,
required this.date,
required this.time,
required this.roomNo,
required this.site,
required this.building,
required this.floor,
required this.clinic,
this.taskNo,
});
}

@ -0,0 +1,247 @@
class OneTimeTaskData {
int? id;
int? oneTimeSettingId;
OneTimeSetting? oneTimeSetting;
String? userId;
String? userName;
int? siteId;
String? siteName;
int? buildingId;
String? buildingName;
int? floorId;
String? floorName;
int? departmentID;
String? departmentName;
int? roomId;
String? roomName;
String? locationDescription;
String? startTime;
String? endTime;
int? statusId;
String? statusName;
int? statusValue;
List<Instruction>? instructions;
List<Attachment>? attachments;
String? createdDate;
String? createdBy;
String? imageRoom; // For backward compatibility - deprecated
List<String>? imageRooms; // For multiple captured images (max 2)
OneTimeTaskData({
this.id,
this.oneTimeSettingId,
this.oneTimeSetting,
this.userId,
this.userName,
this.siteId,
this.siteName,
this.buildingId,
this.buildingName,
this.floorId,
this.floorName,
this.departmentID,
this.departmentName,
this.roomId,
this.roomName,
this.locationDescription,
this.startTime,
this.endTime,
this.statusId,
this.statusName,
this.statusValue,
this.instructions,
this.attachments,
this.createdDate,
this.createdBy,
this.imageRoom,
this.imageRooms,
});
factory OneTimeTaskData.fromJson(Map<String, dynamic> json) {
return OneTimeTaskData(
id: json['id'],
oneTimeSettingId: json['oneTimeSettingId'],
oneTimeSetting: json['oneTimeSetting'] != null ? OneTimeSetting.fromJson(json['oneTimeSetting']) : null,
userId: json['userId'],
userName: json['userName'],
siteId: json['siteId'],
siteName: json['siteName'],
buildingId: json['buildingId'],
buildingName: json['buildingName'],
floorId: json['floorId'],
floorName: json['floorName'],
departmentID: json['departmentID'],
departmentName: json['departmentName'],
roomId: json['roomId'],
roomName: json['roomName'],
locationDescription: json['locationDescription'],
startTime: json['startTime'],
endTime: json['endTime'],
statusId: json['statusId'],
statusName: json['statusName'],
statusValue: json['statusValue'],
instructions: json['instructions'] != null
? (json['instructions'] as List).map((i) => Instruction.fromJson(i)).toList()
: null,
attachments: json['attachments'] != null
? (json['attachments'] as List).map((a) => Attachment.fromJson(a)).toList()
: null,
createdDate: json['createdDate'],
createdBy: json['createdBy'],
imageRoom: json['imageRoom'],
imageRooms: json['imageRooms'] != null ? List<String>.from(json['imageRooms']) : null,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'oneTimeSettingId': oneTimeSettingId,
'oneTimeSetting': oneTimeSetting?.toJson(),
'userId': userId,
'userName': userName,
'siteId': siteId,
'siteName': siteName,
'buildingId': buildingId,
'buildingName': buildingName,
'floorId': floorId,
'floorName': floorName,
'departmentID': departmentID,
'departmentName': departmentName,
'roomId': roomId,
'roomName': roomName,
'locationDescription': locationDescription,
'startTime': startTime,
'endTime': endTime,
'statusId': statusId,
'statusName': statusName,
'statusValue': statusValue,
'instructions': instructions?.map((i) => i.toJson()).toList(),
'attachments': attachments?.map((a) => a.toJson()).toList(),
'createdDate': createdDate,
'createdBy': createdBy,
'imageRoom': imageRoom,
'imageRooms': imageRooms,
};
}
}
class OneTimeSetting {
int? id;
int? taskTypeId;
String? taskTypeName;
int? instructionDescriptionId;
String? instructionDescriptionName;
int? sla;
OneTimeSetting({
this.id,
this.taskTypeId,
this.taskTypeName,
this.instructionDescriptionId,
this.instructionDescriptionName,
this.sla,
});
factory OneTimeSetting.fromJson(Map<String, dynamic> json) {
return OneTimeSetting(
id: json['id'],
taskTypeId: json['taskTypeId'],
taskTypeName: json['taskTypeName'],
instructionDescriptionId: json['instructionDescriptionId'],
instructionDescriptionName: json['instructionDescriptionName'],
sla: json['sla'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'taskTypeId': taskTypeId,
'taskTypeName': taskTypeName,
'instructionDescriptionId': instructionDescriptionId,
'instructionDescriptionName': instructionDescriptionName,
'sla': sla,
};
}
}
class Instruction {
int? id;
int? instructionTextId;
String? instructionText;
bool? isRequired;
bool? isDone;
String? comment;
Instruction({
this.id,
this.instructionTextId,
this.instructionText,
this.isRequired,
this.isDone,
this.comment,
});
factory Instruction.fromJson(Map<String, dynamic> json) {
return Instruction(
id: json['id'],
instructionTextId: json['instructionTextId'],
instructionText: json['instructionText'],
isRequired: json['isRequired'],
isDone: json['isDone'],
comment: json['comment'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'instructionTextId': instructionTextId,
'instructionText': instructionText,
'isRequired': isRequired,
'isDone': isDone,
'comment': comment,
};
}
}
class Attachment {
int? id;
String? name;
String? originalName;
String? fileName; // For backward compatibility
String? fileUrl; // For backward compatibility
String? fileType;
Attachment({
this.id,
this.name,
this.originalName,
this.fileName,
this.fileUrl,
this.fileType,
});
factory Attachment.fromJson(Map<String, dynamic> json) {
return Attachment(
id: json['id'],
name: json['name'],
originalName: json['originalName'],
fileName: json['fileName'],
fileUrl: json['fileUrl'],
fileType: json['fileType'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'originalName': originalName,
'fileName': fileName,
'fileUrl': fileUrl,
'fileType': fileType,
};
}
}

@ -0,0 +1,60 @@
class QrScanLocationData {
int? siteId;
String? siteName;
int? buildingId;
String? buildingName;
int? floorId;
String? floorName;
int? departmentId;
String? departmentName;
int? roomId;
String? roomName;
String? locationDescription;
QrScanLocationData({
this.siteId,
this.siteName,
this.buildingId,
this.buildingName,
this.floorId,
this.floorName,
this.departmentId,
this.departmentName,
this.roomId,
this.roomName,
this.locationDescription,
});
factory QrScanLocationData.fromJson(Map<String, dynamic> json) {
return QrScanLocationData(
siteId: json['siteId'],
siteName: json['siteName'],
buildingId: json['buildingId'],
buildingName: json['buildingName'],
floorId: json['floorId'],
floorName: json['floorName'],
departmentId: json['departmentId'],
departmentName: json['departmentName'],
roomId: json['roomId'],
roomName: json['roomName'],
locationDescription: json['locationDescription'],
);
}
Map<String, dynamic> toJson() {
return {
'siteId': siteId,
'siteName': siteName,
'buildingId': buildingId,
'buildingName': buildingName,
'floorId': floorId,
'floorName': floorName,
'departmentId': departmentId,
'departmentName': departmentName,
'roomId': roomId,
'roomName': roomName,
'locationDescription': locationDescription,
};
}
}

@ -0,0 +1,26 @@
import 'package:test_sa/models/base.dart';
class TaskType extends Base {
int? id;
String? taskTypeName;
TaskType({this.id, this.taskTypeName}) : super(identifier: id?.toString(), name: taskTypeName);
@override
bool operator ==(Object other) => identical(this, other) || other is TaskType && id != null && id == other.id;
@override
int get hashCode => id.hashCode;
Map<String, dynamic> toJson() {
return {"id": id, "taskTypeName": taskTypeName};
}
factory TaskType.fromJson(Map<String, dynamic>? parsedJson) {
if (parsedJson == null) return TaskType();
return TaskType(
id: parsedJson["id"],
taskTypeName: parsedJson["taskTypeName"],
);
}
}

@ -1,68 +0,0 @@
import 'package:flutter/material.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import '../models/cleaner_task_search_result.dart';
class CleanerRequestItemView extends StatelessWidget {
final CleanerTaskSearchResult result;
final VoidCallback? onTap;
final bool showShadow;
const CleanerRequestItemView({Key? key, required this.result, this.onTap, this.showShadow = true}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Task Number and Date Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (result.taskNo != null && result.taskNo!.isNotEmpty) result.taskNo!.heading5(context).custom(color: AppColor.textStyleColor(context)),
if (result.createdDate != null)
Text(
result.createdDate!.split('T').first,
textAlign: TextAlign.end,
style: AppTextStyles.tinyFont.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral50,
),
),
],
),
8.height,
// Location Information
if (result.siteName != null && result.siteName!.isNotEmpty) 'Site: ${result.siteName}'.bodyText(context),
if (result.buildingName != null && result.buildingName!.isNotEmpty) 'Building: ${result.buildingName}'.bodyText(context),
if (result.floorName != null && result.floorName!.isNotEmpty) 'Floor: ${result.floorName}'.bodyText(context),
if (result.departmentName != null && result.departmentName!.isNotEmpty) 'Department: ${result.departmentName}'.bodyText(context),
if (result.roomName != null && result.roomName!.isNotEmpty) 'Room: ${result.roomName}'.bodyText(context),
if (result.locationDescription != null && result.locationDescription!.isNotEmpty) 'Location: ${result.locationDescription}'.bodyText(context),
8.height,
// View Details Link
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
context.translation.viewDetails,
style: AppTextStyles.bodyText.copyWith(color: AppColor.blueStatus(context)),
),
4.width,
Icon(
Icons.arrow_forward,
color: AppColor.blueStatus(context),
size: 14,
)
],
),
],
).toShadowContainer(context, withShadow: showShadow).onPress(onTap);
}
}

@ -1,4 +1,5 @@
// UI: Cleaner Search Details View // UI: Cleaner Search Details View
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/context_extension.dart';
@ -6,16 +7,22 @@ import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart'; import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/ppm/recurrent_wo.dart';
import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart'; import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart'; import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart'; import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
import 'package:test_sa/views/widgets/qr/scan_qr.dart'; import 'package:test_sa/views/widgets/qr/scan_qr.dart';
import 'package:test_sa/views/widgets/requests/request_status.dart';
import '../models/cleaner_task_search_result.dart'; import '../models/cleaner_task_search_result.dart';
import '../provider/cleaner_provider.dart'; import '../provider/cleaner_provider.dart';
import '../../pm_module/recurrent_wo/recurrent_work_order_view.dart'; import '../../pm_module/recurrent_wo/recurrent_work_order_view.dart';
import 'package:test_sa/controllers/providers/api/all_requests_provider.dart'; import 'package:test_sa/controllers/providers/api/all_requests_provider.dart';
import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart';
import 'package:test_sa/controllers/api_routes/api_manager.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:http/http.dart' as http;
class CleanerSearchDetailsView extends StatefulWidget { class CleanerSearchDetailsView extends StatefulWidget {
static const String routeName = '/cleaner_search_details'; static const String routeName = '/cleaner_search_details';
@ -34,8 +41,12 @@ class CleanerSearchDetailsView extends StatefulWidget {
class _CleanerSearchDetailsViewState extends State<CleanerSearchDetailsView> { class _CleanerSearchDetailsViewState extends State<CleanerSearchDetailsView> {
late CleanerProvider _cleanerProvider; late CleanerProvider _cleanerProvider;
bool _isQRScanned = false;
// bool _isQRScanned = false;
bool _isRequestOpen = false;
CleanerTaskSearchResult? _taskData; CleanerTaskSearchResult? _taskData;
RecurrentWoData? _recurrentWoData;
bool _isLoading = false; bool _isLoading = false;
@override @override
@ -56,33 +67,48 @@ class _CleanerSearchDetailsViewState extends State<CleanerSearchDetailsView> {
}); });
try { try {
final allRequestsProvider = Provider.of<AllRequestsProvider>(context, listen: false); // Use direct API call to get better error messages
await allRequestsProvider.getRecurrentWoById(id: widget.requestId); final result = await _getRecurrentWoByIdWithMessage(widget.requestId);
// Map RecurrentWoData to CleanerTaskSearchResult if (result["success"] == true && result["data"] != null) {
if (allRequestsProvider.recurrentWoData != null) { _recurrentWoData = result["data"];
final recurrentData = allRequestsProvider.recurrentWoData!; _isRequestOpen = _recurrentWoData?.status?.value == 4;
_taskData = CleanerTaskSearchResult( _taskData = CleanerTaskSearchResult(
id: recurrentData.id, id: _recurrentWoData!.id,
taskNo: recurrentData.taskNo, taskNo: _recurrentWoData!.taskNo,
siteName: recurrentData.site?.siteName, siteName: _recurrentWoData!.site?.siteName,
buildingName: recurrentData.building?.name, buildingName: _recurrentWoData!.building?.name,
floorName: recurrentData.floor?.name, floorName: _recurrentWoData!.floor?.name,
departmentName: recurrentData.department?.name, departmentName: _recurrentWoData!.department?.name,
roomName: recurrentData.room?.name, roomName: _recurrentWoData!.room?.name,
locationDescription: null, locationDescription: null,
createdDate: recurrentData.scheduleDate, createdDate: _recurrentWoData!.scheduleDate,
); );
} else {
// Show exact backend error message
if (mounted) {
_showErrorDialog(context, result["message"] ?? "Failed to load request details. Please try again.");
}
} }
} catch (e) { } catch (e) {
debugPrint('Error fetching recurrent work order: $e'); debugPrint('Error fetching recurrent work order: $e');
if (mounted) {
_showErrorDialog(context, "Failed to load request details. Please check your connection and try again.");
}
} finally { } finally {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
}); });
} }
} else { } else {
await _cleanerProvider.getCleanerRequestById(id: widget.requestId); try {
await _cleanerProvider.getCleanerRequestById(id: widget.requestId);
} catch (e) {
debugPrint('Error fetching cleaner request: $e');
if (mounted) {
_showErrorDialog(context, "Failed to load request details. Please check your connection and try again.");
}
}
} }
} }
@ -126,7 +152,7 @@ class _CleanerSearchDetailsViewState extends State<CleanerSearchDetailsView> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (!_isQRScanned) ...[ if (_isRequestOpen) ...[
Row( Row(
children: [ children: [
Icon(Icons.info_outline_rounded, size: 16, color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), Icon(Icons.info_outline_rounded, size: 16, color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
@ -135,7 +161,7 @@ class _CleanerSearchDetailsViewState extends State<CleanerSearchDetailsView> {
child: Text( child: Text(
'By scanning the QR code you will accept this task'.addTranslation, 'By scanning the QR code you will accept this task'.addTranslation,
// textAlign: TextAlign.center, // textAlign: TextAlign.center,
style: AppTextStyles.bodyText.copyWith( style: AppTextStyles.bodyText2.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
), ),
), ),
@ -145,53 +171,47 @@ class _CleanerSearchDetailsViewState extends State<CleanerSearchDetailsView> {
8.height, 8.height,
], ],
AppFilledButton( AppFilledButton(
label: _isQRScanned ? 'Update' : 'Scan QR Code', label: _isRequestOpen ? 'Scan QR Code' : 'Update',
maxWidth: true, maxWidth: true,
buttonColor: AppColor.primary10, buttonColor: AppColor.primary10,
onPressed: () async { onPressed: () async {
if (_isQRScanned) { if (_isRequestOpen) {
// If QR is already scanned, navigate to recurrent work order view final result = await Navigator.push(context, MaterialPageRoute(builder: (context) => const ScanQr()));
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => RecurrentWorkOrderView(taskId: widget.requestId),
),
);
} else {
// Open QR scanner
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ScanQr(),
),
);
// If QR code was scanned successfully, check if it matches the room
if (result != null && result is String && result.isNotEmpty) { if (result != null && result is String && result.isNotEmpty) {
// Show loading indicator showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading());
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AppLazyLoading(),
);
// Call the API to check if QR matches the same room try {
final isValid = await CleanerProvider().checkQRSameRoom( final qrResult = await CleanerProvider().checkQRSameRoomWithMessage(planRecurrentTaskId: widget.requestId, qrCode: result);
planRecurrentTaskId: widget.requestId, if (mounted) Navigator.pop(context);
qrCode: result,
);
// Close loading dialog final isValid = qrResult["success"] ?? false;
if (mounted) Navigator.pop(context); final backendMessage = qrResult["message"] ?? "Unknown error occurred";
if (isValid) { if (isValid) {
// Update state to show "Update" button instead of navigating if (mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: widget.requestId)),
);
}
} else {
if (mounted) {
// Show exact backend error message instead of our custom message
_showErrorDialog(context, backendMessage);
}
}
} catch (e) {
if (mounted) Navigator.pop(context);
if (mounted) { if (mounted) {
setState(() { // Extract actual error message from the exception or use generic fallback
_isQRScanned = true; String errorMessage = _extractErrorMessage(e) ?? 'Failed to validate QR code. Please try again.';
}); _showErrorDialog(context, errorMessage);
} }
} }
} }
} else {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: widget.requestId)),
);
} }
}, },
), ),
@ -230,12 +250,24 @@ class _CleanerSearchDetailsViewState extends State<CleanerSearchDetailsView> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( // Text(
'Request Information', // 'Request Information',
style: AppTextStyles.heading4.copyWith( // style: AppTextStyles.heading4.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, // color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
// ),
// ),
if (_recurrentWoData?.status?.name != null)
StatusLabel(
label: _recurrentWoData?.status?.name!,
textColor: AppColor.getRequestStatusTextColorByName(context, _recurrentWoData?.status?.name!),
backgroundColor: AppColor.getRequestStatusColorByName(context, _recurrentWoData?.status?.name!),
), ),
), 8.width,
// StatusLabel(
// label: taskModel?.taskJobStatus?.name ?? widget.requestDetails?.status!,
// textColor: AppColor.getRequestStatusTextColorByName(context, taskModel?.taskJobStatus?.name ?? widget.requestDetails?.status!),
// backgroundColor: AppColor.getRequestStatusColorByName(context, taskModel?.taskJobStatus?.name ?? widget.requestDetails?.status!),
// ),
if (taskResult.createdDate != null) if (taskResult.createdDate != null)
Text( Text(
taskResult.createdDate!.toServiceRequestCardFormat, taskResult.createdDate!.toServiceRequestCardFormat,
@ -245,6 +277,13 @@ class _CleanerSearchDetailsViewState extends State<CleanerSearchDetailsView> {
], ],
), ),
8.height, 8.height,
if (_recurrentWoData?.title?.isNotEmpty ?? false)
Text(
_recurrentWoData?.title ?? "",
style: AppTextStyles.heading4.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
),
),
if (taskResult.taskNo != null && taskResult.taskNo!.isNotEmpty) if (taskResult.taskNo != null && taskResult.taskNo!.isNotEmpty)
Text( Text(
'Task No: ${taskResult.taskNo!}', 'Task No: ${taskResult.taskNo!}',
@ -289,4 +328,86 @@ class _CleanerSearchDetailsViewState extends State<CleanerSearchDetailsView> {
], ],
).toShadowContainer(context, padding: 0); ).toShadowContainer(context, padding: 0);
} }
void _showErrorDialog(BuildContext context, String message) {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: message,
title: 'Error',
onTap: () => Navigator.pop(context),
),
);
}
String? _extractErrorMessage(dynamic error) {
// Implement your logic to extract error message from the exception
// For example, if the error is a custom exception with a message property:
if (error is ExceptionWithMessage) {
return error.message;
} else if (error is String) {
// If the error is a string, return it directly
return error;
} else if (error is Map<String, dynamic>) {
// If the error is a Map (e.g., parsed JSON), extract the message field
return error['message'] ?? error.toString();
}
// Fallback to a generic error message
return 'An unexpected error occurred. Please try again.';
}
/// Custom method to get recurrent work order with proper error message extraction
Future<Map<String, dynamic>> _getRecurrentWoByIdWithMessage(int id) async {
try {
final response = await ApiManager.instance.get(
URLs.getRecurrentPlanByIdUrl + "?planRecurrentTaskId=$id",
enableToastMessage: false, // Disable automatic toast
);
final responseData = json.decode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
// Success response
final recurrentWoData = RecurrentWoData.fromJson(responseData["data"]);
// Apply the same processing as in AllRequestsProvider
recurrentWoData.planRecurrentMedicalTaskRooms?.forEach((element) {
element.planRecurrentMedicalTaskRoomTabs?.forEach((tabs) {
tabs.planRecurrentMedicalTaskRoomTabAttributes?.forEach((attribute) {
if (attribute.attribute?.type == "bool" && attribute.attributeValue == null) {
attribute.attributeValue = "true";
}
});
});
});
return {
"success": true,
"data": recurrentWoData,
"message": "Data loaded successfully",
};
} else {
// Error response - extract the exact error message from backend
return {
"success": false,
"data": null,
"message": responseData["message"] ?? "Failed to load request details",
};
}
} catch (error) {
debugPrint('Error in _getRecurrentWoByIdWithMessage: $error');
return {
"success": false,
"data": null,
"message": "Failed to load request details. Please check your connection and try again.",
};
}
}
}
class ExceptionWithMessage implements Exception {
final String message;
ExceptionWithMessage(this.message);
} }

@ -1,26 +1,22 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:test_sa/controllers/providers/api/hospitals_provider.dart';
import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/string_extensions.dart'; import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/hospital.dart'; import 'package:test_sa/modules/cleaner_module/models/task_type.dart';
import 'package:test_sa/models/lookup.dart'; import 'package:test_sa/modules/cleaner_module/models/qr_scan_location_data.dart';
import 'package:test_sa/models/new_models/department_room_combined.dart'; import 'package:test_sa/modules/cleaner_module/models/one_time_task_data.dart';
import 'package:test_sa/modules/cleaner_module/pages/cleaner_search_list_view.dart';
import 'package:test_sa/modules/cleaner_module/provider/cleaner_provider.dart'; import 'package:test_sa/modules/cleaner_module/provider/cleaner_provider.dart';
import 'package:test_sa/modules/cleaner_module/provider/fms_building_provider.dart'; import 'package:test_sa/modules/cleaner_module/provider/task_type_provider.dart';
import 'package:test_sa/modules/cleaner_module/provider/fms_floor_provider.dart'; import 'package:test_sa/modules/cleaner_module/pages/one_time_task_view.dart';
import 'package:test_sa/modules/cleaner_module/provider/fms_location_description_provider.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart'; import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart'; import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
import 'package:test_sa/new_views/common_widgets/single_item_drop_down_menu.dart'; import 'package:test_sa/new_views/common_widgets/single_item_drop_down_menu.dart';
import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/providers/loading_list_notifier.dart'; import 'package:test_sa/providers/loading_list_notifier.dart';
import 'package:test_sa/models/new_models/building.dart'; import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart';
import 'package:test_sa/models/new_models/floor.dart'; import 'package:test_sa/views/widgets/qr/scan_qr.dart';
import 'package:test_sa/extensions/int_extensions.dart';
class CleanerSearchFormView extends StatefulWidget { class CleanerSearchFormView extends StatefulWidget {
static const String routeName = '/cleaner_search'; static const String routeName = '/cleaner_search';
@ -32,161 +28,146 @@ class CleanerSearchFormView extends StatefulWidget {
} }
class _CleanerSearchFormViewState extends State<CleanerSearchFormView> { class _CleanerSearchFormViewState extends State<CleanerSearchFormView> {
Hospital? selectedSite; TaskType? selectedTaskType;
Building? selectedBuilding; List<TaskType> _taskTypeOptions = [];
Floor? selectedFloor; bool _isLoadingTaskTypes = false;
DepartmentRoomCombined? selectedDepartmentRoom; bool _isScanning = false;
Lookup? selectedLocationDescription; bool _isProceeding = false;
late FocusNode _siteFocusNode; QrScanLocationData? _scannedLocationData;
final TextEditingController _siteController = TextEditingController();
List<Hospital> _siteOptions = [];
List<Building> _buildingOptions = [];
List<Floor> _floorOptions = [];
List<Lookup> _locationDescriptionOptions = [];
List<DepartmentRoomItem> _allDeptRoomList = [];
bool _isLoadingSites = false;
bool _isLoadingBuildings = false;
bool _isLoadingFloors = false;
bool _isLoadingLocationDescriptions = false;
bool _isSearching = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_siteFocusNode = FocusNode(); _loadTaskTypes();
} }
@override @override
void dispose() { void dispose() {
_siteController.dispose();
_siteFocusNode.dispose();
super.dispose(); super.dispose();
} }
Future<void> _searchSites(String query) async { Future<void> _loadTaskTypes() async {
if (query.isEmpty) {
setState(() {
_siteOptions = [];
});
return;
}
setState(() { setState(() {
_isLoadingSites = true; _isLoadingTaskTypes = true;
}); });
try { try {
final sites = await HospitalsProvider().getHospitalsListByVal(searchVal: query); final taskTypes = await TaskTypeProvider().getTaskTypesList();
setState(() { setState(() {
_siteOptions = sites; _taskTypeOptions = taskTypes;
_isLoadingSites = false; _isLoadingTaskTypes = false;
}); });
} catch (e) { } catch (e) {
setState(() { setState(() {
_isLoadingSites = false; _isLoadingTaskTypes = false;
_taskTypeOptions = [];
}); });
_showErrorDialog(context, 'Failed to load task types. Please try again.');
} }
} }
Future<void> _loadBuildings(int siteId) async { Future<void> _onScanQRCode() async {
setState(() { final result = await Navigator.push(
_isLoadingBuildings = true; context,
}); MaterialPageRoute(builder: (context) => const ScanQr()),
);
try { if (result != null && result is String && result.isNotEmpty) {
final buildings = await FmsBuildingProvider().getBuildingsListBySiteId(siteId);
setState(() { setState(() {
_buildingOptions = buildings; _isScanning = true;
_isLoadingBuildings = false;
});
} catch (e) {
setState(() {
_isLoadingBuildings = false;
_buildingOptions = [];
}); });
try {
final qrResult = await CleanerProvider().scanQRCode(qrCode: result);
final isSuccess = qrResult["success"] ?? false;
final backendMessage = qrResult["message"] ?? "Unknown error occurred";
if (isSuccess && qrResult["data"] != null) {
// Store the scanned location data
setState(() {
_scannedLocationData = QrScanLocationData.fromJson(qrResult["data"]);
_isScanning = false;
});
} else {
setState(() {
_isScanning = false;
});
if (mounted) {
_showErrorDialog(context, backendMessage);
}
}
} catch (e) {
setState(() {
_isScanning = false;
});
if (mounted) {
_showErrorDialog(context, 'Failed to scan QR code. Please try again.');
}
}
} }
} }
Future<void> _loadFloors(int siteId, int buildingId) async { Future<void> _onProceed() async {
setState(() { // Validate that task type and location data are available
_isLoadingFloors = true; if (selectedTaskType == null) {
}); _showErrorDialog(context, 'Please select a task type');
return;
}
try { if (_scannedLocationData == null) {
final floors = await FmsFloorProvider().getFloorsListBySiteAndBuilding(siteId, buildingId); _showErrorDialog(context, 'Please scan a QR code first');
setState(() { return;
_floorOptions = floors;
_isLoadingFloors = false;
});
} catch (e) {
setState(() {
_isLoadingFloors = false;
_floorOptions = [];
});
} }
}
Future<void> _loadLocationDescriptions(int siteId, int buildingId, int floorId) async {
setState(() { setState(() {
_isLoadingLocationDescriptions = true; _isProceeding = true;
}); });
try { try {
final response = await FmsLocationDescriptionProvider().getLocationDescriptionsList(siteId, buildingId, floorId); final result = await CleanerProvider().proceedOneTimeTask(
setState(() { oneTimeSettingId: selectedTaskType!.id ?? 0,
_locationDescriptionOptions = response?.locationDescriptions ?? []; siteId: _scannedLocationData!.siteId ?? 0,
_allDeptRoomList = response?.deptRoom ?? []; buildingId: _scannedLocationData!.buildingId ?? 0,
_isLoadingLocationDescriptions = false; floorId: _scannedLocationData!.floorId ?? 0,
}); departmentId: _scannedLocationData!.departmentId ?? 0,
} catch (e) { roomId: _scannedLocationData!.roomId ?? 0,
setState(() { );
_isLoadingLocationDescriptions = false;
_locationDescriptionOptions = [];
_allDeptRoomList = [];
});
}
}
List<DepartmentRoomCombined> _getDepartmentRoomCombinations() {
if (selectedLocationDescription == null || _allDeptRoomList.isEmpty) return [];
// Filter dept room list by selected location description
List<DepartmentRoomItem> filteredList = _allDeptRoomList.where((item) => item.locationDescriptionName == selectedLocationDescription!.name).toList();
// Convert to DepartmentRoomCombined
return filteredList
.map((item) => DepartmentRoomCombined(
deptId: item.deptId,
roomId: item.roomId,
deptName: item.deptName,
roomName: item.roomName,
locationDescriptionName: item.locationDescriptionName,
deptAndRoomName: item.deptAndRoomName,
))
.toList();
}
void _autoSelectDepartmentRoom() {
final combinations = _getDepartmentRoomCombinations();
if (combinations.isEmpty) {
setState(() {
selectedDepartmentRoom = null;
});
return;
}
// If only one item, auto-fill final isSuccess = result["success"] ?? false;
if (combinations.length == 1) { final data = result["data"];
setState(() { final message = result["message"] ?? "Unknown error occurred";
selectedDepartmentRoom = combinations.first;
}); if (isSuccess && data != null) {
} else { // Get the task ID from the response
// If more than one, auto-select first item final taskId = data['id'];
if (mounted && taskId != null) {
// Convert the data to OneTimeTaskData and navigate with the data
final taskData = OneTimeTaskData.fromJson(data);
// Navigate to OneTimeTaskView with the data
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => OneTimeTaskView(
taskId: taskId,
initialData: taskData,
),
),
);
}
} else {
if (mounted) {
_showErrorDialog(context, message);
}
}
} catch (e) {
if (mounted) {
_showErrorDialog(context, 'Failed to proceed. Please try again.');
}
} finally {
setState(() { setState(() {
selectedDepartmentRoom = combinations.first; _isProceeding = false;
}); });
} }
} }
@ -203,236 +184,90 @@ class _CleanerSearchFormViewState extends State<CleanerSearchFormView> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8, spacing: 8,
children: [ children: [
// Site Autocomplete Field // Task Type Dropdown
Container( SingleItemDropDownMenu<TaskType, NullableLoadingProvider>(
padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth, vertical: 8.toScreenHeight),
decoration: BoxDecoration(
color: AppColor.dropDownBackground(context),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColor.border2Color(context), width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Site',
style: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 12, color: AppColor.floatingTextStyleColor(context)),
),
SizedBox(
height: 24.toScreenHeight, // Fixed height for consistency
child: Autocomplete<Hospital>(
optionsBuilder: (TextEditingValue textEditingValue) async {
if (textEditingValue.text.isEmpty) {
return const Iterable<Hospital>.empty();
}
await _searchSites(textEditingValue.text);
return _siteOptions;
},
displayStringForOption: (Hospital option) => option.name ?? "",
onSelected: (Hospital selected) async {
// _siteFocusNode.unfocus();
FocusManager.instance.primaryFocus?.unfocus();
setState(() {
selectedSite = selected;
_siteController.text = selected.name ?? '';
// Reset dependent dropdowns
selectedBuilding = null;
selectedFloor = null;
selectedLocationDescription = null;
selectedDepartmentRoom = null;
_buildingOptions = [];
_floorOptions = [];
_locationDescriptionOptions = [];
_allDeptRoomList = [];
});
// Load buildings for selected site
if (selected.id != null) {
await _loadBuildings(selected.id!);
}
},
fieldViewBuilder: (context, controller, focusNode, onFieldSubmitted) {
if (_siteController.text.isNotEmpty && controller.text.isEmpty) {
controller.text = _siteController.text;
}
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextField(
controller: controller,
// focusNode: _siteFocusNode,
focusNode: focusNode,
cursorColor: AppColor.loadingColor(context),
style: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 14, color: AppColor.textStyleColor(context)),
decoration: InputDecoration(
hintText: 'Search site...',
hintStyle: TextStyle(fontWeight: FontWeight.w500, fontSize: context.isTablet() ? 16 : 12, color: AppColor.labelTextStyleColor(context)),
filled: false,
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: EdgeInsets.zero,
isDense: true,
),
onChanged: (value) {
_siteController.text = value;
},
),
),
if (_isLoadingSites)
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: AppColor.loadingColor(context)),
)
else if (controller.text.isNotEmpty)
GestureDetector(
onTap: () {
controller.clear();
_siteController.clear();
setState(() {
selectedSite = null;
selectedBuilding = null;
selectedFloor = null;
selectedLocationDescription = null;
selectedDepartmentRoom = null;
_siteOptions = [];
_buildingOptions = [];
_floorOptions = [];
_locationDescriptionOptions = [];
_allDeptRoomList = [];
});
},
child: Icon(
Icons.clear,
color: AppColor.icon2Color(context),
size: 20,
),
),
],
);
},
),
),
],
),
),
// Building Dropdown
SingleItemDropDownMenu<Building, NullableLoadingProvider>(
context: context, context: context,
title: 'Building', title: 'Task Type',
initialValue: selectedBuilding, initialValue: selectedTaskType,
showShadow: false, showShadow: false,
showAsBottomSheet: true, showAsBottomSheet: true,
// backgroundColor: AppColor.fieldBgColor(context), backgroundColor: AppColor.fieldBgColor(context),
enabled: _buildingOptions.isNotEmpty, enabled: _taskTypeOptions.isNotEmpty,
loading: _isLoadingBuildings, loading: _isLoadingTaskTypes,
staticData: _buildingOptions, staticData: _taskTypeOptions,
onSelect: (value) async { onSelect: (value) {
_siteFocusNode.unfocus();
setState(() { setState(() {
_siteOptions = []; selectedTaskType = value;
selectedBuilding = value;
selectedFloor = null;
selectedLocationDescription = null;
selectedDepartmentRoom = null;
_floorOptions = [];
_locationDescriptionOptions = [];
_allDeptRoomList = [];
}); });
// Load floors for selected building
if (value != null && selectedSite?.id != null && value.id != null) {
await _loadFloors(selectedSite!.id!, value.id!.toInt());
}
}, },
), ),
// Floor Dropdown // Scan QR Code Button
SingleItemDropDownMenu<Floor, NullableLoadingProvider>( GestureDetector(
context: context, onTap: selectedTaskType != null ? _onScanQRCode : null,
title: 'Floor', child: Container(
initialValue: selectedFloor, height: 50,
showShadow: false, alignment: Alignment.center,
showAsBottomSheet: true, decoration: BoxDecoration(
// backgroundColor: AppColor.fieldBgColor(context), color: selectedTaskType != null ? Colors.white : Colors.grey[300],
enabled: _floorOptions.isNotEmpty, borderRadius: BorderRadius.circular(10),
loading: _isLoadingFloors, border: Border.all(
staticData: _floorOptions, color: selectedTaskType != null ? AppColor.blueStatus(context) : Colors.grey,
onSelect: (value) async { width: 2,
setState(() { ),
selectedFloor = value; ),
selectedLocationDescription = null; padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth, vertical: 8.toScreenHeight),
selectedDepartmentRoom = null; child: Row(
_locationDescriptionOptions = []; mainAxisSize: MainAxisSize.min,
_allDeptRoomList = []; children: [
}); "scan_asset".toSvgAsset(
height: 22,
// Load location descriptions for selected floor fit: BoxFit.fitHeight,
if (value != null && selectedSite?.id != null && selectedBuilding?.id != null && value.id != null) { color: selectedTaskType != null
await _loadLocationDescriptions( ? (context.isDark ? AppColor.neutral20 : AppColor.neutral120)
selectedSite!.id!, : Colors.grey,
selectedBuilding!.id!.toInt(), ),
value.id!.toInt(), 8.width,
); //we can change the implementation when merge with uiux branch
} _scannedLocationData==null? "Scan QR Code".bodyText(context).custom(
}, color: selectedTaskType != null
? (context.isDark ? AppColor.neutral20 : AppColor.black20)
: Colors.grey,
):'Re Scan QR Code'.bodyText(context).custom(
color: selectedTaskType != null
? (context.isDark ? AppColor.neutral20 : AppColor.black20)
: Colors.grey,
),
],
),
),
), ),
// Location Description Dropdown // Loading Indicator
SingleItemDropDownMenu<Lookup, NullableLoadingProvider>( if (_isScanning)
context: context, const Center(
title: 'Location Description', child: Padding(
initialValue: selectedLocationDescription, padding: EdgeInsets.all(16.0),
showShadow: false, child: CircularProgressIndicator(),
showAsBottomSheet: true, ),
// backgroundColor: AppColor.fieldBgColor(context), ),
enabled: _locationDescriptionOptions.isNotEmpty,
loading: _isLoadingLocationDescriptions,
staticData: _locationDescriptionOptions,
onSelect: (value) {
setState(() {
selectedLocationDescription = value;
selectedDepartmentRoom = null;
});
// Auto-select department-room based on location description
_autoSelectDepartmentRoom();
},
),
// Department & Room Combined Dropdown // Scanned Location Data Card
SingleItemDropDownMenu<DepartmentRoomCombined, NullableLoadingProvider>( if (_scannedLocationData != null && !_isScanning) _buildLocationDataCard(context),
context: context,
title: 'Department - Room',
initialValue: selectedDepartmentRoom,
showShadow: false,
showAsBottomSheet: true,
backgroundColor: AppColor.fieldBgColor(context),
enabled: _getDepartmentRoomCombinations().isNotEmpty,
staticData: _getDepartmentRoomCombinations(),
onSelect: (value) {
setState(() {
selectedDepartmentRoom = value;
});
},
),
], ],
).toShadowContainer(context).paddingOnly(top: 20, start: 16, end: 16), ).toShadowContainer(context).paddingOnly(top: 20, start: 16, end: 16),
), ),
), ),
// Footer Action Button
FooterActionButton.footerContainer( FooterActionButton.footerContainer(
context: context, context: context,
child: AppFilledButton( child: AppFilledButton(
buttonColor: AppColor.primary10, buttonColor:_scannedLocationData != null && !_isProceeding? AppColor.primary10: Colors.grey,
label: context.translation.search, label: 'Proceed',
maxWidth: true, maxWidth: true,
onPressed: _isSearching ? null : _onSubmit, onPressed: (_scannedLocationData != null && !_isProceeding) ? _onProceed : null,
loading: _isSearching, loading: _isProceeding,
), ),
), ),
], ],
@ -440,51 +275,78 @@ class _CleanerSearchFormViewState extends State<CleanerSearchFormView> {
); );
} }
Future<void> _onSubmit() async { Widget _buildLocationDataCard(BuildContext context) {
// Validate that at least site is selected final locationData = _scannedLocationData;
if (selectedSite == null) {
'Please select a site'.showToast;
return;
}
try {
setState(() {
_isSearching = true;
});
// Call the search API if (locationData == null) {
final response = await CleanerProvider().searchPlanRecurrentTasksList( return const SizedBox();
pageSize: 10, }
pageNumber: 1,
siteId: selectedSite?.id,
buildingId: selectedBuilding?.id?.toInt(),
floorId: selectedFloor?.id?.toInt(),
departmentId: selectedDepartmentRoom?.deptId,
roomId: selectedDepartmentRoom?.roomId,
);
final results = response['data'] as List<dynamic>; return Column(
final totalRows = response['totalRows'] as int; crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (locationData.siteName != null && locationData.siteName!.isNotEmpty)
Text(
'Site: ${locationData.siteName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.buildingName != null && locationData.buildingName!.isNotEmpty)
Text(
'Building: ${locationData.buildingName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.floorName != null && locationData.floorName!.isNotEmpty)
Text(
'Floor: ${locationData.floorName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.departmentName != null && locationData.departmentName!.isNotEmpty)
Text(
'Department: ${locationData.departmentName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.roomName != null && locationData.roomName!.isNotEmpty)
Text(
'Room: ${locationData.roomName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.locationDescription != null && locationData.locationDescription!.isNotEmpty)
Text(
'Location Description: ${locationData.locationDescription}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
softWrap: true,
maxLines: null,
),
],
).paddingOnly(start: 16, end: 16, top: 16, bottom: 16),
],
).toShadowContainer(context, padding: 0);
}
// Navigate to search results page with the results void _showErrorDialog(BuildContext context, String message) {
Navigator.of(context).pushNamed( showDialog(
CleanerSearchListView.routeName, context: context,
arguments: { builder: (context) => ConfirmDialog(
'searchResults': results, message: message,
'totalRows': totalRows, title: 'Error',
'selectedSite': selectedSite, onTap: () => Navigator.pop(context),
'selectedBuilding': selectedBuilding, ),
'selectedFloor': selectedFloor, );
'selectedLocationDescription': selectedLocationDescription,
'selectedDepartmentRoom': selectedDepartmentRoom,
},
);
} catch (e) {
'Error searching'.showToast;
} finally {
setState(() {
_isSearching = false;
});
}
} }
} }

@ -1,186 +0,0 @@
import 'package:flutter/material.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/hospital.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_room_combined.dart';
import 'package:test_sa/models/new_models/floor.dart';
import 'package:test_sa/modules/cleaner_module/models/cleaner_task_search_result.dart';
import 'package:test_sa/modules/cleaner_module/pages/cleaner_search_details_view.dart';
import 'package:test_sa/modules/cleaner_module/provider/cleaner_provider.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/views/widgets/loaders/app_loading.dart';
import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
import 'cleaner_request_item_view.dart';
class CleanerSearchListView extends StatefulWidget {
static const String routeName = '/cleaner_search_list';
const CleanerSearchListView({Key? key}) : super(key: key);
@override
State<CleanerSearchListView> createState() => _CleanerSearchListViewState();
}
class _CleanerSearchListViewState extends State<CleanerSearchListView> {
List<CleanerTaskSearchResult> _searchResults = [];
bool _isLoading = false;
bool _isLoadingMore = false;
bool _hasMoreData = true;
int _totalRows = 0; // Add this to track total rows from API
// Pagination
int _currentPage = 1;
final int _pageSize = 10;
// Search filters (stored for pagination)
Hospital? _selectedSite;
Building? _selectedBuilding;
Floor? _selectedFloor;
Lookup? _selectedLocationDescription;
DepartmentRoomCombined? _selectedDepartmentRoom;
final ScrollController _scrollController = ScrollController();
final CleanerProvider _cleanerProvider = CleanerProvider();
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Get the search results and filters from navigation arguments
final args = ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
if (args != null && args['searchResults'] != null) {
final results = args['searchResults'] as List<dynamic>;
_searchResults = results.map((json) => CleanerTaskSearchResult.fromJson(json)).toList();
// Get totalRows from API response
_totalRows = args['totalRows'] as int? ?? 0;
// Store search filters for pagination
_selectedSite = args['selectedSite'] as Hospital?;
_selectedBuilding = args['selectedBuilding'] as Building?;
_selectedFloor = args['selectedFloor'] as Floor?;
_selectedLocationDescription = args['selectedLocationDescription'] as Lookup?;
_selectedDepartmentRoom = args['selectedDepartmentRoom'] as DepartmentRoomCombined?;
// Check if there's more data based on totalRows
_hasMoreData = _searchResults.length < _totalRows;
}
}
void _onScroll() {
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) {
// User is near the bottom, load more data
if (!_isLoadingMore && _hasMoreData && !_isLoading) {
_loadMoreData();
}
}
}
Future<void> _loadMoreData() async {
if (_isLoadingMore) return;
setState(() {
_isLoadingMore = true;
});
try {
_currentPage++;
final response = await _cleanerProvider.searchPlanRecurrentTasksList(
pageSize: _pageSize,
pageNumber: _currentPage,
siteId: _selectedSite?.id,
buildingId: _selectedBuilding?.id?.toInt(),
floorId: _selectedFloor?.id?.toInt(),
departmentId: _selectedDepartmentRoom?.deptId,
roomId: _selectedDepartmentRoom?.roomId,
);
final results = response['data'] as List<dynamic>;
final totalRows = response['totalRows'] as int? ?? 0;
final newResults = results.map((json) => CleanerTaskSearchResult.fromJson(json)).toList();
setState(() {
_searchResults.addAll(newResults);
_totalRows = totalRows;
// Check if we have loaded all data based on totalRows
_hasMoreData = _searchResults.length < _totalRows;
_isLoadingMore = false;
});
} catch (e) {
setState(() {
_isLoadingMore = false;
});
debugPrint('Error loading more data: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: DefaultAppBar(
title: 'Search Results'.addTranslation,
onBackPress: () => Navigator.pop(context),
),
body: SafeArea(
child: Column(
children: [
Expanded(
child: (_searchResults.isEmpty && !_isLoading)
? NoDataFound(message: context.translation.noDataFound).center
: ListView.separated(
controller: _scrollController,
padding: const EdgeInsets.all(16),
itemCount: _searchResults.length + (_isLoadingMore ? 1 : 0),
separatorBuilder: (_, __) => const SizedBox(height: 16),
itemBuilder: (context, index) {
if (_isLoading && _searchResults.isEmpty) {
return const SizedBox().toRequestShimmer(context, _isLoading);
}
// Show loading indicator at the bottom
if (index == _searchResults.length) {
return const ALoading().center;
}
final result = _searchResults[index];
return CleanerRequestItemView(
result: result,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CleanerSearchDetailsView(
requestId: result.id ?? 0,
taskResult: result,
),
),
);
},
);
},
),
),
],
),
),
);
}
}

@ -0,0 +1,410 @@
import 'package:flutter/material.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/modules/cleaner_module/models/one_time_task_data.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/views/widgets/images/files_list.dart';
import 'package:test_sa/modules/cm_module/cm_request_utils.dart';
import 'package:test_sa/views/widgets/timer/app_timer.dart';
import 'package:test_sa/models/timer_model.dart';
import 'dart:convert';
class OneTimeTaskPreviewScreen extends StatelessWidget {
static const String routeName = "/one_time_task_preview";
final OneTimeTaskData taskData;
final double totalWorkingHours;
final VoidCallback onConfirm;
const OneTimeTaskPreviewScreen({
Key? key,
required this.taskData,
required this.totalWorkingHours,
required this.onConfirm,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// Debug logging
print('DEBUG Preview Screen:');
print('imageRooms count: ${taskData.imageRooms?.length ?? 0}');
print('attachments count: ${taskData.attachments?.length ?? 0}');
// Determine which images to show: captured images (imageRooms) or API attachments
final hasImageRooms = taskData.imageRooms != null && taskData.imageRooms!.isNotEmpty;
final hasAttachments = taskData.attachments != null && taskData.attachments!.isNotEmpty;
return Scaffold(
appBar: const DefaultAppBar(
title: 'Preview',
),
body: SafeArea(
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Task Information Card
_buildTaskInfoCard(context),
16.height,
// Working Hours Card
if (totalWorkingHours > 0) ...[
_buildWorkingHoursCard(context),
16.height,
],
// Checklist Card
_buildChecklistCard(context),
16.height,
// Captured Images Card
if (hasImageRooms) ...[
_buildImagesCard(context),
16.height,
],
// Attachments Card (from API)
if (hasAttachments) ...[
_buildAttachmentsCard(context),
16.height,
],
],
),
),
),
// Footer with Confirm Button
FooterActionButton.footerContainer(
context: context,
child: AppFilledButton(
label: context.translation.confirm,
buttonColor: AppColor.primary10,
onPressed: onConfirm,
),
),
],
),
),
);
}
Widget _buildTaskInfoCard(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Task Information',
style: AppTextStyles.heading5.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
fontWeight: FontWeight.w600,
),
),
12.height,
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (taskData.oneTimeSetting?.taskTypeName != null) _buildInfoRow(context, 'Task Type', taskData.oneTimeSetting!.taskTypeName!),
if (taskData.siteName != null) _buildInfoRow(context, 'Site', taskData.siteName!),
if (taskData.buildingName != null) _buildInfoRow(context, 'Building', taskData.buildingName!),
if (taskData.floorName != null) _buildInfoRow(context, 'Floor', taskData.floorName!),
if (taskData.departmentName != null) _buildInfoRow(context, 'Department', taskData.departmentName!),
if (taskData.roomName != null) _buildInfoRow(context, 'Room', taskData.roomName!),
if (taskData.locationDescription != null) _buildInfoRow(context, 'Location', taskData.locationDescription!),
],
).paddingAll(16),
],
).toShadowContainer(context, padding: 16);
}
Widget _buildWorkingHoursCard(BuildContext context) {
// Parse timer start time from taskData if available
DateTime? timerStartTime;
if (taskData.startTime != null) {
try {
timerStartTime = DateTime.parse(taskData.startTime!);
} catch (e) {
print('Error parsing start time: $e');
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Working Hours',
style: AppTextStyles.heading5.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
fontWeight: FontWeight.w600,
),
),
12.height,
if (timerStartTime != null)
AppTimer(
label: 'Working Time',
timer: TimerModel(
startAt: timerStartTime,
endAt: null,
durationInSecond: 0,
),
enabled: false, // Disable controls, just show the timer
showTimer: true,
decoration: BoxDecoration(
color: context.isDark ? AppColor.neutral20.withValues(alpha: 0.5) : AppColor.neutral10.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
)
else
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: context.isDark ? AppColor.neutral20.withValues(alpha: 0.5) : AppColor.neutral10.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total Working Hours',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
Text(
CMRequestUtils.formatTotalWorkingHours(totalWorkingHours),
style: AppTextStyles.heading6.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
).toShadowContainer(context, padding: 16);
}
Widget _buildChecklistCard(BuildContext context) {
final completedInstructions = taskData.instructions?.where((i) => i.isDone == true).toList() ?? [];
final totalInstructions = taskData.instructions?.length ?? 0;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Checklist',
style: AppTextStyles.heading5.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
fontWeight: FontWeight.w600,
),
),
Text(
'${completedInstructions.length}/$totalInstructions',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
fontWeight: FontWeight.w600,
),
),
],
),
12.height,
ListView.separated(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
itemCount: completedInstructions.length,
separatorBuilder: (context, index) => const Divider(height: 1, color: AppColor.neutral30, thickness: 1).paddingOnly(top: 12, bottom: 12),
itemBuilder: (context, index) {
final instruction = completedInstructions[index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.check_circle,
color: AppColor.primary10,
size: 20,
),
8.width,
Expanded(
child: Text(
instruction.instructionText ?? '',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
),
],
),
if (instruction.comment != null && instruction.comment!.isNotEmpty) ...[
8.height,
Container(
width: double.infinity, // Force full width
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: context.isDark ? AppColor.neutral20.withValues(alpha: 0.5) : AppColor.neutral10.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Comment:',
style: AppTextStyles.tinyFont.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
fontWeight: FontWeight.w600,
),
),
4.height,
Text(
instruction.comment!,
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
],
),
),
],
],
);
},
).paddingAll(16),
],
).toShadowContainer(context, padding: 16);
}
Widget _buildImagesCard(BuildContext context) {
// Show captured images (base64 from camera)
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Captured Images',
style: AppTextStyles.heading5.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
fontWeight: FontWeight.w600,
),
),
16.height,
Wrap(
spacing: 8,
runSpacing: 8,
children: taskData.imageRooms!.map((imageString) {
return GestureDetector(
onTap: () {
// Show full screen image
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black,
iconTheme: const IconThemeData(color: Colors.white),
),
body: Center(
child: InteractiveViewer(
child: _buildImage(imageString),
),
),
),
),
);
},
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: context.isDark ? AppColor.neutral20 : AppColor.neutral30,
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: _buildImage(imageString),
),
),
);
}).toList(),
),
],
).toShadowContainer(context, padding: 16);
}
Widget _buildAttachmentsCard(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Attachments',
style: AppTextStyles.heading5.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
fontWeight: FontWeight.w600,
),
),
FilesList(
images: taskData.attachments!.map((attachment) => URLs.getFileUrl(attachment.name ?? '') ?? '').toList(),
showAsListView: true,
),
],
).toShadowContainer(context, padding: 16);
}
Widget _buildImage(String imageString) {
try {
// imageString format: "filename|base64data"
final parts = imageString.split('|');
final base64Data = parts.length > 1 ? parts[1] : imageString;
return Image.memory(
base64Decode(base64Data),
fit: BoxFit.cover,
);
} catch (e) {
return const Icon(Icons.broken_image, size: 48);
}
}
Widget _buildInfoRow(BuildContext context, String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Text(
'$label:',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
),
),
),
Expanded(
child: Text(
value,
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
}

@ -0,0 +1,472 @@
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/user_provider.dart';
import 'package:test_sa/dashboard_latest/dashboard_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/extensions/string_extensions.dart';
import 'package:test_sa/modules/cleaner_module/models/one_time_task_data.dart';
import 'package:test_sa/modules/cleaner_module/provider/cleaner_provider.dart';
import 'package:test_sa/modules/cleaner_module/widgets/cleaner_checklist_widget.dart';
import 'package:test_sa/modules/cleaner_module/pages/one_time_task_preview_screen.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/default_app_bar.dart';
import 'package:test_sa/new_views/pages/land_page/land_page.dart';
import 'package:test_sa/views/widgets/loaders/app_loading.dart';
import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart';
import 'package:test_sa/views/widgets/timer/app_timer.dart';
import 'package:test_sa/models/timer_model.dart';
import 'package:test_sa/views/widgets/requests/request_status.dart';
class OneTimeTaskView extends StatefulWidget {
static const String routeName = "/one_time_task";
final int taskId;
final OneTimeTaskData? initialData;
const OneTimeTaskView({Key? key, required this.taskId, this.initialData}) : super(key: key);
@override
State<OneTimeTaskView> createState() => _OneTimeTaskViewState();
}
class _OneTimeTaskViewState extends State<OneTimeTaskView> {
OneTimeTaskData? _taskData;
bool _isLoading = false;
DateTime? _timerStart;
List<ChecklistItem>? _checklistItems; // Cache checklist items
@override
void initState() {
super.initState();
// Use initial data if provided, otherwise load from API
if (widget.initialData != null) {
_taskData = widget.initialData;
// Initialize imageRooms as empty list if null
_taskData?.imageRooms ??= [];
// Start timer automatically if task is in progress
if (_taskData?.statusValue == 1 && _taskData?.startTime != null) {
_timerStart = DateTime.parse(_taskData!.startTime!);
}
} else {
_loadTaskData();
}
}
Future<void> _loadTaskData() async {
setState(() {
_isLoading = true;
});
try {
final result = await CleanerProvider().getOneTimeTaskById(id: widget.taskId);
if (result['success'] && result['data'] != null) {
setState(() {
_taskData = OneTimeTaskData.fromJson(result['data']);
// Initialize imageRooms as empty list if null
_taskData?.imageRooms ??= [];
// Convert attachments from API to imageRooms format for display
if ((_taskData?.imageRooms?.isEmpty ?? true) &&
(_taskData?.attachments != null && _taskData!.attachments!.isNotEmpty)) {
_taskData!.imageRooms = _taskData!.attachments!
.map((attachment) => URLs.getFileUrl(attachment.name ?? '') ?? '')
.where((url) => url.isNotEmpty)
.toList();
}
_isLoading = false;
// Start timer automatically if task is in progress
if (_taskData?.statusValue == 1 && _taskData?.startTime != null) {
_timerStart = DateTime.parse(_taskData!.startTime!);
}
});
} else {
setState(() {
_isLoading = false;
});
if (mounted) {
_showErrorDialog(context, result['message'] ?? 'Failed to load task');
}
}
} catch (e) {
setState(() {
_isLoading = false;
});
if (mounted) {
_showErrorDialog(context, 'Failed to load task data');
}
}
}
bool get isClosed => _taskData?.statusValue == 2;
String _formatDuration(Duration duration) {
if (duration.inDays > 0) {
final hours = duration.inHours % 24;
final minutes = duration.inMinutes % 60;
return '${duration.inDays}d ${hours}h ${minutes}m';
} else if (duration.inHours > 0) {
final minutes = duration.inMinutes % 60;
final seconds = duration.inSeconds % 60;
return '${duration.inHours}h ${minutes}m ${seconds}s';
} else if (duration.inMinutes > 0) {
final seconds = duration.inSeconds % 60;
return '${duration.inMinutes}m ${seconds}s';
} else {
return '${duration.inSeconds}s';
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: DefaultAppBar(
title: 'One Time Task'.addTranslation,
onWillPopScope: null,
onBackPress: (){
WidgetsBinding.instance.addPostFrameCallback((_) {
UserProvider userProvider = Provider.of<UserProvider>(context, listen: false);
DashBoardProvider dashBoardProvider = Provider.of<DashBoardProvider>(context, listen: false);
dashBoardProvider.getDashBoardCount(usersType:userProvider.user!.type!);
dashBoardProvider.resetRequestDataList();
dashBoardProvider.getRequestDetail(usersType: userProvider.user!.type!, status: dashBoardProvider.tabs[dashBoardProvider.currentListIndex].tag);
});
Navigator.pop(context);
},
),
body: _isLoading
? const ALoading()
: _taskData != null
? SafeArea(
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTaskInfoCard(context),
16.height,
CleanerChecklistWidget(
isClosed: isClosed,
checklistItems: _convertInstructionsToChecklist(),
onChecklistChanged: (value) {
setState(() {
_updateInstructions(value);
});
},
capturedImages: _taskData!.imageRooms ?? [],
onImagesChanged: (images) {
setState(() {
_taskData!.imageRooms = images;
});
},
),
],
),
),
),
if (!isClosed) ...[
FooterActionButton.footerContainer(
context: context,
child: AppFilledButton(
label: context.translation.complete,
buttonColor: AppColor.primary10,
onPressed: _onComplete,
),
),
]
],
),
)
: NoDataFound(message: context.translation.noDataFound).center,
).handlePopScope(
cxt: context,
showPopUp: false,
onSave: () {
Navigator.pop(context);
},
);
}
Widget _buildTaskInfoCard(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_taskData?.statusName != null)
StatusLabel(
radius: 4,
label: _taskData!.statusName!,
textColor: AppColor.getHistoryLogStatusTextColorByName(_taskData!.statusName!),
backgroundColor: AppColor.getHistoryLogStatusColorByName(_taskData!.statusName!),
),
if (_taskData?.createdDate != null)
Text(
_taskData!.createdDate!.toString().toServiceRequestCardFormat,
textAlign: TextAlign.end,
style: AppTextStyles.tinyFont.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
),
),
],
),
12.height,
if (_taskData?.oneTimeSetting?.taskTypeName != null)
Text(
_taskData!.oneTimeSetting!.taskTypeName!,
style: AppTextStyles.heading4.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
),
),
8.height,
if (_taskData?.oneTimeSetting?.instructionDescriptionName != null)
Text(
_taskData!.oneTimeSetting!.instructionDescriptionName!,
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
8.height,
if (_taskData?.siteName != null) _buildInfoRow('Site', _taskData!.siteName!),
if (_taskData?.buildingName != null) _buildInfoRow('Building', _taskData!.buildingName!),
if (_taskData?.floorName != null) _buildInfoRow('Floor', _taskData!.floorName!),
if (_taskData?.departmentName != null) _buildInfoRow('Department', _taskData!.departmentName!),
if (_taskData?.roomName != null) _buildInfoRow('Room', _taskData!.roomName!),
if (_taskData?.locationDescription != null) _buildInfoRow('Location', _taskData!.locationDescription!),
// Timer display using AppTimer widget
if (_timerStart != null && !isClosed) ...[
12.height,
AppTimer(
label: 'Working Time',
timer: TimerModel(
startAt: _timerStart,
endAt: null,
durationInSecond: 0,
),
enabled: false, // Disable controls, just show the timer
showTimer: true,
decoration: BoxDecoration(
color: context.isDark ? AppColor.neutral20.withValues(alpha: 0.5) : AppColor.neutral10.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
),
],
// Display total duration if task is completed
if (isClosed && _taskData?.startTime != null && _taskData?.endTime != null) ...[
// 12.height,
Text(
'Total Duration: ${_formatDuration(DateTime.parse(_taskData!.endTime!).difference(DateTime.parse(_taskData!.startTime!)))}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
],
],
).paddingOnly(start: 16, end: 16, top: 16, bottom: 16),
],
).toShadowContainer(context, padding: 0);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
'$label: $value',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
);
}
List<ChecklistItem> _convertInstructionsToChecklist() {
// If checklist items are already created, reuse them
if (_checklistItems != null) {
// Update the existing items with latest data from instructions
for (var item in _checklistItems!) {
final instruction = _taskData?.instructions?.firstWhere((i) => i.id == item.id, orElse: () => Instruction());
if (instruction?.id != null) {
item.isChecked = instruction!.isDone ?? false;
// Only update comment if it's different to avoid cursor jumping
if (item.comment != (instruction.comment ?? '')) {
item.comment = instruction.comment ?? '';
}
}
}
return _checklistItems!;
}
// Create new checklist items only on first call
_checklistItems = _taskData?.instructions?.map((instruction) {
return ChecklistItem(
id: instruction.id ?? 0,
title: instruction.instructionText ?? '',
isChecked: instruction.isDone ?? false,
isMandatory: instruction.isRequired ?? false,
comment: instruction.comment ?? '',
);
}).toList() ??
[];
return _checklistItems!;
}
void _updateInstructions(List<ChecklistItem> checklist) {
_taskData?.instructions = checklist.map((item) {
return Instruction(
id: item.id,
instructionTextId: _taskData?.instructions?.firstWhere((i) => i.id == item.id).instructionTextId,
instructionText: item.title,
isDone: item.isChecked,
isRequired: item.isMandatory,
comment: item.comment,
);
}).toList();
}
void _onComplete() {
// Validate mandatory checklist items
final uncheckedMandatory = _taskData?.instructions?.where((item) => (item.isRequired ?? false) && !(item.isDone ?? false)).toList() ?? [];
if (uncheckedMandatory.isNotEmpty) {
_showErrorDialog(context, "Please complete all mandatory tasks (marked with *)");
return;
}
// Validate that at least one image is captured
final capturedImages = _taskData?.imageRooms ?? [];
print('DEBUG: Captured images count: ${capturedImages.length}');
print('DEBUG: Images: $capturedImages');
if (capturedImages.isEmpty) {
_showErrorDialog(context, "Please capture at least one image before completing");
return;
}
// Calculate working hours
double totalWorkingHours = 0;
if (_timerStart != null) {
final duration = DateTime.now().difference(_timerStart!);
totalWorkingHours = duration.inSeconds / 3600.0;
}
// Navigate to preview screen
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => OneTimeTaskPreviewScreen(
taskData: _taskData!,
totalWorkingHours: totalWorkingHours,
onConfirm: () {
Navigator.pop(context); // Close preview screen
_performComplete(); // Proceed with completion
},
),
),
);
}
Future<void> _performComplete() async {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AppLazyLoading(),
);
try {
final instructionsPayload = _taskData?.instructions?.map((instruction) {
return {
'id': instruction.id,
'isDone': instruction.isDone,
'comment': instruction.comment,
};
}).toList() ??
[];
// Convert images to attachments format
final attachmentsPayload = _taskData?.imageRooms?.map((imageString) {
// imageString format: "filename|base64data"
final parts = imageString.split('|');
final originalName = parts.isNotEmpty ? parts[0] : 'image.jpg';
final base64Data = parts.length > 1 ? parts[1] : imageString;
return {
'id': 0,
'name': "$originalName|$base64Data", // Send full formatted string
'originalName': originalName,
};
}).toList() ??
[];
final result = await CleanerProvider().completeOneTimeTask(
id: _taskData!.id!,
instructions: instructionsPayload,
attachments: attachmentsPayload,
);
if (mounted) Navigator.pop(context); // Close loading dialog
if (result['success']) {
if (mounted) {
// Navigate to dashboard
Navigator.of(context).pushNamedAndRemoveUntil(
LandPage.routeName,
(routes) => false,
);
}
} else {
if (mounted) {
_showErrorDialog(context, result['message'] ?? 'Failed to complete task');
}
}
} catch (e) {
if (mounted) Navigator.pop(context);
if (mounted) {
_showErrorDialog(context, 'Failed to complete task. Please try again.');
}
}
}
void _showErrorDialog(BuildContext context, String message) {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: message,
title: 'Error',
onTap: () => Navigator.pop(context),
),
);
}
@override
void dispose() {
// Dispose all checklist item controllers
if (_checklistItems != null) {
for (var item in _checklistItems!) {
item.dispose();
}
}
super.dispose();
}
}

@ -102,6 +102,7 @@ class CleanerProvider extends ChangeNotifier {
int? floorId, int? floorId,
int? departmentId, int? departmentId,
int? roomId, int? roomId,
int? taskTypeId,
}) async { }) async {
Response response; Response response;
try { try {
@ -115,6 +116,7 @@ class CleanerProvider extends ChangeNotifier {
"floorId": floorId, "floorId": floorId,
"departmentId": departmentId, "departmentId": departmentId,
"roomId": roomId, "roomId": roomId,
"taskTypeId": taskTypeId,
}, },
); );
@ -145,6 +147,7 @@ class CleanerProvider extends ChangeNotifier {
"planRecurrentTaskId": planRecurrentTaskId, "planRecurrentTaskId": planRecurrentTaskId,
"qrCode": qrCode, "qrCode": qrCode,
}, },
showToast: false, // Disable automatic toast message
); );
if (response.statusCode >= 200 && response.statusCode < 300) { if (response.statusCode >= 200 && response.statusCode < 300) {
@ -159,6 +162,227 @@ class CleanerProvider extends ChangeNotifier {
} }
} }
/// Check if QR code matches the same room for a plan recurrent task
/// Returns a map with success status and message from backend
Future<Map<String, dynamic>> checkQRSameRoomWithMessage({
required int planRecurrentTaskId,
required String qrCode,
}) async {
Response response;
try {
response = await ApiManager.instance.post(
URLs.checkQRSameRoom,
body: {
"planRecurrentTaskId": planRecurrentTaskId,
"qrCode": qrCode,
},
showToast: false, // Disable automatic toast message
);
// Parse the response body to extract the message
final responseData = json.decode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
// Success response
final isValid = responseData["data"] == true || responseData["isSuccess"] == true;
return {
"success": isValid,
"message": responseData["message"] ?? (isValid ? "QR code validated successfully" : "QR code validation failed"),
};
} else {
// Error response - extract the exact error message from backend
return {
"success": false,
"message": responseData["message"] ?? "QR code validation failed",
};
}
} catch (error) {
debugPrint('Error checking QR same room: $error');
return {
"success": false,
"message": "Failed to validate QR code. Please check your connection and try again.",
};
}
}
/// Scan QR Code and get task details
/// Returns a map with success status, message, and data from backend
Future<Map<String, dynamic>> scanQRCode({
required String qrCode,
}) async {
Response response;
try {
response = await ApiManager.instance.post(
URLs.scanQRCode,
body: {
"qrCode": qrCode,
},
showToast: false, // Disable automatic toast message
);
// Parse the response body to extract the message
final responseData = json.decode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
// Success response
return {
"success": true,
"data": responseData["data"],
"message": responseData["message"] ?? "QR code scanned successfully",
};
} else {
// Error response - extract the exact error message from backend
return {
"success": false,
"data": null,
"message": responseData["message"] ?? "QR code scan failed",
};
}
} catch (error) {
debugPrint('Error scanning QR code: $error');
return {
"success": false,
"data": null,
"message": "Failed to scan QR code. Please check your connection and try again.",
};
}
}
/// Proceed with OneTime Task request
/// Returns a map with success status, message, and data from backend
Future<Map<String, dynamic>> proceedOneTimeTask({
required int oneTimeSettingId,
required int siteId,
required int buildingId,
required int floorId,
required int departmentId,
required int roomId,
}) async {
Response response;
try {
response = await ApiManager.instance.post(
URLs.proceedRequest,
body: {
"oneTimeSettingId": oneTimeSettingId,
"siteId": siteId,
"buildingId": buildingId,
"floorId": floorId,
"departmentId": departmentId,
"roomId": roomId,
},
showToast: false, // Disable automatic toast message
);
// Parse the response body to extract the message
final responseData = json.decode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
// Success response
return {
"success": true,
"data": responseData["data"],
"message": responseData["message"] ?? "Request proceeded successfully",
};
} else {
// Error response - extract the exact error message from backend
return {
"success": false,
"data": null,
"message": responseData["message"] ?? "Failed to proceed with request",
};
}
} catch (error) {
debugPrint('Error proceeding with request: $error');
return {
"success": false,
"data": null,
"message": "Failed to proceed with request. Please check your connection and try again.",
};
}
}
/// Get OneTime Task by ID using getRequestById endpoint
Future<Map<String, dynamic>> getOneTimeTaskById({
required int id,
}) async {
Response response;
try {
response = await ApiManager.instance.get(
'${URLs.getRequestById}?id=$id',
);
// Parse the response body
final responseData = json.decode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
// Success response
return {
"success": true,
"data": responseData["data"],
"message": responseData["message"] ?? "Task retrieved successfully",
};
} else {
// Error response
return {
"success": false,
"data": null,
"message": responseData["message"] ?? "Failed to retrieve task",
};
}
} catch (error) {
debugPrint('Error getting one time task: $error');
return {
"success": false,
"data": null,
"message": "Failed to retrieve task. Please check your connection and try again.",
};
}
}
/// Complete OneTime Task
Future<Map<String, dynamic>> completeOneTimeTask({
required int id,
required List<Map<String, dynamic>> instructions,
List<Map<String, dynamic>>? attachments,
}) async {
Response response;
try {
response = await ApiManager.instance.post(
URLs.completeRequest,
body: {
"id": id,
"instructions": instructions,
"attachments": attachments ?? [],
},
showToast: false,
);
final responseData = json.decode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
return {
"success": true,
"data": responseData["data"],
"message": responseData["message"] ?? "Task completed successfully",
};
} else {
return {
"success": false,
"data": null,
"message": responseData["message"] ?? "Failed to complete task",
};
}
} catch (error) {
debugPrint('Error completing one time task: $error');
return {
"success": false,
"data": null,
"message": "Failed to complete task. Please check your connection and try again.",
};
}
}
void reset() { void reset() {
_isLoading = false; _isLoading = false;
_searchResults = null; _searchResults = null;

@ -1,85 +0,0 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:http/http.dart';
import 'package:test_sa/controllers/api_routes/api_manager.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/models/new_models/floor.dart';
class FmsFloorProvider extends ChangeNotifier {
//reset provider data
void reset() {
_floors = null;
_stateCode = null;
}
// state code of current request to defied error message
int? _stateCode;
int? get stateCode => _stateCode;
// contain floors data
List<Floor>? _floors;
List<Floor>? get floors => _floors;
// loading state
bool _loading = false;
bool get isLoading => _loading;
set isLoading(bool isLoading) {
_loading = isLoading;
notifyListeners();
}
/// Get floors by site and building ID
Future<int> getFloorsBySiteAndBuilding(int siteId, int buildingId) async {
if (_loading == true) return -2;
_loading = true;
notifyListeners();
Response response;
try {
response = await ApiManager.instance.post(
URLs.getCustomerFloors,
body: {
"siteId": siteId,
"buildingId": buildingId,
},
);
_stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) {
// client's request was successfully received
List floorsListJson = json.decode(response.body)["data"];
_floors = floorsListJson.map((floor) => Floor.fromJson(floor)).toList();
}
_loading = false;
notifyListeners();
return response.statusCode;
} catch (error) {
_loading = false;
_stateCode = -1;
notifyListeners();
return -1;
}
}
/// Get floors list directly without provider state
Future<List<Floor>> getFloorsListBySiteAndBuilding(int siteId, int buildingId) async {
Response response;
try {
response = await ApiManager.instance.post(
URLs.getCustomerFloors,
body: {
"siteId": siteId,
"buildingId": buildingId,
},
);
if (response.statusCode >= 200 && response.statusCode < 300) {
List floorsListJson = json.decode(response.body)["data"];
return floorsListJson.map((floor) => Floor.fromJson(floor)).toList();
}
return [];
} catch (error) {
return [];
}
}
}

@ -1,140 +0,0 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:http/http.dart';
import 'package:test_sa/controllers/api_routes/api_manager.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/models/lookup.dart';
class LocationDescriptionResponse {
List<Lookup>? locationDescriptions;
List<DepartmentRoomItem>? deptRoom;
LocationDescriptionResponse({
this.locationDescriptions,
this.deptRoom,
});
factory LocationDescriptionResponse.fromJson(Map<String, dynamic> json) {
return LocationDescriptionResponse(
locationDescriptions: json['locationDescriptions'] != null
? (json['locationDescriptions'] as List)
.map((e) => Lookup(name: e['locationDescriptionName']))
.toList()
: null,
deptRoom: json['deptRoom'] != null
? (json['deptRoom'] as List)
.map((e) => DepartmentRoomItem.fromJson(e))
.toList()
: null,
);
}
}
class DepartmentRoomItem {
String? locationDescriptionName;
String? deptAndRoomName;
String? deptName;
String? roomName;
int? deptId;
int? roomId;
DepartmentRoomItem({
this.locationDescriptionName,
this.deptAndRoomName,
this.deptName,
this.roomName,
this.deptId,
this.roomId,
});
factory DepartmentRoomItem.fromJson(Map<String, dynamic> json) {
return DepartmentRoomItem(
locationDescriptionName: json['locationDescriptionName'],
deptAndRoomName: json['deptAndRoomName'],
deptName: json['deptName'],
roomName: json['roomName'],
deptId: json['deptId'],
roomId: json['roomId'],
);
}
}
class FmsLocationDescriptionProvider extends ChangeNotifier {
//reset provider data
void reset() {
_locationDescriptions = null;
_deptRoomList = null;
_stateCode = null;
}
// state code of current request to defied error message
int? _stateCode;
int? get stateCode => _stateCode;
// contain location descriptions data
List<Lookup>? _locationDescriptions;
List<Lookup>? get locationDescriptions => _locationDescriptions;
// contain dept room list
List<DepartmentRoomItem>? _deptRoomList;
List<DepartmentRoomItem>? get deptRoomList => _deptRoomList;
// loading state
bool _loading = false;
bool get isLoading => _loading;
set isLoading(bool isLoading) {
_loading = isLoading;
notifyListeners();
}
/// Get location descriptions by site, building, and floor ID
Future<int> getLocationDescriptions(int siteId, int buildingId, int floorId) async {
if (_loading == true) return -2;
_loading = true;
notifyListeners();
Response response;
try {
response = await ApiManager.instance.get(
"${URLs.getCustomerLocationDescription}?siteId=$siteId&buildingId=$buildingId&floorId=$floorId",
enableToastMessage: false,
);
_stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) {
// client's request was successfully received
final jsonData = json.decode(response.body)["data"];
LocationDescriptionResponse locationResponse = LocationDescriptionResponse.fromJson(jsonData);
_locationDescriptions = locationResponse.locationDescriptions;
_deptRoomList = locationResponse.deptRoom;
}
_loading = false;
notifyListeners();
return response.statusCode;
} catch (error) {
_loading = false;
_stateCode = -1;
notifyListeners();
return -1;
}
}
/// Get location descriptions and dept room list directly without provider state
Future<LocationDescriptionResponse?> getLocationDescriptionsList(int siteId, int buildingId, int floorId) async {
Response response;
try {
response = await ApiManager.instance.get(
"${URLs.getCustomerLocationDescription}?siteId=$siteId&buildingId=$buildingId&floorId=$floorId",
enableToastMessage: false,
);
if (response.statusCode >= 200 && response.statusCode < 300) {
final jsonData = json.decode(response.body)["data"];
return LocationDescriptionResponse.fromJson(jsonData);
}
return null;
} catch (error) {
return null;
}
}
}

@ -4,22 +4,22 @@ import 'package:flutter/cupertino.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:test_sa/controllers/api_routes/api_manager.dart'; import 'package:test_sa/controllers/api_routes/api_manager.dart';
import 'package:test_sa/controllers/api_routes/urls.dart'; import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/models/new_models/building.dart'; import 'package:test_sa/modules/cleaner_module/models/task_type.dart';
class FmsBuildingProvider extends ChangeNotifier { class TaskTypeProvider extends ChangeNotifier {
//reset provider data //reset provider data
void reset() { void reset() {
_buildings = null; _taskTypes = null;
_stateCode = null; _stateCode = null;
} }
// state code of current request to defied error message // state code of current request to define error message
int? _stateCode; int? _stateCode;
int? get stateCode => _stateCode; int? get stateCode => _stateCode;
// contain buildings data // contain task types data
List<Building>? _buildings; List<TaskType>? _taskTypes;
List<Building>? get buildings => _buildings; List<TaskType>? get taskTypes => _taskTypes;
// loading state // loading state
bool _loading = false; bool _loading = false;
@ -30,24 +30,19 @@ class FmsBuildingProvider extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
/// Get buildings by site ID /// Get task types
Future<int> getBuildingsBySiteId(int siteId) async { Future<int> getTaskTypes() async {
if (_loading == true) return -2; if (_loading == true) return -2;
_loading = true; _loading = true;
notifyListeners(); notifyListeners();
Response response; Response response;
try { try {
response = await ApiManager.instance.post( response = await ApiManager.instance.get(URLs.getTaskType);
URLs.getCustomerBuildings,
body: {
"siteId": siteId,
},
);
_stateCode = response.statusCode; _stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) { if (response.statusCode >= 200 && response.statusCode < 300) {
// client's request was successfully received // client's request was successfully received
List buildingsListJson = json.decode(response.body)["data"]; List taskTypesListJson = json.decode(response.body)["data"];
_buildings = buildingsListJson.map((building) => Building.fromJson(building)).toList(); _taskTypes = taskTypesListJson.map((taskType) => TaskType.fromJson(taskType)).toList();
} }
_loading = false; _loading = false;
notifyListeners(); notifyListeners();
@ -60,20 +55,15 @@ class FmsBuildingProvider extends ChangeNotifier {
} }
} }
/// Get buildings list directly without provider state /// Get task types list directly without provider state
Future<List<Building>> getBuildingsListBySiteId(int siteId) async { Future<List<TaskType>> getTaskTypesList() async {
Response response; Response response;
try { try {
response = await ApiManager.instance.post( response = await ApiManager.instance.get(URLs.getTaskType);
URLs.getCustomerBuildings,
body: {
"siteId": siteId,
},
);
if (response.statusCode >= 200 && response.statusCode < 300) { if (response.statusCode >= 200 && response.statusCode < 300) {
List buildingsListJson = json.decode(response.body)["data"]; List taskTypesListJson = json.decode(response.body)["data"];
return buildingsListJson.map((building) => Building.fromJson(building)).toList(); return taskTypesListJson.map((taskType) => TaskType.fromJson(taskType)).toList();
} }
return []; return [];
} catch (error) { } catch (error) {
@ -81,3 +71,4 @@ class FmsBuildingProvider extends ChangeNotifier {
} }
} }
} }

@ -1,10 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart'; import 'package:test_sa/extensions/string_extensions.dart';
@ -13,15 +9,24 @@ import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_text_form_field.dart'; import 'package:test_sa/new_views/common_widgets/app_text_form_field.dart';
import 'package:test_sa/views/widgets/buttons/app_back_button.dart'; import 'package:test_sa/views/widgets/buttons/app_back_button.dart';
import 'package:test_sa/modules/cleaner_module/widgets/multi_image_picker.dart';
import 'package:test_sa/views/widgets/images/files_list.dart';
class CleanerChecklistWidget extends StatefulWidget { class CleanerChecklistWidget extends StatefulWidget {
final List<ChecklistItem> checklistItems; final List<ChecklistItem> checklistItems;
final Function(List<ChecklistItem>)? onChecklistChanged; final Function(List<ChecklistItem>)? onChecklistChanged;
final String? capturedImageBase64; final List<String> capturedImages;
final Function(String?)? onImageCaptured; final Function(List<String>)? onImagesChanged;
bool isClosed; final bool isClosed;
CleanerChecklistWidget({Key? key, required this.checklistItems, this.onChecklistChanged, this.capturedImageBase64, this.onImageCaptured, this.isClosed = false}) : super(key: key); const CleanerChecklistWidget({
Key? key,
required this.checklistItems,
this.onChecklistChanged,
required this.capturedImages,
this.onImagesChanged,
this.isClosed = false
}) : super(key: key);
@override @override
State<CleanerChecklistWidget> createState() => _CleanerChecklistWidgetState(); State<CleanerChecklistWidget> createState() => _CleanerChecklistWidgetState();
@ -45,8 +50,15 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
), ),
), ),
16.height, 16.height,
...widget.checklistItems.map((item) => _buildChecklistItem(context, item)).toList(), ListView.separated(
// 16.height, shrinkWrap: true,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (cxt, index) => _buildChecklistItem(context, widget.checklistItems[index]),
separatorBuilder: (cxt, index) => const Divider(height: 1, color: AppColor.neutral30, thickness: 1).paddingOnly(top: 16, bottom: 16),
itemCount: widget.checklistItems.length)
.toShadowContainer(context, padding: 16),
16.height,
_buildImageCaptureWidget(context), _buildImageCaptureWidget(context),
], ],
); );
@ -59,28 +71,71 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// Checkbox with title
Row( Row(
children: [ children: [
Checkbox( AbsorbPointer(
// visualDensity: const VisualDensity(horizontal: -4.0, vertical: -4.0), absorbing: true,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, child: Transform.scale(
scale: 1.2,
child: Checkbox(
value: item.isChecked,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: BorderSide(color: Colors.black.withValues(alpha: .05)),
visualDensity: const VisualDensity(horizontal: -4.0, vertical: -4.0),
activeColor: AppColor.primary10,
onChanged: (bool? value) {},
),
),
),
10.width,
RichText(
text: TextSpan(
children: [
TextSpan(
text: item.title,
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
fontWeight: FontWeight.w500,
),
),
if (item.isMandatory)
TextSpan(
text: ' *',
style: AppTextStyles.bodyText.copyWith(
color: Colors.red,
fontWeight: FontWeight.w500,
),
),
],
),
).expanded,
],
).onPress(() {
setState(() {
item.isChecked = !item.isChecked;
if (!item.isChecked) {
item.commentController.clear();
}
});
widget.onChecklistChanged?.call(widget.checklistItems);
}),
/* ListTileTheme(
horizontalTitleGap: 0,
contentPadding: EdgeInsets.zero,
minVerticalPadding: 0,
child: Container(
color: Colors.amberAccent,
child: CheckboxListTile(
value: item.isChecked, value: item.isChecked,
minVerticalPadding: 0,
controlAffinity: ListTileControlAffinity.leading,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
contentPadding: EdgeInsets.zero,
checkboxScaleFactor: 1.2,
side: BorderSide(color: Colors.black.withValues(alpha: .05)),
visualDensity: const VisualDensity(horizontal: -0.0, vertical: -4.0),
activeColor: AppColor.primary10, activeColor: AppColor.primary10,
onChanged: (bool? value) { title: RichText(
setState(() {
item.isChecked = value ?? false;
if (!item.isChecked) {
item.commentController.clear();
}
});
// Notify parent of changes
widget.onChecklistChanged?.call(widget.checklistItems);
},
),
8.width,
Expanded(
child: RichText(
text: TextSpan( text: TextSpan(
children: [ children: [
TextSpan( TextSpan(
@ -101,13 +156,20 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
], ],
), ),
), ),
onChanged: (bool? value) {
setState(() {
item.isChecked = value ?? false;
if (!item.isChecked) {
item.commentController.clear();
}
});
widget.onChecklistChanged?.call(widget.checklistItems);
},
), ),
], ),
), ),*/
// Comment box - only visible when checked
if (item.isChecked) ...[ if (item.isChecked) ...[
16.height, 12.height,
AppTextFormField( AppTextFormField(
controller: item.commentController, controller: item.commentController,
labelText: context.translation.comment, labelText: context.translation.comment,
@ -120,13 +182,12 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
textInputType: TextInputType.multiline, textInputType: TextInputType.multiline,
onChange: (value) { onChange: (value) {
item.comment = value; item.comment = value;
// Notify parent of changes
widget.onChecklistChanged?.call(widget.checklistItems); widget.onChecklistChanged?.call(widget.checklistItems);
}, },
), ),
], ],
], ],
).toShadowContainer(context).paddingOnly(bottom: 16), ),
); );
} }
@ -135,11 +196,23 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Row(
'Captured Image'.addTranslation, mainAxisAlignment: MainAxisAlignment.spaceBetween,
style: AppTextStyles.heading6.copyWith( children: [
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, Text(
), 'Captured Image'.addTranslation,
style: AppTextStyles.heading6.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
),
),
// Text(
// '(${widget.capturedImages.length}/$maxImages)',
// style: AppTextStyles.bodyText.copyWith(
// color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
// fontWeight: FontWeight.w600,
// ),
// ),
],
), ),
8.height, 8.height,
_buildImageWidget(context), _buildImageWidget(context),
@ -147,205 +220,148 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
).toShadowContainer(context); ).toShadowContainer(context);
} }
static const int maxImages = 2;
/// Build the image widget with loading indicator for network images /// Build the image widget with loading indicator for network images
Widget _buildImageWidget(BuildContext context) { Widget _buildImageWidget(BuildContext context) {
if (widget.capturedImageBase64 == null || widget.capturedImageBase64!.isEmpty) { if (widget.capturedImages.isEmpty) {
// Show camera icon when no image // Show add image button when no image
return Container( return GestureDetector(
height: 200, onTap: widget.isClosed ? null : () async {
width: double.infinity, // Only allow camera - pick one image at a time
decoration: BoxDecoration( final pickedFile = await ImagePicker().pickImage(
color: AppColor.fieldBgColor(context), source: ImageSource.camera,
borderRadius: BorderRadius.circular(8), imageQuality: 70,
), maxWidth: 800,
child: Icon( maxHeight: 800,
Icons.camera_alt, );
size: 48,
color: context.isDark ? AppColor.white10 : AppColor.black10,
),
).onPress(() async {
final pickedFile = await ImagePicker().pickImage(source: ImageSource.camera, imageQuality: 70, maxWidth: 800, maxHeight: 800);
if (pickedFile != null) {
// Convert image to Base64 with filename prefix (same format as CM request)
final bytes = await pickedFile.readAsBytes();
final base64Image = base64Encode(bytes);
final fileName = pickedFile.path.split("/").last;
final formattedImage = "$fileName|$base64Image";
// Update parent with captured image
widget.onImageCaptured?.call(formattedImage);
}
});
}
Widget imageWidget;
final imageString = widget.capturedImageBase64!;
// First check if it's base64 encoded (contains | separator) if (pickedFile != null) {
if (imageString.contains('|')) { final bytes = await pickedFile.readAsBytes();
// Format: "filename|base64data" final base64Image = base64Encode(bytes);
try { final fileName = pickedFile.path.split("/").last;
final base64Data = imageString.split('|').last; final formattedImage = "$fileName|$base64Image";
imageWidget = Image.memory(base64Decode(base64Data), fit: BoxFit.cover);
return Stack(
alignment: AlignmentDirectional.topEnd,
children: [
Container(
height: 200,
width: double.infinity,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(8),
),
child: imageWidget)
.onPress(() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
body: SafeArea(
child: Stack(
children: [
// InteractiveViewer(child: Image(image: getImageObject(itemIndex))).center,
InteractiveViewer(child: imageWidget).center,
const ABackButton(),
],
),
),
),
),
);
}),
_delete(),
],
);
} catch (e) {
debugPrint('Error decoding base64 image: $e');
return _buildCameraIcon(context);
}
}
// Check if it's a local file path // Add to existing images (max 2)
if (_isLocalUrl(imageString)) { final updatedImages = List<String>.from(widget.capturedImages);
try { updatedImages.add(formattedImage);
imageWidget = Image.file(File(imageString), fit: BoxFit.cover);
return Stack(
alignment: AlignmentDirectional.topEnd,
children: [
Container(
height: 200,
width: double.infinity,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(8),
),
child: imageWidget)
.onPress(() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
body: SafeArea(
child: Stack(
children: [
// InteractiveViewer(child: Image(image: getImageObject(itemIndex))).center,
InteractiveViewer(child: imageWidget).center,
const ABackButton(),
],
),
),
),
),
);
}),
_delete(),
],
);
} catch (e) {
debugPrint('Error loading local file: $e');
return _buildCameraIcon(context);
}
}
// If not local and not base64, it's a server filename - use NetworkImage with loading indicator widget.onImagesChanged?.call(updatedImages);
final imageUrl = URLs.getFileUrl(imageString);
if (imageUrl != null) {
imageWidget = Image.network(
imageUrl,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) {
return child;
} }
return Center(
child: CircularProgressIndicator(
color: AppColor.primary10,
value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null,
),
);
},
errorBuilder: (context, error, stackTrace) {
debugPrint('Error loading network image: $error');
return _buildCameraIcon(context);
}, },
); child: Container(
return Stack( height: 120,
alignment: AlignmentDirectional.topEnd, width: 120,
children: [ decoration: BoxDecoration(
Container( color: AppColor.fieldBgColor(context),
height: 200, borderRadius: BorderRadius.circular(8),
width: double.infinity, border: Border.all(
decoration: BoxDecoration( color: context.isDark ? AppColor.neutral20 : AppColor.neutral30,
color: AppColor.fieldBgColor(context), width: 2,
borderRadius: BorderRadius.circular(8), style: BorderStyle.solid,
), ),
child: imageWidget) ),
.onPress(() { child: Column(
Navigator.of(context).push( mainAxisAlignment: MainAxisAlignment.center,
MaterialPageRoute( children: [
builder: (_) => Scaffold( Icon(
body: SafeArea( Icons.add_photo_alternate,
child: Stack( size: 32,
children: [ color: context.isDark ? AppColor.white10 : AppColor.black10,
// InteractiveViewer(child: Image(image: getImageObject(itemIndex))).center, ),
InteractiveViewer(child: imageWidget).center, 4.height,
const ABackButton(), Text(
], 'Add Image',
), style: AppTextStyles.tinyFont.copyWith(
), color: context.isDark ? AppColor.white10 : AppColor.black10,
), ),
), ),
); ],
}), ),
_delete(), ),
],
); );
} }
return _buildCameraIcon(context); // Check if images are URLs from API (like ServiceRequestDetailView does)
} final bool hasApiImages = widget.capturedImages.any((img) =>
img.startsWith('http://') || img.startsWith('https://'));
/// Build camera icon widget // If images are from API (URLs), use FilesList widget like ServiceRequestDetailView
Widget _buildCameraIcon(BuildContext context) { if (hasApiImages && widget.isClosed) {
return Container( return FilesList(
height: 200, images: widget.capturedImages,
width: double.infinity, showAsListView: false,
decoration: BoxDecoration( );
color: AppColor.fieldBgColor(context), }
borderRadius: BorderRadius.circular(8),
), // Otherwise, use MultiImagePicker for base64 images or when editing
child: Icon( return MultiImagePicker(
Icons.camera_alt, imageUrls: widget.capturedImages,
size: 48, enabled: !widget.isClosed,
color: context.isDark ? AppColor.white10 : AppColor.black10, maxImages: 2, // Enforce max 2 images
), onImageTap: (index, imageUrl) {
// Handle image tap - show in full screen
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: Stack(
children: [
InteractiveViewer(
child: _buildImageForViewing(imageUrl),
).center,
const ABackButton(),
],
),
),
),
),
);
},
onDelete: (index) {
// Handle image delete
if (index >= 0) {
setState(() {
widget.capturedImages.removeAt(index);
});
widget.onImagesChanged?.call(widget.capturedImages);
}
},
onAdd: (imageUrl) {
// Handle new image addition
setState(() {
widget.capturedImages.add(imageUrl);
});
widget.onImagesChanged?.call(widget.capturedImages);
},
); );
} }
/// Check if the URL is a local file path /// Build image widget for viewing based on source type
/// Same logic as multi_image_picker_item.dart Widget _buildImageForViewing(String imageUrl) {
bool _isLocalUrl(String url) { try {
if (url.isEmpty) return false; // Check if it's a URL from API
return url.startsWith("/") || url.startsWith("file://") || (url.length > 1 && url.substring(1).startsWith(':\\')); if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
return Image.network(
imageUrl,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.broken_image, size: 48, color: Colors.white);
},
);
}
// Otherwise, it's base64 encoded
final parts = imageUrl.split('|');
final base64Data = parts.length > 1 ? parts[1] : imageUrl;
return Image.memory(
base64Decode(base64Data),
fit: BoxFit.contain,
);
} catch (e) {
return const Icon(Icons.broken_image, size: 48, color: Colors.white);
}
} }
@override @override
@ -353,26 +369,6 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
// Note: Don't dispose controllers here as they're managed by ChecklistItem // Note: Don't dispose controllers here as they're managed by ChecklistItem
super.dispose(); super.dispose();
} }
Widget _delete() {
if (widget.isClosed) return const SizedBox();
return Container(
height: 45.toScreenHeight,
width: 45.toScreenHeight,
margin: const EdgeInsets.only(top: 4, right: 4),
decoration: BoxDecoration(
color: (context.isDark ? AppColor.neutral20 : AppColor.neutral40).withValues(alpha: 0.4),
border: Border.all(
color: (context.isDark ? AppColor.neutral20 : AppColor.neutral40).withValues(alpha: 0.7),
),
borderRadius: BorderRadius.circular(30),
//boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10)],
),
child: "delete".toSvgAsset())
.onPress(() {
widget.onImageCaptured?.call(null);
});
}
} }
/// Model class for checklist items /// Model class for checklist items
@ -390,10 +386,7 @@ class ChecklistItem {
required this.isChecked, required this.isChecked,
required this.comment, required this.comment,
this.isMandatory = false, this.isMandatory = false,
}) : commentController = TextEditingController(text: comment) { }) : commentController = TextEditingController(text: comment);
// Update the controller text whenever comment changes
commentController.text = comment;
}
// Convert to JSON for API submission // Convert to JSON for API submission
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {

@ -0,0 +1,218 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/views/widgets/buttons/app_back_button.dart';
import 'package:test_sa/modules/cm_module/cm_request_utils.dart';
class MultiImagePicker extends StatelessWidget {
final List<String> imageUrls;
final Function(int index, String imageUrl)? onImageTap;
final Function(int index)? onDelete;
final Function(String imageUrl)? onAdd;
final bool enabled;
final int maxImages;
const MultiImagePicker({
Key? key,
required this.imageUrls,
this.onImageTap,
this.onDelete,
this.onAdd,
this.enabled = true,
this.maxImages = 2,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Display existing images horizontally with add button
Row(
children: [
// Display existing images
...List.generate(
imageUrls.length,
(index) {
return Padding(
padding: EdgeInsets.only(right: 8.toScreenWidth),
child: _buildImageItem(context, index),
);
},
),
// Add image button (only show if under max limit)
if (enabled && imageUrls.length < maxImages)
_buildAddImageButton(context),
],
),
],
);
}
Widget _buildImageItem(BuildContext context, int index) {
final imageUrl = imageUrls[index];
return Stack(
alignment: AlignmentDirectional.topEnd,
children: [
Container(
height: 120,
width: 120,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(8),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: _getImageWidget(imageUrl),
),
).onPress(() {
if (onImageTap != null) {
onImageTap!(index, imageUrl);
} else {
_showFullImage(context, imageUrl);
}
}),
if (enabled)
Container(
height: 32,
width: 32,
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: (context.isDark ? AppColor.neutral20 : AppColor.neutral40).withValues(alpha: 0.9),
border: Border.all(
color: (context.isDark ? AppColor.neutral20 : AppColor.neutral40).withValues(alpha: 0.7),
),
borderRadius: BorderRadius.circular(20),
),
child: const Icon(Icons.close, color: Color(0xffF63939), size: 20),
).onPress(() {
if (onDelete != null) {
onDelete!(index);
}
}),
],
);
}
Widget _buildAddImageButton(BuildContext context) {
return GestureDetector(
onTap: () => _pickImage(context),
child: Container(
height: 120,
width: 120,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: context.isDark ? AppColor.neutral20 : AppColor.neutral30,
width: 2,
style: BorderStyle.solid,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.add_photo_alternate,
size: 32,
color: context.isDark ? AppColor.white10 : AppColor.black10,
),
4.height,
Text(
'Add Image',
style: AppTextStyles.tinyFont.copyWith(
color: context.isDark ? AppColor.white10 : AppColor.black10,
),
),
],
),
),
);
}
Widget _getImageWidget(String imageUrl) {
// Check if it's base64 encoded (contains | separator)
if (imageUrl.contains('|')) {
try {
final base64Data = imageUrl.split('|').last;
return Image.memory(
base64Decode(base64Data),
fit: BoxFit.cover,
);
} catch (e) {
return const Icon(Icons.broken_image, size: 48);
}
}
// Check if it's a local file path
if (CMRequestUtils.isLocalUrl(imageUrl)) {
return Image.file(
File(imageUrl),
fit: BoxFit.cover,
);
}
// Otherwise treat as network URL
return Image.network(
imageUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.broken_image, size: 48);
},
);
}
void _pickImage(BuildContext context) async {
// Directly open camera - no bottom sheet
final ImagePicker picker = ImagePicker();
final XFile? pickedFile = await picker.pickImage(
source: ImageSource.camera,
imageQuality: 70,
maxWidth: 800,
maxHeight: 800,
);
if (pickedFile != null) {
final bytes = await pickedFile.readAsBytes();
final base64Image = base64Encode(bytes);
final fileName = pickedFile.path.split("/").last;
final formattedImage = "$fileName|$base64Image";
// Call the onAdd callback to notify parent
if (onAdd != null) {
onAdd!(formattedImage);
}
}
}
void _showFullImage(BuildContext context, String imageUrl) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: Stack(
children: [
Center(
child: InteractiveViewer(
child: _getImageWidget(imageUrl),
),
),
const ABackButton(),
],
),
),
),
),
);
}
}

@ -0,0 +1,137 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/all_requests_and_count_model.dart';
import 'package:test_sa/models/new_models/dashboard_detail.dart';
import 'package:test_sa/modules/cleaner_module/pages/one_time_task_view.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/views/widgets/requests/request_status.dart';
class OneTimeTaskItemView extends StatelessWidget {
final Data? requestData;
final RequestsDetails? requestDetails;
final bool showShadow;
const OneTimeTaskItemView({Key? key, this.requestData, this.requestDetails, this.showShadow = true}) : super(key: key);
@override
Widget build(BuildContext context) {
if (requestData != null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
StatusLabel(
label: requestData!.statusName ?? '',
textColor: AppColor.getRequestStatusTextColorByName(context, requestData!.statusName ?? ''),
backgroundColor: AppColor.getRequestStatusColorByName(context, requestData!.statusName ?? ''),
),
1.width.expanded,
Text(
requestData!.transactionDate?.toServiceRequestCardFormat ?? "",
textAlign: TextAlign.end,
style: AppTextStyles.tinyFont.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral50),
),
],
),
8.height,
(requestData?.typeTransaction ?? context.translation.recurrentWo).heading5(context),
8.height,
'${context.translation.taskNo}: ${requestData!.requestNo}'.bodyText(context),
if (requestData?.siteName != null) '${context.translation.site}: ${requestData!.siteName}'.bodyText(context),
if (requestData!.buildingName != null) '${context.translation.building}: ${requestData!.buildingName}'.bodyText(context),
if (requestData!.departmentName != null) '${context.translation.department}: ${requestData!.departmentName}'.bodyText(context),
if (requestData!.floorName != null) '${context.translation.floor}: ${requestData!.floorName}'.bodyText(context),
if (requestData!.roomName != null) '${context.translation.room}: ${requestData!.roomName}'.bodyText(context),
8.height,
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.translation.viewDetails,
style: AppTextStyles.bodyText.copyWith(color: AppColor.blueStatus(context)),
),
4.width,
Icon(Icons.arrow_forward, color: AppColor.blueStatus(context), size: 14)
],
),
],
).toShadowContainer(context, withShadow: showShadow).onPress(() {
// Navigate to editable view for in-progress tasks
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => OneTimeTaskView(
taskId: requestData!.id ?? 0,
initialData: null,
),
),
);
});
}
if (requestDetails != null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
StatusLabel(
label: requestDetails!.status!,
textColor: AppColor.getRequestStatusTextColorByName(context, requestDetails!.status!),
backgroundColor: AppColor.getRequestStatusColorByName(context, requestDetails!.status!),
),
1.width.expanded,
Text(
requestDetails!.date?.toServiceRequestCardFormat ?? "",
textAlign: TextAlign.end,
style: AppTextStyles.tinyFont.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral50),
),
],
),
8.height,
(requestDetails?.nameOfType ?? context.translation.recurrentWo).heading5(context),
8.height,
'${context.translation.site}: ${requestDetails!.site}'.bodyText(context),
'${context.translation.taskNo}: ${requestDetails!.requestNo}'.bodyText(context),
if (requestDetails!.siteName != null) '${context.translation.site}: ${requestDetails!.siteName}'.bodyText(context),
if (requestDetails!.buildingName != null) '${context.translation.building}: ${requestDetails!.buildingName}'.bodyText(context),
if (requestDetails!.departmentName != null) '${context.translation.department}: ${requestDetails!.departmentName}'.bodyText(context),
if (requestDetails!.floorName != null) '${context.translation.floor}: ${requestDetails!.floorName}'.bodyText(context),
if (requestDetails!.roomName != null) '${context.translation.room}: ${requestDetails!.roomName}'.bodyText(context),
8.height,
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.translation.viewDetails,
style: AppTextStyles.bodyText.copyWith(color: AppColor.blueStatus(context)),
),
4.width,
Icon(Icons.arrow_forward, color: AppColor.blueStatus(context), size: 14)
],
),
],
).toShadowContainer(context, withShadow: showShadow).onPress(() async {
// Navigate to editable view for in-progress tasks
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => OneTimeTaskView(
taskId: requestDetails!.id ?? 0,
initialData: null,
),
),
);
});
}
return const SizedBox();
}
}

@ -141,10 +141,10 @@ class _ServiceRequestDetailViewState extends State<ServiceRequestDetailView> {
// spacing: 8, // spacing: 8,
runSpacing: 8, runSpacing: 8,
children: [ children: [
if (Provider.of<SettingProvider>(context, listen: false).showPriority) ...[ if (Provider.of<SettingProvider>(context, listen: false).showPriority&&workOrder.priority!=null) ...[
StatusLabel( StatusLabel(
label: workOrder.priority?.name, label: workOrder.priority?.name,
id: workOrder.priority!.value!, id: workOrder.priority?.value,
radius: 4, radius: 4,
textColor: AppColor.getPriorityStatusTextColor(context, workOrder.priority!.value!), textColor: AppColor.getPriorityStatusTextColor(context, workOrder.priority!.value!),
backgroundColor: AppColor.getPriorityStatusColor(context, workOrder.priority!.value!), backgroundColor: AppColor.getPriorityStatusColor(context, workOrder.priority!.value!),

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:test_sa/controllers/validator/validator.dart'; import 'package:test_sa/controllers/validator/validator.dart';
import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/enum_extensions.dart';
import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart'; import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart';
@ -324,6 +325,7 @@ class _CreateLoanRequestPageState extends State<CreateLoanRequestPage> with Tick
SingleItemDropDownMenu<TrafDepartment, DepartmentLookupProvider>( SingleItemDropDownMenu<TrafDepartment, DepartmentLookupProvider>(
context: context, context: context,
title: context.translation.department, title: context.translation.department,
matchBy: DropDownMatchType.identifier,
showShadow: false, showShadow: false,
enabled: !isBackUpRequest, enabled: !isBackUpRequest,
validator: (value) { validator: (value) {

@ -7,7 +7,6 @@ import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/ppm/recurrent_wo.dart'; import 'package:test_sa/models/ppm/recurrent_wo.dart';
import 'package:test_sa/modules/cm_module/cm_request_utils.dart';
import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_text_form_field.dart'; import 'package:test_sa/new_views/common_widgets/app_text_form_field.dart';
import 'package:test_sa/new_views/common_widgets/working_time_tile.dart'; import 'package:test_sa/new_views/common_widgets/working_time_tile.dart';
@ -20,9 +19,11 @@ import 'package:test_sa/views/widgets/total_working_time_detail_bottomsheet.dart
class RecurrentTaskInfoWidget extends StatefulWidget { class RecurrentTaskInfoWidget extends StatefulWidget {
final RecurrentWoData? model; final RecurrentWoData? model;
bool isClosed; final bool isClosed;
final bool isHouseKeeper;
final DateTime? timerStartTime;
RecurrentTaskInfoWidget({super.key, required this.model, this.isClosed = false}); const RecurrentTaskInfoWidget({super.key, required this.model, this.isClosed = false, this.isHouseKeeper = false, this.timerStartTime});
@override @override
State<RecurrentTaskInfoWidget> createState() => _RecurrentTaskInfoWidgetState(); State<RecurrentTaskInfoWidget> createState() => _RecurrentTaskInfoWidgetState();
@ -82,6 +83,11 @@ class _RecurrentTaskInfoWidgetState extends State<RecurrentTaskInfoWidget> {
InfoTextWidget(label: context.translation.taskNo, value: widget.model!.taskNo!), InfoTextWidget(label: context.translation.taskNo, value: widget.model!.taskNo!),
InfoTextWidget(label: 'Plan Name:'.addTranslation, value: widget.model?.planRecurrent?.planName), InfoTextWidget(label: 'Plan Name:'.addTranslation, value: widget.model?.planRecurrent?.planName),
InfoTextWidget(label: context.translation.site, value: widget.model!.site!.siteName!), InfoTextWidget(label: context.translation.site, value: widget.model!.site!.siteName!),
if (widget.isHouseKeeper) ...[
'${context.translation.department}: ${widget.model!.department?.name ?? "-"}'.bodyText2(context).custom(color: AppColor.neutral120),
'${context.translation.floor}: ${widget.model!.floor?.name ?? "-"}'.bodyText2(context).custom(color: AppColor.neutral120),
'${context.translation.room}: ${widget.model!.room?.name ?? "-"}'.bodyText2(context).custom(color: AppColor.neutral120),
],
InfoTextWidget(label: context.translation.assignedEmployee, value: widget.model!.engineer!.userName ?? ""), InfoTextWidget(label: context.translation.assignedEmployee, value: widget.model!.engineer!.userName ?? ""),
InfoTextWidget(label: context.translation.scheduledDate, value: widget.model!.scheduleDate!.toMonthYearFormat), InfoTextWidget(label: context.translation.scheduledDate, value: widget.model!.scheduleDate!.toMonthYearFormat),
], ],
@ -92,16 +98,20 @@ class _RecurrentTaskInfoWidgetState extends State<RecurrentTaskInfoWidget> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
buildingInfoWidget(label: context.translation.department, value: widget.model!.department?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context) if (!widget.isHouseKeeper) ...[
.ignorePointer(widget.isClosed), buildingInfoWidget(label: context.translation.department, value: widget.model!.department?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context)
8.height, .ignorePointer(widget.isClosed),
buildingInfoWidget(label: context.translation.floor, value: widget.model!.floor?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context).ignorePointer(widget.isClosed), 8.height,
8.height, buildingInfoWidget(label: context.translation.floor, value: widget.model!.floor?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context).ignorePointer(widget.isClosed),
buildingInfoWidget(label: context.translation.room, value: widget.model!.room?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context).ignorePointer(widget.isClosed), 8.height,
8.height, buildingInfoWidget(label: context.translation.room, value: widget.model!.room?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context).ignorePointer(widget.isClosed),
8.height,
],
_timerWidget(context, totalWorkingHours), _timerWidget(context, totalWorkingHours),
8.height, if (!widget.isHouseKeeper) ...[
_commentWidget(context: context, model: widget.model).ignorePointer(widget.isClosed), 8.height,
_commentWidget(context: context, model: widget.model).ignorePointer(widget.isClosed),
],
], ],
// ], // ],
).toShadowContainer(context, padding: 12) ).toShadowContainer(context, padding: 12)
@ -113,6 +123,27 @@ class _RecurrentTaskInfoWidgetState extends State<RecurrentTaskInfoWidget> {
} }
Widget _timerWidget(BuildContext context, double totalWorkingHours) { Widget _timerWidget(BuildContext context, double totalWorkingHours) {
// For housekeeper: show read-only timer counter with history
if (widget.isHouseKeeper) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (!widget.isClosed) ...[
_HousekeeperTimerWidget(
startTime: widget.timerStartTime,
isClosed: widget.isClosed,
),
12.height,
],
if (totalWorkingHours > 0.0)
WorkingTimeTile(
timerList: timerList,
totalWorkingTime: totalWorkingHours,
),
],
);
}
return Consumer<AllRequestsProvider>(builder: (context, snapshot, child) { return Consumer<AllRequestsProvider>(builder: (context, snapshot, child) {
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -169,6 +200,92 @@ class _RecurrentTaskInfoWidgetState extends State<RecurrentTaskInfoWidget> {
} }
} }
// Read-only timer widget for housekeeper
class _HousekeeperTimerWidget extends StatefulWidget {
final DateTime? startTime;
final bool isClosed;
const _HousekeeperTimerWidget({this.startTime, this.isClosed = false});
@override
State<_HousekeeperTimerWidget> createState() => _HousekeeperTimerWidgetState();
}
class _HousekeeperTimerWidgetState extends State<_HousekeeperTimerWidget> {
String _elapsedTime = "0:00:00";
@override
void initState() {
super.initState();
if (widget.startTime != null && !widget.isClosed) {
_startTimer();
} else if (widget.startTime != null && widget.isClosed) {
// If closed, calculate the final time
_updateElapsedTime();
}
}
void _updateElapsedTime() {
if (widget.startTime != null) {
final duration = DateTime.now().difference(widget.startTime!);
final hours = duration.inHours;
final minutes = duration.inMinutes % 60;
final seconds = duration.inSeconds % 60;
_elapsedTime = "$hours:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}";
}
}
void _startTimer() {
Future.delayed(const Duration(seconds: 1), () {
if (mounted && widget.startTime != null && !widget.isClosed) {
setState(() {
_updateElapsedTime();
});
_startTimer();
}
});
}
@override
Widget build(BuildContext context) {
if (widget.startTime == null && !widget.isClosed) {
return SizedBox.shrink();
}
return Container(
height: 56.toScreenHeight,
padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth),
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(10),
// boxShadow: [
// BoxShadow(
// color: Colors.black.withOpacity(0.05),
// blurRadius: 10,
// )
// ],
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
context.translation.timer.tinyFont(context),
_elapsedTime.bodyText(context).custom(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
),
],
),
),
// No play/pause icon for housekeeper - timer runs automatically
],
),
);
}
}
Widget buildingInfoWidget({required String label, required String? value, required BuildContext context}) { Widget buildingInfoWidget({required String label, required String? value, required BuildContext context}) {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,

@ -1,7 +1,4 @@
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:test_sa/controllers/api_routes/api_manager.dart'; import 'package:test_sa/controllers/api_routes/api_manager.dart';
import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/context_extension.dart';
@ -9,7 +6,6 @@ import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/ppm/recurrent_wo.dart'; import 'package:test_sa/models/ppm/recurrent_wo.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart'; import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/modules/cleaner_module/widgets/cleaner_checklist_widget.dart';
import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart'; import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart'; import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart';
@ -17,6 +13,7 @@ import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/new_views/pages/land_page/land_page.dart'; import 'package:test_sa/new_views/pages/land_page/land_page.dart';
import 'package:test_sa/views/widgets/loaders/app_loading.dart'; import 'package:test_sa/views/widgets/loaders/app_loading.dart';
import 'package:test_sa/views/widgets/loaders/no_data_found.dart'; import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart';
import '../../../../../../controllers/providers/api/all_requests_provider.dart'; import '../../../../../../controllers/providers/api/all_requests_provider.dart';
import 'components/room_tabs_widget.dart'; import 'components/room_tabs_widget.dart';
@ -40,7 +37,6 @@ class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> {
@override @override
void initState() { void initState() {
allRequestsProvider = Provider.of<AllRequestsProvider>(context, listen: false); allRequestsProvider = Provider.of<AllRequestsProvider>(context, listen: false);
// calculateWorkingTime();
Provider.of<AllRequestsProvider>(context, listen: false).getRecurrentWoById(id: widget.taskId!); Provider.of<AllRequestsProvider>(context, listen: false).getRecurrentWoById(id: widget.taskId!);
super.initState(); super.initState();
} }
@ -77,7 +73,12 @@ class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
RecurrentTaskInfoWidget(isClosed: isClosed, model: requestProvider.recurrentWoData), RecurrentTaskInfoWidget(
isClosed: isClosed,
model: requestProvider.recurrentWoData,
isHouseKeeper: false,
timerStartTime: null,
),
IgnorePointer( IgnorePointer(
ignoring: isClosed, ignoring: isClosed,
child: requestProvider.recurrentWoData!.planRecurrentMedicalTaskRooms != null && requestProvider.recurrentWoData!.planRecurrentMedicalTaskRooms!.isNotEmpty child: requestProvider.recurrentWoData!.planRecurrentMedicalTaskRooms != null && requestProvider.recurrentWoData!.planRecurrentMedicalTaskRooms!.isNotEmpty
@ -89,24 +90,6 @@ class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> {
) )
: const SizedBox(), : const SizedBox(),
), ),
16.height,
requestProvider.recurrentWoData!.planRecurrentTaskCheckLists != null && requestProvider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty
? CleanerChecklistWidget(
isClosed: isClosed,
checklistItems: requestProvider.recurrentWoData!.planRecurrentTaskCheckLists ?? [],
onChecklistChanged: (value) {
setState(() {
requestProvider.recurrentWoData!.planRecurrentTaskCheckLists = value;
});
},
capturedImageBase64: requestProvider.recurrentWoData!.imageRoom,
onImageCaptured: (imageBase64) {
setState(() {
requestProvider.recurrentWoData!.imageRoom = imageBase64;
});
},
)
: const SizedBox(),
], ],
), ),
).expanded, ).expanded,
@ -127,7 +110,6 @@ class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> {
AppFilledButton( AppFilledButton(
label: context.translation.complete, label: context.translation.complete,
buttonColor: AppColor.primary10, buttonColor: AppColor.primary10,
// textColor: context.isDark?Colors.white: AppColor.black10,
onPressed: () => _updateTask(context: context, status: 1), onPressed: () => _updateTask(context: context, status: 1),
).expanded, ).expanded,
], ],
@ -146,114 +128,157 @@ class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> {
}); });
}); });
} }
}
void _updateTask({required BuildContext context, required int status}) async { void _updateTask({required BuildContext context, required int status}) async {
AllRequestsProvider allRequestsProvider = Provider.of<AllRequestsProvider>(context, listen: false); // Proceed with update
_performUpdate(context: context, status: status);
}
// Only validate checklist if it exists and status is complete void _performUpdate({required BuildContext context, required int status}) async {
if (status == 1 && allRequestsProvider.recurrentWoData?.planRecurrentTaskCheckLists != null && allRequestsProvider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty) { AllRequestsProvider allRequestsProvider = Provider.of<AllRequestsProvider>(context, listen: false);
// Validate mandatory checklist items only
if (!_validateChecklist(allRequestsProvider.recurrentWoData!)) {
return;
}
// Image validation is handled by backend - no need to force capture here if (validate(context: context, model: allRequestsProvider.recurrentWoData!, status: status)) {
} // Use existing timer logic
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers ?? [];
if (validate(model: allRequestsProvider.recurrentWoData!, status: status)) { if (allRequestsProvider.recurrentWoData?.recurrentWoTimePicker != null) {
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers ?? []; int durationInSecond = allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt!.difference(allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.startAt!).inSeconds;
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add(
PlanRecurrentTaskTimers(
id: 0,
startTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.startAt!.toIso8601String(),
endTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt?.toIso8601String(),
workingHours: ((durationInSecond) / 60 / 60),
),
);
}
allRequestsProvider.recurrentWoData?.timerModelList?.forEach((timer) {
int durationInSecond = timer.endAt!.difference(timer.startAt!).inSeconds;
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add(
PlanRecurrentTaskTimers(
id: 0,
startTime: timer.startAt!.toIso8601String(),
endTime: timer.endAt?.toIso8601String(),
workingHours: ((durationInSecond) / 60 / 60),
),
);
});
if (allRequestsProvider.recurrentWoData?.recurrentWoTimePicker != null) { final uniqueTimersMap = <String, PlanRecurrentTaskTimers>{};
int durationInSecond = allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt!.difference(allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.startAt!).inSeconds; allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.forEach((timer) {
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add( if (timer.startTime != null && timer.endTime != null) {
PlanRecurrentTaskTimers( final key = '${timer.id}_${timer.startTime}_${timer.endTime}';
id: 0, uniqueTimersMap[key] = timer;
startTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.startAt!.toIso8601String(), }
endTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt?.toIso8601String(), });
workingHours: ((durationInSecond) / 60 / 60),
), allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = uniqueTimersMap.values.toList();
); showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading());
}
allRequestsProvider.recurrentWoData?.timerModelList?.forEach((timer) {
int durationInSecond = timer.endAt!.difference(timer.startAt!).inSeconds;
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add(
PlanRecurrentTaskTimers(
id: 0,
startTime: timer.startAt!.toIso8601String(),
endTime: timer.endAt?.toIso8601String(),
workingHours: ((durationInSecond) / 60 / 60),
),
);
});
log('payload for update is ${allRequestsProvider.recurrentWoData?.toJson()}');
showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading());
await allRequestsProvider await allRequestsProvider.updateRecurrentWo(status: status, showToast: false).then((result) {
.updateRecurrentWo( Navigator.pop(context); // Close loading dialog
status: status,
)
.then((success) {
Navigator.pop(context); // Close loading dialog
if (success) {
if (status == 1) {
// When complete is clicked, navigate to LandPage (Dashboard)
allRequestsProvider.reset();
allRequestsProvider.getAllRequests(context, typeTransaction: 5);
allRequestsProvider.recurrentWoData?.recurrentWoTimerModel = null;
// Navigate to LandPage and clear all previous routes bool success = result['success'] ?? false;
Navigator.of(context).pushNamedAndRemoveUntil(LandPage.routeName, (routes) => false); String message = result['message'] ?? '';
if (success) {
if (status == 1) {
// When complete is clicked, navigate to LandPage (Dashboard)
allRequestsProvider.reset();
allRequestsProvider.getAllRequests(context, typeTransaction: 5);
allRequestsProvider.recurrentWoData?.recurrentWoTimerModel = null;
// Navigate to LandPage and clear all previous routes
Navigator.of(context).pushNamedAndRemoveUntil(LandPage.routeName, (routes) => false);
} else {
// For save (status 0), just pop the current screen
allRequestsProvider.recurrentWoData?.recurrentWoTimerModel = null;
Navigator.pop(context);
}
} else { } else {
// For save (status 0), just pop the current screen // Show error dialog with the exact message from API
allRequestsProvider.recurrentWoData?.recurrentWoTimerModel = null; _showErrorDialog(context, message.isNotEmpty ? message : "Failed to update the task. Please try again.");
Navigator.pop(context);
} }
} }).catchError((error) {
}); Navigator.pop(context); // Close loading dialog
_showErrorDialog(context, error.toString());
});
}
} }
}
bool _validateChecklist(RecurrentWoData model) { void _showErrorDialog(BuildContext context, String message) {
// Check if all mandatory checklist items are checked showDialog(
final uncheckedMandatory = model.planRecurrentTaskCheckLists!.where((item) => item.isMandatory && !item.isChecked).toList(); context: context,
builder: (context) => ConfirmDialog(
if (uncheckedMandatory.isNotEmpty) { message: message,
Fluttertoast.showToast( title: 'Error',
msg: "Please complete all mandatory tasks (marked with *)", onTap: () => Navigator.pop(context),
toastLength: Toast.LENGTH_LONG, ),
); );
return false;
} }
return true;
} }
bool validate({required RecurrentWoData model, required int status}) { bool validate({required BuildContext context, required RecurrentWoData model, required int status}) {
// Timer validation logic
bool isTimerPickerEnable = ApiManager.instance.assetGroup?.enabledEngineerTimer ?? false; bool isTimerPickerEnable = ApiManager.instance.assetGroup?.enabledEngineerTimer ?? false;
if (isTimerPickerEnable) { if (isTimerPickerEnable) {
if (model.recurrentWoTimerModel?.startAt == null && model.recurrentWoTimePicker == null) { if (model.recurrentWoTimerModel?.startAt == null && model.recurrentWoTimePicker == null) {
Fluttertoast.showToast(msg: "Working Hours Required"); showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: "Working Hours Required",
title: 'Validation Error',
onTap: () => Navigator.pop(context),
),
);
return false; return false;
} }
if (model.recurrentWoTimePicker == null) { if (model.recurrentWoTimePicker == null) {
if (model.recurrentWoTimerModel?.startAt == null) { if (model.recurrentWoTimerModel?.startAt == null) {
Fluttertoast.showToast(msg: "Working Hours Required"); showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: "Working Hours Required",
title: 'Validation Error',
onTap: () => Navigator.pop(context),
),
);
return false; return false;
} }
if (model.recurrentWoTimerModel?.endAt == null) { if (model.recurrentWoTimerModel?.endAt == null) {
Fluttertoast.showToast(msg: "Please Stop The Timer"); showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: "Please Stop The Timer",
title: 'Validation Error',
onTap: () => Navigator.pop(context),
),
);
return false; return false;
} }
} }
} else { } else {
if (model.recurrentWoTimerModel?.startAt == null) { if (model.recurrentWoTimerModel?.startAt == null) {
Fluttertoast.showToast(msg: "Working Hours Required"); showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: "Working Hours Required",
title: 'Validation Error',
onTap: () => Navigator.pop(context),
),
);
return false; return false;
} }
if (model.recurrentWoTimerModel?.endAt == null) { if (model.recurrentWoTimerModel?.endAt == null) {
Fluttertoast.showToast(msg: "Please Stop The Timer"); showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: "Please Stop The Timer",
title: 'Validation Error',
onTap: () => Navigator.pop(context),
),
);
return false; return false;
} }
} }

@ -168,7 +168,7 @@ class _DeviceFilterScreenState extends State<DeviceFilterScreen> {
Row( Row(
children: [ children: [
ADatePicker( ADatePicker(
label: context.translation.to, label: context.translation.from,
from: DateTime(2000), from: DateTime(2000),
date: DateTime.tryParse(startDate ?? ""), date: DateTime.tryParse(startDate ?? ""),
onDatePicker: (date) { onDatePicker: (date) {

@ -1,3 +1,7 @@
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/context_extension.dart';
@ -7,13 +11,14 @@ import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/helper/utils.dart'; import 'package:test_sa/helper/utils.dart';
import 'package:test_sa/models/device/asset.dart'; import 'package:test_sa/models/device/asset.dart';
import 'package:test_sa/models/generic_attachment_model.dart';
import 'package:test_sa/models/lookup.dart'; import 'package:test_sa/models/lookup.dart';
import 'package:test_sa/models/new_models/traf_customer.dart';
import 'package:test_sa/models/new_models/traf_department.dart'; import 'package:test_sa/models/new_models/traf_department.dart';
import 'package:test_sa/models/new_models/users_based_on_search_model.dart'; import 'package:test_sa/models/new_models/users_based_on_search_model.dart';
import 'package:test_sa/modules/cm_module/cm_request_utils.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart'; import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/modules/traf_module/asset_auto_complete_field.dart'; import 'package:test_sa/modules/traf_module/asset_auto_complete_field.dart';
import 'package:test_sa/modules/traf_module/traf_request_detail_page.dart';
import 'package:test_sa/modules/traf_module/update_traf_request_page.dart';
import 'package:test_sa/modules/traf_module/users_auto_complete_field.dart'; import 'package:test_sa/modules/traf_module/users_auto_complete_field.dart';
import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart'; import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
@ -21,11 +26,12 @@ import 'package:test_sa/new_views/common_widgets/app_text_form_field.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/new_views/common_widgets/multiple_item_drop_down_menu.dart'; import 'package:test_sa/new_views/common_widgets/multiple_item_drop_down_menu.dart';
import 'package:test_sa/new_views/common_widgets/single_item_drop_down_menu.dart'; import 'package:test_sa/new_views/common_widgets/single_item_drop_down_menu.dart';
import 'package:test_sa/providers/loading_list_notifier.dart'; import 'package:test_sa/providers/lookups/customer_lookup_provider.dart';
import 'package:test_sa/providers/lookups/department_lookup_provider.dart'; import 'package:test_sa/providers/lookups/department_lookup_provider.dart';
import 'package:test_sa/providers/lookups/request_type_lookup_provider.dart'; import 'package:test_sa/providers/lookups/request_type_lookup_provider.dart';
import 'package:test_sa/providers/lookups/yes_no_lookup_provider.dart'; import 'package:test_sa/providers/lookups/yes_no_lookup_provider.dart';
import 'package:test_sa/views/widgets/equipment/asset_picker.dart'; import 'package:test_sa/views/widgets/equipment/asset_picker.dart';
import 'package:test_sa/views/widgets/images/multi_image_picker.dart';
import 'traf_request_model.dart'; import 'traf_request_model.dart';
import 'traf_request_provider.dart'; import 'traf_request_provider.dart';
@ -52,27 +58,47 @@ class _CreateTRAFRequestPageState extends State<CreateTRAFRequestPage> {
Lookup? otherServicesEffects; Lookup? otherServicesEffects;
Lookup? useInCombination; Lookup? useInCombination;
TrafCustomer? selectedSite;
TrafDepartment? selectedDepartment;
TrafRequestDataModel? trafRequest; TrafRequestDataModel? trafRequest;
List<Lookup> abc = []; List<Lookup> abc = [];
List<TrafDepartment> departments = []; List<TrafDepartment> departments = [];
List<UsersBasedOnSearchModel> userBasedOnSearch = []; List<UsersBasedOnSearchModel> userBasedOnSearch = [];
List<GenericAttachmentModel> attachments = [];
FocusNode otherServicesEffectsNode = FocusNode(); FocusNode otherServicesEffectsNode = FocusNode();
FocusNode useInCombinationNode = FocusNode(); FocusNode useInCombinationNode = FocusNode();
FocusNode departmentNode = FocusNode(); FocusNode departmentNode = FocusNode();
FocusNode siteDepartmentNode = FocusNode();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
trafRequest = TrafRequestDataModel(id: 0, attachments: [], departments: [], qty: 1); trafRequest = TrafRequestDataModel(id: 0, attachments: [], departments: [], qty: 1);
resetProviders(); WidgetsBinding.instance.addPostFrameCallback((_) {
final requestTypeProvider = Provider.of<RequestTypeLookupProvider>(context, listen: false);
final yesNoProvider = Provider.of<YesNoLookupProvider>(context, listen: false);
final customerProvider = Provider.of<CustomerLookupProvider>(context, listen: false);
if (requestTypeProvider.items.isEmpty) {
requestTypeProvider.reset();
}
if (yesNoProvider.items.isEmpty) {
yesNoProvider.reset();
}
final userId = context.userProvider.user?.userID;
if (customerProvider.items.isEmpty && userId != null && userId.isNotEmpty) {
customerProvider.getDataByUserId(userId);
}
log('user id is ${userId}');
});
} }
void resetProviders() { void resetProviders() {
Provider.of<RequestTypeLookupProvider>(context, listen: false).reset(); Provider.of<RequestTypeLookupProvider>(context, listen: false).reset();
// Provider.of<YesNoLookupProvider>(context, listen: false).reset(); Provider.of<YesNoLookupProvider>(context, listen: false).reset();
} }
@override @override
@ -83,7 +109,7 @@ class _CreateTRAFRequestPageState extends State<CreateTRAFRequestPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: const DefaultAppBar(title: "TRAF Request"), appBar: const DefaultAppBar(title: "Assessment of Need & Technology"),
body: Form( body: Form(
key: _formKey, key: _formKey,
child: Column( child: Column(
@ -181,6 +207,48 @@ class _CreateTRAFRequestPageState extends State<CreateTRAFRequestPage> {
style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, fontWeight: FontWeight.w600), style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, fontWeight: FontWeight.w600),
), ),
12.height, 12.height,
SingleItemDropDownMenu<TrafCustomer, CustomerLookupProvider>(
context: context,
showAsBottomSheet: true,
backgroundColor: AppColor.neutral100,
showShadow: false,
title: "Site",
requestById: int.tryParse(context.userProvider.user?.userID ?? ''),
initialValue: selectedSite,
onSelect: (value) {
selectedSite = value;
trafRequest?.siteId = value?.customerId;
// Reset department when site changes
selectedDepartment = null;
trafRequest?.departmentId = null;
// Reset and fetch department data based on selected site
Provider.of<DepartmentLookupProvider>(context, listen: false).reset();
if (selectedSite != null && selectedSite!.customerId != null) {
// Fetch departments for the selected site
Provider.of<DepartmentLookupProvider>(context, listen: false).getData(id: selectedSite!.customerId);
siteDepartmentNode.requestFocus();
}
setState(() {});
},
),
12.height,
SingleItemDropDownMenu<TrafDepartment, DepartmentLookupProvider>(
context: context,
showAsBottomSheet: true,
backgroundColor: AppColor.neutral100,
showShadow: false,
title: "Department",
requestById: null,
// initialValue: selectedDepartment,
enabled: selectedSite != null,
onSelect: (value) {
selectedDepartment = value;
trafRequest?.departmentId = value?.id?.toString();
setState(() {});
},
),
12.height,
AppTextFormField( AppTextFormField(
initialValue: "", initialValue: "",
labelText: "How would the requested technology solve the current situation and/or serve the purpose?", labelText: "How would the requested technology solve the current situation and/or serve the purpose?",
@ -444,7 +512,18 @@ class _CreateTRAFRequestPageState extends State<CreateTRAFRequestPage> {
trafRequest?.usedWithCombination = value; trafRequest?.usedWithCombination = value;
}, },
), ),
] ],
12.height,
AttachmentPicker(
label: context.translation.attachFiles,
attachment: attachments,
buttonColor: AppColor.black10,
onlyImages: false,
buttonIcon: 'image-plus'.toSvgAsset(color: AppColor.neutral120),
onChange: (attachment) {
attachments = attachment;
},
),
], ],
).toShadowContainer(context, padding: 12, borderRadius: 20), ).toShadowContainer(context, padding: 12, borderRadius: 20),
16.height, 16.height,
@ -492,10 +571,25 @@ class _CreateTRAFRequestPageState extends State<CreateTRAFRequestPage> {
"Pick asset".showToast; "Pick asset".showToast;
return; return;
} }
// Validate site and department selection
if (selectedSite == null) {
"Please select a site".showToast;
return;
}
if (selectedDepartment == null) {
"Please select a department".showToast;
return;
}
if (!_formKey.currentState!.validate()) return; if (!_formKey.currentState!.validate()) return;
trafRequest?.employeeId = context.userProvider.user?.userID; trafRequest?.employeeId = context.userProvider.user?.userID;
trafRequest?.siteId = context.userProvider.user?.clientId; // Set site and department IDs from selected dropdowns
trafRequest?.siteId = selectedSite?.customerId;
trafRequest?.departmentId = selectedDepartment?.id;
trafRequest?.trafContacts = []; trafRequest?.trafContacts = [];
trafRequest?.trafContacts = userBasedOnSearch.map((item) => TrafContacts(id: 0, trafId: 0, name: item.userName, userId: item.userId)).toList(); trafRequest?.trafContacts = userBasedOnSearch.map((item) => TrafContacts(id: 0, trafId: 0, name: item.userName, userId: item.userId)).toList();
@ -522,8 +616,17 @@ class _CreateTRAFRequestPageState extends State<CreateTRAFRequestPage> {
return; return;
} }
// Process attachments similar to CreateCMRequest
trafRequest?.attachments = [];
for (var item in attachments) {
String fileName = CMRequestUtils.isLocalUrl(item.name ?? '') ? ("${item.name ?? ''.split("/").last}|${base64Encode(File(item.name ?? '').readAsBytesSync())}") : item.name ?? '';
trafRequest?.attachments!.add(
Attachments(id: 0, trafId: 0, attachmentName: fileName),
);
}
Utils.showLoading(context); Utils.showLoading(context);
bool isSuccess = await Provider.of<TrafRequestProvider>(context, listen: false).addTraf(trafRequest!.toJson()); bool isSuccess = await Provider.of<TrafRequestProvider>(context, listen: false).addTraf(trafRequest!.toJsonNew());
Utils.hideLoading(context); Utils.hideLoading(context);
if (isSuccess) { if (isSuccess) {
Navigator.pop(context); Navigator.pop(context);

@ -1,18 +1,24 @@
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:test_sa/controllers/providers/api/user_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/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart'; import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/enums/user_types.dart'; import 'package:test_sa/models/enums/user_types.dart';
import 'package:test_sa/modules/traf_module/traf_request_provider.dart'; import 'package:test_sa/modules/traf_module/traf_request_provider.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/views/widgets/images/files_list.dart';
import 'package:test_sa/views/widgets/loaders/app_loading.dart'; import 'package:test_sa/views/widgets/loaders/app_loading.dart';
import 'package:test_sa/views/widgets/loaders/no_data_found.dart'; import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
import 'package:test_sa/views/widgets/item_views/info_header_widget.dart'; import 'package:test_sa/views/widgets/item_views/info_header_widget.dart';
import 'package:test_sa/views/widgets/item_views/info_text_widget.dart'; import 'package:test_sa/views/widgets/item_views/info_text_widget.dart';
import '../../controllers/api_routes/urls.dart';
import 'asset_analytics_model.dart'; import 'asset_analytics_model.dart';
import 'traf_request_model.dart'; import 'traf_request_model.dart';
@ -30,10 +36,14 @@ class TrafRequestDetailPage extends StatefulWidget {
} }
class _TrafRequestDetailPageState extends State<TrafRequestDetailPage> { class _TrafRequestDetailPageState extends State<TrafRequestDetailPage> {
List<File> _attachments = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
Provider.of<TrafRequestProvider>(context, listen: false).getTRAFById(widget.trafId); WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<TrafRequestProvider>(context, listen: false).getTRAFById(widget.trafId);
});
} }
@override @override
@ -45,12 +55,15 @@ class _TrafRequestDetailPageState extends State<TrafRequestDetailPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool isEngineer = (Provider.of<UserProvider>(context, listen: false).user?.type) == UsersTypes.engineer; bool isEngineer = (Provider.of<UserProvider>(context, listen: false).user?.type) == UsersTypes.engineer;
return Scaffold( return Scaffold(
appBar: const DefaultAppBar(title: "TRAF Request"), appBar: const DefaultAppBar(title: "Assessment of Need & Technology"),
body: Selector<TrafRequestProvider, bool>( body: Selector<TrafRequestProvider, bool>(
selector: (_, myModel) => myModel.isLoading, // Selects only the userName selector: (_, myModel) => myModel.isLoading, // Selects only the userName
builder: (_, isLoading, __) { builder: (_, isLoading, __) {
if (isLoading) return const ALoading(); if (isLoading) return const ALoading();
TrafRequestProvider trafProvider = Provider.of<TrafRequestProvider>(context, listen: false); TrafRequestProvider trafProvider = Provider.of<TrafRequestProvider>(context, listen: false);
if (trafProvider.trafRequestDataModel?.attachments != null) {
_attachments = trafProvider.trafRequestDataModel!.attachments!.map((e) => File(URLs.getFileUrl(e.attachmentName ?? '') ?? '')).toList();
}
return trafProvider.trafRequestDataModel == null return trafProvider.trafRequestDataModel == null
? const NoDataFound().center ? const NoDataFound().center
: Column( : Column(
@ -105,8 +118,12 @@ class _TrafRequestDetailPageState extends State<TrafRequestDetailPage> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
InfoHeader16Widget("Request Information"), InfoHeader16Widget("Request Information"),
InfoTextWidget(label: 'TRAF No', value: data.reqCode ?? '-'), InfoTextWidget(label: 'Request No', value: data.reqCode ?? '-'),
InfoTextWidget(label: 'Request type', value: data.requestTypeName ?? '-'), InfoTextWidget(label: 'Request type', value: data.requestTypeName ?? '-'),
if (data.requestTypeValue == 1) ...[
4.height,
'Requested Quantity: ${data.qty ?? '-'}'.bodyText(context), //todo @waseem replace with infotext
],
InfoTextWidget(label: 'How would the requested technology solve the current situation and/or serve the purpose?', value: data.purposeAnswer ?? '-'), InfoTextWidget(label: 'How would the requested technology solve the current situation and/or serve the purpose?', value: data.purposeAnswer ?? '-'),
InfoTextWidget(label: 'What is the current practice?', value: data.currentPractise ?? '-'), InfoTextWidget(label: 'What is the current practice?', value: data.currentPractise ?? '-'),
InfoTextWidget(label: 'Census Q1', value: data.censusQ1 ?? '-'), InfoTextWidget(label: 'Census Q1', value: data.censusQ1 ?? '-'),
@ -115,8 +132,8 @@ class _TrafRequestDetailPageState extends State<TrafRequestDetailPage> {
InfoTextWidget(label: 'Census Q4', value: data.censusQ4 ?? '-'), InfoTextWidget(label: 'Census Q4', value: data.censusQ4 ?? '-'),
InfoTextWidget( InfoTextWidget(
label: 'List down names & contact information of users going to use the technology & specify if they are part-time or full time?', label: 'List down names & contact information of users going to use the technology & specify if they are part-time or full time?',
value: data.trafContacts?.map((item) => item.name).toList().toString() ?? '-'), value: "\n${data.trafContacts?.map((item) => item.name).toList().toString() ?? '-'}"),
InfoTextWidget(label: 'Is the requesting department going to use the technology solely or shared with other departments?', value: data.usingSolelyOrSharedName ?? '-'), InfoTextWidget(label: 'Is the requesting department going to use the technology solely or shared with other departments?', value: "\n${data.usingSolelyOrSharedName ?? '-'}"),
if ((data.departments ?? []).isNotEmpty) ...[ if ((data.departments ?? []).isNotEmpty) ...[
InfoTextWidget(label: 'Technology used with other departments', value: data.departments?.map((item) => item.departmentName).toList().toString() ?? '-'), InfoTextWidget(label: 'Technology used with other departments', value: data.departments?.map((item) => item.departmentName).toList().toString() ?? '-'),
], ],
@ -128,6 +145,16 @@ class _TrafRequestDetailPageState extends State<TrafRequestDetailPage> {
if ((data.usedWithCombination ?? "").isNotEmpty) ...[ if ((data.usedWithCombination ?? "").isNotEmpty) ...[
InfoTextWidget(label: 'Equipment going to be used with combination of other equipments', value: data.usedWithCombination ?? '-'), InfoTextWidget(label: 'Equipment going to be used with combination of other equipments', value: data.usedWithCombination ?? '-'),
], ],
if (_attachments.isNotEmpty) ...[
4.height,
Text(
// todo @waseem conver to header
"Attachments".addTranslation,
style: AppTextStyles.heading6.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50),
),
// 8.height,
FilesList(images: _attachments.map((e) => URLs.getFileUrl(e.path ?? '') ?? '').toList() ?? []),
],
], ],
).toShadowContainer(context); ).toShadowContainer(context);
} }

@ -43,6 +43,22 @@ class TrafRequestItemView extends StatelessWidget {
InfoDateWidget(requestData!.transactionDate ?? ""), InfoDateWidget(requestData!.transactionDate ?? ""),
], ],
), ),
8.height,
(requestData?.typeTransaction ?? "Assessment of Need & Technology").heading5(context), // todo replace to info header
infoWidget(label: context.translation.requestType, value: requestData?.requestTypeName, context: context),
infoWidget(label: "Request No", value: requestData?.requestNo, context: context),
8.height,
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.translation.viewDetails,
style: AppTextStyles.bodyText.copyWith(color: AppColor.blueStatus(context)),
),
4.width,
Icon(Icons.arrow_forward, color: AppColor.blueStatus(context), size: 14)
],
),
InfoHeaderWidget(requestData?.typeTransaction ?? "TRAF Request"), InfoHeaderWidget(requestData?.typeTransaction ?? "TRAF Request"),
InfoTextWidget(label: context.translation.requestType, value: requestData?.requestTypeName), InfoTextWidget(label: context.translation.requestType, value: requestData?.requestTypeName),
InfoTextWidget(label: "TRAF No", value: requestData?.requestNo, showArrow: true), InfoTextWidget(label: "TRAF No", value: requestData?.requestNo, showArrow: true),
@ -73,6 +89,24 @@ class TrafRequestItemView extends StatelessWidget {
InfoDateWidget(requestDetails!.date ?? ""), InfoDateWidget(requestDetails!.date ?? ""),
], ],
), ),
8.height,
(requestDetails?.nameOfType ?? "Assessment of Need & Technology").heading5(context),
8.height,
infoWidget(label: context.translation.requestType, value: requestDetails!.requestType, context: context),
infoWidget(label: "Request No", value: requestDetails!.requestNo, context: context),
infoWidget(label: context.translation.site, value: requestDetails!.site, context: context),
8.height,
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.translation.viewDetails,
style: AppTextStyles.bodyText.copyWith(color: AppColor.blueStatus(context)),
),
4.width,
Icon(Icons.arrow_forward, color: AppColor.blueStatus(context), size: 14)
],
),
InfoHeaderWidget(requestDetails?.nameOfType ?? "TRAF Request"), InfoHeaderWidget(requestDetails?.nameOfType ?? "TRAF Request"),
InfoTextWidget(label: context.translation.requestType, value: requestDetails!.requestType), InfoTextWidget(label: context.translation.requestType, value: requestDetails!.requestType),
InfoTextWidget(label: "TRAF No", value: requestDetails!.requestNo), InfoTextWidget(label: "TRAF No", value: requestDetails!.requestNo),

@ -51,7 +51,7 @@ class TrafRequestDataModel {
String? employeeEmail; String? employeeEmail;
int? positionId; int? positionId;
String? positionName; String? positionName;
String? departmentId; dynamic departmentId;
String? departmentName; String? departmentName;
List<Departments>? departments; List<Departments>? departments;
List<Departments>? trafDepartments; List<Departments>? trafDepartments;
@ -88,10 +88,12 @@ class TrafRequestDataModel {
String? isEffectedName; String? isEffectedName;
String? effectedServices; String? effectedServices;
int? isCombinationId; int? isCombinationId;
int?requestTypeValue;
String? isCombinationName; String? isCombinationName;
String? usedWithCombination; String? usedWithCombination;
String? comment; String? comment;
List<Attachments>? attachments; List<Attachments>? attachments;
bool? isFromMobile;
int? isBudgetId; int? isBudgetId;
String? isBudgetName; String? isBudgetName;
String? firstLineManagerId; String? firstLineManagerId;
@ -186,9 +188,11 @@ class TrafRequestDataModel {
this.isEffectedName, this.isEffectedName,
this.effectedServices, this.effectedServices,
this.isCombinationId, this.isCombinationId,
this.requestTypeValue,
this.isCombinationName, this.isCombinationName,
this.usedWithCombination, this.usedWithCombination,
this.comment, this.comment,
this.isFromMobile = true,
this.attachments, this.attachments,
this.isBudgetId, this.isBudgetId,
this.isBudgetName, this.isBudgetName,
@ -299,9 +303,11 @@ class TrafRequestDataModel {
isEffectedName = json['isEffectedName']; isEffectedName = json['isEffectedName'];
effectedServices = json['effectedServices']; effectedServices = json['effectedServices'];
isCombinationId = json['isCombinationId']; isCombinationId = json['isCombinationId'];
requestTypeValue = json['requestTypeValue'];
isCombinationName = json['isCombinationName']; isCombinationName = json['isCombinationName'];
usedWithCombination = json['usedWithCombination']; usedWithCombination = json['usedWithCombination'];
comment = json['comment']; comment = json['comment'];
isFromMobile = json['isFromMobile'];
if (json['attachments'] != null) { if (json['attachments'] != null) {
attachments = <Attachments>[]; attachments = <Attachments>[];
json['attachments'].forEach((v) { json['attachments'].forEach((v) {
@ -415,9 +421,11 @@ class TrafRequestDataModel {
data['isEffectedName'] = this.isEffectedName; data['isEffectedName'] = this.isEffectedName;
data['effectedServices'] = this.effectedServices; data['effectedServices'] = this.effectedServices;
data['isCombinationId'] = this.isCombinationId; data['isCombinationId'] = this.isCombinationId;
data['requestTypeValue'] = this.requestTypeValue;
data['isCombinationName'] = this.isCombinationName; data['isCombinationName'] = this.isCombinationName;
data['usedWithCombination'] = this.usedWithCombination; data['usedWithCombination'] = this.usedWithCombination;
data['comment'] = this.comment; data['comment'] = this.comment;
data['isFromMobile'] = this.isFromMobile;
if (this.attachments != null) { if (this.attachments != null) {
data['attachments'] = this.attachments!.map((v) => v.toJson()).toList(); data['attachments'] = this.attachments!.map((v) => v.toJson()).toList();
} else { } else {
@ -474,6 +482,68 @@ class TrafRequestDataModel {
data['modifiedDate'] = this.modifiedDate; data['modifiedDate'] = this.modifiedDate;
return data; return data;
} }
Map<String, dynamic> toJsonNew() {
final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id;
data['reqNo'] = reqNo;
data['reqCode'] = reqCode;
data['employeeId'] = employeeId;
data['positionId'] = positionId;
data['departments'] =
departments?.map((v) => v.toJson()).toList() ?? [];
if (this.trafDepartments != null) {
data['trafDepartments'] = this.trafDepartments!.map((v) => v.toJson()).toList();
}
data['siteId'] = siteId;
data['departmentId'] = departmentId;
data['siteName'] = siteName;
data['requesterExtensionNumber'] = requesterExtensionNumber;
data['requesterExtensionName'] = requesterExtensionName;
data['requesterExtensionPositionId'] = requesterExtensionPositionId;
data['requestTypeId'] = requestTypeId;
data['assetNDId'] = assetNDId;
data['assetId'] = assetId;
data['qty'] = qty;
data['purposeAnswer'] = purposeAnswer;
data['currentPractise'] = currentPractise;
data['censusQ1'] = censusQ1;
data['censusQ2'] = censusQ2;
data['censusQ3'] = censusQ3;
data['censusQ4'] = censusQ4;
data['isFromMobile'] = isFromMobile;
data['trafContacts'] =
trafContacts?.map((v) => v.toJson()).toList() ?? [];
data['usingSolelyOrSharedId'] = usingSolelyOrSharedId;
data['isEffectedId'] = isEffectedId;
data['effectedServices'] = effectedServices;
data['isCombinationId'] = isCombinationId;
data['usedWithCombination'] = usedWithCombination;
data['comment'] = comment;
data['isBudgetId'] = isBudgetId;
data['attachments'] =
attachments?.map((v) => v.toJson()).toList() ?? [];
return data;
}
} }
class TrafContacts { class TrafContacts {

@ -19,12 +19,13 @@ class AppBottomNavigationBar extends StatelessWidget {
bool isEngineer = (Provider.of<UserProvider>(context, listen: false).user?.type) == UsersTypes.engineer; bool isEngineer = (Provider.of<UserProvider>(context, listen: false).user?.type) == UsersTypes.engineer;
bool isUserFMS = Provider.of<SettingProvider>(context, listen: false).isUserFMS; bool isUserFMS = Provider.of<SettingProvider>(context, listen: false).isUserFMS;
bool commissioningEng = context.userProvider.isCommissioningEngineer; bool commissioningEng = context.userProvider.isCommissioningEngineer;
bool isHouseKeeper = context.userProvider.isHouseKeeper;
List<NavItemModel> navItems = [ List<NavItemModel> navItems = [
NavItemModel(0, "overview", context.translation.overview), NavItemModel(0, "overview", context.translation.overview),
NavItemModel(1, "request_icon", context.translation.workOrder), if(!isHouseKeeper) NavItemModel(1, "request_icon", context.translation.workOrder),
if (!commissioningEng) NavItemModel(2, "add_icon", context.translation.calendar, showLabel: false), if (!commissioningEng && !isHouseKeeper) NavItemModel(2, "add_icon", context.translation.calendar, showLabel: false),
NavItemModel(3, "assets", context.translation.assets), if(!isHouseKeeper) NavItemModel(3, "assets", context.translation.assets),
NavItemModel(4, "contact", context.translation.contact), NavItemModel(4, "contact", context.translation.contact),
]; ];

@ -172,8 +172,8 @@ class CreateRequestModel {
if (context.userProvider.isQualityUser && context.settingProvider.isUserFlowMedical) { if (context.userProvider.isQualityUser && context.settingProvider.isUserFlowMedical) {
list.add(CreateRequestModel("Equipment Internal Audit Checklist".addTranslation, "wo/equipment_audit_wo", CreateEquipmentInternalAuditForm.id)); list.add(CreateRequestModel("Equipment Internal Audit Checklist".addTranslation, "wo/equipment_audit_wo", CreateEquipmentInternalAuditForm.id));
list.add(CreateRequestModel("System Internal Audit Checklist".addTranslation, "wo/system_audit_wo", CreateSystemInternalAuditForm.id)); list.add(CreateRequestModel("System Internal Audit Checklist".addTranslation, "wo/system_audit_wo", CreateSystemInternalAuditForm.id));
} else if (context.userProvider.isAssessor && context.settingProvider.isUserFlowMedical) { } else if (context.userProvider.isAssessor && context.settingProvider.isUserFlowMedical) { // todo: handled in permission api, so no need to check here
list.add(CreateRequestModel("TRAF".addTranslation, "wo/traf_wo", CreateTRAFRequestPage.id)); // list.add(CreateRequestModel("TRAF".addTranslation, "wo/traf_wo", CreateTRAFRequestPage.id));
} else if (context.userProvider.isEngineer) { } else if (context.userProvider.isEngineer) {
if (Provider.of<SettingProvider>(context, listen: false).engineerCanCreateCM) { if (Provider.of<SettingProvider>(context, listen: false).engineerCanCreateCM) {
list.add(CreateRequestModel(context.translation.correctiveMaintenance, "wo/cm_wo", CreateCMRequest.id)); list.add(CreateRequestModel(context.translation.correctiveMaintenance, "wo/cm_wo", CreateCMRequest.id));
@ -201,10 +201,15 @@ class CreateRequestModel {
// list.add(CreateRequestModel('Demo Request'.addTranslation, "add_icon", CreateDemoRequestPage.id)); // list.add(CreateRequestModel('Demo Request'.addTranslation, "add_icon", CreateDemoRequestPage.id));
} }
} }
// if (context.settingProvider.isUserFlowMedical && (context.userProvider.isAssessor || context.userProvider.isEngineer || context.userProvider.isNurse)) {
// //Traf name changed to Assessment of Need & Technology by client .
// list.add(CreateRequestModel("Assessment of Need & Technology".addTranslation, "add_icon", CreateTRAFRequestPage.id));
// }
context.userProvider.modulesPermissionsList.forEach((module) { context.userProvider.modulesPermissionsList.forEach((module) {
///TODO uncommit this to enable TRAF in production.
if (module.module!.value! == 9 && (module.canAdd ?? false)) { if (module.module!.value! == 9 && (module.canAdd ?? false)) {
list.add(CreateRequestModel("TRAF".addTranslation, "wo/traf_wo", CreateTRAFRequestPage.id)); list.add(CreateRequestModel(module.module?.name ?? "Assessment of Need & Technology".addTranslation, "wo/traf_wo", CreateTRAFRequestPage.id));
} }
if (module.module!.value! == 15 && (module.canAdd ?? false)) { if (module.module!.value! == 15 && (module.canAdd ?? false)) {
list.add(CreateRequestModel('Demo Request'.addTranslation, "wo/demo_wo", CreateDemoRequestPage.id)); list.add(CreateRequestModel('Demo Request'.addTranslation, "wo/demo_wo", CreateDemoRequestPage.id));

@ -136,15 +136,20 @@ class _AllRequestsFilterPageState extends State<AllRequestsFilterPage> {
///TODO need add accessor filter as well. ///TODO need add accessor filter as well.
if (context.settingProvider.isUserFlowMedical && context.userProvider.isAssessor) { if (context.settingProvider.isUserFlowMedical && context.userProvider.isAssessor) {
types = { types = {
'TRAF': 9, // 'TRAF': 9,
//Traf name changed as per client requirements
'Assessment of Need & Technology': 9,
// 'Demo': 15, moved to permission api // 'Demo': 15, moved to permission api
}; };
} }
if (context.settingProvider.isUserFlowMedical && !context.userProvider.isNurse) { if (context.settingProvider.isUserFlowMedical && !context.userProvider.isNurse) {
types['Incidents'] = 16; types['Incidents'] = 16;
} }
//No need to add check for assetgroup type as this role will only for FMS
final statuses = { if (context.userProvider.isHouseKeeper) {
types = {context.translation.recurrentWo: 5};
}
var statuses = {
"All WO": 0, "All WO": 0,
context.translation.open: 1, context.translation.open: 1,
context.translation.inProgress: 2, context.translation.inProgress: 2,
@ -152,6 +157,16 @@ class _AllRequestsFilterPageState extends State<AllRequestsFilterPage> {
context.translation.closed: 3, context.translation.closed: 3,
"Canceled": 5, "Canceled": 5,
}; };
if (context.userProvider.isHouseKeeper) {
statuses = {
"All WO": 0,
// context.translation.open: 1,
context.translation.inProgress: 2,
// context.translation.completed: 4,
context.translation.closed: 3,
// "Canceled": 5,
};
}
return Scaffold( return Scaffold(
appBar: DefaultAppBar( appBar: DefaultAppBar(
title: context.translation.filter, title: context.translation.filter,

@ -100,13 +100,14 @@ class _MyRequestsPageState extends State<MyRequestsPage> with SingleTickerProvid
// switching asset inventory to users permissions // switching asset inventory to users permissions
context.userProvider.modulesPermissionsList.forEach((module) { context.userProvider.modulesPermissionsList.forEach((module) {
///todo uncommit this in prod currently apis is not giving the correct name for traf
if (module.module!.value! == 8 || module.module!.value! == 9 || module.module!.value! == 15) { if (module.module!.value! == 8 || module.module!.value! == 9 || module.module!.value! == 15) {
requestsList.add(Request(module.module!.value!, module.module!.name!.addTranslation)); requestsList.add(Request(module.module!.value!, module.module!.name!.addTranslation));
} }
}); });
//Hide TRAF. //Hide TRAF.
// if (context.settingProvider.isUserFlowMedical && context.userProvider.user!.type == UsersTypes.normal_user) { // if (context.settingProvider.isUserFlowMedical && context.userProvider.user!.type == UsersTypes.normal_user) {
// requestsList.add(Request(9, 'TRAF')); // requestsList.add(Request(9, 'Assessment of Need & Technology'));
// } // }
if (context.userProvider.isEngineer && context.settingProvider.isUserFlowMedical) { if (context.userProvider.isEngineer && context.settingProvider.isUserFlowMedical) {
requestsList.add(Request(10, 'Equipment Internal Audit')); requestsList.add(Request(10, 'Equipment Internal Audit'));
@ -124,7 +125,9 @@ class _MyRequestsPageState extends State<MyRequestsPage> with SingleTickerProvid
if (context.userProvider.isAssessor) { if (context.userProvider.isAssessor) {
requestsList = [ requestsList = [
Request(null, context.translation.allWorkOrder), Request(null, context.translation.allWorkOrder),
Request(9, 'TRAF'), // Request(9, 'TRAF'),
//name changed as requested by client
Request(9, 'Assessment of Need & Technology'),
// Request(15, 'Demo'), moved to permission api // Request(15, 'Demo'), moved to permission api
]; ];
} }
@ -148,6 +151,14 @@ class _MyRequestsPageState extends State<MyRequestsPage> with SingleTickerProvid
requestsList.add(Request(16, "Incidents")); requestsList.add(Request(16, "Incidents"));
} }
if (context.userProvider.isHouseKeeper) {
requestsList = [
Request(null, context.translation.allWorkOrder),
Request(5, context.translation.recurrentWo),
// Request(15, 'Demo'), moved to permission api
];
}
_provider = Provider.of<AllRequestsProvider>(context, listen: false); _provider = Provider.of<AllRequestsProvider>(context, listen: false);
_provider!.reset(); _provider!.reset();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {

@ -26,8 +26,6 @@ class RecurrentWoItemView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final user = Provider.of<UserProvider>(context, listen: false).user; final user = Provider.of<UserProvider>(context, listen: false).user;
final settingProvider = Provider.of<SettingProvider>(context, listen: false);
final bool isFMSEngineer = user?.type == UsersTypes.engineer && settingProvider.isUserFMS;
if (requestData != null) { if (requestData != null) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -50,23 +48,35 @@ class RecurrentWoItemView extends StatelessWidget {
InfoDateWidget(requestData!.transactionDate ?? ""), InfoDateWidget(requestData!.transactionDate ?? ""),
], ],
), ),
8.height,
(requestData?.typeTransaction ?? context.translation.recurrentWo).heading5(context),
8.height,
// '${context.translation.assetNumber}: ${requestData!.assetNumber}'.bodyText(context),
// '${context.translation.assetSN}: ${request.assetSN}'.bodyText(context),//todo ask ahmed
// '${context.translation.code}: ${request.code}'.bodyText(context),
'${context.translation.taskNo}: ${requestData!.requestNo}'.bodyText(context),
if (requestData?.siteName != null) '${context.translation.site}: ${requestData!.siteName}'.bodyText(context),
if (requestData!.buildingName != null) '${context.translation.building}: ${requestData!.buildingName}'.bodyText(context),
if (requestData!.departmentName != null) '${context.translation.department}: ${requestData!.departmentName}'.bodyText(context),
if (requestData!.floorName != null) '${context.translation.floor}: ${requestData!.floorName}'.bodyText(context),
if (requestData!.roomName != null) '${context.translation.room}: ${requestData!.roomName}'.bodyText(context),
8.height,
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.translation.viewDetails,
style: AppTextStyles.bodyText.copyWith(color: AppColor.blueStatus(context)),
),
4.width,
Icon(Icons.arrow_forward, color: AppColor.blueStatus(context), size: 14)
],
),
InfoHeaderWidget(requestData?.typeTransaction ?? context.translation.ppmRequest), InfoHeaderWidget(requestData?.typeTransaction ?? context.translation.ppmRequest),
InfoTextWidget(label: context.translation.taskNo, value: requestData!.requestNo, showArrow: true), InfoTextWidget(label: context.translation.taskNo, value: requestData!.requestNo, showArrow: true),
], ],
).toShadowContainer(context, withShadow: showShadow).onPress(() { ).toShadowContainer(context, withShadow: showShadow).onPress(() {
if (isFMSEngineer && (requestData?.statusValue == 4)) { Navigator.of(context).push(MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: requestData!.id)));
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CleanerSearchDetailsView(
requestId: requestData!.id ?? 0,
taskResult: null,
),
),
);
} else {
Navigator.of(context).push(MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: requestData!.id)));
}
}); });
} }
if (requestDetails != null) { if (requestDetails != null) {
@ -92,22 +102,35 @@ class RecurrentWoItemView extends StatelessWidget {
InfoDateWidget(requestDetails!.date ?? ""), InfoDateWidget(requestDetails!.date ?? ""),
], ],
), ),
8.height,
(requestDetails?.nameOfType ?? context.translation.recurrentWo).heading5(context),
8.height,
// '${context.translation.taskNo}: ${requestDetails!.id}'.bodyText(context),
'${context.translation.site}: ${requestDetails!.site}'.bodyText(context),
// '${context.translation.code}: ${request.code}'.bodyText(context),
'${context.translation.taskNo}: ${requestDetails!.requestNo}'.bodyText(context),
if (requestDetails!.siteName != null) '${context.translation.site}: ${requestDetails!.siteName}'.bodyText(context),
if (requestDetails!.buildingName != null) '${context.translation.building}: ${requestDetails!.buildingName}'.bodyText(context),
if (requestDetails!.departmentName != null) '${context.translation.department}: ${requestDetails!.departmentName}'.bodyText(context),
if (requestDetails!.floorName != null) '${context.translation.floor}: ${requestDetails!.floorName}'.bodyText(context),
if (requestDetails!.roomName != null) '${context.translation.room}: ${requestDetails!.roomName}'.bodyText(context),
8.height,
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.translation.viewDetails,
style: AppTextStyles.bodyText.copyWith(color: AppColor.blueStatus(context)),
),
4.width,
Icon(Icons.arrow_forward, color: AppColor.blueStatus(context), size: 14)
],
),
InfoHeaderWidget(requestDetails?.nameOfType ?? context.translation.recurrentWo), InfoHeaderWidget(requestDetails?.nameOfType ?? context.translation.recurrentWo),
InfoTextWidget(label: context.translation.site, value: requestDetails!.site), InfoTextWidget(label: context.translation.site, value: requestDetails!.site),
InfoTextWidget(label: context.translation.taskNo, value: requestDetails!.requestNo, showArrow: true), InfoTextWidget(label: context.translation.taskNo, value: requestDetails!.requestNo, showArrow: true),
], ],
).toShadowContainer(context, withShadow: showShadow).onPress(() async { ).toShadowContainer(context, withShadow: showShadow).onPress(() async {
if (isFMSEngineer && (requestDetails?.statusValue == 4)) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CleanerSearchDetailsView(
requestId: requestDetails!.id ?? 0,
taskResult: null,
),
),
);
}
Navigator.of(context).push(MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: requestDetails!.id))); Navigator.of(context).push(MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: requestDetails!.id)));
}); });
} }

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/models/new_models/dashboard_detail.dart'; import 'package:test_sa/models/new_models/dashboard_detail.dart';
import 'package:test_sa/modules/asset_delivery_module/pages/asset_delivery_item_view.dart'; import 'package:test_sa/modules/asset_delivery_module/pages/asset_delivery_item_view.dart';
import 'package:test_sa/modules/cleaner_module/widgets/one_time_task_item_view.dart';
import 'package:test_sa/modules/demo_module/demo_request_item_view.dart'; import 'package:test_sa/modules/demo_module/demo_request_item_view.dart';
import 'package:test_sa/modules/incident_module/incident_item_view.dart'; import 'package:test_sa/modules/incident_module/incident_item_view.dart';
import 'package:test_sa/modules/internal_audit_module/pages/equipment_internal_audit/equipment_internal_audit_item_view.dart'; import 'package:test_sa/modules/internal_audit_module/pages/equipment_internal_audit/equipment_internal_audit_item_view.dart';
@ -87,6 +88,8 @@ class RequestPaginatedListview extends StatelessWidget {
return DemoRequestItemView(requestData: request); return DemoRequestItemView(requestData: request);
case 16: case 16:
return IncidentItemView(requestData: request); return IncidentItemView(requestData: request);
case 17:
return OneTimeTaskItemView(requestData: request);
default: default:
return Container( return Container(
height: 100, height: 100,

@ -35,7 +35,7 @@ class ServiceRequestItemView extends StatelessWidget {
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (Provider.of<SettingProvider>(context, listen: false).showPriority) ...[ if (Provider.of<SettingProvider>(context, listen: false).showPriority && requestData?.priorityName != null && requestData!.priorityName!.isNotEmpty) ...[
StatusLabel( StatusLabel(
label: requestData!.priorityName!, label: requestData!.priorityName!,
textColor: AppColor.getRequestStatusTextColorByName(context, requestData!.priorityName!), textColor: AppColor.getRequestStatusTextColorByName(context, requestData!.priorityName!),
@ -76,7 +76,7 @@ class ServiceRequestItemView extends StatelessWidget {
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (Provider.of<SettingProvider>(context, listen: false).showPriority) ...[ if (Provider.of<SettingProvider>(context, listen: false).showPriority && requestDetails!.priority != null && requestDetails!.priority!.isNotEmpty) ...[
StatusLabel( StatusLabel(
label: requestDetails!.priority, label: requestDetails!.priority,
textColor: AppColor.getRequestStatusTextColorByName(context, requestDetails!.priority), textColor: AppColor.getRequestStatusTextColorByName(context, requestDetails!.priority),

@ -6,6 +6,7 @@ import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/all_requests_and_count_model.dart'; import 'package:test_sa/models/all_requests_and_count_model.dart';
import 'package:test_sa/modules/asset_delivery_module/pages/asset_delivery_item_view.dart'; import 'package:test_sa/modules/asset_delivery_module/pages/asset_delivery_item_view.dart';
import 'package:test_sa/modules/cleaner_module/widgets/one_time_task_item_view.dart';
import 'package:test_sa/modules/demo_module/demo_request_item_view.dart'; import 'package:test_sa/modules/demo_module/demo_request_item_view.dart';
import 'package:test_sa/modules/incident_module/incident_item_view.dart'; import 'package:test_sa/modules/incident_module/incident_item_view.dart';
import 'package:test_sa/modules/internal_audit_module/pages/equipment_internal_audit/equipment_internal_audit_item_view.dart'; import 'package:test_sa/modules/internal_audit_module/pages/equipment_internal_audit/equipment_internal_audit_item_view.dart';
@ -140,6 +141,9 @@ class _RequestItemViewListState extends State<RequestItemViewList> with SingleTi
case 16: case 16:
itemWidget = IncidentItemView(requestDetails: widget.list[index]); itemWidget = IncidentItemView(requestDetails: widget.list[index]);
break; break;
case 17:
itemWidget = OneTimeTaskItemView(requestDetails: widget.list[index]);
break;
default: default:
itemWidget = Container( itemWidget = Container(
height: 100, height: 100,

@ -0,0 +1,37 @@
import 'dart:convert';
import 'package:http/http.dart';
import 'package:test_sa/models/new_models/traf_customer.dart';
import 'package:test_sa/providers/loading_list_notifier.dart';
import '../../controllers/api_routes/api_manager.dart';
import '../../controllers/api_routes/urls.dart';
class CustomerLookupProvider extends LoadingListNotifier<TrafCustomer> {
@override
Future getData({int? id}) async {}
Future getDataByUserId(String? userId) async {
if (loading == true) return -2;
if (userId == null || userId.isEmpty) return -1;
loading = true;
notifyListeners();
try {
Response response = await ApiManager.instance.get(URLs.getCustomersByUserId + "?userId=$userId", useCache: true, enableToastMessage: false);
stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) {
List categoriesListJson = json.decode(response.body)["data"];
items = categoriesListJson.map((item) => TrafCustomer.fromJson(item)).toList();
}
loading = false;
notifyListeners();
return response.statusCode;
} catch (error) {
loading = false;
stateCode = -1;
notifyListeners();
return -1;
}
}
}

@ -44,6 +44,7 @@ class ConfirmDialog extends StatelessWidget {
) )
], ],
), ),
16.height,
message.bodyText(context), message.bodyText(context),
28.height, 28.height,
AppFilledButton( AppFilledButton(

@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.7.5+40 version: 1.7.7+42
environment: environment:
sdk: ">=3.5.0 <4.0.0" sdk: ">=3.5.0 <4.0.0"

Loading…
Cancel
Save