diff --git a/lib/controllers/providers/api/all_requests_provider.dart b/lib/controllers/providers/api/all_requests_provider.dart index 435af277..c3d244d3 100644 --- a/lib/controllers/providers/api/all_requests_provider.dart +++ b/lib/controllers/providers/api/all_requests_provider.dart @@ -176,6 +176,10 @@ class AllRequestsProvider extends ChangeNotifier { // list = [16]; list.add(16); } + if ( context.userProvider.isHouseKeeper) { + list = [5]; + } + return list; } @@ -272,22 +276,27 @@ class AllRequestsProvider extends ChangeNotifier { } } - Future updateRecurrentWo({required int status}) async { + Future> updateRecurrentWo({required int status, bool showToast = true}) async { Response response; try { - response = await ApiManager.instance.post(URLs.updateRecurrentPlanUrl, body: recurrentWoData!.toJson(status: status)); + response = await ApiManager.instance.post(URLs.updateRecurrentPlanUrl, body: recurrentWoData!.toJson(status: status), showToast: showToast); stateCode = response.statusCode; - // isLoading = false; - // notifyListeners(); + if (stateCode == 200) { - return true; + return {'success': true, 'message': ''}; + } else { + // Parse error message from response + try { + final responseBody = jsonDecode(response.body); + String errorMessage = responseBody['message'] ?? responseBody['title'] ?? 'Failed to update the task'; + return {'success': false, 'message': errorMessage}; + } catch (e) { + return {'success': false, 'message': 'Failed to update the task. Status code: $stateCode'}; + } } - return false; } catch (error) { - // isLoading = false; stateCode = -1; - // notifyListeners(); - return false; + return {'success': false, 'message': error.toString()}; } } diff --git a/lib/controllers/providers/api/user_provider.dart b/lib/controllers/providers/api/user_provider.dart index ec6f3242..846282cf 100644 --- a/lib/controllers/providers/api/user_provider.dart +++ b/lib/controllers/providers/api/user_provider.dart @@ -51,6 +51,7 @@ class UserProvider extends ChangeNotifier { bool get isQualityUser => user!.type == UsersTypes.qualityUser; bool get isCommissioningEngineer => user!.type == UsersTypes.commissioningEngineer; + bool get isHouseKeeper => user!.type == UsersTypes.houseKeeper; VerifyOtpModel _verifyOtpModel = VerifyOtpModel(); SwipeTransaction _swipeTransactionModel = SwipeTransaction(); diff --git a/lib/dashboard_latest/dashboard_provider.dart b/lib/dashboard_latest/dashboard_provider.dart index 03ca5104..92ecd60e 100644 --- a/lib/dashboard_latest/dashboard_provider.dart +++ b/lib/dashboard_latest/dashboard_provider.dart @@ -118,6 +118,7 @@ class DashBoardProvider extends ChangeNotifier { UsersTypes.qualityUser: URLs.qaAuditorCount, UsersTypes.commissioningEngineer: URLs.commissioningEngineerCount, UsersTypes.engineer: URLs.engineerDashboardCountUrl, + UsersTypes.houseKeeper: URLs.engineerDashboardCountUrl, UsersTypes.assessor: URLs.engineerDashboardCountUrl, UsersTypes.assessorTl: URLs.engineerDashboardCountUrl, }; @@ -257,6 +258,7 @@ class DashBoardProvider extends ChangeNotifier { case UsersTypes.engineer: case UsersTypes.assessor: case UsersTypes.assessorTl: + case UsersTypes.houseKeeper: if (status == 0) { return URLs.engineerDashboardUpcoming; } else if (status == 1 && tabId == 1) { @@ -413,6 +415,14 @@ class CategoryTabs { tabs.add(CategoryTabs(label: 'Completed', tag: 3, id: 4)); return tabs; } + if (userType == UsersTypes.houseKeeper) { + // tabs.add(CategoryTabs(label: 'Upcoming', tag: 0, id: 0)); + // tabs.add(CategoryTabs('Open', 1)); + tabs.add(CategoryTabs(label: 'UnAssigned', tag: 1, id: 1)); + tabs.add(CategoryTabs(label: 'In Progress', tag: 2, id: 2)); + tabs.add(CategoryTabs(label: 'Closed', tag: 4, id: 3)); + return tabs; + } if (userType == UsersTypes.qualityUser) { tabs.add(CategoryTabs(label: 'Open', tag: 1, id: 2)); diff --git a/lib/dashboard_latest/widgets/progress_fragment.dart b/lib/dashboard_latest/widgets/progress_fragment.dart index 4ab32aab..ed7443c2 100644 --- a/lib/dashboard_latest/widgets/progress_fragment.dart +++ b/lib/dashboard_latest/widgets/progress_fragment.dart @@ -183,12 +183,19 @@ class ProgressFragment extends StatelessWidget { ChartData('In Progress', snapshot.dashboardCount?.data?.countInprogress?.toDouble() ?? 0.0, AppColor.orange70), ]; } else if (_userProvider.isQualityUser) { - total = (snapshot.dashboardCount?.data?.countComplete ?? 0) + (snapshot.dashboardCount?.data?.countInprogress ?? 0) + (snapshot.dashboardCount?.data?.countOpen ?? 0); + total = (snapshot.dashboardCount?.data?.countClose ?? 0) + (snapshot.dashboardCount?.data?.countInprogress ?? 0) + (snapshot.dashboardCount?.data?.countOpen ?? 0); chartData = [ - ChartData('Closed', snapshot.dashboardCount?.data?.countComplete?.toDouble() ?? 0.0, AppColor.green70), + ChartData('Closed', snapshot.dashboardCount?.data?.countClose?.toDouble() ?? 0.0, AppColor.green70), ChartData('Open', snapshot.dashboardCount?.data?.countOpen?.toDouble() ?? 0.0, AppColor.blueStatus(context)), ChartData('In Progress', snapshot.dashboardCount?.data?.countInprogress?.toDouble() ?? 0.0, AppColor.orange70), ]; + } else if (_userProvider.isHouseKeeper) { + total = (snapshot.dashboardCount?.data?.countClose ?? 0) + (snapshot.dashboardCount?.data?.countInprogress ?? 0) + (snapshot.dashboardCount?.data?.countOpen ?? 0); + chartData = [ + ChartData('In Progress', snapshot.dashboardCount?.data?.countInprogress?.toDouble() ?? 0.0, AppColor.orange70), + ChartData('Closed', snapshot.dashboardCount?.data?.countClose?.toDouble() ?? 0.0, AppColor.green70), + // ChartData('Open', snapshot.dashboardCount?.data?.countOpen?.toDouble() ?? 0.0, AppColor.blueStatus(context)), + ]; } else { total = (snapshot.dashboardCount?.data?.countComplete ?? 0) + (snapshot.dashboardCount?.data?.countInprogress ?? 0) + (snapshot.dashboardCount?.data?.countOpen ?? 0); chartData = [ diff --git a/lib/dashboard_latest/widgets/request_category_fragment.dart b/lib/dashboard_latest/widgets/request_category_fragment.dart index 2b70da8f..54c7f1a8 100644 --- a/lib/dashboard_latest/widgets/request_category_fragment.dart +++ b/lib/dashboard_latest/widgets/request_category_fragment.dart @@ -24,8 +24,8 @@ class RequestCategoryFragment extends StatelessWidget { final user = Provider.of(context, listen: false).user; final settingProvider = Provider.of(context, listen: false); - // Check if user is FMS Engineer using SettingProvider - final bool isFMSEngineer = user?.type == UsersTypes.engineer && settingProvider.isUserFMS; + // Check if user is houseKeeper + final bool isHouseKeeper = user?.type == UsersTypes.houseKeeper; return Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ //TODO uncommit this to get the calendar card.. @@ -53,7 +53,7 @@ class RequestCategoryFragment extends StatelessWidget { : const SizedBox(), ], ).paddingOnly(start: 14, end: 12) - : isFMSEngineer + : isHouseKeeper ? Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/models/all_requests_and_count_model.dart b/lib/models/all_requests_and_count_model.dart index 09bfc03f..ba261802 100644 --- a/lib/models/all_requests_and_count_model.dart +++ b/lib/models/all_requests_and_count_model.dart @@ -134,36 +134,47 @@ class RequestsDetails { int? transactionType; int? numberOfAssets; int? numberOfSites; + String? siteName; + String? buildingName; + String? departmentName; + String? floorName; + String? roomName; - RequestsDetails( - {this.id, - this.nameOfType, - this.priority, - this.status, - this.statusValue, - this.assetName, - this.assetNo, - this.assetSN, - this.model, - this.supplier, - this.manufacturer, - this.requestType, - this.requestNo, - this.numberOfAssets, - this.numberOfSites, - this.sessionType, - this.gasType, - this.site, - this.statusReceiver, - this.assetTransferFrom, - this.assetTransferTo, - this.code, - this.siteTransferFrom, - this.siteTransferTo, - this.poInternal, - this.poExternal, - this.date, - this.transactionType}); + RequestsDetails({ + this.id, + this.nameOfType, + this.priority, + this.status, + this.statusValue, + this.assetName, + this.assetNo, + this.assetSN, + this.model, + this.supplier, + this.manufacturer, + this.requestType, + this.requestNo, + this.numberOfAssets, + this.numberOfSites, + this.sessionType, + this.gasType, + this.site, + this.statusReceiver, + this.assetTransferFrom, + this.assetTransferTo, + this.code, + this.siteTransferFrom, + this.siteTransferTo, + this.poInternal, + this.poExternal, + this.date, + this.transactionType, + this.siteName, + this.buildingName, + this.departmentName, + this.floorName, + this.roomName, + }); RequestsDetails.fromJson(Map json) { id = json['id']; @@ -194,6 +205,11 @@ class RequestsDetails { siteTransferFrom = json['siteTransferFrom']; siteTransferTo = json['siteTransferTo']; transactionType = json['transactionType']; + siteName = json['siteName']; + buildingName = json['buildingName']; + departmentName = json['departmentName']; + floorName = json['floorName']; + roomName = json['roomName']; } Map toJson() { @@ -224,6 +240,11 @@ class RequestsDetails { data['siteTransferFrom'] = siteTransferFrom; data['siteTransferTo'] = siteTransferTo; data['transactionType'] = transactionType; + data['siteName'] = siteName; + data['buildingName'] = buildingName; + data['departmentName'] = departmentName; + data['floorName'] = floorName; + data['roomName'] = roomName; return data; } } diff --git a/lib/models/enums/user_types.dart b/lib/models/enums/user_types.dart index 13238d83..c807669f 100644 --- a/lib/models/enums/user_types.dart +++ b/lib/models/enums/user_types.dart @@ -5,5 +5,6 @@ enum UsersTypes { assessor, assessorTl, qualityUser, - commissioningEngineer + commissioningEngineer, + houseKeeper } diff --git a/lib/models/new_models/dashboard_count.dart b/lib/models/new_models/dashboard_count.dart index 55bafffa..5c6fbcf8 100644 --- a/lib/models/new_models/dashboard_count.dart +++ b/lib/models/new_models/dashboard_count.dart @@ -37,9 +37,10 @@ class Data { int? countComplete; int? countAcknowledge; int? countHighPriority; + int? countClose; int? countOverdue; - Data({this.countOpen, this.countInprogress, this.countComplete, this.countAcknowledge, this.countHighPriority, this.countOverdue}); + Data({this.countOpen, this.countInprogress, this.countClose,this.countComplete, this.countAcknowledge, this.countHighPriority, this.countOverdue}); Data.fromJson(Map json) { countOpen = json['countOpen'] ?? 0; @@ -47,6 +48,7 @@ class Data { countComplete = json['countComplete'] ?? 0; countAcknowledge = json['countAcknowledge'] ?? 0; countHighPriority = json['countHighPriority'] ?? 0; + countClose = json['countClose'] ?? 0; countOverdue = json['countOverdue'] ?? 0; } @@ -57,6 +59,7 @@ class Data { data['countComplete'] = countComplete; data['countAcknowledge'] = countAcknowledge; data['countHighPriority'] = countHighPriority; + data['countClose'] = countClose; data['countOverdue'] = countOverdue; return data; } diff --git a/lib/models/new_models/dashboard_detail.dart b/lib/models/new_models/dashboard_detail.dart index d351337f..9c00cbbb 100644 --- a/lib/models/new_models/dashboard_detail.dart +++ b/lib/models/new_models/dashboard_detail.dart @@ -63,6 +63,11 @@ class Data { int? numberOfSites; int? numberOfAssets; String? nameOfType; + String? siteName; + String? buildingName; + String? floorName; + String? departmentName; + String? roomName; Data( {this.id, @@ -80,10 +85,15 @@ class Data { this.assetNumber, this.requestTypeName, this.requestNo, - this.requestType, - this.poExternal, - this.poInternal, - this.transactionNo}); + this.requestType, + this.poExternal, + this.poInternal, + this.transactionNo, + this.siteName, + this.departmentName, + this.buildingName, + this.floorName, + this.roomName}); Data.fromJson(Map json) { id = json['id']; @@ -106,6 +116,11 @@ class Data { poInternal = json['poInternal']; poExternal = json['poExternal']; nameOfType = json['nameOfType']; + siteName = json['siteName']; + departmentName = json['departmentName']; + buildingName = json['buildingName']; + floorName = json['floorName']; + roomName = json['roomName']; } Map toJson() { @@ -127,6 +142,11 @@ class Data { data['numberOfAssets'] = numberOfAssets; data['numberOfSites'] = numberOfSites; data['nameOfType'] = nameOfType; + data['siteName'] = siteName; + data['buildingName'] = buildingName; + data['departmentName'] = departmentName; + data['floorName'] = floorName; + data['roomName'] = roomName; return data; } } diff --git a/lib/models/user.dart b/lib/models/user.dart index 17da87c9..9bca631d 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -117,6 +117,8 @@ class User { return UsersTypes.qualityUser; case "R-201": // Quality User return UsersTypes.commissioningEngineer; + case "R-204": // Quality User + return UsersTypes.houseKeeper; default: return null; } diff --git a/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart b/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart index 03e2ee38..6bbf5ef1 100644 --- a/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart +++ b/lib/modules/cleaner_module/pages/cleaner_search_details_view.dart @@ -1,4 +1,5 @@ // UI: Cleaner Search Details View +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:test_sa/extensions/context_extension.dart'; @@ -18,6 +19,10 @@ import '../models/cleaner_task_search_result.dart'; import '../provider/cleaner_provider.dart'; import '../../pm_module/recurrent_wo/recurrent_work_order_view.dart'; import 'package:test_sa/controllers/providers/api/all_requests_provider.dart'; +import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart'; +import 'package:test_sa/controllers/api_routes/api_manager.dart'; +import 'package:test_sa/controllers/api_routes/urls.dart'; +import 'package:http/http.dart' as http; class CleanerSearchDetailsView extends StatefulWidget { static const String routeName = '/cleaner_search_details'; @@ -62,12 +67,11 @@ class _CleanerSearchDetailsViewState extends State { }); try { - final allRequestsProvider = Provider.of(context, listen: false); - await allRequestsProvider.getRecurrentWoById(id: widget.requestId); + // Use direct API call to get better error messages + final result = await _getRecurrentWoByIdWithMessage(widget.requestId); - // Map RecurrentWoData to CleanerTaskSearchResult - if (allRequestsProvider.recurrentWoData != null) { - _recurrentWoData = allRequestsProvider.recurrentWoData!; + if (result["success"] == true && result["data"] != null) { + _recurrentWoData = result["data"]; _isRequestOpen = _recurrentWoData?.status?.value == 4; _taskData = CleanerTaskSearchResult( id: _recurrentWoData!.id, @@ -80,16 +84,31 @@ class _CleanerSearchDetailsViewState extends State { locationDescription: null, createdDate: _recurrentWoData!.scheduleDate, ); + } else { + // Show exact backend error message + if (mounted) { + _showErrorDialog(context, result["message"] ?? "Failed to load request details. Please try again."); + } } } catch (e) { debugPrint('Error fetching recurrent work order: $e'); + if (mounted) { + _showErrorDialog(context, "Failed to load request details. Please check your connection and try again."); + } } finally { setState(() { _isLoading = false; }); } } else { - await _cleanerProvider.getCleanerRequestById(id: widget.requestId); + try { + await _cleanerProvider.getCleanerRequestById(id: widget.requestId); + } catch (e) { + debugPrint('Error fetching cleaner request: $e'); + if (mounted) { + _showErrorDialog(context, "Failed to load request details. Please check your connection and try again."); + } + } } } @@ -160,16 +179,32 @@ class _CleanerSearchDetailsViewState extends State { final result = await Navigator.push(context, MaterialPageRoute(builder: (context) => const ScanQr())); if (result != null && result is String && result.isNotEmpty) { showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading()); - final isValid = await CleanerProvider().checkQRSameRoom(planRecurrentTaskId: widget.requestId, qrCode: result); - if (mounted) Navigator.pop(context); - if (isValid) { - if (mounted) { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: widget.requestId)), - ); + try { + final qrResult = await CleanerProvider().checkQRSameRoomWithMessage(planRecurrentTaskId: widget.requestId, qrCode: result); + if (mounted) Navigator.pop(context); - // getInitialData(); + final isValid = qrResult["success"] ?? false; + final backendMessage = qrResult["message"] ?? "Unknown error occurred"; + + if (isValid) { + if (mounted) { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: widget.requestId)), + ); + } + } else { + if (mounted) { + // Show exact backend error message instead of our custom message + _showErrorDialog(context, backendMessage); + } + } + } catch (e) { + if (mounted) Navigator.pop(context); + if (mounted) { + // Extract actual error message from the exception or use generic fallback + String errorMessage = _extractErrorMessage(e) ?? 'Failed to validate QR code. Please try again.'; + _showErrorDialog(context, errorMessage); } } } @@ -293,4 +328,86 @@ class _CleanerSearchDetailsViewState extends State { ], ).toShadowContainer(context, padding: 0); } + + void _showErrorDialog(BuildContext context, String message) { + showDialog( + context: context, + builder: (context) => ConfirmDialog( + message: message, + title: 'Error', + onTap: () => Navigator.pop(context), + ), + ); + } + + String? _extractErrorMessage(dynamic error) { + // Implement your logic to extract error message from the exception + // For example, if the error is a custom exception with a message property: + if (error is ExceptionWithMessage) { + return error.message; + } else if (error is String) { + // If the error is a string, return it directly + return error; + } else if (error is Map) { + // If the error is a Map (e.g., parsed JSON), extract the message field + return error['message'] ?? error.toString(); + } + + // Fallback to a generic error message + return 'An unexpected error occurred. Please try again.'; + } + + /// Custom method to get recurrent work order with proper error message extraction + Future> _getRecurrentWoByIdWithMessage(int id) async { + try { + final response = await ApiManager.instance.get( + URLs.getRecurrentPlanByIdUrl + "?planRecurrentTaskId=$id", + enableToastMessage: false, // Disable automatic toast + ); + + final responseData = json.decode(response.body); + + if (response.statusCode >= 200 && response.statusCode < 300) { + // Success response + final recurrentWoData = RecurrentWoData.fromJson(responseData["data"]); + + // Apply the same processing as in AllRequestsProvider + recurrentWoData.planRecurrentMedicalTaskRooms?.forEach((element) { + element.planRecurrentMedicalTaskRoomTabs?.forEach((tabs) { + tabs.planRecurrentMedicalTaskRoomTabAttributes?.forEach((attribute) { + if (attribute.attribute?.type == "bool" && attribute.attributeValue == null) { + attribute.attributeValue = "true"; + } + }); + }); + }); + + return { + "success": true, + "data": recurrentWoData, + "message": "Data loaded successfully", + }; + } else { + // Error response - extract the exact error message from backend + return { + "success": false, + "data": null, + "message": responseData["message"] ?? "Failed to load request details", + }; + } + } catch (error) { + debugPrint('Error in _getRecurrentWoByIdWithMessage: $error'); + return { + "success": false, + "data": null, + "message": "Failed to load request details. Please check your connection and try again.", + }; + } + } +} + +class ExceptionWithMessage implements Exception { + final String message; + + ExceptionWithMessage(this.message); } diff --git a/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart b/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart index bb9fb739..2ed65884 100644 --- a/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart +++ b/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart @@ -21,6 +21,7 @@ import 'package:test_sa/providers/loading_list_notifier.dart'; import 'package:test_sa/models/new_models/building.dart'; import 'package:test_sa/models/new_models/floor.dart'; import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart'; class CleanerSearchFormView extends StatefulWidget { static const String routeName = '/cleaner_search'; @@ -87,6 +88,7 @@ class _CleanerSearchFormViewState extends State { setState(() { _isLoadingSites = false; }); + _showErrorDialog(context, 'Failed to load sites. Please try again.'); } } @@ -106,6 +108,7 @@ class _CleanerSearchFormViewState extends State { _isLoadingBuildings = false; _buildingOptions = []; }); + _showErrorDialog(context, 'Failed to load buildings. Please try again.'); } } @@ -125,6 +128,7 @@ class _CleanerSearchFormViewState extends State { _isLoadingFloors = false; _floorOptions = []; }); + _showErrorDialog(context, 'Failed to load floors. Please try again.'); } } @@ -146,6 +150,7 @@ class _CleanerSearchFormViewState extends State { _locationDescriptionOptions = []; _allDeptRoomList = []; }); + _showErrorDialog(context, 'Failed to load location descriptions. Please try again.'); } } @@ -465,7 +470,7 @@ class _CleanerSearchFormViewState extends State { Future _onSubmit() async { // Validate that at least site is selected if (selectedSite == null) { - 'Please select a site'.showToast; + _showErrorDialog(context, 'Please select a site'); return; } @@ -502,11 +507,22 @@ class _CleanerSearchFormViewState extends State { }, ); } catch (e) { - 'Error searching'.showToast; + _showErrorDialog(context, 'Error searching. Please try again.'); } finally { setState(() { _isSearching = false; }); } } + + void _showErrorDialog(BuildContext context, String message) { + showDialog( + context: context, + builder: (context) => ConfirmDialog( + message: message, + title: 'Error', + onTap: () => Navigator.pop(context), + ), + ); + } } diff --git a/lib/modules/cleaner_module/provider/cleaner_provider.dart b/lib/modules/cleaner_module/provider/cleaner_provider.dart index a057aa54..ec6fff3f 100644 --- a/lib/modules/cleaner_module/provider/cleaner_provider.dart +++ b/lib/modules/cleaner_module/provider/cleaner_provider.dart @@ -145,6 +145,7 @@ class CleanerProvider extends ChangeNotifier { "planRecurrentTaskId": planRecurrentTaskId, "qrCode": qrCode, }, + showToast: false, // Disable automatic toast message ); if (response.statusCode >= 200 && response.statusCode < 300) { @@ -159,6 +160,49 @@ class CleanerProvider extends ChangeNotifier { } } + /// Check if QR code matches the same room for a plan recurrent task + /// Returns a map with success status and message from backend + Future> checkQRSameRoomWithMessage({ + required int planRecurrentTaskId, + required String qrCode, + }) async { + Response response; + try { + response = await ApiManager.instance.post( + URLs.checkQRSameRoom, + body: { + "planRecurrentTaskId": planRecurrentTaskId, + "qrCode": qrCode, + }, + showToast: false, // Disable automatic toast message + ); + + // Parse the response body to extract the message + final responseData = json.decode(response.body); + + if (response.statusCode >= 200 && response.statusCode < 300) { + // Success response + final isValid = responseData["data"] == true || responseData["isSuccess"] == true; + return { + "success": isValid, + "message": responseData["message"] ?? (isValid ? "QR code validated successfully" : "QR code validation failed"), + }; + } else { + // Error response - extract the exact error message from backend + return { + "success": false, + "message": responseData["message"] ?? "QR code validation failed", + }; + } + } catch (error) { + debugPrint('Error checking QR same room: $error'); + return { + "success": false, + "message": "Failed to validate QR code. Please check your connection and try again.", + }; + } + } + void reset() { _isLoading = false; _searchResults = null; diff --git a/lib/modules/cleaner_module/widgets/cleaner_preview_dialog.dart b/lib/modules/cleaner_module/widgets/cleaner_preview_dialog.dart new file mode 100644 index 00000000..6abd4d04 --- /dev/null +++ b/lib/modules/cleaner_module/widgets/cleaner_preview_dialog.dart @@ -0,0 +1,457 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:test_sa/controllers/api_routes/urls.dart'; +import 'package:test_sa/extensions/context_extension.dart'; +import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/extensions/widget_extensions.dart'; +import 'package:test_sa/extensions/text_extensions.dart'; +import 'package:test_sa/models/ppm/recurrent_wo.dart'; +import 'package:test_sa/new_views/app_style/app_color.dart'; +import 'package:test_sa/new_views/common_widgets/app_filled_button.dart'; +import 'package:test_sa/views/widgets/buttons/app_back_button.dart'; +import 'package:test_sa/modules/cm_module/cm_request_utils.dart'; +import 'package:test_sa/views/widgets/total_working_time_detail_bottomsheet.dart'; + +class CleanerPreviewDialog extends StatelessWidget { + final RecurrentWoData model; + final double totalWorkingHours; + final VoidCallback onConfirm; + + const CleanerPreviewDialog({ + Key? key, + required this.model, + required this.totalWorkingHours, + required this.onConfirm, + }) : super(key: key); + + static void show( + BuildContext context, { + required RecurrentWoData model, + required double totalWorkingHours, + required VoidCallback onConfirm, + }) { + showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) => CleanerPreviewDialog( + model: model, + totalWorkingHours: totalWorkingHours, + onConfirm: onConfirm, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.85), + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(14), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header matching DefaultAppBar style + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(14), + topRight: Radius.circular(14), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: 'Confirm Details'.heading3(context).custom( + fontWeight: FontWeight.w500, + ), + ), + Icon( + Icons.close, + color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, + size: 24, + ).onPress(() => Navigator.pop(context)), + ], + ), + ), + Divider(height: 1, color: context.isDark ? AppColor.neutral60 : AppColor.neutral100), + // Content + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildPreviewSection(context, 'Task Information', [ + _buildPreviewRow(context, context.translation.title, model.title ?? '-'), + _buildPreviewRow(context, context.translation.taskNo, model.taskNo ?? '-'), + _buildPreviewRow(context, context.translation.site, model.site?.siteName ?? '-'), + _buildPreviewRow(context, context.translation.department, model.department?.name ?? '-'), + _buildPreviewRow(context, context.translation.floor, model.floor?.name ?? '-'), + _buildPreviewRow(context, context.translation.room, model.room?.name ?? '-'), + _buildPreviewRow(context, context.translation.assignedEmployee, model.engineer?.userName ?? '-'), + ]), + 16.height, + // Total Working Time Section - Using WorkingTimeTile design + _buildPreviewSection(context, 'Total Working Time', [ + _buildWorkingTimeTile(context), + ]), + 16.height, + if (model.planRecurrentTaskCheckLists != null && model.planRecurrentTaskCheckLists!.isNotEmpty) ...[ + _buildPreviewSection(context, 'Checklist', [ + ...model.planRecurrentTaskCheckLists! + .map( + (item) => Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + item.isChecked ? Icons.check_circle : Icons.circle_outlined, + color: item.isChecked ? AppColor.primary10 : AppColor.neutral120, + size: 20, + ), + 8.width, + RichText( + text: TextSpan( + children: [ + TextSpan( + text: item.title, + style: AppTextStyles.bodyText2.copyWith( + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, + ), + ), + if (item.isMandatory) + TextSpan( + text: ' *', + style: AppTextStyles.bodyText2.copyWith( + color: Colors.red, + ), + ), + ], + ), + ).expanded, + ], + ), + // Show comment if item is checked and has a comment + if (item.isChecked && item.comment.isNotEmpty == true) ...[ + 8.height, + Container( + width: double.infinity, + padding: const EdgeInsets.all(8), + margin: const EdgeInsets.only(left: 28), + decoration: BoxDecoration( + color: context.isDark ? AppColor.neutral60.withValues(alpha: 0.3) : AppColor.neutral100.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(6), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 'Comment:'.bodyText2(context).custom( + fontWeight: FontWeight.w600, + color: context.isDark ? AppColor.neutral30 : AppColor.neutral120, + ), + 4.height, + item.comment.bodyText2(context).custom( + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, + ), + ], + ), + ), + ], + ], + ), + ), + ) + .toList(), + ]), + 16.height, + ], + if (model.comment != null && model.comment!.isNotEmpty) ...[ + _buildPreviewSection(context, context.translation.comment, [ + model.comment!.bodyText(context), + ]), + 16.height, + ], + if (model.imageRoom != null && model.imageRoom!.isNotEmpty) ...[ + _buildPreviewSection(context, 'Captured Image', [ + _buildImagePreview(context, model.imageRoom!), + ]), + 16.height, + ], + ], + ), + ), + ), + // Footer with Confirm button + Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + border: Border(top: BorderSide(color: context.isDark ? AppColor.neutral60 : AppColor.neutral100)), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(14), + bottomRight: Radius.circular(14), + ), + ), + child: AppFilledButton( + label: context.translation.complete, + buttonColor: AppColor.primary10, + onPressed: () { + Navigator.pop(context); + onConfirm(); + }, + ), + ), + ], + ), + ), + ); + } + + Widget _buildPreviewSection(BuildContext context, String title, List children) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title.heading6(context).custom(fontWeight: FontWeight.w600), + 8.height, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ).toShadowContainer(context, padding: 12), + ], + ); + } + + Widget _buildPreviewRow(BuildContext context, String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 2, + child: '$label:'.bodyText2(context).custom(color: AppColor.neutral120), + ), + 8.width, + Expanded( + flex: 3, + child: value.bodyText2(context).custom(color: AppColor.neutral120), + ), + ], + ), + ); + } + + Widget _buildWorkingTimeTile(BuildContext context) { + // Create timer list from model data, same as in RecurrentTaskInfoWidget + final timerList = model.planRecurrentTaskTimers?.map((e) { + return TimerHistoryModel( + id: e.id, + startTime: e.startTime, + endTime: e.endTime, + workingHours: e.workingHours, + ); + }).toList() ?? []; + + return Container( + height: 56.toScreenHeight, + padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth), + alignment: Alignment.centerLeft, + decoration: BoxDecoration( + color: AppColor.fieldBgColor(context), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Total Working Time", + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: context.isDark ? null : AppColor.neutral20, + fontWeight: FontWeight.w500, + ), + ), + Text( + " ${CMRequestUtils.formatTotalWorkingHours(totalWorkingHours)}", + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ).expanded, + 8.width, + const Icon(Icons.info_outline_rounded, color: AppColor.primary10), + ], + ), + ).onPress(() async { + await showModalBottomSheet( + context: context, + isDismissible: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + clipBehavior: Clip.antiAliasWithSaveLayer, + builder: (BuildContext context) => TotalWorkingTimeDetailBottomSheet(timerList: timerList), + ); + }); + } + + Widget _buildImagePreview(BuildContext context, String imageString) { + Widget imageWidget; + + // First check if it's base64 encoded (contains | separator) + if (imageString.contains('|')) { + // Format: "filename|base64data" + try { + final base64Data = imageString.split('|').last; + imageWidget = Image.memory(base64Decode(base64Data), fit: BoxFit.cover); + return Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration( + color: AppColor.fieldBgColor(context), + borderRadius: BorderRadius.circular(8), + ), + child: imageWidget, + ).onPress(() { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => Scaffold( + body: SafeArea( + child: Stack( + children: [ + InteractiveViewer(child: imageWidget).center, + const ABackButton(), + ], + ), + ), + ), + ), + ); + }); + } catch (e) { + debugPrint('Error decoding base64 image: $e'); + return _buildCameraIcon(context); + } + } + + // Check if it's a local file path + if (_isLocalUrl(imageString)) { + try { + imageWidget = Image.file(File(imageString), fit: BoxFit.cover); + return Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration( + color: AppColor.fieldBgColor(context), + borderRadius: BorderRadius.circular(8), + ), + child: imageWidget, + ).onPress(() { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => Scaffold( + body: SafeArea( + child: Stack( + children: [ + InteractiveViewer(child: imageWidget).center, + const ABackButton(), + ], + ), + ), + ), + ), + ); + }); + } catch (e) { + debugPrint('Error loading local file: $e'); + return _buildCameraIcon(context); + } + } + + // If not local and not base64, it's a server filename - use NetworkImage with loading indicator + final imageUrl = URLs.getFileUrl(imageString); + if (imageUrl != null) { + imageWidget = Image.network( + imageUrl, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) { + return child; + } + return Center( + child: CircularProgressIndicator( + color: AppColor.primary10, + value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + debugPrint('Error loading network image: $error'); + return _buildCameraIcon(context); + }, + ); + return Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration( + color: AppColor.fieldBgColor(context), + borderRadius: BorderRadius.circular(8), + ), + child: imageWidget, + ).onPress(() { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => Scaffold( + body: SafeArea( + child: Stack( + children: [ + InteractiveViewer(child: imageWidget).center, + const ABackButton(), + ], + ), + ), + ), + )); + }); + } + + return _buildCameraIcon(context); + } + + /// Build camera icon widget for fallback + Widget _buildCameraIcon(BuildContext context) { + return Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration( + color: AppColor.fieldBgColor(context), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.camera_alt, + size: 48, + color: context.isDark ? AppColor.white10 : AppColor.black10, + ), + ); + } + + /// Check if the URL is a local file path + bool _isLocalUrl(String url) { + return url.startsWith('/') || url.startsWith('file://') || (url.length > 1 && url[1] == ':'); + } +} diff --git a/lib/modules/pm_module/recurrent_wo/components/task_info_widget.dart b/lib/modules/pm_module/recurrent_wo/components/task_info_widget.dart index d49be1bc..9446e975 100644 --- a/lib/modules/pm_module/recurrent_wo/components/task_info_widget.dart +++ b/lib/modules/pm_module/recurrent_wo/components/task_info_widget.dart @@ -7,7 +7,6 @@ import 'package:test_sa/extensions/string_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/models/ppm/recurrent_wo.dart'; -import 'package:test_sa/modules/cm_module/cm_request_utils.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/common_widgets/app_text_form_field.dart'; import 'package:test_sa/new_views/common_widgets/working_time_tile.dart'; @@ -18,10 +17,11 @@ import 'package:test_sa/views/widgets/total_working_time_detail_bottomsheet.dart class RecurrentTaskInfoWidget extends StatefulWidget { final RecurrentWoData? model; - bool isClosed; - bool isHouseKeeper; + final bool isClosed; + final bool isHouseKeeper; + final DateTime? timerStartTime; - RecurrentTaskInfoWidget({super.key, required this.model, this.isClosed = false, this.isHouseKeeper = false}); + const RecurrentTaskInfoWidget({super.key, required this.model, this.isClosed = false, this.isHouseKeeper = false, this.timerStartTime}); @override State createState() => _RecurrentTaskInfoWidgetState(); @@ -87,7 +87,7 @@ class _RecurrentTaskInfoWidgetState extends State { '${context.translation.floor}: ${widget.model!.floor?.name ?? "-"}'.bodyText2(context).custom(color: AppColor.neutral120), '${context.translation.room}: ${widget.model!.room?.name ?? "-"}'.bodyText2(context).custom(color: AppColor.neutral120), ], - '${context.translation.assignedEmployee}: ${widget.model!.engineer!.userName ?? ""}'.bodyText2(context).custom(color: AppColor.neutral120), + '${context.translation.assignedEmployee}: ${widget.model!.engineer?.userName ?? ""}'.bodyText2(context).custom(color: AppColor.neutral120), '${context.translation.scheduledDate}: ${widget.model!.scheduleDate!.toMonthYearFormat}'.bodyText2(context).custom(color: AppColor.neutral120), ], ).toShadowContainer(context), @@ -122,6 +122,27 @@ class _RecurrentTaskInfoWidgetState extends State { } Widget _timerWidget(BuildContext context, double totalWorkingHours) { + // For housekeeper: show read-only timer counter with history + if (widget.isHouseKeeper) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (!widget.isClosed) ...[ + _HousekeeperTimerWidget( + startTime: widget.timerStartTime, + isClosed: widget.isClosed, + ), + 12.height, + ], + if (totalWorkingHours > 0.0) + WorkingTimeTile( + timerList: timerList, + totalWorkingTime: totalWorkingHours, + ), + ], + ); + } + return Consumer(builder: (context, snapshot, child) { return Column( mainAxisSize: MainAxisSize.min, @@ -178,6 +199,92 @@ class _RecurrentTaskInfoWidgetState extends State { } } +// Read-only timer widget for housekeeper +class _HousekeeperTimerWidget extends StatefulWidget { + final DateTime? startTime; + final bool isClosed; + + const _HousekeeperTimerWidget({this.startTime, this.isClosed = false}); + + @override + State<_HousekeeperTimerWidget> createState() => _HousekeeperTimerWidgetState(); +} + +class _HousekeeperTimerWidgetState extends State<_HousekeeperTimerWidget> { + String _elapsedTime = "0:00:00"; + + @override + void initState() { + super.initState(); + if (widget.startTime != null && !widget.isClosed) { + _startTimer(); + } else if (widget.startTime != null && widget.isClosed) { + // If closed, calculate the final time + _updateElapsedTime(); + } + } + + void _updateElapsedTime() { + if (widget.startTime != null) { + final duration = DateTime.now().difference(widget.startTime!); + final hours = duration.inHours; + final minutes = duration.inMinutes % 60; + final seconds = duration.inSeconds % 60; + _elapsedTime = "$hours:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}"; + } + } + + void _startTimer() { + Future.delayed(const Duration(seconds: 1), () { + if (mounted && widget.startTime != null && !widget.isClosed) { + setState(() { + _updateElapsedTime(); + }); + _startTimer(); + } + }); + } + + @override + Widget build(BuildContext context) { + if (widget.startTime == null && !widget.isClosed) { + return SizedBox.shrink(); + } + + return Container( + height: 56.toScreenHeight, + padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth), + decoration: BoxDecoration( + color: AppColor.fieldBgColor(context), + borderRadius: BorderRadius.circular(10), + // boxShadow: [ + // BoxShadow( + // color: Colors.black.withOpacity(0.05), + // blurRadius: 10, + // ) + // ], + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + context.translation.timer.tinyFont(context), + _elapsedTime.bodyText(context).custom( + color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, + ), + ], + ), + ), + // No play/pause icon for housekeeper - timer runs automatically + ], + ), + ); + } +} + Widget buildingInfoWidget({required String label, required String? value, required BuildContext context}) { return Column( mainAxisAlignment: MainAxisAlignment.start, diff --git a/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart b/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart index 3b1a0a30..b350d626 100644 --- a/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart +++ b/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart @@ -1,7 +1,4 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:provider/provider.dart'; import 'package:test_sa/controllers/api_routes/api_manager.dart'; import 'package:test_sa/extensions/context_extension.dart'; @@ -10,6 +7,7 @@ import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/models/ppm/recurrent_wo.dart'; import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart'; import 'package:test_sa/modules/cleaner_module/widgets/cleaner_checklist_widget.dart'; +import 'package:test_sa/modules/cleaner_module/widgets/cleaner_preview_dialog.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/common_widgets/app_filled_button.dart'; import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart'; @@ -17,6 +15,7 @@ import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; import 'package:test_sa/new_views/pages/land_page/land_page.dart'; import 'package:test_sa/views/widgets/loaders/app_loading.dart'; import 'package:test_sa/views/widgets/loaders/no_data_found.dart'; +import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart'; import '../../../../../../controllers/providers/api/all_requests_provider.dart'; import 'components/room_tabs_widget.dart'; @@ -36,12 +35,21 @@ class RecurrentWorkOrderView extends StatefulWidget { class _RecurrentWorkOrderViewState extends State { AllRequestsProvider? allRequestsProvider; + DateTime? _housekeeperTimerStart; @override void initState() { allRequestsProvider = Provider.of(context, listen: false); - // calculateWorkingTime(); - Provider.of(context, listen: false).getRecurrentWoById(id: widget.taskId!); + Provider.of(context, listen: false).getRecurrentWoById(id: widget.taskId!).then((_) { + // Start timer automatically for housekeeper type + final provider = Provider.of(context, listen: false); + bool isHouseKeeper = provider.recurrentWoData?.planRecurrentTaskCheckLists != null && + provider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty; + + if (isHouseKeeper && provider.recurrentWoData?.status?.value != 1) { + _housekeeperTimerStart = DateTime.now(); + } + }); super.initState(); } @@ -63,7 +71,7 @@ class _RecurrentWorkOrderViewState extends State { onWillPopScope: isClosed ? null : () { - _updateTask(context: context, status: 0); + _updateTask(context: context, status: 0, isHouseKeeper: isHouseKeeper); }, ), body: allRequestsProvider!.isLoading @@ -78,7 +86,12 @@ class _RecurrentWorkOrderViewState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - RecurrentTaskInfoWidget(isClosed: isClosed, model: requestProvider.recurrentWoData, isHouseKeeper: isHouseKeeper), + RecurrentTaskInfoWidget( + isClosed: isClosed, + model: requestProvider.recurrentWoData, + isHouseKeeper: isHouseKeeper, + timerStartTime: _housekeeperTimerStart, + ), IgnorePointer( ignoring: isClosed, child: requestProvider.recurrentWoData!.planRecurrentMedicalTaskRooms != null && requestProvider.recurrentWoData!.planRecurrentMedicalTaskRooms!.isNotEmpty @@ -121,14 +134,13 @@ class _RecurrentWorkOrderViewState extends State { label: context.translation.save, buttonColor: context.isDark ? AppColor.neutral20 : AppColor.white60, textColor: context.isDark ? Colors.white : AppColor.black10, - onPressed: () => _updateTask(context: context, status: 0), + onPressed: () => _updateTask(context: context, status: 0, isHouseKeeper: isHouseKeeper), ).expanded, 12.width, AppFilledButton( label: context.translation.complete, buttonColor: AppColor.primary10, - // textColor: context.isDark?Colors.white: AppColor.black10, - onPressed: () => _updateTask(context: context, status: 1), + onPressed: () => _updateTask(context: context, status: 1, isHouseKeeper: isHouseKeeper), ).expanded, ], ), @@ -142,91 +154,176 @@ class _RecurrentWorkOrderViewState extends State { cxt: context, showPopUp: !isClosed, onSave: () { - _updateTask(context: context, status: 0); + _updateTask(context: context, status: 0, isHouseKeeper: isHouseKeeper); }); }); } -} -void _updateTask({required BuildContext context, required int status}) async { - AllRequestsProvider allRequestsProvider = Provider.of(context, listen: false); + void _updateTask({required BuildContext context, required int status, required bool isHouseKeeper}) async { + AllRequestsProvider allRequestsProvider = Provider.of(context, listen: false); - // Only validate checklist if it exists and status is complete - if (status == 1 && allRequestsProvider.recurrentWoData?.planRecurrentTaskCheckLists != null && allRequestsProvider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty) { - // Validate mandatory checklist items only - if (!_validateChecklist(allRequestsProvider.recurrentWoData!)) { + // Only validate checklist if it exists and status is complete + if (status == 1 && allRequestsProvider.recurrentWoData?.planRecurrentTaskCheckLists != null && allRequestsProvider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty) { + // Validate mandatory checklist items only + if (!_validateChecklist(context, allRequestsProvider.recurrentWoData!)) { + return; + } + } + + // For housekeeper: show preview dialog before completing + if (isHouseKeeper && status == 1) { + _showPreviewDialog(context, allRequestsProvider); return; } - // Image validation is handled by backend - no need to force capture here + // Proceed with update + _performUpdate(context: context, status: status, isHouseKeeper: isHouseKeeper); } - if (validate(model: allRequestsProvider.recurrentWoData!, status: status)) { - allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers ?? []; - - if (allRequestsProvider.recurrentWoData?.recurrentWoTimePicker != null) { - int durationInSecond = allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt!.difference(allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.startAt!).inSeconds; - allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add( - PlanRecurrentTaskTimers( - id: 0, - startTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.startAt!.toIso8601String(), - endTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt?.toIso8601String(), - workingHours: ((durationInSecond) / 60 / 60), - ), - ); - } - allRequestsProvider.recurrentWoData?.timerModelList?.forEach((timer) { - int durationInSecond = timer.endAt!.difference(timer.startAt!).inSeconds; - allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add( - PlanRecurrentTaskTimers( - id: 0, - startTime: timer.startAt!.toIso8601String(), - endTime: timer.endAt?.toIso8601String(), - workingHours: ((durationInSecond) / 60 / 60), - ), - ); - }); + void _performUpdate({required BuildContext context, required int status, required bool isHouseKeeper}) async { + AllRequestsProvider allRequestsProvider = Provider.of(context, listen: false); + + if (validate(context: context, model: allRequestsProvider.recurrentWoData!, status: status, isHouseKeeper: isHouseKeeper)) { + // For housekeeper: automatically add timer data + if (isHouseKeeper && _housekeeperTimerStart != null) { + DateTime endTime = DateTime.now(); + int durationInSecond = endTime.difference(_housekeeperTimerStart!).inSeconds; - final uniqueTimersMap = {}; - allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.forEach((timer) { - if (timer.startTime != null && timer.endTime != null) { - final key = '${timer.id}_${timer.startTime}_${timer.endTime}'; - uniqueTimersMap[key] = timer; + allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers ?? []; + allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add( + PlanRecurrentTaskTimers( + id: 0, + startTime: _housekeeperTimerStart!.toIso8601String(), + endTime: endTime.toIso8601String(), + workingHours: ((durationInSecond) / 60 / 60), + ), + ); + } else { + // For non-housekeeper: use existing timer logic + allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers ?? []; + + if (allRequestsProvider.recurrentWoData?.recurrentWoTimePicker != null) { + int durationInSecond = allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt!.difference(allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.startAt!).inSeconds; + allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add( + PlanRecurrentTaskTimers( + id: 0, + startTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.startAt!.toIso8601String(), + endTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt?.toIso8601String(), + workingHours: ((durationInSecond) / 60 / 60), + ), + ); + } + allRequestsProvider.recurrentWoData?.timerModelList?.forEach((timer) { + int durationInSecond = timer.endAt!.difference(timer.startAt!).inSeconds; + allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add( + PlanRecurrentTaskTimers( + id: 0, + startTime: timer.startAt!.toIso8601String(), + endTime: timer.endAt?.toIso8601String(), + workingHours: ((durationInSecond) / 60 / 60), + ), + ); + }); } - }); - allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = uniqueTimersMap.values.toList(); - showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading()); + final uniqueTimersMap = {}; + allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.forEach((timer) { + if (timer.startTime != null && timer.endTime != null) { + final key = '${timer.id}_${timer.startTime}_${timer.endTime}'; + uniqueTimersMap[key] = timer; + } + }); + + allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = uniqueTimersMap.values.toList(); + showDialog(context: context, barrierDismissible: false, builder: (context) => const AppLazyLoading()); - await allRequestsProvider.updateRecurrentWo(status: status).then((success) { - Navigator.pop(context); // Close loading dialog - if (success) { - if (status == 1) { - // When complete is clicked, navigate to LandPage (Dashboard) - allRequestsProvider.reset(); - allRequestsProvider.getAllRequests(context, typeTransaction: 5); - allRequestsProvider.recurrentWoData?.recurrentWoTimerModel = null; + await allRequestsProvider.updateRecurrentWo(status: status, showToast: false).then((result) { + Navigator.pop(context); // Close loading dialog - // Navigate to LandPage and clear all previous routes - Navigator.of(context).pushNamedAndRemoveUntil(LandPage.routeName, (routes) => false); + bool success = result['success'] ?? false; + String message = result['message'] ?? ''; + + if (success) { + if (status == 1) { + // When complete is clicked, navigate to LandPage (Dashboard) + allRequestsProvider.reset(); + allRequestsProvider.getAllRequests(context, typeTransaction: 5); + allRequestsProvider.recurrentWoData?.recurrentWoTimerModel = null; + + // Navigate to LandPage and clear all previous routes + Navigator.of(context).pushNamedAndRemoveUntil(LandPage.routeName, (routes) => false); + } else { + // For save (status 0), just pop the current screen + allRequestsProvider.recurrentWoData?.recurrentWoTimerModel = null; + Navigator.pop(context); + } } else { - // For save (status 0), just pop the current screen - allRequestsProvider.recurrentWoData?.recurrentWoTimerModel = null; - Navigator.pop(context); + // Show error dialog with the exact message from API + _showErrorDialog(context, message.isNotEmpty ? message : "Failed to update the task. Please try again."); } + }).catchError((error) { + Navigator.pop(context); // Close loading dialog + _showErrorDialog(context, error.toString()); + }); + } + } + + void _showPreviewDialog(BuildContext context, AllRequestsProvider provider) { + final model = provider.recurrentWoData!; + + // Calculate total working time + double totalWorkingHours = 0; + final timers = model.planRecurrentTaskTimers ?? []; + totalWorkingHours = timers.fold(0.0, (sum, item) { + if (item.startTime == null || item.endTime == null) return sum; + try { + final start = DateTime.parse(item.startTime!); + final end = DateTime.parse(item.endTime!); + final diffInHours = end.difference(start).inSeconds / 3600.0; + return sum + diffInHours; + } catch (_) { + return sum; } }); + + // Add current timer if running + if (_housekeeperTimerStart != null) { + final currentDuration = DateTime.now().difference(_housekeeperTimerStart!).inSeconds / 3600.0; + totalWorkingHours += currentDuration; + } + + CleanerPreviewDialog.show( + context, + model: model, + totalWorkingHours: totalWorkingHours, + onConfirm: () => _performUpdate(context: context, status: 1, isHouseKeeper: true), + ); + } + + void _showErrorDialog(BuildContext context, String message) { + showDialog( + context: context, + builder: (context) => ConfirmDialog( + message: message, + title: 'Error', + onTap: () => Navigator.pop(context), + ), + ); } } -bool _validateChecklist(RecurrentWoData model) { +bool _validateChecklist(BuildContext context, RecurrentWoData model) { // Check if all mandatory checklist items are checked final uncheckedMandatory = model.planRecurrentTaskCheckLists!.where((item) => item.isMandatory && !item.isChecked).toList(); if (uncheckedMandatory.isNotEmpty) { - Fluttertoast.showToast( - msg: "Please complete all mandatory tasks (marked with *)", - toastLength: Toast.LENGTH_LONG, + showDialog( + context: context, + builder: (context) => ConfirmDialog( + message: "Please complete all mandatory tasks (marked with *)", + title: 'Validation Error', + onTap: () => Navigator.pop(context), + ), ); return false; } @@ -234,31 +331,72 @@ bool _validateChecklist(RecurrentWoData model) { return true; } -bool validate({required RecurrentWoData model, required int status}) { +bool validate({required BuildContext context, required RecurrentWoData model, required int status, required bool isHouseKeeper}) { + // Skip timer validation for housekeeper type + if (isHouseKeeper) { + return true; + } + + // Original timer validation logic for non-housekeeper types bool isTimerPickerEnable = ApiManager.instance.assetGroup?.enabledEngineerTimer ?? false; if (isTimerPickerEnable) { if (model.recurrentWoTimerModel?.startAt == null && model.recurrentWoTimePicker == null) { - Fluttertoast.showToast(msg: "Working Hours Required"); + showDialog( + context: context, + builder: (context) => ConfirmDialog( + message: "Working Hours Required", + title: 'Validation Error', + onTap: () => Navigator.pop(context), + ), + ); return false; } if (model.recurrentWoTimePicker == null) { if (model.recurrentWoTimerModel?.startAt == null) { - Fluttertoast.showToast(msg: "Working Hours Required"); + showDialog( + context: context, + builder: (context) => ConfirmDialog( + message: "Working Hours Required", + title: 'Validation Error', + onTap: () => Navigator.pop(context), + ), + ); return false; } if (model.recurrentWoTimerModel?.endAt == null) { - Fluttertoast.showToast(msg: "Please Stop The Timer"); + showDialog( + context: context, + builder: (context) => ConfirmDialog( + message: "Please Stop The Timer", + title: 'Validation Error', + onTap: () => Navigator.pop(context), + ), + ); return false; } } } else { if (model.recurrentWoTimerModel?.startAt == null) { - Fluttertoast.showToast(msg: "Working Hours Required"); + showDialog( + context: context, + builder: (context) => ConfirmDialog( + message: "Working Hours Required", + title: 'Validation Error', + onTap: () => Navigator.pop(context), + ), + ); return false; } if (model.recurrentWoTimerModel?.endAt == null) { - Fluttertoast.showToast(msg: "Please Stop The Timer"); + showDialog( + context: context, + builder: (context) => ConfirmDialog( + message: "Please Stop The Timer", + title: 'Validation Error', + onTap: () => Navigator.pop(context), + ), + ); return false; } } diff --git a/lib/new_views/pages/land_page/my_request/all_requests_filter_page.dart b/lib/new_views/pages/land_page/my_request/all_requests_filter_page.dart index 9b5a7441..f1e35b4c 100644 --- a/lib/new_views/pages/land_page/my_request/all_requests_filter_page.dart +++ b/lib/new_views/pages/land_page/my_request/all_requests_filter_page.dart @@ -127,8 +127,11 @@ class _AllRequestsFilterPageState extends State { if (context.settingProvider.isUserFlowMedical && !context.userProvider.isNurse) { types['Incidents'] = 16; } - - final statuses = { +//No need to add check for assetgroup type as this role will only for FMS + if (context.userProvider.isHouseKeeper) { + types = {context.translation.recurrentWo: 5}; + } + var statuses = { "All WO": 0, context.translation.open: 1, context.translation.inProgress: 2, @@ -136,6 +139,16 @@ class _AllRequestsFilterPageState extends State { context.translation.closed: 3, "Canceled": 5, }; + if (context.userProvider.isHouseKeeper) { + statuses = { + "All WO": 0, + // context.translation.open: 1, + context.translation.inProgress: 2, + // context.translation.completed: 4, + context.translation.closed: 3, + // "Canceled": 5, + }; + } return Scaffold( appBar: DefaultAppBar( title: context.translation.filter, diff --git a/lib/new_views/pages/land_page/my_request/my_requests_page.dart b/lib/new_views/pages/land_page/my_request/my_requests_page.dart index ba49458e..123f4ad0 100644 --- a/lib/new_views/pages/land_page/my_request/my_requests_page.dart +++ b/lib/new_views/pages/land_page/my_request/my_requests_page.dart @@ -106,6 +106,14 @@ class _MyRequestsPageState extends State { requestsList.add(Request(16, "Incidents")); } + if (context.userProvider.isHouseKeeper) { + requestsList = [ + Request(null, context.translation.allWorkOrder), + Request(5, context.translation.recurrentWo), + // Request(15, 'Demo'), moved to permission api + ]; + } + _provider = Provider.of(context, listen: false); _provider!.reset(); WidgetsBinding.instance.addPostFrameCallback((_) { diff --git a/lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart b/lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart index 0f63745e..bf3db783 100644 --- a/lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart +++ b/lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart @@ -27,7 +27,7 @@ class RecurrentWoItemView extends StatelessWidget { Widget build(BuildContext context) { final user = Provider.of(context, listen: false).user; final settingProvider = Provider.of(context, listen: false); - final bool isFMSEngineer = user?.type == UsersTypes.engineer && settingProvider.isUserFMS; + final bool isHouseKeeper = user?.type == UsersTypes.houseKeeper; if (requestData != null) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -55,12 +55,22 @@ class RecurrentWoItemView extends StatelessWidget { ], ), 8.height, - (requestData?.typeTransaction ?? context.translation.ppmRequest).heading5(context), + (requestData?.typeTransaction ?? context.translation.recurrentWo).heading5(context), 8.height, // '${context.translation.assetNumber}: ${requestData!.assetNumber}'.bodyText(context), // '${context.translation.assetSN}: ${request.assetSN}'.bodyText(context),//todo ask ahmed // '${context.translation.code}: ${request.code}'.bodyText(context), '${context.translation.taskNo}: ${requestData!.requestNo}'.bodyText(context), + if(requestData?.siteName!=null) + '${context.translation.site}: ${requestData!.siteName}'.bodyText(context), + if(requestData!.buildingName!=null) + '${context.translation.building}: ${requestData!.buildingName}'.bodyText(context), + if(requestData!.departmentName!=null) + '${context.translation.department}: ${requestData!.departmentName}'.bodyText(context), + if(requestData!.floorName!=null) + '${context.translation.floor}: ${requestData!.floorName}'.bodyText(context), + if(requestData!.roomName!=null) + '${context.translation.room}: ${requestData!.roomName}'.bodyText(context), 8.height, Row( mainAxisSize: MainAxisSize.min, @@ -75,7 +85,7 @@ class RecurrentWoItemView extends StatelessWidget { ), ], ).toShadowContainer(context, withShadow: showShadow).onPress(() { - if (isFMSEngineer && (requestData?.statusValue == 4)) { + if (isHouseKeeper && (requestData?.statusValue == 4)) { Navigator.push( context, MaterialPageRoute( @@ -123,6 +133,11 @@ class RecurrentWoItemView extends StatelessWidget { '${context.translation.site}: ${requestDetails!.site}'.bodyText(context), // '${context.translation.code}: ${request.code}'.bodyText(context), '${context.translation.taskNo}: ${requestDetails!.requestNo}'.bodyText(context), + if (requestDetails!.siteName != null) '${context.translation.site}: ${requestDetails!.siteName}'.bodyText(context), + if (requestDetails!.buildingName != null) '${context.translation.building}: ${requestDetails!.buildingName}'.bodyText(context), + if (requestDetails!.departmentName != null) '${context.translation.department}: ${requestDetails!.departmentName}'.bodyText(context), + if (requestDetails!.floorName != null) '${context.translation.floor}: ${requestDetails!.floorName}'.bodyText(context), + if (requestDetails!.roomName != null) '${context.translation.room}: ${requestDetails!.roomName}'.bodyText(context), 8.height, Row( mainAxisSize: MainAxisSize.min, @@ -137,7 +152,7 @@ class RecurrentWoItemView extends StatelessWidget { ), ], ).toShadowContainer(context, withShadow: showShadow).onPress(() async { - if (isFMSEngineer && (requestDetails?.statusValue == 4)) { + if (isHouseKeeper && (requestDetails?.statusValue == 4)) { Navigator.push( context, MaterialPageRoute( diff --git a/lib/views/widgets/dialogs/confirm_dialog.dart b/lib/views/widgets/dialogs/confirm_dialog.dart index 83b05faa..6d8ef764 100644 --- a/lib/views/widgets/dialogs/confirm_dialog.dart +++ b/lib/views/widgets/dialogs/confirm_dialog.dart @@ -44,6 +44,7 @@ class ConfirmDialog extends StatelessWidget { ) ], ), + 16.height, message.bodyText(context), 28.height, AppFilledButton(