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"