From a41459f16bb58e3d60353d3e8f1ebb361d84fb65 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 12 May 2025 16:27:38 +0530 Subject: [PATCH] Add Report Task feature with controller, model, and UI integration --- .../task_planing/report_task_controller.dart | 119 ++++++++ lib/helpers/services/api_endpoints.dart | 1 + lib/helpers/services/api_service.dart | 136 +++++---- lib/model/daily_task_model.dart | 17 +- lib/routes.dart | 7 + lib/view/dashboard/daily_task_screen.dart | 37 ++- lib/view/taskPlaning/report_task_screen.dart | 259 ++++++++++++++++++ 7 files changed, 517 insertions(+), 59 deletions(-) create mode 100644 lib/controller/task_planing/report_task_controller.dart create mode 100644 lib/view/taskPlaning/report_task_screen.dart diff --git a/lib/controller/task_planing/report_task_controller.dart b/lib/controller/task_planing/report_task_controller.dart new file mode 100644 index 0000000..5c8935f --- /dev/null +++ b/lib/controller/task_planing/report_task_controller.dart @@ -0,0 +1,119 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:marco/controller/my_controller.dart'; +import 'package:marco/helpers/widgets/my_form_validator.dart'; +import 'package:marco/helpers/services/api_service.dart'; +import 'package:get/get.dart'; +import 'package:logger/logger.dart'; + +final Logger logger = Logger(); + +class ReportTaskController extends MyController { + List files = []; + MyFormValidator basicValidator = MyFormValidator(); + RxBool isLoading = false.obs; + + @override + void onInit() { + super.onInit(); + logger.i("Initializing ReportTaskController..."); + + // Add form fields to the validator + basicValidator.addField( + 'assigned_date', + label: "Assigned Date", + controller: TextEditingController(), + ); + basicValidator.addField( + 'work_area', + label: "Work Area", + controller: TextEditingController(), + ); + basicValidator.addField( + 'activity', + label: "Activity", + controller: TextEditingController(), + ); + basicValidator.addField( + 'team_size', + label: "Team Size", + controller: TextEditingController(), + ); + basicValidator.addField( + 'task_id', + label: "Task Id", + controller: TextEditingController(), + ); + basicValidator.addField( + 'assigned', + label: "Assigned", + controller: TextEditingController(), + ); + basicValidator.addField( + 'completed_work', + label: "Completed Work", + required: true, + controller: TextEditingController(), + ); + basicValidator.addField( + 'comment', + label: "Comment", + required: true, + controller: TextEditingController(), + ); + basicValidator.addField( + 'assigned_by', + label: "Assigned By", + controller: TextEditingController(), + ); + + logger.i( + "Fields initialized for assigned_date, work_area, activity, team_size, assigned, completed_work, and comment."); + } + + Future reportTask({ + required String projectId, + required String comment, + required int completedTask, + required List> checklist, + required DateTime reportedDate, + }) async { + logger.i("Starting task report..."); + + final completedWork = + basicValidator.getController('completed_work')?.text.trim(); + final commentField = basicValidator.getController('comment')?.text.trim(); + + if (completedWork == null || completedWork.isEmpty) { + Get.snackbar("Error", "Completed work is required."); + return; + } + + if (commentField == null || commentField.isEmpty) { + Get.snackbar("Error", "Comment is required."); + return; + } + + try { + isLoading.value = true; + + final success = await ApiService.reportTask( + id: projectId, + comment: commentField, + completedTask: completedTask, + checkList: checklist, + ); + + if (success) { + Get.snackbar("Success", "Task reported successfully!"); + } else { + Get.snackbar("Error", "Failed to report task."); + } + } catch (e) { + logger.e("Error reporting task: $e"); + Get.snackbar("Error", "An error occurred while reporting the task."); + } finally { + isLoading.value = false; + } + } +} diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 2d8bac8..03e4350 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -17,4 +17,5 @@ class ApiEndpoints { // Daily Task Screen API Endpoints static const String getDailyTask = "/task/list"; + static const String reportTask = "/task/report"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 8681c90..8c626ed 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -152,66 +152,66 @@ class ApiService { // ===== Upload Attendance Image ===== -static Future uploadAttendanceImage( - String id, - String employeeId, - XFile? imageFile, - double latitude, - double longitude, { - required String imageName, - required String projectId, - String comment = "", - required int action, - bool imageCapture = true, - String? markTime, // <-- Optional markTime parameter -}) async { - final now = DateTime.now(); - final body = { - "id": id, - "employeeId": employeeId, - "projectId": projectId, - "markTime": markTime ?? DateFormat('hh:mm a').format(now), - "comment": comment, - "action": action, - "date": DateFormat('yyyy-MM-dd').format(now), - if (imageCapture) "latitude": '$latitude', - if (imageCapture) "longitude": '$longitude', - }; + static Future uploadAttendanceImage( + String id, + String employeeId, + XFile? imageFile, + double latitude, + double longitude, { + required String imageName, + required String projectId, + String comment = "", + required int action, + bool imageCapture = true, + String? markTime, // <-- Optional markTime parameter + }) async { + final now = DateTime.now(); + final body = { + "id": id, + "employeeId": employeeId, + "projectId": projectId, + "markTime": markTime ?? DateFormat('hh:mm a').format(now), + "comment": comment, + "action": action, + "date": DateFormat('yyyy-MM-dd').format(now), + if (imageCapture) "latitude": '$latitude', + if (imageCapture) "longitude": '$longitude', + }; - if (imageCapture && imageFile != null) { - try { - final bytes = await imageFile.readAsBytes(); - final base64Image = base64Encode(bytes); - final fileSize = await imageFile.length(); - final contentType = "image/${imageFile.path.split('.').last}"; + if (imageCapture && imageFile != null) { + try { + final bytes = await imageFile.readAsBytes(); + final base64Image = base64Encode(bytes); + final fileSize = await imageFile.length(); + final contentType = "image/${imageFile.path.split('.').last}"; - body["image"] = { - "fileName": imageName, - "contentType": contentType, - "fileSize": fileSize, - "description": "Employee attendance photo", - "base64Data": base64Image, - }; - } catch (e) { - _log("Image encoding error: $e"); - return false; + body["image"] = { + "fileName": imageName, + "contentType": contentType, + "fileSize": fileSize, + "description": "Employee attendance photo", + "base64Data": base64Image, + }; + } catch (e) { + _log("Image encoding error: $e"); + return false; + } } - } final response = await _postRequest(ApiEndpoints.uploadAttendanceImage, body); - if (response == null) return false; + if (response == null) return false; - final json = jsonDecode(response.body); - if (response.statusCode == 200 && json['success'] == true) { - return true; - } else { - _log("Failed to upload image: ${json['message'] ?? 'Unknown error'}"); + final json = jsonDecode(response.body); + if (response.statusCode == 200 && json['success'] == true) { + return true; + } else { + _log("Failed to upload image: ${json['message'] ?? 'Unknown error'}"); + } + + return false; } - return false; -} - // ===== Utilities ===== static String generateImageName(String employeeId, int count) { @@ -290,8 +290,9 @@ static Future uploadAttendanceImage( return false; } } + // ===== Daily Tasks API Calls ===== - static Future?> getDailyTasks(String projectId, + static Future?> getDailyTasks(String projectId, {DateTime? dateFrom, DateTime? dateTo}) async { final query = { "projectId": projectId, @@ -306,4 +307,35 @@ static Future uploadAttendanceImage( ? _parseResponse(response, label: 'Daily Tasks') : null; } + + static Future reportTask({ + required String id, + required int completedTask, + required String comment, + required List> checkList, + }) async { + final body = { + "id": id, + "completedTask": completedTask, + "comment": comment, + "reportedDate": DateTime.now().toUtc().toIso8601String(), + "checkList": checkList, + }; + + final response = await _postRequest(ApiEndpoints.reportTask, body); + + if (response == null) { + _log("Error: No response from server."); + return false; + } + + final json = jsonDecode(response.body); + + if (response.statusCode == 200 && json['success'] == true) { + return true; + } else { + _log("Failed to report task: ${json['message'] ?? 'Unknown error'}"); + return false; + } + } } diff --git a/lib/model/daily_task_model.dart b/lib/model/daily_task_model.dart index 6799f0f..3a1df1c 100644 --- a/lib/model/daily_task_model.dart +++ b/lib/model/daily_task_model.dart @@ -1,19 +1,22 @@ class TaskModel { final String assignmentDate; + final String id; final WorkItem? workItem; + final String workItemId; final int plannedTask; final int completedTask; final AssignedBy assignedBy; final List teamMembers; final List comments; - // Remove plannedWork and completedWork from direct properties int get plannedWork => workItem?.plannedWork ?? 0; int get completedWork => workItem?.completedWork ?? 0; TaskModel({ required this.assignmentDate, + required this.id, required this.workItem, + required this.workItemId, required this.plannedTask, required this.completedTask, required this.assignedBy, @@ -23,10 +26,13 @@ class TaskModel { factory TaskModel.fromJson(Map json) { final workItemJson = json['workItem']; - final workItem = workItemJson != null ? WorkItem.fromJson(workItemJson) : null; + final workItem = + workItemJson != null ? WorkItem.fromJson(workItemJson) : null; return TaskModel( assignmentDate: json['assignmentDate'], + id: json['id'] ?? '', + workItemId: json['workItemId'], workItem: workItem, plannedTask: json['plannedTask'], completedTask: json['completedTask'], @@ -44,8 +50,6 @@ class TaskModel { class WorkItem { final ActivityMaster? activityMaster; final WorkArea? workArea; - - // Add plannedWork and completedWork as properties of WorkItem final int? plannedWork; final int? completedWork; @@ -61,14 +65,15 @@ class WorkItem { activityMaster: json['activityMaster'] != null ? ActivityMaster.fromJson(json['activityMaster']) : null, - workArea: json['workArea'] != null ? WorkArea.fromJson(json['workArea']) : null, + workArea: json['workArea'] != null + ? WorkArea.fromJson(json['workArea']) + : null, plannedWork: json['plannedWork'], completedWork: json['completedWork'], ); } } - class ActivityMaster { final String activityName; diff --git a/lib/routes.dart b/lib/routes.dart index c074291..033177b 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -15,6 +15,8 @@ import 'package:marco/view/dashboard/dashboard_screen.dart'; import 'package:marco/view/dashboard/add_employee_screen.dart'; import 'package:marco/view/dashboard/employee_screen.dart'; import 'package:marco/view/dashboard/daily_task_screen.dart'; +import 'package:marco/view/taskPlaning/report_task_screen.dart'; + class AuthMiddleware extends GetMiddleware { @override @@ -53,6 +55,11 @@ getPageRoute() { name: '/dashboard/daily-task', page: () => DailyTaskScreen(), middlewares: [AuthMiddleware()]), + GetPage( + name: '/daily-task/report-task', + page: () => ReportTaskScreen(), + middlewares: [AuthMiddleware()]), + // Authentication GetPage(name: '/auth/login', page: () => LoginScreen()), GetPage( diff --git a/lib/view/dashboard/daily_task_screen.dart b/lib/view/dashboard/daily_task_screen.dart index 8d758e7..0ec77bf 100644 --- a/lib/view/dashboard/daily_task_screen.dart +++ b/lib/view/dashboard/daily_task_screen.dart @@ -266,7 +266,42 @@ class _DailyTaskScreenState extends State with UIMixin { DataCell(Row( children: [ ElevatedButton( - onPressed: () {}, + onPressed: () { + final activityName = + task.workItem?.activityMaster?.activityName ?? 'N/A'; + final assigned = '${task.plannedTask ?? "NA"} / ' + '${(task.workItem?.plannedWork != null && task.workItem?.completedWork != null) ? (task.workItem!.plannedWork! - task.workItem.completedWork!) : "NA"}'; + final assignedBy = + "${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}"; + final completed = task.completedTask.toString(); + final assignedOn = DateFormat('dd-MM-yyyy') + .format(DateTime.parse(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((member) => member.firstName).toList(); + + // Navigate with detailed values + Get.toNamed( + '/daily-task/report-task', + arguments: { + 'activity': activityName, + 'assigned': assigned, + 'taskId': taskId, + 'assignedBy': assignedBy, + 'completed': completed, + 'assignedOn': assignedOn, + 'location': location, + 'teamSize': task.teamMembers.length, + 'teamMembers': teamMembers, + }, + ); + }, style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6), minimumSize: const Size(60, 20), diff --git a/lib/view/taskPlaning/report_task_screen.dart b/lib/view/taskPlaning/report_task_screen.dart new file mode 100644 index 0000000..2edab4f --- /dev/null +++ b/lib/view/taskPlaning/report_task_screen.dart @@ -0,0 +1,259 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_lucide/flutter_lucide.dart'; +import 'package:get/get.dart'; +import 'package:marco/controller/task_planing/report_task_controller.dart'; +import 'package:marco/helpers/theme/app_theme.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/utils/my_shadow.dart'; +import 'package:marco/helpers/widgets/my_breadcrumb.dart'; +import 'package:marco/helpers/widgets/my_breadcrumb_item.dart'; +import 'package:marco/helpers/widgets/my_button.dart'; +import 'package:marco/helpers/widgets/my_card.dart'; +import 'package:marco/helpers/widgets/my_flex.dart'; +import 'package:marco/helpers/widgets/my_flex_item.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/view/layouts/layout.dart'; + +class ReportTaskScreen extends StatefulWidget { + const ReportTaskScreen({super.key}); + + @override + State createState() => _ReportTaskScreenState(); +} + +class _ReportTaskScreenState extends State with UIMixin { + final ReportTaskController controller = Get.put(ReportTaskController()); + + @override + Widget build(BuildContext context) { + final taskData = Get.arguments as Map; + print("Task Data: $taskData"); + controller.basicValidator.getController('assigned_date')?.text = + taskData['assignedOn'] ?? ''; + controller.basicValidator.getController('assigned_by')?.text = + taskData['assignedBy'] ?? ''; + controller.basicValidator.getController('work_area')?.text = + taskData['location'] ?? ''; + controller.basicValidator.getController('activity')?.text = + taskData['activity'] ?? ''; + controller.basicValidator.getController('team_size')?.text = + taskData['teamSize'].toString(); + controller.basicValidator.getController('assigned')?.text = + taskData['assigned'] ?? ''; + controller.basicValidator.getController('task_id')?.text = + taskData['taskId'] ?? ''; + + return Layout( + child: GetBuilder( + init: controller, + tag: 'report_task_controller', + builder: (controller) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: MySpacing.x(flexSpacing), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MyText.titleMedium("Report Task", + fontSize: 18, fontWeight: 600), + MyBreadcrumb( + children: [ + MyBreadcrumbItem(name: 'Daily Task'), + MyBreadcrumbItem(name: 'Report Task'), + ], + ), + ], + ), + ), + MySpacing.height(flexSpacing), + Padding( + padding: MySpacing.x(flexSpacing / 2), + child: MyFlex( + children: [ + MyFlexItem(sizes: "lg-8 md-12", child: detail()), + ], + ), + ), + ], + ); + }, + ), + ); + } + + Widget detail() { + return Form( + key: controller.basicValidator.formKey, + child: MyCard.bordered( + borderRadiusAll: 4, + border: Border.all(color: Colors.grey.withOpacity(0.2)), + shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom), + paddingAll: 24, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(LucideIcons.server, size: 16), + MySpacing.width(12), + MyText.titleMedium("General", fontWeight: 600), + ], + ), + MySpacing.height(24), + + // Static fields + buildRow( + "Assigned Date", + controller.basicValidator + .getController('assigned_date') + ?.text + .trim()), + buildRow( + "Assigned By", + controller.basicValidator + .getController('assigned_by') + ?.text + .trim()), + buildRow( + "Work Area", + controller.basicValidator + .getController('work_area') + ?.text + .trim()), + buildRow( + "Activity", + controller.basicValidator + .getController('activity') + ?.text + .trim()), + buildRow( + "Team Size", + controller.basicValidator + .getController('team_size') + ?.text + .trim()), + buildRow( + "Assigned", + controller.basicValidator + .getController('assigned') + ?.text + .trim()), + + // Input fields + MyText.labelMedium("Completed Work"), + MySpacing.height(8), + TextFormField( + validator: + controller.basicValidator.getValidation('completed_work'), + controller: + controller.basicValidator.getController('completed_work'), + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: "eg: 10", + hintStyle: MyTextStyle.bodySmall(xMuted: true), + border: outlineInputBorder, + enabledBorder: outlineInputBorder, + focusedBorder: focusedInputBorder, + contentPadding: MySpacing.all(16), + isCollapsed: true, + floatingLabelBehavior: FloatingLabelBehavior.never, + ), + ), + MySpacing.height(24), + MyText.labelMedium("Comment"), + 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(24), + + // Buttons + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + MyButton.text( + onPressed: () => Get.back(), + padding: MySpacing.xy(20, 16), + splashColor: contentTheme.secondary.withValues(alpha: 0.1), + child: MyText.bodySmall('Cancel'), + ), + MySpacing.width(12), + MyButton( + onPressed: () async { + if (controller.basicValidator.validateForm()) { + await controller.reportTask( + projectId: controller.basicValidator + .getController('task_id') + ?.text ?? + '', // Replace with actual ID + comment: controller.basicValidator + .getController('comment') + ?.text ?? + '', + completedTask: int.tryParse(controller.basicValidator + .getController('completed_work') + ?.text ?? + '') ?? + 0, + checklist: [], + reportedDate: DateTime.now(), + ); + } + }, + elevation: 0, + padding: MySpacing.xy(20, 16), + backgroundColor: contentTheme.primary, + borderRadiusAll: AppStyle.buttonRadius.medium, + child: MyText.bodySmall( + 'Save', + color: contentTheme.onPrimary, + ), + ), + ], + ), + + // Loading spinner + Obx(() { + return controller.isLoading.value + ? Center(child: CircularProgressIndicator()) + : SizedBox.shrink(); + }), + ], + ), + ), + ); + } + + Widget buildRow(String label, String? value) { + print("Label: $label, Value: $value"); + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.labelMedium("$label:"), + MySpacing.width(12), + Expanded( + child: MyText.bodyMedium(value?.isNotEmpty == true ? value! : "-"), + ), + ], + ), + ); + } +}