From 93a23508587bb9b85d3e3b1501b0ff0262b8a6ea Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Tue, 17 Jun 2025 15:41:23 +0530 Subject: [PATCH 01/19] feat: Add report action and comment functionality in daily task planning - Implemented ReportActionBottomSheet for reporting actions on tasks. - Created TaskActionButtons for handling report and comment actions. - Added WorkStatusResponseModel and WorkStatus model for managing work statuses. - Refactored DailyProgressReportScreen to utilize new action buttons for reporting and commenting. - Enhanced task data preparation for reporting and commenting actions. --- .../report_task_action_controller.dart | 308 +++++ lib/helpers/services/api_endpoints.dart | 2 + lib/helpers/services/api_service.dart | 112 +- .../comment_task_bottom_sheet.dart | 942 +++++++-------- .../create_task_botom_sheet.dart | 153 +++ .../report_action_bottom_sheet.dart | 1023 +++++++++++++++++ .../dailyTaskPlaning/task_action_buttons.dart | 195 ++++ .../dailyTaskPlaning/work_status_model.dart | 53 + lib/view/taskPlaning/daily_progress.dart | 219 +--- 9 files changed, 2287 insertions(+), 720 deletions(-) create mode 100644 lib/controller/task_planing/report_task_action_controller.dart create mode 100644 lib/model/dailyTaskPlaning/create_task_botom_sheet.dart create mode 100644 lib/model/dailyTaskPlaning/report_action_bottom_sheet.dart create mode 100644 lib/model/dailyTaskPlaning/task_action_buttons.dart create mode 100644 lib/model/dailyTaskPlaning/work_status_model.dart diff --git a/lib/controller/task_planing/report_task_action_controller.dart b/lib/controller/task_planing/report_task_action_controller.dart new file mode 100644 index 0000000..e01f4e6 --- /dev/null +++ b/lib/controller/task_planing/report_task_action_controller.dart @@ -0,0 +1,308 @@ +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:logger/logger.dart'; + +import 'package:marco/controller/my_controller.dart'; +import 'package:marco/controller/task_planing/daily_task_planing_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/dailyTaskPlaning/work_status_model.dart'; + +final Logger logger = Logger(); + +enum ApiStatus { idle, loading, success, failure } + +class ReportTaskActionController extends MyController { + // ──────────────────────────────────────────────── + // Reactive State + // ──────────────────────────────────────────────── + final RxBool isLoading = false.obs; + final Rx reportStatus = ApiStatus.idle.obs; + final Rx commentStatus = ApiStatus.idle.obs; + + final RxList selectedImages = [].obs; + final RxList workStatus = [].obs; + final RxList workStatuses = [].obs; + + final RxBool showAddTaskCheckbox = false.obs; + final RxBool isAddTaskChecked = false.obs; + + final RxBool isLoadingWorkStatus = false.obs; + final Rxn> selectedTask = Rxn>(); + + final RxString selectedWorkStatusName = ''.obs; + + // ──────────────────────────────────────────────── + // Controllers & Validators + // ──────────────────────────────────────────────── + final MyFormValidator basicValidator = MyFormValidator(); + final DailyTaskPlaningController taskController = Get.put(DailyTaskPlaningController()); + 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 get _allControllers => [ + assignedDateController, + workAreaController, + activityController, + teamSizeController, + taskIdController, + assignedController, + completedWorkController, + commentController, + assignedByController, + teamMembersController, + plannedWorkController, + approvedTaskController, + ]; + + // ──────────────────────────────────────────────── + // Lifecycle Hooks + // ──────────────────────────────────────────────── + @override + void onInit() { + super.onInit(); + logger.i("Initializing ReportTaskController..."); + _initializeFormFields(); + } + + @override + void onClose() { + for (final controller in _allControllers) { + controller.dispose(); + } + super.onClose(); + } + + // ──────────────────────────────────────────────── + // Form Field Setup + // ──────────────────────────────────────────────── + 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); + } + + // ──────────────────────────────────────────────── + // Task Approval Logic + // ──────────────────────────────────────────────── + Future approveTask({ + required String projectId, + required String comment, + required String reportActionId, + required String approvedTaskCount, + List? images, + }) async { + logger.i("Starting task approval..."); + logger.i("Project ID: $projectId"); + logger.i("Comment: $comment"); + logger.i("Report Action ID: $reportActionId"); + logger.i("Approved Task Count: $approvedTaskCount"); + + if (projectId.isEmpty || reportActionId.isEmpty) { + _showError("Project ID and Report Action ID are required."); + return false; + } + + final approvedTaskInt = int.tryParse(approvedTaskCount); + final completedWorkInt = int.tryParse(completedWorkController.text.trim()); + + if (approvedTaskInt == null) { + _showError("Invalid approved task count."); + return false; + } + + if (completedWorkInt != null && approvedTaskInt > completedWorkInt) { + _showError("Approved task count cannot exceed completed work."); + return false; + } + + if (comment.trim().isEmpty) { + _showError("Comment is required."); + return false; + } + + try { + reportStatus.value = ApiStatus.loading; + isLoading.value = true; + + final imageData = await _prepareImages(images); + + final success = await ApiService.approveTask( + id: projectId, + workStatus: reportActionId, + approvedTask: approvedTaskInt, + comment: comment, + images: imageData, + ); + + if (success) { + _showSuccess("Task approved successfully!"); + await taskController.fetchTaskData(projectId); + return true; + } else { + _showError("Failed to approve task."); + return false; + } + } catch (e) { + logger.e("Error approving task: $e"); + _showError("An error occurred."); + return false; + } finally { + isLoading.value = false; + Future.delayed(const Duration(milliseconds: 500), () { + reportStatus.value = ApiStatus.idle; + }); + } + } + + // ──────────────────────────────────────────────── + // Comment Task Logic + // ──────────────────────────────────────────────── + Future commentTask({ + required String projectId, + required String comment, + List? images, + }) async { + logger.i("Starting task comment..."); + + if (commentController.text.trim().isEmpty) { + _showError("Comment is required."); + return; + } + + try { + isLoading.value = true; + + final imageData = await _prepareImages(images); + + 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) { + _showSuccess("Task commented successfully!"); + await taskController.fetchTaskData(projectId); + } else { + _showError("Failed to comment task."); + } + } catch (e) { + logger.e("Error commenting task: $e"); + _showError("An error occurred while commenting the task."); + } finally { + isLoading.value = false; + } + } + + // ──────────────────────────────────────────────── + // API Helpers + // ──────────────────────────────────────────────── + Future fetchWorkStatuses() async { + isLoadingWorkStatus.value = true; + + final response = await ApiService.getWorkStatus(); + if (response != null) { + final model = WorkStatusResponseModel.fromJson(response); + workStatus.assignAll(model.data); + } else { + logger.w("No work statuses found or API call failed."); + } + + isLoadingWorkStatus.value = false; + update(['dashboard_controller']); + } + + Future>?> _prepareImages(List? images) async { + if (images == null || images.isEmpty) return null; + + 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", + }; + })); + + return results.whereType>().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', + }; + } + + // ──────────────────────────────────────────────── + // Image Picker Utils + // ──────────────────────────────────────────────── + Future pickImages({required bool fromCamera}) async { + if (fromCamera) { + final pickedFile = await _picker.pickImage( + source: ImageSource.camera, + imageQuality: 75, + ); + if (pickedFile != null) { + selectedImages.add(File(pickedFile.path)); + } + } else { + final pickedFiles = await _picker.pickMultiImage(imageQuality: 75); + selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path))); + } + } + + void removeImageAt(int index) { + if (index >= 0 && index < selectedImages.length) { + selectedImages.removeAt(index); + } + } + + // ──────────────────────────────────────────────── + // Snackbar Feedback + // ──────────────────────────────────────────────── + void _showError(String message) => showAppSnackbar( + title: "Error", message: message, type: SnackbarType.error); + + void _showSuccess(String message) => showAppSnackbar( + title: "Success", message: message, type: SnackbarType.success); +} diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index dba0bb0..44b5310 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -24,4 +24,6 @@ class ApiEndpoints { static const String commentTask = "/task/comment"; static const String dailyTaskDetails = "/project/details"; static const String assignDailyTask = "/task/assign"; + static const String getWorkStatus = "/master/work-status"; + static const String approveReportAction = "/task/approve"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 0b51c3a..b48f255 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -45,7 +45,8 @@ class ApiService { return null; } - static dynamic _parseResponseForAllData(http.Response response, {String label = ''}) { + static dynamic _parseResponseForAllData(http.Response response, + {String label = ''}) { _log("$label Response: ${response.body}"); try { final json = jsonDecode(response.body); @@ -67,15 +68,18 @@ class ApiService { String? token = await _getToken(); if (token == null) return null; - final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint").replace(queryParameters: queryParams); + final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint") + .replace(queryParameters: queryParams); _log("GET $uri"); try { - final response = await http.get(uri, headers: _headers(token)).timeout(timeout); + final response = + await http.get(uri, headers: _headers(token)).timeout(timeout); if (response.statusCode == 401 && !hasRetried) { _log("Unauthorized. Attempting token refresh..."); if (await AuthService.refreshToken()) { - return await _getRequest(endpoint, queryParams: queryParams, hasRetried: true); + return await _getRequest(endpoint, + queryParams: queryParams, hasRetried: true); } _log("Token refresh failed."); } @@ -106,7 +110,8 @@ class ApiService { if (response.statusCode == 401 && !hasRetried) { _log("Unauthorized POST. Attempting token refresh..."); if (await AuthService.refreshToken()) { - return await _postRequest(endpoint, body, customTimeout: customTimeout, hasRetried: true); + return await _postRequest(endpoint, body, + customTimeout: customTimeout, hasRetried: true); } } return response; @@ -119,14 +124,18 @@ class ApiService { // === Attendance APIs === static Future?> getProjects() async => - _getRequest(ApiEndpoints.getProjects).then((res) => res != null ? _parseResponse(res, label: 'Projects') : null); + _getRequest(ApiEndpoints.getProjects).then( + (res) => res != null ? _parseResponse(res, label: 'Projects') : null); static Future?> getGlobalProjects() async => - _getRequest(ApiEndpoints.getProjects).then((res) => res != null ? _parseResponse(res, label: 'Global Projects') : null); + _getRequest(ApiEndpoints.getProjects).then((res) => + res != null ? _parseResponse(res, label: 'Global Projects') : null); static Future?> getEmployeesByProject(String projectId) async => - _getRequest(ApiEndpoints.getEmployeesByProject, queryParams: {"projectId": projectId}) - .then((res) => res != null ? _parseResponse(res, label: 'Employees') : null); + _getRequest(ApiEndpoints.getEmployeesByProject, + queryParams: {"projectId": projectId}) + .then((res) => + res != null ? _parseResponse(res, label: 'Employees') : null); static Future?> getAttendanceLogs( String projectId, { @@ -135,20 +144,25 @@ class ApiService { }) async { final query = { "projectId": projectId, - if (dateFrom != null) "dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom), + if (dateFrom != null) + "dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom), if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo), }; - return _getRequest(ApiEndpoints.getAttendanceLogs, queryParams: query) - .then((res) => res != null ? _parseResponse(res, label: 'Attendance Logs') : null); + return _getRequest(ApiEndpoints.getAttendanceLogs, queryParams: query).then( + (res) => + res != null ? _parseResponse(res, label: 'Attendance Logs') : null); } static Future?> getAttendanceLogView(String id) async => - _getRequest("${ApiEndpoints.getAttendanceLogView}/$id") - .then((res) => res != null ? _parseResponse(res, label: 'Log Details') : null); + _getRequest("${ApiEndpoints.getAttendanceLogView}/$id").then((res) => + res != null ? _parseResponse(res, label: 'Log Details') : null); static Future?> getRegularizationLogs(String projectId) async => - _getRequest(ApiEndpoints.getRegularizationLogs, queryParams: {"projectId": projectId}) - .then((res) => res != null ? _parseResponse(res, label: 'Regularization Logs') : null); + _getRequest(ApiEndpoints.getRegularizationLogs, + queryParams: {"projectId": projectId}) + .then((res) => res != null + ? _parseResponse(res, label: 'Regularization Logs') + : null); static Future uploadAttendanceImage( String id, @@ -194,7 +208,8 @@ class ApiService { } } - final response = await _postRequest(ApiEndpoints.uploadAttendanceImage, body); + final response = + await _postRequest(ApiEndpoints.uploadAttendanceImage, body); if (response == null) return false; final json = jsonDecode(response.body); @@ -213,17 +228,22 @@ class ApiService { // === Employee APIs === - static Future?> getAllEmployeesByProject(String projectId) async { + static Future?> getAllEmployeesByProject( + String projectId) async { if (projectId.isEmpty) throw ArgumentError('projectId must not be empty'); final endpoint = "${ApiEndpoints.getAllEmployeesByProject}/$projectId"; - return _getRequest(endpoint).then((res) => res != null ? _parseResponse(res, label: 'Employees by Project') : null); + return _getRequest(endpoint).then((res) => res != null + ? _parseResponse(res, label: 'Employees by Project') + : null); } static Future?> getAllEmployees() async => - _getRequest(ApiEndpoints.getAllEmployees).then((res) => res != null ? _parseResponse(res, label: 'All Employees') : null); + _getRequest(ApiEndpoints.getAllEmployees).then((res) => + res != null ? _parseResponse(res, label: 'All Employees') : null); static Future?> getRoles() async => - _getRequest(ApiEndpoints.getRoles).then((res) => res != null ? _parseResponse(res, label: 'Roles') : null); + _getRequest(ApiEndpoints.getRoles).then( + (res) => res != null ? _parseResponse(res, label: 'Roles') : null); static Future createEmployee({ required String firstName, @@ -245,10 +265,13 @@ class ApiService { return response.statusCode == 200 && json['success'] == true; } - static Future?> getEmployeeDetails(String employeeId) async { + static Future?> getEmployeeDetails( + String employeeId) async { final url = "${ApiEndpoints.getEmployeeInfo}/$employeeId"; final response = await _getRequest(url); - final data = response != null ? _parseResponse(response, label: 'Employee Details') : null; + final data = response != null + ? _parseResponse(response, label: 'Employee Details') + : null; return data is Map ? data : null; } @@ -261,11 +284,13 @@ class ApiService { }) async { final query = { "projectId": projectId, - if (dateFrom != null) "dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom), + if (dateFrom != null) + "dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom), if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo), }; - return _getRequest(ApiEndpoints.getDailyTask, queryParams: query) - .then((res) => res != null ? _parseResponse(res, label: 'Daily Tasks') : null); + return _getRequest(ApiEndpoints.getDailyTask, queryParams: query).then( + (res) => + res != null ? _parseResponse(res, label: 'Daily Tasks') : null); } static Future reportTask({ @@ -313,11 +338,13 @@ class ApiService { return response.statusCode == 200 && json['success'] == true; } - static Future?> getDailyTasksDetails(String projectId) async { + static Future?> getDailyTasksDetails( + String projectId) async { final url = "${ApiEndpoints.dailyTaskDetails}/$projectId"; final response = await _getRequest(url); return response != null - ? _parseResponseForAllData(response, label: 'Daily Task Details') as Map? + ? _parseResponseForAllData(response, label: 'Daily Task Details') + as Map? : null; } @@ -333,7 +360,8 @@ class ApiService { "plannedTask": plannedTask, "description": description, "taskTeam": taskTeam, - "assignmentDate": (assignmentDate ?? DateTime.now()).toUtc().toIso8601String(), + "assignmentDate": + (assignmentDate ?? DateTime.now()).toUtc().toIso8601String(), }; final response = await _postRequest(ApiEndpoints.assignDailyTask, body); if (response == null) return false; @@ -345,4 +373,30 @@ class ApiService { _log("Failed to assign daily task: ${json['message'] ?? 'Unknown error'}"); return false; } + + static Future?> getWorkStatus() async => + _getRequest(ApiEndpoints.getWorkStatus).then((res) => res != null + ? _parseResponseForAllData(res, label: 'Work Status') + : null); + static Future approveTask({ + required String id, + required String comment, + required String workStatus, + required int approvedTask, + List>? images, + }) async { + final body = { + "id": id, + "workStatus": workStatus, + "approvedTask": approvedTask, + "comment": comment, + if (images != null && images.isNotEmpty) "images": images, + }; + + final response = await _postRequest(ApiEndpoints.approveReportAction, body); + if (response == null) return false; + + final json = jsonDecode(response.body); + return response.statusCode == 200 && json['success'] == true; + } } diff --git a/lib/model/dailyTaskPlaning/comment_task_bottom_sheet.dart b/lib/model/dailyTaskPlaning/comment_task_bottom_sheet.dart index 281e37a..a968260 100644 --- a/lib/model/dailyTaskPlaning/comment_task_bottom_sheet.dart +++ b/lib/model/dailyTaskPlaning/comment_task_bottom_sheet.dart @@ -11,6 +11,7 @@ import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/my_team_model_sheet.dart'; import 'package:intl/intl.dart'; import 'package:marco/helpers/widgets/image_viewer_dialog.dart'; +import 'dart:io'; class CommentTaskBottomSheet extends StatefulWidget { final Map taskData; @@ -182,87 +183,12 @@ class _CommentTaskBottomSheetState extends State if ((widget.taskData['reportedPreSignedUrls'] as List?) ?.isNotEmpty == - true) ...[ - MySpacing.height(8), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 0.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(Icons.image_outlined, - size: 18, color: Colors.grey[700]), - MySpacing.width(8), - MyText.titleSmall( - "Reported Images", - fontWeight: 600, - ), - ], - ), + true) + buildReportedImagesSection( + imageUrls: List.from( + widget.taskData['reportedPreSignedUrls'] ?? []), + context: context, ), - - MySpacing.height(8), - - Builder( - builder: (context) { - final allImageUrls = List.from( - widget.taskData['reportedPreSignedUrls'] ?? [], - ); - - if (allImageUrls.isEmpty) return const SizedBox(); - - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: - 16.0), // Same horizontal padding - child: SizedBox( - height: 70, - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: allImageUrls.length, - separatorBuilder: (_, __) => - const SizedBox(width: 12), - itemBuilder: (context, index) { - final url = allImageUrls[index]; - return GestureDetector( - onTap: () { - showDialog( - context: context, - barrierColor: Colors.black54, - builder: (_) => ImageViewerDialog( - imageSources: allImageUrls, - initialIndex: index, - ), - ); - }, - child: ClipRRect( - borderRadius: - BorderRadius.circular(12), - child: Image.network( - url, - width: 70, - height: 70, - fit: BoxFit.cover, - errorBuilder: - (context, error, stackTrace) => - Container( - width: 70, - height: 70, - color: Colors.grey.shade200, - child: Icon(Icons.broken_image, - color: Colors.grey[600]), - ), - ), - ), - ); - }, - ), - ), - ); - }, - ), - MySpacing.height(16), - ], Row( children: [ Icon(Icons.comment_outlined, @@ -313,184 +239,51 @@ class _CommentTaskBottomSheetState extends State ), Obx(() { final images = controller.selectedImages; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (images.isEmpty) - Container( - height: 70, - width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Colors.grey.shade300, width: 2), - color: Colors.grey.shade100, - ), - child: Center( - child: Icon(Icons.photo_camera_outlined, - size: 48, color: Colors.grey.shade400), - ), - ) - else - SizedBox( - height: 70, - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: images.length, - separatorBuilder: (context, index) => - SizedBox(height: 12), - itemBuilder: (context, index) { - final file = images[index]; - return Stack( - children: [ - GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (_) => - ImageViewerDialog( - imageSources: images, - initialIndex: index, - ), - ); - }, - child: ClipRRect( - borderRadius: - BorderRadius.circular(12), - child: Image.file( - file, - height: 70, - width: 70, - fit: BoxFit.cover, - ), - ), - ), - Positioned( - top: 4, - right: 4, - child: GestureDetector( - onTap: () => controller - .removeImageAt(index), - child: Container( - decoration: BoxDecoration( - color: Colors.black54, - shape: BoxShape.circle, - ), - child: Icon(Icons.close, - size: 20, - color: Colors.white), - ), - ), - ), - ], - ); - }, - ), + + return buildImagePickerSection( + images: images, + onCameraTap: () => + controller.pickImages(fromCamera: true), + onUploadTap: () => + controller.pickImages(fromCamera: false), + onRemoveImage: (index) => + controller.removeImageAt(index), + onPreviewImage: (index) { + showDialog( + context: context, + builder: (_) => ImageViewerDialog( + imageSources: images, + initialIndex: index, ), - MySpacing.height(16), - Row( - children: [ - Expanded( - child: MyButton.outlined( - onPressed: () => controller.pickImages( - fromCamera: true), - padding: MySpacing.xy(12, 10), - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Icon(Icons.camera_alt, - size: 16, - color: Colors.blueAccent), - MySpacing.width(6), - MyText.bodySmall('Capture', - color: Colors.blueAccent), - ], - ), - ), - ), - MySpacing.width(12), - Expanded( - child: MyButton.outlined( - onPressed: () => controller.pickImages( - fromCamera: false), - padding: MySpacing.xy(12, 10), - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Icon(Icons.upload_file, - size: 16, - color: Colors.blueAccent), - MySpacing.width(6), - MyText.bodySmall('Upload', - color: Colors.blueAccent), - ], - ), - ), - ), - ], - ), - ], + ); + }, ); }), MySpacing.height(24), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - MyButton.text( - onPressed: () => Navigator.of(context).pop(), - padding: MySpacing.xy(20, 16), - splashColor: contentTheme.secondary.withAlpha(25), - child: MyText.bodySmall('Cancel'), - ), - MySpacing.width(12), - Obx(() { - return MyButton( - onPressed: controller.isLoading.value - ? null - : () async { - if (controller.basicValidator - .validateForm()) { - await controller.commentTask( - projectId: controller.basicValidator - .getController('task_id') - ?.text ?? - '', - comment: controller.basicValidator - .getController('comment') - ?.text ?? - '', - images: controller.selectedImages, - ); - - if (widget.onCommentSuccess != null) { - widget.onCommentSuccess!(); - } - } - }, - elevation: 0, - padding: MySpacing.xy(20, 16), - backgroundColor: Colors.blueAccent, - borderRadiusAll: AppStyle.buttonRadius.medium, - child: controller.isLoading.value - ? SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: - AlwaysStoppedAnimation( - contentTheme.onPrimary), - ), - ) - : MyText.bodySmall( - 'Comment', - color: contentTheme.onPrimary, - ), + buildCommentActionButtons( + onCancel: () => Navigator.of(context).pop(), + onSubmit: () async { + if (controller.basicValidator.validateForm()) { + await controller.commentTask( + projectId: controller.basicValidator + .getController('task_id') + ?.text ?? + '', + comment: controller.basicValidator + .getController('comment') + ?.text ?? + '', + images: controller.selectedImages, ); - }), - ], + if (widget.onCommentSuccess != null) { + widget.onCommentSuccess!(); + } + } + }, + isLoading: controller.isLoading, + splashColor: contentTheme.secondary.withAlpha(25), + backgroundColor: Colors.blueAccent, + loadingIndicatorColor: contentTheme.onPrimary, ), MySpacing.height(10), if ((widget.taskData['taskComments'] as List?) @@ -508,257 +301,15 @@ class _CommentTaskBottomSheetState extends State ), ], ), - Divider(), MySpacing.height(12), Builder( builder: (context) { final comments = List>.from( widget.taskData['taskComments'] as List, ); - - comments.sort((a, b) { - final aDate = - DateTime.tryParse(a['date'] ?? '') ?? - DateTime.fromMillisecondsSinceEpoch(0); - final bDate = - DateTime.tryParse(b['date'] ?? '') ?? - DateTime.fromMillisecondsSinceEpoch(0); - return bDate.compareTo( - aDate); // descending: newest first - }); - - return SizedBox( - height: 300, - child: ListView.builder( - padding: const EdgeInsets.symmetric( - vertical: - 8), // Added padding around the list - itemCount: comments.length, - itemBuilder: (context, index) { - final comment = comments[index]; - final commentText = comment['text'] ?? '-'; - final commentedBy = - comment['commentedBy'] ?? 'Unknown'; - final relativeTime = - timeAgo(comment['date'] ?? ''); - // Dummy image URLs (simulate as if coming from backend) - final imageUrls = List.from( - comment['preSignedUrls'] ?? []); - return Container( - margin: const EdgeInsets.symmetric( - vertical: 8), // Spacing between items - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.grey.shade200, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - // 🔹 Top Row: Avatar + Name + Time - Row( - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - Avatar( - firstName: commentedBy - .split(' ') - .first, - lastName: commentedBy - .split(' ') - .length > - 1 - ? commentedBy - .split(' ') - .last - : '', - size: 32, - ), - const SizedBox(width: 12), - Expanded( - child: Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - MyText.bodyMedium( - commentedBy, - fontWeight: 700, - color: - Colors.black87, - ), - MyText.bodySmall( - relativeTime, - fontSize: 12, - color: - Colors.black54, - ), - ], - ), - ), - ], - ), - const SizedBox(height: 12), - // 🔹 Comment text below attachments - Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - MyText.bodyMedium( - commentText, - fontWeight: 500, - color: Colors.black87, - ), - ], - ), - const SizedBox(height: 12), - // 🔹 Attachments row: full width below top row - if (imageUrls.isNotEmpty) ...[ - Row( - crossAxisAlignment: - CrossAxisAlignment - .start, - children: [ - Icon( - Icons - .attach_file_outlined, - size: 18, - color: - Colors.grey[700]), - MyText.bodyMedium( - 'Attachments', - fontWeight: 600, - color: Colors.black87, - ), - ], - ), - const SizedBox(height: 8), - SizedBox( - height: 60, - child: ListView.separated( - padding: const EdgeInsets - .symmetric( - horizontal: 0), - scrollDirection: - Axis.horizontal, - itemCount: - imageUrls.length, - itemBuilder: (context, - imageIndex) { - final imageUrl = - imageUrls[ - imageIndex]; - return GestureDetector( - onTap: () { - showDialog( - context: context, - barrierColor: - Colors - .black54, - builder: (_) => - ImageViewerDialog( - imageSources: - imageUrls, - initialIndex: - imageIndex, - ), - ); - }, - child: Stack( - children: [ - Container( - width: 60, - height: 60, - decoration: - BoxDecoration( - borderRadius: - BorderRadius - .circular( - 12), - color: Colors - .grey[ - 100], - boxShadow: [ - BoxShadow( - color: Colors - .black26, - blurRadius: - 6, - offset: - Offset( - 2, - 2), - ), - ], - ), - child: - ClipRRect( - borderRadius: - BorderRadius - .circular( - 12), - child: Image - .network( - imageUrl, - fit: BoxFit - .cover, - errorBuilder: (context, - error, - stackTrace) => - Container( - color: Colors - .grey[ - 300], - child: Icon( - Icons - .broken_image, - color: - Colors.grey[700]), - ), - ), - ), - ), - const Positioned( - right: 4, - bottom: 4, - child: Icon( - Icons - .zoom_in, - color: Colors - .white70, - size: 16), - ), - ], - ), - ); - }, - separatorBuilder: - (_, __) => - const SizedBox( - width: 12), - ), - ), - const SizedBox(height: 12), - ], - ], - ), - ), - ], - ), - ); - }, - ), - ); + return buildCommentList(comments, context); }, - ), + ) ], ], ), @@ -772,6 +323,79 @@ class _CommentTaskBottomSheetState extends State ); } + Widget buildReportedImagesSection({ + required List imageUrls, + required BuildContext context, + String title = "Reported Images", + }) { + if (imageUrls.isEmpty) return const SizedBox(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MySpacing.height(8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 0.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(Icons.image_outlined, size: 18, color: Colors.grey[700]), + MySpacing.width(8), + MyText.titleSmall( + title, + fontWeight: 600, + ), + ], + ), + ), + MySpacing.height(8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: SizedBox( + height: 70, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: imageUrls.length, + separatorBuilder: (_, __) => const SizedBox(width: 12), + itemBuilder: (context, index) { + final url = imageUrls[index]; + return GestureDetector( + onTap: () { + showDialog( + context: context, + barrierColor: Colors.black54, + builder: (_) => ImageViewerDialog( + imageSources: imageUrls, + initialIndex: index, + ), + ); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + url, + width: 70, + height: 70, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => Container( + width: 70, + height: 70, + color: Colors.grey.shade200, + child: + Icon(Icons.broken_image, color: Colors.grey[600]), + ), + ), + ), + ); + }, + ), + ), + ), + MySpacing.height(16), + ], + ); + } + Widget buildTeamMembers() { final teamMembersText = controller.basicValidator.getController('team_members')?.text ?? ''; @@ -837,6 +461,53 @@ class _CommentTaskBottomSheetState extends State ); } + Widget buildCommentActionButtons({ + required VoidCallback onCancel, + required Future Function() onSubmit, + required RxBool isLoading, + required Color splashColor, + required Color backgroundColor, + required Color loadingIndicatorColor, + double? buttonHeight, + }) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + MyButton.text( + onPressed: onCancel, + padding: MySpacing.xy(20, 16), + splashColor: splashColor, + child: MyText.bodySmall('Cancel'), + ), + MySpacing.width(12), + Obx(() { + return MyButton( + onPressed: isLoading.value ? null : onSubmit, + elevation: 0, + padding: MySpacing.xy(20, 16), + backgroundColor: backgroundColor, + borderRadiusAll: AppStyle.buttonRadius.medium, + child: isLoading.value + ? SizedBox( + width: buttonHeight ?? 16, + height: buttonHeight ?? 16, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + loadingIndicatorColor, + ), + ), + ) + : MyText.bodySmall( + 'Comment', + color: loadingIndicatorColor, + ), + ); + }), + ], + ); + } + Widget buildRow(String label, String? value, {IconData? icon}) { return Padding( padding: const EdgeInsets.only(bottom: 16), @@ -860,4 +531,281 @@ class _CommentTaskBottomSheetState extends State ), ); } + + Widget buildCommentList( + List> comments, BuildContext context) { + comments.sort((a, b) { + final aDate = DateTime.tryParse(a['date'] ?? '') ?? + DateTime.fromMillisecondsSinceEpoch(0); + final bDate = DateTime.tryParse(b['date'] ?? '') ?? + DateTime.fromMillisecondsSinceEpoch(0); + return bDate.compareTo(aDate); // newest first + }); + + return SizedBox( + height: 300, + child: ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: comments.length, + itemBuilder: (context, index) { + final comment = comments[index]; + final commentText = comment['text'] ?? '-'; + final commentedBy = comment['commentedBy'] ?? 'Unknown'; + final relativeTime = timeAgo(comment['date'] ?? ''); + final imageUrls = List.from(comment['preSignedUrls'] ?? []); + + return Container( + margin: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Avatar( + firstName: commentedBy.split(' ').first, + lastName: commentedBy.split(' ').length > 1 + ? commentedBy.split(' ').last + : '', + size: 32, + ), + const SizedBox(width: 12), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MyText.bodyMedium( + commentedBy, + fontWeight: 700, + color: Colors.black87, + ), + MyText.bodySmall( + relativeTime, + fontSize: 12, + color: Colors.black54, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: MyText.bodyMedium( + commentText, + fontWeight: 500, + color: Colors.black87, + ), + ), + ], + ), + const SizedBox(height: 12), + if (imageUrls.isNotEmpty) ...[ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.attach_file_outlined, + size: 18, color: Colors.grey[700]), + MyText.bodyMedium( + 'Attachments', + fontWeight: 600, + color: Colors.black87, + ), + ], + ), + const SizedBox(height: 8), + SizedBox( + height: 60, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: imageUrls.length, + itemBuilder: (context, imageIndex) { + final imageUrl = imageUrls[imageIndex]; + return GestureDetector( + onTap: () { + showDialog( + context: context, + barrierColor: Colors.black54, + builder: (_) => ImageViewerDialog( + imageSources: imageUrls, + initialIndex: imageIndex, + ), + ); + }, + child: Stack( + children: [ + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.grey[100], + boxShadow: [ + BoxShadow( + color: Colors.black26, + blurRadius: 6, + offset: Offset(2, 2), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + imageUrl, + fit: BoxFit.cover, + errorBuilder: + (context, error, stackTrace) => + Container( + color: Colors.grey[300], + child: Icon(Icons.broken_image, + color: Colors.grey[700]), + ), + ), + ), + ), + const Positioned( + right: 4, + bottom: 4, + child: Icon(Icons.zoom_in, + color: Colors.white70, size: 16), + ), + ], + ), + ); + }, + separatorBuilder: (_, __) => + const SizedBox(width: 12), + ), + ), + const SizedBox(height: 12), + ], + ], + ), + ), + ], + ), + ); + }, + ), + ); + } + + Widget buildImagePickerSection({ + required List images, + required VoidCallback onCameraTap, + required VoidCallback onUploadTap, + required void Function(int index) onRemoveImage, + required void Function(int initialIndex) onPreviewImage, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (images.isEmpty) + Container( + height: 70, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300, width: 2), + color: Colors.grey.shade100, + ), + child: Center( + child: Icon(Icons.photo_camera_outlined, + size: 48, color: Colors.grey.shade400), + ), + ) + else + SizedBox( + height: 70, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: images.length, + separatorBuilder: (_, __) => const SizedBox(width: 12), + itemBuilder: (context, index) { + final file = images[index]; + return Stack( + children: [ + GestureDetector( + onTap: () => onPreviewImage(index), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.file( + file, + height: 70, + width: 70, + fit: BoxFit.cover, + ), + ), + ), + Positioned( + top: 4, + right: 4, + child: GestureDetector( + onTap: () => onRemoveImage(index), + child: Container( + decoration: const BoxDecoration( + color: Colors.black54, + shape: BoxShape.circle, + ), + child: const Icon(Icons.close, + size: 20, color: Colors.white), + ), + ), + ), + ], + ); + }, + ), + ), + MySpacing.height(16), + Row( + children: [ + Expanded( + child: MyButton.outlined( + onPressed: onCameraTap, + padding: MySpacing.xy(12, 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.camera_alt, + size: 16, color: Colors.blueAccent), + MySpacing.width(6), + MyText.bodySmall('Capture', color: Colors.blueAccent), + ], + ), + ), + ), + MySpacing.width(12), + Expanded( + child: MyButton.outlined( + onPressed: onUploadTap, + padding: MySpacing.xy(12, 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.upload_file, + size: 16, color: Colors.blueAccent), + MySpacing.width(6), + MyText.bodySmall('Upload', color: Colors.blueAccent), + ], + ), + ), + ), + ], + ), + ], + ); + } } diff --git a/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart b/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart new file mode 100644 index 0000000..196434c --- /dev/null +++ b/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart @@ -0,0 +1,153 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; + +void showManageTaskBottomSheet({ + required String building, + required String floor, + required String workArea, + required String activity, + required String plannedWork, + required String completedWork, + required String unit, + required Function(String) onCategoryChanged, + required VoidCallback onSubmit, +}) { + final List categories = ["Fresh Work", "Repair", "Demo"]; + String selectedCategory = 'Fresh Work'; + + Get.bottomSheet( + StatefulBuilder( + builder: (context, setState) { + return LayoutBuilder( + builder: (context, constraints) { + final isLarge = constraints.maxWidth > 600; + final horizontalPadding = isLarge ? constraints.maxWidth * 0.2 : 16.0; + + return SafeArea( + child: Container( + constraints: const BoxConstraints(maxHeight: 0.95 * 800), + padding: EdgeInsets.fromLTRB(horizontalPadding, 12, horizontalPadding, 24), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Draggable handle + Center( + child: Container( + width: 40, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.grey.shade400, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + + _infoRowWithIcon(Icons.location_city, "Selected Building", building), + _infoRowWithIcon(Icons.apartment, "Selected Floor", floor), + _infoRowWithIcon(Icons.workspaces, "Selected Work Area", workArea), + _infoRowWithIcon(Icons.list_alt, "Selected Activity", activity), + + const SizedBox(height: 12), + Row( + children: [ + Icon(Icons.category_outlined, color: Colors.grey[700], size: 18), + const SizedBox(width: 8), + MyText.bodyMedium("Selected Work Category", fontWeight: 600), + ], + ), + const SizedBox(height: 6), + + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + ), + child: PopupMenuButton( + padding: EdgeInsets.zero, + onSelected: (val) { + setState(() { + selectedCategory = val; + }); + onCategoryChanged(val); + }, + itemBuilder: (context) => categories + .map((e) => PopupMenuItem(value: e, child: Text(e))) + .toList(), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + selectedCategory, + style: TextStyle(fontSize: 14, color: Colors.black87), + ), + const Icon(Icons.arrow_drop_down), + ], + ), + ), + ), + + const SizedBox(height: 12), + _infoRowWithIcon(Icons.edit_calendar, "Planned Work", plannedWork), + _infoRowWithIcon(Icons.check_circle_outline, "Completed Work", completedWork), + _infoRowWithIcon(Icons.straighten, "Unit", unit), + + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + onPressed: onSubmit, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueAccent, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + child: const Text( + "Submit", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + }, + ), + isScrollControlled: true, + ); +} + +Widget _infoRowWithIcon(IconData icon, String title, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, color: Colors.grey[700], size: 18), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyMedium(title, fontWeight: 600), + const SizedBox(height: 2), + MyText.bodySmall(value, color: Colors.grey[800]), + ], + ), + ), + ], + ), + ); +} diff --git a/lib/model/dailyTaskPlaning/report_action_bottom_sheet.dart b/lib/model/dailyTaskPlaning/report_action_bottom_sheet.dart new file mode 100644 index 0000000..f4669de --- /dev/null +++ b/lib/model/dailyTaskPlaning/report_action_bottom_sheet.dart @@ -0,0 +1,1023 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/controller/task_planing/report_task_action_controller.dart'; +import 'package:marco/helpers/theme/app_theme.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/widgets/my_button.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_text_style.dart'; +import 'package:marco/helpers/widgets/avatar.dart'; +import 'package:marco/helpers/widgets/my_team_model_sheet.dart'; +import 'package:intl/intl.dart'; +import 'package:marco/helpers/widgets/image_viewer_dialog.dart'; +import 'package:marco/model/dailyTaskPlaning/create_task_botom_sheet.dart'; +import 'dart:io'; + +class ReportActionBottomSheet extends StatefulWidget { + final Map taskData; + final VoidCallback? onCommentSuccess; + + const ReportActionBottomSheet({ + super.key, + required this.taskData, + this.onCommentSuccess, + }); + + @override + State createState() => + _ReportActionBottomSheetState(); +} + +class _Member { + final String firstName; + _Member(this.firstName); +} + +class _ReportActionBottomSheetState extends State + with UIMixin { + late ReportTaskActionController controller; + + final ScrollController _scrollController = ScrollController(); + String selectedAction = 'Select Action'; + @override + void initState() { + super.initState(); + controller = Get.put( + ReportTaskActionController(), + tag: widget.taskData['taskId'] ?? '', + ); + controller.fetchWorkStatuses(); + controller.basicValidator.getController('approved_task')?.text = + widget.taskData['approvedTask']?.toString() ?? ''; + final data = widget.taskData; + controller.basicValidator.getController('assigned_date')?.text = + data['assignedOn'] ?? ''; + controller.basicValidator.getController('assigned_by')?.text = + data['assignedBy'] ?? ''; + controller.basicValidator.getController('work_area')?.text = + data['location'] ?? ''; + controller.basicValidator.getController('activity')?.text = + data['activity'] ?? ''; + controller.basicValidator.getController('planned_work')?.text = + data['plannedWork'] ?? ''; + controller.basicValidator.getController('completed_work')?.text = + data['completedWork'] ?? ''; + controller.basicValidator.getController('team_members')?.text = + (data['teamMembers'] as List).join(', '); + controller.basicValidator.getController('assigned')?.text = + data['assigned'] ?? ''; + controller.basicValidator.getController('task_id')?.text = + data['taskId'] ?? ''; + controller.basicValidator.getController('comment')?.clear(); + + controller.selectedImages.clear(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + _scrollController.jumpTo(_scrollController.position.maxScrollExtent); + } + }); + } + + String timeAgo(String dateString) { + try { + DateTime date = DateTime.parse(dateString + "Z").toLocal(); + final now = DateTime.now(); + final difference = now.difference(date); + if (difference.inDays > 8) { + return DateFormat('dd-MM-yyyy').format(date); + } else if (difference.inDays >= 1) { + return '${difference.inDays} day${difference.inDays > 1 ? 's' : ''} ago'; + } else if (difference.inHours >= 1) { + return '${difference.inHours} hr${difference.inHours > 1 ? 's' : ''} ago'; + } else if (difference.inMinutes >= 1) { + return '${difference.inMinutes} min${difference.inMinutes > 1 ? 's' : ''} ago'; + } else { + return 'just now'; + } + } catch (e) { + print('Error parsing date: $e'); + return ''; + } + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + ), + child: SingleChildScrollView( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom + 24, + left: 24, + right: 24, + top: 12, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Drag handle + Container( + width: 40, + height: 4, + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: Colors.grey.shade400, + borderRadius: BorderRadius.circular(2), + ), + ), + GetBuilder( + tag: widget.taskData['taskId'] ?? '', + builder: (controller) { + return Form( + key: controller.basicValidator.formKey, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4.0, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MyText.titleMedium( + "Take Report Action", + fontWeight: 600, + fontSize: 18, + ), + ], + ), + MySpacing.height(24), + buildRow( + "Assigned By", + controller.basicValidator + .getController('assigned_by') + ?.text + .trim(), + icon: Icons.person_outline, + ), + buildRow( + "Work Area", + controller.basicValidator + .getController('work_area') + ?.text + .trim(), + icon: Icons.place_outlined, + ), + buildRow( + "Activity", + controller.basicValidator + .getController('activity') + ?.text + .trim(), + icon: Icons.assignment_outlined, + ), + buildRow( + "Planned Work", + controller.basicValidator + .getController('planned_work') + ?.text + .trim(), + icon: Icons.schedule_outlined, + ), + buildRow( + "Completed Work", + controller.basicValidator + .getController('completed_work') + ?.text + .trim(), + icon: Icons.done_all_outlined, + ), + buildTeamMembers(), + MySpacing.height(8), + Row( + children: [ + Icon(Icons.check_circle_outline, + size: 18, color: Colors.grey[700]), + MySpacing.width(8), + MyText.titleSmall( + "Approved Task:", + fontWeight: 600, + ), + ], + ), + MySpacing.height(10), + TextFormField( + controller: controller.basicValidator + .getController('approved_task'), + keyboardType: TextInputType.number, + validator: (value) { + if (value == null || value.isEmpty) + return 'Required'; + if (int.tryParse(value) == null) + return 'Must be a number'; + return null; + }, + decoration: InputDecoration( + hintText: "eg: 5", + hintStyle: MyTextStyle.bodySmall(xMuted: true), + border: outlineInputBorder, + enabledBorder: outlineInputBorder, + focusedBorder: focusedInputBorder, + contentPadding: MySpacing.all(16), + isCollapsed: true, + floatingLabelBehavior: FloatingLabelBehavior.never, + ), + ), + MySpacing.height(10), + + if ((widget.taskData['reportedPreSignedUrls'] + as List?) + ?.isNotEmpty == + true) + buildReportedImagesSection( + imageUrls: List.from( + widget.taskData['reportedPreSignedUrls'] ?? []), + context: context, + ), + MySpacing.height(10), + // Add this in your stateful widget + MyText.titleSmall( + "Report Actions", + fontWeight: 600, + ), + MySpacing.height(10), + + Obx(() { + final isLoading = + controller.isLoadingWorkStatus.value; + final workStatuses = controller.workStatus; + + if (isLoading) { + return const Center( + child: CircularProgressIndicator()); + } + + return PopupMenuButton( + onSelected: (String value) { + controller.selectedWorkStatusName.value = value; + controller.showAddTaskCheckbox.value = true; + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + itemBuilder: (BuildContext context) { + return workStatuses.map((status) { + final statusName = status.name; + + return PopupMenuItem( + value: statusName, + child: Row( + children: [ + Radio( + value: statusName, + groupValue: controller + .selectedWorkStatusName.value, + onChanged: (_) => + Navigator.pop(context, statusName), + ), + const SizedBox(width: 8), + MyText.bodySmall(statusName), + ], + ), + ); + }).toList(); + }, + child: Container( + padding: MySpacing.xy(16, 12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade400), + borderRadius: BorderRadius.circular( + AppStyle.buttonRadius.medium), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + MyText.bodySmall( + controller.selectedWorkStatusName.value + .isEmpty + ? "Select Work Status" + : controller + .selectedWorkStatusName.value, + color: Colors.black87, + ), + const Icon(Icons.arrow_drop_down, size: 20), + ], + ), + ), + ); + }), + MySpacing.height(10), + Obx(() { + if (!controller.showAddTaskCheckbox.value) + return SizedBox.shrink(); + + final checkboxTheme = CheckboxThemeData( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2)), + side: WidgetStateBorderSide.resolveWith((states) => + BorderSide( + color: states.contains(WidgetState.selected) + ? Colors.transparent + : Colors.black)), + fillColor: WidgetStateProperty.resolveWith( + (states) => + states.contains(WidgetState.selected) + ? Colors.blueAccent + : Colors.white), + checkColor: + WidgetStateProperty.all(Colors.white), + ); + + return Theme( + data: Theme.of(context) + .copyWith(checkboxTheme: checkboxTheme), + child: CheckboxListTile( + title: MyText.titleSmall( + "Add new task", + fontWeight: 600, + ), + value: controller.isAddTaskChecked.value, + onChanged: (val) => controller + .isAddTaskChecked.value = val ?? false, + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + ), + ); + }), + MySpacing.height(10), + Row( + children: [ + Icon(Icons.comment_outlined, + size: 18, color: Colors.grey[700]), + MySpacing.width(8), + MyText.titleSmall( + "Comment:", + fontWeight: 600, + ), + ], + ), + MySpacing.height(8), + TextFormField( + validator: controller.basicValidator + .getValidation('comment'), + controller: controller.basicValidator + .getController('comment'), + keyboardType: TextInputType.text, + decoration: InputDecoration( + hintText: "eg: Work done successfully", + hintStyle: MyTextStyle.bodySmall(xMuted: true), + border: outlineInputBorder, + enabledBorder: outlineInputBorder, + focusedBorder: focusedInputBorder, + contentPadding: MySpacing.all(16), + isCollapsed: true, + floatingLabelBehavior: FloatingLabelBehavior.never, + ), + ), + MySpacing.height(16), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.camera_alt_outlined, + size: 18, color: Colors.grey[700]), + MySpacing.width(8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleSmall("Attach Photos:", + fontWeight: 600), + MySpacing.height(12), + ], + ), + ), + ], + ), + Obx(() { + final images = controller.selectedImages; + + return buildImagePickerSection( + images: images, + onCameraTap: () => + controller.pickImages(fromCamera: true), + onUploadTap: () => + controller.pickImages(fromCamera: false), + onRemoveImage: (index) => + controller.removeImageAt(index), + onPreviewImage: (index) { + showDialog( + context: context, + builder: (_) => ImageViewerDialog( + imageSources: images, + initialIndex: index, + ), + ); + }, + ); + }), + MySpacing.height(24), + buildCommentActionButtons( + onCancel: () => Navigator.of(context).pop(), + onSubmit: () async { + if (controller.basicValidator.validateForm()) { + final selectedStatusName = + controller.selectedWorkStatusName.value; + final selectedStatus = + controller.workStatus.firstWhereOrNull( + (status) => status.name == selectedStatusName, + ); + + final reportActionId = + selectedStatus?.id.toString() ?? ''; + final approvedTaskCount = controller + .basicValidator + .getController('approved_task') + ?.text + .trim() ?? + ''; + + final shouldShowAddTaskSheet = + controller.isAddTaskChecked.value; + + final success = await controller.approveTask( + projectId: controller.basicValidator + .getController('task_id') + ?.text ?? + '', + comment: controller.basicValidator + .getController('comment') + ?.text ?? + '', + images: controller.selectedImages, + reportActionId: reportActionId, + approvedTaskCount: approvedTaskCount, + ); + + if (success) { + Navigator.of(context).pop(); + if (shouldShowAddTaskSheet) { + await Future.delayed(Duration( + milliseconds: + 100)); // Ensure animation completes + showManageTaskBottomSheet( + building: widget.taskData['building'] ?? '', + floor: widget.taskData['floor'] ?? '', + workArea: widget.taskData['location'] ?? '', + activity: widget.taskData['activity'] ?? '', + plannedWork: + widget.taskData['plannedWork'] ?? '', + completedWork: + widget.taskData['completedWork'] ?? '', + unit: widget.taskData['unit'] ?? '', + onCategoryChanged: (category) { + debugPrint( + "Category changed to: $category"); + }, + onSubmit: () { + Navigator.of(context) + .pop(); // Close second sheet + }, + ); + } + + // Optional callback after comment success + widget.onCommentSuccess?.call(); + } + } + }, + isLoading: controller.isLoading, + splashColor: contentTheme.secondary.withAlpha(25), + backgroundColor: Colors.blueAccent, + loadingIndicatorColor: contentTheme.onPrimary, + ), + + MySpacing.height(10), + if ((widget.taskData['taskComments'] as List?) + ?.isNotEmpty == + true) ...[ + Row( + children: [ + MySpacing.width(10), + Icon(Icons.chat_bubble_outline, + size: 18, color: Colors.grey[700]), + MySpacing.width(8), + MyText.titleSmall( + "Comments", + fontWeight: 600, + ), + ], + ), + MySpacing.height(12), + Builder( + builder: (context) { + final comments = List>.from( + widget.taskData['taskComments'] as List, + ); + return buildCommentList(comments, context); + }, + ) + ], + ], + ), + ), + ); + }, + ), + ], + ), + ), + ); + } + + Widget buildReportedImagesSection({ + required List imageUrls, + required BuildContext context, + String title = "Reported Images", + }) { + if (imageUrls.isEmpty) return const SizedBox(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MySpacing.height(8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 0.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(Icons.image_outlined, size: 18, color: Colors.grey[700]), + MySpacing.width(8), + MyText.titleSmall( + title, + fontWeight: 600, + ), + ], + ), + ), + MySpacing.height(8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: SizedBox( + height: 70, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: imageUrls.length, + separatorBuilder: (_, __) => const SizedBox(width: 12), + itemBuilder: (context, index) { + final url = imageUrls[index]; + return GestureDetector( + onTap: () { + showDialog( + context: context, + barrierColor: Colors.black54, + builder: (_) => ImageViewerDialog( + imageSources: imageUrls, + initialIndex: index, + ), + ); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + url, + width: 70, + height: 70, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => Container( + width: 70, + height: 70, + color: Colors.grey.shade200, + child: + Icon(Icons.broken_image, color: Colors.grey[600]), + ), + ), + ), + ); + }, + ), + ), + ), + MySpacing.height(16), + ], + ); + } + + Widget buildTeamMembers() { + final teamMembersText = + controller.basicValidator.getController('team_members')?.text ?? ''; + final members = teamMembersText + .split(',') + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .toList(); + + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + MyText.titleSmall( + "Team Members:", + fontWeight: 600, + ), + MySpacing.width(12), + GestureDetector( + onTap: () { + TeamBottomSheet.show( + context: context, + teamMembers: members.map((name) => _Member(name)).toList(), + ); + }, + child: SizedBox( + height: 32, + width: 100, + child: Stack( + children: [ + for (int i = 0; i < members.length.clamp(0, 3); i++) + Positioned( + left: i * 24.0, + child: Tooltip( + message: members[i], + child: Avatar( + firstName: members[i], + lastName: '', + size: 32, + ), + ), + ), + if (members.length > 3) + Positioned( + left: 2 * 24.0, + child: CircleAvatar( + radius: 16, + backgroundColor: Colors.grey.shade300, + child: MyText.bodyMedium( + '+${members.length - 3}', + style: const TextStyle( + fontSize: 12, color: Colors.black87), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget buildCommentActionButtons({ + required VoidCallback onCancel, + required Future Function() onSubmit, + required RxBool isLoading, + required Color splashColor, + required Color backgroundColor, + required Color loadingIndicatorColor, + double? buttonHeight, + }) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + MyButton.text( + onPressed: onCancel, + padding: MySpacing.xy(20, 16), + splashColor: splashColor, + child: MyText.bodySmall('Cancel'), + ), + MySpacing.width(12), + Obx(() { + return MyButton( + onPressed: isLoading.value ? null : onSubmit, + elevation: 0, + padding: MySpacing.xy(20, 16), + backgroundColor: backgroundColor, + borderRadiusAll: AppStyle.buttonRadius.medium, + child: isLoading.value + ? SizedBox( + width: buttonHeight ?? 16, + height: buttonHeight ?? 16, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + loadingIndicatorColor, + ), + ), + ) + : MyText.bodySmall( + 'Submit Report', + color: loadingIndicatorColor, + ), + ); + }), + ], + ); + } + + Widget buildRow(String label, String? value, {IconData? icon}) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (icon != null) + Padding( + padding: const EdgeInsets.only(right: 8.0, top: 2), + child: Icon(icon, size: 18, color: Colors.grey[700]), + ), + MyText.titleSmall( + "$label:", + fontWeight: 600, + ), + MySpacing.width(12), + Expanded( + child: MyText.bodyMedium(value?.isNotEmpty == true ? value! : "-"), + ), + ], + ), + ); + } + + Widget buildCommentList( + List> comments, BuildContext context) { + comments.sort((a, b) { + final aDate = DateTime.tryParse(a['date'] ?? '') ?? + DateTime.fromMillisecondsSinceEpoch(0); + final bDate = DateTime.tryParse(b['date'] ?? '') ?? + DateTime.fromMillisecondsSinceEpoch(0); + return bDate.compareTo(aDate); // newest first + }); + + return SizedBox( + height: 300, + child: ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: comments.length, + itemBuilder: (context, index) { + final comment = comments[index]; + final commentText = comment['text'] ?? '-'; + final commentedBy = comment['commentedBy'] ?? 'Unknown'; + final relativeTime = timeAgo(comment['date'] ?? ''); + final imageUrls = List.from(comment['preSignedUrls'] ?? []); + + return Container( + margin: const EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Avatar( + firstName: commentedBy.split(' ').first, + lastName: commentedBy.split(' ').length > 1 + ? commentedBy.split(' ').last + : '', + size: 32, + ), + const SizedBox(width: 12), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MyText.bodyMedium( + commentedBy, + fontWeight: 700, + color: Colors.black87, + ), + MyText.bodySmall( + relativeTime, + fontSize: 12, + color: Colors.black54, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: MyText.bodyMedium( + commentText, + fontWeight: 500, + color: Colors.black87, + maxLines: null, + ), + ), + ], + ), + const SizedBox(height: 12), + if (imageUrls.isNotEmpty) ...[ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.attach_file_outlined, + size: 18, color: Colors.grey[700]), + MyText.bodyMedium( + 'Attachments', + fontWeight: 600, + color: Colors.black87, + ), + ], + ), + const SizedBox(height: 8), + SizedBox( + height: 60, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: imageUrls.length, + itemBuilder: (context, imageIndex) { + final imageUrl = imageUrls[imageIndex]; + return GestureDetector( + onTap: () { + showDialog( + context: context, + barrierColor: Colors.black54, + builder: (_) => ImageViewerDialog( + imageSources: imageUrls, + initialIndex: imageIndex, + ), + ); + }, + child: Stack( + children: [ + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.grey[100], + boxShadow: [ + BoxShadow( + color: Colors.black26, + blurRadius: 6, + offset: Offset(2, 2), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + imageUrl, + fit: BoxFit.cover, + errorBuilder: + (context, error, stackTrace) => + Container( + color: Colors.grey[300], + child: Icon(Icons.broken_image, + color: Colors.grey[700]), + ), + ), + ), + ), + const Positioned( + right: 4, + bottom: 4, + child: Icon(Icons.zoom_in, + color: Colors.white70, size: 16), + ), + ], + ), + ); + }, + separatorBuilder: (_, __) => + const SizedBox(width: 12), + ), + ), + const SizedBox(height: 12), + ], + ], + ), + ), + ], + ), + ); + }, + ), + ); + } + + Widget buildImagePickerSection({ + required List images, + required VoidCallback onCameraTap, + required VoidCallback onUploadTap, + required void Function(int index) onRemoveImage, + required void Function(int initialIndex) onPreviewImage, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (images.isEmpty) + Container( + height: 70, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300, width: 2), + color: Colors.grey.shade100, + ), + child: Center( + child: Icon(Icons.photo_camera_outlined, + size: 48, color: Colors.grey.shade400), + ), + ) + else + SizedBox( + height: 70, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: images.length, + separatorBuilder: (_, __) => const SizedBox(width: 12), + itemBuilder: (context, index) { + final file = images[index]; + return Stack( + children: [ + GestureDetector( + onTap: () => onPreviewImage(index), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.file( + file, + height: 70, + width: 70, + fit: BoxFit.cover, + ), + ), + ), + Positioned( + top: 4, + right: 4, + child: GestureDetector( + onTap: () => onRemoveImage(index), + child: Container( + decoration: const BoxDecoration( + color: Colors.black54, + shape: BoxShape.circle, + ), + child: const Icon(Icons.close, + size: 20, color: Colors.white), + ), + ), + ), + ], + ); + }, + ), + ), + MySpacing.height(16), + Row( + children: [ + Expanded( + child: MyButton.outlined( + onPressed: onCameraTap, + padding: MySpacing.xy(12, 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.camera_alt, + size: 16, color: Colors.blueAccent), + MySpacing.width(6), + MyText.bodySmall('Capture', color: Colors.blueAccent), + ], + ), + ), + ), + MySpacing.width(12), + Expanded( + child: MyButton.outlined( + onPressed: onUploadTap, + padding: MySpacing.xy(12, 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.upload_file, + size: 16, color: Colors.blueAccent), + MySpacing.width(6), + MyText.bodySmall('Upload', color: Colors.blueAccent), + ], + ), + ), + ), + ], + ), + ], + ); + } +} diff --git a/lib/model/dailyTaskPlaning/task_action_buttons.dart b/lib/model/dailyTaskPlaning/task_action_buttons.dart new file mode 100644 index 0000000..5c188bd --- /dev/null +++ b/lib/model/dailyTaskPlaning/task_action_buttons.dart @@ -0,0 +1,195 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:marco/model/dailyTaskPlaning/comment_task_bottom_sheet.dart'; +import 'package:marco/model/dailyTaskPlaning/report_task_bottom_sheet.dart'; +import 'package:marco/model/dailyTaskPlaning/report_action_bottom_sheet.dart'; + +class TaskActionButtons { + static Widget reportButton({ + required BuildContext context, + required dynamic task, + required int completed, + required VoidCallback refreshCallback, + }) { + return OutlinedButton.icon( + icon: const Icon(Icons.report, size: 18, color: Colors.blueAccent), + label: const Text('Report', style: TextStyle(color: Colors.blueAccent)), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.blueAccent), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: () { + final activityName = + task.workItem?.activityMaster?.activityName ?? 'N/A'; + final assigned = '${(task.plannedTask - completed)}'; + final assignedBy = + "${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}"; + final assignedOn = DateFormat('dd-MM-yyyy').format(task.assignmentDate); + final taskId = task.id; + final location = [ + task.workItem?.workArea?.floor?.building?.name, + task.workItem?.workArea?.floor?.floorName, + task.workItem?.workArea?.areaName, + ].where((e) => e != null && e.isNotEmpty).join(' > '); + + final teamMembers = task.teamMembers.map((e) => e.firstName).toList(); + final pendingWork = (task.workItem?.plannedWork ?? 0) - + (task.workItem?.completedWork ?? 0); + + final taskData = { + 'activity': activityName, + 'assigned': assigned, + 'taskId': taskId, + 'assignedBy': assignedBy, + 'completed': completed, + 'assignedOn': assignedOn, + 'location': location, + 'teamSize': task.teamMembers.length, + 'teamMembers': teamMembers, + 'pendingWork': pendingWork, + }; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (ctx) => Padding( + padding: MediaQuery.of(ctx).viewInsets, + child: ReportTaskBottomSheet( + taskData: taskData, + onReportSuccess: refreshCallback, + ), + ), + ); + }, + ); + } + + static Widget commentButton({ + required BuildContext context, + required dynamic task, + required VoidCallback refreshCallback, + }) { + return OutlinedButton.icon( + icon: const Icon(Icons.comment, size: 18, color: Colors.blueAccent), + label: const Text('Comment', style: TextStyle(color: Colors.blueAccent)), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.blueAccent), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: () { + final taskData = _prepareTaskData(task: task, completed: task.completedTask); + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => CommentTaskBottomSheet( + taskData: taskData, + onCommentSuccess: () { + refreshCallback(); + Navigator.of(context).pop(); + }, + ), + ); + }, + ); + } + + static Widget reportActionButton({ + required BuildContext context, + required dynamic task, + required int completed, + required VoidCallback refreshCallback, + }) { + return OutlinedButton.icon( + icon: const Icon(Icons.report, size: 18, color: Colors.amber), + label: const Text('Take Report Action', + style: TextStyle(color: Colors.amber)), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.amber), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: () { + final taskData = _prepareTaskData(task: task, completed: completed); + + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (ctx) => Padding( + padding: MediaQuery.of(ctx).viewInsets, + child: ReportActionBottomSheet( + taskData: taskData, + ), + ), + ); + }, + ); + } + + static Map _prepareTaskData({ + required dynamic task, + required int completed, +}) { + final activityName = task.workItem?.activityMaster?.activityName ?? 'N/A'; + final assigned = '${(task.plannedTask - completed)}'; + final assignedBy = + "${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}"; + final assignedOn = DateFormat('yyyy-MM-dd').format(task.assignmentDate); + final taskId = task.id; + + final location = [ + task.workItem?.workArea?.floor?.building?.name, + task.workItem?.workArea?.floor?.floorName, + task.workItem?.workArea?.areaName, + ].where((e) => e != null && e.isNotEmpty).join(' > '); + + final teamMembers = task.teamMembers + .map((e) => '${e.firstName} ${e.lastName ?? ''}') + .toList(); + + final pendingWork = + (task.workItem?.plannedWork ?? 0) - (task.workItem?.completedWork ?? 0); + + final taskComments = task.comments.map((comment) { + final isoDate = comment.timestamp.toIso8601String(); + final commenterName = comment.commentedBy.firstName.isNotEmpty + ? "${comment.commentedBy.firstName} ${comment.commentedBy.lastName ?? ''}".trim() + : "Unknown"; + + return { + 'text': comment.comment, + 'date': isoDate, + 'commentedBy': commenterName, + 'preSignedUrls': comment.preSignedUrls, + }; + }).toList(); + + final taskLevelPreSignedUrls = task.reportedPreSignedUrls; + + return { + 'activity': activityName, + 'assigned': assigned, + 'taskId': taskId, + 'assignedBy': assignedBy, + 'completed': completed, + 'plannedWork': task.plannedTask.toString(), + 'completedWork': completed.toString(), + 'assignedOn': assignedOn, + 'location': location, + 'teamSize': task.teamMembers.length, + 'teamMembers': teamMembers, + 'pendingWork': pendingWork, + 'taskComments': taskComments, + 'reportedPreSignedUrls': taskLevelPreSignedUrls, + }; +} + +} diff --git a/lib/model/dailyTaskPlaning/work_status_model.dart b/lib/model/dailyTaskPlaning/work_status_model.dart new file mode 100644 index 0000000..91feab7 --- /dev/null +++ b/lib/model/dailyTaskPlaning/work_status_model.dart @@ -0,0 +1,53 @@ +class WorkStatusResponseModel { + final bool success; + final String message; + final List data; + final dynamic errors; + final int statusCode; + final DateTime timestamp; + + WorkStatusResponseModel({ + required this.success, + required this.message, + required this.data, + required this.errors, + required this.statusCode, + required this.timestamp, + }); + + factory WorkStatusResponseModel.fromJson(Map json) { + return WorkStatusResponseModel( + success: json['success'], + message: json['message'], + data: List.from( + json['data'].map((item) => WorkStatus.fromJson(item)), + ), + errors: json['errors'], + statusCode: json['statusCode'], + timestamp: DateTime.parse(json['timestamp']), + ); + } +} + +class WorkStatus { + final String id; + final String name; + final String description; + final bool isSystem; + + WorkStatus({ + required this.id, + required this.name, + required this.description, + required this.isSystem, + }); + + factory WorkStatus.fromJson(Map json) { + return WorkStatus( + id: json['id'], + name: json['name'], + description: json['description'], + isSystem: json['isSystem'], + ); + } +} diff --git a/lib/view/taskPlaning/daily_progress.dart b/lib/view/taskPlaning/daily_progress.dart index 8247041..d60daf2 100644 --- a/lib/view/taskPlaning/daily_progress.dart +++ b/lib/view/taskPlaning/daily_progress.dart @@ -12,9 +12,8 @@ import 'package:marco/controller/permission_controller.dart'; import 'package:marco/controller/dashboard/daily_task_controller.dart'; import 'package:marco/model/dailyTaskPlaning/daily_progress_report_filter.dart'; import 'package:marco/helpers/widgets/avatar.dart'; -import 'package:marco/model/dailyTaskPlaning/comment_task_bottom_sheet.dart'; -import 'package:marco/model/dailyTaskPlaning/report_task_bottom_sheet.dart'; import 'package:marco/controller/project_controller.dart'; +import 'package:marco/model/dailyTaskPlaning/task_action_buttons.dart'; class DailyProgressReportScreen extends StatefulWidget { const DailyProgressReportScreen({super.key}); @@ -72,14 +71,14 @@ class _DailyProgressReportScreenState extends State @override Widget build(BuildContext context) { return Scaffold( - appBar: PreferredSize( + appBar: PreferredSize( preferredSize: const Size.fromHeight(80), child: AppBar( backgroundColor: const Color(0xFFF5F5F5), elevation: 0.5, foregroundColor: Colors.black, titleSpacing: 0, - centerTitle: false, + centerTitle: false, leading: Padding( padding: const EdgeInsets.only(top: 15.0), // Aligns with title child: IconButton( @@ -91,7 +90,7 @@ class _DailyProgressReportScreenState extends State ), ), title: Padding( - padding: const EdgeInsets.only(top: 15.0), + padding: const EdgeInsets.only(top: 15.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, @@ -459,197 +458,29 @@ class _DailyProgressReportScreenState extends State mainAxisAlignment: MainAxisAlignment.end, children: [ if (task.reportedDate == null || - task.reportedDate.toString().isEmpty) - OutlinedButton.icon( - icon: const Icon(Icons.report, - size: 18, - color: Colors.blueAccent), - label: const Text('Report', - style: TextStyle( - color: Colors.blueAccent)), - style: OutlinedButton.styleFrom( - side: const BorderSide( - color: Colors.blueAccent), - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 8), - textStyle: - const TextStyle(fontSize: 14), - ), - onPressed: () { - final activityName = task - .workItem - ?.activityMaster - ?.activityName ?? - 'N/A'; - final assigned = - '${(task.plannedTask - completed)}'; - final assignedBy = - "${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}"; - final assignedOn = - DateFormat('dd-MM-yyyy').format( - task.assignmentDate); - final taskId = task.id; - final location = [ - task.workItem?.workArea?.floor - ?.building?.name, - task.workItem?.workArea?.floor - ?.floorName, - task.workItem?.workArea?.areaName, - ] - .where((e) => - e != null && e.isNotEmpty) - .join(' > '); - final teamMembers = task.teamMembers - .map((e) => e.firstName) - .toList(); - final pendingWork = (task.workItem - ?.plannedWork ?? - 0) - - (task.workItem?.completedWork ?? - 0); - - final taskData = { - 'activity': activityName, - 'assigned': assigned, - 'taskId': taskId, - 'assignedBy': assignedBy, - 'completed': completed, - 'assignedOn': assignedOn, - 'location': location, - 'teamSize': - task.teamMembers.length, - 'teamMembers': teamMembers, - 'pendingWork': pendingWork, - }; - - showModalBottomSheet( - context: context, - isScrollControlled: true, - shape: - const RoundedRectangleBorder( - borderRadius: - BorderRadius.vertical( - top: Radius.circular( - 16)), - ), - builder: (BuildContext ctx) => - Padding( - padding: MediaQuery.of(ctx) - .viewInsets, - child: ReportTaskBottomSheet( - taskData: taskData, - onReportSuccess: () { - _refreshData(); - }, - ), - ), - ); - }, + task.reportedDate + .toString() + .isEmpty) ...[ + TaskActionButtons.reportButton( + context: context, + task: task, + completed: completed, + refreshCallback: _refreshData, ), - const SizedBox(width: 8), - OutlinedButton.icon( - icon: const Icon(Icons.comment, - size: 18, color: Colors.blueAccent), - label: const Text('Comment', - style: TextStyle( - color: Colors.blueAccent)), - style: OutlinedButton.styleFrom( - side: const BorderSide( - color: Colors.blueAccent), - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 8), - textStyle: - const TextStyle(fontSize: 14), + const SizedBox(width: 8), + ] else ...[ + TaskActionButtons.reportActionButton( + context: context, + task: task, + completed: completed, + refreshCallback: _refreshData, ), - onPressed: () { - final activityName = task - .workItem - ?.activityMaster - ?.activityName ?? - 'N/A'; - final plannedTask = task.plannedTask; - final completed = task.completedTask; - final assigned = - '${(plannedTask - completed)}'; - final plannedWork = - plannedTask.toString(); - final completedWork = - completed.toString(); - final assignedBy = - "${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}"; - final assignedOn = - DateFormat('yyyy-MM-dd') - .format(task.assignmentDate); - final taskId = task.id; - final location = [ - task.workItem?.workArea?.floor - ?.building?.name, - task.workItem?.workArea?.floor - ?.floorName, - task.workItem?.workArea?.areaName, - ] - .where((e) => - e != null && e.isNotEmpty) - .join(' > '); - - final teamMembers = task.teamMembers - .map((e) => - '${e.firstName} ${e.lastName}') - .toList(); - - final taskComments = - task.comments.map((comment) { - final isoDate = comment.timestamp - .toIso8601String(); - final commenterName = comment - .commentedBy - .firstName - .isNotEmpty - ? "${comment.commentedBy.firstName} ${comment.commentedBy.lastName ?? ''}" - .trim() - : "Unknown"; - - return { - 'text': comment.comment, - 'date': isoDate, - 'commentedBy': commenterName, - 'preSignedUrls': - comment.preSignedUrls, - }; - }).toList(); - final taskLevelPreSignedUrls = - task.reportedPreSignedUrls; - - final taskData = { - 'activity': activityName, - 'assigned': assigned, - 'taskId': taskId, - 'assignedBy': assignedBy, - 'completedWork': completedWork, - 'plannedWork': plannedWork, - 'assignedOn': assignedOn, - 'location': location, - 'teamSize': task.teamMembers.length, - 'teamMembers': teamMembers, - 'taskComments': taskComments, - 'reportedPreSignedUrls': - taskLevelPreSignedUrls, - }; - - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (_) => - CommentTaskBottomSheet( - taskData: taskData, - onCommentSuccess: () { - _refreshData(); - Navigator.of(context).pop(); - }, - ), - ); - }, + const SizedBox(width: 8), + ], + TaskActionButtons.commentButton( + context: context, + task: task, + refreshCallback: _refreshData, ), ], ) -- 2.43.0 From 206c84b3a11e4df2c0d0e3fbba636c89a801e550 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Wed, 18 Jun 2025 11:40:08 +0530 Subject: [PATCH 02/19] feat: Implement task management features including task creation, assignment, and employee selection --- .../task_planing/add_task_controller.dart | 202 +++++++++++++ lib/helpers/services/api_endpoints.dart | 2 + lib/helpers/services/api_service.dart | 36 +++ .../create_task_botom_sheet.dart | 284 ++++++++++++++---- .../master_work_category_model.dart | 31 ++ .../report_action_bottom_sheet.dart | 34 ++- .../dailyTaskPlaning/task_action_buttons.dart | 110 +++---- lib/view/taskPlaning/daily_progress.dart | 5 +- pubspec.yaml | 2 - 9 files changed, 572 insertions(+), 134 deletions(-) create mode 100644 lib/controller/task_planing/add_task_controller.dart create mode 100644 lib/model/dailyTaskPlaning/master_work_category_model.dart diff --git a/lib/controller/task_planing/add_task_controller.dart b/lib/controller/task_planing/add_task_controller.dart new file mode 100644 index 0000000..9a8d1d2 --- /dev/null +++ b/lib/controller/task_planing/add_task_controller.dart @@ -0,0 +1,202 @@ +import 'package:get/get.dart'; +import 'package:logger/logger.dart'; +import 'package:marco/helpers/services/api_service.dart'; +import 'package:marco/model/project_model.dart'; +import 'package:marco/model/employee_model.dart'; +import 'package:marco/helpers/widgets/my_form_validator.dart'; +import 'package:marco/helpers/widgets/my_snackbar.dart'; +import 'package:marco/controller/project_controller.dart'; +import 'package:marco/model/dailyTaskPlaning/master_work_category_model.dart'; + +final Logger log = Logger(); + +class AddTaskController extends GetxController { + List projects = []; + List employees = []; + RxMap uploadingStates = {}.obs; + MyFormValidator basicValidator = MyFormValidator(); + RxnInt selectedCategoryId = RxnInt(); + + List> roles = []; + RxnString selectedRoleId = RxnString(); + RxList selectedEmployees = [].obs; + RxBool isLoadingWorkMasterCategories = false.obs; + RxList workMasterCategories = [].obs; + + void updateSelectedEmployees() { + final selected = + employees.where((e) => uploadingStates[e.id]?.value == true).toList(); + selectedEmployees.value = selected; + } + + RxBool isLoading = false.obs; + @override + void onInit() { + super.onInit(); + fetchRoles(); + fetchWorkMasterCategories(); + final projectId = Get.find().selectedProject?.id; + fetchEmployeesByProject(projectId); + } + + String? formFieldValidator(String? value, {required String fieldType}) { + if (value == null || value.trim().isEmpty) { + return 'This field is required'; + } + if (fieldType == "target") { + if (int.tryParse(value.trim()) == null) { + return 'Please enter a valid number'; + } + } + if (fieldType == "description") { + if (value.trim().length < 5) { + return 'Description must be at least 5 characters'; + } + } + return null; + } + + Future fetchRoles() async { + logger.i("Fetching roles..."); + final result = await ApiService.getRoles(); + if (result != null) { + roles = List>.from(result); + logger.i("Roles fetched successfully."); + update(); + } else { + logger.e("Failed to fetch roles."); + } + } + + void onRoleSelected(String? roleId) { + selectedRoleId.value = roleId; + logger.i("Role selected: $roleId"); + } + + Future assignDailyTask({ + required String workItemId, + required int plannedTask, + required String description, + required List taskTeam, + DateTime? assignmentDate, + }) async { + logger.i("Starting assign task..."); + + final response = await ApiService.assignDailyTask( + workItemId: workItemId, + plannedTask: plannedTask, + description: description, + taskTeam: taskTeam, + assignmentDate: assignmentDate, + ); + + if (response == true) { + logger.i("Task assigned successfully."); + showAppSnackbar( + title: "Success", + message: "Task assigned successfully!", + type: SnackbarType.success, + ); + return true; + } else { + logger.e("Failed to assign task."); + showAppSnackbar( + title: "Error", + message: "Failed to assign task.", + type: SnackbarType.error, + ); + return false; + } + } + + Future createTask({ + required String parentTaskId, + required int plannedTask, + required String description, + required List taskTeam, + required String workItemId, + DateTime? assignmentDate, + }) async { + logger.i("Creating new task..."); + + final response = await ApiService.createTask( + parentTaskId: parentTaskId, + plannedTask: plannedTask, + description: description, + taskTeam: taskTeam, + workItemId: workItemId, + assignmentDate: assignmentDate, + ); + + if (response == true) { + logger.i("Task created successfully."); + showAppSnackbar( + title: "Success", + message: "Task created successfully!", + type: SnackbarType.success, + ); + return true; + } else { + logger.e("Failed to create task."); + showAppSnackbar( + title: "Error", + message: "Failed to create task.", + type: SnackbarType.error, + ); + return false; + } + } + + Future fetchEmployeesByProject(String? projectId) async { + if (projectId == null || projectId.isEmpty) { + log.e("Project ID is required but was null or empty."); + return; + } + + isLoading.value = true; + try { + final response = await ApiService.getAllEmployeesByProject(projectId); + if (response != null && response.isNotEmpty) { + employees = + response.map((json) => EmployeeModel.fromJson(json)).toList(); + for (var emp in employees) { + uploadingStates[emp.id] = false.obs; + } + log.i("Employees fetched: ${employees.length} for project $projectId"); + } else { + log.w("No employees found for project $projectId."); + employees = []; + } + } catch (e) { + log.e("Error fetching employees for project $projectId: $e"); + } + + update(); + isLoading.value = false; + } + + Future fetchWorkMasterCategories() async { + isLoadingWorkMasterCategories.value = true; + + final response = await ApiService.getMasterWorkCategories(); + if (response != null) { + try { + final dataList = response['data'] ?? []; + workMasterCategories.assignAll( + List.from( + dataList.map((e) => WorkCategoryModel.fromJson(e)), + ), + ); + logger.i("Work categories fetched: ${dataList.length}"); + } catch (e) { + logger.e("Error parsing work categories: $e"); + workMasterCategories.clear(); + } + } else { + logger.w("No work categories found or API call failed."); + } + + isLoadingWorkMasterCategories.value = false; + update(); + } +} diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 44b5310..851f264 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -26,4 +26,6 @@ class ApiEndpoints { static const String assignDailyTask = "/task/assign"; static const String getWorkStatus = "/master/work-status"; static const String approveReportAction = "/task/approve"; + static const String assignTask = "/task/assign"; + static const String getmasterWorkCategories = "/Master/work-categories"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index b48f255..9c960a1 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -378,6 +378,11 @@ class ApiService { _getRequest(ApiEndpoints.getWorkStatus).then((res) => res != null ? _parseResponseForAllData(res, label: 'Work Status') : null); + static Future?> getMasterWorkCategories() async => + _getRequest(ApiEndpoints.getmasterWorkCategories).then((res) => + res != null + ? _parseResponseForAllData(res, label: 'Master Work Categories') + : null); static Future approveTask({ required String id, required String comment, @@ -399,4 +404,35 @@ class ApiService { final json = jsonDecode(response.body); return response.statusCode == 200 && json['success'] == true; } + + static Future createTask({ + required String parentTaskId, + required int plannedTask, + required String description, + required List taskTeam, + required String workItemId, + DateTime? assignmentDate, + }) async { + final body = { + "assignmentDate": + (assignmentDate ?? DateTime.now()).toUtc().toIso8601String(), + "parentTaskId": parentTaskId, + "plannedTask": plannedTask, + "description": description, + "taskTeam": taskTeam, + "workItemId": workItemId, + }; + + final response = await _postRequest(ApiEndpoints.assignTask, body); + if (response == null) return false; + + final json = jsonDecode(response.body); + if (response.statusCode == 200 && json['success'] == true) { + Get.back(); + return true; + } + + _log("Failed to create task: ${json['message'] ?? 'Unknown error'}"); + return false; + } } diff --git a/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart b/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart index 196434c..fe751c1 100644 --- a/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart +++ b/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart @@ -1,20 +1,92 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:marco/controller/task_planing/add_task_controller.dart'; import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_snackbar.dart'; void showManageTaskBottomSheet({ - required String building, - required String floor, required String workArea, required String activity, - required String plannedWork, required String completedWork, required String unit, required Function(String) onCategoryChanged, + required String parentTaskId, + required int plannedTask, + required String workItemId, required VoidCallback onSubmit, }) { - final List categories = ["Fresh Work", "Repair", "Demo"]; - String selectedCategory = 'Fresh Work'; + final controller = Get.put(AddTaskController()); + final ScrollController employeeListScrollController = ScrollController(); + final TextEditingController plannedTaskController = + TextEditingController(text: plannedTask.toString()); + final TextEditingController descriptionController = TextEditingController(); + + Widget buildEmployeeList() { + final selectedRoleId = controller.selectedRoleId.value; + final filteredEmployees = selectedRoleId == null + ? controller.employees + : controller.employees + .where((e) => e.jobRoleID.toString() == selectedRoleId) + .toList(); + + if (filteredEmployees.isEmpty) { + return MyText.bodySmall("No employees found for selected role."); + } + + return Scrollbar( + controller: employeeListScrollController, + thumbVisibility: true, + interactive: true, + child: ListView.builder( + controller: employeeListScrollController, + shrinkWrap: true, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: filteredEmployees.length, + itemBuilder: (context, index) { + final employee = filteredEmployees[index]; + final rxBool = controller.uploadingStates[employee.id]; + + return Obx(() => Padding( + padding: const EdgeInsets.symmetric(vertical: 0), + child: Row( + children: [ + Theme( + data: Theme.of(context) + .copyWith(unselectedWidgetColor: Colors.black), + child: Checkbox( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + side: const BorderSide(color: Colors.black), + ), + value: rxBool?.value ?? false, + onChanged: (bool? selected) { + if (rxBool != null) { + rxBool.value = selected ?? false; + controller.updateSelectedEmployees(); + } + }, + fillColor: + WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return const Color.fromARGB(255, 95, 132, 255); + } + return Colors.transparent; + }), + checkColor: Colors.white, + side: const BorderSide(color: Colors.black), + ), + ), + const SizedBox(width: 8), + Expanded( + child: MyText.bodySmall(employee.name), + ), + ], + ), + )); + }, + ), + ); + } Get.bottomSheet( StatefulBuilder( @@ -22,12 +94,14 @@ void showManageTaskBottomSheet({ return LayoutBuilder( builder: (context, constraints) { final isLarge = constraints.maxWidth > 600; - final horizontalPadding = isLarge ? constraints.maxWidth * 0.2 : 16.0; + final horizontalPadding = + isLarge ? constraints.maxWidth * 0.2 : 16.0; return SafeArea( child: Container( - constraints: const BoxConstraints(maxHeight: 0.95 * 800), - padding: EdgeInsets.fromLTRB(horizontalPadding, 12, horizontalPadding, 24), + constraints: const BoxConstraints(maxHeight: 760), + padding: EdgeInsets.fromLTRB( + horizontalPadding, 12, horizontalPadding, 24), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), @@ -36,7 +110,6 @@ void showManageTaskBottomSheet({ child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Draggable handle Center( child: Container( width: 40, @@ -48,74 +121,155 @@ void showManageTaskBottomSheet({ ), ), ), - - _infoRowWithIcon(Icons.location_city, "Selected Building", building), - _infoRowWithIcon(Icons.apartment, "Selected Floor", floor), - _infoRowWithIcon(Icons.workspaces, "Selected Work Area", workArea), - _infoRowWithIcon(Icons.list_alt, "Selected Activity", activity), - + _infoRowWithIcon( + Icons.workspaces, "Selected Work Area", workArea), + _infoRowWithIcon( + Icons.list_alt, "Selected Activity", activity), const SizedBox(height: 12), Row( children: [ - Icon(Icons.category_outlined, color: Colors.grey[700], size: 18), + Icon(Icons.edit_calendar, + color: Colors.grey[700], size: 18), const SizedBox(width: 8), - MyText.bodyMedium("Selected Work Category", fontWeight: 600), + MyText.bodyMedium("Planned Work", fontWeight: 600), ], ), const SizedBox(height: 6), - - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(8), - ), - child: PopupMenuButton( - padding: EdgeInsets.zero, - onSelected: (val) { - setState(() { - selectedCategory = val; - }); - onCategoryChanged(val); - }, - itemBuilder: (context) => categories - .map((e) => PopupMenuItem(value: e, child: Text(e))) - .toList(), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - selectedCategory, - style: TextStyle(fontSize: 14, color: Colors.black87), - ), - const Icon(Icons.arrow_drop_down), - ], - ), + TextField( + controller: plannedTaskController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: "Enter planned work", + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8)), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 10), ), ), - const SizedBox(height: 12), - _infoRowWithIcon(Icons.edit_calendar, "Planned Work", plannedWork), - _infoRowWithIcon(Icons.check_circle_outline, "Completed Work", completedWork), - _infoRowWithIcon(Icons.straighten, "Unit", unit), - - const SizedBox(height: 24), - SizedBox( - width: double.infinity, - height: 48, - child: ElevatedButton( - onPressed: onSubmit, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blueAccent, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), - child: const Text( - "Submit", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), - ), + _infoRowWithIcon(Icons.check_circle_outline, + "Completed Work", completedWork), + const SizedBox(height: 16), + MyText.bodyMedium("Description", fontWeight: 700), + const SizedBox(height: 6), + TextField( + controller: descriptionController, + maxLines: 3, + decoration: InputDecoration( + hintText: "Enter task description", + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8)), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 10), ), ), + const SizedBox(height: 24), + MyText.bodyMedium("Select Team Members", fontWeight: 700), + const SizedBox(height: 8), + Obx(() { + if (controller.isLoading.value) { + return const Center( + child: CircularProgressIndicator()); + } + + return Container( + constraints: const BoxConstraints(maxHeight: 150), + child: buildEmployeeList(), + ); + }), + const SizedBox(height: 12), + Obx(() { + if (controller.selectedEmployees.isEmpty) { + return const SizedBox.shrink(); + } + return Wrap( + spacing: 8, + runSpacing: 8, + children: + controller.selectedEmployees.map((employee) { + return Chip( + label: Text(employee.name), + deleteIcon: const Icon(Icons.close), + onDeleted: () { + controller.uploadingStates[employee.id]?.value = + false; + controller.updateSelectedEmployees(); + }, + backgroundColor: Colors.blue.shade100, + labelStyle: const TextStyle(color: Colors.black), + ); + }).toList(), + ); + }), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () { + Get.back(); // Close bottom sheet + }, + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.grey), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + ), + child: MyText.bodyMedium( + "Cancel", + fontWeight: 600, + color: Colors.black, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () async { + final taskTeam = controller.selectedEmployees + .map((e) => e.id) + .toList(); + + if (taskTeam.isEmpty) { + showAppSnackbar( + title: "Team Required", + message: + "Please select at least one team member.", + type: SnackbarType.warning, + ); + return; + } + + final success = await controller.createTask( + parentTaskId: parentTaskId, + plannedTask: int.tryParse( + plannedTaskController.text.trim()) ?? + 0, + description: + descriptionController.text.trim(), + taskTeam: taskTeam, + workItemId: workItemId, + assignmentDate: DateTime.now(), + ); + + if (success) { + Get.back(); + onSubmit(); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueAccent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + ), + child: MyText.bodyMedium( + "Submit", + fontWeight: 600, + color: Colors.white, + ), + ), + ), + ], + ), ], ), ), diff --git a/lib/model/dailyTaskPlaning/master_work_category_model.dart b/lib/model/dailyTaskPlaning/master_work_category_model.dart new file mode 100644 index 0000000..bf87dd0 --- /dev/null +++ b/lib/model/dailyTaskPlaning/master_work_category_model.dart @@ -0,0 +1,31 @@ +class WorkCategoryModel { + final String id; + final String name; + final String description; + final bool isSystem; + + WorkCategoryModel({ + required this.id, + required this.name, + required this.description, + required this.isSystem, + }); + + factory WorkCategoryModel.fromJson(Map json) { + return WorkCategoryModel( + id: json['id'] ?? '', + name: json['name'] ?? '', + description: json['description'] ?? '', + isSystem: json['isSystem'] ?? false, + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'description': description, + 'isSystem': isSystem, + }; + } +} diff --git a/lib/model/dailyTaskPlaning/report_action_bottom_sheet.dart b/lib/model/dailyTaskPlaning/report_action_bottom_sheet.dart index f4669de..b7a40a8 100644 --- a/lib/model/dailyTaskPlaning/report_action_bottom_sheet.dart +++ b/lib/model/dailyTaskPlaning/report_action_bottom_sheet.dart @@ -17,11 +17,16 @@ import 'dart:io'; class ReportActionBottomSheet extends StatefulWidget { final Map taskData; final VoidCallback? onCommentSuccess; - + final String taskDataId; + final String workItemId; + final VoidCallback onReportSuccess; const ReportActionBottomSheet({ super.key, required this.taskData, this.onCommentSuccess, + required this.taskDataId, + required this.workItemId, + required this.onReportSuccess, }); @override @@ -70,7 +75,10 @@ class _ReportActionBottomSheetState extends State controller.basicValidator.getController('task_id')?.text = data['taskId'] ?? ''; controller.basicValidator.getController('comment')?.clear(); - + controller.basicValidator.getController('task_id')?.text = + widget.taskDataId; + controller.basicValidator.getController('work_item_id')?.text = + widget.workItemId; controller.selectedImages.clear(); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -461,16 +469,11 @@ class _ReportActionBottomSheetState extends State if (success) { Navigator.of(context).pop(); if (shouldShowAddTaskSheet) { - await Future.delayed(Duration( - milliseconds: - 100)); // Ensure animation completes + await Future.delayed( + Duration(milliseconds: 100)); showManageTaskBottomSheet( - building: widget.taskData['building'] ?? '', - floor: widget.taskData['floor'] ?? '', workArea: widget.taskData['location'] ?? '', activity: widget.taskData['activity'] ?? '', - plannedWork: - widget.taskData['plannedWork'] ?? '', completedWork: widget.taskData['completedWork'] ?? '', unit: widget.taskData['unit'] ?? '', @@ -478,15 +481,18 @@ class _ReportActionBottomSheetState extends State debugPrint( "Category changed to: $category"); }, + parentTaskId: widget.taskDataId, + plannedTask: int.tryParse( + widget.taskData['plannedWork'] ?? + '0') ?? + 0, + workItemId: widget.workItemId, onSubmit: () { - Navigator.of(context) - .pop(); // Close second sheet + Navigator.of(context).pop(); }, ); } - - // Optional callback after comment success - widget.onCommentSuccess?.call(); + widget.onReportSuccess.call(); } } }, diff --git a/lib/model/dailyTaskPlaning/task_action_buttons.dart b/lib/model/dailyTaskPlaning/task_action_buttons.dart index 5c188bd..933fc3a 100644 --- a/lib/model/dailyTaskPlaning/task_action_buttons.dart +++ b/lib/model/dailyTaskPlaning/task_action_buttons.dart @@ -82,7 +82,8 @@ class TaskActionButtons { textStyle: const TextStyle(fontSize: 14), ), onPressed: () { - final taskData = _prepareTaskData(task: task, completed: task.completedTask); + final taskData = + _prepareTaskData(task: task, completed: task.completedTask); showModalBottomSheet( context: context, isScrollControlled: true, @@ -104,6 +105,8 @@ class TaskActionButtons { required dynamic task, required int completed, required VoidCallback refreshCallback, + required String parentTaskID, + required String workItemId, }) { return OutlinedButton.icon( icon: const Icon(Icons.report, size: 18, color: Colors.amber), @@ -115,7 +118,7 @@ class TaskActionButtons { textStyle: const TextStyle(fontSize: 14), ), onPressed: () { - final taskData = _prepareTaskData(task: task, completed: completed); + final taskData = _prepareTaskData(task: task, completed: completed); showModalBottomSheet( context: context, @@ -127,6 +130,9 @@ class TaskActionButtons { padding: MediaQuery.of(ctx).viewInsets, child: ReportActionBottomSheet( taskData: taskData, + taskDataId: parentTaskID, + workItemId: workItemId, + onReportSuccess: refreshCallback, ), ), ); @@ -135,61 +141,61 @@ class TaskActionButtons { } static Map _prepareTaskData({ - required dynamic task, - required int completed, -}) { - final activityName = task.workItem?.activityMaster?.activityName ?? 'N/A'; - final assigned = '${(task.plannedTask - completed)}'; - final assignedBy = - "${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}"; - final assignedOn = DateFormat('yyyy-MM-dd').format(task.assignmentDate); - final taskId = task.id; + required dynamic task, + required int completed, + }) { + final activityName = task.workItem?.activityMaster?.activityName ?? 'N/A'; + final assigned = '${(task.plannedTask - completed)}'; + final assignedBy = + "${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}"; + final assignedOn = DateFormat('yyyy-MM-dd').format(task.assignmentDate); + final taskId = task.id; - final location = [ - task.workItem?.workArea?.floor?.building?.name, - task.workItem?.workArea?.floor?.floorName, - task.workItem?.workArea?.areaName, - ].where((e) => e != null && e.isNotEmpty).join(' > '); + final location = [ + task.workItem?.workArea?.floor?.building?.name, + task.workItem?.workArea?.floor?.floorName, + task.workItem?.workArea?.areaName, + ].where((e) => e != null && e.isNotEmpty).join(' > '); - final teamMembers = task.teamMembers - .map((e) => '${e.firstName} ${e.lastName ?? ''}') - .toList(); + final teamMembers = task.teamMembers + .map((e) => '${e.firstName} ${e.lastName ?? ''}') + .toList(); - final pendingWork = - (task.workItem?.plannedWork ?? 0) - (task.workItem?.completedWork ?? 0); + final pendingWork = + (task.workItem?.plannedWork ?? 0) - (task.workItem?.completedWork ?? 0); - final taskComments = task.comments.map((comment) { - final isoDate = comment.timestamp.toIso8601String(); - final commenterName = comment.commentedBy.firstName.isNotEmpty - ? "${comment.commentedBy.firstName} ${comment.commentedBy.lastName ?? ''}".trim() - : "Unknown"; + final taskComments = task.comments.map((comment) { + final isoDate = comment.timestamp.toIso8601String(); + final commenterName = comment.commentedBy.firstName.isNotEmpty + ? "${comment.commentedBy.firstName} ${comment.commentedBy.lastName ?? ''}" + .trim() + : "Unknown"; + + return { + 'text': comment.comment, + 'date': isoDate, + 'commentedBy': commenterName, + 'preSignedUrls': comment.preSignedUrls, + }; + }).toList(); + + final taskLevelPreSignedUrls = task.reportedPreSignedUrls; return { - 'text': comment.comment, - 'date': isoDate, - 'commentedBy': commenterName, - 'preSignedUrls': comment.preSignedUrls, + 'activity': activityName, + 'assigned': assigned, + 'taskId': taskId, + 'assignedBy': assignedBy, + 'completed': completed, + 'plannedWork': task.plannedTask.toString(), + 'completedWork': completed.toString(), + 'assignedOn': assignedOn, + 'location': location, + 'teamSize': task.teamMembers.length, + 'teamMembers': teamMembers, + 'pendingWork': pendingWork, + 'taskComments': taskComments, + 'reportedPreSignedUrls': taskLevelPreSignedUrls, }; - }).toList(); - - final taskLevelPreSignedUrls = task.reportedPreSignedUrls; - - return { - 'activity': activityName, - 'assigned': assigned, - 'taskId': taskId, - 'assignedBy': assignedBy, - 'completed': completed, - 'plannedWork': task.plannedTask.toString(), - 'completedWork': completed.toString(), - 'assignedOn': assignedOn, - 'location': location, - 'teamSize': task.teamMembers.length, - 'teamMembers': teamMembers, - 'pendingWork': pendingWork, - 'taskComments': taskComments, - 'reportedPreSignedUrls': taskLevelPreSignedUrls, - }; -} - + } } diff --git a/lib/view/taskPlaning/daily_progress.dart b/lib/view/taskPlaning/daily_progress.dart index d60daf2..2ff80aa 100644 --- a/lib/view/taskPlaning/daily_progress.dart +++ b/lib/view/taskPlaning/daily_progress.dart @@ -379,7 +379,8 @@ class _DailyProgressReportScreenState extends State final progress = (planned != 0) ? (completed / planned).clamp(0.0, 1.0) : 0.0; - + final parentTaskID = task.id; + final workItemId = task.workItem?.id; return Column( children: [ Padding( @@ -472,6 +473,8 @@ class _DailyProgressReportScreenState extends State TaskActionButtons.reportActionButton( context: context, task: task, + parentTaskID: parentTaskID, + workItemId: workItemId.toString(), completed: completed, refreshCallback: _refreshData, ), diff --git a/pubspec.yaml b/pubspec.yaml index 004626a..808debf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -98,8 +98,6 @@ flutter: - assets/avatar/ - assets/coin/ - assets/country/ - - assets/data/ - - assets/dummy/ - assets/lang/ - assets/logo/ - assets/logo/loading_logo.png -- 2.43.0 From 58a66546e49fdf1ed399a799ac87a04edb960dc8 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Wed, 18 Jun 2025 12:18:25 +0530 Subject: [PATCH 03/19] fix: Simplify image picking logic by removing null check for pickedFiles --- lib/controller/task_planing/report_task_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/controller/task_planing/report_task_controller.dart b/lib/controller/task_planing/report_task_controller.dart index ea54937..1bf4201 100644 --- a/lib/controller/task_planing/report_task_controller.dart +++ b/lib/controller/task_planing/report_task_controller.dart @@ -309,7 +309,7 @@ class ReportTaskController extends MyController { } } else { final pickedFiles = await _picker.pickMultiImage(imageQuality: 75); - if (pickedFiles != null && pickedFiles.isNotEmpty) { + if (pickedFiles.isNotEmpty) { selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path))); } } -- 2.43.0 From 5148b415796b026f273845ebb67f2216ec6dc777 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Wed, 18 Jun 2025 12:52:20 +0530 Subject: [PATCH 04/19] feat: Enhance task creation UI with title and description sections --- .../create_task_botom_sheet.dart | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart b/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart index fe751c1..46ed274 100644 --- a/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart +++ b/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart @@ -121,11 +121,25 @@ void showManageTaskBottomSheet({ ), ), ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(width: 8), + MyText.titleMedium( + "Create Task", + fontWeight: 700, + ), + ], + ), + const SizedBox(height: 16), _infoRowWithIcon( Icons.workspaces, "Selected Work Area", workArea), _infoRowWithIcon( Icons.list_alt, "Selected Activity", activity), const SizedBox(height: 12), + _infoRowWithIcon(Icons.check_circle_outline, + "Completed Work", completedWork), + const SizedBox(height: 12), Row( children: [ Icon(Icons.edit_calendar, @@ -146,11 +160,15 @@ void showManageTaskBottomSheet({ horizontal: 12, vertical: 10), ), ), - const SizedBox(height: 12), - _infoRowWithIcon(Icons.check_circle_outline, - "Completed Work", completedWork), const SizedBox(height: 16), - MyText.bodyMedium("Description", fontWeight: 700), + Row( + children: [ + Icon(Icons.description_outlined, + color: Colors.grey[700], size: 18), + const SizedBox(width: 8), + MyText.bodyMedium("Description", fontWeight: 600), + ], + ), const SizedBox(height: 6), TextField( controller: descriptionController, @@ -164,7 +182,14 @@ void showManageTaskBottomSheet({ ), ), const SizedBox(height: 24), - MyText.bodyMedium("Select Team Members", fontWeight: 700), + Row( + children: [ + Icon(Icons.group_add_outlined, + color: Colors.grey[700], size: 18), + const SizedBox(width: 8), + MyText.bodyMedium("Select Team Members", fontWeight: 600), + ], + ), const SizedBox(height: 8), Obx(() { if (controller.isLoading.value) { -- 2.43.0 From 3ede53713d15b7a377d65cfb77b2471a41c697a2 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Wed, 18 Jun 2025 19:15:41 +0530 Subject: [PATCH 05/19] feat: Refactor task management to include work area and activity IDs, update task creation logic, and enhance category selection --- .../task_planing/add_task_controller.dart | 99 +++------ lib/helpers/services/api_endpoints.dart | 2 +- lib/helpers/services/api_service.dart | 23 +- .../create_task_botom_sheet.dart | 201 +++++++----------- .../report_action_bottom_sheet.dart | 15 +- .../dailyTaskPlaning/task_action_buttons.dart | 6 +- lib/model/daily_task_model.dart | 27 ++- lib/view/taskPlaning/daily_progress.dart | 8 +- 8 files changed, 162 insertions(+), 219 deletions(-) diff --git a/lib/controller/task_planing/add_task_controller.dart b/lib/controller/task_planing/add_task_controller.dart index 9a8d1d2..3fa9e16 100644 --- a/lib/controller/task_planing/add_task_controller.dart +++ b/lib/controller/task_planing/add_task_controller.dart @@ -1,42 +1,29 @@ import 'package:get/get.dart'; import 'package:logger/logger.dart'; import 'package:marco/helpers/services/api_service.dart'; -import 'package:marco/model/project_model.dart'; -import 'package:marco/model/employee_model.dart'; import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; -import 'package:marco/controller/project_controller.dart'; import 'package:marco/model/dailyTaskPlaning/master_work_category_model.dart'; final Logger log = Logger(); class AddTaskController extends GetxController { - List projects = []; - List employees = []; RxMap uploadingStates = {}.obs; MyFormValidator basicValidator = MyFormValidator(); - RxnInt selectedCategoryId = RxnInt(); + RxnString selectedCategoryId = RxnString(); + RxnString selectedCategoryName = RxnString(); + var categoryIdNameMap = {}.obs; List> roles = []; RxnString selectedRoleId = RxnString(); - RxList selectedEmployees = [].obs; RxBool isLoadingWorkMasterCategories = false.obs; RxList workMasterCategories = [].obs; - void updateSelectedEmployees() { - final selected = - employees.where((e) => uploadingStates[e.id]?.value == true).toList(); - selectedEmployees.value = selected; - } - RxBool isLoading = false.obs; @override void onInit() { super.onInit(); - fetchRoles(); fetchWorkMasterCategories(); - final projectId = Get.find().selectedProject?.id; - fetchEmployeesByProject(projectId); } String? formFieldValidator(String? value, {required String fieldType}) { @@ -56,23 +43,6 @@ class AddTaskController extends GetxController { return null; } - Future fetchRoles() async { - logger.i("Fetching roles..."); - final result = await ApiService.getRoles(); - if (result != null) { - roles = List>.from(result); - logger.i("Roles fetched successfully."); - update(); - } else { - logger.e("Failed to fetch roles."); - } - } - - void onRoleSelected(String? roleId) { - selectedRoleId.value = roleId; - logger.i("Role selected: $roleId"); - } - Future assignDailyTask({ required String workItemId, required int plannedTask, @@ -111,10 +81,11 @@ class AddTaskController extends GetxController { Future createTask({ required String parentTaskId, + required String workAreaId, + required String activityId, required int plannedTask, - required String description, - required List taskTeam, - required String workItemId, + required String comment, + required String categoryId, DateTime? assignmentDate, }) async { logger.i("Creating new task..."); @@ -122,10 +93,12 @@ class AddTaskController extends GetxController { final response = await ApiService.createTask( parentTaskId: parentTaskId, plannedTask: plannedTask, - description: description, - taskTeam: taskTeam, - workItemId: workItemId, + comment: comment, + workAreaId: workAreaId, + activityId: activityId, assignmentDate: assignmentDate, + categoryId: categoryId, + ); if (response == true) { @@ -147,34 +120,6 @@ class AddTaskController extends GetxController { } } - Future fetchEmployeesByProject(String? projectId) async { - if (projectId == null || projectId.isEmpty) { - log.e("Project ID is required but was null or empty."); - return; - } - - isLoading.value = true; - try { - final response = await ApiService.getAllEmployeesByProject(projectId); - if (response != null && response.isNotEmpty) { - employees = - response.map((json) => EmployeeModel.fromJson(json)).toList(); - for (var emp in employees) { - uploadingStates[emp.id] = false.obs; - } - log.i("Employees fetched: ${employees.length} for project $projectId"); - } else { - log.w("No employees found for project $projectId."); - employees = []; - } - } catch (e) { - log.e("Error fetching employees for project $projectId: $e"); - } - - update(); - isLoading.value = false; - } - Future fetchWorkMasterCategories() async { isLoadingWorkMasterCategories.value = true; @@ -182,15 +127,22 @@ class AddTaskController extends GetxController { if (response != null) { try { final dataList = response['data'] ?? []; - workMasterCategories.assignAll( - List.from( - dataList.map((e) => WorkCategoryModel.fromJson(e)), - ), + + final parsedList = List.from( + dataList.map((e) => WorkCategoryModel.fromJson(e)), ); + + workMasterCategories.assignAll(parsedList); + final Map mapped = { + for (var item in parsedList) item.id: item.name, + }; + categoryIdNameMap.assignAll(mapped); + logger.i("Work categories fetched: ${dataList.length}"); } catch (e) { logger.e("Error parsing work categories: $e"); workMasterCategories.clear(); + categoryIdNameMap.clear(); } } else { logger.w("No work categories found or API call failed."); @@ -199,4 +151,9 @@ class AddTaskController extends GetxController { isLoadingWorkMasterCategories.value = false; update(); } + + void selectCategory(String id) { + selectedCategoryId.value = id; + selectedCategoryName.value = categoryIdNameMap[id]; + } } diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 851f264..87b11cf 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -26,6 +26,6 @@ class ApiEndpoints { static const String assignDailyTask = "/task/assign"; static const String getWorkStatus = "/master/work-status"; static const String approveReportAction = "/task/approve"; - static const String assignTask = "/task/assign"; + static const String assignTask = "/project/task"; static const String getmasterWorkCategories = "/Master/work-categories"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 9c960a1..7ff86d9 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -408,20 +408,21 @@ class ApiService { static Future createTask({ required String parentTaskId, required int plannedTask, - required String description, - required List taskTeam, - required String workItemId, + required String comment, + required String workAreaId, + required String activityId, DateTime? assignmentDate, + required String categoryId, }) async { - final body = { - "assignmentDate": - (assignmentDate ?? DateTime.now()).toUtc().toIso8601String(), + final body = [{ "parentTaskId": parentTaskId, - "plannedTask": plannedTask, - "description": description, - "taskTeam": taskTeam, - "workItemId": workItemId, - }; + "plannedWork": plannedTask, + "comment": comment, + "workAreaID": workAreaId, + "activityID": activityId, + "workCategoryId": categoryId, + 'completedWork': 0, + }]; final response = await _postRequest(ApiEndpoints.assignTask, body); if (response == null) return false; diff --git a/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart b/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart index 46ed274..a86b22e 100644 --- a/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart +++ b/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart @@ -2,8 +2,11 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/controller/task_planing/add_task_controller.dart'; import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:logger/logger.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; +final Logger log = Logger(); + void showManageTaskBottomSheet({ required String workArea, required String activity, @@ -12,82 +15,15 @@ void showManageTaskBottomSheet({ required Function(String) onCategoryChanged, required String parentTaskId, required int plannedTask, - required String workItemId, + required String activityId, + required String workAreaId, required VoidCallback onSubmit, }) { final controller = Get.put(AddTaskController()); - final ScrollController employeeListScrollController = ScrollController(); final TextEditingController plannedTaskController = TextEditingController(text: plannedTask.toString()); final TextEditingController descriptionController = TextEditingController(); - Widget buildEmployeeList() { - final selectedRoleId = controller.selectedRoleId.value; - final filteredEmployees = selectedRoleId == null - ? controller.employees - : controller.employees - .where((e) => e.jobRoleID.toString() == selectedRoleId) - .toList(); - - if (filteredEmployees.isEmpty) { - return MyText.bodySmall("No employees found for selected role."); - } - - return Scrollbar( - controller: employeeListScrollController, - thumbVisibility: true, - interactive: true, - child: ListView.builder( - controller: employeeListScrollController, - shrinkWrap: true, - physics: const AlwaysScrollableScrollPhysics(), - itemCount: filteredEmployees.length, - itemBuilder: (context, index) { - final employee = filteredEmployees[index]; - final rxBool = controller.uploadingStates[employee.id]; - - return Obx(() => Padding( - padding: const EdgeInsets.symmetric(vertical: 0), - child: Row( - children: [ - Theme( - data: Theme.of(context) - .copyWith(unselectedWidgetColor: Colors.black), - child: Checkbox( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - side: const BorderSide(color: Colors.black), - ), - value: rxBool?.value ?? false, - onChanged: (bool? selected) { - if (rxBool != null) { - rxBool.value = selected ?? false; - controller.updateSelectedEmployees(); - } - }, - fillColor: - WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.selected)) { - return const Color.fromARGB(255, 95, 132, 255); - } - return Colors.transparent; - }), - checkColor: Colors.white, - side: const BorderSide(color: Colors.black), - ), - ), - const SizedBox(width: 8), - Expanded( - child: MyText.bodySmall(employee.name), - ), - ], - ), - )); - }, - ), - ); - } - Get.bottomSheet( StatefulBuilder( builder: (context, setState) { @@ -166,7 +102,7 @@ void showManageTaskBottomSheet({ Icon(Icons.description_outlined, color: Colors.grey[700], size: 18), const SizedBox(width: 8), - MyText.bodyMedium("Description", fontWeight: 600), + MyText.bodyMedium("Comment", fontWeight: 600), ], ), const SizedBox(height: 6), @@ -182,48 +118,59 @@ void showManageTaskBottomSheet({ ), ), const SizedBox(height: 24), - Row( + Row( children: [ - Icon(Icons.group_add_outlined, + Icon(Icons.category_outlined, color: Colors.grey[700], size: 18), const SizedBox(width: 8), - MyText.bodyMedium("Select Team Members", fontWeight: 600), + MyText.bodyMedium("Selected Work Category", + fontWeight: 600), ], ), - const SizedBox(height: 8), + const SizedBox(height: 6), Obx(() { - if (controller.isLoading.value) { - return const Center( - child: CircularProgressIndicator()); - } + final categoryMap = controller.categoryIdNameMap; + final String selectedName = + controller.selectedCategoryId.value != null + ? (categoryMap[ + controller.selectedCategoryId.value!] ?? + 'Select Category') + : 'Select Category'; return Container( - constraints: const BoxConstraints(maxHeight: 150), - child: buildEmployeeList(), - ); - }), - const SizedBox(height: 12), - Obx(() { - if (controller.selectedEmployees.isEmpty) { - return const SizedBox.shrink(); - } - return Wrap( - spacing: 8, - runSpacing: 8, - children: - controller.selectedEmployees.map((employee) { - return Chip( - label: Text(employee.name), - deleteIcon: const Icon(Icons.close), - onDeleted: () { - controller.uploadingStates[employee.id]?.value = - false; - controller.updateSelectedEmployees(); - }, - backgroundColor: Colors.blue.shade100, - labelStyle: const TextStyle(color: Colors.black), - ); - }).toList(), + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 14), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + ), + child: PopupMenuButton( + padding: EdgeInsets.zero, + onSelected: (val) { + controller.selectCategory(val); + onCategoryChanged(val); + }, + itemBuilder: (context) => categoryMap.entries + .map( + (entry) => PopupMenuItem( + value: entry.key, + child: Text(entry.value), + ), + ) + .toList(), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + selectedName, + style: const TextStyle( + fontSize: 14, color: Colors.black87), + ), + const Icon(Icons.arrow_drop_down), + ], + ), + ), ); }), const SizedBox(height: 24), @@ -232,7 +179,7 @@ void showManageTaskBottomSheet({ Expanded( child: OutlinedButton( onPressed: () { - Get.back(); // Close bottom sheet + Get.back(); }, style: OutlinedButton.styleFrom( side: const BorderSide(color: Colors.grey), @@ -250,30 +197,42 @@ void showManageTaskBottomSheet({ Expanded( child: ElevatedButton( onPressed: () async { - final taskTeam = controller.selectedEmployees - .map((e) => e.id) - .toList(); + final plannedValue = int.tryParse( + plannedTaskController.text.trim()) ?? + 0; + final comment = + descriptionController.text.trim(); + final assignmentDate = DateTime.now(); - if (taskTeam.isEmpty) { + // 🪵 Log the task creation input values + log.i({ + "message": "Creating task with data", + "parentTaskId": parentTaskId, + "plannedTask": plannedValue, + "comment": comment, + "workAreaId": workAreaId, + "activityId": activityId, + }); + + final selectedCategoryId = + controller.selectedCategoryId.value; + if (selectedCategoryId == null) { showAppSnackbar( - title: "Team Required", - message: - "Please select at least one team member.", - type: SnackbarType.warning, + title: "error", + message: "Please select a work category!", + type: SnackbarType.error, ); return; } final success = await controller.createTask( parentTaskId: parentTaskId, - plannedTask: int.tryParse( - plannedTaskController.text.trim()) ?? - 0, - description: - descriptionController.text.trim(), - taskTeam: taskTeam, - workItemId: workItemId, - assignmentDate: DateTime.now(), + plannedTask: plannedValue, + comment: comment, + workAreaId: workAreaId, + activityId: activityId, + categoryId: + selectedCategoryId, ); if (success) { diff --git a/lib/model/dailyTaskPlaning/report_action_bottom_sheet.dart b/lib/model/dailyTaskPlaning/report_action_bottom_sheet.dart index b7a40a8..addb205 100644 --- a/lib/model/dailyTaskPlaning/report_action_bottom_sheet.dart +++ b/lib/model/dailyTaskPlaning/report_action_bottom_sheet.dart @@ -18,14 +18,17 @@ class ReportActionBottomSheet extends StatefulWidget { final Map taskData; final VoidCallback? onCommentSuccess; final String taskDataId; - final String workItemId; + final String workAreaId; + final String activityId; final VoidCallback onReportSuccess; + const ReportActionBottomSheet({ super.key, required this.taskData, this.onCommentSuccess, required this.taskDataId, - required this.workItemId, + required this.workAreaId, + required this.activityId, required this.onReportSuccess, }); @@ -77,8 +80,7 @@ class _ReportActionBottomSheetState extends State controller.basicValidator.getController('comment')?.clear(); controller.basicValidator.getController('task_id')?.text = widget.taskDataId; - controller.basicValidator.getController('work_item_id')?.text = - widget.workItemId; + controller.selectedImages.clear(); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -486,7 +488,10 @@ class _ReportActionBottomSheetState extends State widget.taskData['plannedWork'] ?? '0') ?? 0, - workItemId: widget.workItemId, + activityId: + widget.activityId, + workAreaId: + widget.workAreaId, onSubmit: () { Navigator.of(context).pop(); }, diff --git a/lib/model/dailyTaskPlaning/task_action_buttons.dart b/lib/model/dailyTaskPlaning/task_action_buttons.dart index 933fc3a..4b0127a 100644 --- a/lib/model/dailyTaskPlaning/task_action_buttons.dart +++ b/lib/model/dailyTaskPlaning/task_action_buttons.dart @@ -106,7 +106,8 @@ class TaskActionButtons { required int completed, required VoidCallback refreshCallback, required String parentTaskID, - required String workItemId, + required String activityId, + required String workAreaId, }) { return OutlinedButton.icon( icon: const Icon(Icons.report, size: 18, color: Colors.amber), @@ -131,7 +132,8 @@ class TaskActionButtons { child: ReportActionBottomSheet( taskData: taskData, taskDataId: parentTaskID, - workItemId: workItemId, + workAreaId: workAreaId, + activityId: activityId, onReportSuccess: refreshCallback, ), ), diff --git a/lib/model/daily_task_model.dart b/lib/model/daily_task_model.dart index 57bf8f0..c25dd70 100644 --- a/lib/model/daily_task_model.dart +++ b/lib/model/daily_task_model.dart @@ -32,8 +32,9 @@ class TaskModel { ? DateTime.tryParse(json['reportedDate']) : null, id: json['id'], - workItem: - json['workItem'] != null ? WorkItem.fromJson(json['workItem']) : null, + workItem: json['workItem'] != null + ? WorkItem.fromJson(json['workItem']) + : null, workItemId: json['workItemId'], plannedTask: json['plannedTask'], completedTask: json['completedTask'], @@ -87,25 +88,39 @@ class WorkItem { } class ActivityMaster { + final String? id; // ✅ Added final String activityName; - ActivityMaster({required this.activityName}); + ActivityMaster({ + this.id, + required this.activityName, + }); factory ActivityMaster.fromJson(Map json) { - return ActivityMaster(activityName: json['activityName'] ?? ''); + return ActivityMaster( + id: json['id']?.toString(), + activityName: json['activityName'] ?? '', + ); } } class WorkArea { + final String? id; // ✅ Added final String areaName; final Floor? floor; - WorkArea({required this.areaName, this.floor}); + WorkArea({ + this.id, + required this.areaName, + this.floor, + }); factory WorkArea.fromJson(Map json) { return WorkArea( + id: json['id']?.toString(), areaName: json['areaName'] ?? '', - floor: json['floor'] != null ? Floor.fromJson(json['floor']) : null, + floor: + json['floor'] != null ? Floor.fromJson(json['floor']) : null, ); } } diff --git a/lib/view/taskPlaning/daily_progress.dart b/lib/view/taskPlaning/daily_progress.dart index 2ff80aa..17d4356 100644 --- a/lib/view/taskPlaning/daily_progress.dart +++ b/lib/view/taskPlaning/daily_progress.dart @@ -368,6 +368,9 @@ class _DailyProgressReportScreenState extends State final activityName = task.workItem?.activityMaster?.activityName ?? 'N/A'; + final activityId = task.workItem?.activityMaster?.id ; + final workAreaId = + task.workItem?.workArea?.id; final location = [ task.workItem?.workArea?.floor?.building?.name, task.workItem?.workArea?.floor?.floorName, @@ -380,7 +383,6 @@ class _DailyProgressReportScreenState extends State ? (completed / planned).clamp(0.0, 1.0) : 0.0; final parentTaskID = task.id; - final workItemId = task.workItem?.id; return Column( children: [ Padding( @@ -474,10 +476,12 @@ class _DailyProgressReportScreenState extends State context: context, task: task, parentTaskID: parentTaskID, - workItemId: workItemId.toString(), + workAreaId: workAreaId.toString(), + activityId: activityId.toString(), completed: completed, refreshCallback: _refreshData, ), + const SizedBox(width: 8), ], TaskActionButtons.commentButton( -- 2.43.0 From c215c4c943b8133ea855dc33bb3ce009352add1d Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 19 Jun 2025 11:03:34 +0530 Subject: [PATCH 06/19] feat: Rename task management function to showCreateTaskBottomSheet and update references --- .../create_task_botom_sheet.dart | 419 +++++++++--------- .../report_action_bottom_sheet.dart | 2 +- 2 files changed, 200 insertions(+), 221 deletions(-) diff --git a/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart b/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart index a86b22e..17e5e10 100644 --- a/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart +++ b/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart @@ -7,7 +7,7 @@ import 'package:marco/helpers/widgets/my_snackbar.dart'; final Logger log = Logger(); -void showManageTaskBottomSheet({ +void showCreateTaskBottomSheet({ required String workArea, required String activity, required String completedWork, @@ -33,232 +33,170 @@ void showManageTaskBottomSheet({ final horizontalPadding = isLarge ? constraints.maxWidth * 0.2 : 16.0; - return SafeArea( + return // Inside showManageTaskBottomSheet... + +SafeArea( + child: Material( + color: Colors.white, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + child: Container( + constraints: const BoxConstraints(maxHeight: 760), + padding: EdgeInsets.fromLTRB(horizontalPadding, 12, horizontalPadding, 24), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( child: Container( - constraints: const BoxConstraints(maxHeight: 760), - padding: EdgeInsets.fromLTRB( - horizontalPadding, 12, horizontalPadding, 24), - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + width: 40, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.grey.shade400, + borderRadius: BorderRadius.circular(2), ), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + ), + ), + Center( + child: MyText.titleLarge( + "Create Task", + fontWeight: 700, + ), + ), + const SizedBox(height: 20), + _infoCardSection([ + _infoRowWithIcon(Icons.workspaces, "Selected Work Area", workArea), + _infoRowWithIcon(Icons.list_alt, "Selected Activity", activity), + _infoRowWithIcon(Icons.check_circle_outline, "Completed Work", completedWork), + ]), + const SizedBox(height: 16), + _sectionTitle(Icons.edit_calendar, "Planned Work"), + const SizedBox(height: 6), + _customTextField( + controller: plannedTaskController, + hint: "Enter planned work", + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + _sectionTitle(Icons.description_outlined, "Comment"), + const SizedBox(height: 6), + _customTextField( + controller: descriptionController, + hint: "Enter task description", + maxLines: 3, + ), + const SizedBox(height: 16), + _sectionTitle(Icons.category_outlined, "Selected Work Category"), + const SizedBox(height: 6), + Obx(() { + final categoryMap = controller.categoryIdNameMap; + final String selectedName = + controller.selectedCategoryId.value != null + ? (categoryMap[controller.selectedCategoryId.value!] ?? 'Select Category') + : 'Select Category'; + + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + decoration: BoxDecoration( + color: Colors.grey.shade100, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + ), + child: PopupMenuButton( + padding: EdgeInsets.zero, + onSelected: (val) { + controller.selectCategory(val); + onCategoryChanged(val); + }, + itemBuilder: (context) => categoryMap.entries + .map( + (entry) => PopupMenuItem( + value: entry.key, + child: Text(entry.value), + ), + ) + .toList(), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Center( - child: Container( - width: 40, - height: 4, - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: Colors.grey.shade400, - borderRadius: BorderRadius.circular(2), - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(width: 8), - MyText.titleMedium( - "Create Task", - fontWeight: 700, - ), - ], - ), - const SizedBox(height: 16), - _infoRowWithIcon( - Icons.workspaces, "Selected Work Area", workArea), - _infoRowWithIcon( - Icons.list_alt, "Selected Activity", activity), - const SizedBox(height: 12), - _infoRowWithIcon(Icons.check_circle_outline, - "Completed Work", completedWork), - const SizedBox(height: 12), - Row( - children: [ - Icon(Icons.edit_calendar, - color: Colors.grey[700], size: 18), - const SizedBox(width: 8), - MyText.bodyMedium("Planned Work", fontWeight: 600), - ], - ), - const SizedBox(height: 6), - TextField( - controller: plannedTaskController, - keyboardType: TextInputType.number, - decoration: InputDecoration( - hintText: "Enter planned work", - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8)), - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 10), - ), - ), - const SizedBox(height: 16), - Row( - children: [ - Icon(Icons.description_outlined, - color: Colors.grey[700], size: 18), - const SizedBox(width: 8), - MyText.bodyMedium("Comment", fontWeight: 600), - ], - ), - const SizedBox(height: 6), - TextField( - controller: descriptionController, - maxLines: 3, - decoration: InputDecoration( - hintText: "Enter task description", - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8)), - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 10), - ), - ), - const SizedBox(height: 24), - Row( - children: [ - Icon(Icons.category_outlined, - color: Colors.grey[700], size: 18), - const SizedBox(width: 8), - MyText.bodyMedium("Selected Work Category", - fontWeight: 600), - ], - ), - const SizedBox(height: 6), - Obx(() { - final categoryMap = controller.categoryIdNameMap; - final String selectedName = - controller.selectedCategoryId.value != null - ? (categoryMap[ - controller.selectedCategoryId.value!] ?? - 'Select Category') - : 'Select Category'; - - return Container( - width: double.infinity, - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 14), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(8), - ), - child: PopupMenuButton( - padding: EdgeInsets.zero, - onSelected: (val) { - controller.selectCategory(val); - onCategoryChanged(val); - }, - itemBuilder: (context) => categoryMap.entries - .map( - (entry) => PopupMenuItem( - value: entry.key, - child: Text(entry.value), - ), - ) - .toList(), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - selectedName, - style: const TextStyle( - fontSize: 14, color: Colors.black87), - ), - const Icon(Icons.arrow_drop_down), - ], - ), - ), - ); - }), - const SizedBox(height: 24), - Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () { - Get.back(); - }, - style: OutlinedButton.styleFrom( - side: const BorderSide(color: Colors.grey), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - ), - child: MyText.bodyMedium( - "Cancel", - fontWeight: 600, - color: Colors.black, - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: ElevatedButton( - onPressed: () async { - final plannedValue = int.tryParse( - plannedTaskController.text.trim()) ?? - 0; - final comment = - descriptionController.text.trim(); - final assignmentDate = DateTime.now(); - - // 🪵 Log the task creation input values - log.i({ - "message": "Creating task with data", - "parentTaskId": parentTaskId, - "plannedTask": plannedValue, - "comment": comment, - "workAreaId": workAreaId, - "activityId": activityId, - }); - - final selectedCategoryId = - controller.selectedCategoryId.value; - if (selectedCategoryId == null) { - showAppSnackbar( - title: "error", - message: "Please select a work category!", - type: SnackbarType.error, - ); - return; - } - - final success = await controller.createTask( - parentTaskId: parentTaskId, - plannedTask: plannedValue, - comment: comment, - workAreaId: workAreaId, - activityId: activityId, - categoryId: - selectedCategoryId, - ); - - if (success) { - Get.back(); - onSubmit(); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blueAccent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - ), - child: MyText.bodyMedium( - "Submit", - fontWeight: 600, - color: Colors.white, - ), - ), - ), - ], + Text( + selectedName, + style: const TextStyle(fontSize: 14, color: Colors.black87), ), + const Icon(Icons.arrow_drop_down), ], ), ), - ), - ); + ); + }), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () => Get.back(), + icon: const Icon(Icons.close, size: 18), + label: MyText.bodyMedium("Cancel", fontWeight: 600), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.grey), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: () async { + final plannedValue = int.tryParse(plannedTaskController.text.trim()) ?? 0; + final comment = descriptionController.text.trim(); + final selectedCategoryId = controller.selectedCategoryId.value; + if (selectedCategoryId == null) { + showAppSnackbar( + title: "error", + message: "Please select a work category!", + type: SnackbarType.error, + ); + return; + } + + final success = await controller.createTask( + parentTaskId: parentTaskId, + plannedTask: plannedValue, + comment: comment, + workAreaId: workAreaId, + activityId: activityId, + categoryId: selectedCategoryId, + ); + + if (success) { + Get.back(); + Future.delayed(const Duration(milliseconds: 300), () { + onSubmit(); + showAppSnackbar( + title: "Success", + message: "Task created successfully!", + type: SnackbarType.success, + ); + }); + } + }, + icon: const Icon(Icons.check, size: 18), + label: MyText.bodyMedium("Submit", color: Colors.white, fontWeight: 600), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueAccent, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), +); }, ); }, @@ -266,6 +204,47 @@ void showManageTaskBottomSheet({ isScrollControlled: true, ); } +Widget _sectionTitle(IconData icon, String title) { + return Row( + children: [ + Icon(icon, color: Colors.grey[700], size: 18), + const SizedBox(width: 8), + MyText.bodyMedium(title, fontWeight: 600), + ], + ); +} + +Widget _customTextField({ + required TextEditingController controller, + required String hint, + int maxLines = 1, + TextInputType keyboardType = TextInputType.text, +}) { + return TextField( + controller: controller, + maxLines: maxLines, + keyboardType: keyboardType, + decoration: InputDecoration( + hintText: hint, + filled: true, + fillColor: Colors.grey.shade100, + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + ), + ); +} + +Widget _infoCardSection(List children) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column(children: children), + ); +} Widget _infoRowWithIcon(IconData icon, String title, String value) { return Padding( diff --git a/lib/model/dailyTaskPlaning/report_action_bottom_sheet.dart b/lib/model/dailyTaskPlaning/report_action_bottom_sheet.dart index addb205..893d6c0 100644 --- a/lib/model/dailyTaskPlaning/report_action_bottom_sheet.dart +++ b/lib/model/dailyTaskPlaning/report_action_bottom_sheet.dart @@ -473,7 +473,7 @@ class _ReportActionBottomSheetState extends State if (shouldShowAddTaskSheet) { await Future.delayed( Duration(milliseconds: 100)); - showManageTaskBottomSheet( + showCreateTaskBottomSheet( workArea: widget.taskData['location'] ?? '', activity: widget.taskData['activity'] ?? '', completedWork: -- 2.43.0 From 44d72b73aceaef98e9901491df7c76ff3889584d Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 19 Jun 2025 11:14:21 +0530 Subject: [PATCH 07/19] feat: Add optional approvedBy field to TaskModel and update JSON parsing --- lib/model/daily_task_model.dart | 13 ++++++++----- lib/view/taskPlaning/daily_progress.dart | 8 +++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/model/daily_task_model.dart b/lib/model/daily_task_model.dart index c25dd70..51475fb 100644 --- a/lib/model/daily_task_model.dart +++ b/lib/model/daily_task_model.dart @@ -7,6 +7,7 @@ class TaskModel { final int plannedTask; final int completedTask; final AssignedBy assignedBy; + final AssignedBy? approvedBy; final List teamMembers; final List comments; final List reportedPreSignedUrls; @@ -20,6 +21,7 @@ class TaskModel { required this.plannedTask, required this.completedTask, required this.assignedBy, + this.approvedBy, required this.teamMembers, required this.comments, required this.reportedPreSignedUrls, @@ -32,13 +34,15 @@ class TaskModel { ? DateTime.tryParse(json['reportedDate']) : null, id: json['id'], - workItem: json['workItem'] != null - ? WorkItem.fromJson(json['workItem']) - : null, + workItem: + json['workItem'] != null ? WorkItem.fromJson(json['workItem']) : null, workItemId: json['workItemId'], plannedTask: json['plannedTask'], completedTask: json['completedTask'], assignedBy: AssignedBy.fromJson(json['assignedBy']), + approvedBy: json['approvedBy'] != null + ? AssignedBy.fromJson(json['approvedBy']) + : null, teamMembers: (json['teamMembers'] as List) .map((e) => TeamMember.fromJson(e)) .toList(), @@ -119,8 +123,7 @@ class WorkArea { return WorkArea( id: json['id']?.toString(), areaName: json['areaName'] ?? '', - floor: - json['floor'] != null ? Floor.fromJson(json['floor']) : null, + floor: json['floor'] != null ? Floor.fromJson(json['floor']) : null, ); } } diff --git a/lib/view/taskPlaning/daily_progress.dart b/lib/view/taskPlaning/daily_progress.dart index 17d4356..8ffaf22 100644 --- a/lib/view/taskPlaning/daily_progress.dart +++ b/lib/view/taskPlaning/daily_progress.dart @@ -368,9 +368,8 @@ class _DailyProgressReportScreenState extends State final activityName = task.workItem?.activityMaster?.activityName ?? 'N/A'; - final activityId = task.workItem?.activityMaster?.id ; - final workAreaId = - task.workItem?.workArea?.id; + final activityId = task.workItem?.activityMaster?.id; + final workAreaId = task.workItem?.workArea?.id; final location = [ task.workItem?.workArea?.floor?.building?.name, task.workItem?.workArea?.floor?.floorName, @@ -471,7 +470,7 @@ class _DailyProgressReportScreenState extends State refreshCallback: _refreshData, ), const SizedBox(width: 8), - ] else ...[ + ] else if (task.approvedBy == null) ...[ TaskActionButtons.reportActionButton( context: context, task: task, @@ -481,7 +480,6 @@ class _DailyProgressReportScreenState extends State completed: completed, refreshCallback: _refreshData, ), - const SizedBox(width: 8), ], TaskActionButtons.commentButton( -- 2.43.0 From f834422c4e54e4c8d0c8cc814c5791a662794a10 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 19 Jun 2025 11:26:42 +0530 Subject: [PATCH 08/19] feat: Increase timeout duration for API requests and update relevant calls --- lib/helpers/services/api_service.dart | 42 ++++++++++++++++++--------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 7ff86d9..b7ebb52 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -13,6 +13,7 @@ final Logger logger = Logger(); class ApiService { static const Duration timeout = Duration(seconds: 10); static const bool enableLogs = true; + static const Duration extendedTimeout = Duration(seconds: 60); // === Helpers === @@ -208,8 +209,11 @@ class ApiService { } } - final response = - await _postRequest(ApiEndpoints.uploadAttendanceImage, body); + final response = await _postRequest( + ApiEndpoints.uploadAttendanceImage, + body, + customTimeout: extendedTimeout, + ); if (response == null) return false; final json = jsonDecode(response.body); @@ -259,7 +263,12 @@ class ApiService { "gender": gender, "jobRoleId": jobRoleId, }; - final response = await _postRequest(ApiEndpoints.createEmployee, body); + final response = await _postRequest( + ApiEndpoints.reportTask, + body, + customTimeout: extendedTimeout, + ); + if (response == null) return false; final json = jsonDecode(response.body); return response.statusCode == 200 && json['success'] == true; @@ -309,7 +318,12 @@ class ApiService { if (images != null && images.isNotEmpty) "images": images, }; - final response = await _postRequest(ApiEndpoints.reportTask, body); + final response = await _postRequest( + ApiEndpoints.commentTask, + body, + customTimeout: extendedTimeout, + ); + if (response == null) return false; final json = jsonDecode(response.body); if (response.statusCode == 200 && json['success'] == true) { @@ -414,15 +428,17 @@ class ApiService { DateTime? assignmentDate, required String categoryId, }) async { - final body = [{ - "parentTaskId": parentTaskId, - "plannedWork": plannedTask, - "comment": comment, - "workAreaID": workAreaId, - "activityID": activityId, - "workCategoryId": categoryId, - 'completedWork': 0, - }]; + final body = [ + { + "parentTaskId": parentTaskId, + "plannedWork": plannedTask, + "comment": comment, + "workAreaID": workAreaId, + "activityID": activityId, + "workCategoryId": categoryId, + 'completedWork': 0, + } + ]; final response = await _postRequest(ApiEndpoints.assignTask, body); if (response == null) return false; -- 2.43.0 From d1305e1dba27752641e50ae20a6311b5aa8056b4 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 19 Jun 2025 11:39:52 +0530 Subject: [PATCH 09/19] feat: Increase default timeout duration for API requests from 10 to 30 seconds --- lib/helpers/services/api_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index b7ebb52..40ef2b6 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -11,7 +11,7 @@ import 'package:marco/helpers/services/storage/local_storage.dart'; final Logger logger = Logger(); class ApiService { - static const Duration timeout = Duration(seconds: 10); + static const Duration timeout = Duration(seconds: 30); static const bool enableLogs = true; static const Duration extendedTimeout = Duration(seconds: 60); -- 2.43.0 From 4ba30145efcb38759aab17ab07b3b397e23f7033 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 19 Jun 2025 12:32:35 +0530 Subject: [PATCH 10/19] feat: Update loading state initialization and remove unused fetchProjects method --- .../dashboard/daily_task_controller.dart | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/lib/controller/dashboard/daily_task_controller.dart b/lib/controller/dashboard/daily_task_controller.dart index be0f5e4..64dadd6 100644 --- a/lib/controller/dashboard/daily_task_controller.dart +++ b/lib/controller/dashboard/daily_task_controller.dart @@ -15,7 +15,7 @@ class DailyTaskController extends GetxController { DateTime? endDateTask; List dailyTasks = []; - final RxSet expandedDates = {}.obs; + final RxSet expandedDates = {}.obs; void toggleDate(String dateKey) { if (expandedDates.contains(dateKey)) { @@ -25,7 +25,7 @@ class DailyTaskController extends GetxController { } } - RxBool isLoading = false.obs; + RxBool isLoading = true.obs; Map> groupedDailyTasks = {}; @override void onInit() { @@ -35,7 +35,6 @@ class DailyTaskController extends GetxController { void _initializeDefaults() { _setDefaultDateRange(); - fetchProjects(); } void _setDefaultDateRange() { @@ -45,22 +44,6 @@ class DailyTaskController extends GetxController { log.i("Default date range set: $startDateTask to $endDateTask"); } - Future fetchProjects() async { - isLoading.value = true; - - final response = await ApiService.getProjects(); - isLoading.value = false; - - if (response?.isEmpty ?? true) { - log.w("No project data found or API call failed."); - return; - } - - projects = response!.map((json) => ProjectModel.fromJson(json)).toList(); - log.i("Projects fetched: ${projects.length} projects loaded."); - update(); - } - Future fetchTaskData(String? projectId) async { if (projectId == null) return; -- 2.43.0 From 660bd3cdf1650a61f0ca1b950068405255d32896 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 19 Jun 2025 13:15:02 +0530 Subject: [PATCH 11/19] feat: Enhance project selection interaction by adding tap gesture to collapse dropdown --- lib/view/layouts/layout.dart | 82 ++++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/lib/view/layouts/layout.dart b/lib/view/layouts/layout.dart index eca8b05..5d5f909 100644 --- a/lib/view/layouts/layout.dart +++ b/lib/view/layouts/layout.dart @@ -46,45 +46,53 @@ class _LayoutState extends State { endDrawer: UserProfileBar(), floatingActionButton: widget.floatingActionButton, body: SafeArea( - child: Stack( - children: [ - Column( - children: [ - _buildHeader(context, isMobile), - Expanded( - child: SingleChildScrollView( - key: controller.scrollKey, - padding: EdgeInsets.symmetric( - horizontal: 0, vertical: isMobile ? 16 : 32), - child: widget.child, - ), - ), - ], - ), - // Overlay project list below header - Obx(() { - if (!projectController.isProjectSelectionExpanded.value) { - return const SizedBox.shrink(); - } - return Positioned( - top: 95, // Adjust based on header card height - left: 16, - right: 16, - child: Material( - elevation: 4, - borderRadius: BorderRadius.circular(12), - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + if (projectController.isProjectSelectionExpanded.value) { + projectController.isProjectSelectionExpanded.value = false; + } + }, + child: Stack( + children: [ + Column( + children: [ + _buildHeader(context, isMobile), + Expanded( + child: SingleChildScrollView( + key: controller.scrollKey, + padding: EdgeInsets.symmetric( + horizontal: 0, vertical: isMobile ? 16 : 32), + child: widget.child, ), - padding: const EdgeInsets.all(10), - child: _buildProjectList(context, isMobile), ), - ), - ); - }), - ], + ], + ), + // Project dropdown overlay + Obx(() { + if (!projectController.isProjectSelectionExpanded.value) { + return const SizedBox.shrink(); + } + return Positioned( + top: 95, + left: 16, + right: 16, + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(12), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.all(10), + child: _buildProjectList(context, isMobile), + ), + ), + ); + }), + ], + ), ), ), ); -- 2.43.0 From ef6521faa27a1a6d6e3efa09b8a798434ff52e4c Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 19 Jun 2025 15:35:09 +0530 Subject: [PATCH 12/19] feat: Implement loading skeletons in dashboard and layout screens for better UX --- lib/helpers/services/api_service.dart | 2 +- lib/view/dashboard/dashboard_screen.dart | 40 ++++++++++++++++ lib/view/layouts/layout.dart | 59 ++++++++++++++++++++++-- 3 files changed, 97 insertions(+), 4 deletions(-) diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 40ef2b6..bd7f81c 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -319,7 +319,7 @@ class ApiService { }; final response = await _postRequest( - ApiEndpoints.commentTask, + ApiEndpoints.reportTask, body, customTimeout: extendedTimeout, ); diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index 5227333..82ebe76 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -133,8 +133,21 @@ class _DashboardScreenState extends State with UIMixin { return GetBuilder( id: 'dashboard_controller', builder: (controller) { + final bool isLoading = controller.isLoading.value; final bool isProjectSelected = controller.selectedProject != null; + if (isLoading) { + return Wrap( + spacing: 10, + runSpacing: 10, + children: List.generate( + 4, + (index) => + _buildStatCardSkeleton(MediaQuery.of(context).size.width / 3), + ), + ); + } + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -184,6 +197,33 @@ class _DashboardScreenState extends State with UIMixin { ); } + Widget _buildStatCardSkeleton(double width) { + return MyCard.bordered( + width: width, + height: 100, + paddingAll: 5, + borderRadiusAll: 10, + border: Border.all(color: Colors.grey.withOpacity(0.15)), + shadow: MyShadow(elevation: 1.5, position: MyShadowPosition.bottom), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MyContainer.rounded( + paddingAll: 12, + color: Colors.grey.shade300, + child: const SizedBox(width: 18, height: 18), + ), + MySpacing.height(8), + Container( + height: 12, + width: 60, + color: Colors.grey.shade300, + ), + ], + ), + ); + } + Widget _buildStatCard(_StatItem statItem, double width, bool isEnabled) { return Opacity( opacity: isEnabled ? 1.0 : 0.4, diff --git a/lib/view/layouts/layout.dart b/lib/view/layouts/layout.dart index 5d5f909..6083c23 100644 --- a/lib/view/layouts/layout.dart +++ b/lib/view/layouts/layout.dart @@ -68,7 +68,6 @@ class _LayoutState extends State { ), ], ), - // Project dropdown overlay Obx(() { if (!projectController.isProjectSelectionExpanded.value) { return const SizedBox.shrink(); @@ -102,6 +101,12 @@ class _LayoutState extends State { return Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), child: Obx(() { + final isLoading = projectController.isLoading.value; + + if (isLoading) { + return _buildLoadingSkeleton(); + } + final isExpanded = projectController.isProjectSelectionExpanded.value; final selectedProjectId = projectController.selectedProjectId?.value; final selectedProject = projectController.projects.firstWhereOrNull( @@ -220,8 +225,6 @@ class _LayoutState extends State { ], ), ), - - // Expanded Project List inside card — only show if projects exist if (isExpanded && hasProjects) Positioned( top: 70, @@ -240,6 +243,56 @@ class _LayoutState extends State { ); } + Widget _buildLoadingSkeleton() { + return Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.all(10), + child: Row( + children: [ + Container( + height: 50, + width: 50, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(8), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 18, + width: 140, + color: Colors.grey.shade300, + ), + const SizedBox(height: 6), + Container( + height: 14, + width: 100, + color: Colors.grey.shade200, + ), + ], + ), + ), + const SizedBox(width: 10), + Container( + height: 30, + width: 30, + color: Colors.grey.shade300, + ), + ], + ), + ), + ); + } + Widget _buildProjectList(BuildContext context, bool isMobile) { return Column( crossAxisAlignment: CrossAxisAlignment.start, -- 2.43.0 From 97c873167faae772911af6d9517de8d45e00bb7b Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 19 Jun 2025 16:22:48 +0530 Subject: [PATCH 13/19] feat: Add employee list skeleton loader for improved loading experience --- .../Attendence/attendance_screen.dart | 95 ++++++++++++++++--- 1 file changed, 80 insertions(+), 15 deletions(-) diff --git a/lib/view/dashboard/Attendence/attendance_screen.dart b/lib/view/dashboard/Attendence/attendance_screen.dart index 0271cfa..a1c55b0 100644 --- a/lib/view/dashboard/Attendence/attendance_screen.dart +++ b/lib/view/dashboard/Attendence/attendance_screen.dart @@ -76,9 +76,9 @@ class _AttendanceScreenState extends State with UIMixin { elevation: 0.5, foregroundColor: Colors.black, titleSpacing: 0, - centerTitle: false, + centerTitle: false, leading: Padding( - padding: const EdgeInsets.only(top: 15.0), + padding: const EdgeInsets.only(top: 15.0), child: IconButton( icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black, size: 20), @@ -88,7 +88,7 @@ class _AttendanceScreenState extends State with UIMixin { ), ), title: Padding( - padding: const EdgeInsets.only(top: 15.0), + padding: const EdgeInsets.only(top: 15.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, @@ -304,10 +304,7 @@ class _AttendanceScreenState extends State with UIMixin { ), ), if (isLoading) - const SizedBox( - height: 120, - child: Center(child: CircularProgressIndicator()), - ) + employeeListSkeletonLoader() else if (employees.isEmpty) SizedBox( height: 120, @@ -453,6 +450,80 @@ class _AttendanceScreenState extends State with UIMixin { }); } + Widget employeeListSkeletonLoader() { + return MyCard.bordered( + borderRadiusAll: 4, + paddingAll: 8, + child: Column( + children: List.generate(4, (index) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + // Avatar placeholder + Container( + width: 31, + height: 31, + decoration: BoxDecoration( + color: Colors.grey.shade300, + shape: BoxShape.circle, + ), + ), + MySpacing.width(16), + // Employee name/designation & buttons + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 12, + width: 100, + color: Colors.grey.shade300, + ), + MySpacing.height(8), + Container( + height: 10, + width: 80, + color: Colors.grey.shade300, + ), + MySpacing.height(12), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + height: 28, + width: 60, + color: Colors.grey.shade300, + ), + MySpacing.width(8), + Container( + height: 28, + width: 60, + color: Colors.grey.shade300, + ), + ], + ), + ], + ), + ), + ], + ), + ), + if (index != 3) + Divider( + color: Colors.grey.withOpacity(0.3), + thickness: 1, + height: 1, + ), + ], + ); + }), + ), + ); + } + Widget employeeLog() { return Obx(() { final logs = List.of(attendanceController.attendanceLogs); @@ -503,10 +574,7 @@ class _AttendanceScreenState extends State with UIMixin { ), ), if (attendanceController.isLoadingAttendanceLogs.value) - const SizedBox( - height: 120, - child: Center(child: CircularProgressIndicator()), - ) + employeeListSkeletonLoader() else if (logs.isEmpty) SizedBox( height: 120, @@ -691,10 +759,7 @@ class _AttendanceScreenState extends State with UIMixin { Obx(() { final employees = attendanceController.regularizationLogs; if (attendanceController.isLoadingRegularizationLogs.value) { - return SizedBox( - height: 120, - child: const Center(child: CircularProgressIndicator()), - ); + return employeeListSkeletonLoader(); } if (employees.isEmpty) { -- 2.43.0 From 405916bb48bc46aa3e2a993a1589835b676a03de Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 19 Jun 2025 16:42:24 +0530 Subject: [PATCH 14/19] feat: Add custom skeleton loaders for employee list and daily progress report screens --- lib/helpers/widgets/my_custom_skeleton.dart | 197 ++++++++++++++++++ .../Attendence/attendance_screen.dart | 81 +------ lib/view/employees/employees_screen.dart | 4 +- lib/view/taskPlaning/daily_progress.dart | 3 +- lib/view/taskPlaning/daily_task_planing.dart | 3 +- 5 files changed, 207 insertions(+), 81 deletions(-) create mode 100644 lib/helpers/widgets/my_custom_skeleton.dart diff --git a/lib/helpers/widgets/my_custom_skeleton.dart b/lib/helpers/widgets/my_custom_skeleton.dart new file mode 100644 index 0000000..241ca2a --- /dev/null +++ b/lib/helpers/widgets/my_custom_skeleton.dart @@ -0,0 +1,197 @@ +import 'package:flutter/material.dart'; +import 'package:marco/helpers/widgets/my_card.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/utils/my_shadow.dart'; + +class SkeletonLoaders { + // Employee List - Card Style + static Widget employeeListSkeletonLoader() { + return Column( + children: List.generate(4, (index) { + return MyCard.bordered( + borderRadiusAll: 12, + paddingAll: 10, + margin: MySpacing.bottom(12), + shadow: MyShadow(elevation: 3), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Avatar + Container( + width: 41, + height: 41, + decoration: BoxDecoration( + color: Colors.grey.shade300, + shape: BoxShape.circle, + ), + ), + MySpacing.width(16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container(height: 14, width: 100, color: Colors.grey.shade300), + MySpacing.width(8), + Container(height: 12, width: 60, color: Colors.grey.shade300), + ], + ), + MySpacing.height(8), + Row( + children: [ + Icon(Icons.email, size: 16, color: Colors.grey.shade300), + MySpacing.width(4), + Container(height: 10, width: 140, color: Colors.grey.shade300), + ], + ), + MySpacing.height(8), + Row( + children: [ + Icon(Icons.phone, size: 16, color: Colors.grey.shade300), + MySpacing.width(4), + Container(height: 10, width: 100, color: Colors.grey.shade300), + ], + ), + ], + ), + ), + ], + ), + ); + }), + ); + } + + // Employee List - Compact Collapsed Style + static Widget employeeListCollapsedSkeletonLoader() { + return MyCard.bordered( + borderRadiusAll: 4, + paddingAll: 8, + child: Column( + children: List.generate(4, (index) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + // Avatar + Container( + width: 31, + height: 31, + decoration: BoxDecoration( + color: Colors.grey.shade300, + shape: BoxShape.circle, + ), + ), + MySpacing.width(16), + // Name, Designation & Buttons + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container(height: 12, width: 100, color: Colors.grey.shade300), + MySpacing.height(8), + Container(height: 10, width: 80, color: Colors.grey.shade300), + MySpacing.height(12), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container(height: 28, width: 60, color: Colors.grey.shade300), + MySpacing.width(8), + Container(height: 28, width: 60, color: Colors.grey.shade300), + ], + ), + ], + ), + ), + ], + ), + ), + if (index != 3) + Divider( + color: Colors.grey.withOpacity(0.3), + thickness: 1, + height: 1, + ), + ], + ); + }), + ), + ); + } + + // Daily Progress Report Header Loader + static Widget dailyProgressReportSkeletonLoader() { + return MyCard.bordered( + borderRadiusAll: 4, + border: Border.all(color: Colors.grey.withOpacity(0.2)), + shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom), + paddingAll: 8, + child: Column( + children: List.generate(3, (index) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container(height: 14, width: 120, color: Colors.grey.shade300), + Icon(Icons.add_circle, color: Colors.grey.shade300), + ], + ), + if (index != 2) ...[ + MySpacing.height(12), + Divider(color: Colors.grey.withOpacity(0.3), thickness: 1), + MySpacing.height(12), + ], + ], + ); + }), + ), + ); + } + + // Daily Progress Planning (Collapsed View) + static Widget dailyProgressPlanningSkeletonCollapsedOnly() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate(3, (index) { + return MyCard.bordered( + borderRadiusAll: 12, + paddingAll: 16, + margin: MySpacing.bottom(12), + shadow: MyShadow(elevation: 3), + child: Row( + children: [ + // Icon placeholder + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.grey.shade300, + shape: BoxShape.circle, + ), + ), + MySpacing.width(12), + // Text line + Expanded( + child: Container(height: 16, color: Colors.grey.shade300), + ), + MySpacing.width(12), + // Expand button placeholder + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey.shade300, + ), + ), + ], + ), + ); + }), + ); + } +} diff --git a/lib/view/dashboard/Attendence/attendance_screen.dart b/lib/view/dashboard/Attendence/attendance_screen.dart index a1c55b0..3974484 100644 --- a/lib/view/dashboard/Attendence/attendance_screen.dart +++ b/lib/view/dashboard/Attendence/attendance_screen.dart @@ -18,6 +18,7 @@ import 'package:marco/model/attendance/attendence_action_button.dart'; import 'package:marco/model/attendance/regualrize_action_button.dart'; import 'package:marco/model/attendance/attendence_filter_sheet.dart'; import 'package:marco/controller/project_controller.dart'; +import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; class AttendanceScreen extends StatefulWidget { AttendanceScreen({super.key}); @@ -304,7 +305,7 @@ class _AttendanceScreenState extends State with UIMixin { ), ), if (isLoading) - employeeListSkeletonLoader() + SkeletonLoaders.employeeListSkeletonLoader() else if (employees.isEmpty) SizedBox( height: 120, @@ -450,80 +451,6 @@ class _AttendanceScreenState extends State with UIMixin { }); } - Widget employeeListSkeletonLoader() { - return MyCard.bordered( - borderRadiusAll: 4, - paddingAll: 8, - child: Column( - children: List.generate(4, (index) { - return Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Row( - children: [ - // Avatar placeholder - Container( - width: 31, - height: 31, - decoration: BoxDecoration( - color: Colors.grey.shade300, - shape: BoxShape.circle, - ), - ), - MySpacing.width(16), - // Employee name/designation & buttons - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 12, - width: 100, - color: Colors.grey.shade300, - ), - MySpacing.height(8), - Container( - height: 10, - width: 80, - color: Colors.grey.shade300, - ), - MySpacing.height(12), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Container( - height: 28, - width: 60, - color: Colors.grey.shade300, - ), - MySpacing.width(8), - Container( - height: 28, - width: 60, - color: Colors.grey.shade300, - ), - ], - ), - ], - ), - ), - ], - ), - ), - if (index != 3) - Divider( - color: Colors.grey.withOpacity(0.3), - thickness: 1, - height: 1, - ), - ], - ); - }), - ), - ); - } - Widget employeeLog() { return Obx(() { final logs = List.of(attendanceController.attendanceLogs); @@ -574,7 +501,7 @@ class _AttendanceScreenState extends State with UIMixin { ), ), if (attendanceController.isLoadingAttendanceLogs.value) - employeeListSkeletonLoader() + SkeletonLoaders.employeeListSkeletonLoader() else if (logs.isEmpty) SizedBox( height: 120, @@ -759,7 +686,7 @@ class _AttendanceScreenState extends State with UIMixin { Obx(() { final employees = attendanceController.regularizationLogs; if (attendanceController.isLoadingRegularizationLogs.value) { - return employeeListSkeletonLoader(); + return SkeletonLoaders.employeeListSkeletonLoader(); } if (employees.isEmpty) { diff --git a/lib/view/employees/employees_screen.dart b/lib/view/employees/employees_screen.dart index 6cc3960..37bbc28 100644 --- a/lib/view/employees/employees_screen.dart +++ b/lib/view/employees/employees_screen.dart @@ -14,7 +14,7 @@ import 'package:marco/controller/dashboard/employees_screen_controller.dart'; import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/model/employees/employee_detail_bottom_sheet.dart'; import 'package:marco/controller/project_controller.dart'; - +import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; class EmployeesScreen extends StatefulWidget { const EmployeesScreen({super.key}); @@ -292,7 +292,7 @@ class _EmployeesScreenState extends State with UIMixin { final isLoading = employeeScreenController.isLoading.value; final employees = employeeScreenController.employees; if (isLoading) { - return const Center(child: CircularProgressIndicator()); + return SkeletonLoaders.employeeListSkeletonLoader(); } if (employees.isEmpty) { return Padding( diff --git a/lib/view/taskPlaning/daily_progress.dart b/lib/view/taskPlaning/daily_progress.dart index 8ffaf22..7076987 100644 --- a/lib/view/taskPlaning/daily_progress.dart +++ b/lib/view/taskPlaning/daily_progress.dart @@ -14,6 +14,7 @@ import 'package:marco/model/dailyTaskPlaning/daily_progress_report_filter.dart'; import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/controller/project_controller.dart'; import 'package:marco/model/dailyTaskPlaning/task_action_buttons.dart'; +import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; class DailyProgressReportScreen extends StatefulWidget { const DailyProgressReportScreen({super.key}); @@ -297,7 +298,7 @@ class _DailyProgressReportScreenState extends State final groupedTasks = dailyTaskController.groupedDailyTasks; if (isLoading) { - return const Center(child: CircularProgressIndicator()); + return SkeletonLoaders.dailyProgressReportSkeletonLoader(); } if (groupedTasks.isEmpty) { diff --git a/lib/view/taskPlaning/daily_task_planing.dart b/lib/view/taskPlaning/daily_task_planing.dart index eb37766..2b1c2d2 100644 --- a/lib/view/taskPlaning/daily_task_planing.dart +++ b/lib/view/taskPlaning/daily_task_planing.dart @@ -11,6 +11,7 @@ import 'package:marco/controller/task_planing/daily_task_planing_controller.dart import 'package:marco/controller/project_controller.dart'; import 'package:percent_indicator/percent_indicator.dart'; import 'package:marco/model/dailyTaskPlaning/assign_task_bottom_sheet .dart'; +import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; class DailyTaskPlaningScreen extends StatefulWidget { DailyTaskPlaningScreen({super.key}); @@ -170,7 +171,7 @@ class _DailyTaskPlaningScreenState extends State final dailyTasks = dailyTaskPlaningController.dailyTasks; if (isLoading) { - return Center(child: CircularProgressIndicator()); + return SkeletonLoaders.dailyProgressPlanningSkeletonCollapsedOnly(); } if (dailyTasks.isEmpty) { -- 2.43.0 From f7671bc5d36a949e5cbd52325eba7b00241ae635 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 19 Jun 2025 18:01:27 +0530 Subject: [PATCH 15/19] feat: Refactor AddEmployeeBottomSheet for improved readability and maintainability --- .../employees/add_employee_bottom_sheet.dart | 670 +++++++++--------- 1 file changed, 329 insertions(+), 341 deletions(-) diff --git a/lib/model/employees/add_employee_bottom_sheet.dart b/lib/model/employees/add_employee_bottom_sheet.dart index c690514..fa0c8d7 100644 --- a/lib/model/employees/add_employee_bottom_sheet.dart +++ b/lib/model/employees/add_employee_bottom_sheet.dart @@ -1,361 +1,103 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; + import 'package:marco/controller/dashboard/add_employee_controller.dart'; -import 'package:marco/helpers/theme/app_theme.dart'; +import 'package:marco/controller/dashboard/employees_screen_controller.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; -import 'package:marco/helpers/widgets/my_button.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text_style.dart'; -import 'package:marco/controller/dashboard/employees_screen_controller.dart'; + class AddEmployeeBottomSheet extends StatefulWidget { @override - _AddEmployeeBottomSheetState createState() => _AddEmployeeBottomSheetState(); + State createState() => _AddEmployeeBottomSheetState(); } -class _AddEmployeeBottomSheetState extends State - with UIMixin { - final AddEmployeeController controller = Get.put(AddEmployeeController()); +class _AddEmployeeBottomSheetState extends State with UIMixin { + final AddEmployeeController _controller = Get.put(AddEmployeeController()); + + late TextEditingController genderController; + late TextEditingController roleController; @override - Widget build(BuildContext context) { - final theme = Theme.of(context); + void initState() { + super.initState(); + genderController = TextEditingController(); + roleController = TextEditingController(); + } - return GetBuilder( - init: controller, - builder: (_) { - return SingleChildScrollView( - padding: MediaQuery.of(context).viewInsets, - child: Container( - padding: - const EdgeInsets.only(top: 12, left: 24, right: 24, bottom: 24), - decoration: BoxDecoration( - color: theme.cardColor, - borderRadius: - const BorderRadius.vertical(top: Radius.circular(24)), - boxShadow: const [ - BoxShadow( - color: Colors.black26, - blurRadius: 10, - spreadRadius: 1, - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Drag handle bar - Center( - child: Container( - width: 40, - height: 4, - margin: const EdgeInsets.only(bottom: 12), - decoration: BoxDecoration( - color: Colors.grey.shade400, - borderRadius: BorderRadius.circular(2), - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - MyText.titleMedium( - "Add Employee", - fontWeight: 600, - fontSize: 18, - ), - ], - ), - MySpacing.height(24), + RelativeRect _popupMenuPosition(BuildContext context) { + final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; + return RelativeRect.fromLTRB(100, 300, overlay.size.width - 100, 0); + } - Form( - key: controller.basicValidator.formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildLabel("First Name"), - MySpacing.height(8), - _buildTextField( - hint: "eg: John", - controller: controller.basicValidator - .getController('first_name')!, - validator: controller.basicValidator - .getValidation('first_name'), - keyboardType: TextInputType.name, - ), - MySpacing.height(24), - - _buildLabel("Last Name"), - MySpacing.height(8), - _buildTextField( - hint: "eg: Doe", - controller: controller.basicValidator - .getController('last_name')!, - validator: controller.basicValidator - .getValidation('last_name'), - keyboardType: TextInputType.name, - ), - MySpacing.height(24), - - _buildLabel("Phone Number"), - MySpacing.height(8), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 14), - decoration: BoxDecoration( - border: - Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(8), - ), - child: PopupMenuButton>( - onSelected: (country) { - setState(() { - controller.selectedCountryCode = - country['code']!; - }); - }, - itemBuilder: (context) => [ - PopupMenuItem( - enabled: false, - padding: EdgeInsets.zero, - child: Container( - padding: EdgeInsets.zero, - height: 200, - width: 100, - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .surface, - borderRadius: - BorderRadius.circular(8), - ), - child: Scrollbar( - child: ListView( - padding: EdgeInsets.zero, - children: controller.countries - .map((country) { - return ListTile( - title: Text( - "${country['name']} (${country['code']})", - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onSurface, - ), - ), - onTap: () => Navigator.pop( - context, country), - hoverColor: Theme.of(context) - .colorScheme - .primary - .withOpacity(0.1), - contentPadding: - const EdgeInsets.symmetric( - horizontal: 12), - ); - }).toList(), - ), - ), - ), - ), - ], - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(controller.selectedCountryCode), - const Icon(Icons.arrow_drop_down), - ], - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - children: [ - TextFormField( - controller: controller.basicValidator - .getController('phone_number'), - validator: (value) { - if (value == null || - value.trim().isEmpty) { - return "Phone number is required"; - } - - final digitsOnly = value.trim(); - final minLength = - controller.minDigitsPerCountry[ - controller - .selectedCountryCode] ?? - 7; - final maxLength = - controller.maxDigitsPerCountry[ - controller - .selectedCountryCode] ?? - 15; - - if (!RegExp(r'^[0-9]+$') - .hasMatch(digitsOnly)) { - return "Phone number must contain digits only"; - } - - if (digitsOnly.length < minLength || - digitsOnly.length > maxLength) { - return "Number Must be between $minLength and $maxLength"; - } - - return null; - }, - keyboardType: TextInputType.phone, - decoration: - _inputDecoration("eg: 9876543210") - .copyWith( - suffixIcon: IconButton( - icon: Icon(Icons.contacts), - tooltip: "Pick from contacts", - onPressed: () async { - await controller - .pickContact(context); - }, - ), - ), - ), - ], - ), - ), - ], - ), - ], - ), - MySpacing.height(24), - - _buildLabel("Select Gender"), - MySpacing.height(8), - DropdownButtonFormField( - value: controller.selectedGender, - dropdownColor: contentTheme.background, - isDense: true, - menuMaxHeight: 200, - decoration: _inputDecoration("Select Gender"), - icon: const Icon(Icons.expand_more, size: 20), - items: Gender.values - .map( - (gender) => DropdownMenuItem( - value: gender, - child: MyText.labelMedium( - gender.name.capitalizeFirst!), - ), - ) - .toList(), - onChanged: controller.onGenderSelected, - ), - MySpacing.height(24), - - _buildLabel("Select Role"), - MySpacing.height(8), - DropdownButtonFormField( - value: controller.selectedRoleId, - dropdownColor: contentTheme.background, - isDense: true, - decoration: _inputDecoration("Select Role"), - icon: const Icon(Icons.expand_more, size: 20), - items: controller.roles - .map( - (role) => DropdownMenuItem( - value: role['id'], - child: Text(role['name']), - ), - ) - .toList(), - onChanged: controller.onRoleSelected, - ), - MySpacing.height(24), - - // Buttons row - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - MyButton.text( - onPressed: () => Navigator.pop(context), - padding: MySpacing.xy(20, 16), - child: MyText.bodySmall("Cancel"), - ), - MySpacing.width(12), - MyButton( - onPressed: () async { - if (controller.basicValidator.validateForm()) { - final success = - await controller.createEmployees(); - if (success) { - // Call refresh logic here via callback or GetX - final employeeController = - Get.find(); - final projectId = - employeeController.selectedProjectId; - if (projectId == null) { - await employeeController - .fetchAllEmployees(); - } else { - await employeeController - .fetchEmployeesByProject(projectId); - } - employeeController - .update(['employee_screen_controller']); - controller.basicValidator - .getController("first_name") - ?.clear(); - controller.basicValidator - .getController("last_name") - ?.clear(); - controller.basicValidator - .getController("phone_number") - ?.clear(); - - controller.selectedGender = null; - controller.selectedRoleId = null; - controller.update(); - } - } - }, - elevation: 0, - padding: MySpacing.xy(20, 16), - backgroundColor: Colors.blueAccent, - borderRadiusAll: AppStyle.buttonRadius.medium, - child: MyText.bodySmall("Save", - color: contentTheme.onPrimary), - ), - ], - ), - ], - ), - ), - ], - ), - ), + void _showGenderPopup(BuildContext context) async { + final selected = await showMenu( + context: context, + position: _popupMenuPosition(context), + items: Gender.values.map((gender) { + return PopupMenuItem( + value: gender, + child: Text(gender.name.capitalizeFirst!), ); - }, + }).toList(), + ); + + if (selected != null) { + _controller.onGenderSelected(selected); + _controller.update(); + } + } + + void _showRolePopup(BuildContext context) async { + final selected = await showMenu( + context: context, + position: _popupMenuPosition(context), + items: _controller.roles.map((role) { + return PopupMenuItem( + value: role['id'], + child: Text(role['name']), + ); + }).toList(), + ); + + if (selected != null) { + _controller.onRoleSelected(selected); + _controller.update(); + } + } + + Widget _sectionLabel(String title) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.labelLarge(title, fontWeight: 600), + MySpacing.height(4), + Divider(thickness: 1, color: Colors.grey.shade200), + ], ); } - Widget _buildLabel(String text) => MyText.labelMedium(text); - - Widget _buildTextField({ + Widget _inputWithIcon({ + required String label, + required String hint, + required IconData icon, required TextEditingController controller, required String? Function(String?)? validator, - required String hint, - TextInputType keyboardType = TextInputType.text, }) { - return TextFormField( - controller: controller, - validator: validator, - keyboardType: keyboardType, - decoration: _inputDecoration(hint), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.labelMedium(label), + MySpacing.height(8), + TextFormField( + controller: controller, + validator: validator, + decoration: _inputDecoration(hint).copyWith( + prefixIcon: Icon(icon, size: 20), + ), + ), + ], ); } @@ -363,12 +105,258 @@ class _AddEmployeeBottomSheetState extends State return InputDecoration( hintText: hint, hintStyle: MyTextStyle.bodySmall(xMuted: true), - border: outlineInputBorder, - enabledBorder: outlineInputBorder, - focusedBorder: focusedInputBorder, + filled: true, + fillColor: Colors.grey.shade100, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.blueAccent, width: 1.5), + ), contentPadding: MySpacing.all(16), - isCollapsed: true, - floatingLabelBehavior: FloatingLabelBehavior.never, + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return GetBuilder( + init: _controller, + builder: (_) { + return SingleChildScrollView( + padding: MediaQuery.of(context).viewInsets, + child: Container( + decoration: BoxDecoration( + color: theme.cardColor, + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 12, offset: Offset(0, -2))], + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Drag Handle + Container( + width: 40, + height: 5, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(10), + ), + ), + MySpacing.height(12), + Text("Add Employee", style: MyTextStyle.titleLarge(fontWeight: 700)), + MySpacing.height(24), + Form( + key: _controller.basicValidator.formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionLabel("Personal Info"), + MySpacing.height(16), + _inputWithIcon( + label: "First Name", + hint: "e.g., John", + icon: Icons.person, + controller: _controller.basicValidator.getController('first_name')!, + validator: _controller.basicValidator.getValidation('first_name'), + ), + MySpacing.height(16), + _inputWithIcon( + label: "Last Name", + hint: "e.g., Doe", + icon: Icons.person_outline, + controller: _controller.basicValidator.getController('last_name')!, + validator: _controller.basicValidator.getValidation('last_name'), + ), + MySpacing.height(16), + _sectionLabel("Contact Details"), + MySpacing.height(16), + MyText.labelMedium("Phone Number"), + MySpacing.height(8), + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(12), + color: Colors.grey.shade100, + ), + child: PopupMenuButton>( + onSelected: (country) { + _controller.selectedCountryCode = country['code']!; + _controller.update(); + }, + itemBuilder: (context) => [ + PopupMenuItem( + enabled: false, + padding: EdgeInsets.zero, + child: SizedBox( + height: 200, + width: 100, + child: ListView( + children: _controller.countries.map((country) { + return ListTile( + dense: true, + title: Text("${country['name']} (${country['code']})"), + onTap: () => Navigator.pop(context, country), + ); + }).toList(), + ), + ), + ), + ], + child: Row( + children: [ + Text(_controller.selectedCountryCode), + const Icon(Icons.arrow_drop_down), + ], + ), + ), + ), + MySpacing.width(12), + Expanded( + child: TextFormField( + controller: _controller.basicValidator.getController('phone_number'), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return "Phone number is required"; + } + + final digitsOnly = value.trim(); + final minLength = _controller.minDigitsPerCountry[_controller.selectedCountryCode] ?? 7; + final maxLength = _controller.maxDigitsPerCountry[_controller.selectedCountryCode] ?? 15; + + if (!RegExp(r'^[0-9]+$').hasMatch(digitsOnly)) { + return "Only digits allowed"; + } + + if (digitsOnly.length < minLength || digitsOnly.length > maxLength) { + return "Between $minLength–$maxLength digits"; + } + + return null; + }, + keyboardType: TextInputType.phone, + decoration: _inputDecoration("e.g., 9876543210").copyWith( + suffixIcon: IconButton( + icon: const Icon(Icons.contacts), + onPressed: () => _controller.pickContact(context), + ), + ), + ), + ), + ], + ), + MySpacing.height(24), + _sectionLabel("Other Details"), + MySpacing.height(16), + MyText.labelMedium("Gender"), + MySpacing.height(8), + GestureDetector( + onTap: () => _showGenderPopup(context), + child: AbsorbPointer( + child: TextFormField( + readOnly: true, + controller: TextEditingController( + text: _controller.selectedGender?.name.capitalizeFirst, + ), + decoration: _inputDecoration("Select Gender").copyWith( + suffixIcon: const Icon(Icons.expand_more), + ), + ), + ), + ), + MySpacing.height(16), + MyText.labelMedium("Role"), + MySpacing.height(8), + GestureDetector( + onTap: () => _showRolePopup(context), + child: AbsorbPointer( + child: TextFormField( + readOnly: true, + controller: TextEditingController( + text: _controller.roles.firstWhereOrNull( + (role) => role['id'] == _controller.selectedRoleId, + )?['name'] ?? "", + ), + decoration: _inputDecoration("Select Role").copyWith( + suffixIcon: const Icon(Icons.expand_more), + ), + ), + ), + ), + MySpacing.height(16), + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.close, size: 18), + label: MyText.bodyMedium("Cancel", fontWeight: 600), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.grey), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: () async { + if (_controller.basicValidator.validateForm()) { + final success = await _controller.createEmployees(); + if (success) { + final employeeController = Get.find(); + final projectId = employeeController.selectedProjectId; + + if (projectId == null) { + await employeeController.fetchAllEmployees(); + } else { + await employeeController.fetchEmployeesByProject(projectId); + } + + employeeController.update(['employee_screen_controller']); + + _controller.basicValidator.getController("first_name")?.clear(); + _controller.basicValidator.getController("last_name")?.clear(); + _controller.basicValidator.getController("phone_number")?.clear(); + _controller.selectedGender = null; + _controller.selectedRoleId = null; + _controller.update(); + + Navigator.pop(context); + } + } + }, + icon: const Icon(Icons.check, size: 18), + label: MyText.bodyMedium("Save", color: Colors.white, fontWeight: 600), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueAccent, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, ); } } -- 2.43.0 From ca8bc26ab5347222da60b81e7f305b68d428aa5a Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 19 Jun 2025 18:02:37 +0530 Subject: [PATCH 16/19] feat: Refactor showCreateTaskBottomSheet for improved structure and readability --- .../create_task_botom_sheet.dart | 333 ++++++++++-------- 1 file changed, 177 insertions(+), 156 deletions(-) diff --git a/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart b/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart index 17e5e10..2ca2e3f 100644 --- a/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart +++ b/lib/model/dailyTaskPlaning/create_task_botom_sheet.dart @@ -35,168 +35,188 @@ void showCreateTaskBottomSheet({ return // Inside showManageTaskBottomSheet... -SafeArea( - child: Material( - color: Colors.white, - borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), - child: Container( - constraints: const BoxConstraints(maxHeight: 760), - padding: EdgeInsets.fromLTRB(horizontalPadding, 12, horizontalPadding, 24), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: Container( - width: 40, - height: 4, - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: Colors.grey.shade400, - borderRadius: BorderRadius.circular(2), - ), - ), - ), - Center( - child: MyText.titleLarge( - "Create Task", - fontWeight: 700, - ), - ), - const SizedBox(height: 20), - _infoCardSection([ - _infoRowWithIcon(Icons.workspaces, "Selected Work Area", workArea), - _infoRowWithIcon(Icons.list_alt, "Selected Activity", activity), - _infoRowWithIcon(Icons.check_circle_outline, "Completed Work", completedWork), - ]), - const SizedBox(height: 16), - _sectionTitle(Icons.edit_calendar, "Planned Work"), - const SizedBox(height: 6), - _customTextField( - controller: plannedTaskController, - hint: "Enter planned work", - keyboardType: TextInputType.number, - ), - const SizedBox(height: 16), - _sectionTitle(Icons.description_outlined, "Comment"), - const SizedBox(height: 6), - _customTextField( - controller: descriptionController, - hint: "Enter task description", - maxLines: 3, - ), - const SizedBox(height: 16), - _sectionTitle(Icons.category_outlined, "Selected Work Category"), - const SizedBox(height: 6), - Obx(() { - final categoryMap = controller.categoryIdNameMap; - final String selectedName = - controller.selectedCategoryId.value != null - ? (categoryMap[controller.selectedCategoryId.value!] ?? 'Select Category') - : 'Select Category'; - - return Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), - decoration: BoxDecoration( - color: Colors.grey.shade100, - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(8), - ), - child: PopupMenuButton( - padding: EdgeInsets.zero, - onSelected: (val) { - controller.selectCategory(val); - onCategoryChanged(val); - }, - itemBuilder: (context) => categoryMap.entries - .map( - (entry) => PopupMenuItem( - value: entry.key, - child: Text(entry.value), + SafeArea( + child: Material( + color: Colors.white, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(20)), + child: Container( + constraints: const BoxConstraints(maxHeight: 760), + padding: EdgeInsets.fromLTRB( + horizontalPadding, 12, horizontalPadding, 24), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 40, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.grey.shade400, + borderRadius: BorderRadius.circular(2), + ), + ), ), - ) - .toList(), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - selectedName, - style: const TextStyle(fontSize: 14, color: Colors.black87), - ), - const Icon(Icons.arrow_drop_down), - ], - ), - ), - ); - }), - const SizedBox(height: 24), - Row( - children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: () => Get.back(), - icon: const Icon(Icons.close, size: 18), - label: MyText.bodyMedium("Cancel", fontWeight: 600), - style: OutlinedButton.styleFrom( - side: const BorderSide(color: Colors.grey), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: ElevatedButton.icon( - onPressed: () async { - final plannedValue = int.tryParse(plannedTaskController.text.trim()) ?? 0; - final comment = descriptionController.text.trim(); - final selectedCategoryId = controller.selectedCategoryId.value; - if (selectedCategoryId == null) { - showAppSnackbar( - title: "error", - message: "Please select a work category!", - type: SnackbarType.error, - ); - return; - } + Center( + child: MyText.titleLarge( + "Create Task", + fontWeight: 700, + ), + ), + const SizedBox(height: 20), + _infoCardSection([ + _infoRowWithIcon( + Icons.workspaces, "Selected Work Area", workArea), + _infoRowWithIcon( + Icons.list_alt, "Selected Activity", activity), + _infoRowWithIcon(Icons.check_circle_outline, + "Completed Work", completedWork), + ]), + const SizedBox(height: 16), + _sectionTitle(Icons.edit_calendar, "Planned Work"), + const SizedBox(height: 6), + _customTextField( + controller: plannedTaskController, + hint: "Enter planned work", + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + _sectionTitle(Icons.description_outlined, "Comment"), + const SizedBox(height: 6), + _customTextField( + controller: descriptionController, + hint: "Enter task description", + maxLines: 3, + ), + const SizedBox(height: 16), + _sectionTitle( + Icons.category_outlined, "Selected Work Category"), + const SizedBox(height: 6), + Obx(() { + final categoryMap = controller.categoryIdNameMap; + final String selectedName = + controller.selectedCategoryId.value != null + ? (categoryMap[controller + .selectedCategoryId.value!] ?? + 'Select Category') + : 'Select Category'; - final success = await controller.createTask( - parentTaskId: parentTaskId, - plannedTask: plannedValue, - comment: comment, - workAreaId: workAreaId, - activityId: activityId, - categoryId: selectedCategoryId, - ); - - if (success) { - Get.back(); - Future.delayed(const Duration(milliseconds: 300), () { - onSubmit(); - showAppSnackbar( - title: "Success", - message: "Task created successfully!", - type: SnackbarType.success, + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 14), + decoration: BoxDecoration( + color: Colors.grey.shade100, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + ), + child: PopupMenuButton( + padding: EdgeInsets.zero, + onSelected: (val) { + controller.selectCategory(val); + onCategoryChanged(val); + }, + itemBuilder: (context) => categoryMap.entries + .map( + (entry) => PopupMenuItem( + value: entry.key, + child: Text(entry.value), + ), + ) + .toList(), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + selectedName, + style: const TextStyle( + fontSize: 14, color: Colors.black87), + ), + const Icon(Icons.arrow_drop_down), + ], + ), + ), ); - }); - } - }, - icon: const Icon(Icons.check, size: 18), - label: MyText.bodyMedium("Submit", color: Colors.white, fontWeight: 600), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blueAccent, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + }), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () => Get.back(), + icon: const Icon(Icons.close, size: 18), + label: MyText.bodyMedium("Cancel", + fontWeight: 600), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.grey), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: () async { + final plannedValue = int.tryParse( + plannedTaskController.text.trim()) ?? + 0; + final comment = + descriptionController.text.trim(); + final selectedCategoryId = + controller.selectedCategoryId.value; + if (selectedCategoryId == null) { + showAppSnackbar( + title: "error", + message: "Please select a work category!", + type: SnackbarType.error, + ); + return; + } + + final success = await controller.createTask( + parentTaskId: parentTaskId, + plannedTask: plannedValue, + comment: comment, + workAreaId: workAreaId, + activityId: activityId, + categoryId: selectedCategoryId, + ); + + if (success) { + Get.back(); + Future.delayed( + const Duration(milliseconds: 300), () { + onSubmit(); + showAppSnackbar( + title: "Success", + message: "Task created successfully!", + type: SnackbarType.success, + ); + }); + } + }, + icon: const Icon(Icons.check, size: 18), + label: MyText.bodyMedium("Submit", + color: Colors.white, fontWeight: 600), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueAccent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), + ), + ), + ), + ], + ), + ], ), ), ), - ], - ), - ], - ), - ), - ), - ), -); + ), + ); }, ); }, @@ -204,6 +224,7 @@ SafeArea( isScrollControlled: true, ); } + Widget _sectionTitle(IconData icon, String title) { return Row( children: [ -- 2.43.0 From 6cdf35374d467a64d84a7446eb97cb209999dc29 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Fri, 20 Jun 2025 19:01:58 +0530 Subject: [PATCH 17/19] feat: Enhance dashboard functionality with attendance overview and chart visualization --- .../dashboard/dashboard_controller.dart | 110 +++++-- lib/helpers/services/api_endpoints.dart | 3 + lib/helpers/services/api_service.dart | 15 + lib/helpers/widgets/my_custom_skeleton.dart | 31 ++ .../dashboard/attendance_overview_model.dart | 19 ++ lib/view/dashboard/dashboard_chart.dart | 295 ++++++++++++++++++ lib/view/dashboard/dashboard_screen.dart | 18 +- 7 files changed, 454 insertions(+), 37 deletions(-) create mode 100644 lib/model/dashboard/attendance_overview_model.dart create mode 100644 lib/view/dashboard/dashboard_chart.dart diff --git a/lib/controller/dashboard/dashboard_controller.dart b/lib/controller/dashboard/dashboard_controller.dart index 2cd18a1..2ce16bc 100644 --- a/lib/controller/dashboard/dashboard_controller.dart +++ b/lib/controller/dashboard/dashboard_controller.dart @@ -1,54 +1,100 @@ import 'package:get/get.dart'; import 'package:logger/logger.dart'; import 'package:marco/helpers/services/api_service.dart'; -import 'package:marco/model/project_model.dart'; +import 'package:marco/controller/project_controller.dart'; final Logger log = Logger(); class DashboardController extends GetxController { - RxList projects = [].obs; - RxString? selectedProjectId; - var isProjectListExpanded = false.obs; - RxBool isProjectSelectionExpanded = true.obs; + // Observables + final RxList> roleWiseData = >[].obs; + final RxBool isLoading = false.obs; + final RxString selectedRange = '7D'.obs; + final RxBool isChartView = true.obs; - void toggleProjectListExpanded() { - isProjectListExpanded.value = !isProjectListExpanded.value; - } - - var isProjectDropdownExpanded = false.obs; - - RxBool isLoading = true.obs; - RxBool isLoadingProjects = true.obs; - RxMap uploadingStates = {}.obs; + // Inject the ProjectController + final ProjectController projectController = Get.find(); @override void onInit() { super.onInit(); - fetchProjects(); - } - /// Fetches projects and initializes selected project. - Future fetchProjects() async { - isLoadingProjects.value = true; - isLoading.value = true; + final selectedProjectIdRx = projectController.selectedProjectId; - final response = await ApiService.getProjects(); + if (selectedProjectIdRx != null) { + // Fix: explicitly cast and use ever with non-nullable type + ever(selectedProjectIdRx, (id) { + if (id.isNotEmpty) { + fetchRoleWiseAttendance(); + } + }); - if (response != null && response.isNotEmpty) { - projects.assignAll( - response.map((json) => ProjectModel.fromJson(json)).toList()); - selectedProjectId = RxString(projects.first.id.toString()); - log.i("Projects fetched: ${projects.length}"); + // Initial load if already has value + if (selectedProjectIdRx.value.isNotEmpty) { + fetchRoleWiseAttendance(); + } } else { - log.w("No projects found or API call failed."); + log.w('selectedProjectId observable is null in ProjectController.'); } - isLoadingProjects.value = false; - isLoading.value = false; - update(['dashboard_controller']); + ever(selectedRange, (_) { + fetchRoleWiseAttendance(); + }); } - void updateSelectedProject(String projectId) { - selectedProjectId?.value = projectId; + int get rangeDays => _getDaysFromRange(selectedRange.value); + + int _getDaysFromRange(String range) { + switch (range) { + case '15D': + return 15; + case '30D': + return 30; + case '7D': + default: + return 7; + } + } + + void updateRange(String range) { + selectedRange.value = range; + } + + void toggleChartView(bool isChart) { + isChartView.value = isChart; + } + + Future refreshDashboard() async { + await fetchRoleWiseAttendance(); + } + + Future fetchRoleWiseAttendance() async { + final String? projectId = projectController.selectedProjectId?.value; + + if (projectId == null || projectId.isEmpty) { + log.w('Project ID is null or empty, skipping API call.'); + return; + } + + try { + isLoading.value = true; + + final List? response = + await ApiService.getDashboardAttendanceOverview(projectId, rangeDays); + + if (response != null) { + roleWiseData.value = + response.map((e) => Map.from(e)).toList(); + log.i('Attendance overview fetched successfully.'); + } else { + log.e('Failed to fetch attendance overview: response is null.'); + roleWiseData.clear(); + } + } catch (e, st) { + log.e('Error fetching attendance overview', error: e, stackTrace: st); + roleWiseData.clear(); + } finally { + isLoading.value = false; + } } } diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 87b11cf..12809fd 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -2,6 +2,9 @@ class ApiEndpoints { static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; + // Dashboard Screen API Endpoints + static const String getDashboardAttendanceOverview = "/dashboard/attendance-overview"; + // Attendance Screen API Endpoints static const String getProjects = "/project/list"; static const String getGlobalProjects = "/project/list/basic"; diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index bd7f81c..7e43290 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -122,6 +122,21 @@ class ApiService { } } +// === Dashboard Endpoints === + + static Future?> getDashboardAttendanceOverview( + String projectId, int days) async { + if (projectId.isEmpty) throw ArgumentError('projectId must not be empty'); + if (days <= 0) throw ArgumentError('days must be greater than 0'); + + final endpoint = + "${ApiEndpoints.getDashboardAttendanceOverview}/$projectId?days=$days"; + + return _getRequest(endpoint).then((res) => res != null + ? _parseResponse(res, label: 'Dashboard Attendance Overview') + : null); + } + // === Attendance APIs === static Future?> getProjects() async => diff --git a/lib/helpers/widgets/my_custom_skeleton.dart b/lib/helpers/widgets/my_custom_skeleton.dart index 241ca2a..1ba3a3e 100644 --- a/lib/helpers/widgets/my_custom_skeleton.dart +++ b/lib/helpers/widgets/my_custom_skeleton.dart @@ -4,6 +4,37 @@ import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/utils/my_shadow.dart'; class SkeletonLoaders { + +static Widget buildLoadingSkeleton() { + return SizedBox( + height: 360, + child: Column( + children: List.generate(5, (index) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: List.generate(6, (i) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + width: 48, + height: 16, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(6), + ), + ); + }), + ), + ), + ); + }), + ), + ); +} + + // Employee List - Card Style static Widget employeeListSkeletonLoader() { return Column( diff --git a/lib/model/dashboard/attendance_overview_model.dart b/lib/model/dashboard/attendance_overview_model.dart new file mode 100644 index 0000000..3124e3f --- /dev/null +++ b/lib/model/dashboard/attendance_overview_model.dart @@ -0,0 +1,19 @@ +class AttendanceOverview { + final String role; + final String date; + final int present; + + AttendanceOverview({ + required this.role, + required this.date, + required this.present, + }); + + factory AttendanceOverview.fromJson(Map json) { + return AttendanceOverview( + role: json['role'] ?? '', + date: json['date'] ?? '', + present: json['present'] ?? 0, + ); + } +} diff --git a/lib/view/dashboard/dashboard_chart.dart b/lib/view/dashboard/dashboard_chart.dart new file mode 100644 index 0000000..d4edec1 --- /dev/null +++ b/lib/view/dashboard/dashboard_chart.dart @@ -0,0 +1,295 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import 'package:syncfusion_flutter_charts/charts.dart'; +import 'package:marco/controller/dashboard/dashboard_controller.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; + +class AttendanceDashboardChart extends StatelessWidget { + final DashboardController controller = Get.find(); + + AttendanceDashboardChart({super.key}); + + List> get filteredData { + final now = DateTime.now(); + final daysBack = controller.rangeDays; + return controller.roleWiseData.where((entry) { + final date = DateTime.parse(entry['date']); + return date.isAfter(now.subtract(Duration(days: daysBack))) && + !date.isAfter(now); + }).toList(); + } + + List get filteredDateTimes { + final uniqueDates = filteredData + .map((e) => DateTime.parse(e['date'] as String)) + .toSet() + .toList() + ..sort(); + return uniqueDates; + } + + List get filteredDates => + filteredDateTimes.map((d) => DateFormat('d MMMM').format(d)).toList(); + + List get filteredRoles => + filteredData.map((e) => e['role'] as String).toSet().toList(); + + final Map _roleColorMap = {}; + final List flatColors = [ + const Color(0xFFE57373), + const Color(0xFF64B5F6), + const Color(0xFF81C784), + const Color(0xFFFFB74D), + const Color(0xFFBA68C8), + const Color(0xFFFF8A65), + const Color(0xFF4DB6AC), + const Color(0xFFA1887F), + const Color(0xFFDCE775), + const Color(0xFF9575CD), + ]; + + Color _getRoleColor(String role) { + if (_roleColorMap.containsKey(role)) return _roleColorMap[role]!; + + final index = _roleColorMap.length % flatColors.length; + final color = flatColors[index]; + _roleColorMap[role] = color; + return color; + } + + @override + Widget build(BuildContext context) { + return Obx(() { + final isChartView = controller.isChartView.value; + final selectedRange = controller.selectedRange.value; + final isLoading = controller.isLoading.value; + + return Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xfff0f4f8), Color(0xffe2ebf0)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Card( + color: Colors.white, + elevation: 6, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + shadowColor: Colors.black12, + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(selectedRange, isChartView), + const SizedBox(height: 12), + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: isLoading + ? SkeletonLoaders.buildLoadingSkeleton() + : isChartView + ? _buildChart() + : _buildTable(), + ), + ], + ), + ), + ), + ); + }); + } + + Widget _buildHeader(String selectedRange, bool isChartView) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyMedium('Attendance Overview', fontWeight: 600), + MyText.bodySmall( + 'Role-wise present count', + color: Colors.grey, + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + PopupMenuButton( + padding: EdgeInsets.zero, + tooltip: 'Select Range', + onSelected: (value) => controller.selectedRange.value = value, + itemBuilder: (context) => const [ + PopupMenuItem(value: '7D', child: Text('Last 7 Days')), + PopupMenuItem(value: '15D', child: Text('Last 15 Days')), + PopupMenuItem(value: '30D', child: Text('Last 30 Days')), + ], + child: Row( + children: [ + const Icon(Icons.calendar_today_outlined, size: 18), + const SizedBox(width: 4), + MyText.labelSmall(selectedRange), + ], + ), + ), + const SizedBox(width: 8), + IconButton( + icon: Icon( + Icons.bar_chart_rounded, + size: 20, + color: isChartView ? Colors.blueAccent : Colors.grey, + ), + visualDensity: VisualDensity(horizontal: -4, vertical: -4), + constraints: const BoxConstraints(), + padding: EdgeInsets.zero, + onPressed: () => controller.isChartView.value = true, + tooltip: 'Chart View', + ), + IconButton( + icon: Icon( + Icons.table_chart, + size: 20, + color: !isChartView ? Colors.blueAccent : Colors.grey, + ), + visualDensity: VisualDensity(horizontal: -4, vertical: -4), + constraints: const BoxConstraints(), + padding: EdgeInsets.zero, + onPressed: () => controller.isChartView.value = false, + tooltip: 'Table View', + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildChart() { + final formattedDateMap = { + for (var e in filteredData) + '${e['role']}_${DateFormat('d MMMM').format(DateTime.parse(e['date']))}': + e['present'] + }; + + return SizedBox( + height: 360, + child: SfCartesianChart( + tooltipBehavior: TooltipBehavior( + enable: true, + shared: true, + activationMode: ActivationMode.singleTap, + tooltipPosition: TooltipPosition.pointer, + ), + legend: const Legend( + isVisible: true, + position: LegendPosition.bottom, + overflowMode: LegendItemOverflowMode.wrap, + ), + primaryXAxis: CategoryAxis( + labelRotation: 45, + majorGridLines: const MajorGridLines(width: 0), + ), + primaryYAxis: NumericAxis( + minimum: 0, + interval: 1, + majorGridLines: const MajorGridLines(width: 0), + ), + series: filteredRoles.map((role) { + final data = filteredDates.map((formattedDate) { + final key = '${role}_$formattedDate'; + return { + 'date': formattedDate, + 'present': formattedDateMap[key] ?? 0 + }; + }).toList(); + + return StackedColumnSeries, String>( + dataSource: data, + xValueMapper: (d, _) => d['date'], + yValueMapper: (d, _) => d['present'], + name: role, + legendIconType: LegendIconType.circle, + dataLabelSettings: const DataLabelSettings(isVisible: true), + color: _getRoleColor(role), + ); + }).toList(), + ), + ); + } + + Widget _buildTable() { + final formattedDateMap = { + for (var e in filteredData) + '${e['role']}_${DateFormat('d MMMM').format(DateTime.parse(e['date']))}': + e['present'] + }; + + return Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(12), + ), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: DataTable( + columnSpacing: 28, + headingRowHeight: 42, + headingRowColor: + WidgetStateProperty.all(Colors.blueAccent.withOpacity(0.1)), + headingTextStyle: const TextStyle( + fontWeight: FontWeight.bold, color: Colors.black87), + columns: [ + DataColumn(label: MyText.labelSmall('Role', fontWeight: 600)), + ...filteredDates.map((date) => DataColumn( + label: MyText.labelSmall(date, fontWeight: 600), + )), + ], + rows: filteredRoles.map((role) { + return DataRow( + cells: [ + DataCell(Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: _rolePill(role), + )), + ...filteredDates.map((date) { + final key = '${role}_$date'; + return DataCell(Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: MyText.labelSmall('${formattedDateMap[key] ?? 0}'), + )); + }), + ], + ); + }).toList(), + ), + ), + ); + } + + Widget _rolePill(String role) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: _getRoleColor(role).withOpacity(0.15), + borderRadius: BorderRadius.circular(8), + ), + child: MyText.labelSmall(role, fontWeight: 500), + ); + } +} diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index 82ebe76..bb3e0b6 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -1,19 +1,22 @@ import 'package:flutter/material.dart'; import 'package:flutter_lucide/flutter_lucide.dart'; import 'package:get/get.dart'; +import 'package:marco/controller/dashboard/dashboard_controller.dart'; +import 'package:marco/controller/project_controller.dart'; +import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/utils/my_shadow.dart'; +import 'package:marco/helpers/widgets/my_button.dart'; import 'package:marco/helpers/widgets/my_card.dart'; import 'package:marco/helpers/widgets/my_container.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/view/dashboard/dashboard_chart.dart'; import 'package:marco/view/layouts/layout.dart'; -import 'package:marco/helpers/services/storage/local_storage.dart'; -import 'package:marco/helpers/widgets/my_button.dart'; -import 'package:marco/controller/project_controller.dart'; class DashboardScreen extends StatefulWidget { const DashboardScreen({super.key}); + static const String dashboardRoute = "/dashboard"; static const String employeesRoute = "/dashboard/employees"; static const String projectsRoute = "/dashboard"; @@ -28,6 +31,8 @@ class DashboardScreen extends StatefulWidget { } class _DashboardScreenState extends State with UIMixin { + final DashboardController dashboardController = + Get.put(DashboardController()); bool hasMpin = true; @override @@ -53,6 +58,9 @@ class _DashboardScreenState extends State with UIMixin { children: [ MySpacing.height(12), _buildDashboardStats(), + MySpacing.height(24), + AttendanceDashboardChart(), + MySpacing.height(300), if (!hasMpin) ...[ MyCard( @@ -63,7 +71,7 @@ class _DashboardScreenState extends State with UIMixin { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(Icons.warning_amber_rounded, + const Icon(Icons.warning_amber_rounded, color: Colors.redAccent, size: 28), MySpacing.width(12), Expanded( @@ -93,7 +101,7 @@ class _DashboardScreenState extends State with UIMixin { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.lock_outline, + const Icon(Icons.lock_outline, size: 18, color: Colors.white), MySpacing.width(8), MyText.bodyMedium( -- 2.43.0 From 6c3370437d1a091d190351798adc690847aa9f58 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Sun, 22 Jun 2025 18:40:10 +0530 Subject: [PATCH 18/19] feat: Update models and API service for improved data handling and type safety --- lib/controller/project_controller.dart | 8 +-- lib/helpers/services/api_service.dart | 51 ++++++++++++------- lib/model/attendance_model.dart | 8 +-- .../daily_task_planing_model.dart | 12 +++-- .../dailyTaskPlaning/task_action_buttons.dart | 4 +- lib/model/daily_task_model.dart | 16 +++--- lib/model/global_project_model.dart | 51 +++++++++++++++++++ lib/model/project_model.dart | 16 +++--- lib/view/dashboard/dashboard_screen.dart | 2 +- lib/view/taskPlaning/daily_progress.dart | 4 +- lib/view/taskPlaning/daily_task_planing.dart | 6 +-- 11 files changed, 127 insertions(+), 51 deletions(-) create mode 100644 lib/model/global_project_model.dart diff --git a/lib/controller/project_controller.dart b/lib/controller/project_controller.dart index 7baf879..b30bcdb 100644 --- a/lib/controller/project_controller.dart +++ b/lib/controller/project_controller.dart @@ -1,13 +1,13 @@ import 'package:get/get.dart'; import 'package:logger/logger.dart'; import 'package:marco/helpers/services/api_service.dart'; -import 'package:marco/model/project_model.dart'; +import 'package:marco/model/global_project_model.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; final Logger log = Logger(); class ProjectController extends GetxController { - RxList projects = [].obs; + RxList projects = [].obs; RxString? selectedProjectId; RxBool isProjectListExpanded = false.obs; RxBool isProjectSelectionExpanded = false.obs; @@ -16,7 +16,7 @@ class ProjectController extends GetxController { RxBool isLoading = true.obs; RxBool isLoadingProjects = true.obs; RxMap uploadingStates = {}.obs; - ProjectModel? get selectedProject { + GlobalProjectModel? get selectedProject { if (selectedProjectId == null || selectedProjectId!.value.isEmpty) return null; return projects.firstWhereOrNull((p) => p.id == selectedProjectId!.value); @@ -50,7 +50,7 @@ class ProjectController extends GetxController { if (response != null && response.isNotEmpty) { projects.assignAll( - response.map((json) => ProjectModel.fromJson(json)).toList()); + response.map((json) => GlobalProjectModel.fromJson(json)).toList()); String? savedId = LocalStorage.getString('selectedProjectId'); if (savedId != null && projects.any((p) => p.id == savedId)) { diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 7e43290..bf3903e 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -46,21 +46,29 @@ class ApiService { return null; } - static dynamic _parseResponseForAllData(http.Response response, - {String label = ''}) { - _log("$label Response: ${response.body}"); - try { - final json = jsonDecode(response.body); - if (response.statusCode == 200 && json['success'] == true) { - return json; - } - _log("API Error [$label]: ${json['message'] ?? 'Unknown error'}"); - } catch (e) { - _log("Response parsing error [$label]: $e"); +static dynamic _parseResponseForAllData(http.Response response, + {String label = ''}) { + _log("$label Response: ${response.body}"); + + try { + final body = response.body.trim(); + if (body.isEmpty) throw FormatException("Empty response body"); + + final json = jsonDecode(body); + + if (response.statusCode == 200 && json['success'] == true) { + return json; } - return null; + + _log("API Error [$label]: ${json['message'] ?? 'Unknown error'}"); + } catch (e) { + _log("Response parsing error [$label]: $e"); } + return null; +} + + static Future _getRequest( String endpoint, { Map? queryParams, @@ -144,7 +152,7 @@ class ApiService { (res) => res != null ? _parseResponse(res, label: 'Projects') : null); static Future?> getGlobalProjects() async => - _getRequest(ApiEndpoints.getProjects).then((res) => + _getRequest(ApiEndpoints.getGlobalProjects).then((res) => res != null ? _parseResponse(res, label: 'Global Projects') : null); static Future?> getEmployeesByProject(String projectId) async => @@ -403,10 +411,19 @@ class ApiService { return false; } - static Future?> getWorkStatus() async => - _getRequest(ApiEndpoints.getWorkStatus).then((res) => res != null - ? _parseResponseForAllData(res, label: 'Work Status') - : null); + static Future?> getWorkStatus() async { + final res = await _getRequest(ApiEndpoints.getWorkStatus); + if (res == null) { + _log('Work Status API returned null'); + return null; + } + + _log('Work Status raw response: ${res.body}'); + + return _parseResponseForAllData(res, label: 'Work Status') + as Map?; + } + static Future?> getMasterWorkCategories() async => _getRequest(ApiEndpoints.getmasterWorkCategories).then((res) => res != null diff --git a/lib/model/attendance_model.dart b/lib/model/attendance_model.dart index 0d6f6a1..9a4ef92 100644 --- a/lib/model/attendance_model.dart +++ b/lib/model/attendance_model.dart @@ -6,8 +6,8 @@ class AttendanceModel { final DateTime startDate; final DateTime endDate; final int teamSize; - final int completedWork; - final int plannedWork; + final double completedWork; + final double plannedWork; AttendanceModel({ required this.id, @@ -30,8 +30,8 @@ class AttendanceModel { startDate: DateTime.tryParse(json['startDate']?.toString() ?? '') ?? DateTime.now(), endDate: DateTime.tryParse(json['endDate']?.toString() ?? '') ?? DateTime.now(), teamSize: int.tryParse(json['teamSize']?.toString() ?? '') ?? 0, - completedWork: int.tryParse(json['completedWork']?.toString() ?? '') ?? 0, - plannedWork: int.tryParse(json['plannedWork']?.toString() ?? '') ?? 0, + completedWork: double.tryParse(json['completedWork']?.toString() ?? '') ?? 0, + plannedWork: double.tryParse(json['plannedWork']?.toString() ?? '') ?? 0, ); } diff --git a/lib/model/dailyTaskPlaning/daily_task_planing_model.dart b/lib/model/dailyTaskPlaning/daily_task_planing_model.dart index da2085b..f1e5e64 100644 --- a/lib/model/dailyTaskPlaning/daily_task_planing_model.dart +++ b/lib/model/dailyTaskPlaning/daily_task_planing_model.dart @@ -127,8 +127,8 @@ class WorkItem { final WorkAreaBasic? workArea; final ActivityMaster? activityMaster; final WorkCategoryMaster? workCategoryMaster; - final int? plannedWork; - final int? completedWork; + final double? plannedWork; + final double? completedWork; final DateTime? taskDate; final String? tenantId; final Tenant? tenant; @@ -165,8 +165,12 @@ class WorkItem { ? WorkCategoryMaster.fromJson( json['workCategoryMaster'] as Map) : null, - plannedWork: json['plannedWork'] as int?, - completedWork: json['completedWork'] as int?, + plannedWork: json['plannedWork'] != null + ? (json['plannedWork'] as num).toDouble() + : null, + completedWork: json['completedWork'] != null + ? (json['completedWork'] as num).toDouble() + : null, taskDate: json['taskDate'] != null ? DateTime.tryParse(json['taskDate']) : null, tenantId: json['tenantId'] as String?, diff --git a/lib/model/dailyTaskPlaning/task_action_buttons.dart b/lib/model/dailyTaskPlaning/task_action_buttons.dart index 4b0127a..fd2a5fe 100644 --- a/lib/model/dailyTaskPlaning/task_action_buttons.dart +++ b/lib/model/dailyTaskPlaning/task_action_buttons.dart @@ -82,8 +82,8 @@ class TaskActionButtons { textStyle: const TextStyle(fontSize: 14), ), onPressed: () { - final taskData = - _prepareTaskData(task: task, completed: task.completedTask); + final taskData = _prepareTaskData(task: task, completed: task.completedTask.toInt()); + showModalBottomSheet( context: context, isScrollControlled: true, diff --git a/lib/model/daily_task_model.dart b/lib/model/daily_task_model.dart index 51475fb..4c93b88 100644 --- a/lib/model/daily_task_model.dart +++ b/lib/model/daily_task_model.dart @@ -4,8 +4,8 @@ class TaskModel { final String id; final WorkItem? workItem; final String workItemId; - final int plannedTask; - final int completedTask; + final double plannedTask; + final double completedTask; final AssignedBy assignedBy; final AssignedBy? approvedBy; final List teamMembers; @@ -37,8 +37,8 @@ class TaskModel { workItem: json['workItem'] != null ? WorkItem.fromJson(json['workItem']) : null, workItemId: json['workItemId'], - plannedTask: json['plannedTask'], - completedTask: json['completedTask'], + plannedTask: (json['plannedTask'] as num).toDouble(), + completedTask: (json['completedTask'] as num).toDouble(), assignedBy: AssignedBy.fromJson(json['assignedBy']), approvedBy: json['approvedBy'] != null ? AssignedBy.fromJson(json['approvedBy']) @@ -60,8 +60,8 @@ class WorkItem { final String? id; final ActivityMaster? activityMaster; final WorkArea? workArea; - final int? plannedWork; - final int? completedWork; + final double? plannedWork; + final double? completedWork; final List preSignedUrls; WorkItem({ @@ -81,8 +81,8 @@ class WorkItem { : null, workArea: json['workArea'] != null ? WorkArea.fromJson(json['workArea']) : null, - plannedWork: json['plannedWork'], - completedWork: json['completedWork'], + plannedWork: (json['plannedWork'] as num?)?.toDouble(), + completedWork: (json['completedWork'] as num?)?.toDouble(), preSignedUrls: (json['preSignedUrls'] as List?) ?.map((e) => e.toString()) .toList() ?? diff --git a/lib/model/global_project_model.dart b/lib/model/global_project_model.dart new file mode 100644 index 0000000..5d1c6b5 --- /dev/null +++ b/lib/model/global_project_model.dart @@ -0,0 +1,51 @@ +class GlobalProjectModel { +final String id; +final String name; +final String projectAddress; +final String contactPerson; +final DateTime startDate; +final DateTime endDate; +final int teamSize; +final String projectStatusId; +final String? tenantId; + +GlobalProjectModel({ +required this.id, +required this.name, +required this.projectAddress, +required this.contactPerson, +required this.startDate, +required this.endDate, +required this.teamSize, +required this.projectStatusId, +this.tenantId, +}); + +factory GlobalProjectModel.fromJson(Map json) { +return GlobalProjectModel( +id: json['id'] ?? '', +name: json['name'] ?? '', +projectAddress: json['projectAddress'] ?? '', +contactPerson: json['contactPerson'] ?? '', +startDate: DateTime.parse(json['startDate']), +endDate: DateTime.parse(json['endDate']), +teamSize: json['teamSize'] ?? 0, // ✅ SAFER +projectStatusId: json['projectStatusId'] ?? '', +tenantId: json['tenantId'], +); +} + +Map toJson() { +return { +'id': id, +'name': name, +'projectAddress': projectAddress, +'contactPerson': contactPerson, +'startDate': startDate.toIso8601String(), +'endDate': endDate.toIso8601String(), +'teamSize': teamSize, +'projectStatusId': projectStatusId, +'tenantId': tenantId, +}; +} +} \ No newline at end of file diff --git a/lib/model/project_model.dart b/lib/model/project_model.dart index 066e2ac..9498cf4 100644 --- a/lib/model/project_model.dart +++ b/lib/model/project_model.dart @@ -6,10 +6,10 @@ class ProjectModel { final DateTime startDate; final DateTime endDate; final int teamSize; - final int completedWork; - final int plannedWork; + final double completedWork; + final double plannedWork; final String projectStatusId; - final String? tenantId; + final String? tenantId; ProjectModel({ required this.id, @@ -35,10 +35,14 @@ class ProjectModel { startDate: DateTime.parse(json['startDate']), endDate: DateTime.parse(json['endDate']), teamSize: json['teamSize'], - completedWork: json['completedWork'], - plannedWork: json['plannedWork'], + completedWork: json['completedWork'] != null + ? (json['completedWork'] as num).toDouble() + : 0.0, + plannedWork: json['plannedWork'] != null + ? (json['plannedWork'] as num).toDouble() + : 0.0, projectStatusId: json['projectStatusId'], - tenantId: json['tenantId'], + tenantId: json['tenantId'], ); } diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index bb3e0b6..33df6b5 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -59,7 +59,7 @@ class _DashboardScreenState extends State with UIMixin { MySpacing.height(12), _buildDashboardStats(), MySpacing.height(24), - AttendanceDashboardChart(), + // AttendanceDashboardChart(), MySpacing.height(300), if (!hasMpin) ...[ diff --git a/lib/view/taskPlaning/daily_progress.dart b/lib/view/taskPlaning/daily_progress.dart index 7076987..9d81018 100644 --- a/lib/view/taskPlaning/daily_progress.dart +++ b/lib/view/taskPlaning/daily_progress.dart @@ -467,7 +467,7 @@ class _DailyProgressReportScreenState extends State TaskActionButtons.reportButton( context: context, task: task, - completed: completed, + completed: completed.toInt(), refreshCallback: _refreshData, ), const SizedBox(width: 8), @@ -478,7 +478,7 @@ class _DailyProgressReportScreenState extends State parentTaskID: parentTaskID, workAreaId: workAreaId.toString(), activityId: activityId.toString(), - completed: completed, + completed: completed.toInt(), refreshCallback: _refreshData, ), const SizedBox(width: 8), diff --git a/lib/view/taskPlaning/daily_task_planing.dart b/lib/view/taskPlaning/daily_task_planing.dart index 2b1c2d2..b28fa77 100644 --- a/lib/view/taskPlaning/daily_task_planing.dart +++ b/lib/view/taskPlaning/daily_task_planing.dart @@ -280,10 +280,10 @@ class _DailyTaskPlaningScreenState extends State floorExpansionState[floorWorkAreaKey] ?? false; final totalPlanned = area.workItems .map((wi) => wi.workItem.plannedWork ?? 0) - .fold(0, (prev, curr) => prev + curr); + .fold(0, (prev, curr) => prev + curr); final totalCompleted = area.workItems .map((wi) => wi.workItem.completedWork ?? 0) - .fold(0, (prev, curr) => prev + curr); + .fold(0, (prev, curr) => prev + curr); final totalProgress = totalPlanned == 0 ? 0.0 : (totalCompleted / totalPlanned).clamp(0.0, 1.0); @@ -430,7 +430,7 @@ class _DailyTaskPlaningScreenState extends State onPressed: () { final pendingTask = (planned - completed) - .clamp(0, planned); + .clamp(0, planned).toInt(); showModalBottomSheet( context: context, -- 2.43.0 From 6e11fc8c523576992c953a7eb14002513602500a Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 23 Jun 2025 13:01:46 +0530 Subject: [PATCH 19/19] feat: Enable AttendanceDashboardChart in the dashboard screen --- lib/view/dashboard/dashboard_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index 33df6b5..bb3e0b6 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -59,7 +59,7 @@ class _DashboardScreenState extends State with UIMixin { MySpacing.height(12), _buildDashboardStats(), MySpacing.height(24), - // AttendanceDashboardChart(), + AttendanceDashboardChart(), MySpacing.height(300), if (!hasMpin) ...[ -- 2.43.0