housekeeping module completed

export_module
WaseemAbbasi22 2 months ago
parent 3352c73e1d
commit 33f1f07e86

@ -0,0 +1,6 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.10515e-06 1C7.10515e-06 0.447715 0.447722 0 1.00001 0H17C17.5523 0 18 0.447715 18 1C18 1.55228 17.5523 2 17 2H1.00001C0.447722 2 7.10515e-06 1.55228 7.10515e-06 1Z" fill="#212936"/>
<path d="M1.00001 6.00001C0.447722 6 3.92408e-06 6.44772 0 7C-3.92403e-06 7.55228 0.447708 8 0.999993 8.00001L12 8.00008C12.5523 8.00009 13 7.55238 13 7.00009C13 6.44781 12.5523 6.00009 12 6.00009L1.00001 6.00001Z" fill="#212936"/>
<path d="M1.00001 12C0.447722 12 7.10515e-06 12.4477 7.10515e-06 13C7.10515e-06 13.5523 0.447722 14 1.00001 14H8.00001C8.55229 14 9.00001 13.5523 9.00001 13C9.00001 12.4477 8.55229 12 8.00001 12H1.00001Z" fill="#212936"/>
<path d="M15.1616 15.5C14.986 15.4999 14.7703 15.4997 14.5939 15.5218L14.5905 15.5222C14.4641 15.538 13.888 15.6098 13.6137 16.1754C13.3387 16.7422 13.6407 17.2424 13.706 17.3506L14.0451 17.819C14.3398 18.1949 14.7455 18.7093 15.1259 19.1003C15.3157 19.2954 15.533 19.4967 15.7639 19.6555C15.9691 19.7967 16.3193 20 16.75 20C17.1807 20 17.5309 19.7967 17.7361 19.6555C17.967 19.4967 18.1843 19.2954 18.3741 19.1003C18.7546 18.7093 19.1603 18.1949 19.4549 17.8191L19.794 17.3506C19.8594 17.2424 20.1613 16.7422 19.8863 16.1754C19.612 15.6098 19.0359 15.538 18.9095 15.5222L18.9061 15.5218C18.7297 15.4997 18.514 15.4999 18.3384 15.5L17.75 15.5V6.99996C17.75 6.44768 17.3023 5.99996 16.75 5.99996C16.1977 5.99996 15.75 6.44768 15.75 6.99996V15.5L15.1616 15.5Z" fill="#212936"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

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

@ -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<DashBoardProvider>(builder: (context, dashboardProvider, child) {
bool isNurse = (Provider.of<UserProvider>(context, listen: false).user?.type) == UsersTypes.normal_user;
final user = Provider.of<UserProvider>(context, listen: false).user;
final settingProvider = Provider.of<SettingProvider>(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: <Widget>[
//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,

@ -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<AllRequestsProvider>(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<AllRequestsProvider>(
// builder: (context, snapshot, _) => RefreshIndicator(
// onRefresh: () {
// snapshot.getRequests();
// Provider.of<NotificationsProvider>(context, listen: false)
// .getSystemNotifications(
// user: Provider.of<UserProvider>(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<AllRequestsProvider>(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<AllRequestsProvider>(
// builder: (context, snapshot, _) => RefreshIndicator(
// onRefresh: () {
// snapshot.getRequests();
// Provider.of<NotificationsProvider>(context, listen: false)
// .getSystemNotifications(
// user: Provider.of<UserProvider>(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

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

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

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

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

@ -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<String, dynamic> json) {
return DepartmentRoomCombined(
deptId: json['deptId'],
roomId: json['roomId'],
deptName: json['deptName'],
roomName: json['roomName'],
locationDescriptionName: json['locationDescriptionName'],
deptAndRoomName: json['deptAndRoomName'],
);
}
Map<String, dynamic> 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;
}

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

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

@ -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>? planRecurrentMedicalTaskRooms;
List<PlanRecurrentTaskTimers>? planRecurrentTaskTimers;
List<ChecklistItem>? planRecurrentTaskCheckLists;
TimerModel? recurrentWoTimerModel = TimerModel();
double? totalWorkingHours = 0.0;
List<TimerModel>? 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<String, dynamic> 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 = <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 = <ChecklistItem>[];
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<String, dynamic> 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 {

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

@ -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<String, dynamic> 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<String, dynamic> toJson() {
return {
'id': id,
'taskNo': taskNo,
'siteName': siteName,
'buildingName': buildingName,
'floorName': floorName,
'departmentName': departmentName,
'roomName': roomName,
'locationDescription': locationDescription,
'createdDate': createdDate,
};
}
String get fullLocation {
List<String> 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 '';
}
}

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

@ -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<CleanerSearchDetailsView> {
late CleanerProvider _cleanerProvider;
bool _isQRScanned = false;
CleanerTaskSearchResult? _taskData;
bool _isLoading = false;
@override
void initState() {
super.initState();
_cleanerProvider = Provider.of<CleanerProvider>(context, listen: false);
_taskData = widget.taskResult;
WidgetsBinding.instance.addPostFrameCallback((_) {
getInitialData();
});
}
Future<void> 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<AllRequestsProvider>(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<CleanerProvider, bool>(
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);
}
}

@ -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<CleanerSearchFormView> createState() => _CleanerSearchFormViewState();
}
class _CleanerSearchFormViewState extends State<CleanerSearchFormView> {
Hospital? selectedSite;
Building? selectedBuilding;
Floor? selectedFloor;
DepartmentRoomCombined? selectedDepartmentRoom;
Lookup? selectedLocationDescription;
late FocusNode _siteFocusNode;
final TextEditingController _siteController = TextEditingController();
List<Hospital> _siteOptions = [];
List<Building> _buildingOptions = [];
List<Floor> _floorOptions = [];
List<Lookup> _locationDescriptionOptions = [];
List<DepartmentRoomItem> _allDeptRoomList = [];
bool _isLoadingSites = false;
bool _isLoadingBuildings = false;
bool _isLoadingFloors = false;
bool _isLoadingLocationDescriptions = false;
bool _isSearching = false;
@override
void initState() {
super.initState();
_siteFocusNode = FocusNode();
}
@override
void dispose() {
_siteController.dispose();
_siteFocusNode.dispose();
super.dispose();
}
Future<void> _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<void> _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<void> _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<void> _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<DepartmentRoomCombined> _getDepartmentRoomCombinations() {
if (selectedLocationDescription == null || _allDeptRoomList.isEmpty) return [];
// Filter dept room list by selected location description
List<DepartmentRoomItem> filteredList = _allDeptRoomList.where((item) => item.locationDescriptionName == selectedLocationDescription!.name).toList();
// Convert to DepartmentRoomCombined
return filteredList
.map((item) => DepartmentRoomCombined(
deptId: item.deptId,
roomId: item.roomId,
deptName: item.deptName,
roomName: item.roomName,
locationDescriptionName: item.locationDescriptionName,
deptAndRoomName: item.deptAndRoomName,
))
.toList();
}
void _autoSelectDepartmentRoom() {
final combinations = _getDepartmentRoomCombinations();
if (combinations.isEmpty) {
setState(() {
selectedDepartmentRoom = null;
});
return;
}
// If only one item, auto-fill
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<Hospital>(
optionsBuilder: (TextEditingValue textEditingValue) async {
if (textEditingValue.text.isEmpty) {
return const Iterable<Hospital>.empty();
}
await _searchSites(textEditingValue.text);
return _siteOptions;
},
displayStringForOption: (Hospital option) => option.name ?? "",
onSelected: (Hospital selected) async {
// _siteFocusNode.unfocus();
FocusManager.instance.primaryFocus?.unfocus();
setState(() {
selectedSite = selected;
_siteController.text = selected.name ?? '';
// Reset dependent dropdowns
selectedBuilding = null;
selectedFloor = null;
selectedLocationDescription = null;
selectedDepartmentRoom = null;
_buildingOptions = [];
_floorOptions = [];
_locationDescriptionOptions = [];
_allDeptRoomList = [];
});
// Load buildings for selected site
if (selected.id != null) {
await _loadBuildings(selected.id!);
}
},
fieldViewBuilder: (context, controller, focusNode, onFieldSubmitted) {
if (_siteController.text.isNotEmpty && controller.text.isEmpty) {
controller.text = _siteController.text;
}
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextField(
controller: controller,
// focusNode: _siteFocusNode,
focusNode: focusNode,
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<Building, NullableLoadingProvider>(
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<Floor, NullableLoadingProvider>(
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<Lookup, NullableLoadingProvider>(
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<DepartmentRoomCombined, NullableLoadingProvider>(
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<void> _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<dynamic>;
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;
});
}
}
}

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

@ -0,0 +1,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<dynamic>? _searchResults;
List<dynamic>? 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<void> 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<int> 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<dynamic>?;
} 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<Map<String, dynamic>> 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<dynamic>? ?? [],
"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<bool> 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();
}
}

@ -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<Building>? _buildings;
List<Building>? 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<int> 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<List<Building>> 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 [];
}
}
}

@ -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<Floor>? _floors;
List<Floor>? get floors => _floors;
// loading state
bool _loading = false;
bool get isLoading => _loading;
set isLoading(bool isLoading) {
_loading = isLoading;
notifyListeners();
}
/// Get floors by site and building ID
Future<int> getFloorsBySiteAndBuilding(int siteId, int buildingId) async {
if (_loading == true) return -2;
_loading = true;
notifyListeners();
Response response;
try {
response = await ApiManager.instance.post(
URLs.getCustomerFloors,
body: {
"siteId": siteId,
"buildingId": buildingId,
},
);
_stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) {
// client's request was successfully received
List floorsListJson = json.decode(response.body)["data"];
_floors = floorsListJson.map((floor) => Floor.fromJson(floor)).toList();
}
_loading = false;
notifyListeners();
return response.statusCode;
} catch (error) {
_loading = false;
_stateCode = -1;
notifyListeners();
return -1;
}
}
/// Get floors list directly without provider state
Future<List<Floor>> getFloorsListBySiteAndBuilding(int siteId, int buildingId) async {
Response response;
try {
response = await ApiManager.instance.post(
URLs.getCustomerFloors,
body: {
"siteId": siteId,
"buildingId": buildingId,
},
);
if (response.statusCode >= 200 && response.statusCode < 300) {
List floorsListJson = json.decode(response.body)["data"];
return floorsListJson.map((floor) => Floor.fromJson(floor)).toList();
}
return [];
} catch (error) {
return [];
}
}
}

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

@ -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<ChecklistItem> checklistItems;
final Function(List<ChecklistItem>)? 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<CleanerChecklistWidget> createState() => _CleanerChecklistWidgetState();
}
class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
@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<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'isChecked': isChecked,
'comment': commentController.text,
'isMandatory': isMandatory,
};
}
void dispose() {
commentController.dispose();
}
}

@ -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<RecurrentWorkOrderView> {
],
)
: 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<RecurrentWorkOrderView> {
void _updateTask({required BuildContext context, required int status}) async {
AllRequestsProvider allRequestsProvider = Provider.of<AllRequestsProvider>(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) {

@ -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<UserProvider>(context, listen: false).user;
final settingProvider = Provider.of<SettingProvider>(context, listen: false);
final bool isFMSEngineer = user?.type == UsersTypes.engineer && settingProvider.isUserFMS;
if (requestData != null) {
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)));
});
}

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

Loading…
Cancel
Save