- Added TaskListModel for managing daily tasks with JSON parsing. - Introduced WorkStatusResponseModel and WorkStatus for handling work status data. - Created MenuResponse and MenuItem models for dynamic menu management. - Updated routes to reflect correct naming conventions for task planning screens. - Enhanced DashboardScreen to include dynamic menu functionality and improved task statistics display. - Developed DailyProgressReportScreen for displaying daily progress reports with filtering options. - Implemented DailyTaskPlanningScreen for planning daily tasks with detailed views and actions. - Refactored left navigation bar to align with updated task planning routes.
		
			
				
	
	
		
			297 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			297 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'dart:convert';
 | |
| import 'dart:io';
 | |
| 
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:get/get.dart';
 | |
| import 'package:image_picker/image_picker.dart';
 | |
| import 'package:marco/helpers/services/app_logger.dart';
 | |
| 
 | |
| import 'package:marco/controller/my_controller.dart';
 | |
| import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
 | |
| import 'package:marco/helpers/services/api_service.dart';
 | |
| import 'package:marco/helpers/widgets/my_form_validator.dart';
 | |
| import 'package:marco/helpers/widgets/my_image_compressor.dart';
 | |
| import 'package:marco/helpers/widgets/my_snackbar.dart';
 | |
| import 'package:marco/model/dailyTaskPlanning/work_status_model.dart';
 | |
| 
 | |
| enum ApiStatus { idle, loading, success, failure }
 | |
| 
 | |
| class ReportTaskActionController extends MyController {
 | |
|   final RxBool isLoading = false.obs;
 | |
|   final Rx<ApiStatus> reportStatus = ApiStatus.idle.obs;
 | |
|   final Rx<ApiStatus> commentStatus = ApiStatus.idle.obs;
 | |
| 
 | |
|   final RxList<File> selectedImages = <File>[].obs;
 | |
|   final RxList<WorkStatus> workStatus = <WorkStatus>[].obs;
 | |
|   final RxList<WorkStatus> workStatuses = <WorkStatus>[].obs;
 | |
| 
 | |
|   final RxBool showAddTaskCheckbox = false.obs;
 | |
|   final RxBool isAddTaskChecked = false.obs;
 | |
| 
 | |
|   final RxBool isLoadingWorkStatus = false.obs;
 | |
|   final Rxn<Map<String, dynamic>> selectedTask = Rxn<Map<String, dynamic>>();
 | |
| 
 | |
|   final RxString selectedWorkStatusName = ''.obs;
 | |
| 
 | |
|   final MyFormValidator basicValidator = MyFormValidator();
 | |
|   final DailyTaskPlanningController taskController = Get.put(DailyTaskPlanningController());
 | |
|   final ImagePicker _picker = ImagePicker();
 | |
| 
 | |
|   final assignedDateController = TextEditingController();
 | |
|   final workAreaController = TextEditingController();
 | |
|   final activityController = TextEditingController();
 | |
|   final teamSizeController = TextEditingController();
 | |
|   final taskIdController = TextEditingController();
 | |
|   final assignedController = TextEditingController();
 | |
|   final completedWorkController = TextEditingController();
 | |
|   final commentController = TextEditingController();
 | |
|   final assignedByController = TextEditingController();
 | |
|   final teamMembersController = TextEditingController();
 | |
|   final plannedWorkController = TextEditingController();
 | |
|   final approvedTaskController = TextEditingController();
 | |
| 
 | |
|   List<TextEditingController> get _allControllers => [
 | |
|         assignedDateController,
 | |
|         workAreaController,
 | |
|         activityController,
 | |
|         teamSizeController,
 | |
|         taskIdController,
 | |
|         assignedController,
 | |
|         completedWorkController,
 | |
|         commentController,
 | |
|         assignedByController,
 | |
|         teamMembersController,
 | |
|         plannedWorkController,
 | |
|         approvedTaskController,
 | |
|       ];
 | |
| 
 | |
|   @override
 | |
|   void onInit() {
 | |
|     super.onInit();
 | |
|     logSafe("Initializing ReportTaskController...");
 | |
|     _initializeFormFields();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void onClose() {
 | |
|     for (final controller in _allControllers) {
 | |
|       controller.dispose();
 | |
|     }
 | |
|     logSafe("Disposed all text controllers in ReportTaskActionController.");
 | |
|     super.onClose();
 | |
|   }
 | |
| 
 | |
|   void _initializeFormFields() {
 | |
|     basicValidator
 | |
|       ..addField('assigned_date', label: "Assigned Date", controller: assignedDateController)
 | |
|       ..addField('work_area', label: "Work Area", controller: workAreaController)
 | |
|       ..addField('activity', label: "Activity", controller: activityController)
 | |
|       ..addField('team_size', label: "Team Size", controller: teamSizeController)
 | |
|       ..addField('task_id', label: "Task Id", controller: taskIdController)
 | |
|       ..addField('assigned', label: "Assigned", controller: assignedController)
 | |
|       ..addField('completed_work', label: "Completed Work", required: true, controller: completedWorkController)
 | |
|       ..addField('comment', label: "Comment", required: true, controller: commentController)
 | |
|       ..addField('assigned_by', label: "Assigned By", controller: assignedByController)
 | |
|       ..addField('team_members', label: "Team Members", controller: teamMembersController)
 | |
|       ..addField('planned_work', label: "Planned Work", controller: plannedWorkController)
 | |
|       ..addField('approved_task', label: "Approved Task", required: true, controller: approvedTaskController);
 | |
|   }
 | |
| 
 | |
|   Future<bool> approveTask({
 | |
|     required String projectId,
 | |
|     required String comment,
 | |
|     required String reportActionId,
 | |
|     required String approvedTaskCount,
 | |
|     List<File>? images,
 | |
|   }) async {
 | |
|     logSafe("approveTask() started", sensitive: false);
 | |
| 
 | |
|     if (projectId.isEmpty || reportActionId.isEmpty) {
 | |
|       _showError("Project ID and Report Action ID are required.");
 | |
|       logSafe("Missing required projectId or reportActionId", level: LogLevel.warning);
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     final approvedTaskInt = int.tryParse(approvedTaskCount);
 | |
|     final completedWorkInt = int.tryParse(completedWorkController.text.trim());
 | |
| 
 | |
|     if (approvedTaskInt == null) {
 | |
|       _showError("Invalid approved task count.");
 | |
|       logSafe("Invalid approvedTaskCount: $approvedTaskCount", level: LogLevel.warning);
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     if (completedWorkInt != null && approvedTaskInt > completedWorkInt) {
 | |
|       _showError("Approved task count cannot exceed completed work.");
 | |
|       logSafe("Validation failed: approved > completed", level: LogLevel.warning);
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     if (comment.trim().isEmpty) {
 | |
|       _showError("Comment is required.");
 | |
|       logSafe("Comment field is empty", level: LogLevel.warning);
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       reportStatus.value = ApiStatus.loading;
 | |
|       isLoading.value = true;
 | |
|       logSafe("Calling _prepareImages() for approval...");
 | |
|       final imageData = await _prepareImages(images);
 | |
| 
 | |
|       logSafe("Calling ApiService.approveTask()");
 | |
|       final success = await ApiService.approveTask(
 | |
|         id: projectId,
 | |
|         workStatus: reportActionId,
 | |
|         approvedTask: approvedTaskInt,
 | |
|         comment: comment,
 | |
|         images: imageData,
 | |
|       );
 | |
| 
 | |
|       if (success) {
 | |
|         logSafe("Task approved successfully");
 | |
|         _showSuccess("Task approved successfully!");
 | |
|         await taskController.fetchTaskData(projectId);
 | |
|         return true;
 | |
|       } else {
 | |
|         logSafe("API returned failure on approveTask", level: LogLevel.error);
 | |
|         _showError("Failed to approve task.");
 | |
|         return false;
 | |
|       }
 | |
|     } catch (e, st) {
 | |
|       logSafe("Error in approveTask: $e", level: LogLevel.error, error: e, stackTrace: st);
 | |
|       _showError("An error occurred.");
 | |
|       return false;
 | |
|     } finally {
 | |
|       isLoading.value = false;
 | |
|       Future.delayed(const Duration(milliseconds: 500), () {
 | |
|         reportStatus.value = ApiStatus.idle;
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   Future<void> commentTask({
 | |
|     required String projectId,
 | |
|     required String comment,
 | |
|     List<File>? images,
 | |
|   }) async {
 | |
|     logSafe("commentTask() started", sensitive: false);
 | |
| 
 | |
|     if (commentController.text.trim().isEmpty) {
 | |
|       _showError("Comment is required.");
 | |
|       logSafe("Comment field is empty", level: LogLevel.warning);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       isLoading.value = true;
 | |
| 
 | |
|       logSafe("Calling _prepareImages() for comment...");
 | |
|       final imageData = await _prepareImages(images);
 | |
| 
 | |
|       logSafe("Calling ApiService.commentTask()");
 | |
|       final success = await ApiService.commentTask(
 | |
|         id: projectId,
 | |
|         comment: commentController.text.trim(),
 | |
|         images: imageData,
 | |
|       ).timeout(const Duration(seconds: 30), onTimeout: () {
 | |
|         throw Exception("Request timed out.");
 | |
|       });
 | |
| 
 | |
|       if (success) {
 | |
|         logSafe("Comment added successfully");
 | |
|         _showSuccess("Task commented successfully!");
 | |
|         await taskController.fetchTaskData(projectId);
 | |
|       } else {
 | |
|         logSafe("API returned failure on commentTask", level: LogLevel.error);
 | |
|         _showError("Failed to comment task.");
 | |
|       }
 | |
|     } catch (e, st) {
 | |
|       logSafe("Error in commentTask: $e", level: LogLevel.error, error: e, stackTrace: st);
 | |
|       _showError("An error occurred while commenting the task.");
 | |
|     } finally {
 | |
|       isLoading.value = false;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   Future<void> fetchWorkStatuses() async {
 | |
|     logSafe("Fetching work statuses...");
 | |
|     isLoadingWorkStatus.value = true;
 | |
| 
 | |
|     final response = await ApiService.getWorkStatus();
 | |
|     if (response != null) {
 | |
|       final model = WorkStatusResponseModel.fromJson(response);
 | |
|       workStatus.assignAll(model.data);
 | |
|       logSafe("Fetched ${model.data.length} work statuses");
 | |
|     } else {
 | |
|       logSafe("No work statuses found or API call failed", level: LogLevel.warning);
 | |
|     }
 | |
| 
 | |
|     isLoadingWorkStatus.value = false;
 | |
|     update(['dashboard_controller']);
 | |
|   }
 | |
| 
 | |
|   Future<List<Map<String, dynamic>>?> _prepareImages(List<File>? images) async {
 | |
|     if (images == null || images.isEmpty) {
 | |
|       logSafe("_prepareImages: No images selected.");
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     logSafe("_prepareImages: Compressing and encoding images...");
 | |
|     final results = await Future.wait(images.map((file) async {
 | |
|       final compressedBytes = await compressImageToUnder100KB(file);
 | |
|       if (compressedBytes == null) return null;
 | |
| 
 | |
|       return {
 | |
|         "fileName": file.path.split('/').last,
 | |
|         "base64Data": base64Encode(compressedBytes),
 | |
|         "contentType": _getContentTypeFromFileName(file.path),
 | |
|         "fileSize": compressedBytes.lengthInBytes,
 | |
|         "description": "Image uploaded for task",
 | |
|       };
 | |
|     }));
 | |
| 
 | |
|     logSafe("_prepareImages: Prepared ${results.whereType<Map<String, dynamic>>().length} images.");
 | |
|     return results.whereType<Map<String, dynamic>>().toList();
 | |
|   }
 | |
| 
 | |
|   String _getContentTypeFromFileName(String fileName) {
 | |
|     final ext = fileName.split('.').last.toLowerCase();
 | |
|     return switch (ext) {
 | |
|       'jpg' || 'jpeg' => 'image/jpeg',
 | |
|       'png' => 'image/png',
 | |
|       'webp' => 'image/webp',
 | |
|       'gif' => 'image/gif',
 | |
|       _ => 'application/octet-stream',
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   Future<void> pickImages({required bool fromCamera}) async {
 | |
|     logSafe("Opening image picker...");
 | |
|     if (fromCamera) {
 | |
|       final pickedFile = await _picker.pickImage(source: ImageSource.camera, imageQuality: 75);
 | |
|       if (pickedFile != null) {
 | |
|         selectedImages.add(File(pickedFile.path));
 | |
|         logSafe("Image added from camera: ${pickedFile.path}",  );
 | |
|       }
 | |
|     } else {
 | |
|       final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
 | |
|       selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
 | |
|       logSafe("${pickedFiles.length} images added from gallery.",  );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void removeImageAt(int index) {
 | |
|     if (index >= 0 && index < selectedImages.length) {
 | |
|       logSafe("Removing image at index $index",  );
 | |
|       selectedImages.removeAt(index);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void _showError(String message) => showAppSnackbar(
 | |
|       title: "Error", message: message, type: SnackbarType.error);
 | |
| 
 | |
|   void _showSuccess(String message) => showAppSnackbar(
 | |
|       title: "Success", message: message, type: SnackbarType.success);
 | |
| }
 |