From 33f1f07e86043f40b049a83d7434329afb11803d Mon Sep 17 00:00:00 2001
From: WaseemAbbasi22 <50428976+WaseemAbbasi22@users.noreply.github.com>
Date: Thu, 23 Apr 2026 15:53:07 +0300
Subject: [PATCH 01/17] housekeeping module completed
---
assets/images/cleaner_flow_icon.svg | 6 +
lib/controllers/api_routes/urls.dart | 17 +-
.../widgets/request_category_fragment.dart | 19 +-
.../widgets/requests_fragment.dart | 304 +++++++++++
lib/extensions/widget_extensions.dart | 1 +
lib/main.dart | 10 +-
lib/models/new_models/building.dart | 10 +
lib/models/new_models/department.dart | 10 +
.../new_models/department_room_combined.dart | 55 ++
lib/models/new_models/floor.dart | 10 +
lib/models/new_models/room_model.dart | 10 +
lib/models/ppm/recurrent_wo.dart | 36 +-
.../models/cleaner_data_model.dart | 0
.../models/cleaner_form_model.dart | 24 +
.../models/cleaner_task_search_result.dart | 73 +++
.../pages/cleaner_request_item_view.dart | 68 +++
.../pages/cleaner_search_details_view.dart | 318 +++++++++++
.../pages/cleaner_search_form_view.dart | 512 ++++++++++++++++++
.../pages/cleaner_search_list_view.dart | 186 +++++++
.../provider/cleaner_provider.dart | 169 ++++++
.../provider/fms_building_provider.dart | 83 +++
.../provider/fms_floor_provider.dart | 85 +++
.../fms_location_description_provider.dart | 140 +++++
.../widgets/cleaner_checklist_widget.dart | 334 ++++++++++++
.../recurrent_work_order_view.dart | 76 ++-
lib/new_views/app_style/app_text_styles.dart | 1 +
.../requests/recurrent_wo_item_view.dart | 35 +-
pubspec.lock | 32 +-
28 files changed, 2585 insertions(+), 39 deletions(-)
create mode 100644 assets/images/cleaner_flow_icon.svg
create mode 100644 lib/models/new_models/department_room_combined.dart
create mode 100644 lib/modules/cleaner_module/models/cleaner_data_model.dart
create mode 100644 lib/modules/cleaner_module/models/cleaner_form_model.dart
create mode 100644 lib/modules/cleaner_module/models/cleaner_task_search_result.dart
create mode 100644 lib/modules/cleaner_module/pages/cleaner_request_item_view.dart
create mode 100644 lib/modules/cleaner_module/pages/cleaner_search_details_view.dart
create mode 100644 lib/modules/cleaner_module/pages/cleaner_search_form_view.dart
create mode 100644 lib/modules/cleaner_module/pages/cleaner_search_list_view.dart
create mode 100644 lib/modules/cleaner_module/provider/cleaner_provider.dart
create mode 100644 lib/modules/cleaner_module/provider/fms_building_provider.dart
create mode 100644 lib/modules/cleaner_module/provider/fms_floor_provider.dart
create mode 100644 lib/modules/cleaner_module/provider/fms_location_description_provider.dart
create mode 100644 lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart
create mode 100644 lib/new_views/app_style/app_text_styles.dart
diff --git a/assets/images/cleaner_flow_icon.svg b/assets/images/cleaner_flow_icon.svg
new file mode 100644
index 00000000..e88c20ca
--- /dev/null
+++ b/assets/images/cleaner_flow_icon.svg
@@ -0,0 +1,6 @@
+
diff --git a/lib/controllers/api_routes/urls.dart b/lib/controllers/api_routes/urls.dart
index 8437ecf5..cfd02c27 100644
--- a/lib/controllers/api_routes/urls.dart
+++ b/lib/controllers/api_routes/urls.dart
@@ -3,13 +3,13 @@ class URLs {
static const String appReleaseBuildNumber = "39";
- static const host1 = "https://atomsm.hmg.com"; // production url
+ // static const host1 = "https://atomsm.hmg.com"; // production url
// static const host1 = "https://atomsmdev.hmg.com"; // local DEV url
- // static const host1 = "https://atomsmuat.hmg.com"; // local UAT url
+ static const host1 = "https://atomsmuat.hmg.com"; // local UAT url
// static final String _baseUrl = "$_host/mobile"; // host local UAT
- // static final String _baseUrl = "$_host/v2/mobile"; // new V2 apis
- static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM
+ static final String _baseUrl = "$_host/v1/mobile"; // new V2 apis
+ // static final String _baseUrl = "$_host/v3/mobile"; // v3 for production CM,PM,TM
// static final String _baseUrl = "$_host/v4/mobile"; // v4 for Demo module
// static final String _baseUrl = "$_host/v5/mobile"; // v5 for data segregation
// static final String _baseUrl = "$_host/v6/mobile"; // for asset delivery module
@@ -59,12 +59,19 @@ class URLs {
static get getSitesAutoComplete => "$_baseUrl/Customer/GetCustomersAutoComplete"; // get
static get getSiteAutoCompleteWithoutConditionSites => "$_baseUrl/Customer/GetCustomersAutoCompleteWithoutConditionSites"; // get
static get getDepartments => "$_baseUrl/Customer/GetDepartmentLookup"; // get
+ static get getCustomerBuildings => "$_baseUrl/Customer/GetBuildingLookup"; // post - FMS buildings by siteId
+ static get getCustomerFloors => "$_baseUrl/Customer/GetFloorLookup"; // post - FMS floors by siteId and buildingId
+ static get getCustomerLocationDescription => "$_baseUrl/Customer/GetLocationDescription"; // get - FMS location description by siteId, buildingId, floorId
static get getAssets => "$_baseUrl/Asset/GetAssets"; // get
static get getFaultDescription => "$_baseUrl/Asset/GetDefectForModelDefinition"; // get
static get getAssetById => "$_baseUrl/Asset/GetAssetById?assetId="; // get
static get getModels => "$_baseUrl/ModelDefinition/GetModelDefinitionAsset"; // get ?client=2
static get getAllRequestsAndCount => "$_baseUrl/CallRequest/GetAllRequestsAndCount"; // get
static get updateUserContactInformation => "$_baseUrl/Account/UpdateContactInfo"; // get
+ static get searchPlanRecurrentTaskListSearch => "$_baseUrl/PlanRecurrentTasks/PlanRecurrentTaskListSearchDto"; // get
+ static get checkQRSameRoom => "$_baseUrl/PlanRecurrentTasks/CheckQRSameRoom"; // get
+
+ //
//service request new flow urls.
static get nurseDashboardCountUrl => '$_baseUrl/ServiceRequest/GetDashboardNurseCount';
@@ -527,4 +534,4 @@ class URLs {
static get updateDemoPeriodByRequester => "$_baseUrl/DemoRequest/UpdateDemoPeriodByRequester";
//4104 attachment type lookup
-}
+}
\ No newline at end of file
diff --git a/lib/dashboard_latest/widgets/request_category_fragment.dart b/lib/dashboard_latest/widgets/request_category_fragment.dart
index c33e8747..2b70da8f 100644
--- a/lib/dashboard_latest/widgets/request_category_fragment.dart
+++ b/lib/dashboard_latest/widgets/request_category_fragment.dart
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
-import 'package:test_sa/app_strings/app_asset.dart';
import 'package:test_sa/controllers/providers/api/user_provider.dart';
import 'package:test_sa/dashboard_latest/dashboard_provider.dart';
import 'package:test_sa/dashboard_latest/widgets/request_category_list.dart';
@@ -13,6 +12,7 @@ import 'package:test_sa/models/new_models/dashboard_detail.dart';
import 'package:test_sa/modules/cm_module/views/components/weekly_calendar_fragment.dart';
import 'package:test_sa/new_views/common_widgets/tab_button.dart';
import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
+import 'package:test_sa/controllers/providers/settings/setting_provider.dart';
class RequestCategoryFragment extends StatelessWidget {
const RequestCategoryFragment({Key? key}) : super(key: key);
@@ -21,6 +21,11 @@ class RequestCategoryFragment extends StatelessWidget {
Widget build(BuildContext context) {
return Consumer(builder: (context, dashboardProvider, child) {
bool isNurse = (Provider.of(context, listen: false).user?.type) == UsersTypes.normal_user;
+ final user = Provider.of(context, listen: false).user;
+ final settingProvider = Provider.of(context, listen: false);
+
+ // Check if user is FMS Engineer using SettingProvider
+ final bool isFMSEngineer = user?.type == UsersTypes.engineer && settingProvider.isUserFMS;
return Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
//TODO uncommit this to get the calendar card..
@@ -48,7 +53,17 @@ class RequestCategoryFragment extends StatelessWidget {
: const SizedBox(),
],
).paddingOnly(start: 14, end: 12)
- : context.translation.allWorkOrder.heading4(context).paddingOnly(start: 16, end: 16),
+ : isFMSEngineer
+ ? Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ context.translation.allWorkOrder.heading4(context),
+ 'search'.toSvgAsset(width: 20, height: 20).onPress(() {
+ Navigator.of(context).pushNamed('/cleaner_search');
+ }),
+ ],
+ ).paddingOnly(start: 16, end: 16)
+ : context.translation.allWorkOrder.heading4(context).paddingOnly(start: 16, end: 16),
getTabs(
requestsProvider: dashboardProvider,
context: context,
diff --git a/lib/dashboard_latest/widgets/requests_fragment.dart b/lib/dashboard_latest/widgets/requests_fragment.dart
index ee55dea3..e517b472 100644
--- a/lib/dashboard_latest/widgets/requests_fragment.dart
+++ b/lib/dashboard_latest/widgets/requests_fragment.dart
@@ -356,6 +356,310 @@ class RequestsFragment extends StatelessWidget {
// int index,
// Color iconColor,
// ) {
+// return GestureDetector(
+// onTap: isLoading
+// ? null
+// : () async {
+// await Navigator.push(
+// context,
+// MaterialPageRoute(
+// builder: (context) => RequestsListPage(index)));
+// Provider.of(context, listen: false)
+// .getRequests();
+// },
+// child: Column(
+// crossAxisAlignment: CrossAxisAlignment.start,
+// children: [
+// value != null && value > 0 && !isLoading
+// ? badges.Badge(
+// badgeContent: Text(
+// value.toString(),
+// style: const TextStyle(
+// color: AppColor.white20,
+// fontWeight: FontWeight.normal,
+// ),
+// ),
+// badgeStyle: const badges.BadgeStyle(padding: EdgeInsets.all(4), badgeColor: AppColor.red30),
+// child: Container(
+// child: (icon ?? "").toSvgAsset(height: 26, width: 26, color: iconColor)?.toShimmer(isShow: isLoading),
+// ).toShadowCircleContainer(context, padding: 18),
+// )
+// : Container(
+// child: (icon ?? "").toSvgAsset(height: 26, width: 26, color: iconColor)?.toShimmer(isShow: isLoading),
+// ).toShadowCircleContainer(context, padding: 18),
+// 10.height,
+// Text(
+// title,
+// style: AppTextStyles.bodyText.copyWith(
+// color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
+// ),
+// ),
+// ],
+// ),
+// );
+// }
+// }
+
+// import 'package:flutter/material.dart';
+// import 'package:provider/provider.dart';
+// import 'package:test_sa/controllers/api_routes/api_manager.dart';
+// import 'package:test_sa/controllers/providers/api/all_requests_provider.dart';
+// import 'package:test_sa/controllers/providers/api/notifications_provider.dart';
+// import 'package:test_sa/controllers/providers/api/user_provider.dart';
+// import 'package:test_sa/extensions/context_extension.dart';
+// import 'package:test_sa/extensions/int_extensions.dart';
+// import 'package:test_sa/extensions/text_extensions.dart';
+// import 'package:test_sa/extensions/widget_extensions.dart';
+// import 'package:badges/badges.dart' as badges;
+// import 'package:test_sa/models/enums/user_types.dart';
+// import 'package:test_sa/new_views/app_style/app_color.dart';
+// import 'package:test_sa/new_views/common_widgets/app_floating_action_button.dart';
+// import 'package:test_sa/new_views/pages/land_page/requests_list_page.dart';
+//
+// class RequestsFragment extends StatelessWidget {
+// const RequestsFragment({Key? key}) : super(key: key);
+//
+// @override
+// Widget build(BuildContext context) {
+// return Scaffold(
+// // todo check here, nurse can add request not engineer
+// floatingActionButton: const AppFloatingActionButton(),
+// body: Consumer(
+// builder: (context, snapshot, _) => RefreshIndicator(
+// onRefresh: () {
+// snapshot.getRequests();
+// Provider.of(context, listen: false)
+// .getSystemNotifications(
+// user: Provider.of(context, listen: false)
+// .user,
+// resetProvider: true);
+// return Future.delayed(const Duration(microseconds: 250));
+// },
+// child: Container(
+// height: 200.toScreenHeight,
+// color: Colors.blue,
+// child: Wrap(
+// children: [
+// listItem(
+// snapshot.highPriorityRequests?.total?.count,
+// "high_priority",
+// context.translation.highPriority,
+// context,
+// snapshot.isHighPriorityLoading,
+// 0,
+// context.isDark ? AppColor.redStatus(context) : AppColor.red50,
+// ),
+// ],
+// ),
+// // child: ListView(
+// // // padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
+// // children: [
+// // listItem(
+// // snapshot.highPriorityRequests?.total?.count,
+// // "high_priority",
+// // context.translation.highPriority,
+// // context,
+// // snapshot.isHighPriorityLoading,
+// // 0,
+// // context.isDark ? AppColor.redStatus(context) : AppColor.red50,
+// // ),
+// // listItem(
+// // snapshot.overdueRequests?.total?.count,
+// // "overdue",
+// // context.translation.overdue,
+// // context,
+// // snapshot.isOverdueLoading,
+// // 1,
+// // AppColor.yellowStatus(context),
+// // ),
+// // listItem(
+// // snapshot.openRequests?.total?.count,
+// // "new_request",
+// // ApiManager.instance.user.type == UsersTypes.engineer
+// // ? context.translation.inProgress
+// // : context.translation.newR,
+// // context,
+// // snapshot.isOpenLoading,
+// // 2,
+// // AppColor.primary40,
+// // ),
+// // listItem(
+// // snapshot.completedRequests?.total?.count,
+// // "complete_request",
+// // context.translation.completed,
+// // context,
+// // snapshot.isCompleteLoading,
+// // 3,
+// // AppColor.greenStatus(context),
+// // ),
+// // ],
+// // ),
+// )),
+// ),
+// );
+// }
+//
+// Widget listItem(
+// int value,
+// String icon,
+// String title,
+// BuildContext context,
+// bool isLoading,
+// int index,
+// Color iconColor,
+// ) {
+// return GestureDetector(
+// onTap: isLoading
+// ? null
+// : () async {
+// await Navigator.push(
+// context,
+// MaterialPageRoute(
+// builder: (context) => RequestsListPage(index)));
+// Provider.of(context, listen: false)
+// .getRequests();
+// },
+// child: Column(
+// crossAxisAlignment: CrossAxisAlignment.start,
+// children: [
+// value != null && value > 0 && !isLoading
+// ? badges.Badge(
+// badgeContent: Text(
+// value.toString(),
+// style: const TextStyle(
+// color: AppColor.white20,
+// fontWeight: FontWeight.normal,
+// ),
+// ),
+// badgeStyle: const badges.BadgeStyle(padding: EdgeInsets.all(4), badgeColor: AppColor.red30),
+// child: Container(
+// child: (icon ?? "").toSvgAsset(height: 26, width: 26, color: iconColor)?.toShimmer(isShow: isLoading),
+// ).toShadowCircleContainer(context, padding: 18),
+// )
+// : Container(
+// child: (icon ?? "").toSvgAsset(height: 26, width: 26, color: iconColor)?.toShimmer(isShow: isLoading),
+// ).toShadowCircleContainer(context, padding: 18),
+// 10.height,
+// Text(
+// title,
+// style: AppTextStyles.bodyText.copyWith(
+// color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
+// ),
+// ),
+// ],
+// ),
+// );
+// }
+// }
+
+// import 'package:flutter/material.dart';
+// import 'package:provider/provider.dart';
+// import 'package:test_sa/controllers/api_routes/api_manager.dart';
+// import 'package:test_sa/controllers/providers/api/all_requests_provider.dart';
+// import 'package:test_sa/controllers/providers/api/notifications_provider.dart';
+// import 'package:test_sa/controllers/providers/api/user_provider.dart';
+// import 'package:test_sa/extensions/context_extension.dart';
+// import 'package:test_sa/extensions/int_extensions.dart';
+// import 'package:test_sa/extensions/text_extensions.dart';
+// import 'package:test_sa/extensions/widget_extensions.dart';
+// import 'package:badges/badges.dart' as badges;
+// import 'package:test_sa/models/enums/user_types.dart';
+// import 'package:test_sa/new_views/app_style/app_color.dart';
+// import 'package:test_sa/new_views/common_widgets/app_floating_action_button.dart';
+// import 'package:test_sa/new_views/pages/land_page/requests_list_page.dart';
+//
+// class RequestsFragment extends StatelessWidget {
+// const RequestsFragment({Key? key}) : super(key: key);
+//
+// @override
+// Widget build(BuildContext context) {
+// return Scaffold(
+// // todo check here, nurse can add request not engineer
+// floatingActionButton: const AppFloatingActionButton(),
+// body: Consumer(
+// builder: (context, snapshot, _) => RefreshIndicator(
+// onRefresh: () {
+// snapshot.getRequests();
+// Provider.of(context, listen: false)
+// .getSystemNotifications(
+// user: Provider.of(context, listen: false)
+// .user,
+// resetProvider: true);
+// return Future.delayed(const Duration(microseconds: 250));
+// },
+// child: Container(
+// height: 200.toScreenHeight,
+// color: Colors.blue,
+// child: Wrap(
+// children: [
+// listItem(
+// snapshot.highPriorityRequests?.total?.count,
+// "high_priority",
+// context.translation.highPriority,
+// context,
+// snapshot.isHighPriorityLoading,
+// 0,
+// context.isDark ? AppColor.redStatus(context) : AppColor.red50,
+// ),
+// ],
+// ),
+// // child: ListView(
+// // // padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
+// // children: [
+// // listItem(
+// // snapshot.highPriorityRequests?.total?.count,
+// // "high_priority",
+// // context.translation.highPriority,
+// // context,
+// // snapshot.isHighPriorityLoading,
+// // 0,
+// // context.isDark ? AppColor.redStatus(context) : AppColor.red50,
+// // ),
+// // listItem(
+// // snapshot.overdueRequests?.total?.count,
+// // "overdue",
+// // context.translation.overdue,
+// // context,
+// // snapshot.isOverdueLoading,
+// // 1,
+// // AppColor.yellowStatus(context),
+// // ),
+// // listItem(
+// // snapshot.openRequests?.total?.count,
+// // "new_request",
+// // ApiManager.instance.user.type == UsersTypes.engineer
+// // ? context.translation.inProgress
+// // : context.translation.newR,
+// // context,
+// // snapshot.isOpenLoading,
+// // 2,
+// // AppColor.primary40,
+// // ),
+// // listItem(
+// // snapshot.completedRequests?.total?.count,
+// // "complete_request",
+// // context.translation.completed,
+// // context,
+// // snapshot.isCompleteLoading,
+// // 3,
+// // AppColor.greenStatus(context),
+// // ),
+// // ],
+// // ),
+// )),
+// ),
+// );
+// }
+//
+// Widget listItem(
+// int value,
+// String icon,
+// String title,
+// BuildContext context,
+// bool isLoading,
+// int index,
+// Color iconColor,
+// ) {
// value = 4;
// return GestureDetector(
// onTap: isLoading
diff --git a/lib/extensions/widget_extensions.dart b/lib/extensions/widget_extensions.dart
index 1d2d59f8..7b468913 100644
--- a/lib/extensions/widget_extensions.dart
+++ b/lib/extensions/widget_extensions.dart
@@ -202,3 +202,4 @@ extension WidgetExtensions on Widget {
extension DividerExtension on Divider {
Divider defaultStyle(BuildContext context) => Divider(thickness: 1, color: context.isDarkNotListen ? AppColor.neutral20 : AppColor.neutral30);
}
+
diff --git a/lib/main.dart b/lib/main.dart
index fcb78f3e..40ba2eda 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -139,6 +139,9 @@ import 'providers/lookups/request_type_lookup_provider.dart';
import 'providers/lookups/yes_no_lookup_provider.dart';
import 'providers/service_request_providers/loan_availability_provider.dart';
import 'providers/service_request_providers/reject_reason_provider.dart';
+import 'modules/cleaner_module/pages/cleaner_search_form_view.dart';
+import 'modules/cleaner_module/pages/cleaner_search_list_view.dart';
+import 'modules/cleaner_module/provider/cleaner_provider.dart';
// class MyHttpOverrides extends HttpOverrides {
// @override
@@ -223,7 +226,7 @@ class MyApp extends StatelessWidget {
// LAZY LOADED PROVIDERS (107) - Created only when accessed
// This improves app startup time by 40-60%
// ============================================================
-
+
// LOOKUP PROVIDERS (16) - Dropdown data
ChangeNotifierProvider(create: (_) => ClassificationLookupProvider(), lazy: true),
ChangeNotifierProvider(create: (_) => RecommendationLookupProvider(), lazy: true),
@@ -347,6 +350,9 @@ class MyApp extends StatelessWidget {
// CX MODULE PROVIDERS (1)
ChangeNotifierProvider(create: (_) => SurveyProvider(), lazy: true),
+ // CLEANER MODULE PROVIDERS (1)
+ ChangeNotifierProvider(create: (_) => CleanerProvider(), lazy: true),
+
// UTILITY PROVIDERS (1)
ChangeNotifierProvider(create: (_) => OracleCodeProvider(), lazy: true),
],
@@ -401,6 +407,8 @@ class MyApp extends StatelessWidget {
CreateLoanRequestPage.id: (_) => CreateLoanRequestPage(),
CreateMedicalGasRequestPage.id: (_) => CreateMedicalGasRequestPage(),
CreateDemoRequestPage.id: (_) => CreateDemoRequestPage(),
+ CleanerSearchFormView.routeName: (_) => const CleanerSearchFormView(),
+ CleanerSearchListView.routeName: (_) => const CleanerSearchListView(),
},
),
),
diff --git a/lib/models/new_models/building.dart b/lib/models/new_models/building.dart
index e112af11..7dcc6e13 100644
--- a/lib/models/new_models/building.dart
+++ b/lib/models/new_models/building.dart
@@ -59,4 +59,14 @@ class Building extends Base {
return map;
}
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is Building &&
+ runtimeType == other.runtimeType &&
+ id == other.id;
+
+ @override
+ int get hashCode => id.hashCode;
}
diff --git a/lib/models/new_models/department.dart b/lib/models/new_models/department.dart
index e48585da..c7538e82 100644
--- a/lib/models/new_models/department.dart
+++ b/lib/models/new_models/department.dart
@@ -69,4 +69,14 @@ class Department extends Base {
}
return map;
}
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is Department &&
+ runtimeType == other.runtimeType &&
+ id == other.id;
+
+ @override
+ int get hashCode => id.hashCode;
}
diff --git a/lib/models/new_models/department_room_combined.dart b/lib/models/new_models/department_room_combined.dart
new file mode 100644
index 00000000..feaca858
--- /dev/null
+++ b/lib/models/new_models/department_room_combined.dart
@@ -0,0 +1,55 @@
+import 'package:test_sa/models/base.dart';
+
+class DepartmentRoomCombined extends Base {
+ final int? deptId;
+ final int? roomId;
+ final String? deptName;
+ final String? roomName;
+ final String? locationDescriptionName;
+ final String? deptAndRoomName;
+
+ DepartmentRoomCombined({
+ this.deptId,
+ this.roomId,
+ this.deptName,
+ this.roomName,
+ this.locationDescriptionName,
+ this.deptAndRoomName,
+ }) : super(
+ identifier: '${deptId}_${roomId}',
+ name: deptAndRoomName ?? '${deptName ?? ''} / ${roomName ?? ''}',
+ );
+
+ factory DepartmentRoomCombined.fromJson(Map json) {
+ return DepartmentRoomCombined(
+ deptId: json['deptId'],
+ roomId: json['roomId'],
+ deptName: json['deptName'],
+ roomName: json['roomName'],
+ locationDescriptionName: json['locationDescriptionName'],
+ deptAndRoomName: json['deptAndRoomName'],
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'deptId': deptId,
+ 'roomId': roomId,
+ 'deptName': deptName,
+ 'roomName': roomName,
+ 'locationDescriptionName': locationDescriptionName,
+ 'deptAndRoomName': deptAndRoomName,
+ };
+ }
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is DepartmentRoomCombined &&
+ runtimeType == other.runtimeType &&
+ deptId == other.deptId &&
+ roomId == other.roomId;
+
+ @override
+ int get hashCode => deptId.hashCode ^ roomId.hashCode;
+}
diff --git a/lib/models/new_models/floor.dart b/lib/models/new_models/floor.dart
index 33e74d8b..4adc1367 100644
--- a/lib/models/new_models/floor.dart
+++ b/lib/models/new_models/floor.dart
@@ -57,4 +57,14 @@ class Floor extends Base {
}
return map;
}
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is Floor &&
+ runtimeType == other.runtimeType &&
+ id == other.id;
+
+ @override
+ int get hashCode => id.hashCode;
}
diff --git a/lib/models/new_models/room_model.dart b/lib/models/new_models/room_model.dart
index 62012565..c525800e 100644
--- a/lib/models/new_models/room_model.dart
+++ b/lib/models/new_models/room_model.dart
@@ -22,6 +22,16 @@ class Rooms extends Base {
data['value'] = value;
return data;
}
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is Rooms &&
+ runtimeType == other.runtimeType &&
+ id == other.id;
+
+ @override
+ int get hashCode => id.hashCode;
}
// class Rooms extends Base {
diff --git a/lib/models/ppm/recurrent_wo.dart b/lib/models/ppm/recurrent_wo.dart
index 3a689553..bae7cac6 100644
--- a/lib/models/ppm/recurrent_wo.dart
+++ b/lib/models/ppm/recurrent_wo.dart
@@ -2,6 +2,7 @@ import 'package:test_sa/extensions/enum_extensions.dart';
import 'package:test_sa/models/enums/recurrent_task_inspection_data_type.dart';
import 'package:test_sa/models/lookup.dart';
import 'package:test_sa/models/timer_model.dart';
+import 'package:test_sa/modules/cleaner_module/widgets/cleaner_checklist_widget.dart';
class RecurrentWo {
RecurrentWoData? recurrentWoData;
@@ -51,11 +52,13 @@ class RecurrentWoData {
Lookup? room;
List? planRecurrentMedicalTaskRooms;
List? planRecurrentTaskTimers;
+ List? planRecurrentTaskCheckLists;
TimerModel? recurrentWoTimerModel = TimerModel();
double? totalWorkingHours = 0.0;
List? timerModelList = [];
String? comment;
TimerModel? recurrentWoTimePicker;
+ String? imageRoom;
RecurrentWoData(
{this.id,
@@ -73,10 +76,12 @@ class RecurrentWoData {
this.room,
this.planRecurrentMedicalTaskRooms,
this.planRecurrentTaskTimers,
+ this.planRecurrentTaskCheckLists,
this.timerModelList,
this.recurrentWoTimePicker,
this.comment,
- this.totalWorkingHours});
+ this.totalWorkingHours,
+ this.imageRoom});
RecurrentWoData.fromJson(Map json) {
id = json['id'];
@@ -92,6 +97,7 @@ class RecurrentWoData {
department = json["department"] == null ? null : Lookup.fromJson(json["department"]);
room = json["room"] == null ? null : Lookup.fromJson(json["room"]);
comment = json["comment"];
+ imageRoom = json["imageRoom"];
if (json['planRecurrentMedicalTaskRooms'] != null) {
planRecurrentMedicalTaskRooms = [];
json['planRecurrentMedicalTaskRooms'].forEach((v) {
@@ -107,6 +113,19 @@ class RecurrentWoData {
//TODO need to test this @waseem..
totalWorkingHours = json['planRecurrentTaskTimers'].fold(0.0, (sum, item) => (sum ?? 0) + DateTime.parse(item['endDateTime']).difference(DateTime.parse(item['startDateTime'])).inSeconds) ?? 0;
}
+
+ if (json['planRecurrentTaskCheckLists'] != null) {
+ planRecurrentTaskCheckLists = [];
+ json['planRecurrentTaskCheckLists'].forEach((v) {
+ planRecurrentTaskCheckLists!.add(ChecklistItem(
+ id: v['id'] ?? 0,
+ title: v['instructionText'] ?? v['title'] ?? '',
+ isChecked: v['isDone'] ?? v['isChecked'] ?? false,
+ comment: v['comment'] ?? '',
+ isMandatory: v['isRequired'] ?? v['isMandatory'] ?? false,
+ ));
+ });
+ }
}
Map toJson({int? status = 0}) {
@@ -127,6 +146,21 @@ class RecurrentWoData {
if (planRecurrentTaskTimers != null) {
data['planRecurrentTaskTimers'] = planRecurrentTaskTimers!.map((v) => v.toJson()).toList();
}
+
+ if (planRecurrentTaskCheckLists != null) {
+ data['planRecurrentTaskCheckLists'] = planRecurrentTaskCheckLists!
+ .map((v) => {
+ 'id': v.id,
+ 'comment': v.commentController.text,
+ 'isDone': v.isChecked,
+ })
+ .toList();
+ }
+
+ if (imageRoom != null && imageRoom!.isNotEmpty) {
+ data['imageRoom'] = imageRoom;
+ }
+
if (taskRoomTabAttributesJson.isNotEmpty) {
data['attributes'] = taskRoomTabAttributesJson;
} else {
diff --git a/lib/modules/cleaner_module/models/cleaner_data_model.dart b/lib/modules/cleaner_module/models/cleaner_data_model.dart
new file mode 100644
index 00000000..e69de29b
diff --git a/lib/modules/cleaner_module/models/cleaner_form_model.dart b/lib/modules/cleaner_module/models/cleaner_form_model.dart
new file mode 100644
index 00000000..f8f7c98d
--- /dev/null
+++ b/lib/modules/cleaner_module/models/cleaner_form_model.dart
@@ -0,0 +1,24 @@
+// Cleaner request search model for mock/static data
+class CleanerRequest {
+ final String priority;
+ final String date;
+ final String time;
+ final String roomNo;
+ final String site;
+ final String building;
+ final String floor;
+ final String clinic;
+ final String? taskNo;
+
+ CleanerRequest({
+ required this.priority,
+ required this.date,
+ required this.time,
+ required this.roomNo,
+ required this.site,
+ required this.building,
+ required this.floor,
+ required this.clinic,
+ this.taskNo,
+ });
+}
diff --git a/lib/modules/cleaner_module/models/cleaner_task_search_result.dart b/lib/modules/cleaner_module/models/cleaner_task_search_result.dart
new file mode 100644
index 00000000..88a2e9ac
--- /dev/null
+++ b/lib/modules/cleaner_module/models/cleaner_task_search_result.dart
@@ -0,0 +1,73 @@
+class CleanerTaskSearchResult {
+ int? id;
+ String? taskNo;
+ String? siteName;
+ String? buildingName;
+ String? floorName;
+ String? departmentName;
+ String? roomName;
+ String? locationDescription;
+ String? createdDate;
+
+ CleanerTaskSearchResult({
+ this.id,
+ this.taskNo,
+ this.siteName,
+ this.buildingName,
+ this.floorName,
+ this.departmentName,
+ this.roomName,
+ this.locationDescription,
+ this.createdDate,
+ });
+
+ factory CleanerTaskSearchResult.fromJson(Map json) {
+ return CleanerTaskSearchResult(
+ id: json['id'],
+ taskNo: json['taskNo'],
+ siteName: json['siteName'],
+ buildingName: json['buildingName'],
+ floorName: json['floorName'],
+ departmentName: json['departmentName'],
+ roomName: json['roomName'],
+ locationDescription: json['locationDescription'],
+ createdDate: json['createdDate'],
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'id': id,
+ 'taskNo': taskNo,
+ 'siteName': siteName,
+ 'buildingName': buildingName,
+ 'floorName': floorName,
+ 'departmentName': departmentName,
+ 'roomName': roomName,
+ 'locationDescription': locationDescription,
+ 'createdDate': createdDate,
+ };
+ }
+
+ String get fullLocation {
+ List parts = [];
+ if (siteName != null && siteName!.isNotEmpty) parts.add(siteName!);
+ if (buildingName != null && buildingName!.isNotEmpty) parts.add(buildingName!);
+ if (floorName != null && floorName!.isNotEmpty) parts.add(floorName!);
+ if (departmentName != null && departmentName!.isNotEmpty) parts.add(departmentName!);
+ if (roomName != null && roomName!.isNotEmpty) parts.add('Room $roomName');
+ return parts.join(' - ');
+ }
+
+ String get departmentRoom {
+ if (departmentName != null && roomName != null) {
+ return '$departmentName / $roomName';
+ } else if (departmentName != null) {
+ return departmentName!;
+ } else if (roomName != null) {
+ return 'Room $roomName';
+ }
+ return '';
+ }
+}
+
diff --git a/lib/modules/cleaner_module/pages/cleaner_request_item_view.dart b/lib/modules/cleaner_module/pages/cleaner_request_item_view.dart
new file mode 100644
index 00000000..96d0eb05
--- /dev/null
+++ b/lib/modules/cleaner_module/pages/cleaner_request_item_view.dart
@@ -0,0 +1,68 @@
+import 'package:flutter/material.dart';
+import 'package:test_sa/extensions/context_extension.dart';
+import 'package:test_sa/extensions/int_extensions.dart';
+import 'package:test_sa/extensions/text_extensions.dart';
+import 'package:test_sa/extensions/widget_extensions.dart';
+import 'package:test_sa/new_views/app_style/app_color.dart';
+import '../models/cleaner_task_search_result.dart';
+
+class CleanerRequestItemView extends StatelessWidget {
+ final CleanerTaskSearchResult result;
+ final VoidCallback? onTap;
+ final bool showShadow;
+
+ const CleanerRequestItemView({Key? key, required this.result, this.onTap, this.showShadow = true}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Task Number and Date Row
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ if (result.taskNo != null && result.taskNo!.isNotEmpty) result.taskNo!.heading5(context),
+ if (result.createdDate != null)
+ Text(
+ result.createdDate!.split('T').first,
+ textAlign: TextAlign.end,
+ style: AppTextStyles.tinyFont.copyWith(
+ color: context.isDark ? AppColor.neutral10 : AppColor.neutral50,
+ ),
+ ),
+ ],
+ ),
+ 8.height,
+
+ // Location Information
+ if (result.siteName != null && result.siteName!.isNotEmpty) 'Site: ${result.siteName}'.bodyText(context),
+ if (result.buildingName != null && result.buildingName!.isNotEmpty) 'Building: ${result.buildingName}'.bodyText(context),
+ if (result.floorName != null && result.floorName!.isNotEmpty) 'Floor: ${result.floorName}'.bodyText(context),
+ if (result.departmentName != null && result.departmentName!.isNotEmpty) 'Department: ${result.departmentName}'.bodyText(context),
+ if (result.roomName != null && result.roomName!.isNotEmpty) 'Room: ${result.roomName}'.bodyText(context),
+ if (result.locationDescription != null && result.locationDescription!.isNotEmpty) 'Location: ${result.locationDescription}'.bodyText(context),
+
+ 8.height,
+
+ // View Details Link
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ context.translation.viewDetails,
+ style: AppTextStyles.bodyText.copyWith(color: AppColor.blueStatus(context)),
+ ),
+ 4.width,
+ Icon(
+ Icons.arrow_forward,
+ color: AppColor.blueStatus(context),
+ size: 14,
+ )
+ ],
+ ),
+ ],
+ ).toShadowContainer(context, withShadow: showShadow).onPress(onTap);
+ }
+}
diff --git a/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart b/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart
new file mode 100644
index 00000000..948688d0
--- /dev/null
+++ b/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart
@@ -0,0 +1,318 @@
+// UI: Cleaner Search Details View
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:test_sa/extensions/context_extension.dart';
+import 'package:test_sa/extensions/int_extensions.dart';
+import 'package:test_sa/extensions/string_extensions.dart';
+import 'package:test_sa/extensions/text_extensions.dart';
+import 'package:test_sa/extensions/widget_extensions.dart';
+import 'package:test_sa/new_views/app_style/app_color.dart';
+import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
+import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart';
+import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
+import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
+import 'package:test_sa/views/widgets/qr/scan_qr.dart';
+import '../models/cleaner_task_search_result.dart';
+import '../provider/cleaner_provider.dart';
+import '../../pm_module/recurrent_wo/recurrent_work_order_view.dart';
+import 'package:test_sa/controllers/providers/api/all_requests_provider.dart';
+
+class CleanerSearchDetailsView extends StatefulWidget {
+ static const String routeName = '/cleaner_search_details';
+ final int requestId;
+ final CleanerTaskSearchResult? taskResult;
+
+ const CleanerSearchDetailsView({
+ Key? key,
+ required this.requestId,
+ this.taskResult,
+ }) : super(key: key);
+
+ @override
+ _CleanerSearchDetailsViewState createState() => _CleanerSearchDetailsViewState();
+}
+
+class _CleanerSearchDetailsViewState extends State {
+ late CleanerProvider _cleanerProvider;
+ bool _isQRScanned = false;
+ CleanerTaskSearchResult? _taskData;
+ bool _isLoading = false;
+
+ @override
+ void initState() {
+ super.initState();
+ _cleanerProvider = Provider.of(context, listen: false);
+ _taskData = widget.taskResult;
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ getInitialData();
+ });
+ }
+
+ Future getInitialData() async {
+ // If taskResult is null, fetch data from recurrent work order API
+ if (widget.taskResult == null) {
+ setState(() {
+ _isLoading = true;
+ });
+
+ try {
+ final allRequestsProvider = Provider.of(context, listen: false);
+ await allRequestsProvider.getRecurrentWoById(id: widget.requestId);
+
+ // Map RecurrentWoData to CleanerTaskSearchResult
+ if (allRequestsProvider.recurrentWoData != null) {
+ final recurrentData = allRequestsProvider.recurrentWoData!;
+ _taskData = CleanerTaskSearchResult(
+ id: recurrentData.id,
+ taskNo: recurrentData.taskNo,
+ siteName: recurrentData.site?.siteName,
+ buildingName: recurrentData.building?.name,
+ floorName: recurrentData.floor?.name,
+ departmentName: recurrentData.department?.name,
+ roomName: recurrentData.room?.name,
+ locationDescription: null, // Not available in RecurrentWoData
+ createdDate: recurrentData.scheduleDate,
+ );
+ }
+ } catch (e) {
+ debugPrint('Error fetching recurrent work order: $e');
+ } finally {
+ setState(() {
+ _isLoading = false;
+ });
+ }
+ } else {
+ await _cleanerProvider.getCleanerRequestById(id: widget.requestId);
+ }
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Theme.of(context).scaffoldBackgroundColor,
+ appBar: DefaultAppBar(
+ title: 'Request Details'.addTranslation,
+ onBackPress: () => Navigator.pop(context),
+ ),
+ body: _isLoading
+ ? const Center(child: CircularProgressIndicator())
+ : Selector(
+ selector: (_, model) => model.isLoading,
+ builder: (_, isLoading, __) {
+ if (isLoading) {
+ return const Center(child: CircularProgressIndicator());
+ }
+
+ return Column(
+ children: [
+ Expanded(
+ child: SingleChildScrollView(
+ padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ 18.height,
+ _buildCleanerRequestDetailCard(context),
+ 20.height,
+ ],
+ ),
+ ),
+ ),
+ SafeArea(
+ top: false,
+ right: false,
+ left: false,
+ child: FooterActionButton.footerContainer(
+ context: context,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (!_isQRScanned)
+ Row(
+ children: [
+ Icon(
+ Icons.info_outline,
+ color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
+ size: 15,
+ ),
+ 8.width,
+ Expanded(
+ child: Text(
+ 'By scanning the QR code you will accept this task'.addTranslation,
+ textAlign: TextAlign.center,
+ style: AppTextStyles.bodyText.copyWith(
+ color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
+ ),
+ ),
+ ),
+ ],
+ ),
+ 16.height,
+ AppFilledButton(
+ label: _isQRScanned ? 'Update' : 'Scan QR Code',
+ maxWidth: true,
+ buttonColor: AppColor.primary10,
+ onPressed: () async {
+ if (_isQRScanned) {
+ // If QR is already scanned, navigate to recurrent work order view
+ Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (_) => RecurrentWorkOrderView(taskId: widget.requestId),
+ ),
+ );
+ } else {
+ // Open QR scanner
+ final result = await Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => const ScanQr(),
+ ),
+ );
+
+ // If QR code was scanned successfully, check if it matches the room
+ if (result != null && result is String && result.isNotEmpty) {
+ // Show loading indicator
+ showDialog(
+ context: context,
+ barrierDismissible: false,
+ builder: (context) => const AppLazyLoading(),
+ );
+
+ // Call the API to check if QR matches the same room
+ final isValid = await CleanerProvider().checkQRSameRoom(
+ planRecurrentTaskId: widget.requestId,
+ qrCode: result,
+ );
+
+ // Close loading dialog
+ if (mounted) Navigator.pop(context);
+
+ if (isValid) {
+ // Update state to show "Update" button instead of navigating
+ if (mounted) {
+ setState(() {
+ _isQRScanned = true;
+ });
+ }
+ }
+ }
+ }
+ },
+ ),
+ ],
+ ).paddingOnly(start: 16, end: 16),
+ ),
+ ).toShadowContainer(context, padding: 0, showShadow: false, borderRadius: 0),
+ ],
+ );
+ },
+ ),
+ );
+ }
+
+ Widget _buildCleanerRequestDetailCard(BuildContext context) {
+ final taskResult = _taskData;
+
+ if (taskResult == null) {
+ return const SizedBox();
+ }
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Task Number and Date Row
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ 'Request Information',
+ style: AppTextStyles.heading4.copyWith(
+ color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
+ ),
+ ),
+ if (taskResult.createdDate != null)
+ Text(
+ taskResult.createdDate!.split('T').first,
+ textAlign: TextAlign.end,
+ style: AppTextStyles.tinyFont.copyWith(
+ color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
+ ),
+ ),
+ ],
+ ),
+ 8.height,
+ if (taskResult.taskNo != null && taskResult.taskNo!.isNotEmpty)
+ Text(
+ 'Task No: ${taskResult.taskNo!}',
+ style: AppTextStyles.bodyText.copyWith(
+ color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
+ ),
+ ),
+ if (taskResult.siteName != null && taskResult.siteName!.isNotEmpty)
+ Text(
+ 'Site: ${taskResult.siteName}',
+ style: AppTextStyles.bodyText.copyWith(
+ color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
+ ),
+ ),
+ if (taskResult.buildingName != null && taskResult.buildingName!.isNotEmpty)
+ Text(
+ 'Building: ${taskResult.buildingName}',
+ style: AppTextStyles.bodyText.copyWith(
+ color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
+ ),
+ ),
+ if (taskResult.floorName != null && taskResult.floorName!.isNotEmpty)
+ Text(
+ 'Floor: ${taskResult.floorName}',
+ style: AppTextStyles.bodyText.copyWith(
+ color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
+ ),
+ ),
+ if (taskResult.departmentName != null && taskResult.departmentName!.isNotEmpty)
+ Text(
+ 'Department: ${taskResult.departmentName}',
+ style: AppTextStyles.bodyText.copyWith(
+ color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
+ ),
+ ),
+ if (taskResult.roomName != null && taskResult.roomName!.isNotEmpty)
+ Text(
+ 'Room: ${taskResult.roomName}',
+ style: AppTextStyles.bodyText.copyWith(
+ color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
+ ),
+ ),
+ if (taskResult.locationDescription != null && taskResult.locationDescription!.isNotEmpty)
+ Text(
+ 'Location: ${taskResult.locationDescription}',
+ style: AppTextStyles.bodyText.copyWith(
+ color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
+ ),
+ ),
+ ],
+ ).expanded,
+ ],
+ ),
+ ],
+ ).paddingOnly(start: 16, end: 16, top: 16, bottom: 8),
+ ],
+ ).toShadowContainer(context, padding: 0);
+ }
+}
diff --git a/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart b/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart
new file mode 100644
index 00000000..bb9fb739
--- /dev/null
+++ b/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart
@@ -0,0 +1,512 @@
+import 'package:flutter/material.dart';
+import 'package:test_sa/controllers/providers/api/hospitals_provider.dart';
+import 'package:test_sa/extensions/context_extension.dart';
+import 'package:test_sa/extensions/string_extensions.dart';
+import 'package:test_sa/extensions/text_extensions.dart';
+import 'package:test_sa/extensions/widget_extensions.dart';
+import 'package:test_sa/models/hospital.dart';
+import 'package:test_sa/models/lookup.dart';
+import 'package:test_sa/models/new_models/department_room_combined.dart';
+import 'package:test_sa/modules/cleaner_module/pages/cleaner_search_list_view.dart';
+import 'package:test_sa/modules/cleaner_module/provider/cleaner_provider.dart';
+import 'package:test_sa/modules/cleaner_module/provider/fms_building_provider.dart';
+import 'package:test_sa/modules/cleaner_module/provider/fms_floor_provider.dart';
+import 'package:test_sa/modules/cleaner_module/provider/fms_location_description_provider.dart';
+import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
+import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
+import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
+import 'package:test_sa/new_views/common_widgets/single_item_drop_down_menu.dart';
+import 'package:test_sa/new_views/app_style/app_color.dart';
+import 'package:test_sa/providers/loading_list_notifier.dart';
+import 'package:test_sa/models/new_models/building.dart';
+import 'package:test_sa/models/new_models/floor.dart';
+import 'package:test_sa/extensions/int_extensions.dart';
+
+class CleanerSearchFormView extends StatefulWidget {
+ static const String routeName = '/cleaner_search';
+
+ const CleanerSearchFormView({Key? key}) : super(key: key);
+
+ @override
+ State createState() => _CleanerSearchFormViewState();
+}
+
+class _CleanerSearchFormViewState extends State {
+ Hospital? selectedSite;
+ Building? selectedBuilding;
+ Floor? selectedFloor;
+ DepartmentRoomCombined? selectedDepartmentRoom;
+ Lookup? selectedLocationDescription;
+ late FocusNode _siteFocusNode;
+ final TextEditingController _siteController = TextEditingController();
+
+ List _siteOptions = [];
+ List _buildingOptions = [];
+ List _floorOptions = [];
+ List _locationDescriptionOptions = [];
+ List _allDeptRoomList = [];
+
+ bool _isLoadingSites = false;
+ bool _isLoadingBuildings = false;
+ bool _isLoadingFloors = false;
+ bool _isLoadingLocationDescriptions = false;
+ bool _isSearching = false;
+
+ @override
+ void initState() {
+ super.initState();
+ _siteFocusNode = FocusNode();
+ }
+
+ @override
+ void dispose() {
+ _siteController.dispose();
+ _siteFocusNode.dispose();
+ super.dispose();
+ }
+
+ Future _searchSites(String query) async {
+ if (query.isEmpty) {
+ setState(() {
+ _siteOptions = [];
+ });
+ return;
+ }
+
+ setState(() {
+ _isLoadingSites = true;
+ });
+
+ try {
+ final sites = await HospitalsProvider().getHospitalsListByVal(searchVal: query);
+ setState(() {
+ _siteOptions = sites;
+ _isLoadingSites = false;
+ });
+ } catch (e) {
+ setState(() {
+ _isLoadingSites = false;
+ });
+ }
+ }
+
+ Future _loadBuildings(int siteId) async {
+ setState(() {
+ _isLoadingBuildings = true;
+ });
+
+ try {
+ final buildings = await FmsBuildingProvider().getBuildingsListBySiteId(siteId);
+ setState(() {
+ _buildingOptions = buildings;
+ _isLoadingBuildings = false;
+ });
+ } catch (e) {
+ setState(() {
+ _isLoadingBuildings = false;
+ _buildingOptions = [];
+ });
+ }
+ }
+
+ Future _loadFloors(int siteId, int buildingId) async {
+ setState(() {
+ _isLoadingFloors = true;
+ });
+
+ try {
+ final floors = await FmsFloorProvider().getFloorsListBySiteAndBuilding(siteId, buildingId);
+ setState(() {
+ _floorOptions = floors;
+ _isLoadingFloors = false;
+ });
+ } catch (e) {
+ setState(() {
+ _isLoadingFloors = false;
+ _floorOptions = [];
+ });
+ }
+ }
+
+ Future _loadLocationDescriptions(int siteId, int buildingId, int floorId) async {
+ setState(() {
+ _isLoadingLocationDescriptions = true;
+ });
+
+ try {
+ final response = await FmsLocationDescriptionProvider().getLocationDescriptionsList(siteId, buildingId, floorId);
+ setState(() {
+ _locationDescriptionOptions = response?.locationDescriptions ?? [];
+ _allDeptRoomList = response?.deptRoom ?? [];
+ _isLoadingLocationDescriptions = false;
+ });
+ } catch (e) {
+ setState(() {
+ _isLoadingLocationDescriptions = false;
+ _locationDescriptionOptions = [];
+ _allDeptRoomList = [];
+ });
+ }
+ }
+
+ List _getDepartmentRoomCombinations() {
+ if (selectedLocationDescription == null || _allDeptRoomList.isEmpty) return [];
+
+ // Filter dept room list by selected location description
+ List filteredList = _allDeptRoomList.where((item) => item.locationDescriptionName == selectedLocationDescription!.name).toList();
+
+ // Convert to DepartmentRoomCombined
+ return filteredList
+ .map((item) => DepartmentRoomCombined(
+ deptId: item.deptId,
+ roomId: item.roomId,
+ deptName: item.deptName,
+ roomName: item.roomName,
+ locationDescriptionName: item.locationDescriptionName,
+ deptAndRoomName: item.deptAndRoomName,
+ ))
+ .toList();
+ }
+
+ void _autoSelectDepartmentRoom() {
+ final combinations = _getDepartmentRoomCombinations();
+
+ if (combinations.isEmpty) {
+ setState(() {
+ selectedDepartmentRoom = null;
+ });
+ return;
+ }
+
+ // If only one item, auto-fill
+ if (combinations.length == 1) {
+ setState(() {
+ selectedDepartmentRoom = combinations.first;
+ });
+ } else {
+ // If more than one, auto-select first item
+ setState(() {
+ selectedDepartmentRoom = combinations.first;
+ });
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: DefaultAppBar(
+ title: 'Search Request',
+ titleStyle: AppTextStyles.heading3.copyWith(
+ fontWeight: FontWeight.w500,
+ color: Theme.of(context).brightness == Brightness.dark ? AppColor.neutral30 : AppColor.neutral50,
+ ),
+ ),
+ body: Column(
+ children: [
+ Expanded(
+ child: SingleChildScrollView(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Site Autocomplete Field
+ Container(
+ padding: EdgeInsets.symmetric(
+ horizontal: 16.toScreenWidth,
+ vertical: 8.toScreenHeight,
+ ),
+ decoration: BoxDecoration(
+ color: AppColor.fieldBgColor(context),
+ borderRadius: BorderRadius.circular(10),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Site',
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
+ color: context.isDark ? AppColor.neutral30 : AppColor.neutral20,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ SizedBox(height: 4.toScreenHeight),
+ SizedBox(
+ height: 24.toScreenHeight, // Fixed height for consistency
+ child: Autocomplete(
+ optionsBuilder: (TextEditingValue textEditingValue) async {
+ if (textEditingValue.text.isEmpty) {
+ return const Iterable.empty();
+ }
+ await _searchSites(textEditingValue.text);
+ return _siteOptions;
+ },
+ displayStringForOption: (Hospital option) => option.name ?? "",
+ onSelected: (Hospital selected) async {
+ // _siteFocusNode.unfocus();
+ FocusManager.instance.primaryFocus?.unfocus();
+ setState(() {
+ selectedSite = selected;
+ _siteController.text = selected.name ?? '';
+ // Reset dependent dropdowns
+ selectedBuilding = null;
+ selectedFloor = null;
+ selectedLocationDescription = null;
+ selectedDepartmentRoom = null;
+ _buildingOptions = [];
+ _floorOptions = [];
+ _locationDescriptionOptions = [];
+ _allDeptRoomList = [];
+ });
+
+ // Load buildings for selected site
+ if (selected.id != null) {
+ await _loadBuildings(selected.id!);
+ }
+ },
+ fieldViewBuilder: (context, controller, focusNode, onFieldSubmitted) {
+ if (_siteController.text.isNotEmpty && controller.text.isEmpty) {
+ controller.text = _siteController.text;
+ }
+
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Expanded(
+ child: TextField(
+ controller: controller,
+ // focusNode: _siteFocusNode,
+ focusNode: focusNode,
+ style: AppTextStyles.bodyText.copyWith(
+ color: context.isDark ? AppColor.neutral30 : AppColor.black10,
+ ),
+ decoration: InputDecoration(
+ hintText: 'Search site...',
+ hintStyle: AppTextStyles.bodyText.copyWith(
+ color: AppColor.neutral20,
+ ),
+ filled: false,
+ border: InputBorder.none,
+ enabledBorder: InputBorder.none,
+ focusedBorder: InputBorder.none,
+ contentPadding: EdgeInsets.zero,
+ isDense: true,
+ ),
+ onChanged: (value) {
+ _siteController.text = value;
+ },
+ ),
+ ),
+ if (_isLoadingSites)
+ const SizedBox(
+ width: 20,
+ height: 20,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ color: AppColor.primary10,
+ ),
+ )
+ else if (controller.text.isNotEmpty)
+ GestureDetector(
+ onTap: () {
+ controller.clear();
+ _siteController.clear();
+ setState(() {
+ selectedSite = null;
+ selectedBuilding = null;
+ selectedFloor = null;
+ selectedLocationDescription = null;
+ selectedDepartmentRoom = null;
+ _siteOptions = [];
+ _buildingOptions = [];
+ _floorOptions = [];
+ _locationDescriptionOptions = [];
+ _allDeptRoomList = [];
+ });
+ },
+ child: const Icon(
+ Icons.clear,
+ color: AppColor.neutral20,
+ size: 20,
+ ),
+ ),
+ ],
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 18),
+
+ // Building Dropdown
+ SingleItemDropDownMenu(
+ context: context,
+ title: 'Building',
+ initialValue: selectedBuilding,
+ showShadow: false,
+ showAsBottomSheet: true,
+ backgroundColor: AppColor.fieldBgColor(context),
+ enabled: _buildingOptions.isNotEmpty,
+ loading: _isLoadingBuildings,
+ staticData: _buildingOptions,
+ onSelect: (value) async {
+ _siteFocusNode.unfocus();
+
+ setState(() {
+ _siteOptions = [];
+ print('site options length ${_siteOptions.length}');
+ selectedBuilding = value;
+ selectedFloor = null;
+ selectedLocationDescription = null;
+ selectedDepartmentRoom = null;
+ _floorOptions = [];
+ _locationDescriptionOptions = [];
+ _allDeptRoomList = [];
+ });
+
+ // Load floors for selected building
+ if (value != null && selectedSite?.id != null && value.id != null) {
+ await _loadFloors(selectedSite!.id!, value.id!.toInt());
+ }
+ },
+ ),
+ const SizedBox(height: 18),
+
+ // Floor Dropdown
+ SingleItemDropDownMenu(
+ context: context,
+ title: 'Floor',
+ initialValue: selectedFloor,
+ showShadow: false,
+ showAsBottomSheet: true,
+ backgroundColor: AppColor.fieldBgColor(context),
+ enabled: _floorOptions.isNotEmpty,
+ loading: _isLoadingFloors,
+ staticData: _floorOptions,
+ onSelect: (value) async {
+ setState(() {
+ selectedFloor = value;
+ selectedLocationDescription = null;
+ selectedDepartmentRoom = null;
+ _locationDescriptionOptions = [];
+ _allDeptRoomList = [];
+ });
+
+ // Load location descriptions for selected floor
+ if (value != null && selectedSite?.id != null && selectedBuilding?.id != null && value.id != null) {
+ await _loadLocationDescriptions(
+ selectedSite!.id!,
+ selectedBuilding!.id!.toInt(),
+ value.id!.toInt(),
+ );
+ }
+ },
+ ),
+ const SizedBox(height: 18),
+
+ // Location Description Dropdown
+ SingleItemDropDownMenu(
+ context: context,
+ title: 'Location Description',
+ initialValue: selectedLocationDescription,
+ showShadow: false,
+ showAsBottomSheet: true,
+ backgroundColor: AppColor.fieldBgColor(context),
+ enabled: _locationDescriptionOptions.isNotEmpty,
+ loading: _isLoadingLocationDescriptions,
+ staticData: _locationDescriptionOptions,
+ onSelect: (value) {
+ setState(() {
+ selectedLocationDescription = value;
+ selectedDepartmentRoom = null;
+ });
+
+ // Auto-select department-room based on location description
+ _autoSelectDepartmentRoom();
+ },
+ ),
+ const SizedBox(height: 18),
+
+ // Department & Room Combined Dropdown
+ SingleItemDropDownMenu(
+ context: context,
+ title: 'Department - Room',
+ initialValue: selectedDepartmentRoom,
+ showShadow: false,
+ showAsBottomSheet: true,
+ backgroundColor: AppColor.fieldBgColor(context),
+ enabled: _getDepartmentRoomCombinations().isNotEmpty,
+ staticData: _getDepartmentRoomCombinations(),
+ onSelect: (value) {
+ setState(() {
+ selectedDepartmentRoom = value;
+ });
+ },
+ ),
+ ],
+ ).toShadowContainer(context).paddingOnly(top: 20, start: 16, end: 16),
+ ),
+ ),
+ FooterActionButton.footerContainer(
+ context: context,
+ child: AppFilledButton(
+ buttonColor: AppColor.primary10,
+ label: context.translation.search,
+ maxWidth: true,
+ onPressed: _isSearching ? null : _onSubmit,
+ loading: _isSearching,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Future _onSubmit() async {
+ // Validate that at least site is selected
+ if (selectedSite == null) {
+ 'Please select a site'.showToast;
+ return;
+ }
+
+ try {
+ setState(() {
+ _isSearching = true;
+ });
+
+ // Call the search API
+ final response = await CleanerProvider().searchPlanRecurrentTasksList(
+ pageSize: 10,
+ pageNumber: 1,
+ siteId: selectedSite?.id,
+ buildingId: selectedBuilding?.id?.toInt(),
+ floorId: selectedFloor?.id?.toInt(),
+ departmentId: selectedDepartmentRoom?.deptId,
+ roomId: selectedDepartmentRoom?.roomId,
+ );
+
+ final results = response['data'] as List;
+ final totalRows = response['totalRows'] as int;
+
+ // Navigate to search results page with the results
+ Navigator.of(context).pushNamed(
+ CleanerSearchListView.routeName,
+ arguments: {
+ 'searchResults': results,
+ 'totalRows': totalRows,
+ 'selectedSite': selectedSite,
+ 'selectedBuilding': selectedBuilding,
+ 'selectedFloor': selectedFloor,
+ 'selectedLocationDescription': selectedLocationDescription,
+ 'selectedDepartmentRoom': selectedDepartmentRoom,
+ },
+ );
+ } catch (e) {
+ 'Error searching'.showToast;
+ } finally {
+ setState(() {
+ _isSearching = false;
+ });
+ }
+ }
+}
diff --git a/lib/modules/cleaner_module/pages/cleaner_search_list_view.dart b/lib/modules/cleaner_module/pages/cleaner_search_list_view.dart
new file mode 100644
index 00000000..884ba220
--- /dev/null
+++ b/lib/modules/cleaner_module/pages/cleaner_search_list_view.dart
@@ -0,0 +1,186 @@
+import 'package:flutter/material.dart';
+import 'package:test_sa/extensions/context_extension.dart';
+import 'package:test_sa/extensions/string_extensions.dart';
+import 'package:test_sa/extensions/widget_extensions.dart';
+import 'package:test_sa/models/hospital.dart';
+import 'package:test_sa/models/lookup.dart';
+import 'package:test_sa/models/new_models/building.dart';
+import 'package:test_sa/models/new_models/department_room_combined.dart';
+import 'package:test_sa/models/new_models/floor.dart';
+import 'package:test_sa/modules/cleaner_module/models/cleaner_task_search_result.dart';
+import 'package:test_sa/modules/cleaner_module/pages/cleaner_search_details_view.dart';
+import 'package:test_sa/modules/cleaner_module/provider/cleaner_provider.dart';
+import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
+import 'package:test_sa/views/widgets/loaders/app_loading.dart';
+import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
+
+import 'cleaner_request_item_view.dart';
+
+class CleanerSearchListView extends StatefulWidget {
+ static const String routeName = '/cleaner_search_list';
+
+ const CleanerSearchListView({Key? key}) : super(key: key);
+
+ @override
+ State createState() => _CleanerSearchListViewState();
+}
+
+class _CleanerSearchListViewState extends State {
+ List _searchResults = [];
+ bool _isLoading = false;
+ bool _isLoadingMore = false;
+ bool _hasMoreData = true;
+ int _totalRows = 0; // Add this to track total rows from API
+
+ // Pagination
+ int _currentPage = 1;
+ final int _pageSize = 10;
+
+ // Search filters (stored for pagination)
+ Hospital? _selectedSite;
+ Building? _selectedBuilding;
+ Floor? _selectedFloor;
+ Lookup? _selectedLocationDescription;
+ DepartmentRoomCombined? _selectedDepartmentRoom;
+
+ final ScrollController _scrollController = ScrollController();
+ final CleanerProvider _cleanerProvider = CleanerProvider();
+
+ @override
+ void initState() {
+ super.initState();
+ _scrollController.addListener(_onScroll);
+ }
+
+ @override
+ void dispose() {
+ _scrollController.dispose();
+ super.dispose();
+ }
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ // Get the search results and filters from navigation arguments
+ final args = ModalRoute.of(context)?.settings.arguments as Map?;
+ if (args != null && args['searchResults'] != null) {
+ final results = args['searchResults'] as List;
+ _searchResults = results.map((json) => CleanerTaskSearchResult.fromJson(json)).toList();
+
+ // Get totalRows from API response
+ _totalRows = args['totalRows'] as int? ?? 0;
+
+ // Store search filters for pagination
+ _selectedSite = args['selectedSite'] as Hospital?;
+ _selectedBuilding = args['selectedBuilding'] as Building?;
+ _selectedFloor = args['selectedFloor'] as Floor?;
+ _selectedLocationDescription = args['selectedLocationDescription'] as Lookup?;
+ _selectedDepartmentRoom = args['selectedDepartmentRoom'] as DepartmentRoomCombined?;
+
+ // Check if there's more data based on totalRows
+ _hasMoreData = _searchResults.length < _totalRows;
+ }
+ }
+
+ void _onScroll() {
+ if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) {
+ // User is near the bottom, load more data
+ if (!_isLoadingMore && _hasMoreData && !_isLoading) {
+ _loadMoreData();
+ }
+ }
+ }
+
+ Future _loadMoreData() async {
+ if (_isLoadingMore) return;
+
+ setState(() {
+ _isLoadingMore = true;
+ });
+
+ try {
+ _currentPage++;
+
+ final response = await _cleanerProvider.searchPlanRecurrentTasksList(
+ pageSize: _pageSize,
+ pageNumber: _currentPage,
+ siteId: _selectedSite?.id,
+ buildingId: _selectedBuilding?.id?.toInt(),
+ floorId: _selectedFloor?.id?.toInt(),
+ departmentId: _selectedDepartmentRoom?.deptId,
+ roomId: _selectedDepartmentRoom?.roomId,
+ );
+
+ final results = response['data'] as List;
+ final totalRows = response['totalRows'] as int? ?? 0;
+
+ final newResults = results.map((json) => CleanerTaskSearchResult.fromJson(json)).toList();
+
+ setState(() {
+ _searchResults.addAll(newResults);
+ _totalRows = totalRows;
+ // Check if we have loaded all data based on totalRows
+ _hasMoreData = _searchResults.length < _totalRows;
+ _isLoadingMore = false;
+ });
+ } catch (e) {
+ setState(() {
+ _isLoadingMore = false;
+ });
+ debugPrint('Error loading more data: $e');
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: DefaultAppBar(
+ title: 'Search Results'.addTranslation,
+ onBackPress: () => Navigator.pop(context),
+ ),
+ body: SafeArea(
+ child: Column(
+ children: [
+ Expanded(
+ child: (_searchResults.isEmpty && !_isLoading)
+ ? NoDataFound(message: context.translation.noDataFound).center
+ : ListView.separated(
+ controller: _scrollController,
+ padding: const EdgeInsets.all(16),
+ itemCount: _searchResults.length + (_isLoadingMore ? 1 : 0),
+ separatorBuilder: (_, __) => const SizedBox(height: 16),
+ itemBuilder: (context, index) {
+ if (_isLoading && _searchResults.isEmpty) {
+ return const SizedBox().toRequestShimmer(context, _isLoading);
+ }
+
+ // Show loading indicator at the bottom
+ if (index == _searchResults.length) {
+ return const ALoading().center;
+ }
+
+ final result = _searchResults[index];
+
+ return CleanerRequestItemView(
+ result: result,
+ onTap: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => CleanerSearchDetailsView(
+ requestId: result.id ?? 0,
+ taskResult: result,
+ ),
+ ),
+ );
+ },
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/modules/cleaner_module/provider/cleaner_provider.dart b/lib/modules/cleaner_module/provider/cleaner_provider.dart
new file mode 100644
index 00000000..a057aa54
--- /dev/null
+++ b/lib/modules/cleaner_module/provider/cleaner_provider.dart
@@ -0,0 +1,169 @@
+import 'dart:convert';
+
+import 'package:flutter/material.dart';
+import 'package:http/http.dart';
+import 'package:test_sa/controllers/api_routes/api_manager.dart';
+import 'package:test_sa/controllers/api_routes/urls.dart';
+
+class CleanerProvider extends ChangeNotifier {
+ bool _isLoading = false;
+ bool get isLoading => _isLoading;
+
+ // Search results
+ List? _searchResults;
+ List? get searchResults => _searchResults;
+
+ // State code for error handling
+ int? _stateCode;
+ int? get stateCode => _stateCode;
+
+ // TODO: Add your cleaner request model here
+ // CleanerRequestModel? _currentCleanerRequest;
+ // CleanerRequestModel? get currentCleanerRequest => _currentCleanerRequest;
+
+ Future getCleanerRequestById({required int id}) async {
+ _isLoading = true;
+ notifyListeners();
+
+ try {
+ // TODO: Implement API call to fetch cleaner request details
+ // Example:
+ // final response = await CleanerApiService.getCleanerRequestById(id);
+ // _currentCleanerRequest = response.data;
+
+ await Future.delayed(const Duration(seconds: 1)); // Simulating API call
+
+ } catch (e) {
+ debugPrint('Error fetching cleaner request: $e');
+ } finally {
+ _isLoading = false;
+ notifyListeners();
+ }
+ }
+
+ /// Search plan recurrent tasks
+ Future searchPlanRecurrentTasks({
+ required int pageSize,
+ required int pageNumber,
+ int? siteId,
+ int? buildingId,
+ int? floorId,
+ int? departmentId,
+ int? roomId,
+ }) async {
+ if (_isLoading == true) return -2;
+ _isLoading = true;
+ notifyListeners();
+
+ Response response;
+ try {
+ response = await ApiManager.instance.post(
+ URLs.searchPlanRecurrentTaskListSearch,
+ body: {
+ "pageSize": pageSize,
+ "pageNumber": pageNumber,
+ "siteId": siteId,
+ "buildingId": buildingId,
+ "floorId": floorId,
+ "departmentId": departmentId,
+ "roomId": roomId,
+ },
+ );
+
+ _stateCode = response.statusCode;
+
+ if (response.statusCode >= 200 && response.statusCode < 300) {
+ // client's request was successfully received
+ final responseData = json.decode(response.body);
+ _searchResults = responseData["data"] as List?;
+ } else {
+ _searchResults = null;
+ }
+
+ _isLoading = false;
+ notifyListeners();
+ return response.statusCode;
+ } catch (error) {
+ debugPrint('Error searching plan recurrent tasks: $error');
+ _isLoading = false;
+ _stateCode = -1;
+ _searchResults = null;
+ notifyListeners();
+ return -1;
+ }
+ }
+
+ /// Search plan recurrent tasks and return results directly without provider state
+ Future