Merge branch 'refs/heads/main_production_ai_enhancement' into main_production_ai_enhancement_rollout_changes

# Conflicts:
#	lib/controllers/api_routes/urls.dart
#	lib/controllers/providers/api/all_requests_provider.dart
main_production_ai_enhancement
Sikander Saleem 2 weeks ago
commit 9574937596

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

@ -1,7 +1,7 @@
class URLs {
URLs._();
static const String appReleaseBuildNumber = "40";
static const String appReleaseBuildNumber = "41";
// static const host1 = "https://atomsm.hmg.com"; // production url
// static const host1 = "https://atomsmdev.hmg.com"; // local DEV url
@ -59,13 +59,24 @@ 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
//One Time Task Urls cleaner module new flow urls ....
static get getTaskType => "$_baseUrl/OneTimeTasks/GetTaskTypes";
static get scanQRCode => "$_baseUrl/OneTimeTasks/ScanQRCode";
static get proceedRequest => "$_baseUrl/OneTimeTasks/Proceed";
static get completeRequest => "$_baseUrl/OneTimeTasks/Complete";
static get getRequestById => "$_baseUrl/OneTimeTasks/GetById";
//service request new flow urls.
static get nurseDashboardCountUrl => '$_baseUrl/ServiceRequest/GetDashboardNurseCount';
@ -528,4 +539,4 @@ class URLs {
static get updateDemoPeriodByRequester => "$_baseUrl/DemoRequest/UpdateDemoPeriodByRequester";
//4104 attachment type lookup
}
}

@ -183,9 +183,11 @@ class AllRequestsProvider extends ChangeNotifier {
// list = [16];
list.add(16);
}
list.forEach((element) {
log('request status are ${element.toString()}');
});
if ( context.userProvider.isHouseKeeper) {
list = [5];
}
return list;
}
@ -281,23 +283,27 @@ class AllRequestsProvider extends ChangeNotifier {
}
}
Future<bool> updateRecurrentWo({required int status}) async {
isLoading = true;
Future<Map<String, dynamic>> updateRecurrentWo({required int status, bool showToast = true}) async {
Response response;
try {
response = await ApiManager.instance.post(URLs.updateRecurrentPlanUrl, body: recurrentWoData!.toJson(status: status));
response = await ApiManager.instance.post(URLs.updateRecurrentPlanUrl, body: recurrentWoData!.toJson(status: status), showToast: showToast);
stateCode = response.statusCode;
isLoading = false;
notifyListeners();
if (stateCode == 200) {
return true;
return {'success': true, 'message': ''};
} else {
// Parse error message from response
try {
final responseBody = jsonDecode(response.body);
String errorMessage = responseBody['message'] ?? responseBody['title'] ?? 'Failed to update the task';
return {'success': false, 'message': errorMessage};
} catch (e) {
return {'success': false, 'message': 'Failed to update the task. Status code: $stateCode'};
}
}
return false;
} catch (error) {
isLoading = false;
stateCode = -1;
notifyListeners();
return false;
return {'success': false, 'message': error.toString()};
}
}

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

@ -118,6 +118,7 @@ class DashBoardProvider extends ChangeNotifier {
UsersTypes.qualityUser: URLs.qaAuditorCount,
UsersTypes.commissioningEngineer: URLs.commissioningEngineerCount,
UsersTypes.engineer: URLs.engineerDashboardCountUrl,
UsersTypes.houseKeeper: URLs.engineerDashboardCountUrl,
UsersTypes.assessor: URLs.engineerDashboardCountUrl,
UsersTypes.assessorTl: URLs.engineerDashboardCountUrl,
};
@ -257,6 +258,7 @@ class DashBoardProvider extends ChangeNotifier {
case UsersTypes.engineer:
case UsersTypes.assessor:
case UsersTypes.assessorTl:
case UsersTypes.houseKeeper:
if (status == 0) {
return URLs.engineerDashboardUpcoming;
} else if (status == 1 && tabId == 1) {
@ -413,6 +415,14 @@ class CategoryTabs {
tabs.add(CategoryTabs(label: 'Completed', tag: 3, id: 4));
return tabs;
}
if (userType == UsersTypes.houseKeeper) {
// tabs.add(CategoryTabs(label: 'Upcoming', tag: 0, id: 0));
// tabs.add(CategoryTabs('Open', 1));
// tabs.add(CategoryTabs(label: 'UnAssigned', tag: 1, id: 1));
tabs.add(CategoryTabs(label: 'In Progress', tag: 2, id: 2));
tabs.add(CategoryTabs(label: 'Completed', tag: 4, id: 3));
return tabs;
}
if (userType == UsersTypes.qualityUser) {
tabs.add(CategoryTabs(label: 'Open', tag: 1, id: 2));

@ -183,12 +183,19 @@ class ProgressFragment extends StatelessWidget {
ChartData('In Progress', snapshot.dashboardCount?.data?.countInprogress?.toDouble() ?? 0.0, AppColor.orange70),
];
} else if (_userProvider.isQualityUser) {
total = (snapshot.dashboardCount?.data?.countComplete ?? 0) + (snapshot.dashboardCount?.data?.countInprogress ?? 0) + (snapshot.dashboardCount?.data?.countOpen ?? 0);
total = (snapshot.dashboardCount?.data?.countClose ?? 0) + (snapshot.dashboardCount?.data?.countInprogress ?? 0) + (snapshot.dashboardCount?.data?.countOpen ?? 0);
chartData = [
ChartData('Closed', snapshot.dashboardCount?.data?.countComplete?.toDouble() ?? 0.0, AppColor.green70),
ChartData('Closed', snapshot.dashboardCount?.data?.countClose?.toDouble() ?? 0.0, AppColor.green70),
ChartData('Open', snapshot.dashboardCount?.data?.countOpen?.toDouble() ?? 0.0, AppColor.blueStatus(context)),
ChartData('In Progress', snapshot.dashboardCount?.data?.countInprogress?.toDouble() ?? 0.0, AppColor.orange70),
];
} else if (_userProvider.isHouseKeeper) {
total = (snapshot.dashboardCount?.data?.countClose ?? 0) + (snapshot.dashboardCount?.data?.countInprogress ?? 0) + (snapshot.dashboardCount?.data?.countOpen ?? 0);
chartData = [
ChartData('In Progress', snapshot.dashboardCount?.data?.countInprogress?.toDouble() ?? 0.0, AppColor.orange70),
ChartData('Completed', snapshot.dashboardCount?.data?.countClose?.toDouble() ?? 0.0, AppColor.green70),
// ChartData('Open', snapshot.dashboardCount?.data?.countOpen?.toDouble() ?? 0.0, AppColor.blueStatus(context)),
];
} else {
total = (snapshot.dashboardCount?.data?.countComplete ?? 0) + (snapshot.dashboardCount?.data?.countInprogress ?? 0) + (snapshot.dashboardCount?.data?.countOpen ?? 0);
chartData = [

@ -1,11 +1,11 @@
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';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/enums/user_types.dart';
@ -13,6 +13,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 +22,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 houseKeeper
final bool isHouseKeeper = user?.type == UsersTypes.houseKeeper;
return Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
//TODO uncommit this to get the calendar card..
@ -48,7 +54,17 @@ class RequestCategoryFragment extends StatelessWidget {
: const SizedBox(),
],
).paddingOnly(start: 14, end: 12)
: context.translation.allWorkOrder.heading4(context).paddingOnly(start: 16, end: 16),
: isHouseKeeper
? Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
'All Tasks'.addTranslation.heading4(context),
'add_icon'.toSvgAsset(width: 20, height: 20).onPress(() {
Navigator.of(context).pushNamed('/cleaner_search');
}),
],
).paddingOnly(start: 16, end: 16)
: context.translation.allWorkOrder.heading4(context).paddingOnly(start: 16, end: 16),
getTabs(
requestsProvider: dashboardProvider,
context: context,

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

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

@ -15,6 +15,8 @@ extension WidgetExtensions on Widget {
Widget get center => Center(child: this);
Widget ignorePointer(bool value) => IgnorePointer(ignoring: value, child: this);
BoxShadow get boxShadowR14 => BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 5, offset: const Offset(0, 0), spreadRadius: 0);
Widget circle(double value) => ClipRRect(borderRadius: BorderRadius.circular(value), child: this);

@ -140,6 +140,8 @@ 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/provider/cleaner_provider.dart';
// class MyHttpOverrides extends HttpOverrides {
// @override
@ -224,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),
@ -349,6 +351,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),
],
@ -403,6 +408,7 @@ class MyApp extends StatelessWidget {
CreateLoanRequestPage.id: (_) => CreateLoanRequestPage(),
CreateMedicalGasRequestPage.id: (_) => CreateMedicalGasRequestPage(),
CreateDemoRequestPage.id: (_) => CreateDemoRequestPage(),
CleanerSearchFormView.routeName: (_) => const CleanerSearchFormView(),
},
),
),

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

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

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

@ -37,9 +37,10 @@ class Data {
int? countComplete;
int? countAcknowledge;
int? countHighPriority;
int? countClose;
int? countOverdue;
Data({this.countOpen, this.countInprogress, this.countComplete, this.countAcknowledge, this.countHighPriority, this.countOverdue});
Data({this.countOpen, this.countInprogress, this.countClose,this.countComplete, this.countAcknowledge, this.countHighPriority, this.countOverdue});
Data.fromJson(Map<String, dynamic> json) {
countOpen = json['countOpen'] ?? 0;
@ -47,6 +48,7 @@ class Data {
countComplete = json['countComplete'] ?? 0;
countAcknowledge = json['countAcknowledge'] ?? 0;
countHighPriority = json['countHighPriority'] ?? 0;
countClose = json['countClose'] ?? 0;
countOverdue = json['countOverdue'] ?? 0;
}
@ -57,6 +59,7 @@ class Data {
data['countComplete'] = countComplete;
data['countAcknowledge'] = countAcknowledge;
data['countHighPriority'] = countHighPriority;
data['countClose'] = countClose;
data['countOverdue'] = countOverdue;
return data;
}

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

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

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

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

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

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

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

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

@ -0,0 +1,413 @@
// UI: Cleaner Search Details View
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/ppm/recurrent_wo.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
import 'package:test_sa/views/widgets/qr/scan_qr.dart';
import 'package:test_sa/views/widgets/requests/request_status.dart';
import '../models/cleaner_task_search_result.dart';
import '../provider/cleaner_provider.dart';
import '../../pm_module/recurrent_wo/recurrent_work_order_view.dart';
import 'package:test_sa/controllers/providers/api/all_requests_provider.dart';
import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart';
import 'package:test_sa/controllers/api_routes/api_manager.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:http/http.dart' as http;
class CleanerSearchDetailsView extends StatefulWidget {
static const String routeName = '/cleaner_search_details';
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;
bool _isRequestOpen = false;
CleanerTaskSearchResult? _taskData;
RecurrentWoData? _recurrentWoData;
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 {
// Use direct API call to get better error messages
final result = await _getRecurrentWoByIdWithMessage(widget.requestId);
if (result["success"] == true && result["data"] != null) {
_recurrentWoData = result["data"];
_isRequestOpen = _recurrentWoData?.status?.value == 4;
_taskData = CleanerTaskSearchResult(
id: _recurrentWoData!.id,
taskNo: _recurrentWoData!.taskNo,
siteName: _recurrentWoData!.site?.siteName,
buildingName: _recurrentWoData!.building?.name,
floorName: _recurrentWoData!.floor?.name,
departmentName: _recurrentWoData!.department?.name,
roomName: _recurrentWoData!.room?.name,
locationDescription: null,
createdDate: _recurrentWoData!.scheduleDate,
);
} else {
// Show exact backend error message
if (mounted) {
_showErrorDialog(context, result["message"] ?? "Failed to load request details. Please try again.");
}
}
} catch (e) {
debugPrint('Error fetching recurrent work order: $e');
if (mounted) {
_showErrorDialog(context, "Failed to load request details. Please check your connection and try again.");
}
} finally {
setState(() {
_isLoading = false;
});
}
} else {
try {
await _cleanerProvider.getCleanerRequestById(id: widget.requestId);
} catch (e) {
debugPrint('Error fetching cleaner request: $e');
if (mounted) {
_showErrorDialog(context, "Failed to load request details. Please check your connection and try again.");
}
}
}
}
@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())
: SafeArea(
child: Selector<CleanerProvider, bool>(
selector: (_, model) => model.isLoading,
builder: (_, isLoading, __) {
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
return Column(
children: [
SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildCleanerRequestDetailCard(context),
],
),
).expanded,
FooterActionButton.footerContainer(
context: context,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_isRequestOpen) ...[
Row(
children: [
Icon(Icons.info_outline_rounded, size: 16, color: context.isDark ? AppColor.neutral10 : AppColor.neutral120),
8.width,
Expanded(
child: Text(
'By scanning the QR code you will accept this task'.addTranslation,
// textAlign: TextAlign.center,
style: AppTextStyles.bodyText2.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
),
],
),
8.height,
],
AppFilledButton(
label: _isRequestOpen ? 'Scan QR Code' : 'Update',
maxWidth: true,
buttonColor: AppColor.primary10,
onPressed: () async {
if (_isRequestOpen) {
final result = await Navigator.push(context, MaterialPageRoute(builder: (context) => const ScanQr()));
if (result != null && result is String && result.isNotEmpty) {
showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading());
try {
final qrResult = await CleanerProvider().checkQRSameRoomWithMessage(planRecurrentTaskId: widget.requestId, qrCode: result);
if (mounted) Navigator.pop(context);
final isValid = qrResult["success"] ?? false;
final backendMessage = qrResult["message"] ?? "Unknown error occurred";
if (isValid) {
if (mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: widget.requestId)),
);
}
} else {
if (mounted) {
// Show exact backend error message instead of our custom message
_showErrorDialog(context, backendMessage);
}
}
} catch (e) {
if (mounted) Navigator.pop(context);
if (mounted) {
// Extract actual error message from the exception or use generic fallback
String errorMessage = _extractErrorMessage(e) ?? 'Failed to validate QR code. Please try again.';
_showErrorDialog(context, errorMessage);
}
}
}
} else {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: widget.requestId)),
);
}
},
),
],
),
).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 (_recurrentWoData?.status?.name != null)
StatusLabel(
label: _recurrentWoData?.status?.name!,
textColor: AppColor.getRequestStatusTextColorByName(context, _recurrentWoData?.status?.name!),
backgroundColor: AppColor.getRequestStatusColorByName(context, _recurrentWoData?.status?.name!),
),
8.width,
// StatusLabel(
// label: taskModel?.taskJobStatus?.name ?? widget.requestDetails?.status!,
// textColor: AppColor.getRequestStatusTextColorByName(context, taskModel?.taskJobStatus?.name ?? widget.requestDetails?.status!),
// backgroundColor: AppColor.getRequestStatusColorByName(context, taskModel?.taskJobStatus?.name ?? widget.requestDetails?.status!),
// ),
if (taskResult.createdDate != null)
Text(
taskResult.createdDate!.toServiceRequestCardFormat,
textAlign: TextAlign.end,
style: AppTextStyles.tinyFont.copyWith(color: context.isDark ? AppColor.neutral30 : AppColor.neutral50),
),
],
),
8.height,
if (_recurrentWoData?.title?.isNotEmpty ?? false)
Text(
_recurrentWoData?.title ?? "",
style: AppTextStyles.heading4.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
),
),
if (taskResult.taskNo != null && taskResult.taskNo!.isNotEmpty)
Text(
'Task No: ${taskResult.taskNo!}',
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);
}
void _showErrorDialog(BuildContext context, String message) {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: message,
title: 'Error',
onTap: () => Navigator.pop(context),
),
);
}
String? _extractErrorMessage(dynamic error) {
// Implement your logic to extract error message from the exception
// For example, if the error is a custom exception with a message property:
if (error is ExceptionWithMessage) {
return error.message;
} else if (error is String) {
// If the error is a string, return it directly
return error;
} else if (error is Map<String, dynamic>) {
// If the error is a Map (e.g., parsed JSON), extract the message field
return error['message'] ?? error.toString();
}
// Fallback to a generic error message
return 'An unexpected error occurred. Please try again.';
}
/// Custom method to get recurrent work order with proper error message extraction
Future<Map<String, dynamic>> _getRecurrentWoByIdWithMessage(int id) async {
try {
final response = await ApiManager.instance.get(
URLs.getRecurrentPlanByIdUrl + "?planRecurrentTaskId=$id",
enableToastMessage: false, // Disable automatic toast
);
final responseData = json.decode(response.body);
if (response.statusCode >= 200 && response.statusCode < 300) {
// Success response
final recurrentWoData = RecurrentWoData.fromJson(responseData["data"]);
// Apply the same processing as in AllRequestsProvider
recurrentWoData.planRecurrentMedicalTaskRooms?.forEach((element) {
element.planRecurrentMedicalTaskRoomTabs?.forEach((tabs) {
tabs.planRecurrentMedicalTaskRoomTabAttributes?.forEach((attribute) {
if (attribute.attribute?.type == "bool" && attribute.attributeValue == null) {
attribute.attributeValue = "true";
}
});
});
});
return {
"success": true,
"data": recurrentWoData,
"message": "Data loaded successfully",
};
} else {
// Error response - extract the exact error message from backend
return {
"success": false,
"data": null,
"message": responseData["message"] ?? "Failed to load request details",
};
}
} catch (error) {
debugPrint('Error in _getRecurrentWoByIdWithMessage: $error');
return {
"success": false,
"data": null,
"message": "Failed to load request details. Please check your connection and try again.",
};
}
}
}
class ExceptionWithMessage implements Exception {
final String message;
ExceptionWithMessage(this.message);
}

@ -0,0 +1,359 @@
import 'package:flutter/material.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/modules/cleaner_module/models/task_type.dart';
import 'package:test_sa/modules/cleaner_module/models/qr_scan_location_data.dart';
import 'package:test_sa/modules/cleaner_module/models/one_time_task_data.dart';
import 'package:test_sa/modules/cleaner_module/provider/cleaner_provider.dart';
import 'package:test_sa/modules/cleaner_module/provider/task_type_provider.dart';
import 'package:test_sa/modules/cleaner_module/pages/one_time_task_view.dart';
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/views/widgets/dialogs/confirm_dialog.dart';
import 'package:test_sa/views/widgets/qr/scan_qr.dart';
class CleanerSearchFormView extends StatefulWidget {
static const String routeName = '/cleaner_search';
const CleanerSearchFormView({Key? key}) : super(key: key);
@override
State<CleanerSearchFormView> createState() => _CleanerSearchFormViewState();
}
class _CleanerSearchFormViewState extends State<CleanerSearchFormView> {
TaskType? selectedTaskType;
List<TaskType> _taskTypeOptions = [];
bool _isLoadingTaskTypes = false;
bool _isScanning = false;
bool _isProceeding = false;
QrScanLocationData? _scannedLocationData;
@override
void initState() {
super.initState();
_loadTaskTypes();
}
@override
void dispose() {
super.dispose();
}
Future<void> _loadTaskTypes() async {
setState(() {
_isLoadingTaskTypes = true;
});
try {
final taskTypes = await TaskTypeProvider().getTaskTypesList();
setState(() {
_taskTypeOptions = taskTypes;
_isLoadingTaskTypes = false;
});
} catch (e) {
setState(() {
_isLoadingTaskTypes = false;
_taskTypeOptions = [];
});
_showErrorDialog(context, 'Failed to load task types. Please try again.');
}
}
Future<void> _onScanQRCode() async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const ScanQr()),
);
if (result != null && result is String && result.isNotEmpty) {
setState(() {
_isScanning = true;
});
try {
final qrResult = await CleanerProvider().scanQRCode(qrCode: result);
final isSuccess = qrResult["success"] ?? false;
final backendMessage = qrResult["message"] ?? "Unknown error occurred";
if (isSuccess && qrResult["data"] != null) {
// Store the scanned location data
setState(() {
_scannedLocationData = QrScanLocationData.fromJson(qrResult["data"]);
_isScanning = false;
});
} else {
setState(() {
_isScanning = false;
});
if (mounted) {
_showErrorDialog(context, backendMessage);
}
}
} catch (e) {
setState(() {
_isScanning = false;
});
if (mounted) {
_showErrorDialog(context, 'Failed to scan QR code. Please try again.');
}
}
}
}
Future<void> _onProceed() async {
// Validate that task type and location data are available
if (selectedTaskType == null) {
_showErrorDialog(context, 'Please select a task type');
return;
}
if (_scannedLocationData == null) {
_showErrorDialog(context, 'Please scan a QR code first');
return;
}
setState(() {
_isProceeding = true;
});
try {
final result = await CleanerProvider().proceedOneTimeTask(
oneTimeSettingId: selectedTaskType!.id ?? 0,
siteId: _scannedLocationData!.siteId ?? 0,
buildingId: _scannedLocationData!.buildingId ?? 0,
floorId: _scannedLocationData!.floorId ?? 0,
departmentId: _scannedLocationData!.departmentId ?? 0,
roomId: _scannedLocationData!.roomId ?? 0,
);
final isSuccess = result["success"] ?? false;
final data = result["data"];
final message = result["message"] ?? "Unknown error occurred";
if (isSuccess && data != null) {
// Get the task ID from the response
final taskId = data['id'];
if (mounted && taskId != null) {
// Convert the data to OneTimeTaskData and navigate with the data
final taskData = OneTimeTaskData.fromJson(data);
// Navigate to OneTimeTaskView with the data
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => OneTimeTaskView(
taskId: taskId,
initialData: taskData,
),
),
);
}
} else {
if (mounted) {
_showErrorDialog(context, message);
}
}
} catch (e) {
if (mounted) {
_showErrorDialog(context, 'Failed to proceed. Please try again.');
}
} finally {
setState(() {
_isProceeding = false;
});
}
}
@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: [
// Task Type Dropdown
SingleItemDropDownMenu<TaskType, NullableLoadingProvider>(
context: context,
title: 'Task Type',
initialValue: selectedTaskType,
showShadow: false,
showAsBottomSheet: true,
backgroundColor: AppColor.fieldBgColor(context),
enabled: _taskTypeOptions.isNotEmpty,
loading: _isLoadingTaskTypes,
staticData: _taskTypeOptions,
onSelect: (value) {
setState(() {
selectedTaskType = value;
});
},
),
const SizedBox(height: 18),
// Scan QR Code Button
GestureDetector(
onTap: selectedTaskType != null ? _onScanQRCode : null,
child: Container(
height: 50,
alignment: Alignment.center,
decoration: BoxDecoration(
color: selectedTaskType != null ? Colors.white : Colors.grey[300],
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: selectedTaskType != null ? AppColor.blueStatus(context) : Colors.grey,
width: 2,
),
),
padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth, vertical: 8.toScreenHeight),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
"scan_asset".toSvgAsset(
height: 22,
fit: BoxFit.fitHeight,
color: selectedTaskType != null
? (context.isDark ? AppColor.neutral20 : AppColor.neutral120)
: Colors.grey,
),
8.width,
//we can change the implementation when merge with uiux branch
_scannedLocationData==null? "Scan QR Code".bodyText(context).custom(
color: selectedTaskType != null
? (context.isDark ? AppColor.neutral20 : AppColor.black20)
: Colors.grey,
):'Re Scan QR Code'.bodyText(context).custom(
color: selectedTaskType != null
? (context.isDark ? AppColor.neutral20 : AppColor.black20)
: Colors.grey,
),
],
),
),
),
const SizedBox(height: 18),
// Loading Indicator
if (_isScanning)
const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(),
),
),
// Scanned Location Data Card
if (_scannedLocationData != null && !_isScanning) _buildLocationDataCard(context),
],
).toShadowContainer(context).paddingOnly(top: 20, start: 16, end: 16),
),
),
// Footer Action Button
FooterActionButton.footerContainer(
context: context,
child: AppFilledButton(
buttonColor:_scannedLocationData != null && !_isProceeding? AppColor.primary10: Colors.grey,
label: 'Proceed',
maxWidth: true,
onPressed: (_scannedLocationData != null && !_isProceeding) ? _onProceed : null,
loading: _isProceeding,
),
),
],
),
);
}
Widget _buildLocationDataCard(BuildContext context) {
final locationData = _scannedLocationData;
if (locationData == null) {
return const SizedBox();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (locationData.siteName != null && locationData.siteName!.isNotEmpty)
Text(
'Site: ${locationData.siteName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.buildingName != null && locationData.buildingName!.isNotEmpty)
Text(
'Building: ${locationData.buildingName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.floorName != null && locationData.floorName!.isNotEmpty)
Text(
'Floor: ${locationData.floorName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.departmentName != null && locationData.departmentName!.isNotEmpty)
Text(
'Department: ${locationData.departmentName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.roomName != null && locationData.roomName!.isNotEmpty)
Text(
'Room: ${locationData.roomName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.locationDescription != null && locationData.locationDescription!.isNotEmpty)
Text(
'Location Description: ${locationData.locationDescription}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
softWrap: true,
maxLines: null,
),
],
).paddingOnly(start: 16, end: 16, top: 16, bottom: 16),
],
).toShadowContainer(context, padding: 0);
}
void _showErrorDialog(BuildContext context, String message) {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: message,
title: 'Error',
onTap: () => Navigator.pop(context),
),
);
}
}

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

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

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

@ -0,0 +1,74 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:http/http.dart';
import 'package:test_sa/controllers/api_routes/api_manager.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/modules/cleaner_module/models/task_type.dart';
class TaskTypeProvider extends ChangeNotifier {
//reset provider data
void reset() {
_taskTypes = null;
_stateCode = null;
}
// state code of current request to define error message
int? _stateCode;
int? get stateCode => _stateCode;
// contain task types data
List<TaskType>? _taskTypes;
List<TaskType>? get taskTypes => _taskTypes;
// loading state
bool _loading = false;
bool get isLoading => _loading;
set isLoading(bool isLoading) {
_loading = isLoading;
notifyListeners();
}
/// Get task types
Future<int> getTaskTypes() async {
if (_loading == true) return -2;
_loading = true;
notifyListeners();
Response response;
try {
response = await ApiManager.instance.get(URLs.getTaskType);
_stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) {
// client's request was successfully received
List taskTypesListJson = json.decode(response.body)["data"];
_taskTypes = taskTypesListJson.map((taskType) => TaskType.fromJson(taskType)).toList();
}
_loading = false;
notifyListeners();
return response.statusCode;
} catch (error) {
_loading = false;
_stateCode = -1;
notifyListeners();
return -1;
}
}
/// Get task types list directly without provider state
Future<List<TaskType>> getTaskTypesList() async {
Response response;
try {
response = await ApiManager.instance.get(URLs.getTaskType);
if (response.statusCode >= 200 && response.statusCode < 300) {
List taskTypesListJson = json.decode(response.body)["data"];
return taskTypesListJson.map((taskType) => TaskType.fromJson(taskType)).toList();
}
return [];
} catch (error) {
return [];
}
}
}

@ -0,0 +1,405 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/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';
import 'package:test_sa/views/widgets/buttons/app_back_button.dart';
import 'package:test_sa/modules/cleaner_module/widgets/multi_image_picker.dart';
import 'package:test_sa/views/widgets/images/files_list.dart';
class CleanerChecklistWidget extends StatefulWidget {
final List<ChecklistItem> checklistItems;
final Function(List<ChecklistItem>)? onChecklistChanged;
final List<String> capturedImages;
final Function(List<String>)? onImagesChanged;
final bool isClosed;
const CleanerChecklistWidget({
Key? key,
required this.checklistItems,
this.onChecklistChanged,
required this.capturedImages,
this.onImagesChanged,
this.isClosed = false
}) : 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,
ListView.separated(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (cxt, index) => _buildChecklistItem(context, widget.checklistItems[index]),
separatorBuilder: (cxt, index) => const Divider(height: 1, color: AppColor.neutral30, thickness: 1).paddingOnly(top: 16, bottom: 16),
itemCount: widget.checklistItems.length)
.toShadowContainer(context, padding: 16),
16.height,
_buildImageCaptureWidget(context),
],
);
}
Widget _buildChecklistItem(BuildContext context, ChecklistItem item) {
return IgnorePointer(
ignoring: widget.isClosed,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
AbsorbPointer(
absorbing: true,
child: Transform.scale(
scale: 1.2,
child: Checkbox(
value: item.isChecked,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: BorderSide(color: Colors.black.withValues(alpha: .05)),
visualDensity: const VisualDensity(horizontal: -4.0, vertical: -4.0),
activeColor: AppColor.primary10,
onChanged: (bool? value) {},
),
),
),
10.width,
RichText(
text: TextSpan(
children: [
TextSpan(
text: item.title,
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
fontWeight: FontWeight.w500,
),
),
if (item.isMandatory)
TextSpan(
text: ' *',
style: AppTextStyles.bodyText.copyWith(
color: Colors.red,
fontWeight: FontWeight.w500,
),
),
],
),
).expanded,
],
).onPress(() {
setState(() {
item.isChecked = !item.isChecked;
if (!item.isChecked) {
item.commentController.clear();
}
});
widget.onChecklistChanged?.call(widget.checklistItems);
}),
/* ListTileTheme(
horizontalTitleGap: 0,
contentPadding: EdgeInsets.zero,
minVerticalPadding: 0,
child: Container(
color: Colors.amberAccent,
child: CheckboxListTile(
value: item.isChecked,
minVerticalPadding: 0,
controlAffinity: ListTileControlAffinity.leading,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
contentPadding: EdgeInsets.zero,
checkboxScaleFactor: 1.2,
side: BorderSide(color: Colors.black.withValues(alpha: .05)),
visualDensity: const VisualDensity(horizontal: -0.0, vertical: -4.0),
activeColor: AppColor.primary10,
title: RichText(
text: TextSpan(
children: [
TextSpan(
text: item.title,
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
fontWeight: FontWeight.w500,
),
),
if (item.isMandatory)
TextSpan(
text: ' *',
style: AppTextStyles.bodyText.copyWith(
color: Colors.red,
fontWeight: FontWeight.w500,
),
),
],
),
),
onChanged: (bool? value) {
setState(() {
item.isChecked = value ?? false;
if (!item.isChecked) {
item.commentController.clear();
}
});
widget.onChecklistChanged?.call(widget.checklistItems);
},
),
),
),*/
if (item.isChecked) ...[
12.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;
widget.onChecklistChanged?.call(widget.checklistItems);
},
),
],
],
),
);
}
Widget _buildImageCaptureWidget(BuildContext context) {
// Base64 image display and capture widget
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Captured Image'.addTranslation,
style: AppTextStyles.heading6.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
),
),
// Text(
// '(${widget.capturedImages.length}/$maxImages)',
// style: AppTextStyles.bodyText.copyWith(
// color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
// fontWeight: FontWeight.w600,
// ),
// ),
],
),
8.height,
_buildImageWidget(context),
],
).toShadowContainer(context);
}
static const int maxImages = 2;
/// Build the image widget with loading indicator for network images
Widget _buildImageWidget(BuildContext context) {
if (widget.capturedImages.isEmpty) {
// Show add image button when no image
return GestureDetector(
onTap: widget.isClosed ? null : () async {
// Only allow camera - pick one image at a time
final pickedFile = await ImagePicker().pickImage(
source: ImageSource.camera,
imageQuality: 70,
maxWidth: 800,
maxHeight: 800,
);
if (pickedFile != null) {
final bytes = await pickedFile.readAsBytes();
final base64Image = base64Encode(bytes);
final fileName = pickedFile.path.split("/").last;
final formattedImage = "$fileName|$base64Image";
// Add to existing images (max 2)
final updatedImages = List<String>.from(widget.capturedImages);
updatedImages.add(formattedImage);
widget.onImagesChanged?.call(updatedImages);
}
},
child: Container(
height: 120,
width: 120,
decoration: BoxDecoration(
color: AppColor.fieldBgColor(context),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: context.isDark ? AppColor.neutral20 : AppColor.neutral30,
width: 2,
style: BorderStyle.solid,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.add_photo_alternate,
size: 32,
color: context.isDark ? AppColor.white10 : AppColor.black10,
),
4.height,
Text(
'Add Image',
style: AppTextStyles.tinyFont.copyWith(
color: context.isDark ? AppColor.white10 : AppColor.black10,
),
),
],
),
),
);
}
// Check if images are URLs from API (like ServiceRequestDetailView does)
final bool hasApiImages = widget.capturedImages.any((img) =>
img.startsWith('http://') || img.startsWith('https://'));
// If images are from API (URLs), use FilesList widget like ServiceRequestDetailView
if (hasApiImages && widget.isClosed) {
return FilesList(
images: widget.capturedImages,
showAsListView: false,
);
}
// Otherwise, use MultiImagePicker for base64 images or when editing
return MultiImagePicker(
imageUrls: widget.capturedImages,
enabled: !widget.isClosed,
maxImages: 2, // Enforce max 2 images
onImageTap: (index, imageUrl) {
// Handle image tap - show in full screen
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: Stack(
children: [
InteractiveViewer(
child: _buildImageForViewing(imageUrl),
).center,
const ABackButton(),
],
),
),
),
),
);
},
onDelete: (index) {
// Handle image delete
if (index >= 0) {
setState(() {
widget.capturedImages.removeAt(index);
});
widget.onImagesChanged?.call(widget.capturedImages);
}
},
onAdd: (imageUrl) {
// Handle new image addition
setState(() {
widget.capturedImages.add(imageUrl);
});
widget.onImagesChanged?.call(widget.capturedImages);
},
);
}
/// Build image widget for viewing based on source type
Widget _buildImageForViewing(String imageUrl) {
try {
// Check if it's a URL from API
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
return Image.network(
imageUrl,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.broken_image, size: 48, color: Colors.white);
},
);
}
// Otherwise, it's base64 encoded
final parts = imageUrl.split('|');
final base64Data = parts.length > 1 ? parts[1] : imageUrl;
return Image.memory(
base64Decode(base64Data),
fit: BoxFit.contain,
);
} catch (e) {
return const Icon(Icons.broken_image, size: 48, color: Colors.white);
}
}
@override
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);
// 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();
}
}

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

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

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

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

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

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/controllers/api_routes/api_manager.dart';
import 'package:test_sa/extensions/context_extension.dart';
@ -11,9 +10,10 @@ 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 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart';
import '../../../../../../controllers/providers/api/all_requests_provider.dart';
import 'components/room_tabs_widget.dart';
@ -37,7 +37,6 @@ class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> {
@override
void initState() {
allRequestsProvider = Provider.of<AllRequestsProvider>(context, listen: false);
// calculateWorkingTime();
Provider.of<AllRequestsProvider>(context, listen: false).getRecurrentWoById(id: widget.taskId!);
super.initState();
}
@ -47,148 +46,238 @@ class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> {
super.dispose();
}
bool isClosed = false;
@override
Widget build(BuildContext context) {
return Consumer<AllRequestsProvider>(builder: (context, requestProvider, child) {
isClosed = requestProvider.recurrentWoData?.status?.value == 1;
return Scaffold(
appBar: DefaultAppBar(
title: context.translation.recurrentWo,
onWillPopScope: () {
_updateTask(context: context, status: 0);
},
onWillPopScope: isClosed
? null
: () {
_updateTask(context: context, status: 0);
},
),
body: allRequestsProvider!.isLoading
? const ALoading()
: requestProvider.recurrentWoData != null
? Column(
children: [
SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RecurrentTaskInfoWidget(model: requestProvider.recurrentWoData),
requestProvider.recurrentWoData!.planRecurrentMedicalTaskRooms != null && requestProvider.recurrentWoData!.planRecurrentMedicalTaskRooms!.isNotEmpty
? Column(
children: [
8.height,
RoomTabsWidget(model: requestProvider.recurrentWoData),
],
)
: const SizedBox(),
],
),
).expanded,
if (requestProvider.recurrentWoData?.status?.value != 1) ...[
FooterActionButton.footerContainer(
context: context,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
? SafeArea(
child: Column(
children: [
SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppFilledButton(
label: context.translation.save,
buttonColor: context.isDark ? AppColor.neutral20 : AppColor.white60,
textColor: context.isDark ? Colors.white : AppColor.black10,
onPressed: () => _updateTask(context: context, status: 0),
).expanded,
12.width,
AppFilledButton(
label: context.translation.complete,
buttonColor: AppColor.primary10,
// textColor: context.isDark?Colors.white: AppColor.black10,
onPressed: () => _updateTask(context: context, status: 1),
).expanded,
RecurrentTaskInfoWidget(
isClosed: isClosed,
model: requestProvider.recurrentWoData,
isHouseKeeper: false,
timerStartTime: null,
),
IgnorePointer(
ignoring: isClosed,
child: requestProvider.recurrentWoData!.planRecurrentMedicalTaskRooms != null && requestProvider.recurrentWoData!.planRecurrentMedicalTaskRooms!.isNotEmpty
? Column(
children: [
8.height,
RoomTabsWidget(model: requestProvider.recurrentWoData),
],
)
: const SizedBox(),
),
],
),
),
]
],
).expanded,
if (requestProvider.recurrentWoData?.status?.value != 1) ...[
FooterActionButton.footerContainer(
context: context,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
AppFilledButton(
label: context.translation.save,
buttonColor: context.isDark ? AppColor.neutral20 : AppColor.white60,
textColor: context.isDark ? Colors.white : AppColor.black10,
onPressed: () => _updateTask(context: context, status: 0),
).expanded,
12.width,
AppFilledButton(
label: context.translation.complete,
buttonColor: AppColor.primary10,
onPressed: () => _updateTask(context: context, status: 1),
).expanded,
],
),
),
]
],
),
)
: NoDataFound(message: context.translation.noDataFound).center,
).handlePopScope(
cxt: context,
showPopUp: !isClosed,
onSave: () {
_updateTask(context: context, status: 0);
});
});
}
}
void _updateTask({required BuildContext context, required int status}) async {
AllRequestsProvider allRequestsProvider = Provider.of<AllRequestsProvider>(context, listen: false);
if (validate(model: allRequestsProvider.recurrentWoData!)) {
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
workingHours: ((durationInSecond) / 60 / 60),
),
);
}
allRequestsProvider.recurrentWoData?.timerModelList?.forEach((timer) {
int durationInSecond = timer.endAt!.difference(timer.startAt!).inSeconds;
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add(
PlanRecurrentTaskTimers(
id: 0,
startTime: timer.startAt!.toIso8601String(), // Handle potential null
endTime: timer.endAt?.toIso8601String(), // Handle potential null
workingHours: ((durationInSecond) / 60 / 60),
),
);
});
void _updateTask({required BuildContext context, required int status}) async {
// Proceed with update
_performUpdate(context: context, status: status);
}
await allRequestsProvider
.updateRecurrentWo(
status: status,
)
.then((success) {
Navigator.pop(context);
if (success) {
if (status == 1) {
// when click complete then this request remove from the list and status changes to closed..
allRequestsProvider.reset();
allRequestsProvider.getAllRequests(context, typeTransaction: 5);
}
allRequestsProvider.recurrentWoData?.recurrentWoTimerModel = null;
Navigator.pop(context);
void _performUpdate({required BuildContext context, required int status}) async {
AllRequestsProvider allRequestsProvider = Provider.of<AllRequestsProvider>(context, listen: false);
if (validate(context: context, model: allRequestsProvider.recurrentWoData!, status: status)) {
// Use existing timer logic
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers ?? [];
if (allRequestsProvider.recurrentWoData?.recurrentWoTimePicker != null) {
int durationInSecond = allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt!.difference(allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.startAt!).inSeconds;
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add(
PlanRecurrentTaskTimers(
id: 0,
startTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.startAt!.toIso8601String(),
endTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt?.toIso8601String(),
workingHours: ((durationInSecond) / 60 / 60),
),
);
}
});
allRequestsProvider.recurrentWoData?.timerModelList?.forEach((timer) {
int durationInSecond = timer.endAt!.difference(timer.startAt!).inSeconds;
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add(
PlanRecurrentTaskTimers(
id: 0,
startTime: timer.startAt!.toIso8601String(),
endTime: timer.endAt?.toIso8601String(),
workingHours: ((durationInSecond) / 60 / 60),
),
);
});
final uniqueTimersMap = <String, PlanRecurrentTaskTimers>{};
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.forEach((timer) {
if (timer.startTime != null && timer.endTime != null) {
final key = '${timer.id}_${timer.startTime}_${timer.endTime}';
uniqueTimersMap[key] = timer;
}
});
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = uniqueTimersMap.values.toList();
showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading());
await allRequestsProvider.updateRecurrentWo(status: status, showToast: false).then((result) {
Navigator.pop(context); // Close loading dialog
bool success = result['success'] ?? false;
String message = result['message'] ?? '';
if (success) {
if (status == 1) {
// When complete is clicked, navigate to LandPage (Dashboard)
allRequestsProvider.reset();
allRequestsProvider.getAllRequests(context, typeTransaction: 5);
allRequestsProvider.recurrentWoData?.recurrentWoTimerModel = null;
// Navigate to LandPage and clear all previous routes
Navigator.of(context).pushNamedAndRemoveUntil(LandPage.routeName, (routes) => false);
} else {
// For save (status 0), just pop the current screen
allRequestsProvider.recurrentWoData?.recurrentWoTimerModel = null;
Navigator.pop(context);
}
} else {
// Show error dialog with the exact message from API
_showErrorDialog(context, message.isNotEmpty ? message : "Failed to update the task. Please try again.");
}
}).catchError((error) {
Navigator.pop(context); // Close loading dialog
_showErrorDialog(context, error.toString());
});
}
}
void _showErrorDialog(BuildContext context, String message) {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: message,
title: 'Error',
onTap: () => Navigator.pop(context),
),
);
}
}
bool validate({required RecurrentWoData model}) {
bool validate({required BuildContext context, required RecurrentWoData model, required int status}) {
// Timer validation logic
bool isTimerPickerEnable = ApiManager.instance.assetGroup?.enabledEngineerTimer ?? false;
if (isTimerPickerEnable) {
if (model.recurrentWoTimerModel?.startAt == null && model.recurrentWoTimePicker == null) {
Fluttertoast.showToast(msg: "Working Hours Required");
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: "Working Hours Required",
title: 'Validation Error',
onTap: () => Navigator.pop(context),
),
);
return false;
}
if (model.recurrentWoTimePicker == null) {
if (model.recurrentWoTimerModel?.startAt == null) {
Fluttertoast.showToast(msg: "Working Hours Required");
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: "Working Hours Required",
title: 'Validation Error',
onTap: () => Navigator.pop(context),
),
);
return false;
}
if (model.recurrentWoTimerModel?.endAt == null) {
Fluttertoast.showToast(msg: "Please Stop The Timer");
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: "Please Stop The Timer",
title: 'Validation Error',
onTap: () => Navigator.pop(context),
),
);
return false;
}
}
} else {
if (model.recurrentWoTimerModel?.startAt == null) {
Fluttertoast.showToast(msg: "Working Hours Required");
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: "Working Hours Required",
title: 'Validation Error',
onTap: () => Navigator.pop(context),
),
);
return false;
}
if (model.recurrentWoTimerModel?.endAt == null) {
Fluttertoast.showToast(msg: "Please Stop The Timer");
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: "Please Stop The Timer",
title: 'Validation Error',
onTap: () => Navigator.pop(context),
),
);
return false;
}
}

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

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

@ -118,8 +118,8 @@ class CreateRequestModel {
if (context.userProvider.isQualityUser && context.settingProvider.isUserFlowMedical) {
list.add(CreateRequestModel("Equipment Internal Audit Checklist".addTranslation, "add_icon", CreateEquipmentInternalAuditForm.id));
list.add(CreateRequestModel("System Internal Audit Checklist".addTranslation, "add_icon", CreateSystemInternalAuditForm.id));
} else if (context.userProvider.isAssessor && context.settingProvider.isUserFlowMedical) {
list.add(CreateRequestModel("TRAF".addTranslation, "add_icon", CreateTRAFRequestPage.id));
} else if (context.userProvider.isAssessor && context.settingProvider.isUserFlowMedical) { // todo: handled in permission api, so no need to check here
// list.add(CreateRequestModel("TRAF".addTranslation, "add_icon", CreateTRAFRequestPage.id));
} else if (context.userProvider.isEngineer) {
if (Provider.of<SettingProvider>(context, listen: false).engineerCanCreateCM) {
list.add(CreateRequestModel(context.translation.correctiveMaintenance, "add_icon", CreateCMRequest.id));

@ -129,8 +129,11 @@ class _AllRequestsFilterPageState extends State<AllRequestsFilterPage> {
if (context.settingProvider.isUserFlowMedical && !context.userProvider.isNurse) {
types['Incidents'] = 16;
}
final statuses = {
//No need to add check for assetgroup type as this role will only for FMS
if (context.userProvider.isHouseKeeper) {
types = {context.translation.recurrentWo: 5};
}
var statuses = {
"All WO": 0,
context.translation.open: 1,
context.translation.inProgress: 2,
@ -138,6 +141,16 @@ class _AllRequestsFilterPageState extends State<AllRequestsFilterPage> {
context.translation.closed: 3,
"Canceled": 5,
};
if (context.userProvider.isHouseKeeper) {
statuses = {
"All WO": 0,
// context.translation.open: 1,
context.translation.inProgress: 2,
// context.translation.completed: 4,
context.translation.closed: 3,
// "Canceled": 5,
};
}
return Scaffold(
appBar: DefaultAppBar(
title: context.translation.filter,

@ -109,6 +109,14 @@ class _MyRequestsPageState extends State<MyRequestsPage> {
requestsList.add(Request(16, "Incidents"));
}
if (context.userProvider.isHouseKeeper) {
requestsList = [
Request(null, context.translation.allWorkOrder),
Request(5, context.translation.recurrentWo),
// Request(15, 'Demo'), moved to permission api
];
}
_provider = Provider.of<AllRequestsProvider>(context, listen: false);
_provider!.reset();
WidgetsBinding.instance.addPostFrameCallback((_) {

@ -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,7 @@ class RecurrentWoItemView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final user = Provider.of<UserProvider>(context, listen: false).user;
if (requestData != null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -47,12 +53,17 @@ class RecurrentWoItemView extends StatelessWidget {
],
),
8.height,
(requestData?.typeTransaction ?? context.translation.ppmRequest).heading5(context),
(requestData?.typeTransaction ?? context.translation.recurrentWo).heading5(context),
8.height,
// '${context.translation.assetNumber}: ${requestData!.assetNumber}'.bodyText(context),
// '${context.translation.assetSN}: ${request.assetSN}'.bodyText(context),//todo ask ahmed
// '${context.translation.code}: ${request.code}'.bodyText(context),
'${context.translation.taskNo}: ${requestData!.requestNo}'.bodyText(context),
if (requestData?.siteName != null) '${context.translation.site}: ${requestData!.siteName}'.bodyText(context),
if (requestData!.buildingName != null) '${context.translation.building}: ${requestData!.buildingName}'.bodyText(context),
if (requestData!.departmentName != null) '${context.translation.department}: ${requestData!.departmentName}'.bodyText(context),
if (requestData!.floorName != null) '${context.translation.floor}: ${requestData!.floorName}'.bodyText(context),
if (requestData!.roomName != null) '${context.translation.room}: ${requestData!.roomName}'.bodyText(context),
8.height,
Row(
mainAxisSize: MainAxisSize.min,
@ -103,6 +114,11 @@ class RecurrentWoItemView extends StatelessWidget {
'${context.translation.site}: ${requestDetails!.site}'.bodyText(context),
// '${context.translation.code}: ${request.code}'.bodyText(context),
'${context.translation.taskNo}: ${requestDetails!.requestNo}'.bodyText(context),
if (requestDetails!.siteName != null) '${context.translation.site}: ${requestDetails!.siteName}'.bodyText(context),
if (requestDetails!.buildingName != null) '${context.translation.building}: ${requestDetails!.buildingName}'.bodyText(context),
if (requestDetails!.departmentName != null) '${context.translation.department}: ${requestDetails!.departmentName}'.bodyText(context),
if (requestDetails!.floorName != null) '${context.translation.floor}: ${requestDetails!.floorName}'.bodyText(context),
if (requestDetails!.roomName != null) '${context.translation.room}: ${requestDetails!.roomName}'.bodyText(context),
8.height,
Row(
mainAxisSize: MainAxisSize.min,

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

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

@ -7,6 +7,7 @@ import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/all_requests_and_count_model.dart';
import 'package:test_sa/modules/asset_delivery_module/pages/asset_delivery_item_view.dart';
import 'package:test_sa/modules/cleaner_module/widgets/one_time_task_item_view.dart';
import 'package:test_sa/modules/demo_module/demo_request_item_view.dart';
import 'package:test_sa/modules/incident_module/incident_item_view.dart';
import 'package:test_sa/modules/internal_audit_module/pages/equipment_internal_audit/equipment_internal_audit_item_view.dart';
@ -76,6 +77,8 @@ class RequestItemViewList extends StatelessWidget {
return DemoRequestItemView(requestDetails: list[index]);
case 16:
return IncidentItemView(requestDetails: list[index]);
case 17:
return OneTimeTaskItemView(requestDetails: list[index]);
default:
Container(
height: 100,

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

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

Loading…
Cancel
Save