From 33f1f07e86043f40b049a83d7434329afb11803d Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Thu, 23 Apr 2026 15:53:07 +0300 Subject: [PATCH 01/17] housekeeping module completed --- assets/images/cleaner_flow_icon.svg | 6 + lib/controllers/api_routes/urls.dart | 17 +- .../widgets/request_category_fragment.dart | 19 +- .../widgets/requests_fragment.dart | 304 +++++++++++ lib/extensions/widget_extensions.dart | 1 + lib/main.dart | 10 +- lib/models/new_models/building.dart | 10 + lib/models/new_models/department.dart | 10 + .../new_models/department_room_combined.dart | 55 ++ lib/models/new_models/floor.dart | 10 + lib/models/new_models/room_model.dart | 10 + lib/models/ppm/recurrent_wo.dart | 36 +- .../models/cleaner_data_model.dart | 0 .../models/cleaner_form_model.dart | 24 + .../models/cleaner_task_search_result.dart | 73 +++ .../pages/cleaner_request_item_view.dart | 68 +++ .../pages/cleaner_search_details_view.dart | 318 +++++++++++ .../pages/cleaner_search_form_view.dart | 512 ++++++++++++++++++ .../pages/cleaner_search_list_view.dart | 186 +++++++ .../provider/cleaner_provider.dart | 169 ++++++ .../provider/fms_building_provider.dart | 83 +++ .../provider/fms_floor_provider.dart | 85 +++ .../fms_location_description_provider.dart | 140 +++++ .../widgets/cleaner_checklist_widget.dart | 334 ++++++++++++ .../recurrent_work_order_view.dart | 76 ++- lib/new_views/app_style/app_text_styles.dart | 1 + .../requests/recurrent_wo_item_view.dart | 35 +- pubspec.lock | 32 +- 28 files changed, 2585 insertions(+), 39 deletions(-) create mode 100644 assets/images/cleaner_flow_icon.svg create mode 100644 lib/models/new_models/department_room_combined.dart create mode 100644 lib/modules/cleaner_module/models/cleaner_data_model.dart create mode 100644 lib/modules/cleaner_module/models/cleaner_form_model.dart create mode 100644 lib/modules/cleaner_module/models/cleaner_task_search_result.dart create mode 100644 lib/modules/cleaner_module/pages/cleaner_request_item_view.dart create mode 100644 lib/modules/cleaner_module/pages/cleaner_search_details_view.dart create mode 100644 lib/modules/cleaner_module/pages/cleaner_search_form_view.dart create mode 100644 lib/modules/cleaner_module/pages/cleaner_search_list_view.dart create mode 100644 lib/modules/cleaner_module/provider/cleaner_provider.dart create mode 100644 lib/modules/cleaner_module/provider/fms_building_provider.dart create mode 100644 lib/modules/cleaner_module/provider/fms_floor_provider.dart create mode 100644 lib/modules/cleaner_module/provider/fms_location_description_provider.dart create mode 100644 lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart create mode 100644 lib/new_views/app_style/app_text_styles.dart diff --git a/assets/images/cleaner_flow_icon.svg b/assets/images/cleaner_flow_icon.svg new file mode 100644 index 00000000..e88c20ca --- /dev/null +++ b/assets/images/cleaner_flow_icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart index 8437ecf5..cfd02c27 100644 --- a/lib/controllers/api_routes/urls.dart +++ b/lib/controllers/api_routes/urls.dart @@ -3,13 +3,13 @@ class URLs { static const String appReleaseBuildNumber = "39"; - 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://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/v2/mobile"; // new V2 apis - static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM + static final String _baseUrl = "$_host/v1/mobile"; // new V2 apis + // 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/v5/mobile"; // v5 for data segregation // static final String _baseUrl = "$_host/v6/mobile"; // for asset delivery module @@ -59,12 +59,19 @@ class URLs { static get getSitesAutoComplete => "$_baseUrl/Customer/GetCustomersAutoComplete"; // get static get getSiteAutoCompleteWithoutConditionSites => "$_baseUrl/Customer/GetCustomersAutoCompleteWithoutConditionSites"; // get static get getDepartments => "$_baseUrl/Customer/GetDepartmentLookup"; // get + static get getCustomerBuildings => "$_baseUrl/Customer/GetBuildingLookup"; // post - FMS buildings by siteId + static get getCustomerFloors => "$_baseUrl/Customer/GetFloorLookup"; // post - FMS floors by siteId and buildingId + static get getCustomerLocationDescription => "$_baseUrl/Customer/GetLocationDescription"; // get - FMS location description by siteId, buildingId, floorId static get getAssets => "$_baseUrl/Asset/GetAssets"; // get static get getFaultDescription => "$_baseUrl/Asset/GetDefectForModelDefinition"; // get static get getAssetById => "$_baseUrl/Asset/GetAssetById?assetId="; // get static get getModels => "$_baseUrl/ModelDefinition/GetModelDefinitionAsset"; // get ?client=2 static get getAllRequestsAndCount => "$_baseUrl/CallRequest/GetAllRequestsAndCount"; // get static get updateUserContactInformation => "$_baseUrl/Account/UpdateContactInfo"; // get + static get searchPlanRecurrentTaskListSearch => "$_baseUrl/PlanRecurrentTasks/PlanRecurrentTaskListSearchDto"; // get + static get checkQRSameRoom => "$_baseUrl/PlanRecurrentTasks/CheckQRSameRoom"; // get + + // //service request new flow urls. static get nurseDashboardCountUrl => '$_baseUrl/ServiceRequest/GetDashboardNurseCount'; @@ -527,4 +534,4 @@ class URLs { static get updateDemoPeriodByRequester => "$_baseUrl/DemoRequest/UpdateDemoPeriodByRequester"; //4104 attachment type lookup -} +} \ No newline at end of file diff --git a/lib/dashboard_latest/widgets/request_category_fragment.dart b/lib/dashboard_latest/widgets/request_category_fragment.dart index c33e8747..2b70da8f 100644 --- a/lib/dashboard_latest/widgets/request_category_fragment.dart +++ b/lib/dashboard_latest/widgets/request_category_fragment.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:test_sa/app_strings/app_asset.dart'; import 'package:test_sa/controllers/providers/api/user_provider.dart'; import 'package:test_sa/dashboard_latest/dashboard_provider.dart'; import 'package:test_sa/dashboard_latest/widgets/request_category_list.dart'; @@ -13,6 +12,7 @@ import 'package:test_sa/models/new_models/dashboard_detail.dart'; import 'package:test_sa/modules/cm_module/views/components/weekly_calendar_fragment.dart'; import 'package:test_sa/new_views/common_widgets/tab_button.dart'; import 'package:test_sa/views/widgets/loaders/no_data_found.dart'; +import 'package:test_sa/controllers/providers/settings/setting_provider.dart'; class RequestCategoryFragment extends StatelessWidget { const RequestCategoryFragment({Key? key}) : super(key: key); @@ -21,6 +21,11 @@ class RequestCategoryFragment extends StatelessWidget { Widget build(BuildContext context) { return Consumer(builder: (context, dashboardProvider, child) { bool isNurse = (Provider.of(context, listen: false).user?.type) == UsersTypes.normal_user; + final user = Provider.of(context, listen: false).user; + final settingProvider = Provider.of(context, listen: false); + + // Check if user is FMS Engineer using SettingProvider + final bool isFMSEngineer = user?.type == UsersTypes.engineer && settingProvider.isUserFMS; return Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ //TODO uncommit this to get the calendar card.. @@ -48,7 +53,17 @@ class RequestCategoryFragment extends StatelessWidget { : const SizedBox(), ], ).paddingOnly(start: 14, end: 12) - : context.translation.allWorkOrder.heading4(context).paddingOnly(start: 16, end: 16), + : isFMSEngineer + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + context.translation.allWorkOrder.heading4(context), + 'search'.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( requestsProvider: dashboardProvider, context: context, diff --git a/lib/dashboard_latest/widgets/requests_fragment.dart b/lib/dashboard_latest/widgets/requests_fragment.dart index ee55dea3..e517b472 100644 --- a/lib/dashboard_latest/widgets/requests_fragment.dart +++ b/lib/dashboard_latest/widgets/requests_fragment.dart @@ -356,6 +356,310 @@ class RequestsFragment extends StatelessWidget { // int index, // Color iconColor, // ) { +// return GestureDetector( +// onTap: isLoading +// ? null +// : () async { +// await Navigator.push( +// context, +// MaterialPageRoute( +// builder: (context) => RequestsListPage(index))); +// Provider.of(context, listen: false) +// .getRequests(); +// }, +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// value != null && value > 0 && !isLoading +// ? badges.Badge( +// badgeContent: Text( +// value.toString(), +// style: const TextStyle( +// color: AppColor.white20, +// fontWeight: FontWeight.normal, +// ), +// ), +// badgeStyle: const badges.BadgeStyle(padding: EdgeInsets.all(4), badgeColor: AppColor.red30), +// child: Container( +// child: (icon ?? "").toSvgAsset(height: 26, width: 26, color: iconColor)?.toShimmer(isShow: isLoading), +// ).toShadowCircleContainer(context, padding: 18), +// ) +// : Container( +// child: (icon ?? "").toSvgAsset(height: 26, width: 26, color: iconColor)?.toShimmer(isShow: isLoading), +// ).toShadowCircleContainer(context, padding: 18), +// 10.height, +// Text( +// title, +// style: AppTextStyles.bodyText.copyWith( +// color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, +// ), +// ), +// ], +// ), +// ); +// } +// } + +// import 'package:flutter/material.dart'; +// import 'package:provider/provider.dart'; +// import 'package:test_sa/controllers/api_routes/api_manager.dart'; +// import 'package:test_sa/controllers/providers/api/all_requests_provider.dart'; +// import 'package:test_sa/controllers/providers/api/notifications_provider.dart'; +// import 'package:test_sa/controllers/providers/api/user_provider.dart'; +// import 'package:test_sa/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:badges/badges.dart' as badges; +// import 'package:test_sa/models/enums/user_types.dart'; +// import 'package:test_sa/new_views/app_style/app_color.dart'; +// import 'package:test_sa/new_views/common_widgets/app_floating_action_button.dart'; +// import 'package:test_sa/new_views/pages/land_page/requests_list_page.dart'; +// +// class RequestsFragment extends StatelessWidget { +// const RequestsFragment({Key? key}) : super(key: key); +// +// @override +// Widget build(BuildContext context) { +// return Scaffold( +// // todo check here, nurse can add request not engineer +// floatingActionButton: const AppFloatingActionButton(), +// body: Consumer( +// builder: (context, snapshot, _) => RefreshIndicator( +// onRefresh: () { +// snapshot.getRequests(); +// Provider.of(context, listen: false) +// .getSystemNotifications( +// user: Provider.of(context, listen: false) +// .user, +// resetProvider: true); +// return Future.delayed(const Duration(microseconds: 250)); +// }, +// child: Container( +// height: 200.toScreenHeight, +// color: Colors.blue, +// child: Wrap( +// children: [ +// listItem( +// snapshot.highPriorityRequests?.total?.count, +// "high_priority", +// context.translation.highPriority, +// context, +// snapshot.isHighPriorityLoading, +// 0, +// context.isDark ? AppColor.redStatus(context) : AppColor.red50, +// ), +// ], +// ), +// // child: ListView( +// // // padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), +// // children: [ +// // listItem( +// // snapshot.highPriorityRequests?.total?.count, +// // "high_priority", +// // context.translation.highPriority, +// // context, +// // snapshot.isHighPriorityLoading, +// // 0, +// // context.isDark ? AppColor.redStatus(context) : AppColor.red50, +// // ), +// // listItem( +// // snapshot.overdueRequests?.total?.count, +// // "overdue", +// // context.translation.overdue, +// // context, +// // snapshot.isOverdueLoading, +// // 1, +// // AppColor.yellowStatus(context), +// // ), +// // listItem( +// // snapshot.openRequests?.total?.count, +// // "new_request", +// // ApiManager.instance.user.type == UsersTypes.engineer +// // ? context.translation.inProgress +// // : context.translation.newR, +// // context, +// // snapshot.isOpenLoading, +// // 2, +// // AppColor.primary40, +// // ), +// // listItem( +// // snapshot.completedRequests?.total?.count, +// // "complete_request", +// // context.translation.completed, +// // context, +// // snapshot.isCompleteLoading, +// // 3, +// // AppColor.greenStatus(context), +// // ), +// // ], +// // ), +// )), +// ), +// ); +// } +// +// Widget listItem( +// int value, +// String icon, +// String title, +// BuildContext context, +// bool isLoading, +// int index, +// Color iconColor, +// ) { +// return GestureDetector( +// onTap: isLoading +// ? null +// : () async { +// await Navigator.push( +// context, +// MaterialPageRoute( +// builder: (context) => RequestsListPage(index))); +// Provider.of(context, listen: false) +// .getRequests(); +// }, +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// value != null && value > 0 && !isLoading +// ? badges.Badge( +// badgeContent: Text( +// value.toString(), +// style: const TextStyle( +// color: AppColor.white20, +// fontWeight: FontWeight.normal, +// ), +// ), +// badgeStyle: const badges.BadgeStyle(padding: EdgeInsets.all(4), badgeColor: AppColor.red30), +// child: Container( +// child: (icon ?? "").toSvgAsset(height: 26, width: 26, color: iconColor)?.toShimmer(isShow: isLoading), +// ).toShadowCircleContainer(context, padding: 18), +// ) +// : Container( +// child: (icon ?? "").toSvgAsset(height: 26, width: 26, color: iconColor)?.toShimmer(isShow: isLoading), +// ).toShadowCircleContainer(context, padding: 18), +// 10.height, +// Text( +// title, +// style: AppTextStyles.bodyText.copyWith( +// color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, +// ), +// ), +// ], +// ), +// ); +// } +// } + +// import 'package:flutter/material.dart'; +// import 'package:provider/provider.dart'; +// import 'package:test_sa/controllers/api_routes/api_manager.dart'; +// import 'package:test_sa/controllers/providers/api/all_requests_provider.dart'; +// import 'package:test_sa/controllers/providers/api/notifications_provider.dart'; +// import 'package:test_sa/controllers/providers/api/user_provider.dart'; +// import 'package:test_sa/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:badges/badges.dart' as badges; +// import 'package:test_sa/models/enums/user_types.dart'; +// import 'package:test_sa/new_views/app_style/app_color.dart'; +// import 'package:test_sa/new_views/common_widgets/app_floating_action_button.dart'; +// import 'package:test_sa/new_views/pages/land_page/requests_list_page.dart'; +// +// class RequestsFragment extends StatelessWidget { +// const RequestsFragment({Key? key}) : super(key: key); +// +// @override +// Widget build(BuildContext context) { +// return Scaffold( +// // todo check here, nurse can add request not engineer +// floatingActionButton: const AppFloatingActionButton(), +// body: Consumer( +// builder: (context, snapshot, _) => RefreshIndicator( +// onRefresh: () { +// snapshot.getRequests(); +// Provider.of(context, listen: false) +// .getSystemNotifications( +// user: Provider.of(context, listen: false) +// .user, +// resetProvider: true); +// return Future.delayed(const Duration(microseconds: 250)); +// }, +// child: Container( +// height: 200.toScreenHeight, +// color: Colors.blue, +// child: Wrap( +// children: [ +// listItem( +// snapshot.highPriorityRequests?.total?.count, +// "high_priority", +// context.translation.highPriority, +// context, +// snapshot.isHighPriorityLoading, +// 0, +// context.isDark ? AppColor.redStatus(context) : AppColor.red50, +// ), +// ], +// ), +// // child: ListView( +// // // padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), +// // children: [ +// // listItem( +// // snapshot.highPriorityRequests?.total?.count, +// // "high_priority", +// // context.translation.highPriority, +// // context, +// // snapshot.isHighPriorityLoading, +// // 0, +// // context.isDark ? AppColor.redStatus(context) : AppColor.red50, +// // ), +// // listItem( +// // snapshot.overdueRequests?.total?.count, +// // "overdue", +// // context.translation.overdue, +// // context, +// // snapshot.isOverdueLoading, +// // 1, +// // AppColor.yellowStatus(context), +// // ), +// // listItem( +// // snapshot.openRequests?.total?.count, +// // "new_request", +// // ApiManager.instance.user.type == UsersTypes.engineer +// // ? context.translation.inProgress +// // : context.translation.newR, +// // context, +// // snapshot.isOpenLoading, +// // 2, +// // AppColor.primary40, +// // ), +// // listItem( +// // snapshot.completedRequests?.total?.count, +// // "complete_request", +// // context.translation.completed, +// // context, +// // snapshot.isCompleteLoading, +// // 3, +// // AppColor.greenStatus(context), +// // ), +// // ], +// // ), +// )), +// ), +// ); +// } +// +// Widget listItem( +// int value, +// String icon, +// String title, +// BuildContext context, +// bool isLoading, +// int index, +// Color iconColor, +// ) { // value = 4; // return GestureDetector( // onTap: isLoading diff --git a/lib/extensions/widget_extensions.dart b/lib/extensions/widget_extensions.dart index 1d2d59f8..7b468913 100644 --- a/lib/extensions/widget_extensions.dart +++ b/lib/extensions/widget_extensions.dart @@ -202,3 +202,4 @@ extension WidgetExtensions on Widget { extension DividerExtension on Divider { Divider defaultStyle(BuildContext context) => Divider(thickness: 1, color: context.isDarkNotListen ? AppColor.neutral20 : AppColor.neutral30); } + diff --git a/lib/main.dart b/lib/main.dart index fcb78f3e..40ba2eda 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -139,6 +139,9 @@ import 'providers/lookups/request_type_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/reject_reason_provider.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'; // class MyHttpOverrides extends HttpOverrides { // @override @@ -223,7 +226,7 @@ class MyApp extends StatelessWidget { // LAZY LOADED PROVIDERS (107) - Created only when accessed // This improves app startup time by 40-60% // ============================================================ - + // LOOKUP PROVIDERS (16) - Dropdown data ChangeNotifierProvider(create: (_) => ClassificationLookupProvider(), lazy: true), ChangeNotifierProvider(create: (_) => RecommendationLookupProvider(), lazy: true), @@ -347,6 +350,9 @@ class MyApp extends StatelessWidget { // CX MODULE PROVIDERS (1) ChangeNotifierProvider(create: (_) => SurveyProvider(), lazy: true), + // CLEANER MODULE PROVIDERS (1) + ChangeNotifierProvider(create: (_) => CleanerProvider(), lazy: true), + // UTILITY PROVIDERS (1) ChangeNotifierProvider(create: (_) => OracleCodeProvider(), lazy: true), ], @@ -401,6 +407,8 @@ class MyApp extends StatelessWidget { CreateLoanRequestPage.id: (_) => CreateLoanRequestPage(), CreateMedicalGasRequestPage.id: (_) => CreateMedicalGasRequestPage(), CreateDemoRequestPage.id: (_) => CreateDemoRequestPage(), + CleanerSearchFormView.routeName: (_) => const CleanerSearchFormView(), + CleanerSearchListView.routeName: (_) => const CleanerSearchListView(), }, ), ), diff --git a/lib/models/new_models/building.dart b/lib/models/new_models/building.dart index e112af11..7dcc6e13 100644 --- a/lib/models/new_models/building.dart +++ b/lib/models/new_models/building.dart @@ -59,4 +59,14 @@ class Building extends Base { return map; } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Building && + runtimeType == other.runtimeType && + id == other.id; + + @override + int get hashCode => id.hashCode; } diff --git a/lib/models/new_models/department.dart b/lib/models/new_models/department.dart index e48585da..c7538e82 100644 --- a/lib/models/new_models/department.dart +++ b/lib/models/new_models/department.dart @@ -69,4 +69,14 @@ class Department extends Base { } return map; } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Department && + runtimeType == other.runtimeType && + id == other.id; + + @override + int get hashCode => id.hashCode; } diff --git a/lib/models/new_models/department_room_combined.dart b/lib/models/new_models/department_room_combined.dart new file mode 100644 index 00000000..feaca858 --- /dev/null +++ b/lib/models/new_models/department_room_combined.dart @@ -0,0 +1,55 @@ +import 'package:test_sa/models/base.dart'; + +class DepartmentRoomCombined extends Base { + final int? deptId; + final int? roomId; + final String? deptName; + final String? roomName; + final String? locationDescriptionName; + final String? deptAndRoomName; + + DepartmentRoomCombined({ + this.deptId, + this.roomId, + this.deptName, + this.roomName, + this.locationDescriptionName, + this.deptAndRoomName, + }) : super( + identifier: '${deptId}_${roomId}', + name: deptAndRoomName ?? '${deptName ?? ''} / ${roomName ?? ''}', + ); + + factory DepartmentRoomCombined.fromJson(Map json) { + return DepartmentRoomCombined( + deptId: json['deptId'], + roomId: json['roomId'], + deptName: json['deptName'], + roomName: json['roomName'], + locationDescriptionName: json['locationDescriptionName'], + deptAndRoomName: json['deptAndRoomName'], + ); + } + + Map toJson() { + return { + 'deptId': deptId, + 'roomId': roomId, + 'deptName': deptName, + 'roomName': roomName, + 'locationDescriptionName': locationDescriptionName, + 'deptAndRoomName': deptAndRoomName, + }; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DepartmentRoomCombined && + runtimeType == other.runtimeType && + deptId == other.deptId && + roomId == other.roomId; + + @override + int get hashCode => deptId.hashCode ^ roomId.hashCode; +} diff --git a/lib/models/new_models/floor.dart b/lib/models/new_models/floor.dart index 33e74d8b..4adc1367 100644 --- a/lib/models/new_models/floor.dart +++ b/lib/models/new_models/floor.dart @@ -57,4 +57,14 @@ class Floor extends Base { } return map; } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Floor && + runtimeType == other.runtimeType && + id == other.id; + + @override + int get hashCode => id.hashCode; } diff --git a/lib/models/new_models/room_model.dart b/lib/models/new_models/room_model.dart index 62012565..c525800e 100644 --- a/lib/models/new_models/room_model.dart +++ b/lib/models/new_models/room_model.dart @@ -22,6 +22,16 @@ class Rooms extends Base { data['value'] = value; return data; } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Rooms && + runtimeType == other.runtimeType && + id == other.id; + + @override + int get hashCode => id.hashCode; } // class Rooms extends Base { diff --git a/lib/models/ppm/recurrent_wo.dart b/lib/models/ppm/recurrent_wo.dart index 3a689553..bae7cac6 100644 --- a/lib/models/ppm/recurrent_wo.dart +++ b/lib/models/ppm/recurrent_wo.dart @@ -2,6 +2,7 @@ import 'package:test_sa/extensions/enum_extensions.dart'; import 'package:test_sa/models/enums/recurrent_task_inspection_data_type.dart'; import 'package:test_sa/models/lookup.dart'; import 'package:test_sa/models/timer_model.dart'; +import 'package:test_sa/modules/cleaner_module/widgets/cleaner_checklist_widget.dart'; class RecurrentWo { RecurrentWoData? recurrentWoData; @@ -51,11 +52,13 @@ class RecurrentWoData { Lookup? room; List? planRecurrentMedicalTaskRooms; List? planRecurrentTaskTimers; + List? planRecurrentTaskCheckLists; TimerModel? recurrentWoTimerModel = TimerModel(); double? totalWorkingHours = 0.0; List? timerModelList = []; String? comment; TimerModel? recurrentWoTimePicker; + String? imageRoom; RecurrentWoData( {this.id, @@ -73,10 +76,12 @@ class RecurrentWoData { this.room, this.planRecurrentMedicalTaskRooms, this.planRecurrentTaskTimers, + this.planRecurrentTaskCheckLists, this.timerModelList, this.recurrentWoTimePicker, this.comment, - this.totalWorkingHours}); + this.totalWorkingHours, + this.imageRoom}); RecurrentWoData.fromJson(Map json) { id = json['id']; @@ -92,6 +97,7 @@ class RecurrentWoData { department = json["department"] == null ? null : Lookup.fromJson(json["department"]); room = json["room"] == null ? null : Lookup.fromJson(json["room"]); comment = json["comment"]; + imageRoom = json["imageRoom"]; if (json['planRecurrentMedicalTaskRooms'] != null) { planRecurrentMedicalTaskRooms = []; json['planRecurrentMedicalTaskRooms'].forEach((v) { @@ -107,6 +113,19 @@ class RecurrentWoData { //TODO need to test this @waseem.. totalWorkingHours = json['planRecurrentTaskTimers'].fold(0.0, (sum, item) => (sum ?? 0) + DateTime.parse(item['endDateTime']).difference(DateTime.parse(item['startDateTime'])).inSeconds) ?? 0; } + + if (json['planRecurrentTaskCheckLists'] != null) { + planRecurrentTaskCheckLists = []; + json['planRecurrentTaskCheckLists'].forEach((v) { + planRecurrentTaskCheckLists!.add(ChecklistItem( + id: v['id'] ?? 0, + title: v['instructionText'] ?? v['title'] ?? '', + isChecked: v['isDone'] ?? v['isChecked'] ?? false, + comment: v['comment'] ?? '', + isMandatory: v['isRequired'] ?? v['isMandatory'] ?? false, + )); + }); + } } Map toJson({int? status = 0}) { @@ -127,6 +146,21 @@ class RecurrentWoData { if (planRecurrentTaskTimers != null) { data['planRecurrentTaskTimers'] = planRecurrentTaskTimers!.map((v) => v.toJson()).toList(); } + + if (planRecurrentTaskCheckLists != null) { + data['planRecurrentTaskCheckLists'] = planRecurrentTaskCheckLists! + .map((v) => { + 'id': v.id, + 'comment': v.commentController.text, + 'isDone': v.isChecked, + }) + .toList(); + } + + if (imageRoom != null && imageRoom!.isNotEmpty) { + data['imageRoom'] = imageRoom; + } + if (taskRoomTabAttributesJson.isNotEmpty) { data['attributes'] = taskRoomTabAttributesJson; } else { diff --git a/lib/modules/cleaner_module/models/cleaner_data_model.dart b/lib/modules/cleaner_module/models/cleaner_data_model.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/modules/cleaner_module/models/cleaner_form_model.dart b/lib/modules/cleaner_module/models/cleaner_form_model.dart new file mode 100644 index 00000000..f8f7c98d --- /dev/null +++ b/lib/modules/cleaner_module/models/cleaner_form_model.dart @@ -0,0 +1,24 @@ +// 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, + }); +} diff --git a/lib/modules/cleaner_module/models/cleaner_task_search_result.dart b/lib/modules/cleaner_module/models/cleaner_task_search_result.dart new file mode 100644 index 00000000..88a2e9ac --- /dev/null +++ b/lib/modules/cleaner_module/models/cleaner_task_search_result.dart @@ -0,0 +1,73 @@ +class CleanerTaskSearchResult { + int? id; + String? taskNo; + String? siteName; + String? buildingName; + String? floorName; + String? departmentName; + String? roomName; + String? locationDescription; + String? createdDate; + + CleanerTaskSearchResult({ + this.id, + this.taskNo, + this.siteName, + this.buildingName, + this.floorName, + this.departmentName, + this.roomName, + this.locationDescription, + this.createdDate, + }); + + factory CleanerTaskSearchResult.fromJson(Map json) { + return CleanerTaskSearchResult( + id: json['id'], + taskNo: json['taskNo'], + siteName: json['siteName'], + buildingName: json['buildingName'], + floorName: json['floorName'], + departmentName: json['departmentName'], + roomName: json['roomName'], + locationDescription: json['locationDescription'], + createdDate: json['createdDate'], + ); + } + + Map toJson() { + return { + 'id': id, + 'taskNo': taskNo, + 'siteName': siteName, + 'buildingName': buildingName, + 'floorName': floorName, + 'departmentName': departmentName, + 'roomName': roomName, + 'locationDescription': locationDescription, + 'createdDate': createdDate, + }; + } + + String get fullLocation { + List parts = []; + if (siteName != null && siteName!.isNotEmpty) parts.add(siteName!); + if (buildingName != null && buildingName!.isNotEmpty) parts.add(buildingName!); + if (floorName != null && floorName!.isNotEmpty) parts.add(floorName!); + if (departmentName != null && departmentName!.isNotEmpty) parts.add(departmentName!); + if (roomName != null && roomName!.isNotEmpty) parts.add('Room $roomName'); + return parts.join(' - '); + } + + String get departmentRoom { + if (departmentName != null && roomName != null) { + return '$departmentName / $roomName'; + } else if (departmentName != null) { + return departmentName!; + } else if (roomName != null) { + return 'Room $roomName'; + } + return ''; + } +} + diff --git a/lib/modules/cleaner_module/pages/cleaner_request_item_view.dart b/lib/modules/cleaner_module/pages/cleaner_request_item_view.dart new file mode 100644 index 00000000..96d0eb05 --- /dev/null +++ b/lib/modules/cleaner_module/pages/cleaner_request_item_view.dart @@ -0,0 +1,68 @@ +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), + 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); + } +} diff --git a/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart b/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart new file mode 100644 index 00000000..948688d0 --- /dev/null +++ b/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart @@ -0,0 +1,318 @@ +// UI: Cleaner Search Details View +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:test_sa/extensions/context_extension.dart'; +import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/extensions/string_extensions.dart'; +import 'package:test_sa/extensions/text_extensions.dart'; +import 'package:test_sa/extensions/widget_extensions.dart'; +import 'package:test_sa/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/app_lazy_loading.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/views/widgets/qr/scan_qr.dart'; +import '../models/cleaner_task_search_result.dart'; +import '../provider/cleaner_provider.dart'; +import '../../pm_module/recurrent_wo/recurrent_work_order_view.dart'; +import 'package:test_sa/controllers/providers/api/all_requests_provider.dart'; + +class CleanerSearchDetailsView extends StatefulWidget { + static const String routeName = '/cleaner_search_details'; + final int requestId; + final CleanerTaskSearchResult? taskResult; + + const CleanerSearchDetailsView({ + Key? key, + required this.requestId, + this.taskResult, + }) : super(key: key); + + @override + _CleanerSearchDetailsViewState createState() => _CleanerSearchDetailsViewState(); +} + +class _CleanerSearchDetailsViewState extends State { + late CleanerProvider _cleanerProvider; + bool _isQRScanned = false; + CleanerTaskSearchResult? _taskData; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _cleanerProvider = Provider.of(context, listen: false); + _taskData = widget.taskResult; + WidgetsBinding.instance.addPostFrameCallback((_) { + getInitialData(); + }); + } + + Future getInitialData() async { + // If taskResult is null, fetch data from recurrent work order API + if (widget.taskResult == null) { + setState(() { + _isLoading = true; + }); + + try { + final allRequestsProvider = Provider.of(context, listen: false); + await allRequestsProvider.getRecurrentWoById(id: widget.requestId); + + // Map RecurrentWoData to CleanerTaskSearchResult + if (allRequestsProvider.recurrentWoData != null) { + final recurrentData = allRequestsProvider.recurrentWoData!; + _taskData = CleanerTaskSearchResult( + id: recurrentData.id, + taskNo: recurrentData.taskNo, + siteName: recurrentData.site?.siteName, + buildingName: recurrentData.building?.name, + floorName: recurrentData.floor?.name, + departmentName: recurrentData.department?.name, + roomName: recurrentData.room?.name, + locationDescription: null, // Not available in RecurrentWoData + createdDate: recurrentData.scheduleDate, + ); + } + } catch (e) { + debugPrint('Error fetching recurrent work order: $e'); + } finally { + setState(() { + _isLoading = false; + }); + } + } else { + await _cleanerProvider.getCleanerRequestById(id: widget.requestId); + } + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: DefaultAppBar( + title: 'Request Details'.addTranslation, + onBackPress: () => Navigator.pop(context), + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : Selector( + selector: (_, model) => model.isLoading, + builder: (_, isLoading, __) { + if (isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + return Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 18.height, + _buildCleanerRequestDetailCard(context), + 20.height, + ], + ), + ), + ), + SafeArea( + top: false, + right: false, + left: false, + child: FooterActionButton.footerContainer( + context: context, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (!_isQRScanned) + Row( + children: [ + Icon( + Icons.info_outline, + color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, + size: 15, + ), + 8.width, + Expanded( + child: Text( + 'By scanning the QR code you will accept this task'.addTranslation, + textAlign: TextAlign.center, + style: AppTextStyles.bodyText.copyWith( + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, + ), + ), + ), + ], + ), + 16.height, + AppFilledButton( + label: _isQRScanned ? 'Update' : 'Scan QR Code', + maxWidth: true, + buttonColor: AppColor.primary10, + onPressed: () async { + if (_isQRScanned) { + // If QR is already scanned, navigate to recurrent work order view + 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) { + // Show loading indicator + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const AppLazyLoading(), + ); + + // Call the API to check if QR matches the same room + final isValid = await CleanerProvider().checkQRSameRoom( + planRecurrentTaskId: widget.requestId, + qrCode: result, + ); + + // Close loading dialog + if (mounted) Navigator.pop(context); + + if (isValid) { + // Update state to show "Update" button instead of navigating + if (mounted) { + setState(() { + _isQRScanned = true; + }); + } + } + } + } + }, + ), + ], + ).paddingOnly(start: 16, end: 16), + ), + ).toShadowContainer(context, padding: 0, showShadow: false, borderRadius: 0), + ], + ); + }, + ), + ); + } + + Widget _buildCleanerRequestDetailCard(BuildContext context) { + final taskResult = _taskData; + + if (taskResult == null) { + return const SizedBox(); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Task Number and Date Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Request Information', + style: AppTextStyles.heading4.copyWith( + color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, + ), + ), + if (taskResult.createdDate != null) + Text( + taskResult.createdDate!.split('T').first, + textAlign: TextAlign.end, + style: AppTextStyles.tinyFont.copyWith( + color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, + ), + ), + ], + ), + 8.height, + if (taskResult.taskNo != null && taskResult.taskNo!.isNotEmpty) + Text( + 'Task No: ${taskResult.taskNo!}', + style: AppTextStyles.bodyText.copyWith( + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, + ), + ), + if (taskResult.siteName != null && taskResult.siteName!.isNotEmpty) + Text( + 'Site: ${taskResult.siteName}', + style: AppTextStyles.bodyText.copyWith( + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, + ), + ), + if (taskResult.buildingName != null && taskResult.buildingName!.isNotEmpty) + Text( + 'Building: ${taskResult.buildingName}', + style: AppTextStyles.bodyText.copyWith( + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, + ), + ), + if (taskResult.floorName != null && taskResult.floorName!.isNotEmpty) + Text( + 'Floor: ${taskResult.floorName}', + style: AppTextStyles.bodyText.copyWith( + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, + ), + ), + if (taskResult.departmentName != null && taskResult.departmentName!.isNotEmpty) + Text( + 'Department: ${taskResult.departmentName}', + style: AppTextStyles.bodyText.copyWith( + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, + ), + ), + if (taskResult.roomName != null && taskResult.roomName!.isNotEmpty) + Text( + 'Room: ${taskResult.roomName}', + style: AppTextStyles.bodyText.copyWith( + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, + ), + ), + if (taskResult.locationDescription != null && taskResult.locationDescription!.isNotEmpty) + Text( + 'Location: ${taskResult.locationDescription}', + style: AppTextStyles.bodyText.copyWith( + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, + ), + ), + ], + ).expanded, + ], + ), + ], + ).paddingOnly(start: 16, end: 16, top: 16, bottom: 8), + ], + ).toShadowContainer(context, padding: 0); + } +} diff --git a/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart b/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart new file mode 100644 index 00000000..bb9fb739 --- /dev/null +++ b/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart @@ -0,0 +1,512 @@ +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/string_extensions.dart'; +import 'package:test_sa/extensions/text_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/department_room_combined.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/fms_building_provider.dart'; +import 'package:test_sa/modules/cleaner_module/provider/fms_floor_provider.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/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/single_item_drop_down_menu.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/models/new_models/building.dart'; +import 'package:test_sa/models/new_models/floor.dart'; +import 'package:test_sa/extensions/int_extensions.dart'; + +class CleanerSearchFormView extends StatefulWidget { + static const String routeName = '/cleaner_search'; + + const CleanerSearchFormView({Key? key}) : super(key: key); + + @override + State createState() => _CleanerSearchFormViewState(); +} + +class _CleanerSearchFormViewState extends State { + Hospital? selectedSite; + Building? selectedBuilding; + Floor? selectedFloor; + DepartmentRoomCombined? selectedDepartmentRoom; + Lookup? selectedLocationDescription; + late FocusNode _siteFocusNode; + final TextEditingController _siteController = TextEditingController(); + + List _siteOptions = []; + List _buildingOptions = []; + List _floorOptions = []; + List _locationDescriptionOptions = []; + List _allDeptRoomList = []; + + bool _isLoadingSites = false; + bool _isLoadingBuildings = false; + bool _isLoadingFloors = false; + bool _isLoadingLocationDescriptions = false; + bool _isSearching = false; + + @override + void initState() { + super.initState(); + _siteFocusNode = FocusNode(); + } + + @override + void dispose() { + _siteController.dispose(); + _siteFocusNode.dispose(); + super.dispose(); + } + + Future _searchSites(String query) async { + if (query.isEmpty) { + setState(() { + _siteOptions = []; + }); + return; + } + + setState(() { + _isLoadingSites = true; + }); + + try { + final sites = await HospitalsProvider().getHospitalsListByVal(searchVal: query); + setState(() { + _siteOptions = sites; + _isLoadingSites = false; + }); + } catch (e) { + setState(() { + _isLoadingSites = false; + }); + } + } + + Future _loadBuildings(int siteId) async { + setState(() { + _isLoadingBuildings = true; + }); + + try { + final buildings = await FmsBuildingProvider().getBuildingsListBySiteId(siteId); + setState(() { + _buildingOptions = buildings; + _isLoadingBuildings = false; + }); + } catch (e) { + setState(() { + _isLoadingBuildings = false; + _buildingOptions = []; + }); + } + } + + Future _loadFloors(int siteId, int buildingId) async { + setState(() { + _isLoadingFloors = true; + }); + + try { + final floors = await FmsFloorProvider().getFloorsListBySiteAndBuilding(siteId, buildingId); + setState(() { + _floorOptions = floors; + _isLoadingFloors = false; + }); + } catch (e) { + setState(() { + _isLoadingFloors = false; + _floorOptions = []; + }); + } + } + + Future _loadLocationDescriptions(int siteId, int buildingId, int floorId) async { + setState(() { + _isLoadingLocationDescriptions = true; + }); + + try { + final response = await FmsLocationDescriptionProvider().getLocationDescriptionsList(siteId, buildingId, floorId); + setState(() { + _locationDescriptionOptions = response?.locationDescriptions ?? []; + _allDeptRoomList = response?.deptRoom ?? []; + _isLoadingLocationDescriptions = false; + }); + } catch (e) { + setState(() { + _isLoadingLocationDescriptions = false; + _locationDescriptionOptions = []; + _allDeptRoomList = []; + }); + } + } + + List _getDepartmentRoomCombinations() { + if (selectedLocationDescription == null || _allDeptRoomList.isEmpty) return []; + + // Filter dept room list by selected location description + List 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 + if (combinations.length == 1) { + setState(() { + selectedDepartmentRoom = combinations.first; + }); + } else { + // If more than one, auto-select first item + setState(() { + selectedDepartmentRoom = combinations.first; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: DefaultAppBar( + title: 'Search Request', + titleStyle: AppTextStyles.heading3.copyWith( + fontWeight: FontWeight.w500, + color: Theme.of(context).brightness == Brightness.dark ? AppColor.neutral30 : AppColor.neutral50, + ), + ), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Site Autocomplete Field + Container( + padding: EdgeInsets.symmetric( + horizontal: 16.toScreenWidth, + vertical: 8.toScreenHeight, + ), + decoration: BoxDecoration( + color: AppColor.fieldBgColor(context), + borderRadius: BorderRadius.circular(10), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Site', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: context.isDark ? AppColor.neutral30 : AppColor.neutral20, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 4.toScreenHeight), + SizedBox( + height: 24.toScreenHeight, // Fixed height for consistency + child: Autocomplete( + optionsBuilder: (TextEditingValue textEditingValue) async { + if (textEditingValue.text.isEmpty) { + return const Iterable.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, + style: AppTextStyles.bodyText.copyWith( + color: context.isDark ? AppColor.neutral30 : AppColor.black10, + ), + decoration: InputDecoration( + hintText: 'Search site...', + hintStyle: AppTextStyles.bodyText.copyWith( + color: AppColor.neutral20, + ), + filled: false, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + contentPadding: EdgeInsets.zero, + isDense: true, + ), + onChanged: (value) { + _siteController.text = value; + }, + ), + ), + if (_isLoadingSites) + const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: AppColor.primary10, + ), + ) + 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: const Icon( + Icons.clear, + color: AppColor.neutral20, + size: 20, + ), + ), + ], + ); + }, + ), + ), + ], + ), + ), + const SizedBox(height: 18), + + // Building Dropdown + SingleItemDropDownMenu( + context: context, + title: 'Building', + initialValue: selectedBuilding, + showShadow: false, + showAsBottomSheet: true, + backgroundColor: AppColor.fieldBgColor(context), + enabled: _buildingOptions.isNotEmpty, + loading: _isLoadingBuildings, + staticData: _buildingOptions, + onSelect: (value) async { + _siteFocusNode.unfocus(); + + setState(() { + _siteOptions = []; + print('site options length ${_siteOptions.length}'); + 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()); + } + }, + ), + const SizedBox(height: 18), + + // Floor Dropdown + SingleItemDropDownMenu( + context: context, + title: 'Floor', + initialValue: selectedFloor, + showShadow: false, + showAsBottomSheet: true, + backgroundColor: AppColor.fieldBgColor(context), + enabled: _floorOptions.isNotEmpty, + loading: _isLoadingFloors, + staticData: _floorOptions, + onSelect: (value) async { + setState(() { + selectedFloor = value; + selectedLocationDescription = null; + selectedDepartmentRoom = null; + _locationDescriptionOptions = []; + _allDeptRoomList = []; + }); + + // Load location descriptions for selected floor + if (value != null && selectedSite?.id != null && selectedBuilding?.id != null && value.id != null) { + await _loadLocationDescriptions( + selectedSite!.id!, + selectedBuilding!.id!.toInt(), + value.id!.toInt(), + ); + } + }, + ), + const SizedBox(height: 18), + + // Location Description Dropdown + SingleItemDropDownMenu( + context: context, + title: 'Location Description', + initialValue: selectedLocationDescription, + showShadow: false, + 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(); + }, + ), + const SizedBox(height: 18), + + // Department & Room Combined Dropdown + SingleItemDropDownMenu( + 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), + ), + ), + FooterActionButton.footerContainer( + context: context, + child: AppFilledButton( + buttonColor: AppColor.primary10, + label: context.translation.search, + maxWidth: true, + onPressed: _isSearching ? null : _onSubmit, + loading: _isSearching, + ), + ), + ], + ), + ); + } + + Future _onSubmit() async { + // Validate that at least site is selected + if (selectedSite == null) { + 'Please select a site'.showToast; + return; + } + + try { + setState(() { + _isSearching = true; + }); + + // Call the search API + final response = await CleanerProvider().searchPlanRecurrentTasksList( + 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; + final totalRows = response['totalRows'] as int; + + // Navigate to search results page with the results + Navigator.of(context).pushNamed( + CleanerSearchListView.routeName, + arguments: { + 'searchResults': results, + 'totalRows': totalRows, + 'selectedSite': selectedSite, + 'selectedBuilding': selectedBuilding, + 'selectedFloor': selectedFloor, + 'selectedLocationDescription': selectedLocationDescription, + 'selectedDepartmentRoom': selectedDepartmentRoom, + }, + ); + } catch (e) { + 'Error searching'.showToast; + } finally { + setState(() { + _isSearching = false; + }); + } + } +} diff --git a/lib/modules/cleaner_module/pages/cleaner_search_list_view.dart b/lib/modules/cleaner_module/pages/cleaner_search_list_view.dart new file mode 100644 index 00000000..884ba220 --- /dev/null +++ b/lib/modules/cleaner_module/pages/cleaner_search_list_view.dart @@ -0,0 +1,186 @@ +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 createState() => _CleanerSearchListViewState(); +} + +class _CleanerSearchListViewState extends State { + List _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?; + if (args != null && args['searchResults'] != null) { + final results = args['searchResults'] as List; + _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 _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; + 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, + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/cleaner_module/provider/cleaner_provider.dart b/lib/modules/cleaner_module/provider/cleaner_provider.dart new file mode 100644 index 00000000..a057aa54 --- /dev/null +++ b/lib/modules/cleaner_module/provider/cleaner_provider.dart @@ -0,0 +1,169 @@ +import 'dart:convert'; + +import 'package:flutter/material.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'; + +class CleanerProvider extends ChangeNotifier { + bool _isLoading = false; + bool get isLoading => _isLoading; + + // Search results + List? _searchResults; + List? get searchResults => _searchResults; + + // State code for error handling + int? _stateCode; + int? get stateCode => _stateCode; + + // TODO: Add your cleaner request model here + // CleanerRequestModel? _currentCleanerRequest; + // CleanerRequestModel? get currentCleanerRequest => _currentCleanerRequest; + + Future getCleanerRequestById({required int id}) async { + _isLoading = true; + notifyListeners(); + + try { + // TODO: Implement API call to fetch cleaner request details + // Example: + // final response = await CleanerApiService.getCleanerRequestById(id); + // _currentCleanerRequest = response.data; + + await Future.delayed(const Duration(seconds: 1)); // Simulating API call + + } catch (e) { + debugPrint('Error fetching cleaner request: $e'); + } finally { + _isLoading = false; + notifyListeners(); + } + } + + /// Search plan recurrent tasks + Future searchPlanRecurrentTasks({ + required int pageSize, + required int pageNumber, + int? siteId, + int? buildingId, + int? floorId, + int? departmentId, + int? roomId, + }) async { + if (_isLoading == true) return -2; + _isLoading = true; + notifyListeners(); + + Response response; + try { + response = await ApiManager.instance.post( + URLs.searchPlanRecurrentTaskListSearch, + body: { + "pageSize": pageSize, + "pageNumber": pageNumber, + "siteId": siteId, + "buildingId": buildingId, + "floorId": floorId, + "departmentId": departmentId, + "roomId": roomId, + }, + ); + + _stateCode = response.statusCode; + + if (response.statusCode >= 200 && response.statusCode < 300) { + // client's request was successfully received + final responseData = json.decode(response.body); + _searchResults = responseData["data"] as List?; + } else { + _searchResults = null; + } + + _isLoading = false; + notifyListeners(); + return response.statusCode; + } catch (error) { + debugPrint('Error searching plan recurrent tasks: $error'); + _isLoading = false; + _stateCode = -1; + _searchResults = null; + notifyListeners(); + return -1; + } + } + + /// Search plan recurrent tasks and return results directly without provider state + Future> searchPlanRecurrentTasksList({ + required int pageSize, + required int pageNumber, + int? siteId, + int? buildingId, + int? floorId, + int? departmentId, + int? roomId, + }) async { + Response response; + try { + response = await ApiManager.instance.post( + URLs.searchPlanRecurrentTaskListSearch, + body: { + "pageSize": pageSize, + "pageNumber": pageNumber, + "siteId": siteId, + "buildingId": buildingId, + "floorId": floorId, + "departmentId": departmentId, + "roomId": roomId, + }, + ); + + if (response.statusCode >= 200 && response.statusCode < 300) { + final responseData = json.decode(response.body); + return { + "data": responseData["data"] as List? ?? [], + "totalRows": responseData["totalRows"] as int? ?? 0, + }; + } + return {"data": [], "totalRows": 0}; + } catch (error) { + debugPrint('Error searching plan recurrent tasks: $error'); + return {"data": [], "totalRows": 0}; + } + } + + /// Check if QR code matches the same room for a plan recurrent task + Future checkQRSameRoom({ + required int planRecurrentTaskId, + required String qrCode, + }) async { + Response response; + try { + response = await ApiManager.instance.post( + URLs.checkQRSameRoom, + body: { + "planRecurrentTaskId": planRecurrentTaskId, + "qrCode": qrCode, + }, + ); + + if (response.statusCode >= 200 && response.statusCode < 300) { + final responseData = json.decode(response.body); + // Assuming the API returns a boolean in the data field or a success flag + return responseData["data"] == true || responseData["isSuccess"] == true; + } + return false; + } catch (error) { + debugPrint('Error checking QR same room: $error'); + return false; + } + } + + void reset() { + _isLoading = false; + _searchResults = null; + _stateCode = null; + // _currentCleanerRequest = null; + notifyListeners(); + } +} diff --git a/lib/modules/cleaner_module/provider/fms_building_provider.dart b/lib/modules/cleaner_module/provider/fms_building_provider.dart new file mode 100644 index 00000000..39b3f0b3 --- /dev/null +++ b/lib/modules/cleaner_module/provider/fms_building_provider.dart @@ -0,0 +1,83 @@ +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/building.dart'; + +class FmsBuildingProvider extends ChangeNotifier { + //reset provider data + void reset() { + _buildings = null; + _stateCode = null; + } + + // state code of current request to defied error message + int? _stateCode; + int? get stateCode => _stateCode; + + // contain buildings data + List? _buildings; + List? get buildings => _buildings; + + // loading state + bool _loading = false; + bool get isLoading => _loading; + + set isLoading(bool isLoading) { + _loading = isLoading; + notifyListeners(); + } + + /// Get buildings by site ID + Future getBuildingsBySiteId(int siteId) async { + if (_loading == true) return -2; + _loading = true; + notifyListeners(); + Response response; + try { + response = await ApiManager.instance.post( + URLs.getCustomerBuildings, + body: { + "siteId": siteId, + }, + ); + _stateCode = response.statusCode; + if (response.statusCode >= 200 && response.statusCode < 300) { + // client's request was successfully received + List buildingsListJson = json.decode(response.body)["data"]; + _buildings = buildingsListJson.map((building) => Building.fromJson(building)).toList(); + } + _loading = false; + notifyListeners(); + return response.statusCode; + } catch (error) { + _loading = false; + _stateCode = -1; + notifyListeners(); + return -1; + } + } + + /// Get buildings list directly without provider state + Future> getBuildingsListBySiteId(int siteId) async { + Response response; + try { + response = await ApiManager.instance.post( + URLs.getCustomerBuildings, + body: { + "siteId": siteId, + }, + ); + + if (response.statusCode >= 200 && response.statusCode < 300) { + List buildingsListJson = json.decode(response.body)["data"]; + return buildingsListJson.map((building) => Building.fromJson(building)).toList(); + } + return []; + } catch (error) { + return []; + } + } +} diff --git a/lib/modules/cleaner_module/provider/fms_floor_provider.dart b/lib/modules/cleaner_module/provider/fms_floor_provider.dart new file mode 100644 index 00000000..d3700761 --- /dev/null +++ b/lib/modules/cleaner_module/provider/fms_floor_provider.dart @@ -0,0 +1,85 @@ +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? _floors; + List? 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 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> 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 []; + } + } +} diff --git a/lib/modules/cleaner_module/provider/fms_location_description_provider.dart b/lib/modules/cleaner_module/provider/fms_location_description_provider.dart new file mode 100644 index 00000000..db799dc4 --- /dev/null +++ b/lib/modules/cleaner_module/provider/fms_location_description_provider.dart @@ -0,0 +1,140 @@ +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? locationDescriptions; + List? deptRoom; + + LocationDescriptionResponse({ + this.locationDescriptions, + this.deptRoom, + }); + + factory LocationDescriptionResponse.fromJson(Map 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 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? _locationDescriptions; + List? get locationDescriptions => _locationDescriptions; + + // contain dept room list + List? _deptRoomList; + List? 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 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 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; + } + } +} diff --git a/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart b/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart new file mode 100644 index 00000000..0e8d9de0 --- /dev/null +++ b/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart @@ -0,0 +1,334 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.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/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/new_views/app_style/app_color.dart'; +import 'package:test_sa/new_views/common_widgets/app_text_form_field.dart'; + +class CleanerChecklistWidget extends StatefulWidget { + final List checklistItems; + final Function(List)? onChecklistChanged; + final String? capturedImageBase64; + final Function(String?)? onImageCaptured; + + const CleanerChecklistWidget({ + Key? key, + required this.checklistItems, + this.onChecklistChanged, + this.capturedImageBase64, + this.onImageCaptured, + }) : super(key: key); + + @override + State createState() => _CleanerChecklistWidgetState(); +} + +class _CleanerChecklistWidgetState extends State { + @override + Widget build(BuildContext context) { + // Only show if checklist items are not empty + if (widget.checklistItems.isEmpty) { + return const SizedBox(); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Checklist'.addTranslation, + style: AppTextStyles.heading4.copyWith( + color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, + ), + ), + 16.height, + ...widget.checklistItems.map((item) => _buildChecklistItem(context, item)).toList(), + 16.height, + _buildImageCaptureWidget(context), + ], + ); + } + + Widget _buildChecklistItem(BuildContext context, ChecklistItem item) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Checkbox with title + Row( + children: [ + Checkbox( + value: item.isChecked, + activeColor: AppColor.primary10, + onChanged: (bool? value) { + 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( + children: [ + TextSpan( + text: item.title, + style: AppTextStyles.bodyText.copyWith( + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, + fontWeight: FontWeight.w600, + ), + ), + if (item.isMandatory) + TextSpan( + text: ' *', + style: AppTextStyles.bodyText.copyWith( + color: Colors.red, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ], + ), + + // Comment box - only visible when checked + if (item.isChecked) ...[ + 16.height, + AppTextFormField( + controller: item.commentController, + labelText: context.translation.comment, + backgroundColor: AppColor.fieldBgColor(context), + showShadow: false, + labelStyle: AppTextStyles.textFieldLabelStyle.copyWith( + color: context.isDark ? AppColor.white10 : AppColor.black10, + ), + alignLabelWithHint: true, + textInputType: TextInputType.multiline, + onChange: (value) { + item.comment = value; + // Notify parent of changes + widget.onChecklistChanged?.call(widget.checklistItems); + }, + ), + ], + ], + ).toShadowContainer(context).paddingOnly(bottom: 16); + } + + Widget _buildImageCaptureWidget(BuildContext context) { + // Base64 image display and capture widget + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Captured Image'.addTranslation, + style: AppTextStyles.heading6.copyWith( + color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, + ), + ), + 8.height, + GestureDetector( + onTap: () async { + // Image capture logic + final pickedFile = await ImagePicker().pickImage(source: ImageSource.camera); + 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); + } + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: _buildImageWidget(context), + ), + ), + ], + ).toShadowContainer(context); + } + + /// Build the image widget with loading indicator for network images + Widget _buildImageWidget(BuildContext context) { + if (widget.capturedImageBase64 == null || widget.capturedImageBase64!.isEmpty) { + // Show camera icon when no image + return Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration( + color: AppColor.fieldBgColor(context), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.camera_alt, + size: 48, + color: context.isDark ? AppColor.white10 : AppColor.black10, + ), + ); + } + + final imageString = widget.capturedImageBase64!; + + // First check if it's base64 encoded (contains | separator) + if (imageString.contains('|')) { + // Format: "filename|base64data" + try { + final base64Data = imageString.split('|').last; + return Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration( + color: AppColor.fieldBgColor(context), + borderRadius: BorderRadius.circular(8), + ), + child: Image.memory( + base64Decode(base64Data), + fit: BoxFit.cover, + ), + ); + } catch (e) { + debugPrint('Error decoding base64 image: $e'); + return _buildCameraIcon(context); + } + } + + // Check if it's a local file path + if (_isLocalUrl(imageString)) { + try { + return Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration( + color: AppColor.fieldBgColor(context), + borderRadius: BorderRadius.circular(8), + ), + child: Image.file( + File(imageString), + fit: BoxFit.cover, + ), + ); + } 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 + final imageUrl = URLs.getFileUrl(imageString); + if (imageUrl != null) { + return Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration( + color: AppColor.fieldBgColor(context), + borderRadius: BorderRadius.circular(8), + ), + child: 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); + }, + ), + ); + } + + return _buildCameraIcon(context); + } + + /// Build camera icon widget + Widget _buildCameraIcon(BuildContext context) { + return Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration( + color: AppColor.fieldBgColor(context), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.camera_alt, + size: 48, + color: context.isDark ? AppColor.white10 : AppColor.black10, + ), + ); + } + + /// Check if the URL is a local file path + /// Same logic as multi_image_picker_item.dart + bool _isLocalUrl(String url) { + if (url.isEmpty) return false; + return url.startsWith("/") || url.startsWith("file://") || (url.length > 1 && url.substring(1).startsWith(':\\')); + } + + @override + void dispose() { + // Note: Don't dispose controllers here as they're managed by ChecklistItem + super.dispose(); + } +} + +/// Model class for checklist items +class ChecklistItem { + final int id; + final String title; + bool isChecked; + String comment; + final bool isMandatory; + final TextEditingController commentController; + + ChecklistItem({ + required this.id, + required this.title, + required this.isChecked, + required this.comment, + this.isMandatory = false, + }) : commentController = TextEditingController(text: comment) { + // Update the controller text whenever comment changes + commentController.text = comment; + } + + // Convert to JSON for API submission + Map toJson() { + return { + 'id': id, + 'title': title, + 'isChecked': isChecked, + 'comment': commentController.text, + 'isMandatory': isMandatory, + }; + } + + void dispose() { + commentController.dispose(); + } +} diff --git a/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart b/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart index 43601cec..11853b27 100644 --- a/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart +++ b/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:provider/provider.dart'; @@ -7,13 +9,14 @@ import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.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/cleaner_module/widgets/cleaner_checklist_widget.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/total_working_time_detail_bottomsheet.dart'; import '../../../../../../controllers/providers/api/all_requests_provider.dart'; import 'components/room_tabs_widget.dart'; @@ -77,6 +80,23 @@ class _RecurrentWorkOrderViewState extends State { ], ) : const SizedBox(), + 16.height, + requestProvider.recurrentWoData!.planRecurrentTaskCheckLists != null && requestProvider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty + ? CleanerChecklistWidget( + 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, @@ -118,17 +138,26 @@ class _RecurrentWorkOrderViewState extends State { void _updateTask({required BuildContext context, required int status}) async { AllRequestsProvider allRequestsProvider = Provider.of(context, listen: false); - if (validate(model: allRequestsProvider.recurrentWoData!)) { + // Only validate checklist if it exists and status is complete + if (status == 1 && allRequestsProvider.recurrentWoData?.planRecurrentTaskCheckLists != null && allRequestsProvider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty) { + // Validate mandatory checklist items only + if (!_validateChecklist(allRequestsProvider.recurrentWoData!)) { + return; + } + + // Image validation is handled by backend - no need to force capture here + } + + if (validate(model: allRequestsProvider.recurrentWoData!, status: status)) { allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers ?? []; - showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading()); if (allRequestsProvider.recurrentWoData?.recurrentWoTimePicker != null) { 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(), // Handle potential null - endTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt?.toIso8601String(), // Handle potential null + startTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.startAt!.toIso8601String(), + endTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt?.toIso8601String(), workingHours: ((durationInSecond) / 60 / 60), ), ); @@ -138,33 +167,56 @@ void _updateTask({required BuildContext context, required int status}) async { allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add( PlanRecurrentTaskTimers( id: 0, - startTime: timer.startAt!.toIso8601String(), // Handle potential null - endTime: timer.endAt?.toIso8601String(), // Handle potential null + 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 .updateRecurrentWo( status: status, ) .then((success) { - Navigator.pop(context); + Navigator.pop(context); // Close loading dialog if (success) { if (status == 1) { - // when click complete then this request remove from the list and status changes to closed.. + // 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); } - allRequestsProvider.recurrentWoData?.recurrentWoTimerModel = null; - Navigator.pop(context); } }); } } -bool validate({required RecurrentWoData model}) { +bool _validateChecklist(RecurrentWoData model) { + // Check if all mandatory checklist items are checked + final uncheckedMandatory = model.planRecurrentTaskCheckLists!.where((item) => item.isMandatory && !item.isChecked).toList(); + + if (uncheckedMandatory.isNotEmpty) { + Fluttertoast.showToast( + msg: "Please complete all mandatory tasks (marked with *)", + toastLength: Toast.LENGTH_LONG, + ); + return false; + } + + return true; +} + +bool validate({required RecurrentWoData model, required int status}) { bool isTimerPickerEnable = ApiManager.instance.assetGroup?.enabledEngineerTimer ?? false; if (isTimerPickerEnable) { diff --git a/lib/new_views/app_style/app_text_styles.dart b/lib/new_views/app_style/app_text_styles.dart new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/lib/new_views/app_style/app_text_styles.dart @@ -0,0 +1 @@ + diff --git a/lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart b/lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart index b03643d2..0f63745e 100644 --- a/lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart +++ b/lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart @@ -1,16 +1,21 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:test_sa/controllers/providers/api/user_provider.dart'; +import 'package:test_sa/controllers/providers/settings/setting_provider.dart'; import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/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/enums/user_types.dart'; import 'package:test_sa/models/new_models/dashboard_detail.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/pm_module/recurrent_wo/recurrent_work_order_view.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/views/widgets/requests/request_status.dart'; - class RecurrentWoItemView extends StatelessWidget { final Data? requestData; final RequestsDetails? requestDetails; @@ -20,6 +25,9 @@ class RecurrentWoItemView extends StatelessWidget { @override Widget build(BuildContext context) { + final user = Provider.of(context, listen: false).user; + final settingProvider = Provider.of(context, listen: false); + final bool isFMSEngineer = user?.type == UsersTypes.engineer && settingProvider.isUserFMS; if (requestData != null) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -67,7 +75,19 @@ class RecurrentWoItemView extends StatelessWidget { ), ], ).toShadowContainer(context, withShadow: showShadow).onPress(() { - Navigator.of(context).push(MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: requestData!.id))); + if (isFMSEngineer && (requestData?.statusValue == 4)) { + 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) { @@ -117,6 +137,17 @@ class RecurrentWoItemView extends StatelessWidget { ), ], ).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))); }); } diff --git a/pubspec.lock b/pubspec.lock index 5880d411..4f2f810a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -593,14 +593,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.2.0" - flutter_vibrate: - dependency: "direct main" - description: - name: flutter_vibrate - sha256: "6eea0e94dd7dc8c7872e994a1139d55a8640e32fd5898ff59ee8bd53b8dea3b3" - url: "https://pub.dev" - source: hosted - version: "1.4.0" flutter_web_plugins: dependency: transitive description: flutter @@ -718,6 +710,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + haptic_feedback: + dependency: "direct main" + description: + name: haptic_feedback + sha256: dcc2494994c41428823f8f2082fd17a4e89e372bd142e07681420cbfaf99dcad + url: "https://pub.dev" + source: hosted + version: "0.6.4+3" hooks: dependency: transitive description: @@ -1002,10 +1002,10 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.19" material_color_utilities: dependency: transitive description: @@ -1074,10 +1074,10 @@ packages: dependency: "direct main" description: name: nfc_manager - sha256: "24c78b0e5702da53e7f8794d073624c0bee7cd99924f257cbd11f5d1c5866879" + sha256: "1b8d3788ed71bb34f59878a445e433891eb2268353289515c1542b2780151fe6" url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.2.1" objective_c: dependency: transitive description: @@ -1719,10 +1719,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.10" timezone: dependency: transitive description: @@ -1940,5 +1940,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.11.0 <4.0.0" - flutter: ">=3.38.5" + dart: ">=3.11.4 <4.0.0" + flutter: ">=3.38.4" From 023a47f69f05c51e0126711439076c28282d5d15 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Sun, 26 Apr 2026 16:18:07 +0300 Subject: [PATCH 02/17] recurrent task, cleaner improvements. --- lib/extensions/widget_extensions.dart | 3 +- .../pages/cleaner_search_details_view.dart | 94 ++--- .../widgets/cleaner_checklist_widget.dart | 352 +++++++++++------- .../components/task_info_widget.dart | 12 +- .../recurrent_work_order_view.dart | 134 ++++--- 5 files changed, 331 insertions(+), 264 deletions(-) diff --git a/lib/extensions/widget_extensions.dart b/lib/extensions/widget_extensions.dart index 7b468913..252971f7 100644 --- a/lib/extensions/widget_extensions.dart +++ b/lib/extensions/widget_extensions.dart @@ -15,6 +15,8 @@ extension WidgetExtensions on Widget { Widget get center => Center(child: this); + Widget ignorePointer(bool value) => IgnorePointer(ignoring: value, child: this); + BoxShadow get boxShadowR14 => BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 5, offset: const Offset(0, 0), spreadRadius: 0); Widget circle(double value) => ClipRRect(borderRadius: BorderRadius.circular(value), child: this); @@ -202,4 +204,3 @@ extension WidgetExtensions on Widget { extension DividerExtension on Divider { Divider defaultStyle(BuildContext context) => Divider(thickness: 1, color: context.isDarkNotListen ? AppColor.neutral20 : AppColor.neutral30); } - diff --git a/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart b/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart index 948688d0..65d7b525 100644 --- a/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart +++ b/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart @@ -70,7 +70,7 @@ class _CleanerSearchDetailsViewState extends State { floorName: recurrentData.floor?.name, departmentName: recurrentData.department?.name, roomName: recurrentData.room?.name, - locationDescription: null, // Not available in RecurrentWoData + locationDescription: null, createdDate: recurrentData.scheduleDate, ); } @@ -101,51 +101,40 @@ class _CleanerSearchDetailsViewState extends State { ), body: _isLoading ? const Center(child: CircularProgressIndicator()) - : Selector( - selector: (_, model) => model.isLoading, - builder: (_, isLoading, __) { - if (isLoading) { - return const Center(child: CircularProgressIndicator()); - } + : SafeArea( + child: Selector( + selector: (_, model) => model.isLoading, + builder: (_, isLoading, __) { + if (isLoading) { + return const Center(child: CircularProgressIndicator()); + } - return Column( - children: [ - Expanded( - child: SingleChildScrollView( - padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth), + return Column( + children: [ + SingleChildScrollView( + padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - 18.height, _buildCleanerRequestDetailCard(context), - 20.height, ], ), - ), - ), - SafeArea( - top: false, - right: false, - left: false, - child: FooterActionButton.footerContainer( + ).expanded, + FooterActionButton.footerContainer( context: context, child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (!_isQRScanned) + if (!_isQRScanned) ...[ Row( children: [ - Icon( - Icons.info_outline, - color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, - size: 15, - ), + Icon(Icons.info_outline_rounded, size: 16, color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), 8.width, Expanded( child: Text( 'By scanning the QR code you will accept this task'.addTranslation, - textAlign: TextAlign.center, + // textAlign: TextAlign.center, style: AppTextStyles.bodyText.copyWith( color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, ), @@ -153,7 +142,8 @@ class _CleanerSearchDetailsViewState extends State { ), ], ), - 16.height, + 8.height, + ], AppFilledButton( label: _isQRScanned ? 'Update' : 'Scan QR Code', maxWidth: true, @@ -206,12 +196,12 @@ class _CleanerSearchDetailsViewState extends State { }, ), ], - ).paddingOnly(start: 16, end: 16), - ), - ).toShadowContainer(context, padding: 0, showShadow: false, borderRadius: 0), - ], - ); - }, + ), + ).toShadowContainer(context, padding: 0, showShadow: false, borderRadius: 0), + ], + ); + }, + ), ), ); } @@ -248,11 +238,9 @@ class _CleanerSearchDetailsViewState extends State { ), if (taskResult.createdDate != null) Text( - taskResult.createdDate!.split('T').first, + taskResult.createdDate!.toServiceRequestCardFormat, textAlign: TextAlign.end, - style: AppTextStyles.tinyFont.copyWith( - color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, - ), + style: AppTextStyles.tinyFont.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50), ), ], ), @@ -260,51 +248,37 @@ class _CleanerSearchDetailsViewState extends State { if (taskResult.taskNo != null && taskResult.taskNo!.isNotEmpty) Text( 'Task No: ${taskResult.taskNo!}', - style: AppTextStyles.bodyText.copyWith( - color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, - ), + style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), ), if (taskResult.siteName != null && taskResult.siteName!.isNotEmpty) Text( 'Site: ${taskResult.siteName}', - style: AppTextStyles.bodyText.copyWith( - color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, - ), + style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), ), if (taskResult.buildingName != null && taskResult.buildingName!.isNotEmpty) Text( 'Building: ${taskResult.buildingName}', - style: AppTextStyles.bodyText.copyWith( - color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, - ), + style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), ), if (taskResult.floorName != null && taskResult.floorName!.isNotEmpty) Text( 'Floor: ${taskResult.floorName}', - style: AppTextStyles.bodyText.copyWith( - color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, - ), + style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), ), if (taskResult.departmentName != null && taskResult.departmentName!.isNotEmpty) Text( 'Department: ${taskResult.departmentName}', - style: AppTextStyles.bodyText.copyWith( - color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, - ), + style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), ), if (taskResult.roomName != null && taskResult.roomName!.isNotEmpty) Text( 'Room: ${taskResult.roomName}', - style: AppTextStyles.bodyText.copyWith( - color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, - ), + style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), ), if (taskResult.locationDescription != null && taskResult.locationDescription!.isNotEmpty) Text( 'Location: ${taskResult.locationDescription}', - style: AppTextStyles.bodyText.copyWith( - color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, - ), + style: AppTextStyles.bodyText.copyWith(color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), ), ], ).expanded, diff --git a/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart b/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart index 0e8d9de0..9f6f117b 100644 --- a/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart +++ b/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart @@ -12,20 +12,16 @@ 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 'package:test_sa/new_views/common_widgets/app_text_form_field.dart'; +import 'package:test_sa/views/widgets/buttons/app_back_button.dart'; class CleanerChecklistWidget extends StatefulWidget { final List checklistItems; final Function(List)? onChecklistChanged; final String? capturedImageBase64; final Function(String?)? onImageCaptured; + bool isClosed; - const CleanerChecklistWidget({ - Key? key, - required this.checklistItems, - this.onChecklistChanged, - this.capturedImageBase64, - this.onImageCaptured, - }) : super(key: key); + CleanerChecklistWidget({Key? key, required this.checklistItems, this.onChecklistChanged, this.capturedImageBase64, this.onImageCaptured, this.isClosed = false}) : super(key: key); @override State createState() => _CleanerChecklistWidgetState(); @@ -50,82 +46,88 @@ class _CleanerChecklistWidgetState extends State { ), 16.height, ...widget.checklistItems.map((item) => _buildChecklistItem(context, item)).toList(), - 16.height, + // 16.height, _buildImageCaptureWidget(context), ], ); } Widget _buildChecklistItem(BuildContext context, ChecklistItem item) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Checkbox with title - Row( - children: [ - Checkbox( - value: item.isChecked, - activeColor: AppColor.primary10, - onChanged: (bool? value) { - 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( - children: [ - TextSpan( - text: item.title, - style: AppTextStyles.bodyText.copyWith( - color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, - fontWeight: FontWeight.w600, - ), - ), - if (item.isMandatory) + return IgnorePointer( + ignoring: widget.isClosed, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Checkbox with title + Row( + children: [ + Checkbox( + // visualDensity: const VisualDensity(horizontal: -4.0, vertical: -4.0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: item.isChecked, + activeColor: AppColor.primary10, + onChanged: (bool? value) { + 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( + children: [ TextSpan( - text: ' *', + text: item.title, style: AppTextStyles.bodyText.copyWith( - color: Colors.red, - fontWeight: FontWeight.w600, + 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, + ), + ), + ], + ), ), ), - ), - ], - ), + ], + ), - // Comment box - only visible when checked - if (item.isChecked) ...[ - 16.height, - AppTextFormField( - controller: item.commentController, - labelText: context.translation.comment, - backgroundColor: AppColor.fieldBgColor(context), - showShadow: false, - labelStyle: AppTextStyles.textFieldLabelStyle.copyWith( - color: context.isDark ? AppColor.white10 : AppColor.black10, + // Comment box - only visible when checked + if (item.isChecked) ...[ + 16.height, + AppTextFormField( + controller: item.commentController, + labelText: context.translation.comment, + backgroundColor: AppColor.fieldBgColor(context), + showShadow: false, + labelStyle: AppTextStyles.textFieldLabelStyle.copyWith( + color: context.isDark ? AppColor.white10 : AppColor.black10, + ), + alignLabelWithHint: true, + textInputType: TextInputType.multiline, + onChange: (value) { + item.comment = value; + // Notify parent of changes + widget.onChecklistChanged?.call(widget.checklistItems); + }, ), - alignLabelWithHint: true, - textInputType: TextInputType.multiline, - onChange: (value) { - item.comment = value; - // Notify parent of changes - widget.onChecklistChanged?.call(widget.checklistItems); - }, - ), + ], ], - ], - ).toShadowContainer(context).paddingOnly(bottom: 16); + ).toShadowContainer(context).paddingOnly(bottom: 16), + ); } Widget _buildImageCaptureWidget(BuildContext context) { @@ -140,26 +142,7 @@ class _CleanerChecklistWidgetState extends State { ), ), 8.height, - GestureDetector( - onTap: () async { - // Image capture logic - final pickedFile = await ImagePicker().pickImage(source: ImageSource.camera); - 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); - } - }, - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: _buildImageWidget(context), - ), - ), + _buildImageWidget(context), ], ).toShadowContainer(context); } @@ -180,9 +163,23 @@ class _CleanerChecklistWidgetState extends State { 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) @@ -190,17 +187,37 @@ class _CleanerChecklistWidgetState extends State { // Format: "filename|base64data" try { final base64Data = imageString.split('|').last; - return Container( - height: 200, - width: double.infinity, - decoration: BoxDecoration( - color: AppColor.fieldBgColor(context), - borderRadius: BorderRadius.circular(8), - ), - child: Image.memory( - base64Decode(base64Data), - fit: BoxFit.cover, - ), + 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'); @@ -211,17 +228,37 @@ class _CleanerChecklistWidgetState extends State { // Check if it's a local file path if (_isLocalUrl(imageString)) { try { - return Container( - height: 200, - width: double.infinity, - decoration: BoxDecoration( - color: AppColor.fieldBgColor(context), - borderRadius: BorderRadius.circular(8), - ), - child: Image.file( - File(imageString), - fit: BoxFit.cover, - ), + 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'); @@ -232,34 +269,55 @@ class _CleanerChecklistWidgetState extends State { // If not local and not base64, it's a server filename - use NetworkImage with loading indicator final imageUrl = URLs.getFileUrl(imageString); if (imageUrl != null) { - return Container( - height: 200, - width: double.infinity, - decoration: BoxDecoration( - color: AppColor.fieldBgColor(context), - borderRadius: BorderRadius.circular(8), - ), - child: 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, + 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); + }, + ); + 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(), + ], + ), + ), + ), ), ); - }, - errorBuilder: (context, error, stackTrace) { - debugPrint('Error loading network image: $error'); - return _buildCameraIcon(context); - }, - ), + }), + _delete(), + ], ); } @@ -295,6 +353,26 @@ class _CleanerChecklistWidgetState extends State { // Note: Don't dispose controllers here as they're managed by ChecklistItem 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: const Icon(Icons.delete_rounded, color: Color(0xffF63939))) + .onPress(() { + widget.onImageCaptured?.call(null); + }); + } } /// Model class for checklist items diff --git a/lib/modules/pm_module/recurrent_wo/components/task_info_widget.dart b/lib/modules/pm_module/recurrent_wo/components/task_info_widget.dart index 7b7880ff..22a6477a 100644 --- a/lib/modules/pm_module/recurrent_wo/components/task_info_widget.dart +++ b/lib/modules/pm_module/recurrent_wo/components/task_info_widget.dart @@ -18,8 +18,9 @@ import 'package:test_sa/views/widgets/total_working_time_detail_bottomsheet.dart class RecurrentTaskInfoWidget extends StatefulWidget { final RecurrentWoData? model; + bool isClosed; - RecurrentTaskInfoWidget({super.key, required this.model}); + RecurrentTaskInfoWidget({super.key, required this.model, this.isClosed = false}); @override State createState() => _RecurrentTaskInfoWidgetState(); @@ -90,15 +91,16 @@ class _RecurrentTaskInfoWidgetState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - buildingInfoWidget(label: context.translation.department, value: widget.model!.department?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context), + buildingInfoWidget(label: context.translation.department, value: widget.model!.department?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context) + .ignorePointer(widget.isClosed), 8.height, - buildingInfoWidget(label: context.translation.floor, value: widget.model!.floor?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context), + buildingInfoWidget(label: context.translation.floor, value: widget.model!.floor?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context).ignorePointer(widget.isClosed), 8.height, - buildingInfoWidget(label: context.translation.room, value: widget.model!.room?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context), + buildingInfoWidget(label: context.translation.room, value: widget.model!.room?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context).ignorePointer(widget.isClosed), 8.height, _timerWidget(context, totalWorkingHours), 8.height, - _commentWidget(context: context, model: widget.model) + _commentWidget(context: context, model: widget.model).ignorePointer(widget.isClosed), ], // ], ).toShadowContainer(context, padding: 12) diff --git a/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart b/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart index 11853b27..800b19f0 100644 --- a/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart +++ b/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart @@ -50,84 +50,96 @@ class _RecurrentWorkOrderViewState extends State { super.dispose(); } + bool isClosed = false; + @override Widget build(BuildContext context) { return Consumer(builder: (context, requestProvider, child) { + isClosed = requestProvider.recurrentWoData?.status?.value == 1; return Scaffold( appBar: DefaultAppBar( title: context.translation.recurrentWo, - onWillPopScope: () { - _updateTask(context: context, status: 0); - }, + onWillPopScope: isClosed + ? null + : () { + _updateTask(context: context, status: 0); + }, ), body: allRequestsProvider!.isLoading ? const ALoading() : requestProvider.recurrentWoData != null - ? Column( - children: [ - SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - RecurrentTaskInfoWidget(model: requestProvider.recurrentWoData), - requestProvider.recurrentWoData!.planRecurrentMedicalTaskRooms != null && requestProvider.recurrentWoData!.planRecurrentMedicalTaskRooms!.isNotEmpty - ? Column( - children: [ - 8.height, - RoomTabsWidget(model: requestProvider.recurrentWoData), - ], - ) - : const SizedBox(), - 16.height, - requestProvider.recurrentWoData!.planRecurrentTaskCheckLists != null && requestProvider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty - ? CleanerChecklistWidget( - 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, - if (requestProvider.recurrentWoData?.status?.value != 1) ...[ - FooterActionButton.footerContainer( - context: context, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, + ? SafeArea( + child: Column( + children: [ + SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - AppFilledButton( - label: context.translation.save, - buttonColor: context.isDark ? AppColor.neutral20 : AppColor.white60, - textColor: context.isDark ? Colors.white : AppColor.black10, - onPressed: () => _updateTask(context: context, status: 0), - ).expanded, - 12.width, - AppFilledButton( - label: context.translation.complete, - buttonColor: AppColor.primary10, - // textColor: context.isDark?Colors.white: AppColor.black10, - onPressed: () => _updateTask(context: context, status: 1), - ).expanded, + RecurrentTaskInfoWidget(isClosed: isClosed,model: requestProvider.recurrentWoData), + IgnorePointer( + ignoring: isClosed, + child: requestProvider.recurrentWoData!.planRecurrentMedicalTaskRooms != null && requestProvider.recurrentWoData!.planRecurrentMedicalTaskRooms!.isNotEmpty + ? Column( + children: [ + 8.height, + RoomTabsWidget(model: requestProvider.recurrentWoData), + ], + ) + : 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, + if (requestProvider.recurrentWoData?.status?.value != 1) ...[ + FooterActionButton.footerContainer( + context: context, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + AppFilledButton( + label: context.translation.save, + buttonColor: context.isDark ? AppColor.neutral20 : AppColor.white60, + textColor: context.isDark ? Colors.white : AppColor.black10, + onPressed: () => _updateTask(context: context, status: 0), + ).expanded, + 12.width, + AppFilledButton( + label: context.translation.complete, + buttonColor: AppColor.primary10, + // textColor: context.isDark?Colors.white: AppColor.black10, + onPressed: () => _updateTask(context: context, status: 1), + ).expanded, + ], + ), + ), + ] + ], + ), ) : NoDataFound(message: context.translation.noDataFound).center, ).handlePopScope( cxt: context, + showPopUp: !isClosed, onSave: () { _updateTask(context: context, status: 0); }); From 8d60f4c81e3fac91ffeb8f341c2d8c12eb764be6 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Mon, 27 Apr 2026 10:38:18 +0300 Subject: [PATCH 03/17] improvements. --- .../pages/cleaner_search_details_view.dart | 106 +++++++++--------- .../pages/cleaner_search_list_view.dart | 5 +- 2 files changed, 54 insertions(+), 57 deletions(-) diff --git a/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart b/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart index 65d7b525..5d20de82 100644 --- a/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart +++ b/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart @@ -6,12 +6,14 @@ 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/ppm/recurrent_wo.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/app_lazy_loading.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/views/widgets/qr/scan_qr.dart'; +import 'package:test_sa/views/widgets/requests/request_status.dart'; import '../models/cleaner_task_search_result.dart'; import '../provider/cleaner_provider.dart'; import '../../pm_module/recurrent_wo/recurrent_work_order_view.dart'; @@ -34,8 +36,12 @@ class CleanerSearchDetailsView extends StatefulWidget { class _CleanerSearchDetailsViewState extends State { late CleanerProvider _cleanerProvider; - bool _isQRScanned = false; + + // bool _isQRScanned = false; + + bool _isRequestOpen = false; CleanerTaskSearchResult? _taskData; + RecurrentWoData? _recurrentWoData; bool _isLoading = false; @override @@ -61,17 +67,18 @@ class _CleanerSearchDetailsViewState extends State { // Map RecurrentWoData to CleanerTaskSearchResult if (allRequestsProvider.recurrentWoData != null) { - final recurrentData = allRequestsProvider.recurrentWoData!; + _recurrentWoData = allRequestsProvider.recurrentWoData!; + _isRequestOpen = _recurrentWoData?.status?.value == 4; _taskData = CleanerTaskSearchResult( - id: recurrentData.id, - taskNo: recurrentData.taskNo, - siteName: recurrentData.site?.siteName, - buildingName: recurrentData.building?.name, - floorName: recurrentData.floor?.name, - departmentName: recurrentData.department?.name, - roomName: recurrentData.room?.name, + id: _recurrentWoData!.id, + taskNo: _recurrentWoData!.taskNo, + siteName: _recurrentWoData!.site?.siteName, + buildingName: _recurrentWoData!.building?.name, + floorName: _recurrentWoData!.floor?.name, + departmentName: _recurrentWoData!.department?.name, + roomName: _recurrentWoData!.room?.name, locationDescription: null, - createdDate: recurrentData.scheduleDate, + createdDate: _recurrentWoData!.scheduleDate, ); } } catch (e) { @@ -126,7 +133,7 @@ class _CleanerSearchDetailsViewState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (!_isQRScanned) ...[ + if (_isRequestOpen) ...[ Row( children: [ Icon(Icons.info_outline_rounded, size: 16, color: context.isDark ? AppColor.neutral10 : AppColor.neutral120), @@ -135,7 +142,7 @@ class _CleanerSearchDetailsViewState extends State { child: Text( 'By scanning the QR code you will accept this task'.addTranslation, // textAlign: TextAlign.center, - style: AppTextStyles.bodyText.copyWith( + style: AppTextStyles.bodyText2.copyWith( color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, ), ), @@ -145,53 +152,27 @@ class _CleanerSearchDetailsViewState extends State { 8.height, ], AppFilledButton( - label: _isQRScanned ? 'Update' : 'Scan QR Code', + label: _isRequestOpen ? 'Scan QR Code' : 'Update', maxWidth: true, buttonColor: AppColor.primary10, onPressed: () async { - if (_isQRScanned) { - // If QR is already scanned, navigate to recurrent work order view - 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 (_isRequestOpen) { + final result = await Navigator.push(context, MaterialPageRoute(builder: (context) => const ScanQr())); if (result != null && result is String && result.isNotEmpty) { - // Show loading indicator - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => const AppLazyLoading(), - ); - - // Call the API to check if QR matches the same room - final isValid = await CleanerProvider().checkQRSameRoom( - planRecurrentTaskId: widget.requestId, - qrCode: result, - ); - - // Close loading dialog + showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading()); + final isValid = await CleanerProvider().checkQRSameRoom(planRecurrentTaskId: widget.requestId, qrCode: result); if (mounted) Navigator.pop(context); if (isValid) { - // Update state to show "Update" button instead of navigating if (mounted) { - setState(() { - _isQRScanned = true; - }); + getInitialData(); } } } + } else { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: widget.requestId)), + ); } }, ), @@ -230,12 +211,24 @@ class _CleanerSearchDetailsViewState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - 'Request Information', - style: AppTextStyles.heading4.copyWith( - color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, + // Text( + // 'Request Information', + // style: AppTextStyles.heading4.copyWith( + // 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) Text( taskResult.createdDate!.toServiceRequestCardFormat, @@ -245,6 +238,13 @@ class _CleanerSearchDetailsViewState extends State { ], ), 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) Text( 'Task No: ${taskResult.taskNo!}', diff --git a/lib/modules/cleaner_module/pages/cleaner_search_list_view.dart b/lib/modules/cleaner_module/pages/cleaner_search_list_view.dart index 884ba220..87e25e22 100644 --- a/lib/modules/cleaner_module/pages/cleaner_search_list_view.dart +++ b/lib/modules/cleaner_module/pages/cleaner_search_list_view.dart @@ -167,10 +167,7 @@ class _CleanerSearchListViewState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => CleanerSearchDetailsView( - requestId: result.id ?? 0, - taskResult: result, - ), + builder: (context) => CleanerSearchDetailsView(requestId: result.id ?? 0, taskResult: null), ), ); }, From d242b190ce436081cefdbece17c98cf88cf23c60 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Mon, 27 Apr 2026 16:31:54 +0300 Subject: [PATCH 04/17] improvements. --- .../pages/cleaner_search_details_view.dart | 6 +- .../widgets/cleaner_checklist_widget.dart | 62 +++++++++++-------- .../components/task_info_widget.dart | 30 ++++++--- .../recurrent_work_order_view.dart | 5 +- 4 files changed, 63 insertions(+), 40 deletions(-) diff --git a/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart b/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart index 5d20de82..03e2ee38 100644 --- a/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart +++ b/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart @@ -165,7 +165,11 @@ class _CleanerSearchDetailsViewState extends State { if (isValid) { if (mounted) { - getInitialData(); + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: widget.requestId)), + ); + + // getInitialData(); } } } diff --git a/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart b/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart index 9f6f117b..ad3cb08b 100644 --- a/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart +++ b/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart @@ -45,8 +45,15 @@ class _CleanerChecklistWidgetState extends State { ), ), 16.height, - ...widget.checklistItems.map((item) => _buildChecklistItem(context, item)).toList(), - // 16.height, + ListView.separated( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (cxt, index) => _buildChecklistItem(context, widget.checklistItems[index]), + separatorBuilder: (cxt, index) => const Divider().defaultStyle(context), + itemCount: widget.checklistItems.length) + .toShadowContainer(context, padding: 16), + 16.height, _buildImageCaptureWidget(context), ], ); @@ -59,28 +66,21 @@ class _CleanerChecklistWidgetState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - // Checkbox with title - Row( - children: [ - Checkbox( - // visualDensity: const VisualDensity(horizontal: -4.0, vertical: -4.0), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ListTileTheme( + horizontalTitleGap: 0, + child: Container( + alignment: Alignment.center, + height: 50.toScreenHeight, + child: CheckboxListTile( value: item.isChecked, + 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, - onChanged: (bool? value) { - 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( + title: RichText( text: TextSpan( children: [ TextSpan( @@ -101,13 +101,21 @@ class _CleanerChecklistWidgetState extends State { ], ), ), + onChanged: (bool? value) { + setState(() { + item.isChecked = value ?? false; + if (!item.isChecked) { + item.commentController.clear(); + } + }); + // Notify parent of changes + widget.onChecklistChanged?.call(widget.checklistItems); + }, ), - ], + ), ), - - // Comment box - only visible when checked if (item.isChecked) ...[ - 16.height, + // 16.height, AppTextFormField( controller: item.commentController, labelText: context.translation.comment, @@ -126,7 +134,7 @@ class _CleanerChecklistWidgetState extends State { ), ], ], - ).toShadowContainer(context).paddingOnly(bottom: 16), + ), ); } diff --git a/lib/modules/pm_module/recurrent_wo/components/task_info_widget.dart b/lib/modules/pm_module/recurrent_wo/components/task_info_widget.dart index 22a6477a..d49be1bc 100644 --- a/lib/modules/pm_module/recurrent_wo/components/task_info_widget.dart +++ b/lib/modules/pm_module/recurrent_wo/components/task_info_widget.dart @@ -19,8 +19,9 @@ import 'package:test_sa/views/widgets/total_working_time_detail_bottomsheet.dart class RecurrentTaskInfoWidget extends StatefulWidget { final RecurrentWoData? model; bool isClosed; + bool isHouseKeeper; - RecurrentTaskInfoWidget({super.key, required this.model, this.isClosed = false}); + RecurrentTaskInfoWidget({super.key, required this.model, this.isClosed = false, this.isHouseKeeper = false}); @override State createState() => _RecurrentTaskInfoWidgetState(); @@ -81,6 +82,11 @@ class _RecurrentTaskInfoWidgetState extends State { '${context.translation.taskNo}: ${widget.model!.taskNo!}'.bodyText2(context).custom(color: AppColor.neutral120), 'Plan Name: ${widget.model?.planRecurrent?.planName ?? "-"}'.bodyText2(context).custom(color: AppColor.neutral120), '${context.translation.site}: ${widget.model!.site!.siteName!}'.bodyText2(context).custom(color: AppColor.neutral120), + 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), + ], '${context.translation.assignedEmployee}: ${widget.model!.engineer!.userName ?? ""}'.bodyText2(context).custom(color: AppColor.neutral120), '${context.translation.scheduledDate}: ${widget.model!.scheduleDate!.toMonthYearFormat}'.bodyText2(context).custom(color: AppColor.neutral120), ], @@ -91,16 +97,20 @@ class _RecurrentTaskInfoWidgetState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - buildingInfoWidget(label: context.translation.department, value: widget.model!.department?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context) - .ignorePointer(widget.isClosed), - 8.height, - buildingInfoWidget(label: context.translation.floor, value: widget.model!.floor?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context).ignorePointer(widget.isClosed), - 8.height, - buildingInfoWidget(label: context.translation.room, value: widget.model!.room?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context).ignorePointer(widget.isClosed), - 8.height, + if (!widget.isHouseKeeper) ...[ + buildingInfoWidget(label: context.translation.department, value: widget.model!.department?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context) + .ignorePointer(widget.isClosed), + 8.height, + buildingInfoWidget(label: context.translation.floor, value: widget.model!.floor?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context).ignorePointer(widget.isClosed), + 8.height, + buildingInfoWidget(label: context.translation.room, value: widget.model!.room?.name?.cleanupWhitespace.capitalizeFirstOfEach, context: context).ignorePointer(widget.isClosed), + 8.height, + ], _timerWidget(context, totalWorkingHours), - 8.height, - _commentWidget(context: context, model: widget.model).ignorePointer(widget.isClosed), + if (!widget.isHouseKeeper) ...[ + 8.height, + _commentWidget(context: context, model: widget.model).ignorePointer(widget.isClosed), + ], ], // ], ).toShadowContainer(context, padding: 12) diff --git a/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart b/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart index 800b19f0..8f99d03c 100644 --- a/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart +++ b/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart @@ -56,6 +56,7 @@ class _RecurrentWorkOrderViewState extends State { Widget build(BuildContext context) { return Consumer(builder: (context, requestProvider, child) { isClosed = requestProvider.recurrentWoData?.status?.value == 1; + bool isHouseKeeper = requestProvider.recurrentWoData!.planRecurrentTaskCheckLists != null && requestProvider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty; return Scaffold( appBar: DefaultAppBar( title: context.translation.recurrentWo, @@ -77,7 +78,7 @@ class _RecurrentWorkOrderViewState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - RecurrentTaskInfoWidget(isClosed: isClosed,model: requestProvider.recurrentWoData), + RecurrentTaskInfoWidget(isClosed: isClosed, model: requestProvider.recurrentWoData, isHouseKeeper: isHouseKeeper), IgnorePointer( ignoring: isClosed, child: requestProvider.recurrentWoData!.planRecurrentMedicalTaskRooms != null && requestProvider.recurrentWoData!.planRecurrentMedicalTaskRooms!.isNotEmpty @@ -90,7 +91,7 @@ class _RecurrentWorkOrderViewState extends State { : const SizedBox(), ), 16.height, - requestProvider.recurrentWoData!.planRecurrentTaskCheckLists != null && requestProvider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty + isHouseKeeper ? CleanerChecklistWidget( isClosed: isClosed, checklistItems: requestProvider.recurrentWoData!.planRecurrentTaskCheckLists ?? [], From 515da156b2b932f29b9aaed22ad2265f34b75ace Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Tue, 28 Apr 2026 09:14:06 +0300 Subject: [PATCH 05/17] improvements. --- .../widgets/cleaner_checklist_widget.dart | 64 ++++++++++++++++--- .../recurrent_work_order_view.dart | 17 +++-- 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart b/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart index ad3cb08b..9ec79ab6 100644 --- a/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart +++ b/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart @@ -50,7 +50,7 @@ class _CleanerChecklistWidgetState extends State { padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), itemBuilder: (cxt, index) => _buildChecklistItem(context, widget.checklistItems[index]), - separatorBuilder: (cxt, index) => const Divider().defaultStyle(context), + 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, @@ -66,13 +66,63 @@ class _CleanerChecklistWidgetState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - ListTileTheme( + Row( + children: [ + AbsorbPointer( + absorbing: true, + 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( - alignment: Alignment.center, - height: 50.toScreenHeight, + color: Colors.amberAccent, child: CheckboxListTile( value: item.isChecked, + minVerticalPadding: 0, controlAffinity: ListTileControlAffinity.leading, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, contentPadding: EdgeInsets.zero, @@ -108,14 +158,13 @@ class _CleanerChecklistWidgetState extends State { item.commentController.clear(); } }); - // Notify parent of changes widget.onChecklistChanged?.call(widget.checklistItems); }, ), ), - ), + ),*/ if (item.isChecked) ...[ - // 16.height, + 12.height, AppTextFormField( controller: item.commentController, labelText: context.translation.comment, @@ -128,7 +177,6 @@ class _CleanerChecklistWidgetState extends State { textInputType: TextInputType.multiline, onChange: (value) { item.comment = value; - // Notify parent of changes widget.onChecklistChanged?.call(widget.checklistItems); }, ), diff --git a/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart b/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart index 8f99d03c..3b1a0a30 100644 --- a/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart +++ b/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart @@ -186,14 +186,19 @@ void _updateTask({required BuildContext context, required int status}) async { ), ); }); - log('payload for update is ${allRequestsProvider.recurrentWoData?.toJson()}'); + + final uniqueTimersMap = {}; + allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.forEach((timer) { + if (timer.startTime != null && timer.endTime != null) { + final key = '${timer.id}_${timer.startTime}_${timer.endTime}'; + uniqueTimersMap[key] = timer; + } + }); + + allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = uniqueTimersMap.values.toList(); showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading()); - await allRequestsProvider - .updateRecurrentWo( - status: status, - ) - .then((success) { + await allRequestsProvider.updateRecurrentWo(status: status).then((success) { Navigator.pop(context); // Close loading dialog if (success) { if (status == 1) { From d1a94df33d5e9cf9b380ba9e968a0ce8e43eca23 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Tue, 28 Apr 2026 10:11:23 +0300 Subject: [PATCH 06/17] improvements. --- lib/controllers/providers/api/all_requests_provider.dart | 9 ++++----- .../cleaner_module/pages/cleaner_request_item_view.dart | 3 ++- .../tm_module/device_transfer/device_filter_screen.dart | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/controllers/providers/api/all_requests_provider.dart b/lib/controllers/providers/api/all_requests_provider.dart index de05fc92..435af277 100644 --- a/lib/controllers/providers/api/all_requests_provider.dart +++ b/lib/controllers/providers/api/all_requests_provider.dart @@ -273,21 +273,20 @@ class AllRequestsProvider extends ChangeNotifier { } Future updateRecurrentWo({required int status}) async { - isLoading = true; Response response; try { response = await ApiManager.instance.post(URLs.updateRecurrentPlanUrl, body: recurrentWoData!.toJson(status: status)); stateCode = response.statusCode; - isLoading = false; - notifyListeners(); + // isLoading = false; + // notifyListeners(); if (stateCode == 200) { return true; } return false; } catch (error) { - isLoading = false; + // isLoading = false; stateCode = -1; - notifyListeners(); + // notifyListeners(); return false; } } diff --git a/lib/modules/cleaner_module/pages/cleaner_request_item_view.dart b/lib/modules/cleaner_module/pages/cleaner_request_item_view.dart index 96d0eb05..0ffc8e96 100644 --- a/lib/modules/cleaner_module/pages/cleaner_request_item_view.dart +++ b/lib/modules/cleaner_module/pages/cleaner_request_item_view.dart @@ -1,6 +1,7 @@ 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/new_views/app_style/app_color.dart'; @@ -26,7 +27,7 @@ class CleanerRequestItemView extends StatelessWidget { if (result.taskNo != null && result.taskNo!.isNotEmpty) result.taskNo!.heading5(context), if (result.createdDate != null) Text( - result.createdDate!.split('T').first, + result.createdDate!.toServiceRequestCardFormat, textAlign: TextAlign.end, style: AppTextStyles.tinyFont.copyWith( color: context.isDark ? AppColor.neutral10 : AppColor.neutral50, diff --git a/lib/modules/tm_module/device_transfer/device_filter_screen.dart b/lib/modules/tm_module/device_transfer/device_filter_screen.dart index b0cf860f..3c89a299 100644 --- a/lib/modules/tm_module/device_transfer/device_filter_screen.dart +++ b/lib/modules/tm_module/device_transfer/device_filter_screen.dart @@ -126,7 +126,7 @@ class _DeviceFilterScreenState extends State { Row( children: [ ADatePicker( - label: context.translation.to, + label: context.translation.from, from: DateTime(2000), date: DateTime.tryParse(startDate ?? ""), onDatePicker: (date) { From d3dadb70c3e5b5ad243b00ea546d248f585f229f Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Tue, 5 May 2026 11:05:08 +0300 Subject: [PATCH 07/17] improvements --- .../providers/api/all_requests_provider.dart | 27 +- .../providers/api/user_provider.dart | 1 + lib/dashboard_latest/dashboard_provider.dart | 10 + .../widgets/progress_fragment.dart | 11 +- .../widgets/request_category_fragment.dart | 6 +- lib/models/all_requests_and_count_model.dart | 79 +-- lib/models/enums/user_types.dart | 3 +- lib/models/new_models/dashboard_count.dart | 5 +- lib/models/new_models/dashboard_detail.dart | 28 +- lib/models/user.dart | 2 + .../pages/cleaner_search_details_view.dart | 145 +++++- .../pages/cleaner_search_form_view.dart | 20 +- .../provider/cleaner_provider.dart | 44 ++ .../widgets/cleaner_preview_dialog.dart | 457 ++++++++++++++++++ .../components/task_info_widget.dart | 117 ++++- .../recurrent_work_order_view.dart | 288 ++++++++--- .../my_request/all_requests_filter_page.dart | 17 +- .../my_request/my_requests_page.dart | 8 + .../requests/recurrent_wo_item_view.dart | 23 +- lib/views/widgets/dialogs/confirm_dialog.dart | 1 + 20 files changed, 1141 insertions(+), 151 deletions(-) create mode 100644 lib/modules/cleaner_module/widgets/cleaner_preview_dialog.dart diff --git a/lib/controllers/providers/api/all_requests_provider.dart b/lib/controllers/providers/api/all_requests_provider.dart index 435af277..c3d244d3 100644 --- a/lib/controllers/providers/api/all_requests_provider.dart +++ b/lib/controllers/providers/api/all_requests_provider.dart @@ -176,6 +176,10 @@ class AllRequestsProvider extends ChangeNotifier { // list = [16]; list.add(16); } + if ( context.userProvider.isHouseKeeper) { + list = [5]; + } + return list; } @@ -272,22 +276,27 @@ class AllRequestsProvider extends ChangeNotifier { } } - Future updateRecurrentWo({required int status}) async { + Future> updateRecurrentWo({required int status, bool showToast = true}) async { Response response; 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; - // isLoading = false; - // notifyListeners(); + 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) { - // isLoading = false; stateCode = -1; - // notifyListeners(); - return false; + return {'success': false, 'message': error.toString()}; } } diff --git a/lib/controllers/providers/api/user_provider.dart b/lib/controllers/providers/api/user_provider.dart index ec6f3242..846282cf 100644 --- a/lib/controllers/providers/api/user_provider.dart +++ b/lib/controllers/providers/api/user_provider.dart @@ -51,6 +51,7 @@ class UserProvider extends ChangeNotifier { bool get isQualityUser => user!.type == UsersTypes.qualityUser; bool get isCommissioningEngineer => user!.type == UsersTypes.commissioningEngineer; + bool get isHouseKeeper => user!.type == UsersTypes.houseKeeper; VerifyOtpModel _verifyOtpModel = VerifyOtpModel(); SwipeTransaction _swipeTransactionModel = SwipeTransaction(); diff --git a/lib/dashboard_latest/dashboard_provider.dart b/lib/dashboard_latest/dashboard_provider.dart index 03ca5104..92ecd60e 100644 --- a/lib/dashboard_latest/dashboard_provider.dart +++ b/lib/dashboard_latest/dashboard_provider.dart @@ -118,6 +118,7 @@ class DashBoardProvider extends ChangeNotifier { UsersTypes.qualityUser: URLs.qaAuditorCount, UsersTypes.commissioningEngineer: URLs.commissioningEngineerCount, UsersTypes.engineer: URLs.engineerDashboardCountUrl, + UsersTypes.houseKeeper: URLs.engineerDashboardCountUrl, UsersTypes.assessor: URLs.engineerDashboardCountUrl, UsersTypes.assessorTl: URLs.engineerDashboardCountUrl, }; @@ -257,6 +258,7 @@ class DashBoardProvider extends ChangeNotifier { case UsersTypes.engineer: case UsersTypes.assessor: case UsersTypes.assessorTl: + case UsersTypes.houseKeeper: if (status == 0) { return URLs.engineerDashboardUpcoming; } else if (status == 1 && tabId == 1) { @@ -413,6 +415,14 @@ class CategoryTabs { tabs.add(CategoryTabs(label: 'Completed', tag: 3, id: 4)); 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: 'Closed', tag: 4, id: 3)); + return tabs; + } if (userType == UsersTypes.qualityUser) { tabs.add(CategoryTabs(label: 'Open', tag: 1, id: 2)); diff --git a/lib/dashboard_latest/widgets/progress_fragment.dart b/lib/dashboard_latest/widgets/progress_fragment.dart index 4ab32aab..ed7443c2 100644 --- a/lib/dashboard_latest/widgets/progress_fragment.dart +++ b/lib/dashboard_latest/widgets/progress_fragment.dart @@ -183,12 +183,19 @@ class ProgressFragment extends StatelessWidget { ChartData('In Progress', snapshot.dashboardCount?.data?.countInprogress?.toDouble() ?? 0.0, AppColor.orange70), ]; } 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('Closed', snapshot.dashboardCount?.data?.countComplete?.toDouble() ?? 0.0, AppColor.green70), + ChartData('Closed', snapshot.dashboardCount?.data?.countClose?.toDouble() ?? 0.0, AppColor.green70), ChartData('Open', snapshot.dashboardCount?.data?.countOpen?.toDouble() ?? 0.0, AppColor.blueStatus(context)), ChartData('In Progress', snapshot.dashboardCount?.data?.countInprogress?.toDouble() ?? 0.0, AppColor.orange70), ]; + } 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('Closed', snapshot.dashboardCount?.data?.countClose?.toDouble() ?? 0.0, AppColor.green70), + // ChartData('Open', snapshot.dashboardCount?.data?.countOpen?.toDouble() ?? 0.0, AppColor.blueStatus(context)), + ]; } else { total = (snapshot.dashboardCount?.data?.countComplete ?? 0) + (snapshot.dashboardCount?.data?.countInprogress ?? 0) + (snapshot.dashboardCount?.data?.countOpen ?? 0); chartData = [ diff --git a/lib/dashboard_latest/widgets/request_category_fragment.dart b/lib/dashboard_latest/widgets/request_category_fragment.dart index 2b70da8f..54c7f1a8 100644 --- a/lib/dashboard_latest/widgets/request_category_fragment.dart +++ b/lib/dashboard_latest/widgets/request_category_fragment.dart @@ -24,8 +24,8 @@ class RequestCategoryFragment extends StatelessWidget { final user = Provider.of(context, listen: false).user; final settingProvider = Provider.of(context, listen: false); - // Check if user is FMS Engineer using SettingProvider - final bool isFMSEngineer = user?.type == UsersTypes.engineer && settingProvider.isUserFMS; + // Check if user is houseKeeper + final bool isHouseKeeper = user?.type == UsersTypes.houseKeeper; return Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ //TODO uncommit this to get the calendar card.. @@ -53,7 +53,7 @@ class RequestCategoryFragment extends StatelessWidget { : const SizedBox(), ], ).paddingOnly(start: 14, end: 12) - : isFMSEngineer + : isHouseKeeper ? Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/models/all_requests_and_count_model.dart b/lib/models/all_requests_and_count_model.dart index 09bfc03f..ba261802 100644 --- a/lib/models/all_requests_and_count_model.dart +++ b/lib/models/all_requests_and_count_model.dart @@ -134,36 +134,47 @@ class RequestsDetails { int? transactionType; int? numberOfAssets; int? numberOfSites; + String? siteName; + String? buildingName; + String? departmentName; + String? floorName; + String? roomName; - RequestsDetails( - {this.id, - this.nameOfType, - this.priority, - this.status, - this.statusValue, - this.assetName, - this.assetNo, - this.assetSN, - this.model, - this.supplier, - this.manufacturer, - this.requestType, - this.requestNo, - this.numberOfAssets, - this.numberOfSites, - this.sessionType, - this.gasType, - this.site, - this.statusReceiver, - this.assetTransferFrom, - this.assetTransferTo, - this.code, - this.siteTransferFrom, - this.siteTransferTo, - this.poInternal, - this.poExternal, - this.date, - this.transactionType}); + RequestsDetails({ + this.id, + this.nameOfType, + this.priority, + this.status, + this.statusValue, + this.assetName, + this.assetNo, + this.assetSN, + this.model, + this.supplier, + this.manufacturer, + this.requestType, + this.requestNo, + this.numberOfAssets, + this.numberOfSites, + this.sessionType, + this.gasType, + this.site, + this.statusReceiver, + this.assetTransferFrom, + this.assetTransferTo, + this.code, + this.siteTransferFrom, + this.siteTransferTo, + this.poInternal, + this.poExternal, + this.date, + this.transactionType, + this.siteName, + this.buildingName, + this.departmentName, + this.floorName, + this.roomName, + }); RequestsDetails.fromJson(Map json) { id = json['id']; @@ -194,6 +205,11 @@ class RequestsDetails { siteTransferFrom = json['siteTransferFrom']; siteTransferTo = json['siteTransferTo']; transactionType = json['transactionType']; + siteName = json['siteName']; + buildingName = json['buildingName']; + departmentName = json['departmentName']; + floorName = json['floorName']; + roomName = json['roomName']; } Map toJson() { @@ -224,6 +240,11 @@ class RequestsDetails { data['siteTransferFrom'] = siteTransferFrom; data['siteTransferTo'] = siteTransferTo; data['transactionType'] = transactionType; + data['siteName'] = siteName; + data['buildingName'] = buildingName; + data['departmentName'] = departmentName; + data['floorName'] = floorName; + data['roomName'] = roomName; return data; } } diff --git a/lib/models/enums/user_types.dart b/lib/models/enums/user_types.dart index 13238d83..c807669f 100644 --- a/lib/models/enums/user_types.dart +++ b/lib/models/enums/user_types.dart @@ -5,5 +5,6 @@ enum UsersTypes { assessor, assessorTl, qualityUser, - commissioningEngineer + commissioningEngineer, + houseKeeper } diff --git a/lib/models/new_models/dashboard_count.dart b/lib/models/new_models/dashboard_count.dart index 55bafffa..5c6fbcf8 100644 --- a/lib/models/new_models/dashboard_count.dart +++ b/lib/models/new_models/dashboard_count.dart @@ -37,9 +37,10 @@ class Data { int? countComplete; int? countAcknowledge; int? countHighPriority; + int? countClose; int? countOverdue; - Data({this.countOpen, this.countInprogress, this.countComplete, this.countAcknowledge, this.countHighPriority, this.countOverdue}); + Data({this.countOpen, this.countInprogress, this.countClose,this.countComplete, this.countAcknowledge, this.countHighPriority, this.countOverdue}); Data.fromJson(Map json) { countOpen = json['countOpen'] ?? 0; @@ -47,6 +48,7 @@ class Data { countComplete = json['countComplete'] ?? 0; countAcknowledge = json['countAcknowledge'] ?? 0; countHighPriority = json['countHighPriority'] ?? 0; + countClose = json['countClose'] ?? 0; countOverdue = json['countOverdue'] ?? 0; } @@ -57,6 +59,7 @@ class Data { data['countComplete'] = countComplete; data['countAcknowledge'] = countAcknowledge; data['countHighPriority'] = countHighPriority; + data['countClose'] = countClose; data['countOverdue'] = countOverdue; return data; } diff --git a/lib/models/new_models/dashboard_detail.dart b/lib/models/new_models/dashboard_detail.dart index d351337f..9c00cbbb 100644 --- a/lib/models/new_models/dashboard_detail.dart +++ b/lib/models/new_models/dashboard_detail.dart @@ -63,6 +63,11 @@ class Data { int? numberOfSites; int? numberOfAssets; String? nameOfType; + String? siteName; + String? buildingName; + String? floorName; + String? departmentName; + String? roomName; Data( {this.id, @@ -80,10 +85,15 @@ class Data { this.assetNumber, this.requestTypeName, this.requestNo, - this.requestType, - this.poExternal, - this.poInternal, - this.transactionNo}); + this.requestType, + this.poExternal, + this.poInternal, + this.transactionNo, + this.siteName, + this.departmentName, + this.buildingName, + this.floorName, + this.roomName}); Data.fromJson(Map json) { id = json['id']; @@ -106,6 +116,11 @@ class Data { poInternal = json['poInternal']; poExternal = json['poExternal']; nameOfType = json['nameOfType']; + siteName = json['siteName']; + departmentName = json['departmentName']; + buildingName = json['buildingName']; + floorName = json['floorName']; + roomName = json['roomName']; } Map toJson() { @@ -127,6 +142,11 @@ class Data { data['numberOfAssets'] = numberOfAssets; data['numberOfSites'] = numberOfSites; data['nameOfType'] = nameOfType; + data['siteName'] = siteName; + data['buildingName'] = buildingName; + data['departmentName'] = departmentName; + data['floorName'] = floorName; + data['roomName'] = roomName; return data; } } diff --git a/lib/models/user.dart b/lib/models/user.dart index 17da87c9..9bca631d 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -117,6 +117,8 @@ class User { return UsersTypes.qualityUser; case "R-201": // Quality User return UsersTypes.commissioningEngineer; + case "R-204": // Quality User + return UsersTypes.houseKeeper; default: return null; } diff --git a/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart b/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart index 03e2ee38..6bbf5ef1 100644 --- a/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart +++ b/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart @@ -1,4 +1,5 @@ // UI: Cleaner Search Details View +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:test_sa/extensions/context_extension.dart'; @@ -18,6 +19,10 @@ import '../models/cleaner_task_search_result.dart'; import '../provider/cleaner_provider.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/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 { static const String routeName = '/cleaner_search_details'; @@ -62,12 +67,11 @@ class _CleanerSearchDetailsViewState extends State { }); try { - final allRequestsProvider = Provider.of(context, listen: false); - await allRequestsProvider.getRecurrentWoById(id: widget.requestId); + // Use direct API call to get better error messages + final result = await _getRecurrentWoByIdWithMessage(widget.requestId); - // Map RecurrentWoData to CleanerTaskSearchResult - if (allRequestsProvider.recurrentWoData != null) { - _recurrentWoData = allRequestsProvider.recurrentWoData!; + if (result["success"] == true && result["data"] != null) { + _recurrentWoData = result["data"]; _isRequestOpen = _recurrentWoData?.status?.value == 4; _taskData = CleanerTaskSearchResult( id: _recurrentWoData!.id, @@ -80,16 +84,31 @@ class _CleanerSearchDetailsViewState extends State { locationDescription: null, 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) { debugPrint('Error fetching recurrent work order: $e'); + if (mounted) { + _showErrorDialog(context, "Failed to load request details. Please check your connection and try again."); + } } finally { setState(() { _isLoading = false; }); } } 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."); + } + } } } @@ -160,16 +179,32 @@ class _CleanerSearchDetailsViewState extends State { final result = await Navigator.push(context, MaterialPageRoute(builder: (context) => const ScanQr())); if (result != null && result is String && result.isNotEmpty) { showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading()); - final isValid = await CleanerProvider().checkQRSameRoom(planRecurrentTaskId: widget.requestId, qrCode: result); - if (mounted) Navigator.pop(context); - if (isValid) { - if (mounted) { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: widget.requestId)), - ); + try { + final qrResult = await CleanerProvider().checkQRSameRoomWithMessage(planRecurrentTaskId: widget.requestId, qrCode: result); + if (mounted) Navigator.pop(context); - // getInitialData(); + final isValid = qrResult["success"] ?? false; + final backendMessage = qrResult["message"] ?? "Unknown error occurred"; + + if (isValid) { + 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) { + // Extract actual error message from the exception or use generic fallback + String errorMessage = _extractErrorMessage(e) ?? 'Failed to validate QR code. Please try again.'; + _showErrorDialog(context, errorMessage); } } } @@ -293,4 +328,86 @@ class _CleanerSearchDetailsViewState extends State { ], ).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) { + // 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> _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); } diff --git a/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart b/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart index bb9fb739..2ed65884 100644 --- a/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart +++ b/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart @@ -21,6 +21,7 @@ import 'package:test_sa/providers/loading_list_notifier.dart'; import 'package:test_sa/models/new_models/building.dart'; import 'package:test_sa/models/new_models/floor.dart'; import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart'; class CleanerSearchFormView extends StatefulWidget { static const String routeName = '/cleaner_search'; @@ -87,6 +88,7 @@ class _CleanerSearchFormViewState extends State { setState(() { _isLoadingSites = false; }); + _showErrorDialog(context, 'Failed to load sites. Please try again.'); } } @@ -106,6 +108,7 @@ class _CleanerSearchFormViewState extends State { _isLoadingBuildings = false; _buildingOptions = []; }); + _showErrorDialog(context, 'Failed to load buildings. Please try again.'); } } @@ -125,6 +128,7 @@ class _CleanerSearchFormViewState extends State { _isLoadingFloors = false; _floorOptions = []; }); + _showErrorDialog(context, 'Failed to load floors. Please try again.'); } } @@ -146,6 +150,7 @@ class _CleanerSearchFormViewState extends State { _locationDescriptionOptions = []; _allDeptRoomList = []; }); + _showErrorDialog(context, 'Failed to load location descriptions. Please try again.'); } } @@ -465,7 +470,7 @@ class _CleanerSearchFormViewState extends State { Future _onSubmit() async { // Validate that at least site is selected if (selectedSite == null) { - 'Please select a site'.showToast; + _showErrorDialog(context, 'Please select a site'); return; } @@ -502,11 +507,22 @@ class _CleanerSearchFormViewState extends State { }, ); } catch (e) { - 'Error searching'.showToast; + _showErrorDialog(context, 'Error searching. Please try again.'); } finally { setState(() { _isSearching = false; }); } } + + void _showErrorDialog(BuildContext context, String message) { + showDialog( + context: context, + builder: (context) => ConfirmDialog( + message: message, + title: 'Error', + onTap: () => Navigator.pop(context), + ), + ); + } } diff --git a/lib/modules/cleaner_module/provider/cleaner_provider.dart b/lib/modules/cleaner_module/provider/cleaner_provider.dart index a057aa54..ec6fff3f 100644 --- a/lib/modules/cleaner_module/provider/cleaner_provider.dart +++ b/lib/modules/cleaner_module/provider/cleaner_provider.dart @@ -145,6 +145,7 @@ class CleanerProvider extends ChangeNotifier { "planRecurrentTaskId": planRecurrentTaskId, "qrCode": qrCode, }, + showToast: false, // Disable automatic toast message ); if (response.statusCode >= 200 && response.statusCode < 300) { @@ -159,6 +160,49 @@ 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> 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.", + }; + } + } + void reset() { _isLoading = false; _searchResults = null; diff --git a/lib/modules/cleaner_module/widgets/cleaner_preview_dialog.dart b/lib/modules/cleaner_module/widgets/cleaner_preview_dialog.dart new file mode 100644 index 00000000..6abd4d04 --- /dev/null +++ b/lib/modules/cleaner_module/widgets/cleaner_preview_dialog.dart @@ -0,0 +1,457 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:test_sa/controllers/api_routes/urls.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/models/ppm/recurrent_wo.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/views/widgets/buttons/app_back_button.dart'; +import 'package:test_sa/modules/cm_module/cm_request_utils.dart'; +import 'package:test_sa/views/widgets/total_working_time_detail_bottomsheet.dart'; + +class CleanerPreviewDialog extends StatelessWidget { + final RecurrentWoData model; + final double totalWorkingHours; + final VoidCallback onConfirm; + + const CleanerPreviewDialog({ + Key? key, + required this.model, + required this.totalWorkingHours, + required this.onConfirm, + }) : super(key: key); + + static void show( + BuildContext context, { + required RecurrentWoData model, + required double totalWorkingHours, + required VoidCallback onConfirm, + }) { + showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) => CleanerPreviewDialog( + model: model, + totalWorkingHours: totalWorkingHours, + onConfirm: onConfirm, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.85), + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(14), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header matching DefaultAppBar style + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(14), + topRight: Radius.circular(14), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: 'Confirm Details'.heading3(context).custom( + fontWeight: FontWeight.w500, + ), + ), + Icon( + Icons.close, + color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, + size: 24, + ).onPress(() => Navigator.pop(context)), + ], + ), + ), + Divider(height: 1, color: context.isDark ? AppColor.neutral60 : AppColor.neutral100), + // Content + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildPreviewSection(context, 'Task Information', [ + _buildPreviewRow(context, context.translation.title, model.title ?? '-'), + _buildPreviewRow(context, context.translation.taskNo, model.taskNo ?? '-'), + _buildPreviewRow(context, context.translation.site, model.site?.siteName ?? '-'), + _buildPreviewRow(context, context.translation.department, model.department?.name ?? '-'), + _buildPreviewRow(context, context.translation.floor, model.floor?.name ?? '-'), + _buildPreviewRow(context, context.translation.room, model.room?.name ?? '-'), + _buildPreviewRow(context, context.translation.assignedEmployee, model.engineer?.userName ?? '-'), + ]), + 16.height, + // Total Working Time Section - Using WorkingTimeTile design + _buildPreviewSection(context, 'Total Working Time', [ + _buildWorkingTimeTile(context), + ]), + 16.height, + if (model.planRecurrentTaskCheckLists != null && model.planRecurrentTaskCheckLists!.isNotEmpty) ...[ + _buildPreviewSection(context, 'Checklist', [ + ...model.planRecurrentTaskCheckLists! + .map( + (item) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + item.isChecked ? Icons.check_circle : Icons.circle_outlined, + color: item.isChecked ? AppColor.primary10 : AppColor.neutral120, + size: 20, + ), + 8.width, + RichText( + text: TextSpan( + children: [ + TextSpan( + text: item.title, + style: AppTextStyles.bodyText2.copyWith( + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, + ), + ), + if (item.isMandatory) + TextSpan( + text: ' *', + style: AppTextStyles.bodyText2.copyWith( + color: Colors.red, + ), + ), + ], + ), + ).expanded, + ], + ), + // Show comment if item is checked and has a comment + if (item.isChecked && item.comment.isNotEmpty == true) ...[ + 8.height, + Container( + width: double.infinity, + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.only(left: 28), + decoration: BoxDecoration( + color: context.isDark ? AppColor.neutral60.withValues(alpha: 0.3) : AppColor.neutral100.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(6), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 'Comment:'.bodyText2(context).custom( + fontWeight: FontWeight.w600, + color: context.isDark ? AppColor.neutral30 : AppColor.neutral120, + ), + 4.height, + item.comment.bodyText2(context).custom( + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, + ), + ], + ), + ), + ], + ], + ), + ), + ) + .toList(), + ]), + 16.height, + ], + if (model.comment != null && model.comment!.isNotEmpty) ...[ + _buildPreviewSection(context, context.translation.comment, [ + model.comment!.bodyText(context), + ]), + 16.height, + ], + if (model.imageRoom != null && model.imageRoom!.isNotEmpty) ...[ + _buildPreviewSection(context, 'Captured Image', [ + _buildImagePreview(context, model.imageRoom!), + ]), + 16.height, + ], + ], + ), + ), + ), + // Footer with Confirm button + Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + border: Border(top: BorderSide(color: context.isDark ? AppColor.neutral60 : AppColor.neutral100)), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(14), + bottomRight: Radius.circular(14), + ), + ), + child: AppFilledButton( + label: context.translation.complete, + buttonColor: AppColor.primary10, + onPressed: () { + Navigator.pop(context); + onConfirm(); + }, + ), + ), + ], + ), + ), + ); + } + + Widget _buildPreviewSection(BuildContext context, String title, List children) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title.heading6(context).custom(fontWeight: FontWeight.w600), + 8.height, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ).toShadowContainer(context, padding: 12), + ], + ); + } + + Widget _buildPreviewRow(BuildContext context, String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 2, + child: '$label:'.bodyText2(context).custom(color: AppColor.neutral120), + ), + 8.width, + Expanded( + flex: 3, + child: value.bodyText2(context).custom(color: AppColor.neutral120), + ), + ], + ), + ); + } + + Widget _buildWorkingTimeTile(BuildContext context) { + // Create timer list from model data, same as in RecurrentTaskInfoWidget + final timerList = model.planRecurrentTaskTimers?.map((e) { + return TimerHistoryModel( + id: e.id, + startTime: e.startTime, + endTime: e.endTime, + workingHours: e.workingHours, + ); + }).toList() ?? []; + + return Container( + height: 56.toScreenHeight, + padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth), + alignment: Alignment.centerLeft, + decoration: BoxDecoration( + color: AppColor.fieldBgColor(context), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Total Working Time", + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: context.isDark ? null : AppColor.neutral20, + fontWeight: FontWeight.w500, + ), + ), + Text( + " ${CMRequestUtils.formatTotalWorkingHours(totalWorkingHours)}", + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ).expanded, + 8.width, + const Icon(Icons.info_outline_rounded, color: AppColor.primary10), + ], + ), + ).onPress(() async { + await showModalBottomSheet( + context: context, + isDismissible: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + clipBehavior: Clip.antiAliasWithSaveLayer, + builder: (BuildContext context) => TotalWorkingTimeDetailBottomSheet(timerList: timerList), + ); + }); + } + + Widget _buildImagePreview(BuildContext context, String imageString) { + Widget imageWidget; + + // First check if it's base64 encoded (contains | separator) + if (imageString.contains('|')) { + // Format: "filename|base64data" + try { + final base64Data = imageString.split('|').last; + imageWidget = Image.memory(base64Decode(base64Data), fit: BoxFit.cover); + return 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: imageWidget).center, + const ABackButton(), + ], + ), + ), + ), + ), + ); + }); + } catch (e) { + debugPrint('Error decoding base64 image: $e'); + return _buildCameraIcon(context); + } + } + + // Check if it's a local file path + if (_isLocalUrl(imageString)) { + try { + imageWidget = Image.file(File(imageString), fit: BoxFit.cover); + return 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: imageWidget).center, + const ABackButton(), + ], + ), + ), + ), + ), + ); + }); + } 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 + 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); + }, + ); + return 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: imageWidget).center, + const ABackButton(), + ], + ), + ), + ), + )); + }); + } + + return _buildCameraIcon(context); + } + + /// Build camera icon widget for fallback + Widget _buildCameraIcon(BuildContext context) { + return Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration( + color: AppColor.fieldBgColor(context), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.camera_alt, + size: 48, + color: context.isDark ? AppColor.white10 : AppColor.black10, + ), + ); + } + + /// Check if the URL is a local file path + bool _isLocalUrl(String url) { + return url.startsWith('/') || url.startsWith('file://') || (url.length > 1 && url[1] == ':'); + } +} diff --git a/lib/modules/pm_module/recurrent_wo/components/task_info_widget.dart b/lib/modules/pm_module/recurrent_wo/components/task_info_widget.dart index d49be1bc..9446e975 100644 --- a/lib/modules/pm_module/recurrent_wo/components/task_info_widget.dart +++ b/lib/modules/pm_module/recurrent_wo/components/task_info_widget.dart @@ -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/widget_extensions.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/common_widgets/app_text_form_field.dart'; import 'package:test_sa/new_views/common_widgets/working_time_tile.dart'; @@ -18,10 +17,11 @@ import 'package:test_sa/views/widgets/total_working_time_detail_bottomsheet.dart class RecurrentTaskInfoWidget extends StatefulWidget { final RecurrentWoData? model; - bool isClosed; - bool isHouseKeeper; + final bool isClosed; + final bool isHouseKeeper; + final DateTime? timerStartTime; - RecurrentTaskInfoWidget({super.key, required this.model, this.isClosed = false, this.isHouseKeeper = false}); + const RecurrentTaskInfoWidget({super.key, required this.model, this.isClosed = false, this.isHouseKeeper = false, this.timerStartTime}); @override State createState() => _RecurrentTaskInfoWidgetState(); @@ -87,7 +87,7 @@ class _RecurrentTaskInfoWidgetState extends State { '${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), ], - '${context.translation.assignedEmployee}: ${widget.model!.engineer!.userName ?? ""}'.bodyText2(context).custom(color: AppColor.neutral120), + '${context.translation.assignedEmployee}: ${widget.model!.engineer?.userName ?? ""}'.bodyText2(context).custom(color: AppColor.neutral120), '${context.translation.scheduledDate}: ${widget.model!.scheduleDate!.toMonthYearFormat}'.bodyText2(context).custom(color: AppColor.neutral120), ], ).toShadowContainer(context), @@ -122,6 +122,27 @@ class _RecurrentTaskInfoWidgetState extends State { } 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(builder: (context, snapshot, child) { return Column( mainAxisSize: MainAxisSize.min, @@ -178,6 +199,92 @@ class _RecurrentTaskInfoWidgetState extends State { } } +// 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}) { return Column( mainAxisAlignment: MainAxisAlignment.start, diff --git a/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart b/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart index 3b1a0a30..b350d626 100644 --- a/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart +++ b/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart @@ -1,7 +1,4 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:provider/provider.dart'; import 'package:test_sa/controllers/api_routes/api_manager.dart'; import 'package:test_sa/extensions/context_extension.dart'; @@ -10,6 +7,7 @@ import 'package:test_sa/extensions/widget_extensions.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/cleaner_module/widgets/cleaner_checklist_widget.dart'; +import 'package:test_sa/modules/cleaner_module/widgets/cleaner_preview_dialog.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'; @@ -17,6 +15,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/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 '../../../../../../controllers/providers/api/all_requests_provider.dart'; import 'components/room_tabs_widget.dart'; @@ -36,12 +35,21 @@ class RecurrentWorkOrderView extends StatefulWidget { class _RecurrentWorkOrderViewState extends State { AllRequestsProvider? allRequestsProvider; + DateTime? _housekeeperTimerStart; @override void initState() { allRequestsProvider = Provider.of(context, listen: false); - // calculateWorkingTime(); - Provider.of(context, listen: false).getRecurrentWoById(id: widget.taskId!); + Provider.of(context, listen: false).getRecurrentWoById(id: widget.taskId!).then((_) { + // Start timer automatically for housekeeper type + final provider = Provider.of(context, listen: false); + bool isHouseKeeper = provider.recurrentWoData?.planRecurrentTaskCheckLists != null && + provider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty; + + if (isHouseKeeper && provider.recurrentWoData?.status?.value != 1) { + _housekeeperTimerStart = DateTime.now(); + } + }); super.initState(); } @@ -63,7 +71,7 @@ class _RecurrentWorkOrderViewState extends State { onWillPopScope: isClosed ? null : () { - _updateTask(context: context, status: 0); + _updateTask(context: context, status: 0, isHouseKeeper: isHouseKeeper); }, ), body: allRequestsProvider!.isLoading @@ -78,7 +86,12 @@ class _RecurrentWorkOrderViewState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - RecurrentTaskInfoWidget(isClosed: isClosed, model: requestProvider.recurrentWoData, isHouseKeeper: isHouseKeeper), + RecurrentTaskInfoWidget( + isClosed: isClosed, + model: requestProvider.recurrentWoData, + isHouseKeeper: isHouseKeeper, + timerStartTime: _housekeeperTimerStart, + ), IgnorePointer( ignoring: isClosed, child: requestProvider.recurrentWoData!.planRecurrentMedicalTaskRooms != null && requestProvider.recurrentWoData!.planRecurrentMedicalTaskRooms!.isNotEmpty @@ -121,14 +134,13 @@ class _RecurrentWorkOrderViewState extends State { label: context.translation.save, buttonColor: context.isDark ? AppColor.neutral20 : AppColor.white60, textColor: context.isDark ? Colors.white : AppColor.black10, - onPressed: () => _updateTask(context: context, status: 0), + onPressed: () => _updateTask(context: context, status: 0, isHouseKeeper: isHouseKeeper), ).expanded, 12.width, AppFilledButton( label: context.translation.complete, buttonColor: AppColor.primary10, - // textColor: context.isDark?Colors.white: AppColor.black10, - onPressed: () => _updateTask(context: context, status: 1), + onPressed: () => _updateTask(context: context, status: 1, isHouseKeeper: isHouseKeeper), ).expanded, ], ), @@ -142,91 +154,176 @@ class _RecurrentWorkOrderViewState extends State { cxt: context, showPopUp: !isClosed, onSave: () { - _updateTask(context: context, status: 0); + _updateTask(context: context, status: 0, isHouseKeeper: isHouseKeeper); }); }); } -} -void _updateTask({required BuildContext context, required int status}) async { - AllRequestsProvider allRequestsProvider = Provider.of(context, listen: false); + void _updateTask({required BuildContext context, required int status, required bool isHouseKeeper}) async { + AllRequestsProvider allRequestsProvider = Provider.of(context, listen: false); - // Only validate checklist if it exists and status is complete - if (status == 1 && allRequestsProvider.recurrentWoData?.planRecurrentTaskCheckLists != null && allRequestsProvider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty) { - // Validate mandatory checklist items only - if (!_validateChecklist(allRequestsProvider.recurrentWoData!)) { + // Only validate checklist if it exists and status is complete + if (status == 1 && allRequestsProvider.recurrentWoData?.planRecurrentTaskCheckLists != null && allRequestsProvider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty) { + // Validate mandatory checklist items only + if (!_validateChecklist(context, allRequestsProvider.recurrentWoData!)) { + return; + } + } + + // For housekeeper: show preview dialog before completing + if (isHouseKeeper && status == 1) { + _showPreviewDialog(context, allRequestsProvider); return; } - // Image validation is handled by backend - no need to force capture here + // Proceed with update + _performUpdate(context: context, status: status, isHouseKeeper: isHouseKeeper); } - if (validate(model: allRequestsProvider.recurrentWoData!, status: status)) { - allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers ?? []; - - if (allRequestsProvider.recurrentWoData?.recurrentWoTimePicker != null) { - 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), - ), - ); - }); + void _performUpdate({required BuildContext context, required int status, required bool isHouseKeeper}) async { + AllRequestsProvider allRequestsProvider = Provider.of(context, listen: false); + + if (validate(context: context, model: allRequestsProvider.recurrentWoData!, status: status, isHouseKeeper: isHouseKeeper)) { + // For housekeeper: automatically add timer data + if (isHouseKeeper && _housekeeperTimerStart != null) { + DateTime endTime = DateTime.now(); + int durationInSecond = endTime.difference(_housekeeperTimerStart!).inSeconds; - final uniqueTimersMap = {}; - allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.forEach((timer) { - if (timer.startTime != null && timer.endTime != null) { - final key = '${timer.id}_${timer.startTime}_${timer.endTime}'; - uniqueTimersMap[key] = timer; + allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers ?? []; + allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add( + PlanRecurrentTaskTimers( + id: 0, + startTime: _housekeeperTimerStart!.toIso8601String(), + endTime: endTime.toIso8601String(), + workingHours: ((durationInSecond) / 60 / 60), + ), + ); + } else { + // For non-housekeeper: use existing timer logic + allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers ?? []; + + if (allRequestsProvider.recurrentWoData?.recurrentWoTimePicker != null) { + 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), + ), + ); + }); } - }); - allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = uniqueTimersMap.values.toList(); - showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading()); + final uniqueTimersMap = {}; + allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.forEach((timer) { + if (timer.startTime != null && timer.endTime != null) { + final key = '${timer.id}_${timer.startTime}_${timer.endTime}'; + uniqueTimersMap[key] = timer; + } + }); + + allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = uniqueTimersMap.values.toList(); + showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading()); - await allRequestsProvider.updateRecurrentWo(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; + await allRequestsProvider.updateRecurrentWo(status: status, showToast: false).then((result) { + Navigator.pop(context); // Close loading dialog - // Navigate to LandPage and clear all previous routes - Navigator.of(context).pushNamedAndRemoveUntil(LandPage.routeName, (routes) => false); + bool success = result['success'] ?? 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 { - // For save (status 0), just pop the current screen - allRequestsProvider.recurrentWoData?.recurrentWoTimerModel = null; - Navigator.pop(context); + // Show error dialog with the exact message from API + _showErrorDialog(context, message.isNotEmpty ? message : "Failed to update the task. Please try again."); } + }).catchError((error) { + Navigator.pop(context); // Close loading dialog + _showErrorDialog(context, error.toString()); + }); + } + } + + void _showPreviewDialog(BuildContext context, AllRequestsProvider provider) { + final model = provider.recurrentWoData!; + + // Calculate total working time + double totalWorkingHours = 0; + final timers = model.planRecurrentTaskTimers ?? []; + totalWorkingHours = timers.fold(0.0, (sum, item) { + if (item.startTime == null || item.endTime == null) return sum; + try { + final start = DateTime.parse(item.startTime!); + final end = DateTime.parse(item.endTime!); + final diffInHours = end.difference(start).inSeconds / 3600.0; + return sum + diffInHours; + } catch (_) { + return sum; } }); + + // Add current timer if running + if (_housekeeperTimerStart != null) { + final currentDuration = DateTime.now().difference(_housekeeperTimerStart!).inSeconds / 3600.0; + totalWorkingHours += currentDuration; + } + + CleanerPreviewDialog.show( + context, + model: model, + totalWorkingHours: totalWorkingHours, + onConfirm: () => _performUpdate(context: context, status: 1, isHouseKeeper: true), + ); + } + + void _showErrorDialog(BuildContext context, String message) { + showDialog( + context: context, + builder: (context) => ConfirmDialog( + message: message, + title: 'Error', + onTap: () => Navigator.pop(context), + ), + ); } } -bool _validateChecklist(RecurrentWoData model) { +bool _validateChecklist(BuildContext context, RecurrentWoData model) { // Check if all mandatory checklist items are checked final uncheckedMandatory = model.planRecurrentTaskCheckLists!.where((item) => item.isMandatory && !item.isChecked).toList(); if (uncheckedMandatory.isNotEmpty) { - Fluttertoast.showToast( - msg: "Please complete all mandatory tasks (marked with *)", - toastLength: Toast.LENGTH_LONG, + showDialog( + context: context, + builder: (context) => ConfirmDialog( + message: "Please complete all mandatory tasks (marked with *)", + title: 'Validation Error', + onTap: () => Navigator.pop(context), + ), ); return false; } @@ -234,31 +331,72 @@ bool _validateChecklist(RecurrentWoData model) { return true; } -bool validate({required RecurrentWoData model, required int status}) { +bool validate({required BuildContext context, required RecurrentWoData model, required int status, required bool isHouseKeeper}) { + // Skip timer validation for housekeeper type + if (isHouseKeeper) { + return true; + } + + // Original timer validation logic for non-housekeeper types bool isTimerPickerEnable = ApiManager.instance.assetGroup?.enabledEngineerTimer ?? false; if (isTimerPickerEnable) { 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; } if (model.recurrentWoTimePicker == 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; } 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; } } } else { 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; } 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; } } diff --git a/lib/new_views/pages/land_page/my_request/all_requests_filter_page.dart b/lib/new_views/pages/land_page/my_request/all_requests_filter_page.dart index 9b5a7441..f1e35b4c 100644 --- a/lib/new_views/pages/land_page/my_request/all_requests_filter_page.dart +++ b/lib/new_views/pages/land_page/my_request/all_requests_filter_page.dart @@ -127,8 +127,11 @@ class _AllRequestsFilterPageState extends State { if (context.settingProvider.isUserFlowMedical && !context.userProvider.isNurse) { types['Incidents'] = 16; } - - final statuses = { +//No need to add check for assetgroup type as this role will only for FMS + if (context.userProvider.isHouseKeeper) { + types = {context.translation.recurrentWo: 5}; + } + var statuses = { "All WO": 0, context.translation.open: 1, context.translation.inProgress: 2, @@ -136,6 +139,16 @@ class _AllRequestsFilterPageState extends State { context.translation.closed: 3, "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( appBar: DefaultAppBar( title: context.translation.filter, diff --git a/lib/new_views/pages/land_page/my_request/my_requests_page.dart b/lib/new_views/pages/land_page/my_request/my_requests_page.dart index ba49458e..123f4ad0 100644 --- a/lib/new_views/pages/land_page/my_request/my_requests_page.dart +++ b/lib/new_views/pages/land_page/my_request/my_requests_page.dart @@ -106,6 +106,14 @@ class _MyRequestsPageState extends State { 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(context, listen: false); _provider!.reset(); WidgetsBinding.instance.addPostFrameCallback((_) { diff --git a/lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart b/lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart index 0f63745e..bf3db783 100644 --- a/lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart +++ b/lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart @@ -27,7 +27,7 @@ class RecurrentWoItemView extends StatelessWidget { Widget build(BuildContext context) { final user = Provider.of(context, listen: false).user; final settingProvider = Provider.of(context, listen: false); - final bool isFMSEngineer = user?.type == UsersTypes.engineer && settingProvider.isUserFMS; + final bool isHouseKeeper = user?.type == UsersTypes.houseKeeper; if (requestData != null) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -55,12 +55,22 @@ class RecurrentWoItemView extends StatelessWidget { ], ), 8.height, - (requestData?.typeTransaction ?? context.translation.ppmRequest).heading5(context), + (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, @@ -75,7 +85,7 @@ class RecurrentWoItemView extends StatelessWidget { ), ], ).toShadowContainer(context, withShadow: showShadow).onPress(() { - if (isFMSEngineer && (requestData?.statusValue == 4)) { + if (isHouseKeeper && (requestData?.statusValue == 4)) { Navigator.push( context, MaterialPageRoute( @@ -123,6 +133,11 @@ class RecurrentWoItemView extends StatelessWidget { '${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, @@ -137,7 +152,7 @@ class RecurrentWoItemView extends StatelessWidget { ), ], ).toShadowContainer(context, withShadow: showShadow).onPress(() async { - if (isFMSEngineer && (requestDetails?.statusValue == 4)) { + if (isHouseKeeper && (requestDetails?.statusValue == 4)) { Navigator.push( context, MaterialPageRoute( diff --git a/lib/views/widgets/dialogs/confirm_dialog.dart b/lib/views/widgets/dialogs/confirm_dialog.dart index 83b05faa..6d8ef764 100644 --- a/lib/views/widgets/dialogs/confirm_dialog.dart +++ b/lib/views/widgets/dialogs/confirm_dialog.dart @@ -44,6 +44,7 @@ class ConfirmDialog extends StatelessWidget { ) ], ), + 16.height, message.bodyText(context), 28.height, AppFilledButton( From 05960f28e059ad962f1bb9a2ae9b9761adedd6b7 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Tue, 5 May 2026 15:57:50 +0300 Subject: [PATCH 08/17] house keeper should not create any request. --- lib/new_views/common_widgets/app_bottom_nav_bar.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/new_views/common_widgets/app_bottom_nav_bar.dart b/lib/new_views/common_widgets/app_bottom_nav_bar.dart index 4195d6e4..611afb27 100644 --- a/lib/new_views/common_widgets/app_bottom_nav_bar.dart +++ b/lib/new_views/common_widgets/app_bottom_nav_bar.dart @@ -19,11 +19,12 @@ class AppBottomNavigationBar extends StatelessWidget { bool isEngineer = (Provider.of(context, listen: false).user?.type) == UsersTypes.engineer; bool isUserFMS = Provider.of(context, listen: false).isUserFMS; bool commissioningEng = context.userProvider.isCommissioningEngineer; + bool isHouseKeeper = context.userProvider.isHouseKeeper; List navItems = [ NavItemModel(0, "overview", context.translation.overview), 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), NavItemModel(4, "contact", context.translation.contact), ]; From 5c46fdd8e5e50d5ec6c1fca0040aba0ce438cbd2 Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Thu, 21 May 2026 15:24:33 +0300 Subject: [PATCH 09/17] new flow api implemented --- lib/controllers/api_routes/urls.dart | 8 +- .../models/one_time_task_data.dart | 236 +++++++ .../models/qr_scan_location_data.dart | 60 ++ .../cleaner_module/models/task_type.dart | 26 + .../pages/cleaner_search_form_view.dart | 625 +++++++----------- .../pages/one_time_task_view.dart | 437 ++++++++++++ .../provider/cleaner_provider.dart | 179 +++++ .../provider/task_type_provider.dart | 74 +++ 8 files changed, 1243 insertions(+), 402 deletions(-) create mode 100644 lib/modules/cleaner_module/models/one_time_task_data.dart create mode 100644 lib/modules/cleaner_module/models/qr_scan_location_data.dart create mode 100644 lib/modules/cleaner_module/models/task_type.dart create mode 100644 lib/modules/cleaner_module/pages/one_time_task_view.dart create mode 100644 lib/modules/cleaner_module/provider/task_type_provider.dart diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart index 2ba006ab..e2b02926 100644 --- a/lib/controllers/api_routes/urls.dart +++ b/lib/controllers/api_routes/urls.dart @@ -71,8 +71,12 @@ class URLs { static get searchPlanRecurrentTaskListSearch => "$_baseUrl/PlanRecurrentTasks/PlanRecurrentTaskListSearchDto"; // 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. static get nurseDashboardCountUrl => '$_baseUrl/ServiceRequest/GetDashboardNurseCount'; diff --git a/lib/modules/cleaner_module/models/one_time_task_data.dart b/lib/modules/cleaner_module/models/one_time_task_data.dart new file mode 100644 index 00000000..817ae685 --- /dev/null +++ b/lib/modules/cleaner_module/models/one_time_task_data.dart @@ -0,0 +1,236 @@ +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? instructions; + List? attachments; + String? createdDate; + String? createdBy; + String? imageRoom; // For captured image + + 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, + }); + + factory OneTimeTaskData.fromJson(Map 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'], + ); + } + + Map 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, + }; + } +} + +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 json) { + return OneTimeSetting( + id: json['id'], + taskTypeId: json['taskTypeId'], + taskTypeName: json['taskTypeName'], + instructionDescriptionId: json['instructionDescriptionId'], + instructionDescriptionName: json['instructionDescriptionName'], + sla: json['sla'], + ); + } + + Map 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 json) { + return Instruction( + id: json['id'], + instructionTextId: json['instructionTextId'], + instructionText: json['instructionText'], + isRequired: json['isRequired'], + isDone: json['isDone'], + comment: json['comment'], + ); + } + + Map toJson() { + return { + 'id': id, + 'instructionTextId': instructionTextId, + 'instructionText': instructionText, + 'isRequired': isRequired, + 'isDone': isDone, + 'comment': comment, + }; + } +} + +class Attachment { + int? id; + String? fileName; + String? fileUrl; + String? fileType; + + Attachment({ + this.id, + this.fileName, + this.fileUrl, + this.fileType, + }); + + factory Attachment.fromJson(Map json) { + return Attachment( + id: json['id'], + fileName: json['fileName'], + fileUrl: json['fileUrl'], + fileType: json['fileType'], + ); + } + + Map toJson() { + return { + 'id': id, + 'fileName': fileName, + 'fileUrl': fileUrl, + 'fileType': fileType, + }; + } +} + diff --git a/lib/modules/cleaner_module/models/qr_scan_location_data.dart b/lib/modules/cleaner_module/models/qr_scan_location_data.dart new file mode 100644 index 00000000..a5b914d0 --- /dev/null +++ b/lib/modules/cleaner_module/models/qr_scan_location_data.dart @@ -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 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 toJson() { + return { + 'siteId': siteId, + 'siteName': siteName, + 'buildingId': buildingId, + 'buildingName': buildingName, + 'floorId': floorId, + 'floorName': floorName, + 'departmentId': departmentId, + 'departmentName': departmentName, + 'roomId': roomId, + 'roomName': roomName, + 'locationDescription': locationDescription, + }; + } +} + diff --git a/lib/modules/cleaner_module/models/task_type.dart b/lib/modules/cleaner_module/models/task_type.dart new file mode 100644 index 00000000..a42b906b --- /dev/null +++ b/lib/modules/cleaner_module/models/task_type.dart @@ -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 toJson() { + return {"id": id, "taskTypeName": taskTypeName}; + } + + factory TaskType.fromJson(Map? parsedJson) { + if (parsedJson == null) return TaskType(); + return TaskType( + id: parsedJson["id"], + taskTypeName: parsedJson["taskTypeName"], + ); + } +} diff --git a/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart b/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart index 2ed65884..9cc1f1d8 100644 --- a/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart +++ b/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart @@ -1,27 +1,21 @@ 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/string_extensions.dart'; +import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; -import 'package:test_sa/models/hospital.dart'; -import 'package:test_sa/models/lookup.dart'; -import 'package:test_sa/models/new_models/department_room_combined.dart'; -import 'package:test_sa/modules/cleaner_module/pages/cleaner_search_list_view.dart'; +import 'package:test_sa/modules/cleaner_module/models/task_type.dart'; +import 'package:test_sa/modules/cleaner_module/models/qr_scan_location_data.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/fms_floor_provider.dart'; -import 'package:test_sa/modules/cleaner_module/provider/fms_location_description_provider.dart'; +import 'package:test_sa/modules/cleaner_module/provider/task_type_provider.dart'; +import 'package:test_sa/modules/cleaner_module/pages/one_time_task_view.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/app_filled_button.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/providers/loading_list_notifier.dart'; -import 'package:test_sa/models/new_models/building.dart'; -import 'package:test_sa/models/new_models/floor.dart'; -import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart'; +import 'package:test_sa/views/widgets/qr/scan_qr.dart'; class CleanerSearchFormView extends StatefulWidget { static const String routeName = '/cleaner_search'; @@ -33,165 +27,140 @@ class CleanerSearchFormView extends StatefulWidget { } class _CleanerSearchFormViewState extends State { - Hospital? selectedSite; - Building? selectedBuilding; - Floor? selectedFloor; - DepartmentRoomCombined? selectedDepartmentRoom; - Lookup? selectedLocationDescription; - late FocusNode _siteFocusNode; - final TextEditingController _siteController = TextEditingController(); - - List _siteOptions = []; - List _buildingOptions = []; - List _floorOptions = []; - List _locationDescriptionOptions = []; - List _allDeptRoomList = []; - - bool _isLoadingSites = false; - bool _isLoadingBuildings = false; - bool _isLoadingFloors = false; - bool _isLoadingLocationDescriptions = false; - bool _isSearching = false; + TaskType? selectedTaskType; + List _taskTypeOptions = []; + bool _isLoadingTaskTypes = false; + bool _isScanning = false; + bool _isProceeding = false; + QrScanLocationData? _scannedLocationData; @override void initState() { super.initState(); - _siteFocusNode = FocusNode(); + _loadTaskTypes(); } @override void dispose() { - _siteController.dispose(); - _siteFocusNode.dispose(); super.dispose(); } - Future _searchSites(String query) async { - if (query.isEmpty) { - setState(() { - _siteOptions = []; - }); - return; - } - + Future _loadTaskTypes() async { setState(() { - _isLoadingSites = true; + _isLoadingTaskTypes = true; }); try { - final sites = await HospitalsProvider().getHospitalsListByVal(searchVal: query); + final taskTypes = await TaskTypeProvider().getTaskTypesList(); setState(() { - _siteOptions = sites; - _isLoadingSites = false; + _taskTypeOptions = taskTypes; + _isLoadingTaskTypes = false; }); } catch (e) { setState(() { - _isLoadingSites = false; + _isLoadingTaskTypes = false; + _taskTypeOptions = []; }); - _showErrorDialog(context, 'Failed to load sites. Please try again.'); + _showErrorDialog(context, 'Failed to load task types. Please try again.'); } } - Future _loadBuildings(int siteId) async { - setState(() { - _isLoadingBuildings = true; - }); + Future _onScanQRCode() async { + final result = await Navigator.push( + context, + MaterialPageRoute(builder: (context) => const ScanQr()), + ); - try { - final buildings = await FmsBuildingProvider().getBuildingsListBySiteId(siteId); - setState(() { - _buildingOptions = buildings; - _isLoadingBuildings = false; - }); - } catch (e) { + if (result != null && result is String && result.isNotEmpty) { setState(() { - _isLoadingBuildings = false; - _buildingOptions = []; + _isScanning = true; }); - _showErrorDialog(context, 'Failed to load buildings. Please try again.'); + + 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 _loadFloors(int siteId, int buildingId) async { - setState(() { - _isLoadingFloors = true; - }); + Future _onProceed() async { + // Validate that task type and location data are available + if (selectedTaskType == null) { + _showErrorDialog(context, 'Please select a task type'); + return; + } - try { - final floors = await FmsFloorProvider().getFloorsListBySiteAndBuilding(siteId, buildingId); - setState(() { - _floorOptions = floors; - _isLoadingFloors = false; - }); - } catch (e) { - setState(() { - _isLoadingFloors = false; - _floorOptions = []; - }); - _showErrorDialog(context, 'Failed to load floors. Please try again.'); + if (_scannedLocationData == null) { + _showErrorDialog(context, 'Please scan a QR code first'); + return; } - } - Future _loadLocationDescriptions(int siteId, int buildingId, int floorId) async { setState(() { - _isLoadingLocationDescriptions = true; + _isProceeding = true; }); try { - final response = await FmsLocationDescriptionProvider().getLocationDescriptionsList(siteId, buildingId, floorId); - setState(() { - _locationDescriptionOptions = response?.locationDescriptions ?? []; - _allDeptRoomList = response?.deptRoom ?? []; - _isLoadingLocationDescriptions = false; - }); - } catch (e) { - setState(() { - _isLoadingLocationDescriptions = false; - _locationDescriptionOptions = []; - _allDeptRoomList = []; - }); - _showErrorDialog(context, 'Failed to load location descriptions. Please try again.'); - } - } - - List _getDepartmentRoomCombinations() { - if (selectedLocationDescription == null || _allDeptRoomList.isEmpty) return []; - - // Filter dept room list by selected location description - List 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(); - } + final result = await CleanerProvider().proceedOneTimeTask( + oneTimeSettingId: selectedTaskType!.id ?? 0, + siteId: _scannedLocationData!.siteId ?? 0, + buildingId: _scannedLocationData!.buildingId ?? 0, + floorId: _scannedLocationData!.floorId ?? 0, + departmentId: _scannedLocationData!.departmentId ?? 0, + roomId: _scannedLocationData!.roomId ?? 0, + ); - void _autoSelectDepartmentRoom() { - final combinations = _getDepartmentRoomCombinations(); + final isSuccess = result["success"] ?? false; + final data = result["data"]; + final message = result["message"] ?? "Unknown error occurred"; - if (combinations.isEmpty) { - setState(() { - selectedDepartmentRoom = null; - }); - return; - } + if (isSuccess && data != null) { + // Get the task ID from the response + final taskId = data['id']; - // If only one item, auto-fill - if (combinations.length == 1) { - setState(() { - selectedDepartmentRoom = combinations.first; - }); - } else { - // If more than one, auto-select first item + if (mounted && taskId != null) { + // Navigate to OneTimeTaskView + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => OneTimeTaskView(taskId: taskId), + ), + ); + } + } else { + if (mounted) { + _showErrorDialog(context, message); + } + } + } catch (e) { + if (mounted) { + _showErrorDialog(context, 'Failed to proceed. Please try again.'); + } + } finally { setState(() { - selectedDepartmentRoom = combinations.first; + _isProceeding = false; }); } } @@ -213,253 +182,92 @@ class _CleanerSearchFormViewState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Site Autocomplete Field - Container( - padding: EdgeInsets.symmetric( - horizontal: 16.toScreenWidth, - vertical: 8.toScreenHeight, - ), - decoration: BoxDecoration( - color: AppColor.fieldBgColor(context), - borderRadius: BorderRadius.circular(10), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Site', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: context.isDark ? AppColor.neutral30 : AppColor.neutral20, - fontWeight: FontWeight.w500, - ), - ), - SizedBox(height: 4.toScreenHeight), - SizedBox( - height: 24.toScreenHeight, // Fixed height for consistency - child: Autocomplete( - optionsBuilder: (TextEditingValue textEditingValue) async { - if (textEditingValue.text.isEmpty) { - return const Iterable.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, - style: AppTextStyles.bodyText.copyWith( - color: context.isDark ? AppColor.neutral30 : AppColor.black10, - ), - decoration: InputDecoration( - hintText: 'Search site...', - hintStyle: AppTextStyles.bodyText.copyWith( - color: AppColor.neutral20, - ), - filled: false, - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - contentPadding: EdgeInsets.zero, - isDense: true, - ), - onChanged: (value) { - _siteController.text = value; - }, - ), - ), - if (_isLoadingSites) - const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: AppColor.primary10, - ), - ) - 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: const Icon( - Icons.clear, - color: AppColor.neutral20, - size: 20, - ), - ), - ], - ); - }, - ), - ), - ], - ), - ), - const SizedBox(height: 18), - - // Building Dropdown - SingleItemDropDownMenu( + // Task Type Dropdown + SingleItemDropDownMenu( context: context, - title: 'Building', - initialValue: selectedBuilding, + title: 'Task Type', + initialValue: selectedTaskType, showShadow: false, showAsBottomSheet: true, backgroundColor: AppColor.fieldBgColor(context), - enabled: _buildingOptions.isNotEmpty, - loading: _isLoadingBuildings, - staticData: _buildingOptions, - onSelect: (value) async { - _siteFocusNode.unfocus(); - + enabled: _taskTypeOptions.isNotEmpty, + loading: _isLoadingTaskTypes, + staticData: _taskTypeOptions, + onSelect: (value) { setState(() { - _siteOptions = []; - print('site options length ${_siteOptions.length}'); - selectedBuilding = value; - selectedFloor = null; - selectedLocationDescription = null; - selectedDepartmentRoom = null; - _floorOptions = []; - _locationDescriptionOptions = []; - _allDeptRoomList = []; + selectedTaskType = value; }); - - // Load floors for selected building - if (value != null && selectedSite?.id != null && value.id != null) { - await _loadFloors(selectedSite!.id!, value.id!.toInt()); - } }, ), const SizedBox(height: 18), - // Floor Dropdown - SingleItemDropDownMenu( - context: context, - title: 'Floor', - initialValue: selectedFloor, - showShadow: false, - showAsBottomSheet: true, - backgroundColor: AppColor.fieldBgColor(context), - enabled: _floorOptions.isNotEmpty, - loading: _isLoadingFloors, - staticData: _floorOptions, - onSelect: (value) async { - setState(() { - selectedFloor = value; - selectedLocationDescription = null; - selectedDepartmentRoom = null; - _locationDescriptionOptions = []; - _allDeptRoomList = []; - }); - - // Load location descriptions for selected floor - if (value != null && selectedSite?.id != null && selectedBuilding?.id != null && value.id != null) { - await _loadLocationDescriptions( - selectedSite!.id!, - selectedBuilding!.id!.toInt(), - value.id!.toInt(), - ); - } - }, + // Scan QR Code Button + GestureDetector( + onTap: selectedTaskType != null ? _onScanQRCode : null, + child: Container( + height: 50, + alignment: Alignment.center, + decoration: BoxDecoration( + color: selectedTaskType != null ? Colors.white : Colors.grey[300], + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: selectedTaskType != null ? AppColor.blueStatus(context) : Colors.grey, + width: 2, + ), + ), + padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth, vertical: 8.toScreenHeight), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + "scan_asset".toSvgAsset( + height: 22, + fit: BoxFit.fitHeight, + color: selectedTaskType != null + ? (context.isDark ? AppColor.neutral20 : AppColor.neutral120) + : Colors.grey, + ), + 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, + ), + ], + ), + ), ), const SizedBox(height: 18), - // Location Description Dropdown - SingleItemDropDownMenu( - context: context, - title: 'Location Description', - initialValue: selectedLocationDescription, - showShadow: false, - 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(); - }, - ), - const SizedBox(height: 18), + // Loading Indicator + if (_isScanning) + const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: CircularProgressIndicator(), + ), + ), - // Department & Room Combined Dropdown - SingleItemDropDownMenu( - 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; - }); - }, - ), + // Scanned Location Data Card + if (_scannedLocationData != null && !_isScanning) _buildLocationDataCard(context), ], ).toShadowContainer(context).paddingOnly(top: 20, start: 16, end: 16), ), ), + + // Footer Action Button FooterActionButton.footerContainer( context: context, child: AppFilledButton( - buttonColor: AppColor.primary10, - label: context.translation.search, + buttonColor:_scannedLocationData != null && !_isProceeding? AppColor.primary10: Colors.grey, + label: 'Proceed', maxWidth: true, - onPressed: _isSearching ? null : _onSubmit, - loading: _isSearching, + onPressed: (_scannedLocationData != null && !_isProceeding) ? _onProceed : null, + loading: _isProceeding, ), ), ], @@ -467,52 +275,69 @@ class _CleanerSearchFormViewState extends State { ); } - Future _onSubmit() async { - // Validate that at least site is selected - if (selectedSite == null) { - _showErrorDialog(context, 'Please select a site'); - return; - } + Widget _buildLocationDataCard(BuildContext context) { + final locationData = _scannedLocationData; - try { - setState(() { - _isSearching = true; - }); - - // Call the search API - final response = await CleanerProvider().searchPlanRecurrentTasksList( - 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; - final totalRows = response['totalRows'] as int; - - // Navigate to search results page with the results - Navigator.of(context).pushNamed( - CleanerSearchListView.routeName, - arguments: { - 'searchResults': results, - 'totalRows': totalRows, - 'selectedSite': selectedSite, - 'selectedBuilding': selectedBuilding, - 'selectedFloor': selectedFloor, - 'selectedLocationDescription': selectedLocationDescription, - 'selectedDepartmentRoom': selectedDepartmentRoom, - }, - ); - } catch (e) { - _showErrorDialog(context, 'Error searching. Please try again.'); - } finally { - setState(() { - _isSearching = false; - }); + if (locationData == null) { + return const SizedBox(); } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + 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, + ), + ), + ], + ), + ], + ), + ], + ).paddingOnly(start: 16, end: 16, top: 16, bottom: 16), + ], + ).toShadowContainer(context, padding: 0); } void _showErrorDialog(BuildContext context, String message) { diff --git a/lib/modules/cleaner_module/pages/one_time_task_view.dart b/lib/modules/cleaner_module/pages/one_time_task_view.dart new file mode 100644 index 00000000..d1037aa2 --- /dev/null +++ b/lib/modules/cleaner_module/pages/one_time_task_view.dart @@ -0,0 +1,437 @@ +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/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/widgets/cleaner_preview_dialog.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'; + +class OneTimeTaskView extends StatefulWidget { + static const String routeName = "/one_time_task"; + final int taskId; + + const OneTimeTaskView({Key? key, required this.taskId}) : super(key: key); + + @override + State createState() => _OneTimeTaskViewState(); +} + +class _OneTimeTaskViewState extends State { + OneTimeTaskData? _taskData; + bool _isLoading = false; + DateTime? _timerStart; + + @override + void initState() { + super.initState(); + _loadTaskData(); + } + + Future _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']); + _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; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: DefaultAppBar( + title: 'One Time Task', + onWillPopScope: isClosed + ? null + : () { + _showExitDialog(); + }, + ), + 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); + }); + }, + capturedImageBase64: _taskData!.imageRoom, + onImageCaptured: (imageBase64) { + setState(() { + _taskData!.imageRoom = imageBase64; + }); + }, + ), + ], + ), + ), + ), + if (!isClosed) ...[ + FooterActionButton.footerContainer( + context: context, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + AppFilledButton( + label: context.translation.save, + buttonColor: context.isDark ? AppColor.neutral20 : AppColor.white60, + textColor: context.isDark ? Colors.white : AppColor.black10, + onPressed: () => Navigator.pop(context), + ).expanded, + 12.width, + AppFilledButton( + label: context.translation.complete, + buttonColor: AppColor.primary10, + onPressed: _onComplete, + ).expanded, + ], + ), + ), + ] + ], + ), + ) + : NoDataFound(message: context.translation.noDataFound).center, + ).handlePopScope( + cxt: context, + showPopUp: !isClosed, + 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, + children: [ + if (_taskData?.statusName != null) + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: _getStatusColor().withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + _taskData!.statusName!, + style: AppTextStyles.bodyText.copyWith( + color: _getStatusColor(), + fontWeight: FontWeight.w600, + ), + ), + ), + if (_taskData?.createdDate != null) + Text( + _formatDate(_taskData!.createdDate!), + 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 + if (_timerStart != null && !isClosed) ...[ + 12.height, + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColor.primary10.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: AppColor.primary10), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Working Time:', + style: AppTextStyles.bodyText.copyWith( + fontWeight: FontWeight.w600, + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, + ), + ), + Text( + _formatDuration(DateTime.now().difference(_timerStart!)), + style: AppTextStyles.heading4.copyWith( + color: AppColor.primary10, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ], + ).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 _convertInstructionsToChecklist() { + return _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() ?? []; + } + + void _updateInstructions(List 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 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; + } + + // Show preview dialog + _showPreviewDialog(); + } + + void _showPreviewDialog() { + // Calculate working hours + double totalWorkingHours = 0; + if (_timerStart != null) { + final duration = DateTime.now().difference(_timerStart!); + totalWorkingHours = duration.inSeconds / 3600.0; + } + + CleanerPreviewDialog.show( + context, + model: _convertToRecurrentWoData(), + totalWorkingHours: totalWorkingHours, + onConfirm: _performComplete, + ); + } + + // Convert OneTimeTaskData to RecurrentWoData for preview dialog compatibility + dynamic _convertToRecurrentWoData() { + return { + 'siteName': _taskData?.siteName, + 'buildingName': _taskData?.buildingName, + 'floorName': _taskData?.floorName, + 'departmentName': _taskData?.departmentName, + 'roomName': _taskData?.roomName, + 'locationDescription': _taskData?.locationDescription, + 'taskNo': 'OT-${_taskData?.id}', + 'title': _taskData?.oneTimeSetting?.taskTypeName, + }; + } + + Future _performComplete() async { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const AppLazyLoading(), + ); + + try { + final instructionsPayload = _taskData?.instructions?.map((instruction) { + return { + 'id': instruction.id, + 'instructionTextId': instruction.instructionTextId, + 'instructionText': instruction.instructionText, + 'isRequired': instruction.isRequired, + 'isDone': instruction.isDone, + 'comment': instruction.comment, + }; + }).toList() ?? []; + + final result = await CleanerProvider().completeOneTimeTask( + id: _taskData!.id!, + instructions: instructionsPayload, + imageRoom: _taskData?.imageRoom, + ); + + 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 _showExitDialog() { + showDialog( + context: context, + builder: (context) => ConfirmDialog( + message: 'Do you want to exit without completing?', + title: 'Confirm', + onTap: () { + Navigator.pop(context); // Close dialog + Navigator.pop(context); // Exit screen + }, + ), + ); + } + + void _showErrorDialog(BuildContext context, String message) { + showDialog( + context: context, + builder: (context) => ConfirmDialog( + message: message, + title: 'Error', + onTap: () => Navigator.pop(context), + ), + ); + } + + Color _getStatusColor() { + if (_taskData?.statusValue == 2) { + return Colors.green; + } + return AppColor.primary10; + } + + String _formatDate(String dateStr) { + try { + final date = DateTime.parse(dateStr); + return '${date.day}/${date.month}/${date.year}'; + } catch (e) { + return dateStr; + } + } + + String _formatDuration(Duration duration) { + final hours = duration.inHours; + final minutes = duration.inMinutes.remainder(60); + final seconds = duration.inSeconds.remainder(60); + return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + } +} diff --git a/lib/modules/cleaner_module/provider/cleaner_provider.dart b/lib/modules/cleaner_module/provider/cleaner_provider.dart index ec6fff3f..18a442b3 100644 --- a/lib/modules/cleaner_module/provider/cleaner_provider.dart +++ b/lib/modules/cleaner_module/provider/cleaner_provider.dart @@ -102,6 +102,7 @@ class CleanerProvider extends ChangeNotifier { int? floorId, int? departmentId, int? roomId, + int? taskTypeId, }) async { Response response; try { @@ -115,6 +116,7 @@ class CleanerProvider extends ChangeNotifier { "floorId": floorId, "departmentId": departmentId, "roomId": roomId, + "taskTypeId": taskTypeId, }, ); @@ -203,6 +205,183 @@ class CleanerProvider extends ChangeNotifier { } } + /// Scan QR Code and get task details + /// Returns a map with success status, message, and data from backend + Future> 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> 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> getOneTimeTaskById({ + required int id, + }) async { + Response response; + try { + response = await ApiManager.instance.get( + '${URLs.getRequestById}/$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> completeOneTimeTask({ + required int id, + required List> instructions, + String? imageRoom, + }) async { + Response response; + try { + response = await ApiManager.instance.post( + URLs.completeRequest, + body: { + "id": id, + "instructions": instructions, + "imageRoom": imageRoom, + }, + 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() { _isLoading = false; _searchResults = null; diff --git a/lib/modules/cleaner_module/provider/task_type_provider.dart b/lib/modules/cleaner_module/provider/task_type_provider.dart new file mode 100644 index 00000000..624e5c36 --- /dev/null +++ b/lib/modules/cleaner_module/provider/task_type_provider.dart @@ -0,0 +1,74 @@ +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/modules/cleaner_module/models/task_type.dart'; + +class TaskTypeProvider extends ChangeNotifier { + //reset provider data + void reset() { + _taskTypes = null; + _stateCode = null; + } + + // state code of current request to define error message + int? _stateCode; + int? get stateCode => _stateCode; + + // contain task types data + List? _taskTypes; + List? get taskTypes => _taskTypes; + + // loading state + bool _loading = false; + bool get isLoading => _loading; + + set isLoading(bool isLoading) { + _loading = isLoading; + notifyListeners(); + } + + /// Get task types + Future getTaskTypes() async { + if (_loading == true) return -2; + _loading = true; + notifyListeners(); + Response response; + try { + response = await ApiManager.instance.get(URLs.getTaskType); + _stateCode = response.statusCode; + if (response.statusCode >= 200 && response.statusCode < 300) { + // client's request was successfully received + List taskTypesListJson = json.decode(response.body)["data"]; + _taskTypes = taskTypesListJson.map((taskType) => TaskType.fromJson(taskType)).toList(); + } + _loading = false; + notifyListeners(); + return response.statusCode; + } catch (error) { + _loading = false; + _stateCode = -1; + notifyListeners(); + return -1; + } + } + + /// Get task types list directly without provider state + Future> getTaskTypesList() async { + Response response; + try { + response = await ApiManager.instance.get(URLs.getTaskType); + + if (response.statusCode >= 200 && response.statusCode < 300) { + List taskTypesListJson = json.decode(response.body)["data"]; + return taskTypesListJson.map((taskType) => TaskType.fromJson(taskType)).toList(); + } + return []; + } catch (error) { + return []; + } + } +} + From c35162fd88cfec22faea14e5c9e41771b1f2fe8b Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:49:14 +0300 Subject: [PATCH 10/17] cleaner module implementation new flow complete --- lib/dashboard_latest/dashboard_provider.dart | 4 +- .../widgets/progress_fragment.dart | 2 +- .../widgets/request_category_fragment.dart | 5 +- .../widgets/request_category_list.dart | 3 + lib/main.dart | 2 - .../models/cleaner_data_model.dart | 0 .../models/cleaner_form_model.dart | 24 - .../models/one_time_task_data.dart | 19 +- .../pages/cleaner_request_item_view.dart | 69 --- .../pages/cleaner_search_form_view.dart | 98 ++-- .../pages/cleaner_search_list_view.dart | 183 ------- .../pages/one_time_task_preview_screen.dart | 373 ++++++++++++++ .../pages/one_time_task_view.dart | 326 +++++++------ .../provider/cleaner_provider.dart | 7 +- .../provider/fms_building_provider.dart | 83 ---- .../provider/fms_floor_provider.dart | 85 ---- .../fms_location_description_provider.dart | 140 ------ .../widgets/cleaner_checklist_widget.dart | 377 ++++++--------- .../widgets/cleaner_preview_dialog.dart | 457 ------------------ .../widgets/multi_image_picker.dart | 218 +++++++++ .../widgets/one_time_task_item_view.dart | 137 ++++++ .../recurrent_work_order_view.dart | 179 ++----- .../common_widgets/app_bottom_nav_bar.dart | 4 +- .../requests/recurrent_wo_item_view.dart | 42 +- .../requests/request_paginated_listview.dart | 3 + .../widgets/request_item_view_list.dart | 3 + 26 files changed, 1174 insertions(+), 1669 deletions(-) delete mode 100644 lib/modules/cleaner_module/models/cleaner_data_model.dart delete mode 100644 lib/modules/cleaner_module/models/cleaner_form_model.dart delete mode 100644 lib/modules/cleaner_module/pages/cleaner_request_item_view.dart delete mode 100644 lib/modules/cleaner_module/pages/cleaner_search_list_view.dart create mode 100644 lib/modules/cleaner_module/pages/one_time_task_preview_screen.dart delete mode 100644 lib/modules/cleaner_module/provider/fms_building_provider.dart delete mode 100644 lib/modules/cleaner_module/provider/fms_floor_provider.dart delete mode 100644 lib/modules/cleaner_module/provider/fms_location_description_provider.dart delete mode 100644 lib/modules/cleaner_module/widgets/cleaner_preview_dialog.dart create mode 100644 lib/modules/cleaner_module/widgets/multi_image_picker.dart create mode 100644 lib/modules/cleaner_module/widgets/one_time_task_item_view.dart diff --git a/lib/dashboard_latest/dashboard_provider.dart b/lib/dashboard_latest/dashboard_provider.dart index 92ecd60e..c104de4f 100644 --- a/lib/dashboard_latest/dashboard_provider.dart +++ b/lib/dashboard_latest/dashboard_provider.dart @@ -418,9 +418,9 @@ class CategoryTabs { 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: 'UnAssigned', tag: 1, id: 1)); tabs.add(CategoryTabs(label: 'In Progress', tag: 2, id: 2)); - tabs.add(CategoryTabs(label: 'Closed', tag: 4, id: 3)); + tabs.add(CategoryTabs(label: 'Completed', tag: 4, id: 3)); return tabs; } diff --git a/lib/dashboard_latest/widgets/progress_fragment.dart b/lib/dashboard_latest/widgets/progress_fragment.dart index ed7443c2..3316617a 100644 --- a/lib/dashboard_latest/widgets/progress_fragment.dart +++ b/lib/dashboard_latest/widgets/progress_fragment.dart @@ -193,7 +193,7 @@ class ProgressFragment extends StatelessWidget { 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('Closed', snapshot.dashboardCount?.data?.countClose?.toDouble() ?? 0.0, AppColor.green70), + ChartData('Completed', snapshot.dashboardCount?.data?.countClose?.toDouble() ?? 0.0, AppColor.green70), // ChartData('Open', snapshot.dashboardCount?.data?.countOpen?.toDouble() ?? 0.0, AppColor.blueStatus(context)), ]; } else { diff --git a/lib/dashboard_latest/widgets/request_category_fragment.dart b/lib/dashboard_latest/widgets/request_category_fragment.dart index 54c7f1a8..2eeb897b 100644 --- a/lib/dashboard_latest/widgets/request_category_fragment.dart +++ b/lib/dashboard_latest/widgets/request_category_fragment.dart @@ -5,6 +5,7 @@ import 'package:test_sa/dashboard_latest/dashboard_provider.dart'; import 'package:test_sa/dashboard_latest/widgets/request_category_list.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/enums/user_types.dart'; @@ -57,8 +58,8 @@ class RequestCategoryFragment extends StatelessWidget { ? Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - context.translation.allWorkOrder.heading4(context), - 'search'.toSvgAsset(width: 20, height: 20).onPress(() { + 'All Tasks'.addTranslation.heading4(context), + 'add_icon'.toSvgAsset(width: 20, height: 20).onPress(() { Navigator.of(context).pushNamed('/cleaner_search'); }), ], diff --git a/lib/dashboard_latest/widgets/request_category_list.dart b/lib/dashboard_latest/widgets/request_category_list.dart index e48f9f7f..3f7808d6 100644 --- a/lib/dashboard_latest/widgets/request_category_list.dart +++ b/lib/dashboard_latest/widgets/request_category_list.dart @@ -6,6 +6,7 @@ import 'package:test_sa/extensions/widget_extensions.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_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/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'; @@ -76,6 +77,8 @@ class RequestCategoryList extends StatelessWidget { return MedicalGasInspectionItemView(requestData: request); case 15: return DemoRequestItemView(requestData: request); + case 17: + return OneTimeTaskItemView(requestData: request); default: return Container( height: 100, diff --git a/lib/main.dart b/lib/main.dart index 40ba2eda..b6d04cdb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -140,7 +140,6 @@ import 'providers/lookups/yes_no_lookup_provider.dart'; import 'providers/service_request_providers/loan_availability_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_list_view.dart'; import 'modules/cleaner_module/provider/cleaner_provider.dart'; // class MyHttpOverrides extends HttpOverrides { @@ -408,7 +407,6 @@ class MyApp extends StatelessWidget { CreateMedicalGasRequestPage.id: (_) => CreateMedicalGasRequestPage(), CreateDemoRequestPage.id: (_) => CreateDemoRequestPage(), CleanerSearchFormView.routeName: (_) => const CleanerSearchFormView(), - CleanerSearchListView.routeName: (_) => const CleanerSearchListView(), }, ), ), diff --git a/lib/modules/cleaner_module/models/cleaner_data_model.dart b/lib/modules/cleaner_module/models/cleaner_data_model.dart deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/modules/cleaner_module/models/cleaner_form_model.dart b/lib/modules/cleaner_module/models/cleaner_form_model.dart deleted file mode 100644 index f8f7c98d..00000000 --- a/lib/modules/cleaner_module/models/cleaner_form_model.dart +++ /dev/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, - }); -} diff --git a/lib/modules/cleaner_module/models/one_time_task_data.dart b/lib/modules/cleaner_module/models/one_time_task_data.dart index 817ae685..f348300a 100644 --- a/lib/modules/cleaner_module/models/one_time_task_data.dart +++ b/lib/modules/cleaner_module/models/one_time_task_data.dart @@ -24,7 +24,8 @@ class OneTimeTaskData { List? attachments; String? createdDate; String? createdBy; - String? imageRoom; // For captured image + String? imageRoom; // For backward compatibility - deprecated + List? imageRooms; // For multiple captured images (max 2) OneTimeTaskData({ this.id, @@ -53,6 +54,7 @@ class OneTimeTaskData { this.createdDate, this.createdBy, this.imageRoom, + this.imageRooms, }); factory OneTimeTaskData.fromJson(Map json) { @@ -87,6 +89,7 @@ class OneTimeTaskData { createdDate: json['createdDate'], createdBy: json['createdBy'], imageRoom: json['imageRoom'], + imageRooms: json['imageRooms'] != null ? List.from(json['imageRooms']) : null, ); } @@ -118,6 +121,7 @@ class OneTimeTaskData { 'createdDate': createdDate, 'createdBy': createdBy, 'imageRoom': imageRoom, + 'imageRooms': imageRooms, }; } } @@ -204,12 +208,16 @@ class Instruction { class Attachment { int? id; - String? fileName; - String? fileUrl; + 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, @@ -218,6 +226,8 @@ class Attachment { factory Attachment.fromJson(Map json) { return Attachment( id: json['id'], + name: json['name'], + originalName: json['originalName'], fileName: json['fileName'], fileUrl: json['fileUrl'], fileType: json['fileType'], @@ -227,10 +237,11 @@ class Attachment { Map toJson() { return { 'id': id, + 'name': name, + 'originalName': originalName, 'fileName': fileName, 'fileUrl': fileUrl, 'fileType': fileType, }; } } - diff --git a/lib/modules/cleaner_module/pages/cleaner_request_item_view.dart b/lib/modules/cleaner_module/pages/cleaner_request_item_view.dart deleted file mode 100644 index 0ffc8e96..00000000 --- a/lib/modules/cleaner_module/pages/cleaner_request_item_view.dart +++ /dev/null @@ -1,69 +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/string_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), - if (result.createdDate != null) - Text( - result.createdDate!.toServiceRequestCardFormat, - 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); - } -} diff --git a/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart b/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart index 9cc1f1d8..e2761c08 100644 --- a/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart +++ b/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart @@ -5,6 +5,7 @@ import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/modules/cleaner_module/models/task_type.dart'; import 'package:test_sa/modules/cleaner_module/models/qr_scan_location_data.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/provider/task_type_provider.dart'; import 'package:test_sa/modules/cleaner_module/pages/one_time_task_view.dart'; @@ -142,10 +143,16 @@ class _CleanerSearchFormViewState extends State { final taskId = data['id']; if (mounted && taskId != null) { - // Navigate to OneTimeTaskView + // 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), + builder: (context) => OneTimeTaskView( + taskId: taskId, + initialData: taskData, + ), ), ); } @@ -289,51 +296,50 @@ class _CleanerSearchFormViewState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - 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.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), ], diff --git a/lib/modules/cleaner_module/pages/cleaner_search_list_view.dart b/lib/modules/cleaner_module/pages/cleaner_search_list_view.dart deleted file mode 100644 index 87e25e22..00000000 --- a/lib/modules/cleaner_module/pages/cleaner_search_list_view.dart +++ /dev/null @@ -1,183 +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 createState() => _CleanerSearchListViewState(); -} - -class _CleanerSearchListViewState extends State { - List _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?; - if (args != null && args['searchResults'] != null) { - final results = args['searchResults'] as List; - _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 _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; - 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: null), - ), - ); - }, - ); - }, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/modules/cleaner_module/pages/one_time_task_preview_screen.dart b/lib/modules/cleaner_module/pages/one_time_task_preview_screen.dart new file mode 100644 index 00000000..18e4d0ad --- /dev/null +++ b/lib/modules/cleaner_module/pages/one_time_task_preview_screen.dart @@ -0,0 +1,373 @@ +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 '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) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Working Hours', + style: AppTextStyles.heading5.copyWith( + color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, + fontWeight: FontWeight.w600, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Total Working Hours', + style: AppTextStyles.bodyText.copyWith( + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, + ), + ), + Text( + '${totalWorkingHours.toStringAsFixed(2)} hrs', + style: AppTextStyles.heading6.copyWith( + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, + fontWeight: FontWeight.w600, + ), + ), + ], + ).paddingAll(12), + ], + ).toShadowContainer(context, padding: 12); + } + + 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, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/cleaner_module/pages/one_time_task_view.dart b/lib/modules/cleaner_module/pages/one_time_task_view.dart index d1037aa2..e8e1ce28 100644 --- a/lib/modules/cleaner_module/pages/one_time_task_view.dart +++ b/lib/modules/cleaner_module/pages/one_time_task_view.dart @@ -1,12 +1,17 @@ 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/widgets/cleaner_preview_dialog.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'; @@ -16,12 +21,16 @@ 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}) : super(key: key); + const OneTimeTaskView({Key? key, required this.taskId, this.initialData}) : super(key: key); @override State createState() => _OneTimeTaskViewState(); @@ -31,11 +40,25 @@ class _OneTimeTaskViewState extends State { OneTimeTaskData? _taskData; bool _isLoading = false; DateTime? _timerStart; + List? _checklistItems; // Cache checklist items @override void initState() { super.initState(); - _loadTaskData(); + // 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 _loadTaskData() async { @@ -49,6 +72,19 @@ class _OneTimeTaskViewState extends State { 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 @@ -80,12 +116,18 @@ class _OneTimeTaskViewState extends State { Widget build(BuildContext context) { return Scaffold( appBar: DefaultAppBar( - title: 'One Time Task', - onWillPopScope: isClosed - ? null - : () { - _showExitDialog(); - }, + title: 'One Time Task'.addTranslation, + onWillPopScope: null, + onBackPress: (){ + WidgetsBinding.instance.addPostFrameCallback((_) { + UserProvider userProvider = Provider.of(context, listen: false); + DashBoardProvider dashBoardProvider = Provider.of(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() @@ -110,10 +152,10 @@ class _OneTimeTaskViewState extends State { _updateInstructions(value); }); }, - capturedImageBase64: _taskData!.imageRoom, - onImageCaptured: (imageBase64) { + capturedImages: _taskData!.imageRooms ?? [], + onImagesChanged: (images) { setState(() { - _taskData!.imageRoom = imageBase64; + _taskData!.imageRooms = images; }); }, ), @@ -124,22 +166,10 @@ class _OneTimeTaskViewState extends State { if (!isClosed) ...[ FooterActionButton.footerContainer( context: context, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - AppFilledButton( - label: context.translation.save, - buttonColor: context.isDark ? AppColor.neutral20 : AppColor.white60, - textColor: context.isDark ? Colors.white : AppColor.black10, - onPressed: () => Navigator.pop(context), - ).expanded, - 12.width, - AppFilledButton( - label: context.translation.complete, - buttonColor: AppColor.primary10, - onPressed: _onComplete, - ).expanded, - ], + child: AppFilledButton( + label: context.translation.complete, + buttonColor: AppColor.primary10, + onPressed: _onComplete, ), ), ] @@ -149,7 +179,7 @@ class _OneTimeTaskViewState extends State { : NoDataFound(message: context.translation.noDataFound).center, ).handlePopScope( cxt: context, - showPopUp: !isClosed, + showPopUp: false, onSave: () { Navigator.pop(context); }, @@ -166,25 +196,19 @@ class _OneTimeTaskViewState extends State { children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, children: [ if (_taskData?.statusName != null) - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: _getStatusColor().withOpacity(0.1), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - _taskData!.statusName!, - style: AppTextStyles.bodyText.copyWith( - color: _getStatusColor(), - fontWeight: FontWeight.w600, - ), - ), + StatusLabel( + radius: 4, + label: _taskData!.statusName!, + textColor: AppColor.getHistoryLogStatusTextColorByName(_taskData!.statusName!), + backgroundColor: AppColor.getHistoryLogStatusColorByName(_taskData!.statusName!), ), if (_taskData?.createdDate != null) Text( - _formatDate(_taskData!.createdDate!), + _taskData!.createdDate!.toString().toServiceRequestCardFormat, + textAlign: TextAlign.end, style: AppTextStyles.tinyFont.copyWith( color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, ), @@ -208,47 +232,24 @@ class _OneTimeTaskViewState extends State { ), ), 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 + 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, - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: AppColor.primary10.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: AppColor.primary10), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Working Time:', - style: AppTextStyles.bodyText.copyWith( - fontWeight: FontWeight.w600, - color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, - ), - ), - Text( - _formatDuration(DateTime.now().difference(_timerStart!)), - style: AppTextStyles.heading4.copyWith( - color: AppColor.primary10, - fontWeight: FontWeight.bold, - ), - ), - ], + AppTimer( + label: 'Working Time', + timer: TimerModel( + startAt: _timerStart, + endAt: null, + durationInSecond: 0, ), + enabled: false, // Disable controls, just show the timer + showTimer: true, ), ], ], @@ -270,15 +271,35 @@ class _OneTimeTaskViewState extends State { } List _convertInstructionsToChecklist() { - return _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() ?? []; + // 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 checklist) { @@ -295,21 +316,24 @@ class _OneTimeTaskViewState extends State { } void _onComplete() { - // Validate mandatory items - final uncheckedMandatory = _taskData?.instructions?.where((item) => - (item.isRequired ?? false) && !(item.isDone ?? false) - ).toList() ?? []; + // 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; } - // Show preview dialog - _showPreviewDialog(); - } + // 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; + } - void _showPreviewDialog() { // Calculate working hours double totalWorkingHours = 0; if (_timerStart != null) { @@ -317,28 +341,21 @@ class _OneTimeTaskViewState extends State { totalWorkingHours = duration.inSeconds / 3600.0; } - CleanerPreviewDialog.show( - context, - model: _convertToRecurrentWoData(), - totalWorkingHours: totalWorkingHours, - onConfirm: _performComplete, + // 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 + }, + ), + ), ); } - // Convert OneTimeTaskData to RecurrentWoData for preview dialog compatibility - dynamic _convertToRecurrentWoData() { - return { - 'siteName': _taskData?.siteName, - 'buildingName': _taskData?.buildingName, - 'floorName': _taskData?.floorName, - 'departmentName': _taskData?.departmentName, - 'roomName': _taskData?.roomName, - 'locationDescription': _taskData?.locationDescription, - 'taskNo': 'OT-${_taskData?.id}', - 'title': _taskData?.oneTimeSetting?.taskTypeName, - }; - } - Future _performComplete() async { showDialog( context: context, @@ -348,20 +365,33 @@ class _OneTimeTaskViewState extends State { try { final instructionsPayload = _taskData?.instructions?.map((instruction) { - return { - 'id': instruction.id, - 'instructionTextId': instruction.instructionTextId, - 'instructionText': instruction.instructionText, - 'isRequired': instruction.isRequired, - 'isDone': instruction.isDone, - 'comment': instruction.comment, - }; - }).toList() ?? []; + 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, - imageRoom: _taskData?.imageRoom, + attachments: attachmentsPayload, ); if (mounted) Navigator.pop(context); // Close loading dialog @@ -387,20 +417,6 @@ class _OneTimeTaskViewState extends State { } } - void _showExitDialog() { - showDialog( - context: context, - builder: (context) => ConfirmDialog( - message: 'Do you want to exit without completing?', - title: 'Confirm', - onTap: () { - Navigator.pop(context); // Close dialog - Navigator.pop(context); // Exit screen - }, - ), - ); - } - void _showErrorDialog(BuildContext context, String message) { showDialog( context: context, @@ -412,26 +428,14 @@ class _OneTimeTaskViewState extends State { ); } - Color _getStatusColor() { - if (_taskData?.statusValue == 2) { - return Colors.green; - } - return AppColor.primary10; - } - - String _formatDate(String dateStr) { - try { - final date = DateTime.parse(dateStr); - return '${date.day}/${date.month}/${date.year}'; - } catch (e) { - return dateStr; + @override + void dispose() { + // Dispose all checklist item controllers + if (_checklistItems != null) { + for (var item in _checklistItems!) { + item.dispose(); + } } - } - - String _formatDuration(Duration duration) { - final hours = duration.inHours; - final minutes = duration.inMinutes.remainder(60); - final seconds = duration.inSeconds.remainder(60); - return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + super.dispose(); } } diff --git a/lib/modules/cleaner_module/provider/cleaner_provider.dart b/lib/modules/cleaner_module/provider/cleaner_provider.dart index 18a442b3..ecae5532 100644 --- a/lib/modules/cleaner_module/provider/cleaner_provider.dart +++ b/lib/modules/cleaner_module/provider/cleaner_provider.dart @@ -308,7 +308,7 @@ class CleanerProvider extends ChangeNotifier { Response response; try { response = await ApiManager.instance.get( - '${URLs.getRequestById}/$id', + '${URLs.getRequestById}?id=$id', ); // Parse the response body @@ -336,6 +336,7 @@ class CleanerProvider extends ChangeNotifier { "data": null, "message": "Failed to retrieve task. Please check your connection and try again.", }; + } } @@ -343,7 +344,7 @@ class CleanerProvider extends ChangeNotifier { Future> completeOneTimeTask({ required int id, required List> instructions, - String? imageRoom, + List>? attachments, }) async { Response response; try { @@ -352,7 +353,7 @@ class CleanerProvider extends ChangeNotifier { body: { "id": id, "instructions": instructions, - "imageRoom": imageRoom, + "attachments": attachments ?? [], }, showToast: false, ); diff --git a/lib/modules/cleaner_module/provider/fms_building_provider.dart b/lib/modules/cleaner_module/provider/fms_building_provider.dart deleted file mode 100644 index 39b3f0b3..00000000 --- a/lib/modules/cleaner_module/provider/fms_building_provider.dart +++ /dev/null @@ -1,83 +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/building.dart'; - -class FmsBuildingProvider extends ChangeNotifier { - //reset provider data - void reset() { - _buildings = null; - _stateCode = null; - } - - // state code of current request to defied error message - int? _stateCode; - int? get stateCode => _stateCode; - - // contain buildings data - List? _buildings; - List? get buildings => _buildings; - - // loading state - bool _loading = false; - bool get isLoading => _loading; - - set isLoading(bool isLoading) { - _loading = isLoading; - notifyListeners(); - } - - /// Get buildings by site ID - Future getBuildingsBySiteId(int siteId) async { - if (_loading == true) return -2; - _loading = true; - notifyListeners(); - Response response; - try { - response = await ApiManager.instance.post( - URLs.getCustomerBuildings, - body: { - "siteId": siteId, - }, - ); - _stateCode = response.statusCode; - if (response.statusCode >= 200 && response.statusCode < 300) { - // client's request was successfully received - List buildingsListJson = json.decode(response.body)["data"]; - _buildings = buildingsListJson.map((building) => Building.fromJson(building)).toList(); - } - _loading = false; - notifyListeners(); - return response.statusCode; - } catch (error) { - _loading = false; - _stateCode = -1; - notifyListeners(); - return -1; - } - } - - /// Get buildings list directly without provider state - Future> getBuildingsListBySiteId(int siteId) async { - Response response; - try { - response = await ApiManager.instance.post( - URLs.getCustomerBuildings, - body: { - "siteId": siteId, - }, - ); - - if (response.statusCode >= 200 && response.statusCode < 300) { - List buildingsListJson = json.decode(response.body)["data"]; - return buildingsListJson.map((building) => Building.fromJson(building)).toList(); - } - return []; - } catch (error) { - return []; - } - } -} diff --git a/lib/modules/cleaner_module/provider/fms_floor_provider.dart b/lib/modules/cleaner_module/provider/fms_floor_provider.dart deleted file mode 100644 index d3700761..00000000 --- a/lib/modules/cleaner_module/provider/fms_floor_provider.dart +++ /dev/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? _floors; - List? 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 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> 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 []; - } - } -} diff --git a/lib/modules/cleaner_module/provider/fms_location_description_provider.dart b/lib/modules/cleaner_module/provider/fms_location_description_provider.dart deleted file mode 100644 index db799dc4..00000000 --- a/lib/modules/cleaner_module/provider/fms_location_description_provider.dart +++ /dev/null @@ -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? locationDescriptions; - List? deptRoom; - - LocationDescriptionResponse({ - this.locationDescriptions, - this.deptRoom, - }); - - factory LocationDescriptionResponse.fromJson(Map 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 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? _locationDescriptions; - List? get locationDescriptions => _locationDescriptions; - - // contain dept room list - List? _deptRoomList; - List? 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 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 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; - } - } -} diff --git a/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart b/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart index 9ec79ab6..804372c1 100644 --- a/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart +++ b/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart @@ -1,10 +1,6 @@ import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.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/int_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/common_widgets/app_text_form_field.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 { final List checklistItems; final Function(List)? onChecklistChanged; - final String? capturedImageBase64; - final Function(String?)? onImageCaptured; - bool isClosed; + final List capturedImages; + final Function(List)? onImagesChanged; + 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 State createState() => _CleanerChecklistWidgetState(); @@ -191,11 +196,23 @@ class _CleanerChecklistWidgetState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Captured Image'.addTranslation, - style: AppTextStyles.heading6.copyWith( - color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + 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, _buildImageWidget(context), @@ -203,205 +220,148 @@ class _CleanerChecklistWidgetState extends State { ).toShadowContainer(context); } + static const int maxImages = 2; + /// Build the image widget with loading indicator for network images Widget _buildImageWidget(BuildContext context) { - if (widget.capturedImageBase64 == null || widget.capturedImageBase64!.isEmpty) { - // Show camera icon when no image - return Container( - height: 200, - width: double.infinity, - decoration: BoxDecoration( - color: AppColor.fieldBgColor(context), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - 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); - } - }); - } + if (widget.capturedImages.isEmpty) { + // Show add image button when no image + return GestureDetector( + onTap: widget.isClosed ? null : () async { + // Only allow camera - pick one image at a time + final pickedFile = await ImagePicker().pickImage( + source: ImageSource.camera, + imageQuality: 70, + maxWidth: 800, + maxHeight: 800, + ); - Widget imageWidget; + if (pickedFile != null) { + final bytes = await pickedFile.readAsBytes(); + final base64Image = base64Encode(bytes); + final fileName = pickedFile.path.split("/").last; + final formattedImage = "$fileName|$base64Image"; - final imageString = widget.capturedImageBase64!; + // Add to existing images (max 2) + final updatedImages = List.from(widget.capturedImages); + updatedImages.add(formattedImage); - // First check if it's base64 encoded (contains | separator) - if (imageString.contains('|')) { - // Format: "filename|base64data" - try { - final base64Data = imageString.split('|').last; - 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 - if (_isLocalUrl(imageString)) { - try { - 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 - final imageUrl = URLs.getFileUrl(imageString); - if (imageUrl != null) { - imageWidget = Image.network( - imageUrl, - fit: BoxFit.cover, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) { - return child; + widget.onImagesChanged?.call(updatedImages); } - 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); }, - ); - 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(), - ], - ), - ), + 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, ), ), - ); - }), - _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 - Widget _buildCameraIcon(BuildContext context) { - return Container( - height: 200, - width: double.infinity, - decoration: BoxDecoration( - color: AppColor.fieldBgColor(context), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.camera_alt, - size: 48, - color: context.isDark ? AppColor.white10 : AppColor.black10, - ), + // If images are from API (URLs), use FilesList widget like ServiceRequestDetailView + if (hasApiImages && widget.isClosed) { + return FilesList( + images: widget.capturedImages, + showAsListView: false, + ); + } + + // Otherwise, use MultiImagePicker for base64 images or when editing + return MultiImagePicker( + imageUrls: widget.capturedImages, + enabled: !widget.isClosed, + 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 - /// Same logic as multi_image_picker_item.dart - bool _isLocalUrl(String url) { - if (url.isEmpty) return false; - return url.startsWith("/") || url.startsWith("file://") || (url.length > 1 && url.substring(1).startsWith(':\\')); + /// Build image widget for viewing based on source type + Widget _buildImageForViewing(String imageUrl) { + try { + // Check if it's a URL from API + 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 @@ -409,26 +369,6 @@ class _CleanerChecklistWidgetState extends State { // Note: Don't dispose controllers here as they're managed by ChecklistItem 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: const Icon(Icons.delete_rounded, color: Color(0xffF63939))) - .onPress(() { - widget.onImageCaptured?.call(null); - }); - } } /// Model class for checklist items @@ -446,10 +386,7 @@ class ChecklistItem { required this.isChecked, required this.comment, this.isMandatory = false, - }) : commentController = TextEditingController(text: comment) { - // Update the controller text whenever comment changes - commentController.text = comment; - } + }) : commentController = TextEditingController(text: comment); // Convert to JSON for API submission Map toJson() { diff --git a/lib/modules/cleaner_module/widgets/cleaner_preview_dialog.dart b/lib/modules/cleaner_module/widgets/cleaner_preview_dialog.dart deleted file mode 100644 index 6abd4d04..00000000 --- a/lib/modules/cleaner_module/widgets/cleaner_preview_dialog.dart +++ /dev/null @@ -1,457 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:test_sa/controllers/api_routes/urls.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/models/ppm/recurrent_wo.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/views/widgets/buttons/app_back_button.dart'; -import 'package:test_sa/modules/cm_module/cm_request_utils.dart'; -import 'package:test_sa/views/widgets/total_working_time_detail_bottomsheet.dart'; - -class CleanerPreviewDialog extends StatelessWidget { - final RecurrentWoData model; - final double totalWorkingHours; - final VoidCallback onConfirm; - - const CleanerPreviewDialog({ - Key? key, - required this.model, - required this.totalWorkingHours, - required this.onConfirm, - }) : super(key: key); - - static void show( - BuildContext context, { - required RecurrentWoData model, - required double totalWorkingHours, - required VoidCallback onConfirm, - }) { - showDialog( - context: context, - barrierDismissible: false, - builder: (dialogContext) => CleanerPreviewDialog( - model: model, - totalWorkingHours: totalWorkingHours, - onConfirm: onConfirm, - ), - ); - } - - @override - Widget build(BuildContext context) { - return Dialog( - backgroundColor: Colors.transparent, - insetPadding: const EdgeInsets.symmetric(horizontal: 16), - child: Container( - constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.85), - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - borderRadius: BorderRadius.circular(14), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Header matching DefaultAppBar style - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(14), - topRight: Radius.circular(14), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: 'Confirm Details'.heading3(context).custom( - fontWeight: FontWeight.w500, - ), - ), - Icon( - Icons.close, - color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, - size: 24, - ).onPress(() => Navigator.pop(context)), - ], - ), - ), - Divider(height: 1, color: context.isDark ? AppColor.neutral60 : AppColor.neutral100), - // Content - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildPreviewSection(context, 'Task Information', [ - _buildPreviewRow(context, context.translation.title, model.title ?? '-'), - _buildPreviewRow(context, context.translation.taskNo, model.taskNo ?? '-'), - _buildPreviewRow(context, context.translation.site, model.site?.siteName ?? '-'), - _buildPreviewRow(context, context.translation.department, model.department?.name ?? '-'), - _buildPreviewRow(context, context.translation.floor, model.floor?.name ?? '-'), - _buildPreviewRow(context, context.translation.room, model.room?.name ?? '-'), - _buildPreviewRow(context, context.translation.assignedEmployee, model.engineer?.userName ?? '-'), - ]), - 16.height, - // Total Working Time Section - Using WorkingTimeTile design - _buildPreviewSection(context, 'Total Working Time', [ - _buildWorkingTimeTile(context), - ]), - 16.height, - if (model.planRecurrentTaskCheckLists != null && model.planRecurrentTaskCheckLists!.isNotEmpty) ...[ - _buildPreviewSection(context, 'Checklist', [ - ...model.planRecurrentTaskCheckLists! - .map( - (item) => Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - item.isChecked ? Icons.check_circle : Icons.circle_outlined, - color: item.isChecked ? AppColor.primary10 : AppColor.neutral120, - size: 20, - ), - 8.width, - RichText( - text: TextSpan( - children: [ - TextSpan( - text: item.title, - style: AppTextStyles.bodyText2.copyWith( - color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, - ), - ), - if (item.isMandatory) - TextSpan( - text: ' *', - style: AppTextStyles.bodyText2.copyWith( - color: Colors.red, - ), - ), - ], - ), - ).expanded, - ], - ), - // Show comment if item is checked and has a comment - if (item.isChecked && item.comment.isNotEmpty == true) ...[ - 8.height, - Container( - width: double.infinity, - padding: const EdgeInsets.all(8), - margin: const EdgeInsets.only(left: 28), - decoration: BoxDecoration( - color: context.isDark ? AppColor.neutral60.withValues(alpha: 0.3) : AppColor.neutral100.withValues(alpha: 0.5), - borderRadius: BorderRadius.circular(6), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - 'Comment:'.bodyText2(context).custom( - fontWeight: FontWeight.w600, - color: context.isDark ? AppColor.neutral30 : AppColor.neutral120, - ), - 4.height, - item.comment.bodyText2(context).custom( - color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, - ), - ], - ), - ), - ], - ], - ), - ), - ) - .toList(), - ]), - 16.height, - ], - if (model.comment != null && model.comment!.isNotEmpty) ...[ - _buildPreviewSection(context, context.translation.comment, [ - model.comment!.bodyText(context), - ]), - 16.height, - ], - if (model.imageRoom != null && model.imageRoom!.isNotEmpty) ...[ - _buildPreviewSection(context, 'Captured Image', [ - _buildImagePreview(context, model.imageRoom!), - ]), - 16.height, - ], - ], - ), - ), - ), - // Footer with Confirm button - Container( - padding: EdgeInsets.all(16), - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - border: Border(top: BorderSide(color: context.isDark ? AppColor.neutral60 : AppColor.neutral100)), - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(14), - bottomRight: Radius.circular(14), - ), - ), - child: AppFilledButton( - label: context.translation.complete, - buttonColor: AppColor.primary10, - onPressed: () { - Navigator.pop(context); - onConfirm(); - }, - ), - ), - ], - ), - ), - ); - } - - Widget _buildPreviewSection(BuildContext context, String title, List children) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - title.heading6(context).custom(fontWeight: FontWeight.w600), - 8.height, - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: children, - ).toShadowContainer(context, padding: 12), - ], - ); - } - - Widget _buildPreviewRow(BuildContext context, String label, String value) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 2, - child: '$label:'.bodyText2(context).custom(color: AppColor.neutral120), - ), - 8.width, - Expanded( - flex: 3, - child: value.bodyText2(context).custom(color: AppColor.neutral120), - ), - ], - ), - ); - } - - Widget _buildWorkingTimeTile(BuildContext context) { - // Create timer list from model data, same as in RecurrentTaskInfoWidget - final timerList = model.planRecurrentTaskTimers?.map((e) { - return TimerHistoryModel( - id: e.id, - startTime: e.startTime, - endTime: e.endTime, - workingHours: e.workingHours, - ); - }).toList() ?? []; - - return Container( - height: 56.toScreenHeight, - padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth), - alignment: Alignment.centerLeft, - decoration: BoxDecoration( - color: AppColor.fieldBgColor(context), - borderRadius: BorderRadius.circular(10), - ), - child: Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "Total Working Time", - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: context.isDark ? null : AppColor.neutral20, - fontWeight: FontWeight.w500, - ), - ), - Text( - " ${CMRequestUtils.formatTotalWorkingHours(totalWorkingHours)}", - style: Theme.of(context).textTheme.bodyMedium, - ), - ], - ).expanded, - 8.width, - const Icon(Icons.info_outline_rounded, color: AppColor.primary10), - ], - ), - ).onPress(() async { - await showModalBottomSheet( - context: context, - isDismissible: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), - clipBehavior: Clip.antiAliasWithSaveLayer, - builder: (BuildContext context) => TotalWorkingTimeDetailBottomSheet(timerList: timerList), - ); - }); - } - - Widget _buildImagePreview(BuildContext context, String imageString) { - Widget imageWidget; - - // First check if it's base64 encoded (contains | separator) - if (imageString.contains('|')) { - // Format: "filename|base64data" - try { - final base64Data = imageString.split('|').last; - imageWidget = Image.memory(base64Decode(base64Data), fit: BoxFit.cover); - return 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: imageWidget).center, - const ABackButton(), - ], - ), - ), - ), - ), - ); - }); - } catch (e) { - debugPrint('Error decoding base64 image: $e'); - return _buildCameraIcon(context); - } - } - - // Check if it's a local file path - if (_isLocalUrl(imageString)) { - try { - imageWidget = Image.file(File(imageString), fit: BoxFit.cover); - return 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: imageWidget).center, - const ABackButton(), - ], - ), - ), - ), - ), - ); - }); - } 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 - 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); - }, - ); - return 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: imageWidget).center, - const ABackButton(), - ], - ), - ), - ), - )); - }); - } - - return _buildCameraIcon(context); - } - - /// Build camera icon widget for fallback - Widget _buildCameraIcon(BuildContext context) { - return Container( - height: 200, - width: double.infinity, - decoration: BoxDecoration( - color: AppColor.fieldBgColor(context), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.camera_alt, - size: 48, - color: context.isDark ? AppColor.white10 : AppColor.black10, - ), - ); - } - - /// Check if the URL is a local file path - bool _isLocalUrl(String url) { - return url.startsWith('/') || url.startsWith('file://') || (url.length > 1 && url[1] == ':'); - } -} diff --git a/lib/modules/cleaner_module/widgets/multi_image_picker.dart b/lib/modules/cleaner_module/widgets/multi_image_picker.dart new file mode 100644 index 00000000..002acbe4 --- /dev/null +++ b/lib/modules/cleaner_module/widgets/multi_image_picker.dart @@ -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 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(), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/modules/cleaner_module/widgets/one_time_task_item_view.dart b/lib/modules/cleaner_module/widgets/one_time_task_item_view.dart new file mode 100644 index 00000000..9adfdd58 --- /dev/null +++ b/lib/modules/cleaner_module/widgets/one_time_task_item_view.dart @@ -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(); + } +} diff --git a/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart b/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart index b350d626..0857e414 100644 --- a/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart +++ b/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart @@ -6,8 +6,6 @@ import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.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/cleaner_module/widgets/cleaner_checklist_widget.dart'; -import 'package:test_sa/modules/cleaner_module/widgets/cleaner_preview_dialog.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'; @@ -35,21 +33,11 @@ class RecurrentWorkOrderView extends StatefulWidget { class _RecurrentWorkOrderViewState extends State { AllRequestsProvider? allRequestsProvider; - DateTime? _housekeeperTimerStart; @override void initState() { allRequestsProvider = Provider.of(context, listen: false); - Provider.of(context, listen: false).getRecurrentWoById(id: widget.taskId!).then((_) { - // Start timer automatically for housekeeper type - final provider = Provider.of(context, listen: false); - bool isHouseKeeper = provider.recurrentWoData?.planRecurrentTaskCheckLists != null && - provider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty; - - if (isHouseKeeper && provider.recurrentWoData?.status?.value != 1) { - _housekeeperTimerStart = DateTime.now(); - } - }); + Provider.of(context, listen: false).getRecurrentWoById(id: widget.taskId!); super.initState(); } @@ -64,14 +52,13 @@ class _RecurrentWorkOrderViewState extends State { Widget build(BuildContext context) { return Consumer(builder: (context, requestProvider, child) { isClosed = requestProvider.recurrentWoData?.status?.value == 1; - bool isHouseKeeper = requestProvider.recurrentWoData!.planRecurrentTaskCheckLists != null && requestProvider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty; return Scaffold( appBar: DefaultAppBar( title: context.translation.recurrentWo, onWillPopScope: isClosed ? null : () { - _updateTask(context: context, status: 0, isHouseKeeper: isHouseKeeper); + _updateTask(context: context, status: 0); }, ), body: allRequestsProvider!.isLoading @@ -89,8 +76,8 @@ class _RecurrentWorkOrderViewState extends State { RecurrentTaskInfoWidget( isClosed: isClosed, model: requestProvider.recurrentWoData, - isHouseKeeper: isHouseKeeper, - timerStartTime: _housekeeperTimerStart, + isHouseKeeper: false, + timerStartTime: null, ), IgnorePointer( ignoring: isClosed, @@ -103,24 +90,6 @@ class _RecurrentWorkOrderViewState extends State { ) : const SizedBox(), ), - 16.height, - isHouseKeeper - ? 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, @@ -134,13 +103,13 @@ class _RecurrentWorkOrderViewState extends State { label: context.translation.save, buttonColor: context.isDark ? AppColor.neutral20 : AppColor.white60, textColor: context.isDark ? Colors.white : AppColor.black10, - onPressed: () => _updateTask(context: context, status: 0, isHouseKeeper: isHouseKeeper), + onPressed: () => _updateTask(context: context, status: 0), ).expanded, 12.width, AppFilledButton( label: context.translation.complete, buttonColor: AppColor.primary10, - onPressed: () => _updateTask(context: context, status: 1, isHouseKeeper: isHouseKeeper), + onPressed: () => _updateTask(context: context, status: 1), ).expanded, ], ), @@ -154,77 +123,45 @@ class _RecurrentWorkOrderViewState extends State { cxt: context, showPopUp: !isClosed, onSave: () { - _updateTask(context: context, status: 0, isHouseKeeper: isHouseKeeper); + _updateTask(context: context, status: 0); }); }); } - void _updateTask({required BuildContext context, required int status, required bool isHouseKeeper}) async { - AllRequestsProvider allRequestsProvider = Provider.of(context, listen: false); - - // Only validate checklist if it exists and status is complete - if (status == 1 && allRequestsProvider.recurrentWoData?.planRecurrentTaskCheckLists != null && allRequestsProvider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty) { - // Validate mandatory checklist items only - if (!_validateChecklist(context, allRequestsProvider.recurrentWoData!)) { - return; - } - } - - // For housekeeper: show preview dialog before completing - if (isHouseKeeper && status == 1) { - _showPreviewDialog(context, allRequestsProvider); - return; - } - + void _updateTask({required BuildContext context, required int status}) async { // Proceed with update - _performUpdate(context: context, status: status, isHouseKeeper: isHouseKeeper); + _performUpdate(context: context, status: status); } - void _performUpdate({required BuildContext context, required int status, required bool isHouseKeeper}) async { + void _performUpdate({required BuildContext context, required int status}) async { AllRequestsProvider allRequestsProvider = Provider.of(context, listen: false); - if (validate(context: context, model: allRequestsProvider.recurrentWoData!, status: status, isHouseKeeper: isHouseKeeper)) { - // For housekeeper: automatically add timer data - if (isHouseKeeper && _housekeeperTimerStart != null) { - DateTime endTime = DateTime.now(); - int durationInSecond = endTime.difference(_housekeeperTimerStart!).inSeconds; + if (validate(context: context, model: allRequestsProvider.recurrentWoData!, status: status)) { + // Use existing timer logic + allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers ?? []; - allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers ?? []; + if (allRequestsProvider.recurrentWoData?.recurrentWoTimePicker != null) { + int durationInSecond = allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt!.difference(allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.startAt!).inSeconds; allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add( PlanRecurrentTaskTimers( id: 0, - startTime: _housekeeperTimerStart!.toIso8601String(), - endTime: endTime.toIso8601String(), + startTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.startAt!.toIso8601String(), + endTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt?.toIso8601String(), workingHours: ((durationInSecond) / 60 / 60), ), ); - } else { - // For non-housekeeper: use existing timer logic - allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers ?? []; - - if (allRequestsProvider.recurrentWoData?.recurrentWoTimePicker != null) { - 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), - ), - ); - }); } + 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), + ), + ); + }); final uniqueTimersMap = {}; allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.forEach((timer) { @@ -268,38 +205,6 @@ class _RecurrentWorkOrderViewState extends State { } } - void _showPreviewDialog(BuildContext context, AllRequestsProvider provider) { - final model = provider.recurrentWoData!; - - // Calculate total working time - double totalWorkingHours = 0; - final timers = model.planRecurrentTaskTimers ?? []; - totalWorkingHours = timers.fold(0.0, (sum, item) { - if (item.startTime == null || item.endTime == null) return sum; - try { - final start = DateTime.parse(item.startTime!); - final end = DateTime.parse(item.endTime!); - final diffInHours = end.difference(start).inSeconds / 3600.0; - return sum + diffInHours; - } catch (_) { - return sum; - } - }); - - // Add current timer if running - if (_housekeeperTimerStart != null) { - final currentDuration = DateTime.now().difference(_housekeeperTimerStart!).inSeconds / 3600.0; - totalWorkingHours += currentDuration; - } - - CleanerPreviewDialog.show( - context, - model: model, - totalWorkingHours: totalWorkingHours, - onConfirm: () => _performUpdate(context: context, status: 1, isHouseKeeper: true), - ); - } - void _showErrorDialog(BuildContext context, String message) { showDialog( context: context, @@ -312,32 +217,8 @@ class _RecurrentWorkOrderViewState extends State { } } -bool _validateChecklist(BuildContext context, RecurrentWoData model) { - // Check if all mandatory checklist items are checked - final uncheckedMandatory = model.planRecurrentTaskCheckLists!.where((item) => item.isMandatory && !item.isChecked).toList(); - - if (uncheckedMandatory.isNotEmpty) { - showDialog( - context: context, - builder: (context) => ConfirmDialog( - message: "Please complete all mandatory tasks (marked with *)", - title: 'Validation Error', - onTap: () => Navigator.pop(context), - ), - ); - return false; - } - - return true; -} - -bool validate({required BuildContext context, required RecurrentWoData model, required int status, required bool isHouseKeeper}) { - // Skip timer validation for housekeeper type - if (isHouseKeeper) { - return true; - } - - // Original timer validation logic for non-housekeeper types +bool validate({required BuildContext context, required RecurrentWoData model, required int status}) { + // Timer validation logic bool isTimerPickerEnable = ApiManager.instance.assetGroup?.enabledEngineerTimer ?? false; if (isTimerPickerEnable) { diff --git a/lib/new_views/common_widgets/app_bottom_nav_bar.dart b/lib/new_views/common_widgets/app_bottom_nav_bar.dart index 611afb27..cc12846e 100644 --- a/lib/new_views/common_widgets/app_bottom_nav_bar.dart +++ b/lib/new_views/common_widgets/app_bottom_nav_bar.dart @@ -23,9 +23,9 @@ class AppBottomNavigationBar extends StatelessWidget { List navItems = [ NavItemModel(0, "overview", context.translation.overview), - NavItemModel(1, "request_icon", context.translation.workOrder), + if(!isHouseKeeper) NavItemModel(1, "request_icon", context.translation.workOrder), 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), ]; diff --git a/lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart b/lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart index bf3db783..1ad1297c 100644 --- a/lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart +++ b/lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart @@ -26,8 +26,6 @@ class RecurrentWoItemView extends StatelessWidget { @override Widget build(BuildContext context) { final user = Provider.of(context, listen: false).user; - final settingProvider = Provider.of(context, listen: false); - final bool isHouseKeeper = user?.type == UsersTypes.houseKeeper; if (requestData != null) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -61,16 +59,11 @@ class RecurrentWoItemView extends StatelessWidget { // '${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), + 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, @@ -85,19 +78,7 @@ class RecurrentWoItemView extends StatelessWidget { ), ], ).toShadowContainer(context, withShadow: showShadow).onPress(() { - if (isHouseKeeper && (requestData?.statusValue == 4)) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CleanerSearchDetailsView( - requestId: requestData!.id ?? 0, - taskResult: null, - ), - ), - ); - } else { - Navigator.of(context).push(MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: requestData!.id))); - } + Navigator.of(context).push(MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: requestData!.id))); }); } if (requestDetails != null) { @@ -152,17 +133,6 @@ class RecurrentWoItemView extends StatelessWidget { ), ], ).toShadowContainer(context, withShadow: showShadow).onPress(() async { - if (isHouseKeeper && (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))); }); } diff --git a/lib/new_views/pages/land_page/requests/request_paginated_listview.dart b/lib/new_views/pages/land_page/requests/request_paginated_listview.dart index a5fa049b..704836c4 100644 --- a/lib/new_views/pages/land_page/requests/request_paginated_listview.dart +++ b/lib/new_views/pages/land_page/requests/request_paginated_listview.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:test_sa/extensions/int_extensions.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/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/incident_module/incident_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); case 16: return IncidentItemView(requestData: request); + case 17: + return OneTimeTaskItemView(requestData: request); default: return Container( height: 100, diff --git a/lib/new_views/pages/land_page/widgets/request_item_view_list.dart b/lib/new_views/pages/land_page/widgets/request_item_view_list.dart index e0005278..158d822c 100644 --- a/lib/new_views/pages/land_page/widgets/request_item_view_list.dart +++ b/lib/new_views/pages/land_page/widgets/request_item_view_list.dart @@ -7,6 +7,7 @@ import 'package:test_sa/extensions/int_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/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/incident_module/incident_item_view.dart'; import 'package:test_sa/modules/internal_audit_module/pages/equipment_internal_audit/equipment_internal_audit_item_view.dart'; @@ -76,6 +77,8 @@ class RequestItemViewList extends StatelessWidget { return DemoRequestItemView(requestDetails: list[index]); case 16: return IncidentItemView(requestDetails: list[index]); + case 17: + return OneTimeTaskItemView(requestDetails: list[index]); default: Container( height: 100, From 7327cda1bd6570c6c13aa7732aece90e9ff1ce46 Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:14:25 +0300 Subject: [PATCH 11/17] add duration for task when completed --- .../pages/one_time_task_view.dart | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/modules/cleaner_module/pages/one_time_task_view.dart b/lib/modules/cleaner_module/pages/one_time_task_view.dart index e8e1ce28..bfc8e6fe 100644 --- a/lib/modules/cleaner_module/pages/one_time_task_view.dart +++ b/lib/modules/cleaner_module/pages/one_time_task_view.dart @@ -112,6 +112,23 @@ class _OneTimeTaskViewState extends State { 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( @@ -252,6 +269,16 @@ class _OneTimeTaskViewState extends State { showTimer: true, ), ], + // 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), ], From 154982d9b0f5e88c7eabe2d7f3e3442e1b9065a0 Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Sun, 7 Jun 2026 09:37:19 +0300 Subject: [PATCH 12/17] improvements --- .../pages/one_time_task_preview_screen.dart | 71 ++++++++++++++----- .../pages/one_time_task_view.dart | 4 ++ 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/lib/modules/cleaner_module/pages/one_time_task_preview_screen.dart b/lib/modules/cleaner_module/pages/one_time_task_preview_screen.dart index 18e4d0ad..0a6eeaf6 100644 --- a/lib/modules/cleaner_module/pages/one_time_task_preview_screen.dart +++ b/lib/modules/cleaner_module/pages/one_time_task_preview_screen.dart @@ -10,6 +10,9 @@ 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 { @@ -124,6 +127,16 @@ class OneTimeTaskPreviewScreen extends StatelessWidget { } 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: [ @@ -134,26 +147,50 @@ class OneTimeTaskPreviewScreen extends StatelessWidget { fontWeight: FontWeight.w600, ), ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Total Working Hours', - style: AppTextStyles.bodyText.copyWith( - color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, - ), + 12.height, + if (timerStartTime != null) + AppTimer( + label: 'Working Time', + timer: TimerModel( + startAt: timerStartTime, + endAt: null, + durationInSecond: 0, ), - Text( - '${totalWorkingHours.toStringAsFixed(2)} hrs', - style: AppTextStyles.heading6.copyWith( - color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, - fontWeight: FontWeight.w600, - ), + 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), ), - ], - ).paddingAll(12), + ) + 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: 12); + ).toShadowContainer(context, padding: 16); } Widget _buildChecklistCard(BuildContext context) { diff --git a/lib/modules/cleaner_module/pages/one_time_task_view.dart b/lib/modules/cleaner_module/pages/one_time_task_view.dart index bfc8e6fe..d40921cb 100644 --- a/lib/modules/cleaner_module/pages/one_time_task_view.dart +++ b/lib/modules/cleaner_module/pages/one_time_task_view.dart @@ -267,6 +267,10 @@ class _OneTimeTaskViewState extends State { ), 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 From f2b472f4df70ad7103da7f617196d2e2b12c2ca4 Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:41:39 +0300 Subject: [PATCH 13/17] added identifier for loan request department model --- lib/models/new_models/traf_department.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/models/new_models/traf_department.dart b/lib/models/new_models/traf_department.dart index 5a2e256d..3ac9841c 100644 --- a/lib/models/new_models/traf_department.dart +++ b/lib/models/new_models/traf_department.dart @@ -3,9 +3,9 @@ import 'package:test_sa/models/new_models/room_model.dart'; import '../base.dart'; class TrafDepartment extends Base { - TrafDepartment({ - 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 + TrafDepartment( + {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 TrafDepartment.fromJson(Map json) { departmentName = json['departmentName'] ?? json['name']; @@ -15,6 +15,7 @@ class TrafDepartment extends Base { costCenterName = json['costCenterName']; name = json['name']; id = json['id']; + identifier = id?.toString() ?? ''; createdBy = json['createdBy']; createdDate = json['createdDate']; modifiedBy = json['modifiedBy']; From df8948ea496051f814e88c69d4046f2e5c50c36b Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Sun, 14 Jun 2026 11:37:41 +0300 Subject: [PATCH 14/17] null check added for priority value for CM --- .../views/components/service_request_detail_view.dart | 4 ++-- .../pages/land_page/requests/service_request_item_view.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/modules/cm_module/views/components/service_request_detail_view.dart b/lib/modules/cm_module/views/components/service_request_detail_view.dart index 1310fb79..a6d4d880 100644 --- a/lib/modules/cm_module/views/components/service_request_detail_view.dart +++ b/lib/modules/cm_module/views/components/service_request_detail_view.dart @@ -135,10 +135,10 @@ class _ServiceRequestDetailViewState extends State { // spacing: 8, runSpacing: 8, children: [ - if (Provider.of(context, listen: false).showPriority) ...[ + if (Provider.of(context, listen: false).showPriority&&workOrder.priority!=null) ...[ StatusLabel( label: workOrder.priority?.name, - id: workOrder.priority!.value!, + id: workOrder.priority?.value, radius: 4, textColor: AppColor.getPriorityStatusTextColor(context, workOrder.priority!.value!), backgroundColor: AppColor.getPriorityStatusColor(context, workOrder.priority!.value!), diff --git a/lib/new_views/pages/land_page/requests/service_request_item_view.dart b/lib/new_views/pages/land_page/requests/service_request_item_view.dart index b24a01cc..f7128ad6 100644 --- a/lib/new_views/pages/land_page/requests/service_request_item_view.dart +++ b/lib/new_views/pages/land_page/requests/service_request_item_view.dart @@ -32,7 +32,7 @@ class ServiceRequestItemView extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (Provider.of(context, listen: false).showPriority) ...[ + if (Provider.of(context, listen: false).showPriority && requestData?.priorityName != null && requestData!.priorityName!.isNotEmpty) ...[ StatusLabel( label: requestData!.priorityName!, textColor: AppColor.getRequestStatusTextColorByName(context, requestData!.priorityName!), @@ -95,7 +95,7 @@ class ServiceRequestItemView extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (Provider.of(context, listen: false).showPriority) ...[ + if (Provider.of(context, listen: false).showPriority && requestDetails!.priority != null && requestDetails!.priority!.isNotEmpty) ...[ StatusLabel( label: requestDetails!.priority, textColor: AppColor.getRequestStatusTextColorByName(context, requestDetails!.priority), From d6f2d78351bb3b75d1f7ac3eb3378762f69df215 Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Sun, 14 Jun 2026 11:38:35 +0300 Subject: [PATCH 15/17] Revert "added identifier for loan request department model" This reverts commit f2b472f4df70ad7103da7f617196d2e2b12c2ca4. --- lib/models/new_models/traf_department.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/models/new_models/traf_department.dart b/lib/models/new_models/traf_department.dart index 3ac9841c..5a2e256d 100644 --- a/lib/models/new_models/traf_department.dart +++ b/lib/models/new_models/traf_department.dart @@ -3,9 +3,9 @@ import 'package:test_sa/models/new_models/room_model.dart'; import '../base.dart'; class TrafDepartment extends Base { - TrafDepartment( - {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 + TrafDepartment({ + 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 TrafDepartment.fromJson(Map json) { departmentName = json['departmentName'] ?? json['name']; @@ -15,7 +15,6 @@ class TrafDepartment extends Base { costCenterName = json['costCenterName']; name = json['name']; id = json['id']; - identifier = id?.toString() ?? ''; createdBy = json['createdBy']; createdDate = json['createdDate']; modifiedBy = json['modifiedBy']; From 6cad1866e712b52d2eedadfbc5b0d81fe8a59d19 Mon Sep 17 00:00:00 2001 From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com> Date: Sun, 14 Jun 2026 15:02:22 +0300 Subject: [PATCH 16/17] added identifier for loan creation department to match the department with cm request --- lib/models/new_models/traf_department.dart | 41 +++++++++---------- .../pages/create_loan_request_page.dart | 2 + 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/lib/models/new_models/traf_department.dart b/lib/models/new_models/traf_department.dart index 5a2e256d..232470a4 100644 --- a/lib/models/new_models/traf_department.dart +++ b/lib/models/new_models/traf_department.dart @@ -3,9 +3,9 @@ import 'package:test_sa/models/new_models/room_model.dart'; import '../base.dart'; class TrafDepartment extends Base { - TrafDepartment({ - 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 + TrafDepartment( + {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 TrafDepartment.fromJson(Map json) { departmentName = json['departmentName'] ?? json['name']; @@ -15,6 +15,7 @@ class TrafDepartment extends Base { costCenterName = json['costCenterName']; name = json['name']; id = json['id']; + identifier = id?.toString() ?? ''; createdBy = json['createdBy']; createdDate = json['createdDate']; modifiedBy = json['modifiedBy']; @@ -33,24 +34,22 @@ class TrafDepartment extends Base { String? modifiedBy; String? modifiedDate; - // TrafDepartment copyWith({ - // num? id, // Parameters are now nullable - // String? departmentName, - // String? departmentCode, - // String? departmentId, - // String? ntCode, - // List? rooms, - // }) => - // TrafDepartment( - // id: id ?? this.id, - // departmentName: departmentName ?? this.departmentName, - // departmentCode: departmentCode ?? this.departmentCode, - // departmentId: departmentId ?? this.departmentId, - // ntCode: ntCode ?? this.ntCode, - // rooms: rooms ?? this.rooms, - // ); - - +// TrafDepartment copyWith({ +// num? id, // Parameters are now nullable +// String? departmentName, +// String? departmentCode, +// String? departmentId, +// String? ntCode, +// List? rooms, +// }) => +// TrafDepartment( +// id: id ?? this.id, +// departmentName: departmentName ?? this.departmentName, +// departmentCode: departmentCode ?? this.departmentCode, +// departmentId: departmentId ?? this.departmentId, +// ntCode: ntCode ?? this.ntCode, +// rooms: rooms ?? this.rooms, +// ); } // class TrafDepartment extends Base { diff --git a/lib/modules/loan_module/pages/create_loan_request_page.dart b/lib/modules/loan_module/pages/create_loan_request_page.dart index 0b2dc53e..450ea806 100644 --- a/lib/modules/loan_module/pages/create_loan_request_page.dart +++ b/lib/modules/loan_module/pages/create_loan_request_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:test_sa/controllers/validator/validator.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/string_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; @@ -315,6 +316,7 @@ class _CreateLoanRequestPageState extends State with Tick SingleItemDropDownMenu( context: context, title: context.translation.department, + matchBy: DropDownMatchType.identifier, showShadow: false, enabled: !isBackUpRequest, validator: (value) { From 0e4714cd9aa36ec3d89e715d5f2fcc6135d0b43e Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Sun, 14 Jun 2026 18:55:29 +0300 Subject: [PATCH 17/17] 1.7.6+41 uploaded on stores. --- lib/controllers/api_routes/urls.dart | 10 +++++----- .../land_page/create_request_type_bottom_sheet.dart | 4 ++-- pubspec.yaml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart index e2b02926..06107f57 100644 --- a/lib/controllers/api_routes/urls.dart +++ b/lib/controllers/api_routes/urls.dart @@ -1,15 +1,15 @@ class URLs { URLs._(); - static const String appReleaseBuildNumber = "40"; + static const String appReleaseBuildNumber = "41"; - // 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://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/v1/mobile"; // new V2 apis - // static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM + // static final String _baseUrl = "$_host/mobile"; // new V2 apis + 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/v5/mobile"; // v5 for data segregation // static final String _baseUrl = "$_host/v6/mobile"; // for asset delivery module diff --git a/lib/new_views/pages/land_page/create_request_type_bottom_sheet.dart b/lib/new_views/pages/land_page/create_request_type_bottom_sheet.dart index 62a4e558..bb6cbfc6 100644 --- a/lib/new_views/pages/land_page/create_request_type_bottom_sheet.dart +++ b/lib/new_views/pages/land_page/create_request_type_bottom_sheet.dart @@ -118,8 +118,8 @@ class CreateRequestModel { if (context.userProvider.isQualityUser && context.settingProvider.isUserFlowMedical) { list.add(CreateRequestModel("Equipment Internal Audit Checklist".addTranslation, "add_icon", CreateEquipmentInternalAuditForm.id)); list.add(CreateRequestModel("System Internal Audit Checklist".addTranslation, "add_icon", CreateSystemInternalAuditForm.id)); - } else if (context.userProvider.isAssessor && context.settingProvider.isUserFlowMedical) { - list.add(CreateRequestModel("TRAF".addTranslation, "add_icon", CreateTRAFRequestPage.id)); + } else if (context.userProvider.isAssessor && context.settingProvider.isUserFlowMedical) { // todo: handled in permission api, so no need to check here + // list.add(CreateRequestModel("TRAF".addTranslation, "add_icon", CreateTRAFRequestPage.id)); } else if (context.userProvider.isEngineer) { if (Provider.of(context, listen: false).engineerCanCreateCM) { list.add(CreateRequestModel(context.translation.correctiveMaintenance, "add_icon", CreateCMRequest.id)); diff --git a/pubspec.yaml b/pubspec.yaml index bf8e3371..be0dccbc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.7.5+40 +version: 1.7.6+41 environment: sdk: ">=3.5.0 <4.0.0"