cleaner module implementation new flow complete

cleaner_module_new_flow
WaseemAbbasi22 4 weeks ago
parent 5c46fdd8e5
commit c35162fd88

@ -418,9 +418,9 @@ class CategoryTabs {
if (userType == UsersTypes.houseKeeper) { if (userType == UsersTypes.houseKeeper) {
// tabs.add(CategoryTabs(label: 'Upcoming', tag: 0, id: 0)); // tabs.add(CategoryTabs(label: 'Upcoming', tag: 0, id: 0));
// tabs.add(CategoryTabs('Open', 1)); // tabs.add(CategoryTabs('Open', 1));
tabs.add(CategoryTabs(label: 'UnAssigned', tag: 1, id: 1)); // tabs.add(CategoryTabs(label: 'UnAssigned', tag: 1, id: 1));
tabs.add(CategoryTabs(label: 'In Progress', tag: 2, id: 2)); tabs.add(CategoryTabs(label: 'In Progress', tag: 2, id: 2));
tabs.add(CategoryTabs(label: 'Closed', tag: 4, id: 3)); tabs.add(CategoryTabs(label: 'Completed', tag: 4, id: 3));
return tabs; return tabs;
} }

@ -193,7 +193,7 @@ class ProgressFragment extends StatelessWidget {
total = (snapshot.dashboardCount?.data?.countClose ?? 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 = [
ChartData('In Progress', snapshot.dashboardCount?.data?.countInprogress?.toDouble() ?? 0.0, AppColor.orange70), ChartData('In Progress', snapshot.dashboardCount?.data?.countInprogress?.toDouble() ?? 0.0, AppColor.orange70),
ChartData('Closed', snapshot.dashboardCount?.data?.countClose?.toDouble() ?? 0.0, AppColor.green70), ChartData('Completed', snapshot.dashboardCount?.data?.countClose?.toDouble() ?? 0.0, AppColor.green70),
// ChartData('Open', snapshot.dashboardCount?.data?.countOpen?.toDouble() ?? 0.0, AppColor.blueStatus(context)), // ChartData('Open', snapshot.dashboardCount?.data?.countOpen?.toDouble() ?? 0.0, AppColor.blueStatus(context)),
]; ];
} else { } else {

@ -5,6 +5,7 @@ import 'package:test_sa/dashboard_latest/dashboard_provider.dart';
import 'package:test_sa/dashboard_latest/widgets/request_category_list.dart'; import 'package:test_sa/dashboard_latest/widgets/request_category_list.dart';
import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/enums/user_types.dart'; import 'package:test_sa/models/enums/user_types.dart';
@ -57,8 +58,8 @@ class RequestCategoryFragment extends StatelessWidget {
? Row( ? Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
context.translation.allWorkOrder.heading4(context), 'All Tasks'.addTranslation.heading4(context),
'search'.toSvgAsset(width: 20, height: 20).onPress(() { 'add_icon'.toSvgAsset(width: 20, height: 20).onPress(() {
Navigator.of(context).pushNamed('/cleaner_search'); Navigator.of(context).pushNamed('/cleaner_search');
}), }),
], ],

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

@ -140,7 +140,6 @@ import 'providers/lookups/yes_no_lookup_provider.dart';
import 'providers/service_request_providers/loan_availability_provider.dart'; import 'providers/service_request_providers/loan_availability_provider.dart';
import 'providers/service_request_providers/reject_reason_provider.dart'; import 'providers/service_request_providers/reject_reason_provider.dart';
import 'modules/cleaner_module/pages/cleaner_search_form_view.dart'; import 'modules/cleaner_module/pages/cleaner_search_form_view.dart';
import 'modules/cleaner_module/pages/cleaner_search_list_view.dart';
import 'modules/cleaner_module/provider/cleaner_provider.dart'; import 'modules/cleaner_module/provider/cleaner_provider.dart';
// class MyHttpOverrides extends HttpOverrides { // class MyHttpOverrides extends HttpOverrides {
@ -408,7 +407,6 @@ class MyApp extends StatelessWidget {
CreateMedicalGasRequestPage.id: (_) => CreateMedicalGasRequestPage(), CreateMedicalGasRequestPage.id: (_) => CreateMedicalGasRequestPage(),
CreateDemoRequestPage.id: (_) => CreateDemoRequestPage(), CreateDemoRequestPage.id: (_) => CreateDemoRequestPage(),
CleanerSearchFormView.routeName: (_) => const CleanerSearchFormView(), CleanerSearchFormView.routeName: (_) => const CleanerSearchFormView(),
CleanerSearchListView.routeName: (_) => const CleanerSearchListView(),
}, },
), ),
), ),

@ -1,24 +0,0 @@
// Cleaner request search model for mock/static data
class CleanerRequest {
final String priority;
final String date;
final String time;
final String roomNo;
final String site;
final String building;
final String floor;
final String clinic;
final String? taskNo;
CleanerRequest({
required this.priority,
required this.date,
required this.time,
required this.roomNo,
required this.site,
required this.building,
required this.floor,
required this.clinic,
this.taskNo,
});
}

@ -24,7 +24,8 @@ class OneTimeTaskData {
List<Attachment>? attachments; List<Attachment>? attachments;
String? createdDate; String? createdDate;
String? createdBy; String? createdBy;
String? imageRoom; // For captured image String? imageRoom; // For backward compatibility - deprecated
List<String>? imageRooms; // For multiple captured images (max 2)
OneTimeTaskData({ OneTimeTaskData({
this.id, this.id,
@ -53,6 +54,7 @@ class OneTimeTaskData {
this.createdDate, this.createdDate,
this.createdBy, this.createdBy,
this.imageRoom, this.imageRoom,
this.imageRooms,
}); });
factory OneTimeTaskData.fromJson(Map<String, dynamic> json) { factory OneTimeTaskData.fromJson(Map<String, dynamic> json) {
@ -87,6 +89,7 @@ class OneTimeTaskData {
createdDate: json['createdDate'], createdDate: json['createdDate'],
createdBy: json['createdBy'], createdBy: json['createdBy'],
imageRoom: json['imageRoom'], imageRoom: json['imageRoom'],
imageRooms: json['imageRooms'] != null ? List<String>.from(json['imageRooms']) : null,
); );
} }
@ -118,6 +121,7 @@ class OneTimeTaskData {
'createdDate': createdDate, 'createdDate': createdDate,
'createdBy': createdBy, 'createdBy': createdBy,
'imageRoom': imageRoom, 'imageRoom': imageRoom,
'imageRooms': imageRooms,
}; };
} }
} }
@ -204,12 +208,16 @@ class Instruction {
class Attachment { class Attachment {
int? id; int? id;
String? fileName; String? name;
String? fileUrl; String? originalName;
String? fileName; // For backward compatibility
String? fileUrl; // For backward compatibility
String? fileType; String? fileType;
Attachment({ Attachment({
this.id, this.id,
this.name,
this.originalName,
this.fileName, this.fileName,
this.fileUrl, this.fileUrl,
this.fileType, this.fileType,
@ -218,6 +226,8 @@ class Attachment {
factory Attachment.fromJson(Map<String, dynamic> json) { factory Attachment.fromJson(Map<String, dynamic> json) {
return Attachment( return Attachment(
id: json['id'], id: json['id'],
name: json['name'],
originalName: json['originalName'],
fileName: json['fileName'], fileName: json['fileName'],
fileUrl: json['fileUrl'], fileUrl: json['fileUrl'],
fileType: json['fileType'], fileType: json['fileType'],
@ -227,10 +237,11 @@ class Attachment {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'id': id, 'id': id,
'name': name,
'originalName': originalName,
'fileName': fileName, 'fileName': fileName,
'fileUrl': fileUrl, 'fileUrl': fileUrl,
'fileType': fileType, 'fileType': fileType,
}; };
} }
} }

@ -1,69 +0,0 @@
import 'package:flutter/material.dart';
import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/new_views/app_style/app_color.dart';
import '../models/cleaner_task_search_result.dart';
class CleanerRequestItemView extends StatelessWidget {
final CleanerTaskSearchResult result;
final VoidCallback? onTap;
final bool showShadow;
const CleanerRequestItemView({Key? key, required this.result, this.onTap, this.showShadow = true}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Task Number and Date Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (result.taskNo != null && result.taskNo!.isNotEmpty) result.taskNo!.heading5(context),
if (result.createdDate != null)
Text(
result.createdDate!.toServiceRequestCardFormat,
textAlign: TextAlign.end,
style: AppTextStyles.tinyFont.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral50,
),
),
],
),
8.height,
// Location Information
if (result.siteName != null && result.siteName!.isNotEmpty) 'Site: ${result.siteName}'.bodyText(context),
if (result.buildingName != null && result.buildingName!.isNotEmpty) 'Building: ${result.buildingName}'.bodyText(context),
if (result.floorName != null && result.floorName!.isNotEmpty) 'Floor: ${result.floorName}'.bodyText(context),
if (result.departmentName != null && result.departmentName!.isNotEmpty) 'Department: ${result.departmentName}'.bodyText(context),
if (result.roomName != null && result.roomName!.isNotEmpty) 'Room: ${result.roomName}'.bodyText(context),
if (result.locationDescription != null && result.locationDescription!.isNotEmpty) 'Location: ${result.locationDescription}'.bodyText(context),
8.height,
// View Details Link
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
context.translation.viewDetails,
style: AppTextStyles.bodyText.copyWith(color: AppColor.blueStatus(context)),
),
4.width,
Icon(
Icons.arrow_forward,
color: AppColor.blueStatus(context),
size: 14,
)
],
),
],
).toShadowContainer(context, withShadow: showShadow).onPress(onTap);
}
}

@ -5,6 +5,7 @@ import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/modules/cleaner_module/models/task_type.dart'; import 'package:test_sa/modules/cleaner_module/models/task_type.dart';
import 'package:test_sa/modules/cleaner_module/models/qr_scan_location_data.dart'; import 'package:test_sa/modules/cleaner_module/models/qr_scan_location_data.dart';
import 'package:test_sa/modules/cleaner_module/models/one_time_task_data.dart';
import 'package:test_sa/modules/cleaner_module/provider/cleaner_provider.dart'; import 'package:test_sa/modules/cleaner_module/provider/cleaner_provider.dart';
import 'package:test_sa/modules/cleaner_module/provider/task_type_provider.dart'; import 'package:test_sa/modules/cleaner_module/provider/task_type_provider.dart';
import 'package:test_sa/modules/cleaner_module/pages/one_time_task_view.dart'; import 'package:test_sa/modules/cleaner_module/pages/one_time_task_view.dart';
@ -142,10 +143,16 @@ class _CleanerSearchFormViewState extends State<CleanerSearchFormView> {
final taskId = data['id']; final taskId = data['id'];
if (mounted && taskId != null) { if (mounted && taskId != null) {
// Navigate to OneTimeTaskView // Convert the data to OneTimeTaskData and navigate with the data
final taskData = OneTimeTaskData.fromJson(data);
// Navigate to OneTimeTaskView with the data
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => OneTimeTaskView(taskId: taskId), builder: (context) => OneTimeTaskView(
taskId: taskId,
initialData: taskData,
),
), ),
); );
} }
@ -289,51 +296,50 @@ class _CleanerSearchFormViewState extends State<CleanerSearchFormView> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( if (locationData.siteName != null && locationData.siteName!.isNotEmpty)
crossAxisAlignment: CrossAxisAlignment.start, Text(
children: [ 'Site: ${locationData.siteName}',
Column( style: AppTextStyles.bodyText.copyWith(
crossAxisAlignment: CrossAxisAlignment.start, color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
children: [
if (locationData.siteName != null && locationData.siteName!.isNotEmpty)
Text(
'Site: ${locationData.siteName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.buildingName != null && locationData.buildingName!.isNotEmpty)
Text(
'Building: ${locationData.buildingName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.floorName != null && locationData.floorName!.isNotEmpty)
Text(
'Floor: ${locationData.floorName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.departmentName != null && locationData.departmentName!.isNotEmpty)
Text(
'Department: ${locationData.departmentName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.roomName != null && locationData.roomName!.isNotEmpty)
Text(
'Room: ${locationData.roomName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
],
), ),
], ),
), if (locationData.buildingName != null && locationData.buildingName!.isNotEmpty)
Text(
'Building: ${locationData.buildingName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.floorName != null && locationData.floorName!.isNotEmpty)
Text(
'Floor: ${locationData.floorName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.departmentName != null && locationData.departmentName!.isNotEmpty)
Text(
'Department: ${locationData.departmentName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.roomName != null && locationData.roomName!.isNotEmpty)
Text(
'Room: ${locationData.roomName}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
if (locationData.locationDescription != null && locationData.locationDescription!.isNotEmpty)
Text(
'Location Description: ${locationData.locationDescription}',
style: AppTextStyles.bodyText.copyWith(
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
softWrap: true,
maxLines: null,
),
], ],
).paddingOnly(start: 16, end: 16, top: 16, bottom: 16), ).paddingOnly(start: 16, end: 16, top: 16, bottom: 16),
], ],

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

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

@ -1,12 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/controllers/providers/api/user_provider.dart';
import 'package:test_sa/dashboard_latest/dashboard_provider.dart';
import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/modules/cleaner_module/models/one_time_task_data.dart'; import 'package:test_sa/modules/cleaner_module/models/one_time_task_data.dart';
import 'package:test_sa/modules/cleaner_module/provider/cleaner_provider.dart'; import 'package:test_sa/modules/cleaner_module/provider/cleaner_provider.dart';
import 'package:test_sa/modules/cleaner_module/widgets/cleaner_checklist_widget.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/modules/cleaner_module/pages/one_time_task_preview_screen.dart';
import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart'; import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart';
import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/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_filled_button.dart';
@ -16,12 +21,16 @@ 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/app_loading.dart';
import 'package:test_sa/views/widgets/loaders/no_data_found.dart'; import 'package:test_sa/views/widgets/loaders/no_data_found.dart';
import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart'; import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart';
import 'package:test_sa/views/widgets/timer/app_timer.dart';
import 'package:test_sa/models/timer_model.dart';
import 'package:test_sa/views/widgets/requests/request_status.dart';
class OneTimeTaskView extends StatefulWidget { class OneTimeTaskView extends StatefulWidget {
static const String routeName = "/one_time_task"; static const String routeName = "/one_time_task";
final int taskId; final int taskId;
final OneTimeTaskData? initialData;
const OneTimeTaskView({Key? key, required this.taskId}) : super(key: key); const OneTimeTaskView({Key? key, required this.taskId, this.initialData}) : super(key: key);
@override @override
State<OneTimeTaskView> createState() => _OneTimeTaskViewState(); State<OneTimeTaskView> createState() => _OneTimeTaskViewState();
@ -31,11 +40,25 @@ class _OneTimeTaskViewState extends State<OneTimeTaskView> {
OneTimeTaskData? _taskData; OneTimeTaskData? _taskData;
bool _isLoading = false; bool _isLoading = false;
DateTime? _timerStart; DateTime? _timerStart;
List<ChecklistItem>? _checklistItems; // Cache checklist items
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadTaskData(); // Use initial data if provided, otherwise load from API
if (widget.initialData != null) {
_taskData = widget.initialData;
// Initialize imageRooms as empty list if null
_taskData?.imageRooms ??= [];
// Start timer automatically if task is in progress
if (_taskData?.statusValue == 1 && _taskData?.startTime != null) {
_timerStart = DateTime.parse(_taskData!.startTime!);
}
} else {
_loadTaskData();
}
} }
Future<void> _loadTaskData() async { Future<void> _loadTaskData() async {
@ -49,6 +72,19 @@ class _OneTimeTaskViewState extends State<OneTimeTaskView> {
if (result['success'] && result['data'] != null) { if (result['success'] && result['data'] != null) {
setState(() { setState(() {
_taskData = OneTimeTaskData.fromJson(result['data']); _taskData = OneTimeTaskData.fromJson(result['data']);
// Initialize imageRooms as empty list if null
_taskData?.imageRooms ??= [];
// Convert attachments from API to imageRooms format for display
if ((_taskData?.imageRooms?.isEmpty ?? true) &&
(_taskData?.attachments != null && _taskData!.attachments!.isNotEmpty)) {
_taskData!.imageRooms = _taskData!.attachments!
.map((attachment) => URLs.getFileUrl(attachment.name ?? '') ?? '')
.where((url) => url.isNotEmpty)
.toList();
}
_isLoading = false; _isLoading = false;
// Start timer automatically if task is in progress // Start timer automatically if task is in progress
@ -80,12 +116,18 @@ class _OneTimeTaskViewState extends State<OneTimeTaskView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: DefaultAppBar( appBar: DefaultAppBar(
title: 'One Time Task', title: 'One Time Task'.addTranslation,
onWillPopScope: isClosed onWillPopScope: null,
? null onBackPress: (){
: () { WidgetsBinding.instance.addPostFrameCallback((_) {
_showExitDialog(); UserProvider userProvider = Provider.of<UserProvider>(context, listen: false);
}, DashBoardProvider dashBoardProvider = Provider.of<DashBoardProvider>(context, listen: false);
dashBoardProvider.getDashBoardCount(usersType:userProvider.user!.type!);
dashBoardProvider.resetRequestDataList();
dashBoardProvider.getRequestDetail(usersType: userProvider.user!.type!, status: dashBoardProvider.tabs[dashBoardProvider.currentListIndex].tag);
});
Navigator.pop(context);
},
), ),
body: _isLoading body: _isLoading
? const ALoading() ? const ALoading()
@ -110,10 +152,10 @@ class _OneTimeTaskViewState extends State<OneTimeTaskView> {
_updateInstructions(value); _updateInstructions(value);
}); });
}, },
capturedImageBase64: _taskData!.imageRoom, capturedImages: _taskData!.imageRooms ?? [],
onImageCaptured: (imageBase64) { onImagesChanged: (images) {
setState(() { setState(() {
_taskData!.imageRoom = imageBase64; _taskData!.imageRooms = images;
}); });
}, },
), ),
@ -124,22 +166,10 @@ class _OneTimeTaskViewState extends State<OneTimeTaskView> {
if (!isClosed) ...[ if (!isClosed) ...[
FooterActionButton.footerContainer( FooterActionButton.footerContainer(
context: context, context: context,
child: Row( child: AppFilledButton(
mainAxisAlignment: MainAxisAlignment.spaceAround, label: context.translation.complete,
children: [ buttonColor: AppColor.primary10,
AppFilledButton( onPressed: _onComplete,
label: context.translation.save,
buttonColor: context.isDark ? AppColor.neutral20 : AppColor.white60,
textColor: context.isDark ? Colors.white : AppColor.black10,
onPressed: () => Navigator.pop(context),
).expanded,
12.width,
AppFilledButton(
label: context.translation.complete,
buttonColor: AppColor.primary10,
onPressed: _onComplete,
).expanded,
],
), ),
), ),
] ]
@ -149,7 +179,7 @@ class _OneTimeTaskViewState extends State<OneTimeTaskView> {
: NoDataFound(message: context.translation.noDataFound).center, : NoDataFound(message: context.translation.noDataFound).center,
).handlePopScope( ).handlePopScope(
cxt: context, cxt: context,
showPopUp: !isClosed, showPopUp: false,
onSave: () { onSave: () {
Navigator.pop(context); Navigator.pop(context);
}, },
@ -166,25 +196,19 @@ class _OneTimeTaskViewState extends State<OneTimeTaskView> {
children: [ children: [
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (_taskData?.statusName != null) if (_taskData?.statusName != null)
Container( StatusLabel(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), radius: 4,
decoration: BoxDecoration( label: _taskData!.statusName!,
color: _getStatusColor().withOpacity(0.1), textColor: AppColor.getHistoryLogStatusTextColorByName(_taskData!.statusName!),
borderRadius: BorderRadius.circular(4), backgroundColor: AppColor.getHistoryLogStatusColorByName(_taskData!.statusName!),
),
child: Text(
_taskData!.statusName!,
style: AppTextStyles.bodyText.copyWith(
color: _getStatusColor(),
fontWeight: FontWeight.w600,
),
),
), ),
if (_taskData?.createdDate != null) if (_taskData?.createdDate != null)
Text( Text(
_formatDate(_taskData!.createdDate!), _taskData!.createdDate!.toString().toServiceRequestCardFormat,
textAlign: TextAlign.end,
style: AppTextStyles.tinyFont.copyWith( style: AppTextStyles.tinyFont.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
), ),
@ -208,47 +232,24 @@ class _OneTimeTaskViewState extends State<OneTimeTaskView> {
), ),
), ),
8.height, 8.height,
if (_taskData?.siteName != null) if (_taskData?.siteName != null) _buildInfoRow('Site', _taskData!.siteName!),
_buildInfoRow('Site', _taskData!.siteName!), if (_taskData?.buildingName != null) _buildInfoRow('Building', _taskData!.buildingName!),
if (_taskData?.buildingName != null) if (_taskData?.floorName != null) _buildInfoRow('Floor', _taskData!.floorName!),
_buildInfoRow('Building', _taskData!.buildingName!), if (_taskData?.departmentName != null) _buildInfoRow('Department', _taskData!.departmentName!),
if (_taskData?.floorName != null) if (_taskData?.roomName != null) _buildInfoRow('Room', _taskData!.roomName!),
_buildInfoRow('Floor', _taskData!.floorName!), if (_taskData?.locationDescription != null) _buildInfoRow('Location', _taskData!.locationDescription!),
if (_taskData?.departmentName != null) // Timer display using AppTimer widget
_buildInfoRow('Department', _taskData!.departmentName!),
if (_taskData?.roomName != null)
_buildInfoRow('Room', _taskData!.roomName!),
if (_taskData?.locationDescription != null)
_buildInfoRow('Location', _taskData!.locationDescription!),
// Timer display
if (_timerStart != null && !isClosed) ...[ if (_timerStart != null && !isClosed) ...[
12.height, 12.height,
Container( AppTimer(
padding: const EdgeInsets.all(12), label: 'Working Time',
decoration: BoxDecoration( timer: TimerModel(
color: AppColor.primary10.withOpacity(0.1), startAt: _timerStart,
borderRadius: BorderRadius.circular(8), endAt: null,
border: Border.all(color: AppColor.primary10), durationInSecond: 0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Working Time:',
style: AppTextStyles.bodyText.copyWith(
fontWeight: FontWeight.w600,
color: context.isDark ? AppColor.neutral10 : AppColor.neutral120,
),
),
Text(
_formatDuration(DateTime.now().difference(_timerStart!)),
style: AppTextStyles.heading4.copyWith(
color: AppColor.primary10,
fontWeight: FontWeight.bold,
),
),
],
), ),
enabled: false, // Disable controls, just show the timer
showTimer: true,
), ),
], ],
], ],
@ -270,15 +271,35 @@ class _OneTimeTaskViewState extends State<OneTimeTaskView> {
} }
List<ChecklistItem> _convertInstructionsToChecklist() { List<ChecklistItem> _convertInstructionsToChecklist() {
return _taskData?.instructions?.map((instruction) { // If checklist items are already created, reuse them
return ChecklistItem( if (_checklistItems != null) {
id: instruction.id ?? 0, // Update the existing items with latest data from instructions
title: instruction.instructionText ?? '', for (var item in _checklistItems!) {
isChecked: instruction.isDone ?? false, final instruction = _taskData?.instructions?.firstWhere((i) => i.id == item.id, orElse: () => Instruction());
isMandatory: instruction.isRequired ?? false, if (instruction?.id != null) {
comment: instruction.comment ?? '', item.isChecked = instruction!.isDone ?? false;
); // Only update comment if it's different to avoid cursor jumping
}).toList() ?? []; if (item.comment != (instruction.comment ?? '')) {
item.comment = instruction.comment ?? '';
}
}
}
return _checklistItems!;
}
// Create new checklist items only on first call
_checklistItems = _taskData?.instructions?.map((instruction) {
return ChecklistItem(
id: instruction.id ?? 0,
title: instruction.instructionText ?? '',
isChecked: instruction.isDone ?? false,
isMandatory: instruction.isRequired ?? false,
comment: instruction.comment ?? '',
);
}).toList() ??
[];
return _checklistItems!;
} }
void _updateInstructions(List<ChecklistItem> checklist) { void _updateInstructions(List<ChecklistItem> checklist) {
@ -295,21 +316,24 @@ class _OneTimeTaskViewState extends State<OneTimeTaskView> {
} }
void _onComplete() { void _onComplete() {
// Validate mandatory items // Validate mandatory checklist items
final uncheckedMandatory = _taskData?.instructions?.where((item) => final uncheckedMandatory = _taskData?.instructions?.where((item) => (item.isRequired ?? false) && !(item.isDone ?? false)).toList() ?? [];
(item.isRequired ?? false) && !(item.isDone ?? false)
).toList() ?? [];
if (uncheckedMandatory.isNotEmpty) { if (uncheckedMandatory.isNotEmpty) {
_showErrorDialog(context, "Please complete all mandatory tasks (marked with *)"); _showErrorDialog(context, "Please complete all mandatory tasks (marked with *)");
return; return;
} }
// Show preview dialog // Validate that at least one image is captured
_showPreviewDialog(); final capturedImages = _taskData?.imageRooms ?? [];
} print('DEBUG: Captured images count: ${capturedImages.length}');
print('DEBUG: Images: $capturedImages');
if (capturedImages.isEmpty) {
_showErrorDialog(context, "Please capture at least one image before completing");
return;
}
void _showPreviewDialog() {
// Calculate working hours // Calculate working hours
double totalWorkingHours = 0; double totalWorkingHours = 0;
if (_timerStart != null) { if (_timerStart != null) {
@ -317,28 +341,21 @@ class _OneTimeTaskViewState extends State<OneTimeTaskView> {
totalWorkingHours = duration.inSeconds / 3600.0; totalWorkingHours = duration.inSeconds / 3600.0;
} }
CleanerPreviewDialog.show( // Navigate to preview screen
context, Navigator.of(context).push(
model: _convertToRecurrentWoData(), MaterialPageRoute(
totalWorkingHours: totalWorkingHours, builder: (context) => OneTimeTaskPreviewScreen(
onConfirm: _performComplete, taskData: _taskData!,
totalWorkingHours: totalWorkingHours,
onConfirm: () {
Navigator.pop(context); // Close preview screen
_performComplete(); // Proceed with completion
},
),
),
); );
} }
// Convert OneTimeTaskData to RecurrentWoData for preview dialog compatibility
dynamic _convertToRecurrentWoData() {
return {
'siteName': _taskData?.siteName,
'buildingName': _taskData?.buildingName,
'floorName': _taskData?.floorName,
'departmentName': _taskData?.departmentName,
'roomName': _taskData?.roomName,
'locationDescription': _taskData?.locationDescription,
'taskNo': 'OT-${_taskData?.id}',
'title': _taskData?.oneTimeSetting?.taskTypeName,
};
}
Future<void> _performComplete() async { Future<void> _performComplete() async {
showDialog( showDialog(
context: context, context: context,
@ -348,20 +365,33 @@ class _OneTimeTaskViewState extends State<OneTimeTaskView> {
try { try {
final instructionsPayload = _taskData?.instructions?.map((instruction) { final instructionsPayload = _taskData?.instructions?.map((instruction) {
return { return {
'id': instruction.id, 'id': instruction.id,
'instructionTextId': instruction.instructionTextId, 'isDone': instruction.isDone,
'instructionText': instruction.instructionText, 'comment': instruction.comment,
'isRequired': instruction.isRequired, };
'isDone': instruction.isDone, }).toList() ??
'comment': instruction.comment, [];
};
}).toList() ?? []; // Convert images to attachments format
final attachmentsPayload = _taskData?.imageRooms?.map((imageString) {
// imageString format: "filename|base64data"
final parts = imageString.split('|');
final originalName = parts.isNotEmpty ? parts[0] : 'image.jpg';
final base64Data = parts.length > 1 ? parts[1] : imageString;
return {
'id': 0,
'name': "$originalName|$base64Data", // Send full formatted string
'originalName': originalName,
};
}).toList() ??
[];
final result = await CleanerProvider().completeOneTimeTask( final result = await CleanerProvider().completeOneTimeTask(
id: _taskData!.id!, id: _taskData!.id!,
instructions: instructionsPayload, instructions: instructionsPayload,
imageRoom: _taskData?.imageRoom, attachments: attachmentsPayload,
); );
if (mounted) Navigator.pop(context); // Close loading dialog if (mounted) Navigator.pop(context); // Close loading dialog
@ -387,20 +417,6 @@ class _OneTimeTaskViewState extends State<OneTimeTaskView> {
} }
} }
void _showExitDialog() {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: 'Do you want to exit without completing?',
title: 'Confirm',
onTap: () {
Navigator.pop(context); // Close dialog
Navigator.pop(context); // Exit screen
},
),
);
}
void _showErrorDialog(BuildContext context, String message) { void _showErrorDialog(BuildContext context, String message) {
showDialog( showDialog(
context: context, context: context,
@ -412,26 +428,14 @@ class _OneTimeTaskViewState extends State<OneTimeTaskView> {
); );
} }
Color _getStatusColor() { @override
if (_taskData?.statusValue == 2) { void dispose() {
return Colors.green; // Dispose all checklist item controllers
} if (_checklistItems != null) {
return AppColor.primary10; for (var item in _checklistItems!) {
} item.dispose();
}
String _formatDate(String dateStr) {
try {
final date = DateTime.parse(dateStr);
return '${date.day}/${date.month}/${date.year}';
} catch (e) {
return dateStr;
} }
} super.dispose();
String _formatDuration(Duration duration) {
final hours = duration.inHours;
final minutes = duration.inMinutes.remainder(60);
final seconds = duration.inSeconds.remainder(60);
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
} }
} }

@ -308,7 +308,7 @@ class CleanerProvider extends ChangeNotifier {
Response response; Response response;
try { try {
response = await ApiManager.instance.get( response = await ApiManager.instance.get(
'${URLs.getRequestById}/$id', '${URLs.getRequestById}?id=$id',
); );
// Parse the response body // Parse the response body
@ -336,6 +336,7 @@ class CleanerProvider extends ChangeNotifier {
"data": null, "data": null,
"message": "Failed to retrieve task. Please check your connection and try again.", "message": "Failed to retrieve task. Please check your connection and try again.",
}; };
} }
} }
@ -343,7 +344,7 @@ class CleanerProvider extends ChangeNotifier {
Future<Map<String, dynamic>> completeOneTimeTask({ Future<Map<String, dynamic>> completeOneTimeTask({
required int id, required int id,
required List<Map<String, dynamic>> instructions, required List<Map<String, dynamic>> instructions,
String? imageRoom, List<Map<String, dynamic>>? attachments,
}) async { }) async {
Response response; Response response;
try { try {
@ -352,7 +353,7 @@ class CleanerProvider extends ChangeNotifier {
body: { body: {
"id": id, "id": id,
"instructions": instructions, "instructions": instructions,
"imageRoom": imageRoom, "attachments": attachments ?? [],
}, },
showToast: false, showToast: false,
); );

@ -1,83 +0,0 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:http/http.dart';
import 'package:test_sa/controllers/api_routes/api_manager.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/models/new_models/building.dart';
class FmsBuildingProvider extends ChangeNotifier {
//reset provider data
void reset() {
_buildings = null;
_stateCode = null;
}
// state code of current request to defied error message
int? _stateCode;
int? get stateCode => _stateCode;
// contain buildings data
List<Building>? _buildings;
List<Building>? get buildings => _buildings;
// loading state
bool _loading = false;
bool get isLoading => _loading;
set isLoading(bool isLoading) {
_loading = isLoading;
notifyListeners();
}
/// Get buildings by site ID
Future<int> getBuildingsBySiteId(int siteId) async {
if (_loading == true) return -2;
_loading = true;
notifyListeners();
Response response;
try {
response = await ApiManager.instance.post(
URLs.getCustomerBuildings,
body: {
"siteId": siteId,
},
);
_stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) {
// client's request was successfully received
List buildingsListJson = json.decode(response.body)["data"];
_buildings = buildingsListJson.map((building) => Building.fromJson(building)).toList();
}
_loading = false;
notifyListeners();
return response.statusCode;
} catch (error) {
_loading = false;
_stateCode = -1;
notifyListeners();
return -1;
}
}
/// Get buildings list directly without provider state
Future<List<Building>> getBuildingsListBySiteId(int siteId) async {
Response response;
try {
response = await ApiManager.instance.post(
URLs.getCustomerBuildings,
body: {
"siteId": siteId,
},
);
if (response.statusCode >= 200 && response.statusCode < 300) {
List buildingsListJson = json.decode(response.body)["data"];
return buildingsListJson.map((building) => Building.fromJson(building)).toList();
}
return [];
} catch (error) {
return [];
}
}
}

@ -1,85 +0,0 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:http/http.dart';
import 'package:test_sa/controllers/api_routes/api_manager.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/models/new_models/floor.dart';
class FmsFloorProvider extends ChangeNotifier {
//reset provider data
void reset() {
_floors = null;
_stateCode = null;
}
// state code of current request to defied error message
int? _stateCode;
int? get stateCode => _stateCode;
// contain floors data
List<Floor>? _floors;
List<Floor>? get floors => _floors;
// loading state
bool _loading = false;
bool get isLoading => _loading;
set isLoading(bool isLoading) {
_loading = isLoading;
notifyListeners();
}
/// Get floors by site and building ID
Future<int> getFloorsBySiteAndBuilding(int siteId, int buildingId) async {
if (_loading == true) return -2;
_loading = true;
notifyListeners();
Response response;
try {
response = await ApiManager.instance.post(
URLs.getCustomerFloors,
body: {
"siteId": siteId,
"buildingId": buildingId,
},
);
_stateCode = response.statusCode;
if (response.statusCode >= 200 && response.statusCode < 300) {
// client's request was successfully received
List floorsListJson = json.decode(response.body)["data"];
_floors = floorsListJson.map((floor) => Floor.fromJson(floor)).toList();
}
_loading = false;
notifyListeners();
return response.statusCode;
} catch (error) {
_loading = false;
_stateCode = -1;
notifyListeners();
return -1;
}
}
/// Get floors list directly without provider state
Future<List<Floor>> getFloorsListBySiteAndBuilding(int siteId, int buildingId) async {
Response response;
try {
response = await ApiManager.instance.post(
URLs.getCustomerFloors,
body: {
"siteId": siteId,
"buildingId": buildingId,
},
);
if (response.statusCode >= 200 && response.statusCode < 300) {
List floorsListJson = json.decode(response.body)["data"];
return floorsListJson.map((floor) => Floor.fromJson(floor)).toList();
}
return [];
} catch (error) {
return [];
}
}
}

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

@ -1,10 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:test_sa/controllers/api_routes/urls.dart';
import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart'; import 'package:test_sa/extensions/string_extensions.dart';
@ -13,15 +9,24 @@ import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/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/app_text_form_field.dart';
import 'package:test_sa/views/widgets/buttons/app_back_button.dart'; import 'package:test_sa/views/widgets/buttons/app_back_button.dart';
import 'package:test_sa/modules/cleaner_module/widgets/multi_image_picker.dart';
import 'package:test_sa/views/widgets/images/files_list.dart';
class CleanerChecklistWidget extends StatefulWidget { class CleanerChecklistWidget extends StatefulWidget {
final List<ChecklistItem> checklistItems; final List<ChecklistItem> checklistItems;
final Function(List<ChecklistItem>)? onChecklistChanged; final Function(List<ChecklistItem>)? onChecklistChanged;
final String? capturedImageBase64; final List<String> capturedImages;
final Function(String?)? onImageCaptured; final Function(List<String>)? onImagesChanged;
bool isClosed; final bool isClosed;
CleanerChecklistWidget({Key? key, required this.checklistItems, this.onChecklistChanged, this.capturedImageBase64, this.onImageCaptured, this.isClosed = false}) : super(key: key); const CleanerChecklistWidget({
Key? key,
required this.checklistItems,
this.onChecklistChanged,
required this.capturedImages,
this.onImagesChanged,
this.isClosed = false
}) : super(key: key);
@override @override
State<CleanerChecklistWidget> createState() => _CleanerChecklistWidgetState(); State<CleanerChecklistWidget> createState() => _CleanerChecklistWidgetState();
@ -191,11 +196,23 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Row(
'Captured Image'.addTranslation, mainAxisAlignment: MainAxisAlignment.spaceBetween,
style: AppTextStyles.heading6.copyWith( children: [
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, Text(
), 'Captured Image'.addTranslation,
style: AppTextStyles.heading6.copyWith(
color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
),
),
// Text(
// '(${widget.capturedImages.length}/$maxImages)',
// style: AppTextStyles.bodyText.copyWith(
// color: context.isDark ? AppColor.neutral30 : AppColor.neutral50,
// fontWeight: FontWeight.w600,
// ),
// ),
],
), ),
8.height, 8.height,
_buildImageWidget(context), _buildImageWidget(context),
@ -203,205 +220,148 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
).toShadowContainer(context); ).toShadowContainer(context);
} }
static const int maxImages = 2;
/// Build the image widget with loading indicator for network images /// Build the image widget with loading indicator for network images
Widget _buildImageWidget(BuildContext context) { Widget _buildImageWidget(BuildContext context) {
if (widget.capturedImageBase64 == null || widget.capturedImageBase64!.isEmpty) { if (widget.capturedImages.isEmpty) {
// Show camera icon when no image // Show add image button when no image
return Container( return GestureDetector(
height: 200, onTap: widget.isClosed ? null : () async {
width: double.infinity, // Only allow camera - pick one image at a time
decoration: BoxDecoration( final pickedFile = await ImagePicker().pickImage(
color: AppColor.fieldBgColor(context), source: ImageSource.camera,
borderRadius: BorderRadius.circular(8), imageQuality: 70,
), maxWidth: 800,
child: Icon( maxHeight: 800,
Icons.camera_alt, );
size: 48,
color: context.isDark ? AppColor.white10 : AppColor.black10,
),
).onPress(() async {
final pickedFile = await ImagePicker().pickImage(source: ImageSource.camera, imageQuality: 70, maxWidth: 800, maxHeight: 800);
if (pickedFile != null) {
// Convert image to Base64 with filename prefix (same format as CM request)
final bytes = await pickedFile.readAsBytes();
final base64Image = base64Encode(bytes);
final fileName = pickedFile.path.split("/").last;
final formattedImage = "$fileName|$base64Image";
// Update parent with captured image
widget.onImageCaptured?.call(formattedImage);
}
});
}
Widget imageWidget; if (pickedFile != null) {
final bytes = await pickedFile.readAsBytes();
final base64Image = base64Encode(bytes);
final fileName = pickedFile.path.split("/").last;
final formattedImage = "$fileName|$base64Image";
final imageString = widget.capturedImageBase64!; // Add to existing images (max 2)
final updatedImages = List<String>.from(widget.capturedImages);
updatedImages.add(formattedImage);
// First check if it's base64 encoded (contains | separator) widget.onImagesChanged?.call(updatedImages);
if (imageString.contains('|')) {
// Format: "filename|base64data"
try {
final base64Data = imageString.split('|').last;
imageWidget = Image.memory(base64Decode(base64Data), fit: BoxFit.cover);
return Stack(
alignment: AlignmentDirectional.topEnd,
children: [
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: Image(image: getImageObject(itemIndex))).center,
InteractiveViewer(child: imageWidget).center,
const ABackButton(),
],
),
),
),
),
);
}),
_delete(),
],
);
} 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 Stack(
alignment: AlignmentDirectional.topEnd,
children: [
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: Image(image: getImageObject(itemIndex))).center,
InteractiveViewer(child: imageWidget).center,
const ABackButton(),
],
),
),
),
),
);
}),
_delete(),
],
);
} 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);
}, },
); child: Container(
return Stack( height: 120,
alignment: AlignmentDirectional.topEnd, width: 120,
children: [ decoration: BoxDecoration(
Container( color: AppColor.fieldBgColor(context),
height: 200, borderRadius: BorderRadius.circular(8),
width: double.infinity, border: Border.all(
decoration: BoxDecoration( color: context.isDark ? AppColor.neutral20 : AppColor.neutral30,
color: AppColor.fieldBgColor(context), width: 2,
borderRadius: BorderRadius.circular(8), style: BorderStyle.solid,
), ),
child: imageWidget) ),
.onPress(() { child: Column(
Navigator.of(context).push( mainAxisAlignment: MainAxisAlignment.center,
MaterialPageRoute( children: [
builder: (_) => Scaffold( Icon(
body: SafeArea( Icons.add_photo_alternate,
child: Stack( size: 32,
children: [ color: context.isDark ? AppColor.white10 : AppColor.black10,
// InteractiveViewer(child: Image(image: getImageObject(itemIndex))).center, ),
InteractiveViewer(child: imageWidget).center, 4.height,
const ABackButton(), Text(
], 'Add Image',
), style: AppTextStyles.tinyFont.copyWith(
), color: context.isDark ? AppColor.white10 : AppColor.black10,
), ),
), ),
); ],
}), ),
_delete(), ),
],
); );
} }
return _buildCameraIcon(context); // Check if images are URLs from API (like ServiceRequestDetailView does)
} final bool hasApiImages = widget.capturedImages.any((img) =>
img.startsWith('http://') || img.startsWith('https://'));
/// Build camera icon widget // If images are from API (URLs), use FilesList widget like ServiceRequestDetailView
Widget _buildCameraIcon(BuildContext context) { if (hasApiImages && widget.isClosed) {
return Container( return FilesList(
height: 200, images: widget.capturedImages,
width: double.infinity, showAsListView: false,
decoration: BoxDecoration( );
color: AppColor.fieldBgColor(context), }
borderRadius: BorderRadius.circular(8),
), // Otherwise, use MultiImagePicker for base64 images or when editing
child: Icon( return MultiImagePicker(
Icons.camera_alt, imageUrls: widget.capturedImages,
size: 48, enabled: !widget.isClosed,
color: context.isDark ? AppColor.white10 : AppColor.black10, maxImages: 2, // Enforce max 2 images
), onImageTap: (index, imageUrl) {
// Handle image tap - show in full screen
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: Stack(
children: [
InteractiveViewer(
child: _buildImageForViewing(imageUrl),
).center,
const ABackButton(),
],
),
),
),
),
);
},
onDelete: (index) {
// Handle image delete
if (index >= 0) {
setState(() {
widget.capturedImages.removeAt(index);
});
widget.onImagesChanged?.call(widget.capturedImages);
}
},
onAdd: (imageUrl) {
// Handle new image addition
setState(() {
widget.capturedImages.add(imageUrl);
});
widget.onImagesChanged?.call(widget.capturedImages);
},
); );
} }
/// Check if the URL is a local file path /// Build image widget for viewing based on source type
/// Same logic as multi_image_picker_item.dart Widget _buildImageForViewing(String imageUrl) {
bool _isLocalUrl(String url) { try {
if (url.isEmpty) return false; // Check if it's a URL from API
return url.startsWith("/") || url.startsWith("file://") || (url.length > 1 && url.substring(1).startsWith(':\\')); if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
return Image.network(
imageUrl,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.broken_image, size: 48, color: Colors.white);
},
);
}
// Otherwise, it's base64 encoded
final parts = imageUrl.split('|');
final base64Data = parts.length > 1 ? parts[1] : imageUrl;
return Image.memory(
base64Decode(base64Data),
fit: BoxFit.contain,
);
} catch (e) {
return const Icon(Icons.broken_image, size: 48, color: Colors.white);
}
} }
@override @override
@ -409,26 +369,6 @@ class _CleanerChecklistWidgetState extends State<CleanerChecklistWidget> {
// Note: Don't dispose controllers here as they're managed by ChecklistItem // Note: Don't dispose controllers here as they're managed by ChecklistItem
super.dispose(); super.dispose();
} }
Widget _delete() {
if (widget.isClosed) return const SizedBox();
return Container(
height: 45.toScreenHeight,
width: 45.toScreenHeight,
margin: const EdgeInsets.only(top: 4, right: 4),
decoration: BoxDecoration(
color: (context.isDark ? AppColor.neutral20 : AppColor.neutral40).withValues(alpha: 0.4),
border: Border.all(
color: (context.isDark ? AppColor.neutral20 : AppColor.neutral40).withValues(alpha: 0.7),
),
borderRadius: BorderRadius.circular(30),
//boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10)],
),
child: const Icon(Icons.delete_rounded, color: Color(0xffF63939)))
.onPress(() {
widget.onImageCaptured?.call(null);
});
}
} }
/// Model class for checklist items /// Model class for checklist items
@ -446,10 +386,7 @@ class ChecklistItem {
required this.isChecked, required this.isChecked,
required this.comment, required this.comment,
this.isMandatory = false, this.isMandatory = false,
}) : commentController = TextEditingController(text: comment) { }) : commentController = TextEditingController(text: comment);
// Update the controller text whenever comment changes
commentController.text = comment;
}
// Convert to JSON for API submission // Convert to JSON for API submission
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {

@ -1,457 +0,0 @@
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] == ':');
}
}

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

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

@ -6,8 +6,6 @@ import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/models/ppm/recurrent_wo.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/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/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_filled_button.dart';
import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart'; import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart';
@ -35,21 +33,11 @@ class RecurrentWorkOrderView extends StatefulWidget {
class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> { class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> {
AllRequestsProvider? allRequestsProvider; AllRequestsProvider? allRequestsProvider;
DateTime? _housekeeperTimerStart;
@override @override
void initState() { void initState() {
allRequestsProvider = Provider.of<AllRequestsProvider>(context, listen: false); allRequestsProvider = Provider.of<AllRequestsProvider>(context, listen: false);
Provider.of<AllRequestsProvider>(context, listen: false).getRecurrentWoById(id: widget.taskId!).then((_) { Provider.of<AllRequestsProvider>(context, listen: false).getRecurrentWoById(id: widget.taskId!);
// 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(); super.initState();
} }
@ -64,14 +52,13 @@ class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<AllRequestsProvider>(builder: (context, requestProvider, child) { return Consumer<AllRequestsProvider>(builder: (context, requestProvider, child) {
isClosed = requestProvider.recurrentWoData?.status?.value == 1; isClosed = requestProvider.recurrentWoData?.status?.value == 1;
bool isHouseKeeper = requestProvider.recurrentWoData!.planRecurrentTaskCheckLists != null && requestProvider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty;
return Scaffold( return Scaffold(
appBar: DefaultAppBar( appBar: DefaultAppBar(
title: context.translation.recurrentWo, title: context.translation.recurrentWo,
onWillPopScope: isClosed onWillPopScope: isClosed
? null ? null
: () { : () {
_updateTask(context: context, status: 0, isHouseKeeper: isHouseKeeper); _updateTask(context: context, status: 0);
}, },
), ),
body: allRequestsProvider!.isLoading body: allRequestsProvider!.isLoading
@ -89,8 +76,8 @@ class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> {
RecurrentTaskInfoWidget( RecurrentTaskInfoWidget(
isClosed: isClosed, isClosed: isClosed,
model: requestProvider.recurrentWoData, model: requestProvider.recurrentWoData,
isHouseKeeper: isHouseKeeper, isHouseKeeper: false,
timerStartTime: _housekeeperTimerStart, timerStartTime: null,
), ),
IgnorePointer( IgnorePointer(
ignoring: isClosed, ignoring: isClosed,
@ -103,24 +90,6 @@ class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> {
) )
: const SizedBox(), : const SizedBox(),
), ),
16.height,
isHouseKeeper
? CleanerChecklistWidget(
isClosed: isClosed,
checklistItems: requestProvider.recurrentWoData!.planRecurrentTaskCheckLists ?? [],
onChecklistChanged: (value) {
setState(() {
requestProvider.recurrentWoData!.planRecurrentTaskCheckLists = value;
});
},
capturedImageBase64: requestProvider.recurrentWoData!.imageRoom,
onImageCaptured: (imageBase64) {
setState(() {
requestProvider.recurrentWoData!.imageRoom = imageBase64;
});
},
)
: const SizedBox(),
], ],
), ),
).expanded, ).expanded,
@ -134,13 +103,13 @@ class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> {
label: context.translation.save, label: context.translation.save,
buttonColor: context.isDark ? AppColor.neutral20 : AppColor.white60, buttonColor: context.isDark ? AppColor.neutral20 : AppColor.white60,
textColor: context.isDark ? Colors.white : AppColor.black10, textColor: context.isDark ? Colors.white : AppColor.black10,
onPressed: () => _updateTask(context: context, status: 0, isHouseKeeper: isHouseKeeper), onPressed: () => _updateTask(context: context, status: 0),
).expanded, ).expanded,
12.width, 12.width,
AppFilledButton( AppFilledButton(
label: context.translation.complete, label: context.translation.complete,
buttonColor: AppColor.primary10, buttonColor: AppColor.primary10,
onPressed: () => _updateTask(context: context, status: 1, isHouseKeeper: isHouseKeeper), onPressed: () => _updateTask(context: context, status: 1),
).expanded, ).expanded,
], ],
), ),
@ -154,77 +123,45 @@ class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> {
cxt: context, cxt: context,
showPopUp: !isClosed, showPopUp: !isClosed,
onSave: () { onSave: () {
_updateTask(context: context, status: 0, isHouseKeeper: isHouseKeeper); _updateTask(context: context, status: 0);
}); });
}); });
} }
void _updateTask({required BuildContext context, required int status, required bool isHouseKeeper}) async { void _updateTask({required BuildContext context, required int status}) 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(context, allRequestsProvider.recurrentWoData!)) {
return;
}
}
// For housekeeper: show preview dialog before completing
if (isHouseKeeper && status == 1) {
_showPreviewDialog(context, allRequestsProvider);
return;
}
// Proceed with update // Proceed with update
_performUpdate(context: context, status: status, isHouseKeeper: isHouseKeeper); _performUpdate(context: context, status: status);
} }
void _performUpdate({required BuildContext context, required int status, required bool isHouseKeeper}) async { void _performUpdate({required BuildContext context, required int status}) async {
AllRequestsProvider allRequestsProvider = Provider.of<AllRequestsProvider>(context, listen: false); AllRequestsProvider allRequestsProvider = Provider.of<AllRequestsProvider>(context, listen: false);
if (validate(context: context, model: allRequestsProvider.recurrentWoData!, status: status, isHouseKeeper: isHouseKeeper)) { if (validate(context: context, model: allRequestsProvider.recurrentWoData!, status: status)) {
// For housekeeper: automatically add timer data // Use existing timer logic
if (isHouseKeeper && _housekeeperTimerStart != null) { allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers ?? [];
DateTime endTime = DateTime.now();
int durationInSecond = endTime.difference(_housekeeperTimerStart!).inSeconds;
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( allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add(
PlanRecurrentTaskTimers( PlanRecurrentTaskTimers(
id: 0, id: 0,
startTime: _housekeeperTimerStart!.toIso8601String(), startTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.startAt!.toIso8601String(),
endTime: endTime.toIso8601String(), endTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt?.toIso8601String(),
workingHours: ((durationInSecond) / 60 / 60), 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?.timerModelList?.forEach((timer) {
int durationInSecond = timer.endAt!.difference(timer.startAt!).inSeconds;
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add(
PlanRecurrentTaskTimers(
id: 0,
startTime: timer.startAt!.toIso8601String(),
endTime: timer.endAt?.toIso8601String(),
workingHours: ((durationInSecond) / 60 / 60),
),
);
});
final uniqueTimersMap = <String, PlanRecurrentTaskTimers>{}; final uniqueTimersMap = <String, PlanRecurrentTaskTimers>{};
allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.forEach((timer) { allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.forEach((timer) {
@ -268,38 +205,6 @@ class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> {
} }
} }
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) { void _showErrorDialog(BuildContext context, String message) {
showDialog( showDialog(
context: context, context: context,
@ -312,32 +217,8 @@ class _RecurrentWorkOrderViewState extends State<RecurrentWorkOrderView> {
} }
} }
bool _validateChecklist(BuildContext context, RecurrentWoData model) { bool validate({required BuildContext context, required RecurrentWoData model, required int status}) {
// Check if all mandatory checklist items are checked // Timer validation logic
final uncheckedMandatory = model.planRecurrentTaskCheckLists!.where((item) => item.isMandatory && !item.isChecked).toList();
if (uncheckedMandatory.isNotEmpty) {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
message: "Please complete all mandatory tasks (marked with *)",
title: 'Validation Error',
onTap: () => Navigator.pop(context),
),
);
return false;
}
return true;
}
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; bool isTimerPickerEnable = ApiManager.instance.assetGroup?.enabledEngineerTimer ?? false;
if (isTimerPickerEnable) { if (isTimerPickerEnable) {

@ -23,9 +23,9 @@ class AppBottomNavigationBar extends StatelessWidget {
List<NavItemModel> navItems = [ List<NavItemModel> navItems = [
NavItemModel(0, "overview", context.translation.overview), NavItemModel(0, "overview", context.translation.overview),
NavItemModel(1, "request_icon", context.translation.workOrder), if(!isHouseKeeper) NavItemModel(1, "request_icon", context.translation.workOrder),
if (!commissioningEng && !isHouseKeeper) NavItemModel(2, "add_icon", context.translation.calendar, showLabel: false), if (!commissioningEng && !isHouseKeeper) NavItemModel(2, "add_icon", context.translation.calendar, showLabel: false),
NavItemModel(3, "assets", context.translation.assets), if(!isHouseKeeper) NavItemModel(3, "assets", context.translation.assets),
NavItemModel(4, "contact", context.translation.contact), NavItemModel(4, "contact", context.translation.contact),
]; ];

@ -26,8 +26,6 @@ class RecurrentWoItemView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final user = Provider.of<UserProvider>(context, listen: false).user; final user = Provider.of<UserProvider>(context, listen: false).user;
final settingProvider = Provider.of<SettingProvider>(context, listen: false);
final bool isHouseKeeper = user?.type == UsersTypes.houseKeeper;
if (requestData != null) { if (requestData != null) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -61,16 +59,11 @@ class RecurrentWoItemView extends StatelessWidget {
// '${context.translation.assetSN}: ${request.assetSN}'.bodyText(context),//todo ask ahmed // '${context.translation.assetSN}: ${request.assetSN}'.bodyText(context),//todo ask ahmed
// '${context.translation.code}: ${request.code}'.bodyText(context), // '${context.translation.code}: ${request.code}'.bodyText(context),
'${context.translation.taskNo}: ${requestData!.requestNo}'.bodyText(context), '${context.translation.taskNo}: ${requestData!.requestNo}'.bodyText(context),
if(requestData?.siteName!=null) if (requestData?.siteName != null) '${context.translation.site}: ${requestData!.siteName}'.bodyText(context),
'${context.translation.site}: ${requestData!.siteName}'.bodyText(context), if (requestData!.buildingName != null) '${context.translation.building}: ${requestData!.buildingName}'.bodyText(context),
if(requestData!.buildingName!=null) if (requestData!.departmentName != null) '${context.translation.department}: ${requestData!.departmentName}'.bodyText(context),
'${context.translation.building}: ${requestData!.buildingName}'.bodyText(context), if (requestData!.floorName != null) '${context.translation.floor}: ${requestData!.floorName}'.bodyText(context),
if(requestData!.departmentName!=null) if (requestData!.roomName != null) '${context.translation.room}: ${requestData!.roomName}'.bodyText(context),
'${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, 8.height,
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -85,19 +78,7 @@ class RecurrentWoItemView extends StatelessWidget {
), ),
], ],
).toShadowContainer(context, withShadow: showShadow).onPress(() { ).toShadowContainer(context, withShadow: showShadow).onPress(() {
if (isHouseKeeper && (requestData?.statusValue == 4)) { Navigator.of(context).push(MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: requestData!.id)));
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CleanerSearchDetailsView(
requestId: requestData!.id ?? 0,
taskResult: null,
),
),
);
} else {
Navigator.of(context).push(MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: requestData!.id)));
}
}); });
} }
if (requestDetails != null) { if (requestDetails != null) {
@ -152,17 +133,6 @@ class RecurrentWoItemView extends StatelessWidget {
), ),
], ],
).toShadowContainer(context, withShadow: showShadow).onPress(() async { ).toShadowContainer(context, withShadow: showShadow).onPress(() async {
if (isHouseKeeper && (requestDetails?.statusValue == 4)) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CleanerSearchDetailsView(
requestId: requestDetails!.id ?? 0,
taskResult: null,
),
),
);
}
Navigator.of(context).push(MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: requestDetails!.id))); Navigator.of(context).push(MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: requestDetails!.id)));
}); });
} }

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

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

Loading…
Cancel
Save