diff --git a/lib/dashboard_latest/dashboard_provider.dart b/lib/dashboard_latest/dashboard_provider.dart index 92ecd60e..c104de4f 100644 --- a/lib/dashboard_latest/dashboard_provider.dart +++ b/lib/dashboard_latest/dashboard_provider.dart @@ -418,9 +418,9 @@ class CategoryTabs { if (userType == UsersTypes.houseKeeper) { // tabs.add(CategoryTabs(label: 'Upcoming', tag: 0, id: 0)); // tabs.add(CategoryTabs('Open', 1)); - tabs.add(CategoryTabs(label: 'UnAssigned', tag: 1, id: 1)); + // tabs.add(CategoryTabs(label: 'UnAssigned', tag: 1, id: 1)); 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; } diff --git a/lib/dashboard_latest/widgets/progress_fragment.dart b/lib/dashboard_latest/widgets/progress_fragment.dart index ed7443c2..3316617a 100644 --- a/lib/dashboard_latest/widgets/progress_fragment.dart +++ b/lib/dashboard_latest/widgets/progress_fragment.dart @@ -193,7 +193,7 @@ class ProgressFragment extends StatelessWidget { total = (snapshot.dashboardCount?.data?.countClose ?? 0) + (snapshot.dashboardCount?.data?.countInprogress ?? 0) + (snapshot.dashboardCount?.data?.countOpen ?? 0); chartData = [ ChartData('In Progress', snapshot.dashboardCount?.data?.countInprogress?.toDouble() ?? 0.0, AppColor.orange70), - ChartData('Closed', snapshot.dashboardCount?.data?.countClose?.toDouble() ?? 0.0, AppColor.green70), + ChartData('Completed', snapshot.dashboardCount?.data?.countClose?.toDouble() ?? 0.0, AppColor.green70), // ChartData('Open', snapshot.dashboardCount?.data?.countOpen?.toDouble() ?? 0.0, AppColor.blueStatus(context)), ]; } else { diff --git a/lib/dashboard_latest/widgets/request_category_fragment.dart b/lib/dashboard_latest/widgets/request_category_fragment.dart index 54c7f1a8..2eeb897b 100644 --- a/lib/dashboard_latest/widgets/request_category_fragment.dart +++ b/lib/dashboard_latest/widgets/request_category_fragment.dart @@ -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/extensions/context_extension.dart'; import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/extensions/string_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/models/enums/user_types.dart'; @@ -57,8 +58,8 @@ class RequestCategoryFragment extends StatelessWidget { ? Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - context.translation.allWorkOrder.heading4(context), - 'search'.toSvgAsset(width: 20, height: 20).onPress(() { + 'All Tasks'.addTranslation.heading4(context), + 'add_icon'.toSvgAsset(width: 20, height: 20).onPress(() { Navigator.of(context).pushNamed('/cleaner_search'); }), ], diff --git a/lib/dashboard_latest/widgets/request_category_list.dart b/lib/dashboard_latest/widgets/request_category_list.dart index e48f9f7f..3f7808d6 100644 --- a/lib/dashboard_latest/widgets/request_category_list.dart +++ b/lib/dashboard_latest/widgets/request_category_list.dart @@ -6,6 +6,7 @@ import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/models/new_models/dashboard_detail.dart'; import 'package:test_sa/modules/asset_delivery_module/pages/asset_delivery_item_view.dart'; import 'package:test_sa/modules/asset_inventory_module/pages/inventory_session_item_view.dart'; +import 'package:test_sa/modules/cleaner_module/widgets/one_time_task_item_view.dart'; import 'package:test_sa/modules/demo_module/demo_request_item_view.dart'; import 'package:test_sa/modules/internal_audit_module/pages/equipment_internal_audit/equipment_internal_audit_item_view.dart'; import 'package:test_sa/modules/loan_module/pages/loan_equipment_item_view.dart'; @@ -76,6 +77,8 @@ class RequestCategoryList extends StatelessWidget { return MedicalGasInspectionItemView(requestData: request); case 15: return DemoRequestItemView(requestData: request); + case 17: + return OneTimeTaskItemView(requestData: request); default: return Container( height: 100, diff --git a/lib/main.dart b/lib/main.dart index 40ba2eda..b6d04cdb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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/reject_reason_provider.dart'; import 'modules/cleaner_module/pages/cleaner_search_form_view.dart'; -import 'modules/cleaner_module/pages/cleaner_search_list_view.dart'; import 'modules/cleaner_module/provider/cleaner_provider.dart'; // class MyHttpOverrides extends HttpOverrides { @@ -408,7 +407,6 @@ class MyApp extends StatelessWidget { CreateMedicalGasRequestPage.id: (_) => CreateMedicalGasRequestPage(), CreateDemoRequestPage.id: (_) => CreateDemoRequestPage(), CleanerSearchFormView.routeName: (_) => const CleanerSearchFormView(), - CleanerSearchListView.routeName: (_) => const CleanerSearchListView(), }, ), ), diff --git a/lib/modules/cleaner_module/models/cleaner_data_model.dart b/lib/modules/cleaner_module/models/cleaner_data_model.dart deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/modules/cleaner_module/models/cleaner_form_model.dart b/lib/modules/cleaner_module/models/cleaner_form_model.dart deleted file mode 100644 index f8f7c98d..00000000 --- a/lib/modules/cleaner_module/models/cleaner_form_model.dart +++ /dev/null @@ -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, - }); -} diff --git a/lib/modules/cleaner_module/models/one_time_task_data.dart b/lib/modules/cleaner_module/models/one_time_task_data.dart index 817ae685..f348300a 100644 --- a/lib/modules/cleaner_module/models/one_time_task_data.dart +++ b/lib/modules/cleaner_module/models/one_time_task_data.dart @@ -24,7 +24,8 @@ class OneTimeTaskData { List? attachments; String? createdDate; String? createdBy; - String? imageRoom; // For captured image + String? imageRoom; // For backward compatibility - deprecated + List? imageRooms; // For multiple captured images (max 2) OneTimeTaskData({ this.id, @@ -53,6 +54,7 @@ class OneTimeTaskData { this.createdDate, this.createdBy, this.imageRoom, + this.imageRooms, }); factory OneTimeTaskData.fromJson(Map json) { @@ -87,6 +89,7 @@ class OneTimeTaskData { createdDate: json['createdDate'], createdBy: json['createdBy'], imageRoom: json['imageRoom'], + imageRooms: json['imageRooms'] != null ? List.from(json['imageRooms']) : null, ); } @@ -118,6 +121,7 @@ class OneTimeTaskData { 'createdDate': createdDate, 'createdBy': createdBy, 'imageRoom': imageRoom, + 'imageRooms': imageRooms, }; } } @@ -204,12 +208,16 @@ class Instruction { class Attachment { int? id; - String? fileName; - String? fileUrl; + String? name; + String? originalName; + String? fileName; // For backward compatibility + String? fileUrl; // For backward compatibility String? fileType; Attachment({ this.id, + this.name, + this.originalName, this.fileName, this.fileUrl, this.fileType, @@ -218,6 +226,8 @@ class Attachment { factory Attachment.fromJson(Map json) { return Attachment( id: json['id'], + name: json['name'], + originalName: json['originalName'], fileName: json['fileName'], fileUrl: json['fileUrl'], fileType: json['fileType'], @@ -227,10 +237,11 @@ class Attachment { Map toJson() { return { 'id': id, + 'name': name, + 'originalName': originalName, 'fileName': fileName, 'fileUrl': fileUrl, 'fileType': fileType, }; } } - diff --git a/lib/modules/cleaner_module/pages/cleaner_request_item_view.dart b/lib/modules/cleaner_module/pages/cleaner_request_item_view.dart deleted file mode 100644 index 0ffc8e96..00000000 --- a/lib/modules/cleaner_module/pages/cleaner_request_item_view.dart +++ /dev/null @@ -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); - } -} diff --git a/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart b/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart index 9cc1f1d8..e2761c08 100644 --- a/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart +++ b/lib/modules/cleaner_module/pages/cleaner_search_form_view.dart @@ -5,6 +5,7 @@ import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/modules/cleaner_module/models/task_type.dart'; import 'package:test_sa/modules/cleaner_module/models/qr_scan_location_data.dart'; +import 'package:test_sa/modules/cleaner_module/models/one_time_task_data.dart'; import 'package:test_sa/modules/cleaner_module/provider/cleaner_provider.dart'; import 'package:test_sa/modules/cleaner_module/provider/task_type_provider.dart'; import 'package:test_sa/modules/cleaner_module/pages/one_time_task_view.dart'; @@ -142,10 +143,16 @@ class _CleanerSearchFormViewState extends State { final taskId = data['id']; 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( MaterialPageRoute( - builder: (context) => OneTimeTaskView(taskId: taskId), + builder: (context) => OneTimeTaskView( + taskId: taskId, + initialData: taskData, + ), ), ); } @@ -289,51 +296,50 @@ class _CleanerSearchFormViewState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (locationData.siteName != null && locationData.siteName!.isNotEmpty) - Text( - 'Site: ${locationData.siteName}', - style: AppTextStyles.bodyText.copyWith( - color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, - ), - ), - if (locationData.buildingName != null && locationData.buildingName!.isNotEmpty) - Text( - 'Building: ${locationData.buildingName}', - style: AppTextStyles.bodyText.copyWith( - color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, - ), - ), - if (locationData.floorName != null && locationData.floorName!.isNotEmpty) - Text( - 'Floor: ${locationData.floorName}', - style: AppTextStyles.bodyText.copyWith( - color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, - ), - ), - if (locationData.departmentName != null && locationData.departmentName!.isNotEmpty) - Text( - 'Department: ${locationData.departmentName}', - style: AppTextStyles.bodyText.copyWith( - color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, - ), - ), - if (locationData.roomName != null && locationData.roomName!.isNotEmpty) - Text( - 'Room: ${locationData.roomName}', - style: AppTextStyles.bodyText.copyWith( - color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, - ), - ), - ], + if (locationData.siteName != null && locationData.siteName!.isNotEmpty) + Text( + 'Site: ${locationData.siteName}', + style: AppTextStyles.bodyText.copyWith( + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, ), - ], - ), + ), + if (locationData.buildingName != null && locationData.buildingName!.isNotEmpty) + Text( + 'Building: ${locationData.buildingName}', + style: AppTextStyles.bodyText.copyWith( + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, + ), + ), + if (locationData.floorName != null && locationData.floorName!.isNotEmpty) + Text( + 'Floor: ${locationData.floorName}', + style: AppTextStyles.bodyText.copyWith( + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, + ), + ), + if (locationData.departmentName != null && locationData.departmentName!.isNotEmpty) + Text( + 'Department: ${locationData.departmentName}', + style: AppTextStyles.bodyText.copyWith( + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, + ), + ), + if (locationData.roomName != null && locationData.roomName!.isNotEmpty) + Text( + 'Room: ${locationData.roomName}', + style: AppTextStyles.bodyText.copyWith( + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, + ), + ), + if (locationData.locationDescription != null && locationData.locationDescription!.isNotEmpty) + Text( + 'Location Description: ${locationData.locationDescription}', + style: AppTextStyles.bodyText.copyWith( + color: context.isDark ? AppColor.neutral10 : AppColor.neutral120, + ), + softWrap: true, + maxLines: null, + ), ], ).paddingOnly(start: 16, end: 16, top: 16, bottom: 16), ], diff --git a/lib/modules/cleaner_module/pages/cleaner_search_list_view.dart b/lib/modules/cleaner_module/pages/cleaner_search_list_view.dart deleted file mode 100644 index 87e25e22..00000000 --- a/lib/modules/cleaner_module/pages/cleaner_search_list_view.dart +++ /dev/null @@ -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 createState() => _CleanerSearchListViewState(); -} - -class _CleanerSearchListViewState extends State { - List _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?; - if (args != null && args['searchResults'] != null) { - final results = args['searchResults'] as List; - _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 _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; - 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), - ), - ); - }, - ); - }, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/modules/cleaner_module/pages/one_time_task_preview_screen.dart b/lib/modules/cleaner_module/pages/one_time_task_preview_screen.dart new file mode 100644 index 00000000..18e4d0ad --- /dev/null +++ b/lib/modules/cleaner_module/pages/one_time_task_preview_screen.dart @@ -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, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/cleaner_module/pages/one_time_task_view.dart b/lib/modules/cleaner_module/pages/one_time_task_view.dart index d1037aa2..e8e1ce28 100644 --- a/lib/modules/cleaner_module/pages/one_time_task_view.dart +++ b/lib/modules/cleaner_module/pages/one_time_task_view.dart @@ -1,12 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:test_sa/controllers/api_routes/urls.dart'; +import 'package:test_sa/controllers/providers/api/user_provider.dart'; +import 'package:test_sa/dashboard_latest/dashboard_provider.dart'; import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; +import 'package:test_sa/extensions/string_extensions.dart'; import 'package:test_sa/modules/cleaner_module/models/one_time_task_data.dart'; import 'package:test_sa/modules/cleaner_module/provider/cleaner_provider.dart'; import 'package:test_sa/modules/cleaner_module/widgets/cleaner_checklist_widget.dart'; -import 'package:test_sa/modules/cleaner_module/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/new_views/app_style/app_color.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/no_data_found.dart'; import 'package:test_sa/views/widgets/dialogs/confirm_dialog.dart'; +import 'package:test_sa/views/widgets/timer/app_timer.dart'; +import 'package:test_sa/models/timer_model.dart'; +import 'package:test_sa/views/widgets/requests/request_status.dart'; class OneTimeTaskView extends StatefulWidget { static const String routeName = "/one_time_task"; final int taskId; + final OneTimeTaskData? initialData; - const OneTimeTaskView({Key? key, required this.taskId}) : super(key: key); + const OneTimeTaskView({Key? key, required this.taskId, this.initialData}) : super(key: key); @override State createState() => _OneTimeTaskViewState(); @@ -31,11 +40,25 @@ class _OneTimeTaskViewState extends State { OneTimeTaskData? _taskData; bool _isLoading = false; DateTime? _timerStart; + List? _checklistItems; // Cache checklist items @override void 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 _loadTaskData() async { @@ -49,6 +72,19 @@ class _OneTimeTaskViewState extends State { if (result['success'] && result['data'] != null) { setState(() { _taskData = OneTimeTaskData.fromJson(result['data']); + + // Initialize imageRooms as empty list if null + _taskData?.imageRooms ??= []; + + // Convert attachments from API to imageRooms format for display + if ((_taskData?.imageRooms?.isEmpty ?? true) && + (_taskData?.attachments != null && _taskData!.attachments!.isNotEmpty)) { + _taskData!.imageRooms = _taskData!.attachments! + .map((attachment) => URLs.getFileUrl(attachment.name ?? '') ?? '') + .where((url) => url.isNotEmpty) + .toList(); + } + _isLoading = false; // Start timer automatically if task is in progress @@ -80,12 +116,18 @@ class _OneTimeTaskViewState extends State { Widget build(BuildContext context) { return Scaffold( appBar: DefaultAppBar( - title: 'One Time Task', - onWillPopScope: isClosed - ? null - : () { - _showExitDialog(); - }, + title: 'One Time Task'.addTranslation, + onWillPopScope: null, + onBackPress: (){ + WidgetsBinding.instance.addPostFrameCallback((_) { + UserProvider userProvider = Provider.of(context, listen: false); + DashBoardProvider dashBoardProvider = Provider.of(context, listen: false); + dashBoardProvider.getDashBoardCount(usersType:userProvider.user!.type!); + dashBoardProvider.resetRequestDataList(); + dashBoardProvider.getRequestDetail(usersType: userProvider.user!.type!, status: dashBoardProvider.tabs[dashBoardProvider.currentListIndex].tag); + }); + Navigator.pop(context); + }, ), body: _isLoading ? const ALoading() @@ -110,10 +152,10 @@ class _OneTimeTaskViewState extends State { _updateInstructions(value); }); }, - capturedImageBase64: _taskData!.imageRoom, - onImageCaptured: (imageBase64) { + capturedImages: _taskData!.imageRooms ?? [], + onImagesChanged: (images) { setState(() { - _taskData!.imageRoom = imageBase64; + _taskData!.imageRooms = images; }); }, ), @@ -124,22 +166,10 @@ class _OneTimeTaskViewState extends State { if (!isClosed) ...[ FooterActionButton.footerContainer( context: context, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - AppFilledButton( - label: context.translation.save, - buttonColor: context.isDark ? AppColor.neutral20 : AppColor.white60, - textColor: context.isDark ? Colors.white : AppColor.black10, - onPressed: () => Navigator.pop(context), - ).expanded, - 12.width, - AppFilledButton( - label: context.translation.complete, - buttonColor: AppColor.primary10, - onPressed: _onComplete, - ).expanded, - ], + child: AppFilledButton( + label: context.translation.complete, + buttonColor: AppColor.primary10, + onPressed: _onComplete, ), ), ] @@ -149,7 +179,7 @@ class _OneTimeTaskViewState extends State { : NoDataFound(message: context.translation.noDataFound).center, ).handlePopScope( cxt: context, - showPopUp: !isClosed, + showPopUp: false, onSave: () { Navigator.pop(context); }, @@ -166,25 +196,19 @@ class _OneTimeTaskViewState extends State { children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, children: [ if (_taskData?.statusName != null) - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: _getStatusColor().withOpacity(0.1), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - _taskData!.statusName!, - style: AppTextStyles.bodyText.copyWith( - color: _getStatusColor(), - fontWeight: FontWeight.w600, - ), - ), + StatusLabel( + radius: 4, + label: _taskData!.statusName!, + textColor: AppColor.getHistoryLogStatusTextColorByName(_taskData!.statusName!), + backgroundColor: AppColor.getHistoryLogStatusColorByName(_taskData!.statusName!), ), if (_taskData?.createdDate != null) Text( - _formatDate(_taskData!.createdDate!), + _taskData!.createdDate!.toString().toServiceRequestCardFormat, + textAlign: TextAlign.end, style: AppTextStyles.tinyFont.copyWith( color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, ), @@ -208,47 +232,24 @@ class _OneTimeTaskViewState extends State { ), ), 8.height, - if (_taskData?.siteName != null) - _buildInfoRow('Site', _taskData!.siteName!), - if (_taskData?.buildingName != null) - _buildInfoRow('Building', _taskData!.buildingName!), - if (_taskData?.floorName != null) - _buildInfoRow('Floor', _taskData!.floorName!), - if (_taskData?.departmentName != null) - _buildInfoRow('Department', _taskData!.departmentName!), - if (_taskData?.roomName != null) - _buildInfoRow('Room', _taskData!.roomName!), - if (_taskData?.locationDescription != null) - _buildInfoRow('Location', _taskData!.locationDescription!), - // Timer display + if (_taskData?.siteName != null) _buildInfoRow('Site', _taskData!.siteName!), + if (_taskData?.buildingName != null) _buildInfoRow('Building', _taskData!.buildingName!), + if (_taskData?.floorName != null) _buildInfoRow('Floor', _taskData!.floorName!), + if (_taskData?.departmentName != null) _buildInfoRow('Department', _taskData!.departmentName!), + if (_taskData?.roomName != null) _buildInfoRow('Room', _taskData!.roomName!), + if (_taskData?.locationDescription != null) _buildInfoRow('Location', _taskData!.locationDescription!), + // Timer display using AppTimer widget if (_timerStart != null && !isClosed) ...[ 12.height, - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: AppColor.primary10.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: AppColor.primary10), - ), - 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, - ), - ), - ], + AppTimer( + label: 'Working Time', + timer: TimerModel( + startAt: _timerStart, + endAt: null, + durationInSecond: 0, ), + enabled: false, // Disable controls, just show the timer + showTimer: true, ), ], ], @@ -270,15 +271,35 @@ class _OneTimeTaskViewState extends State { } List _convertInstructionsToChecklist() { - return _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() ?? []; + // If checklist items are already created, reuse them + if (_checklistItems != null) { + // Update the existing items with latest data from instructions + for (var item in _checklistItems!) { + final instruction = _taskData?.instructions?.firstWhere((i) => i.id == item.id, orElse: () => Instruction()); + if (instruction?.id != null) { + item.isChecked = instruction!.isDone ?? false; + // Only update comment if it's different to avoid cursor jumping + if (item.comment != (instruction.comment ?? '')) { + item.comment = instruction.comment ?? ''; + } + } + } + return _checklistItems!; + } + + // Create new checklist items only on first call + _checklistItems = _taskData?.instructions?.map((instruction) { + return ChecklistItem( + id: instruction.id ?? 0, + title: instruction.instructionText ?? '', + isChecked: instruction.isDone ?? false, + isMandatory: instruction.isRequired ?? false, + comment: instruction.comment ?? '', + ); + }).toList() ?? + []; + + return _checklistItems!; } void _updateInstructions(List checklist) { @@ -295,21 +316,24 @@ class _OneTimeTaskViewState extends State { } void _onComplete() { - // Validate mandatory items - final uncheckedMandatory = _taskData?.instructions?.where((item) => - (item.isRequired ?? false) && !(item.isDone ?? false) - ).toList() ?? []; + // Validate mandatory checklist items + final uncheckedMandatory = _taskData?.instructions?.where((item) => (item.isRequired ?? false) && !(item.isDone ?? false)).toList() ?? []; if (uncheckedMandatory.isNotEmpty) { _showErrorDialog(context, "Please complete all mandatory tasks (marked with *)"); return; } - // Show preview dialog - _showPreviewDialog(); - } + // Validate that at least one image is captured + final capturedImages = _taskData?.imageRooms ?? []; + print('DEBUG: Captured images count: ${capturedImages.length}'); + print('DEBUG: Images: $capturedImages'); + + if (capturedImages.isEmpty) { + _showErrorDialog(context, "Please capture at least one image before completing"); + return; + } - void _showPreviewDialog() { // Calculate working hours double totalWorkingHours = 0; if (_timerStart != null) { @@ -317,28 +341,21 @@ class _OneTimeTaskViewState extends State { totalWorkingHours = duration.inSeconds / 3600.0; } - CleanerPreviewDialog.show( - context, - model: _convertToRecurrentWoData(), - totalWorkingHours: totalWorkingHours, - onConfirm: _performComplete, + // Navigate to preview screen + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => OneTimeTaskPreviewScreen( + taskData: _taskData!, + totalWorkingHours: totalWorkingHours, + onConfirm: () { + Navigator.pop(context); // Close preview screen + _performComplete(); // Proceed with completion + }, + ), + ), ); } - // 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 _performComplete() async { showDialog( context: context, @@ -348,20 +365,33 @@ class _OneTimeTaskViewState extends State { try { final instructionsPayload = _taskData?.instructions?.map((instruction) { - return { - 'id': instruction.id, - 'instructionTextId': instruction.instructionTextId, - 'instructionText': instruction.instructionText, - 'isRequired': instruction.isRequired, - 'isDone': instruction.isDone, - 'comment': instruction.comment, - }; - }).toList() ?? []; + return { + 'id': instruction.id, + 'isDone': instruction.isDone, + 'comment': instruction.comment, + }; + }).toList() ?? + []; + + // Convert images to attachments format + final attachmentsPayload = _taskData?.imageRooms?.map((imageString) { + // imageString format: "filename|base64data" + final parts = imageString.split('|'); + final originalName = parts.isNotEmpty ? parts[0] : 'image.jpg'; + final base64Data = parts.length > 1 ? parts[1] : imageString; + + return { + 'id': 0, + 'name': "$originalName|$base64Data", // Send full formatted string + 'originalName': originalName, + }; + }).toList() ?? + []; final result = await CleanerProvider().completeOneTimeTask( id: _taskData!.id!, instructions: instructionsPayload, - imageRoom: _taskData?.imageRoom, + attachments: attachmentsPayload, ); if (mounted) Navigator.pop(context); // Close loading dialog @@ -387,20 +417,6 @@ class _OneTimeTaskViewState extends State { } } - 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) { showDialog( context: context, @@ -412,26 +428,14 @@ class _OneTimeTaskViewState extends State { ); } - Color _getStatusColor() { - if (_taskData?.statusValue == 2) { - return Colors.green; - } - return AppColor.primary10; - } - - String _formatDate(String dateStr) { - try { - final date = DateTime.parse(dateStr); - return '${date.day}/${date.month}/${date.year}'; - } catch (e) { - return dateStr; + @override + void dispose() { + // Dispose all checklist item controllers + if (_checklistItems != null) { + for (var item in _checklistItems!) { + item.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')}'; + super.dispose(); } } diff --git a/lib/modules/cleaner_module/provider/cleaner_provider.dart b/lib/modules/cleaner_module/provider/cleaner_provider.dart index 18a442b3..ecae5532 100644 --- a/lib/modules/cleaner_module/provider/cleaner_provider.dart +++ b/lib/modules/cleaner_module/provider/cleaner_provider.dart @@ -308,7 +308,7 @@ class CleanerProvider extends ChangeNotifier { Response response; try { response = await ApiManager.instance.get( - '${URLs.getRequestById}/$id', + '${URLs.getRequestById}?id=$id', ); // Parse the response body @@ -336,6 +336,7 @@ class CleanerProvider extends ChangeNotifier { "data": null, "message": "Failed to retrieve task. Please check your connection and try again.", }; + } } @@ -343,7 +344,7 @@ class CleanerProvider extends ChangeNotifier { Future> completeOneTimeTask({ required int id, required List> instructions, - String? imageRoom, + List>? attachments, }) async { Response response; try { @@ -352,7 +353,7 @@ class CleanerProvider extends ChangeNotifier { body: { "id": id, "instructions": instructions, - "imageRoom": imageRoom, + "attachments": attachments ?? [], }, showToast: false, ); diff --git a/lib/modules/cleaner_module/provider/fms_building_provider.dart b/lib/modules/cleaner_module/provider/fms_building_provider.dart deleted file mode 100644 index 39b3f0b3..00000000 --- a/lib/modules/cleaner_module/provider/fms_building_provider.dart +++ /dev/null @@ -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? _buildings; - List? 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 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> 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 []; - } - } -} diff --git a/lib/modules/cleaner_module/provider/fms_floor_provider.dart b/lib/modules/cleaner_module/provider/fms_floor_provider.dart deleted file mode 100644 index d3700761..00000000 --- a/lib/modules/cleaner_module/provider/fms_floor_provider.dart +++ /dev/null @@ -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? _floors; - List? 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 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> 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 []; - } - } -} diff --git a/lib/modules/cleaner_module/provider/fms_location_description_provider.dart b/lib/modules/cleaner_module/provider/fms_location_description_provider.dart deleted file mode 100644 index db799dc4..00000000 --- a/lib/modules/cleaner_module/provider/fms_location_description_provider.dart +++ /dev/null @@ -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? locationDescriptions; - List? deptRoom; - - LocationDescriptionResponse({ - this.locationDescriptions, - this.deptRoom, - }); - - factory LocationDescriptionResponse.fromJson(Map 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 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? _locationDescriptions; - List? get locationDescriptions => _locationDescriptions; - - // contain dept room list - List? _deptRoomList; - List? 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 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 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; - } - } -} diff --git a/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart b/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart index 9ec79ab6..804372c1 100644 --- a/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart +++ b/lib/modules/cleaner_module/widgets/cleaner_checklist_widget.dart @@ -1,10 +1,6 @@ import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:test_sa/controllers/api_routes/urls.dart'; import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/string_extensions.dart'; @@ -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/common_widgets/app_text_form_field.dart'; import 'package:test_sa/views/widgets/buttons/app_back_button.dart'; +import 'package:test_sa/modules/cleaner_module/widgets/multi_image_picker.dart'; +import 'package:test_sa/views/widgets/images/files_list.dart'; class CleanerChecklistWidget extends StatefulWidget { final List checklistItems; final Function(List)? onChecklistChanged; - final String? capturedImageBase64; - final Function(String?)? onImageCaptured; - bool isClosed; + final List capturedImages; + final Function(List)? onImagesChanged; + 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 State createState() => _CleanerChecklistWidgetState(); @@ -191,11 +196,23 @@ class _CleanerChecklistWidgetState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Captured Image'.addTranslation, - style: AppTextStyles.heading6.copyWith( - color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Captured Image'.addTranslation, + style: AppTextStyles.heading6.copyWith( + color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, + ), + ), + // Text( + // '(${widget.capturedImages.length}/$maxImages)', + // style: AppTextStyles.bodyText.copyWith( + // color: context.isDark ? AppColor.neutral30 : AppColor.neutral50, + // fontWeight: FontWeight.w600, + // ), + // ), + ], ), 8.height, _buildImageWidget(context), @@ -203,205 +220,148 @@ class _CleanerChecklistWidgetState extends State { ).toShadowContainer(context); } + static const int maxImages = 2; + /// Build the image widget with loading indicator for network images Widget _buildImageWidget(BuildContext context) { - if (widget.capturedImageBase64 == null || widget.capturedImageBase64!.isEmpty) { - // Show camera icon when no image - return Container( - height: 200, - width: double.infinity, - decoration: BoxDecoration( - color: AppColor.fieldBgColor(context), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.camera_alt, - size: 48, - color: context.isDark ? AppColor.white10 : AppColor.black10, - ), - ).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); - } - }); - } + if (widget.capturedImages.isEmpty) { + // Show add image button when no image + return GestureDetector( + onTap: widget.isClosed ? null : () async { + // Only allow camera - pick one image at a time + final pickedFile = await ImagePicker().pickImage( + source: ImageSource.camera, + imageQuality: 70, + maxWidth: 800, + maxHeight: 800, + ); - 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.from(widget.capturedImages); + updatedImages.add(formattedImage); - // 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 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; + widget.onImagesChanged?.call(updatedImages); } - 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 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(), - ], - ), - ), + 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, ), ), - ); - }), - _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 - 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, - ), + // If images are from API (URLs), use FilesList widget like ServiceRequestDetailView + if (hasApiImages && widget.isClosed) { + return FilesList( + images: widget.capturedImages, + showAsListView: false, + ); + } + + // Otherwise, use MultiImagePicker for base64 images or when editing + return MultiImagePicker( + imageUrls: widget.capturedImages, + enabled: !widget.isClosed, + maxImages: 2, // Enforce max 2 images + onImageTap: (index, imageUrl) { + // Handle image tap - show in full screen + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => Scaffold( + backgroundColor: Colors.black, + body: SafeArea( + child: Stack( + children: [ + InteractiveViewer( + child: _buildImageForViewing(imageUrl), + ).center, + const ABackButton(), + ], + ), + ), + ), + ), + ); + }, + onDelete: (index) { + // Handle image delete + if (index >= 0) { + setState(() { + widget.capturedImages.removeAt(index); + }); + widget.onImagesChanged?.call(widget.capturedImages); + } + }, + onAdd: (imageUrl) { + // Handle new image addition + setState(() { + widget.capturedImages.add(imageUrl); + }); + widget.onImagesChanged?.call(widget.capturedImages); + }, ); } - /// Check if the URL is a local file path - /// Same logic as multi_image_picker_item.dart - bool _isLocalUrl(String url) { - if (url.isEmpty) return false; - return url.startsWith("/") || url.startsWith("file://") || (url.length > 1 && url.substring(1).startsWith(':\\')); + /// Build image widget for viewing based on source type + Widget _buildImageForViewing(String imageUrl) { + try { + // Check if it's a URL from API + if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) { + return Image.network( + imageUrl, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return const Icon(Icons.broken_image, size: 48, color: Colors.white); + }, + ); + } + + // Otherwise, it's base64 encoded + final parts = imageUrl.split('|'); + final base64Data = parts.length > 1 ? parts[1] : imageUrl; + return Image.memory( + base64Decode(base64Data), + fit: BoxFit.contain, + ); + } catch (e) { + return const Icon(Icons.broken_image, size: 48, color: Colors.white); + } } @override @@ -409,26 +369,6 @@ class _CleanerChecklistWidgetState extends State { // Note: Don't dispose controllers here as they're managed by ChecklistItem 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 @@ -446,10 +386,7 @@ class ChecklistItem { required this.isChecked, required this.comment, this.isMandatory = false, - }) : commentController = TextEditingController(text: comment) { - // Update the controller text whenever comment changes - commentController.text = comment; - } + }) : commentController = TextEditingController(text: comment); // Convert to JSON for API submission Map toJson() { diff --git a/lib/modules/cleaner_module/widgets/cleaner_preview_dialog.dart b/lib/modules/cleaner_module/widgets/cleaner_preview_dialog.dart deleted file mode 100644 index 6abd4d04..00000000 --- a/lib/modules/cleaner_module/widgets/cleaner_preview_dialog.dart +++ /dev/null @@ -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 children) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - title.heading6(context).custom(fontWeight: FontWeight.w600), - 8.height, - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: children, - ).toShadowContainer(context, padding: 12), - ], - ); - } - - Widget _buildPreviewRow(BuildContext context, String label, String value) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 2, - child: '$label:'.bodyText2(context).custom(color: AppColor.neutral120), - ), - 8.width, - Expanded( - flex: 3, - child: value.bodyText2(context).custom(color: AppColor.neutral120), - ), - ], - ), - ); - } - - Widget _buildWorkingTimeTile(BuildContext context) { - // Create timer list from model data, same as in RecurrentTaskInfoWidget - final timerList = model.planRecurrentTaskTimers?.map((e) { - return TimerHistoryModel( - id: e.id, - startTime: e.startTime, - endTime: e.endTime, - workingHours: e.workingHours, - ); - }).toList() ?? []; - - return Container( - height: 56.toScreenHeight, - padding: EdgeInsets.symmetric(horizontal: 16.toScreenWidth), - alignment: Alignment.centerLeft, - decoration: BoxDecoration( - color: AppColor.fieldBgColor(context), - borderRadius: BorderRadius.circular(10), - ), - child: Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "Total Working Time", - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: context.isDark ? null : AppColor.neutral20, - fontWeight: FontWeight.w500, - ), - ), - Text( - " ${CMRequestUtils.formatTotalWorkingHours(totalWorkingHours)}", - style: Theme.of(context).textTheme.bodyMedium, - ), - ], - ).expanded, - 8.width, - const Icon(Icons.info_outline_rounded, color: AppColor.primary10), - ], - ), - ).onPress(() async { - await showModalBottomSheet( - context: context, - isDismissible: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), - clipBehavior: Clip.antiAliasWithSaveLayer, - builder: (BuildContext context) => TotalWorkingTimeDetailBottomSheet(timerList: timerList), - ); - }); - } - - Widget _buildImagePreview(BuildContext context, String imageString) { - Widget imageWidget; - - // First check if it's base64 encoded (contains | separator) - if (imageString.contains('|')) { - // Format: "filename|base64data" - try { - final base64Data = imageString.split('|').last; - imageWidget = Image.memory(base64Decode(base64Data), fit: BoxFit.cover); - return Container( - height: 200, - width: double.infinity, - decoration: BoxDecoration( - color: AppColor.fieldBgColor(context), - borderRadius: BorderRadius.circular(8), - ), - child: imageWidget, - ).onPress(() { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => Scaffold( - body: SafeArea( - child: Stack( - children: [ - InteractiveViewer(child: imageWidget).center, - const ABackButton(), - ], - ), - ), - ), - ), - ); - }); - } catch (e) { - debugPrint('Error decoding base64 image: $e'); - return _buildCameraIcon(context); - } - } - - // Check if it's a local file path - if (_isLocalUrl(imageString)) { - try { - imageWidget = Image.file(File(imageString), fit: BoxFit.cover); - return Container( - height: 200, - width: double.infinity, - decoration: BoxDecoration( - color: AppColor.fieldBgColor(context), - borderRadius: BorderRadius.circular(8), - ), - child: imageWidget, - ).onPress(() { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => Scaffold( - body: SafeArea( - child: Stack( - children: [ - InteractiveViewer(child: imageWidget).center, - const ABackButton(), - ], - ), - ), - ), - ), - ); - }); - } catch (e) { - debugPrint('Error loading local file: $e'); - return _buildCameraIcon(context); - } - } - - // If not local and not base64, it's a server filename - use NetworkImage with loading indicator - final imageUrl = URLs.getFileUrl(imageString); - if (imageUrl != null) { - imageWidget = Image.network( - imageUrl, - fit: BoxFit.cover, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) { - return child; - } - return Center( - child: CircularProgressIndicator( - color: AppColor.primary10, - value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, - ), - ); - }, - errorBuilder: (context, error, stackTrace) { - debugPrint('Error loading network image: $error'); - return _buildCameraIcon(context); - }, - ); - return Container( - height: 200, - width: double.infinity, - decoration: BoxDecoration( - color: AppColor.fieldBgColor(context), - borderRadius: BorderRadius.circular(8), - ), - child: imageWidget, - ).onPress(() { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => Scaffold( - body: SafeArea( - child: Stack( - children: [ - InteractiveViewer(child: imageWidget).center, - const ABackButton(), - ], - ), - ), - ), - )); - }); - } - - return _buildCameraIcon(context); - } - - /// Build camera icon widget for fallback - Widget _buildCameraIcon(BuildContext context) { - return Container( - height: 200, - width: double.infinity, - decoration: BoxDecoration( - color: AppColor.fieldBgColor(context), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - Icons.camera_alt, - size: 48, - color: context.isDark ? AppColor.white10 : AppColor.black10, - ), - ); - } - - /// Check if the URL is a local file path - bool _isLocalUrl(String url) { - return url.startsWith('/') || url.startsWith('file://') || (url.length > 1 && url[1] == ':'); - } -} diff --git a/lib/modules/cleaner_module/widgets/multi_image_picker.dart b/lib/modules/cleaner_module/widgets/multi_image_picker.dart new file mode 100644 index 00000000..002acbe4 --- /dev/null +++ b/lib/modules/cleaner_module/widgets/multi_image_picker.dart @@ -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 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(), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/modules/cleaner_module/widgets/one_time_task_item_view.dart b/lib/modules/cleaner_module/widgets/one_time_task_item_view.dart new file mode 100644 index 00000000..9adfdd58 --- /dev/null +++ b/lib/modules/cleaner_module/widgets/one_time_task_item_view.dart @@ -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(); + } +} diff --git a/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart b/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart index b350d626..0857e414 100644 --- a/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart +++ b/lib/modules/pm_module/recurrent_wo/recurrent_work_order_view.dart @@ -6,8 +6,6 @@ import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/models/ppm/recurrent_wo.dart'; import 'package:test_sa/modules/cm_module/views/components/action_button/footer_action_button.dart'; -import 'package:test_sa/modules/cleaner_module/widgets/cleaner_checklist_widget.dart'; -import 'package:test_sa/modules/cleaner_module/widgets/cleaner_preview_dialog.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/common_widgets/app_filled_button.dart'; import 'package:test_sa/new_views/common_widgets/app_lazy_loading.dart'; @@ -35,21 +33,11 @@ class RecurrentWorkOrderView extends StatefulWidget { class _RecurrentWorkOrderViewState extends State { AllRequestsProvider? allRequestsProvider; - DateTime? _housekeeperTimerStart; @override void initState() { allRequestsProvider = Provider.of(context, listen: false); - Provider.of(context, listen: false).getRecurrentWoById(id: widget.taskId!).then((_) { - // Start timer automatically for housekeeper type - final provider = Provider.of(context, listen: false); - bool isHouseKeeper = provider.recurrentWoData?.planRecurrentTaskCheckLists != null && - provider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty; - - if (isHouseKeeper && provider.recurrentWoData?.status?.value != 1) { - _housekeeperTimerStart = DateTime.now(); - } - }); + Provider.of(context, listen: false).getRecurrentWoById(id: widget.taskId!); super.initState(); } @@ -64,14 +52,13 @@ class _RecurrentWorkOrderViewState extends State { Widget build(BuildContext context) { return Consumer(builder: (context, requestProvider, child) { isClosed = requestProvider.recurrentWoData?.status?.value == 1; - bool isHouseKeeper = requestProvider.recurrentWoData!.planRecurrentTaskCheckLists != null && requestProvider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty; return Scaffold( appBar: DefaultAppBar( title: context.translation.recurrentWo, onWillPopScope: isClosed ? null : () { - _updateTask(context: context, status: 0, isHouseKeeper: isHouseKeeper); + _updateTask(context: context, status: 0); }, ), body: allRequestsProvider!.isLoading @@ -89,8 +76,8 @@ class _RecurrentWorkOrderViewState extends State { RecurrentTaskInfoWidget( isClosed: isClosed, model: requestProvider.recurrentWoData, - isHouseKeeper: isHouseKeeper, - timerStartTime: _housekeeperTimerStart, + isHouseKeeper: false, + timerStartTime: null, ), IgnorePointer( ignoring: isClosed, @@ -103,24 +90,6 @@ class _RecurrentWorkOrderViewState extends State { ) : 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, @@ -134,13 +103,13 @@ class _RecurrentWorkOrderViewState extends State { label: context.translation.save, buttonColor: context.isDark ? AppColor.neutral20 : AppColor.white60, textColor: context.isDark ? Colors.white : AppColor.black10, - onPressed: () => _updateTask(context: context, status: 0, isHouseKeeper: isHouseKeeper), + onPressed: () => _updateTask(context: context, status: 0), ).expanded, 12.width, AppFilledButton( label: context.translation.complete, buttonColor: AppColor.primary10, - onPressed: () => _updateTask(context: context, status: 1, isHouseKeeper: isHouseKeeper), + onPressed: () => _updateTask(context: context, status: 1), ).expanded, ], ), @@ -154,77 +123,45 @@ class _RecurrentWorkOrderViewState extends State { cxt: context, showPopUp: !isClosed, 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 { - AllRequestsProvider allRequestsProvider = Provider.of(context, listen: false); - - // Only validate checklist if it exists and status is complete - if (status == 1 && allRequestsProvider.recurrentWoData?.planRecurrentTaskCheckLists != null && allRequestsProvider.recurrentWoData!.planRecurrentTaskCheckLists!.isNotEmpty) { - // Validate mandatory checklist items only - if (!_validateChecklist(context, allRequestsProvider.recurrentWoData!)) { - return; - } - } - - // For housekeeper: show preview dialog before completing - if (isHouseKeeper && status == 1) { - _showPreviewDialog(context, allRequestsProvider); - return; - } - + void _updateTask({required BuildContext context, required int status}) async { // 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(context, listen: false); - if (validate(context: context, model: allRequestsProvider.recurrentWoData!, status: status, isHouseKeeper: isHouseKeeper)) { - // For housekeeper: automatically add timer data - if (isHouseKeeper && _housekeeperTimerStart != null) { - DateTime endTime = DateTime.now(); - int durationInSecond = endTime.difference(_housekeeperTimerStart!).inSeconds; + if (validate(context: context, model: allRequestsProvider.recurrentWoData!, status: status)) { + // Use existing timer logic + allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers ?? []; - 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: _housekeeperTimerStart!.toIso8601String(), - endTime: endTime.toIso8601String(), + startTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.startAt!.toIso8601String(), + endTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt?.toIso8601String(), workingHours: ((durationInSecond) / 60 / 60), ), ); - } else { - // For non-housekeeper: use existing timer logic - allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers = allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers ?? []; - - if (allRequestsProvider.recurrentWoData?.recurrentWoTimePicker != null) { - int durationInSecond = allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt!.difference(allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.startAt!).inSeconds; - allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add( - PlanRecurrentTaskTimers( - id: 0, - startTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.startAt!.toIso8601String(), - endTime: allRequestsProvider.recurrentWoData!.recurrentWoTimePicker!.endAt?.toIso8601String(), - workingHours: ((durationInSecond) / 60 / 60), - ), - ); - } - allRequestsProvider.recurrentWoData?.timerModelList?.forEach((timer) { - int durationInSecond = timer.endAt!.difference(timer.startAt!).inSeconds; - allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.add( - PlanRecurrentTaskTimers( - id: 0, - startTime: timer.startAt!.toIso8601String(), - endTime: timer.endAt?.toIso8601String(), - workingHours: ((durationInSecond) / 60 / 60), - ), - ); - }); } + allRequestsProvider.recurrentWoData?.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 = {}; allRequestsProvider.recurrentWoData?.planRecurrentTaskTimers?.forEach((timer) { @@ -268,38 +205,6 @@ class _RecurrentWorkOrderViewState extends State { } } - void _showPreviewDialog(BuildContext context, AllRequestsProvider provider) { - final model = provider.recurrentWoData!; - - // Calculate total working time - double totalWorkingHours = 0; - final timers = model.planRecurrentTaskTimers ?? []; - totalWorkingHours = timers.fold(0.0, (sum, item) { - if (item.startTime == null || item.endTime == null) return sum; - try { - final start = DateTime.parse(item.startTime!); - final end = DateTime.parse(item.endTime!); - final diffInHours = end.difference(start).inSeconds / 3600.0; - return sum + diffInHours; - } catch (_) { - return sum; - } - }); - - // Add current timer if running - if (_housekeeperTimerStart != null) { - final currentDuration = DateTime.now().difference(_housekeeperTimerStart!).inSeconds / 3600.0; - totalWorkingHours += currentDuration; - } - - CleanerPreviewDialog.show( - context, - model: model, - totalWorkingHours: totalWorkingHours, - onConfirm: () => _performUpdate(context: context, status: 1, isHouseKeeper: true), - ); - } - void _showErrorDialog(BuildContext context, String message) { showDialog( context: context, @@ -312,32 +217,8 @@ class _RecurrentWorkOrderViewState extends State { } } -bool _validateChecklist(BuildContext context, RecurrentWoData model) { - // Check if all mandatory checklist items are checked - final uncheckedMandatory = model.planRecurrentTaskCheckLists!.where((item) => item.isMandatory && !item.isChecked).toList(); - - if (uncheckedMandatory.isNotEmpty) { - 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 validate({required BuildContext context, required RecurrentWoData model, required int status}) { + // Timer validation logic bool isTimerPickerEnable = ApiManager.instance.assetGroup?.enabledEngineerTimer ?? false; if (isTimerPickerEnable) { diff --git a/lib/new_views/common_widgets/app_bottom_nav_bar.dart b/lib/new_views/common_widgets/app_bottom_nav_bar.dart index 611afb27..cc12846e 100644 --- a/lib/new_views/common_widgets/app_bottom_nav_bar.dart +++ b/lib/new_views/common_widgets/app_bottom_nav_bar.dart @@ -23,9 +23,9 @@ class AppBottomNavigationBar extends StatelessWidget { List navItems = [ 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), - NavItemModel(3, "assets", context.translation.assets), + if(!isHouseKeeper) NavItemModel(3, "assets", context.translation.assets), NavItemModel(4, "contact", context.translation.contact), ]; diff --git a/lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart b/lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart index bf3db783..1ad1297c 100644 --- a/lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart +++ b/lib/new_views/pages/land_page/requests/recurrent_wo_item_view.dart @@ -26,8 +26,6 @@ class RecurrentWoItemView extends StatelessWidget { @override Widget build(BuildContext context) { final user = Provider.of(context, listen: false).user; - final settingProvider = Provider.of(context, listen: false); - final bool isHouseKeeper = user?.type == UsersTypes.houseKeeper; if (requestData != null) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -61,16 +59,11 @@ class RecurrentWoItemView extends StatelessWidget { // '${context.translation.assetSN}: ${request.assetSN}'.bodyText(context),//todo ask ahmed // '${context.translation.code}: ${request.code}'.bodyText(context), '${context.translation.taskNo}: ${requestData!.requestNo}'.bodyText(context), - if(requestData?.siteName!=null) - '${context.translation.site}: ${requestData!.siteName}'.bodyText(context), - if(requestData!.buildingName!=null) - '${context.translation.building}: ${requestData!.buildingName}'.bodyText(context), - if(requestData!.departmentName!=null) - '${context.translation.department}: ${requestData!.departmentName}'.bodyText(context), - if(requestData!.floorName!=null) - '${context.translation.floor}: ${requestData!.floorName}'.bodyText(context), - if(requestData!.roomName!=null) - '${context.translation.room}: ${requestData!.roomName}'.bodyText(context), + 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, @@ -85,19 +78,7 @@ class RecurrentWoItemView extends StatelessWidget { ), ], ).toShadowContainer(context, withShadow: showShadow).onPress(() { - if (isHouseKeeper && (requestData?.statusValue == 4)) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CleanerSearchDetailsView( - requestId: requestData!.id ?? 0, - taskResult: null, - ), - ), - ); - } else { - Navigator.of(context).push(MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: requestData!.id))); - } + Navigator.of(context).push(MaterialPageRoute(builder: (_) => RecurrentWorkOrderView(taskId: requestData!.id))); }); } if (requestDetails != null) { @@ -152,17 +133,6 @@ class RecurrentWoItemView extends StatelessWidget { ), ], ).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))); }); } diff --git a/lib/new_views/pages/land_page/requests/request_paginated_listview.dart b/lib/new_views/pages/land_page/requests/request_paginated_listview.dart index a5fa049b..704836c4 100644 --- a/lib/new_views/pages/land_page/requests/request_paginated_listview.dart +++ b/lib/new_views/pages/land_page/requests/request_paginated_listview.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/models/new_models/dashboard_detail.dart'; import 'package:test_sa/modules/asset_delivery_module/pages/asset_delivery_item_view.dart'; +import 'package:test_sa/modules/cleaner_module/widgets/one_time_task_item_view.dart'; import 'package:test_sa/modules/demo_module/demo_request_item_view.dart'; import 'package:test_sa/modules/incident_module/incident_item_view.dart'; import 'package:test_sa/modules/internal_audit_module/pages/equipment_internal_audit/equipment_internal_audit_item_view.dart'; @@ -87,6 +88,8 @@ class RequestPaginatedListview extends StatelessWidget { return DemoRequestItemView(requestData: request); case 16: return IncidentItemView(requestData: request); + case 17: + return OneTimeTaskItemView(requestData: request); default: return Container( height: 100, diff --git a/lib/new_views/pages/land_page/widgets/request_item_view_list.dart b/lib/new_views/pages/land_page/widgets/request_item_view_list.dart index e0005278..158d822c 100644 --- a/lib/new_views/pages/land_page/widgets/request_item_view_list.dart +++ b/lib/new_views/pages/land_page/widgets/request_item_view_list.dart @@ -7,6 +7,7 @@ import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/models/all_requests_and_count_model.dart'; import 'package:test_sa/modules/asset_delivery_module/pages/asset_delivery_item_view.dart'; +import 'package:test_sa/modules/cleaner_module/widgets/one_time_task_item_view.dart'; import 'package:test_sa/modules/demo_module/demo_request_item_view.dart'; import 'package:test_sa/modules/incident_module/incident_item_view.dart'; import 'package:test_sa/modules/internal_audit_module/pages/equipment_internal_audit/equipment_internal_audit_item_view.dart'; @@ -76,6 +77,8 @@ class RequestItemViewList extends StatelessWidget { return DemoRequestItemView(requestDetails: list[index]); case 16: return IncidentItemView(requestDetails: list[index]); + case 17: + return OneTimeTaskItemView(requestDetails: list[index]); default: Container( height: 100,