improvements

cleaner_module_new_flow
WaseemAbbasi22 2 months ago
parent d1a94df33d
commit d3dadb70c3

@ -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<bool> updateRecurrentWo({required int status}) async {
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: 'Closed', 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('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 = [

@ -24,8 +24,8 @@ class RequestCategoryFragment extends StatelessWidget {
final user = Provider.of<UserProvider>(context, listen: false).user;
final settingProvider = Provider.of<SettingProvider>(context, listen: false);
// Check if user is FMS Engineer using SettingProvider
final bool isFMSEngineer = user?.type == UsersTypes.engineer && settingProvider.isUserFMS;
// 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..
@ -53,7 +53,7 @@ class RequestCategoryFragment extends StatelessWidget {
: const SizedBox(),
],
).paddingOnly(start: 14, end: 12)
: isFMSEngineer
: isHouseKeeper
? Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [

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

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

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

@ -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<CleanerSearchDetailsView> {
});
try {
final allRequestsProvider = Provider.of<AllRequestsProvider>(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<CleanerSearchDetailsView> {
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<CleanerSearchDetailsView> {
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<CleanerSearchDetailsView> {
],
).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);
}

@ -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<CleanerSearchFormView> {
setState(() {
_isLoadingSites = false;
});
_showErrorDialog(context, 'Failed to load sites. Please try again.');
}
}
@ -106,6 +108,7 @@ class _CleanerSearchFormViewState extends State<CleanerSearchFormView> {
_isLoadingBuildings = false;
_buildingOptions = [];
});
_showErrorDialog(context, 'Failed to load buildings. Please try again.');
}
}
@ -125,6 +128,7 @@ class _CleanerSearchFormViewState extends State<CleanerSearchFormView> {
_isLoadingFloors = false;
_floorOptions = [];
});
_showErrorDialog(context, 'Failed to load floors. Please try again.');
}
}
@ -146,6 +150,7 @@ class _CleanerSearchFormViewState extends State<CleanerSearchFormView> {
_locationDescriptionOptions = [];
_allDeptRoomList = [];
});
_showErrorDialog(context, 'Failed to load location descriptions. Please try again.');
}
}
@ -465,7 +470,7 @@ class _CleanerSearchFormViewState extends State<CleanerSearchFormView> {
Future<void> _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<CleanerSearchFormView> {
},
);
} 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),
),
);
}
}

@ -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<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.",
};
}
}
void reset() {
_isLoading = false;
_searchResults = null;

@ -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<Widget> 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] == ':');
}
}

@ -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<RecurrentTaskInfoWidget> createState() => _RecurrentTaskInfoWidgetState();
@ -87,7 +87,7 @@ class _RecurrentTaskInfoWidgetState extends State<RecurrentTaskInfoWidget> {
'${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<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,
@ -178,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,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<RecurrentWorkOrderView> {
AllRequestsProvider? allRequestsProvider;
DateTime? _housekeeperTimerStart;
@override
void initState() {
allRequestsProvider = Provider.of<AllRequestsProvider>(context, listen: false);
// calculateWorkingTime();
Provider.of<AllRequestsProvider>(context, listen: false).getRecurrentWoById(id: widget.taskId!);
Provider.of<AllRequestsProvider>(context, listen: false).getRecurrentWoById(id: widget.taskId!).then((_) {
// Start timer automatically for housekeeper type
final provider = Provider.of<AllRequestsProvider>(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<RecurrentWorkOrderView> {
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<RecurrentWorkOrderView> {
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<RecurrentWorkOrderView> {
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<RecurrentWorkOrderView> {
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<AllRequestsProvider>(context, listen: false);
void _updateTask({required BuildContext context, required int status, required bool isHouseKeeper}) async {
AllRequestsProvider allRequestsProvider = Provider.of<AllRequestsProvider>(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<AllRequestsProvider>(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 = <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 = 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 = <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).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<double>(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;
}
}

@ -127,8 +127,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,
@ -136,6 +139,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,

@ -106,6 +106,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((_) {

@ -27,7 +27,7 @@ class RecurrentWoItemView extends StatelessWidget {
Widget build(BuildContext context) {
final user = Provider.of<UserProvider>(context, listen: false).user;
final settingProvider = Provider.of<SettingProvider>(context, listen: false);
final bool isFMSEngineer = user?.type == UsersTypes.engineer && settingProvider.isUserFMS;
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(

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

Loading…
Cancel
Save