diff --git a/lib/controller/directory/directory_controller.dart b/lib/controller/directory/directory_controller.dart index 8de4291..596e63e 100644 --- a/lib/controller/directory/directory_controller.dart +++ b/lib/controller/directory/directory_controller.dart @@ -7,6 +7,7 @@ import 'package:marco/model/directory/directory_comment_model.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; class DirectoryController extends GetxController { + // Contacts RxList allContacts = [].obs; RxList filteredContacts = [].obs; RxList contactCategories = [].obs; @@ -16,16 +17,10 @@ class DirectoryController extends GetxController { RxBool isLoading = false.obs; RxList contactBuckets = [].obs; RxString searchQuery = ''.obs; - RxBool showFabMenu = false.obs; - final RxBool showFullEditorToolbar = false.obs; - final RxBool isEditorFocused = false.obs; - RxBool isNotesView = false.obs; - - final Map> contactCommentsMap = {}; - RxList getCommentsForContact(String contactId) { - return contactCommentsMap[contactId] ?? [].obs; - } + // Notes / Comments + final Map> activeCommentsMap = {}; + final Map> inactiveCommentsMap = {}; final editingCommentId = Rxn(); @override @@ -34,26 +29,53 @@ class DirectoryController extends GetxController { fetchContacts(); fetchBuckets(); } -// inside DirectoryController + + // -------------------- COMMENTS -------------------- + + RxList getCommentsForContact(String contactId, {bool active = true}) { + return active + ? activeCommentsMap[contactId] ?? [].obs + : inactiveCommentsMap[contactId] ?? [].obs; + } + + Future fetchCommentsForContact(String contactId, {bool active = true}) async { + try { + final data = await ApiService.getDirectoryComments(contactId, active: active); + final comments = data?.map((e) => DirectoryComment.fromJson(e)).toList() ?? []; + + if (active) { + activeCommentsMap[contactId] = [].obs..assignAll(comments); + } else { + inactiveCommentsMap[contactId] = [].obs..assignAll(comments); + } + } catch (e, stack) { + logSafe("Error fetching ${active ? 'active' : 'inactive'} comments: $e", level: LogLevel.error); + logSafe(stack.toString(), level: LogLevel.debug); + + if (active) { + activeCommentsMap[contactId] = [].obs; + } else { + inactiveCommentsMap[contactId] = [].obs; + } + } + } + + RxList combinedComments(String contactId) { + final active = getCommentsForContact(contactId, active: true).toList(); + final inactive = getCommentsForContact(contactId, active: false).toList(); + + final combined = [...active, ...inactive] + ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); + + return combined.obs; + } Future updateComment(DirectoryComment comment) async { try { - logSafe( - "Attempting to update comment. id: ${comment.id}, contactId: ${comment.contactId}"); - - final commentList = contactCommentsMap[comment.contactId]; final oldComment = - commentList?.firstWhereOrNull((c) => c.id == comment.id); - - if (oldComment == null) { - logSafe("Old comment not found. id: ${comment.id}"); - } else { - logSafe("Old comment note: ${oldComment.note}"); - logSafe("New comment note: ${comment.note}"); - } + getCommentsForContact(comment.contactId).firstWhereOrNull((c) => c.id == comment.id); if (oldComment != null && oldComment.note.trim() == comment.note.trim()) { - logSafe("No changes detected in comment. id: ${comment.id}"); showAppSnackbar( title: "No Changes", message: "No changes were made to the comment.", @@ -62,33 +84,28 @@ class DirectoryController extends GetxController { return; } - final success = await ApiService.updateContactComment( - comment.id, - comment.note, - comment.contactId, - ); + final success = await ApiService.updateContactComment(comment.id, comment.note, comment.contactId); if (success) { - logSafe("Comment updated successfully. id: ${comment.id}"); - await fetchCommentsForContact(comment.contactId); + await fetchCommentsForContact(comment.contactId, active: true); + await fetchCommentsForContact(comment.contactId, active: false); - // ✅ Show success message showAppSnackbar( title: "Success", message: "Comment updated successfully.", type: SnackbarType.success, ); } else { - logSafe("Failed to update comment via API. id: ${comment.id}"); showAppSnackbar( title: "Error", message: "Failed to update comment.", type: SnackbarType.error, ); } - } catch (e, stackTrace) { - logSafe("Update comment failed: ${e.toString()}"); - logSafe("StackTrace: ${stackTrace.toString()}"); + } catch (e, stack) { + logSafe("Update comment failed: ${e.toString()}", level: LogLevel.error); + logSafe(stack.toString(), level: LogLevel.debug); + showAppSnackbar( title: "Error", message: "Failed to update comment.", @@ -97,45 +114,15 @@ class DirectoryController extends GetxController { } } - Future fetchCommentsForContact(String contactId, - {bool active = true}) async { - try { - final data = - await ApiService.getDirectoryComments(contactId, active: active); - logSafe( - "Fetched ${active ? 'active' : 'inactive'} comments for contact $contactId: $data"); - - final comments = - data?.map((e) => DirectoryComment.fromJson(e)).toList() ?? []; - - if (!contactCommentsMap.containsKey(contactId)) { - contactCommentsMap[contactId] = [].obs; - } - - contactCommentsMap[contactId]!.assignAll(comments); - contactCommentsMap[contactId]?.refresh(); - } catch (e) { - logSafe( - "Error fetching ${active ? 'active' : 'inactive'} comments for contact $contactId: $e", - level: LogLevel.error); - - contactCommentsMap[contactId] ??= [].obs; - contactCommentsMap[contactId]!.clear(); - } - } - - /// 🗑️ Delete a comment (soft delete) Future deleteComment(String commentId, String contactId) async { try { - logSafe("Deleting comment. id: $commentId"); - final success = await ApiService.restoreContactComment(commentId, false); if (success) { - logSafe("Comment deleted successfully. id: $commentId"); + if (editingCommentId.value == commentId) editingCommentId.value = null; - // Refresh comments after deletion - await fetchCommentsForContact(contactId); + await fetchCommentsForContact(contactId, active: true); + await fetchCommentsForContact(contactId, active: false); showAppSnackbar( title: "Deleted", @@ -143,7 +130,6 @@ class DirectoryController extends GetxController { type: SnackbarType.success, ); } else { - logSafe("Failed to delete comment via API. id: $commentId"); showAppSnackbar( title: "Error", message: "Failed to delete comment.", @@ -151,8 +137,9 @@ class DirectoryController extends GetxController { ); } } catch (e, stack) { - logSafe("Delete comment failed: ${e.toString()}", level: LogLevel.error); - logSafe("StackTrace: $stack", level: LogLevel.debug); + logSafe("Delete comment failed: $e", level: LogLevel.error); + logSafe(stack.toString(), level: LogLevel.debug); + showAppSnackbar( title: "Error", message: "Something went wrong while deleting comment.", @@ -161,18 +148,13 @@ class DirectoryController extends GetxController { } } - /// ♻️ Restore a previously deleted comment Future restoreComment(String commentId, String contactId) async { try { - logSafe("Restoring comment. id: $commentId"); - final success = await ApiService.restoreContactComment(commentId, true); if (success) { - logSafe("Comment restored successfully. id: $commentId"); - - // Refresh comments after restore - await fetchCommentsForContact(contactId); + await fetchCommentsForContact(contactId, active: true); + await fetchCommentsForContact(contactId, active: false); showAppSnackbar( title: "Restored", @@ -180,7 +162,6 @@ class DirectoryController extends GetxController { type: SnackbarType.success, ); } else { - logSafe("Failed to restore comment via API. id: $commentId"); showAppSnackbar( title: "Error", message: "Failed to restore comment.", @@ -188,8 +169,9 @@ class DirectoryController extends GetxController { ); } } catch (e, stack) { - logSafe("Restore comment failed: ${e.toString()}", level: LogLevel.error); - logSafe("StackTrace: $stack", level: LogLevel.debug); + logSafe("Restore comment failed: $e", level: LogLevel.error); + logSafe(stack.toString(), level: LogLevel.debug); + showAppSnackbar( title: "Error", message: "Something went wrong while restoring comment.", @@ -198,6 +180,8 @@ class DirectoryController extends GetxController { } } + // -------------------- CONTACTS -------------------- + Future fetchBuckets() async { try { final response = await ApiService.getContactBucketList(); @@ -219,7 +203,6 @@ class DirectoryController extends GetxController { isLoading.value = true; final response = await ApiService.getDirectoryData(isActive: active); - if (response != null) { final contacts = response.map((e) => ContactModel.fromJson(e)).toList(); allContacts.assignAll(contacts); @@ -238,14 +221,12 @@ class DirectoryController extends GetxController { void extractCategoriesFromContacts() { final uniqueCategories = {}; - for (final contact in allContacts) { final category = contact.contactCategory; if (category != null && !uniqueCategories.containsKey(category.id)) { uniqueCategories[category.id] = category; } } - contactCategories.value = uniqueCategories.values.toList(); } @@ -271,11 +252,8 @@ class DirectoryController extends GetxController { final categoryNameMatch = contact.contactCategory?.name.toLowerCase().contains(query) ?? false; final bucketNameMatch = contact.bucketIds.any((id) { - final bucketName = contactBuckets - .firstWhereOrNull((b) => b.id == id) - ?.name - .toLowerCase() ?? - ''; + final bucketName = + contactBuckets.firstWhereOrNull((b) => b.id == id)?.name.toLowerCase() ?? ''; return bucketName.contains(query); }); @@ -291,7 +269,6 @@ class DirectoryController extends GetxController { return categoryMatch && bucketMatch && searchMatch; }).toList(); - // 🔑 Ensure results are always alphabetically sorted filteredContacts .sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); } diff --git a/lib/controller/task_planning/daily_task_controller.dart b/lib/controller/task_planning/daily_task_controller.dart index 93486b4..3d54dfc 100644 --- a/lib/controller/task_planning/daily_task_controller.dart +++ b/lib/controller/task_planning/daily_task_controller.dart @@ -4,6 +4,7 @@ import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/model/project_model.dart'; import 'package:marco/model/dailyTaskPlanning/daily_task_model.dart'; +import 'package:marco/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart'; class DailyTaskController extends GetxController { List projects = []; @@ -23,6 +24,12 @@ class DailyTaskController extends GetxController { } } + RxSet selectedBuildings = {}.obs; + RxSet selectedFloors = {}.obs; + RxSet selectedActivities = {}.obs; + RxSet selectedServices = {}.obs; + + RxBool isFilterLoading = false.obs; RxBool isLoading = true.obs; RxBool isLoadingMore = false.obs; Map> groupedDailyTasks = {}; @@ -51,9 +58,18 @@ class DailyTaskController extends GetxController { ); } + void clearTaskFilters() { + selectedBuildings.clear(); + selectedFloors.clear(); + selectedActivities.clear(); + selectedServices.clear(); + startDateTask = null; + endDateTask = null; + update(); + } + Future fetchTaskData( String projectId, { - List? serviceIds, int pageNumber = 1, int pageSize = 20, bool isLoadMore = false, @@ -68,11 +84,19 @@ class DailyTaskController extends GetxController { isLoadingMore.value = true; } + // Create the filter object + final filter = { + "buildingIds": selectedBuildings.toList(), + "floorIds": selectedFloors.toList(), + "activityIds": selectedActivities.toList(), + "serviceIds": selectedServices.toList(), + "dateFrom": startDateTask?.toIso8601String(), + "dateTo": endDateTask?.toIso8601String(), + }; + final response = await ApiService.getDailyTasks( projectId, - dateFrom: startDateTask, - dateTo: endDateTask, - serviceIds: serviceIds, + filter: filter, pageNumber: pageNumber, pageSize: pageSize, ); @@ -95,6 +119,35 @@ class DailyTaskController extends GetxController { update(); } + FilterData? taskFilterData; + + Future fetchTaskFilter(String projectId) async { + isFilterLoading.value = true; + try { + final filterResponse = await ApiService.getDailyTaskFilter(projectId); + + if (filterResponse != null && filterResponse.success) { + taskFilterData = + filterResponse.data; // now taskFilterData is FilterData? + logSafe( + "Task filter fetched successfully. Buildings: ${taskFilterData?.buildings.length}, Floors: ${taskFilterData?.floors.length}", + level: LogLevel.info, + ); + } else { + logSafe( + "Failed to fetch task filter for projectId: $projectId", + level: LogLevel.warning, + ); + } + } catch (e, stack) { + logSafe("Exception in fetchTaskFilter: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } finally { + isFilterLoading.value = false; + update(); + } + } + Future selectDateRangeForTaskData( BuildContext context, DailyTaskController controller, diff --git a/lib/controller/task_planning/daily_task_planning_controller.dart b/lib/controller/task_planning/daily_task_planning_controller.dart index f94af8b..0eda9d7 100644 --- a/lib/controller/task_planning/daily_task_planning_controller.dart +++ b/lib/controller/task_planning/daily_task_planning_controller.dart @@ -19,7 +19,9 @@ class DailyTaskPlanningController extends GetxController { List> roles = []; RxBool isAssigningTask = false.obs; RxnString selectedRoleId = RxnString(); - RxBool isLoading = false.obs; + RxBool isFetchingTasks = true.obs; + RxBool isFetchingProjects = true.obs; + RxBool isFetchingEmployees = true.obs; @override void onInit() { @@ -109,7 +111,7 @@ class DailyTaskPlanningController extends GetxController { } Future fetchProjects() async { - isLoading.value = true; + isFetchingProjects.value = true; try { final response = await ApiService.getProjects(); if (response?.isEmpty ?? true) { @@ -126,7 +128,7 @@ class DailyTaskPlanningController extends GetxController { logSafe("Error fetching projects", level: LogLevel.error, error: e, stackTrace: stack); } finally { - isLoading.value = false; + isFetchingProjects.value = false; } } @@ -137,7 +139,7 @@ class DailyTaskPlanningController extends GetxController { return; } - isLoading.value = true; + isFetchingTasks.value = true; try { // Fetch infra details final infraResponse = await ApiService.getInfraDetails(projectId); @@ -232,7 +234,7 @@ class DailyTaskPlanningController extends GetxController { logSafe("Error fetching daily task data", level: LogLevel.error, error: e, stackTrace: stack); } finally { - isLoading.value = false; + isFetchingTasks.value = false; update(); } } @@ -244,7 +246,7 @@ class DailyTaskPlanningController extends GetxController { return; } - isLoading.value = true; + isFetchingEmployees.value = true; try { final response = await ApiService.getAllEmployeesByProject(projectId); if (response != null && response.isNotEmpty) { @@ -272,7 +274,7 @@ class DailyTaskPlanningController extends GetxController { stackTrace: stack, ); } finally { - isLoading.value = false; + isFetchingEmployees.value = false; update(); } } diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 960c3a7..437b8b5 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -41,6 +41,7 @@ class ApiEndpoints { static const String approveReportAction = "/task/approve"; static const String assignTask = "/project/task"; static const String getmasterWorkCategories = "/Master/work-categories"; + static const String getDailyTaskProjectProgressFilter = "/task/filter"; ////// Directory Module API Endpoints /////// static const String getDirectoryContacts = "/directory"; diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 583e3c1..ec0578e 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -21,6 +21,7 @@ import 'package:marco/model/document/document_version_model.dart'; import 'package:marco/model/attendance/organization_per_project_list_model.dart'; import 'package:marco/model/tenant/tenant_services_model.dart'; import 'package:marco/model/dailyTaskPlanning/daily_task_model.dart'; +import 'package:marco/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart'; class ApiService { static const bool enableLogs = true; @@ -2125,42 +2126,67 @@ class ApiService { } // === Daily Task APIs === + /// Get Daily Task Project Report Filter + static Future getDailyTaskFilter( + String projectId) async { + final endpoint = + "${ApiEndpoints.getDailyTaskProjectProgressFilter}/$projectId"; + logSafe("Fetching daily task Progress filter for projectId: $projectId"); - static Future?> getDailyTasks( - String projectId, { - DateTime? dateFrom, - DateTime? dateTo, - List? serviceIds, - int pageNumber = 1, - int pageSize = 20, -}) async { - final filterBody = { - "serviceIds": serviceIds ?? [], - }; + try { + final response = await _getRequest(endpoint); - final query = { - "projectId": projectId, - "pageNumber": pageNumber.toString(), - "pageSize": pageSize.toString(), - if (dateFrom != null) - "dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom), - if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo), - "filter": jsonEncode(filterBody), - }; + if (response == null) { + logSafe("Daily task filter request failed: null response", + level: LogLevel.error); + return null; + } - final uri = - Uri.parse(ApiEndpoints.getDailyTask).replace(queryParameters: query); + final jsonResponse = _parseResponseForAllData(response, + label: "Daily Task Progress Filter"); - final response = await _getRequest(uri.toString()); - final parsed = response != null ? _parseResponse(response, label: 'Daily Tasks') : null; + if (jsonResponse != null) { + return DailyProgressReportFilterResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getDailyTask Progress Filter: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } - if (parsed != null && parsed['data'] != null) { - return (parsed['data'] as List).map((e) => TaskModel.fromJson(e)).toList(); + return null; } - return null; -} + static Future?> getDailyTasks( + String projectId, { + Map? filter, // <-- New: combined filter + int pageNumber = 1, + int pageSize = 20, + }) async { + // Build query parameters + final query = { + "projectId": projectId, + "pageNumber": pageNumber.toString(), + "pageSize": pageSize.toString(), + if (filter != null) "filter": jsonEncode(filter), + }; + final uri = + Uri.parse(ApiEndpoints.getDailyTask).replace(queryParameters: query); + + final response = await _getRequest(uri.toString()); + final parsed = response != null + ? _parseResponse(response, label: 'Daily Tasks') + : null; + + if (parsed != null && parsed['data'] != null) { + return (parsed['data'] as List) + .map((e) => TaskModel.fromJson(e)) + .toList(); + } + + return null; + } static Future reportTask({ required String id, diff --git a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart index eca72d2..2bf9d88 100644 --- a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart +++ b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart @@ -152,7 +152,7 @@ class _AssignTaskBottomSheetState extends State { Widget _buildEmployeeList() { return Obx(() { - if (controller.isLoading.value) { + if (controller.isFetchingTasks.value) { // Skeleton loader instead of CircularProgressIndicator return ListView.separated( shrinkWrap: true, diff --git a/lib/model/dailyTaskPlanning/daily_progress_report_filter.dart b/lib/model/dailyTaskPlanning/daily_progress_report_filter.dart index 7a6744b..56ca74c 100644 --- a/lib/model/dailyTaskPlanning/daily_progress_report_filter.dart +++ b/lib/model/dailyTaskPlanning/daily_progress_report_filter.dart @@ -1,82 +1,289 @@ import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; +import 'package:get/get.dart'; import 'package:marco/controller/task_planning/daily_task_controller.dart'; -import 'package:marco/controller/permission_controller.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; -class DailyProgressReportFilter extends StatelessWidget { +class DailyTaskFilterBottomSheet extends StatelessWidget { final DailyTaskController controller; - final PermissionController permissionController; - const DailyProgressReportFilter({ - super.key, - required this.controller, - required this.permissionController, - }); - - String getLabelText() { - final startDate = controller.startDateTask; - final endDate = controller.endDateTask; - if (startDate != null && endDate != null) { - final start = DateFormat('dd MM yyyy').format(startDate); - final end = DateFormat('dd MM yyyy').format(endDate); - return "$start - $end"; - } - return "Select Date Range"; - } + const DailyTaskFilterBottomSheet({super.key, required this.controller}); @override Widget build(BuildContext context) { + final filterData = controller.taskFilterData; + if (filterData == null) return const SizedBox.shrink(); + + final hasFilters = [ + filterData.buildings, + filterData.floors, + filterData.activities, + filterData.services, + ].any((list) => list.isNotEmpty); + return BaseBottomSheet( title: "Filter Tasks", - onCancel: () => Navigator.pop(context), - + submitText: "Apply", + showButtons: hasFilters, + onCancel: () => Get.back(), onSubmit: () { - Navigator.pop(context, { - 'startDate': controller.startDateTask, - 'endDate': controller.endDateTask, - }); + if (controller.selectedProjectId != null) { + controller.fetchTaskData( + controller.selectedProjectId!, + ); + } + + Get.back(); }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.titleSmall("Select Date Range", fontWeight: 600), - const SizedBox(height: 8), - InkWell( - borderRadius: BorderRadius.circular(10), - onTap: () => controller.selectDateRangeForTaskData( - context, - controller, - ), - child: Ink( - decoration: BoxDecoration( - color: Colors.grey.shade100, - border: Border.all(color: Colors.grey.shade400), - borderRadius: BorderRadius.circular(10), - ), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - child: Row( + child: SingleChildScrollView( + child: hasFilters + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(Icons.date_range, color: Colors.blue.shade600), - const SizedBox(width: 12), - Expanded( - child: Text( - getLabelText(), - style: const TextStyle( - fontSize: 16, - color: Colors.black87, - fontWeight: FontWeight.w500, + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () { + controller.clearTaskFilters(); + }, + child: MyText( + "Reset Filter", + style: const TextStyle( + color: Colors.red, + fontWeight: FontWeight.w600, + ), ), - overflow: TextOverflow.ellipsis, ), ), - const Icon(Icons.arrow_drop_down, color: Colors.grey), + MySpacing.height(8), + _multiSelectField( + label: "Buildings", + items: filterData.buildings, + fallback: "Select Buildings", + selectedValues: controller.selectedBuildings, + ), + _multiSelectField( + label: "Floors", + items: filterData.floors, + fallback: "Select Floors", + selectedValues: controller.selectedFloors, + ), + _multiSelectField( + label: "Activities", + items: filterData.activities, + fallback: "Select Activities", + selectedValues: controller.selectedActivities, + ), + _multiSelectField( + label: "Services", + items: filterData.services, + fallback: "Select Services", + selectedValues: controller.selectedServices, + ), + MySpacing.height(8), + _dateRangeSelector(context), ], + ) + : Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: MyText( + "No filters available", + style: const TextStyle(color: Colors.grey), + ), + ), + ), + ), + ); + } + + Widget _multiSelectField({ + required String label, + required List items, + required String fallback, + required RxSet selectedValues, + }) { + if (items.isEmpty) return const SizedBox.shrink(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.labelMedium(label), + MySpacing.height(8), + Obx(() { + final selectedNames = items + .where((item) => selectedValues.contains(item.id)) + .map((item) => item.name) + .join(", "); + final displayText = + selectedNames.isNotEmpty ? selectedNames : fallback; + + return Builder( + builder: (context) { + return GestureDetector( + onTap: () async { + final RenderBox button = + context.findRenderObject() as RenderBox; + final RenderBox overlay = Overlay.of(context) + .context + .findRenderObject() as RenderBox; + + final position = button.localToGlobal(Offset.zero); + + await showMenu( + context: context, + position: RelativeRect.fromLTRB( + position.dx, + position.dy + button.size.height, + overlay.size.width - position.dx - button.size.width, + 0, + ), + items: items.map((item) { + return PopupMenuItem( + enabled: false, + child: StatefulBuilder( + builder: (context, setState) { + final isChecked = selectedValues.contains(item.id); + return CheckboxListTile( + dense: true, + value: isChecked, + contentPadding: EdgeInsets.zero, + controlAffinity: ListTileControlAffinity.leading, + title: MyText(item.name), + + // --- Styles to match Document Filter --- + checkColor: Colors.white, + side: const BorderSide( + color: Colors.black, width: 1.5), + fillColor: + MaterialStateProperty.resolveWith( + (states) { + if (states.contains(MaterialState.selected)) { + return Colors.indigo; + } + return Colors.white; + }, + ), + + onChanged: (val) { + if (val == true) { + selectedValues.add(item.id); + } else { + selectedValues.remove(item.id); + } + setState(() {}); + }, + ); + }, + ), + ); + }).toList(), + ); + }, + child: Container( + padding: MySpacing.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: MyText( + displayText, + style: const TextStyle(color: Colors.black87), + overflow: TextOverflow.ellipsis, + ), + ), + const Icon(Icons.arrow_drop_down, color: Colors.grey), + ], + ), + ), + ); + }, + ); + }), + MySpacing.height(16), + ], + ); + } + + Widget _dateRangeSelector(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.labelMedium("Select Date Range"), + MySpacing.height(8), + Row( + children: [ + Expanded( + child: _dateButton( + label: controller.startDateTask != null + ? "${controller.startDateTask!.day}/${controller.startDateTask!.month}/${controller.startDateTask!.year}" + : "From Date", + onTap: () async { + final picked = await showDatePicker( + context: context, + initialDate: controller.startDateTask ?? DateTime.now(), + firstDate: DateTime(2022), + lastDate: DateTime.now(), + ); + if (picked != null) { + controller.startDateTask = picked; + controller.update(); // rebuild widget + } + }, ), ), - ), - ], + MySpacing.width(12), + Expanded( + child: _dateButton( + label: controller.endDateTask != null + ? "${controller.endDateTask!.day}/${controller.endDateTask!.month}/${controller.endDateTask!.year}" + : "To Date", + onTap: () async { + final picked = await showDatePicker( + context: context, + initialDate: controller.endDateTask ?? DateTime.now(), + firstDate: DateTime(2022), + lastDate: DateTime.now(), + ); + if (picked != null) { + controller.endDateTask = picked; + controller.update(); + } + }, + ), + ), + ], + ), + MySpacing.height(16), + ], + ); + } + + Widget _dateButton({required String label, required VoidCallback onTap}) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Icon(Icons.calendar_today, size: 16, color: Colors.grey), + const SizedBox(width: 8), + Expanded( + child: MyText(label), + ), + ], + ), ), ); } diff --git a/lib/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart b/lib/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart new file mode 100644 index 0000000..e87c975 --- /dev/null +++ b/lib/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart @@ -0,0 +1,128 @@ +class DailyProgressReportFilterResponse { + final bool success; + final String message; + final FilterData? data; + final dynamic errors; + final int statusCode; + final String timestamp; + + DailyProgressReportFilterResponse({ + required this.success, + required this.message, + this.data, + this.errors, + required this.statusCode, + required this.timestamp, + }); + + factory DailyProgressReportFilterResponse.fromJson(Map json) { + return DailyProgressReportFilterResponse( + success: json['success'], + message: json['message'], + data: json['data'] != null ? FilterData.fromJson(json['data']) : null, + errors: json['errors'], + statusCode: json['statusCode'], + timestamp: json['timestamp'], + ); + } + + Map toJson() => { + 'success': success, + 'message': message, + 'data': data?.toJson(), + 'errors': errors, + 'statusCode': statusCode, + 'timestamp': timestamp, + }; +} + +class FilterData { + final List buildings; + final List floors; + final List activities; + final List services; + + FilterData({ + required this.buildings, + required this.floors, + required this.activities, + required this.services, + }); + + factory FilterData.fromJson(Map json) { + return FilterData( + buildings: (json['buildings'] as List) + .map((e) => Building.fromJson(e)) + .toList(), + floors: + (json['floors'] as List).map((e) => Floor.fromJson(e)).toList(), + activities: + (json['activities'] as List).map((e) => Activity.fromJson(e)).toList(), + services: + (json['services'] as List).map((e) => Service.fromJson(e)).toList(), + ); + } + + Map toJson() => { + 'buildings': buildings.map((e) => e.toJson()).toList(), + 'floors': floors.map((e) => e.toJson()).toList(), + 'activities': activities.map((e) => e.toJson()).toList(), + 'services': services.map((e) => e.toJson()).toList(), + }; +} + +class Building { + final String id; + final String name; + + Building({required this.id, required this.name}); + + factory Building.fromJson(Map json) => + Building(id: json['id'], name: json['name']); + + Map toJson() => {'id': id, 'name': name}; +} + +class Floor { + final String id; + final String name; + final String buildingId; + + Floor({required this.id, required this.name, required this.buildingId}); + + factory Floor.fromJson(Map json) => Floor( + id: json['id'], + name: json['name'], + buildingId: json['buildingId'], + ); + + Map toJson() => { + 'id': id, + 'name': name, + 'buildingId': buildingId, + }; +} + +class Activity { + final String id; + final String name; + + Activity({required this.id, required this.name}); + + factory Activity.fromJson(Map json) => + Activity(id: json['id'], name: json['name']); + + Map toJson() => {'id': id, 'name': name}; +} + +class Service { + final String id; + final String name; + + Service({required this.id, required this.name}); + + factory Service.fromJson(Map json) => + Service(id: json['id'], name: json['name']); + + Map toJson() => {'id': id, 'name': name}; +} diff --git a/lib/model/employees/add_employee_bottom_sheet.dart b/lib/model/employees/add_employee_bottom_sheet.dart index 656e45d..434224d 100644 --- a/lib/model/employees/add_employee_bottom_sheet.dart +++ b/lib/model/employees/add_employee_bottom_sheet.dart @@ -479,7 +479,7 @@ class _AddEmployeeBottomSheetState extends State context: context, initialDate: _controller.joiningDate ?? DateTime.now(), firstDate: DateTime(2000), - lastDate: DateTime(2100), + lastDate: DateTime.now(), ); if (picked != null) { diff --git a/lib/routes.dart b/lib/routes.dart index 4984fe1..37a166e 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -12,7 +12,7 @@ import 'package:marco/view/error_pages/error_500_screen.dart'; import 'package:marco/view/dashboard/dashboard_screen.dart'; import 'package:marco/view/Attendence/attendance_screen.dart'; import 'package:marco/view/taskPlanning/daily_task_planning.dart'; -import 'package:marco/view/taskPlanning/daily_progress.dart'; +import 'package:marco/view/taskPlanning/daily_progress_report.dart'; import 'package:marco/view/employees/employees_screen.dart'; import 'package:marco/view/auth/login_option_screen.dart'; import 'package:marco/view/auth/mpin_screen.dart'; diff --git a/lib/view/directory/contact_detail_screen.dart b/lib/view/directory/contact_detail_screen.dart index b5394d8..28eaddd 100644 --- a/lib/view/directory/contact_detail_screen.dart +++ b/lib/view/directory/contact_detail_screen.dart @@ -17,6 +17,7 @@ import 'package:marco/helpers/utils/date_time_utils.dart'; import 'package:marco/model/directory/add_contact_bottom_sheet.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; import 'package:marco/helpers/widgets/my_confirmation_dialog.dart'; +import 'package:marco/model/directory/directory_comment_model.dart'; // HELPER: Delta to HTML conversion String _convertDeltaToHtml(dynamic delta) { @@ -344,20 +345,7 @@ class _ContactDetailScreenState extends State { Widget _buildCommentsTab() { return Obx(() { final contactId = contactRx.value.id; - - // Get active and inactive comments - final activeComments = directoryController - .getCommentsForContact(contactId) - .where((c) => c.isActive) - .toList(); - final inactiveComments = directoryController - .getCommentsForContact(contactId) - .where((c) => !c.isActive) - .toList(); - - // Combine both and keep the same sorting (recent first) - final comments = - [...activeComments, ...inactiveComments].reversed.toList(); + final comments = directoryController.combinedComments(contactId); final editingId = directoryController.editingCommentId.value; return Stack( @@ -413,7 +401,8 @@ class _ContactDetailScreenState extends State { }); } - Widget _buildCommentItem(comment, editingId, contactId) { + Widget _buildCommentItem( + DirectoryComment comment, String? editingId, String contactId) { final isEditing = editingId == comment.id; final initials = comment.createdBy.firstName.isNotEmpty ? comment.createdBy.firstName[0].toUpperCase() @@ -427,180 +416,190 @@ class _ContactDetailScreenState extends State { ) : null; + final isInactive = !comment.isActive; + return Container( - margin: const EdgeInsets.symmetric(vertical: 6), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: Colors.grey.shade200), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.03), - blurRadius: 6, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 🧑 Header - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Avatar( - firstName: initials, - lastName: '', - size: 40, - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Full name on top - Text( - "${comment.createdBy.firstName} ${comment.createdBy.lastName}", - style: const TextStyle( - fontWeight: FontWeight.w700, - fontSize: 15, - color: Colors.black87, - ), - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 2), - // Job Role - if (comment.createdBy.jobRoleName?.isNotEmpty ?? false) + margin: const EdgeInsets.symmetric(vertical: 6), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: Colors.grey.shade200), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 🧑 Header + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Avatar( + firstName: initials, + lastName: '', + size: 40, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text( - comment.createdBy.jobRoleName, + "${comment.createdBy.firstName} ${comment.createdBy.lastName}", style: TextStyle( - fontSize: 13, - color: Colors.indigo[600], - fontWeight: FontWeight.w500, + fontWeight: FontWeight.w700, + fontSize: 15, + color: isInactive ? Colors.grey : Colors.black87, + fontStyle: + isInactive ? FontStyle.italic : FontStyle.normal, + ), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + if (comment.createdBy.jobRoleName.isNotEmpty) + Text( + comment.createdBy.jobRoleName, + style: TextStyle( + fontSize: 13, + color: + isInactive ? Colors.grey : Colors.indigo[600], + fontWeight: FontWeight.w500, + fontStyle: isInactive + ? FontStyle.italic + : FontStyle.normal, + ), + ), + const SizedBox(height: 2), + Text( + DateTimeUtils.convertUtcToLocal( + comment.createdAt.toString(), + format: 'dd MMM yyyy, hh:mm a', + ), + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + fontStyle: + isInactive ? FontStyle.italic : FontStyle.normal, ), ), - const SizedBox(height: 2), - // Timestamp - Text( - DateTimeUtils.convertUtcToLocal( - comment.createdAt.toString(), - format: 'dd MMM yyyy, hh:mm a', + ], + ), + ), + + // ⚡ Action buttons + if (!isInactive) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.edit_outlined, + size: 18, color: Colors.indigo), + tooltip: "Edit", + splashRadius: 18, + onPressed: () { + directoryController.editingCommentId.value = + isEditing ? null : comment.id; + }, ), - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], + IconButton( + icon: const Icon(Icons.delete_outline, + size: 18, color: Colors.red), + tooltip: "Delete", + splashRadius: 18, + onPressed: () async { + await Get.dialog( + ConfirmDialog( + title: "Delete Note", + message: + "Are you sure you want to delete this note?", + confirmText: "Delete", + confirmColor: Colors.red, + icon: Icons.delete_forever, + onConfirm: () async { + await directoryController.deleteComment( + comment.id, contactId); + }, + ), + ); + }, ), - ), - ], - ), - ), - - // ⚡ Action buttons - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (!comment.isActive) - IconButton( - icon: const Icon(Icons.restore, - size: 18, color: Colors.green), - tooltip: "Restore", - splashRadius: 18, - onPressed: () async { - await Get.dialog( - ConfirmDialog( - title: "Restore Note", - message: - "Are you sure you want to restore this note?", - confirmText: "Restore", - confirmColor: Colors.green, - icon: Icons.restore, - onConfirm: () async { - await directoryController.restoreComment( - comment.id, contactId); - }, - ), - ); - }, - ), - if (comment.isActive) ...[ - IconButton( - icon: const Icon(Icons.edit_outlined, - size: 18, color: Colors.indigo), - tooltip: "Edit", - splashRadius: 18, - onPressed: () { - directoryController.editingCommentId.value = - isEditing ? null : comment.id; - }, - ), - IconButton( - icon: const Icon(Icons.delete_outline, - size: 18, color: Colors.red), - tooltip: "Delete", - splashRadius: 18, - onPressed: () async { - await Get.dialog( - ConfirmDialog( - title: "Delete Note", - message: - "Are you sure you want to delete this note?", - confirmText: "Delete", - confirmColor: Colors.red, - icon: Icons.delete_forever, - onConfirm: () async { - await directoryController.deleteComment( - comment.id, contactId); - }, - ), - ); - }, - ), - ], - ], - ), - ], - ), - - const SizedBox(height: 8), - - // 📝 Comment Content - if (isEditing && quillController != null) - CommentEditorCard( - controller: quillController, - onCancel: () => directoryController.editingCommentId.value = null, - onSave: (ctrl) async { - final delta = ctrl.document.toDelta(); - final htmlOutput = _convertDeltaToHtml(delta); - final updated = comment.copyWith(note: htmlOutput); - await directoryController.updateComment(updated); - await directoryController.fetchCommentsForContact(contactId); - directoryController.editingCommentId.value = null; - }, - ) - else - html.Html( - data: comment.note, - style: { - "body": html.Style( - margin: html.Margins.zero, - padding: html.HtmlPaddings.zero, - fontSize: html.FontSize(14), - color: Colors.black87, - ), - "p": html.Style( - margin: html.Margins.only(bottom: 6), - lineHeight: const html.LineHeight(1.4), - ), - "strong": html.Style( - fontWeight: FontWeight.w700, - color: Colors.black87, - ), - }, + ], + ) + else + IconButton( + icon: const Icon(Icons.restore, + size: 18, color: Colors.green), + tooltip: "Restore", + splashRadius: 18, + onPressed: () async { + await Get.dialog( + ConfirmDialog( + title: "Restore Note", + message: + "Are you sure you want to restore this note?", + confirmText: "Restore", + confirmColor: Colors.green, + icon: Icons.restore, + onConfirm: () async { + await directoryController.restoreComment( + comment.id, contactId); + }, + ), + ); + }, + ), + ], ), - ], - ), - ); + + const SizedBox(height: 8), + + // 📝 Comment Content + if (isEditing && quillController != null) + CommentEditorCard( + controller: quillController, + onCancel: () => + directoryController.editingCommentId.value = null, + onSave: (ctrl) async { + final delta = ctrl.document.toDelta(); + final htmlOutput = _convertDeltaToHtml(delta); + final updated = comment.copyWith(note: htmlOutput); + await directoryController.updateComment(updated); + await directoryController.fetchCommentsForContact(contactId, + active: true); + await directoryController.fetchCommentsForContact(contactId, + active: false); + directoryController.editingCommentId.value = null; + }, + ) + else + html.Html( + data: comment.note, + style: { + "body": html.Style( + margin: html.Margins.zero, + padding: html.HtmlPaddings.zero, + fontSize: html.FontSize(14), + color: isInactive ? Colors.grey : Colors.black87, + fontStyle: isInactive ? FontStyle.italic : FontStyle.normal, + ), + "p": html.Style( + margin: html.Margins.only(bottom: 6), + lineHeight: const html.LineHeight(1.4), + ), + "strong": html.Style( + fontWeight: FontWeight.w700, + color: isInactive ? Colors.grey : Colors.black87, + ), + }, + ), + ], + )); } Widget _iconInfoRow( diff --git a/lib/view/taskPlanning/daily_progress.dart b/lib/view/taskPlanning/daily_progress.dart deleted file mode 100644 index 103ffde..0000000 --- a/lib/view/taskPlanning/daily_progress.dart +++ /dev/null @@ -1,572 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:intl/intl.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_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/controller/permission_controller.dart'; -import 'package:marco/controller/task_planning/daily_task_controller.dart'; -import 'package:marco/model/dailyTaskPlanning/daily_progress_report_filter.dart'; -import 'package:marco/helpers/widgets/avatar.dart'; -import 'package:marco/controller/project_controller.dart'; -import 'package:marco/model/dailyTaskPlanning/task_action_buttons.dart'; -import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; -import 'package:marco/helpers/utils/permission_constants.dart'; -import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; -import 'package:marco/controller/tenant/service_controller.dart'; -import 'package:marco/helpers/widgets/tenant/service_selector.dart'; - -class DailyProgressReportScreen extends StatefulWidget { - const DailyProgressReportScreen({super.key}); - - @override - State createState() => - _DailyProgressReportScreenState(); -} - -class TaskChartData { - final String label; - final num value; - final Color color; - - TaskChartData(this.label, this.value, this.color); -} - -class _DailyProgressReportScreenState extends State - with UIMixin { - final DailyTaskController dailyTaskController = - Get.put(DailyTaskController()); - final PermissionController permissionController = - Get.put(PermissionController()); - final ProjectController projectController = Get.find(); - final ServiceController serviceController = Get.put(ServiceController()); - final ScrollController _scrollController = ScrollController(); - - @override - void initState() { - super.initState(); - _scrollController.addListener(() { - if (_scrollController.position.pixels >= - _scrollController.position.maxScrollExtent - 100 && - dailyTaskController.hasMore && - !dailyTaskController.isLoadingMore.value) { - final projectId = dailyTaskController.selectedProjectId; - if (projectId != null && projectId.isNotEmpty) { - dailyTaskController.fetchTaskData( - projectId, - pageNumber: dailyTaskController.currentPage + 1, - pageSize: dailyTaskController.pageSize, - isLoadMore: true, - ); - } - } - }); - final initialProjectId = projectController.selectedProjectId.value; - if (initialProjectId.isNotEmpty) { - dailyTaskController.selectedProjectId = initialProjectId; - dailyTaskController.fetchTaskData(initialProjectId); - serviceController.fetchServices(initialProjectId); - } - - // Update when project changes - ever(projectController.selectedProjectId, (newProjectId) async { - if (newProjectId.isNotEmpty && - newProjectId != dailyTaskController.selectedProjectId) { - dailyTaskController.selectedProjectId = newProjectId; - await dailyTaskController.fetchTaskData(newProjectId); - await serviceController.fetchServices(newProjectId); - dailyTaskController.update(['daily_progress_report_controller']); - } - }); - } - - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(72), - child: AppBar( - backgroundColor: const Color(0xFFF5F5F5), - elevation: 0.5, - automaticallyImplyLeading: false, - titleSpacing: 0, - title: Padding( - padding: MySpacing.xy(16, 0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - icon: const Icon(Icons.arrow_back_ios_new, - color: Colors.black, size: 20), - onPressed: () => Get.offNamed('/dashboard'), - ), - MySpacing.width(8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - MyText.titleLarge( - 'Daily Progress Report', - fontWeight: 700, - color: Colors.black, - ), - MySpacing.height(2), - GetBuilder( - builder: (projectController) { - final projectName = - projectController.selectedProject?.name ?? - 'Select Project'; - return Row( - children: [ - const Icon(Icons.work_outline, - size: 14, color: Colors.grey), - MySpacing.width(4), - Expanded( - child: MyText.bodySmall( - projectName, - fontWeight: 600, - overflow: TextOverflow.ellipsis, - color: Colors.grey[700], - ), - ), - ], - ); - }, - ), - ], - ), - ), - ], - ), - ), - ), - ), - body: SafeArea( - child: MyRefreshIndicator( - onRefresh: _refreshData, - child: CustomScrollView( - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - SliverToBoxAdapter( - child: GetBuilder( - init: dailyTaskController, - tag: 'daily_progress_report_controller', - builder: (controller) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MySpacing.height(flexSpacing), - - // --- ADD SERVICE SELECTOR HERE --- - Padding( - padding: MySpacing.x(10), - child: ServiceSelector( - controller: serviceController, - height: 40, - onSelectionChanged: (service) async { - final projectId = - dailyTaskController.selectedProjectId; - if (projectId?.isNotEmpty ?? false) { - await dailyTaskController.fetchTaskData( - projectId!, - serviceIds: - service != null ? [service.id] : null, - pageNumber: 1, - pageSize: 20, - ); - } - }, - ), - ), - - _buildActionBar(), - Padding( - padding: MySpacing.x(8), - child: _buildDailyProgressReportTab(), - ), - ], - ); - }, - ), - ), - ], - ), - ), - ), - ); - } - - Widget _buildActionBar() { - return Padding( - padding: MySpacing.x(flexSpacing), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - _buildActionItem( - label: "Filter", - icon: Icons.tune, - tooltip: 'Filter Project', - onTap: _openFilterSheet, - ), - ], - ), - ); - } - - Widget _buildActionItem({ - required String label, - required IconData icon, - required String tooltip, - required VoidCallback onTap, - Color? color, - }) { - return Row( - children: [ - MyText.bodyMedium(label, fontWeight: 600), - Tooltip( - message: tooltip, - child: InkWell( - borderRadius: BorderRadius.circular(22), - onTap: onTap, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon(icon, color: color, size: 22), - ), - ), - ), - ), - ], - ); - } - - Future _openFilterSheet() async { - final result = await showModalBottomSheet>( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => DailyProgressReportFilter( - controller: dailyTaskController, - permissionController: permissionController, - ), - ); - - if (result != null) { - final selectedProjectId = result['projectId'] as String?; - if (selectedProjectId != null && - selectedProjectId != dailyTaskController.selectedProjectId) { - dailyTaskController.selectedProjectId = selectedProjectId; - await dailyTaskController.fetchTaskData(selectedProjectId); - dailyTaskController.update(['daily_progress_report_controller']); - } - } - } - - Future _refreshData() async { - final projectId = dailyTaskController.selectedProjectId; - if (projectId != null) { - try { - await dailyTaskController.fetchTaskData(projectId); - } catch (e) { - debugPrint('Error refreshing task data: $e'); - } - } - } - - void _showTeamMembersBottomSheet(List members) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - isDismissible: true, - enableDrag: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(12)), - ), - builder: (context) { - return GestureDetector( - onTap: () {}, - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: - const BorderRadius.vertical(top: Radius.circular(12)), - ), - padding: const EdgeInsets.fromLTRB(16, 24, 16, 24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.titleMedium( - 'Team Members', - fontWeight: 600, - ), - const SizedBox(height: 8), - const Divider(thickness: 1), - const SizedBox(height: 8), - ...members.map((member) { - final firstName = member.firstName ?? 'Unnamed'; - final lastName = member.lastName ?? 'User'; - return ListTile( - contentPadding: EdgeInsets.zero, - leading: Avatar( - firstName: firstName, - lastName: lastName, - size: 31, - ), - title: MyText.bodyMedium( - '$firstName $lastName', - fontWeight: 600, - ), - ); - }), - const SizedBox(height: 8), - ], - ), - ), - ); - }, - ); - } - - Widget _buildDailyProgressReportTab() { - return Obx(() { - final isLoading = dailyTaskController.isLoading.value; - final groupedTasks = dailyTaskController.groupedDailyTasks; - - // Initial loading skeleton - if (isLoading && dailyTaskController.currentPage == 1) { - return SkeletonLoaders.dailyProgressReportSkeletonLoader(); - } - - // No tasks - if (groupedTasks.isEmpty) { - return Center( - child: MyText.bodySmall( - "No Progress Report Found", - fontWeight: 600, - ), - ); - } - - final sortedDates = groupedTasks.keys.toList() - ..sort((a, b) => b.compareTo(a)); - - // If only one date, make it expanded by default - if (sortedDates.length == 1 && - !dailyTaskController.expandedDates.contains(sortedDates[0])) { - dailyTaskController.expandedDates.add(sortedDates[0]); - } - - return MyCard.bordered( - borderRadiusAll: 10, - border: Border.all(color: Colors.grey.withOpacity(0.2)), - shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom), - paddingAll: 8, - child: ListView.builder( - controller: _scrollController, - shrinkWrap: true, - physics: const AlwaysScrollableScrollPhysics(), - itemCount: sortedDates.length + 1, // +1 for loading indicator - itemBuilder: (context, dateIndex) { - // Bottom loading indicator - if (dateIndex == sortedDates.length) { - return Obx(() => dailyTaskController.isLoadingMore.value - ? const Padding( - padding: EdgeInsets.all(16.0), - child: Center(child: CircularProgressIndicator()), - ) - : const SizedBox.shrink()); - } - - final dateKey = sortedDates[dateIndex]; - final tasksForDate = groupedTasks[dateKey]!; - final date = DateTime.tryParse(dateKey); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: () => dailyTaskController.toggleDate(dateKey), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - MyText.bodyMedium( - date != null - ? DateFormat('dd MMM yyyy').format(date) - : dateKey, - fontWeight: 700, - ), - Obx(() => Icon( - dailyTaskController.expandedDates.contains(dateKey) - ? Icons.remove_circle - : Icons.add_circle, - color: Colors.blueAccent, - )), - ], - ), - ), - Obx(() { - if (!dailyTaskController.expandedDates.contains(dateKey)) { - return const SizedBox.shrink(); - } - - return Column( - children: tasksForDate.asMap().entries.map((entry) { - final task = entry.value; - - 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, - task.workItem?.workArea?.areaName - ].where((e) => e?.isNotEmpty ?? false).join(' > '); - - final planned = task.plannedTask; - final completed = task.completedTask; - final progress = (planned != 0) - ? (completed / planned).clamp(0.0, 1.0) - : 0.0; - final parentTaskID = task.id; - - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: MyContainer( - paddingAll: 12, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.bodyMedium(activityName, fontWeight: 600), - const SizedBox(height: 2), - MyText.bodySmall(location, color: Colors.grey), - const SizedBox(height: 8), - GestureDetector( - onTap: () => _showTeamMembersBottomSheet( - task.teamMembers), - child: Row( - children: [ - const Icon(Icons.group, - size: 18, color: Colors.blueAccent), - const SizedBox(width: 6), - MyText.bodyMedium('Team', - color: Colors.blueAccent, - fontWeight: 600), - ], - ), - ), - const SizedBox(height: 8), - MyText.bodySmall( - "Completed: $completed / $planned", - fontWeight: 600, - color: Colors.black87, - ), - const SizedBox(height: 6), - Stack( - children: [ - Container( - height: 5, - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(6), - ), - ), - FractionallySizedBox( - widthFactor: progress, - child: Container( - height: 5, - decoration: BoxDecoration( - color: progress >= 1.0 - ? Colors.green - : progress >= 0.5 - ? Colors.amber - : Colors.red, - borderRadius: BorderRadius.circular(6), - ), - ), - ), - ], - ), - const SizedBox(height: 4), - MyText.bodySmall( - "${(progress * 100).toStringAsFixed(1)}%", - fontWeight: 500, - color: progress >= 1.0 - ? Colors.green[700] - : progress >= 0.5 - ? Colors.amber[800] - : Colors.red[700], - ), - const SizedBox(height: 12), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if ((task.reportedDate == null || - task.reportedDate - .toString() - .isEmpty) && - permissionController.hasPermission( - Permissions.assignReportTask)) ...[ - TaskActionButtons.reportButton( - context: context, - task: task, - completed: completed.toInt(), - refreshCallback: _refreshData, - ), - const SizedBox(width: 4), - ] else if (task.approvedBy == null && - permissionController.hasPermission( - Permissions.approveTask)) ...[ - TaskActionButtons.reportActionButton( - context: context, - task: task, - parentTaskID: parentTaskID, - workAreaId: workAreaId.toString(), - activityId: activityId.toString(), - completed: completed.toInt(), - refreshCallback: _refreshData, - ), - const SizedBox(width: 5), - ], - TaskActionButtons.commentButton( - context: context, - task: task, - parentTaskID: parentTaskID, - workAreaId: workAreaId.toString(), - activityId: activityId.toString(), - refreshCallback: _refreshData, - ), - ], - ), - ), - ], - ), - ), - ); - }).toList(), - ); - }) - ], - ); - }, - ), - ); - }); - } -} diff --git a/lib/view/taskPlanning/daily_progress_report.dart b/lib/view/taskPlanning/daily_progress_report.dart new file mode 100644 index 0000000..8f85883 --- /dev/null +++ b/lib/view/taskPlanning/daily_progress_report.dart @@ -0,0 +1,562 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.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_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/controller/permission_controller.dart'; +import 'package:marco/controller/task_planning/daily_task_controller.dart'; +import 'package:marco/model/dailyTaskPlanning/daily_progress_report_filter.dart'; +import 'package:marco/helpers/widgets/avatar.dart'; +import 'package:marco/controller/project_controller.dart'; +import 'package:marco/model/dailyTaskPlanning/task_action_buttons.dart'; +import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; +import 'package:marco/helpers/utils/permission_constants.dart'; +import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; + +class DailyProgressReportScreen extends StatefulWidget { + const DailyProgressReportScreen({super.key}); + + @override + State createState() => + _DailyProgressReportScreenState(); +} + +class TaskChartData { + final String label; + final num value; + final Color color; + + TaskChartData(this.label, this.value, this.color); +} + +class _DailyProgressReportScreenState extends State + with UIMixin { + final DailyTaskController dailyTaskController = + Get.put(DailyTaskController()); + final PermissionController permissionController = + Get.put(PermissionController()); + final ProjectController projectController = Get.find(); + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _scrollController.addListener(() { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 100 && + dailyTaskController.hasMore && + !dailyTaskController.isLoadingMore.value) { + final projectId = dailyTaskController.selectedProjectId; + if (projectId != null && projectId.isNotEmpty) { + dailyTaskController.fetchTaskData( + projectId, + pageNumber: dailyTaskController.currentPage + 1, + pageSize: dailyTaskController.pageSize, + isLoadMore: true, + ); + } + } + }); + final initialProjectId = projectController.selectedProjectId.value; + if (initialProjectId.isNotEmpty) { + dailyTaskController.selectedProjectId = initialProjectId; + dailyTaskController.fetchTaskData(initialProjectId); + } + + // Update when project changes + ever(projectController.selectedProjectId, (newProjectId) async { + if (newProjectId.isNotEmpty && + newProjectId != dailyTaskController.selectedProjectId) { + dailyTaskController.selectedProjectId = newProjectId; + await dailyTaskController.fetchTaskData(newProjectId); + dailyTaskController.update(['daily_progress_report_controller']); + } + }); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(72), + child: AppBar( + backgroundColor: const Color(0xFFF5F5F5), + elevation: 0.5, + automaticallyImplyLeading: false, + titleSpacing: 0, + title: Padding( + padding: MySpacing.xy(16, 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios_new, + color: Colors.black, size: 20), + onPressed: () => Get.offNamed('/dashboard'), + ), + MySpacing.width(8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + MyText.titleLarge( + 'Daily Progress Report', + fontWeight: 700, + color: Colors.black, + ), + MySpacing.height(2), + GetBuilder( + builder: (projectController) { + final projectName = + projectController.selectedProject?.name ?? + 'Select Project'; + return Row( + children: [ + const Icon(Icons.work_outline, + size: 14, color: Colors.grey), + MySpacing.width(4), + Expanded( + child: MyText.bodySmall( + projectName, + fontWeight: 600, + overflow: TextOverflow.ellipsis, + color: Colors.grey[700], + ), + ), + ], + ); + }, + ), + ], + ), + ), + ], + ), + ), + ), + ), + body: SafeArea( + child: MyRefreshIndicator( + onRefresh: _refreshData, + child: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + SliverToBoxAdapter( + child: GetBuilder( + init: dailyTaskController, + tag: 'daily_progress_report_controller', + builder: (controller) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MySpacing.height(flexSpacing), + Padding( + padding: MySpacing.x(15), + child: Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + InkWell( + borderRadius: BorderRadius.circular(22), + onTap: _openFilterSheet, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + child: Row( + children: [ + MyText.bodySmall( + "Filter", + fontWeight: 600, + color: Colors.black, + ), + const SizedBox(width: 4), + Icon(Icons.tune, + size: 20, color: Colors.black), + + ], + ), + ), + ), + ], + ), + ), + MySpacing.height(8), + Padding( + padding: MySpacing.x(8), + child: _buildDailyProgressReportTab(), + ), + ], + ); + }, + ), + ), + ], + ), + ), + ), + ); + } + + Future _openFilterSheet() async { + // ✅ Fetch filter data first + if (dailyTaskController.taskFilterData == null) { + await dailyTaskController + .fetchTaskFilter(dailyTaskController.selectedProjectId ?? ''); + } + + final result = await showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => DailyTaskFilterBottomSheet( + controller: dailyTaskController, + ), + ); + + if (result != null) { + final selectedProjectId = result['projectId'] as String?; + if (selectedProjectId != null && + selectedProjectId != dailyTaskController.selectedProjectId) { + dailyTaskController.selectedProjectId = selectedProjectId; + await dailyTaskController.fetchTaskData(selectedProjectId); + dailyTaskController.update(['daily_progress_report_controller']); + } + } + } + + Future _refreshData() async { + final projectId = dailyTaskController.selectedProjectId; + if (projectId != null) { + try { + await dailyTaskController.fetchTaskData(projectId); + } catch (e) { + debugPrint('Error refreshing task data: $e'); + } + } + } + + void _showTeamMembersBottomSheet(List members) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + isDismissible: true, + enableDrag: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(12)), + ), + builder: (context) { + return GestureDetector( + onTap: () {}, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(12)), + ), + padding: const EdgeInsets.fromLTRB(16, 24, 16, 24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleMedium( + 'Team Members', + fontWeight: 600, + ), + const SizedBox(height: 8), + const Divider(thickness: 1), + const SizedBox(height: 8), + ...members.map((member) { + final firstName = member.firstName ?? 'Unnamed'; + final lastName = member.lastName ?? 'User'; + return ListTile( + contentPadding: EdgeInsets.zero, + leading: Avatar( + firstName: firstName, + lastName: lastName, + size: 31, + ), + title: MyText.bodyMedium( + '$firstName $lastName', + fontWeight: 600, + ), + ); + }), + const SizedBox(height: 8), + ], + ), + ), + ); + }, + ); + } + + Widget _buildDailyProgressReportTab() { + return Obx(() { + final isLoading = dailyTaskController.isLoading.value; + final groupedTasks = dailyTaskController.groupedDailyTasks; + + // 🟡 Show loading skeleton on first load + if (isLoading && dailyTaskController.currentPage == 1) { + return SkeletonLoaders.dailyProgressReportSkeletonLoader(); + } + + // ⚪ No data available + if (groupedTasks.isEmpty) { + return Center( + child: MyText.bodySmall( + "No Progress Report Found", + fontWeight: 600, + ), + ); + } + + // 🔽 Sort all date keys by descending (latest first) + final sortedDates = groupedTasks.keys.toList() + ..sort((a, b) => b.compareTo(a)); + + // 🔹 Auto expand if only one date present + if (sortedDates.length == 1 && + !dailyTaskController.expandedDates.contains(sortedDates[0])) { + dailyTaskController.expandedDates.add(sortedDates[0]); + } + + // 🧱 Return a scrollable column of cards + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...sortedDates.map((dateKey) { + final tasksForDate = groupedTasks[dateKey]!; + final date = DateTime.tryParse(dateKey); + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: MyCard.bordered( + borderRadiusAll: 10, + border: Border.all(color: Colors.grey.withOpacity(0.2)), + shadow: + MyShadow(elevation: 1, position: MyShadowPosition.bottom), + paddingAll: 12, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 🗓️ Date Header + GestureDetector( + onTap: () => dailyTaskController.toggleDate(dateKey), + child: Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MyText.bodyMedium( + date != null + ? DateFormat('dd MMM yyyy').format(date) + : dateKey, + fontWeight: 700, + ), + Obx(() => Icon( + dailyTaskController.expandedDates + .contains(dateKey) + ? Icons.remove_circle + : Icons.add_circle, + color: Colors.blueAccent, + )), + ], + ), + ), + ), + + // 🔽 Task List (expandable) + Obx(() { + if (!dailyTaskController.expandedDates + .contains(dateKey)) { + return const SizedBox.shrink(); + } + + return Column( + children: tasksForDate.map((task) { + 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, + task.workItem?.workArea?.areaName + ].where((e) => e?.isNotEmpty ?? false).join(' > '); + + final planned = task.plannedTask; + final completed = task.completedTask; + final progress = (planned != 0) + ? (completed / planned).clamp(0.0, 1.0) + : 0.0; + final parentTaskID = task.id; + + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: MyContainer( + paddingAll: 12, + borderRadiusAll: 8, + border: Border.all( + color: Colors.grey.withOpacity(0.2)), + color: Colors.white, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 🏗️ Activity name & location + MyText.bodyMedium(activityName, + fontWeight: 600), + const SizedBox(height: 2), + MyText.bodySmall(location, + color: Colors.grey), + const SizedBox(height: 8), + + // 👥 Team Members + GestureDetector( + onTap: () => _showTeamMembersBottomSheet( + task.teamMembers), + child: Row( + children: [ + const Icon(Icons.group, + size: 18, color: Colors.blueAccent), + const SizedBox(width: 6), + MyText.bodyMedium( + 'Team', + color: Colors.blueAccent, + fontWeight: 600, + ), + ], + ), + ), + const SizedBox(height: 8), + + // 📊 Progress info + MyText.bodySmall( + "Completed: $completed / $planned", + fontWeight: 600, + color: Colors.black87, + ), + const SizedBox(height: 6), + Stack( + children: [ + Container( + height: 5, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: + BorderRadius.circular(6), + ), + ), + FractionallySizedBox( + widthFactor: progress, + child: Container( + height: 5, + decoration: BoxDecoration( + color: progress >= 1.0 + ? Colors.green + : progress >= 0.5 + ? Colors.amber + : Colors.red, + borderRadius: + BorderRadius.circular(6), + ), + ), + ), + ], + ), + const SizedBox(height: 4), + MyText.bodySmall( + "${(progress * 100).toStringAsFixed(1)}%", + fontWeight: 500, + color: progress >= 1.0 + ? Colors.green[700] + : progress >= 0.5 + ? Colors.amber[800] + : Colors.red[700], + ), + const SizedBox(height: 12), + + // 🎯 Action Buttons + SingleChildScrollView( + scrollDirection: Axis.horizontal, + physics: const ClampingScrollPhysics(), + primary: false, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if ((task.reportedDate == null || + task.reportedDate + .toString() + .isEmpty) && + permissionController.hasPermission( + Permissions + .assignReportTask)) ...[ + TaskActionButtons.reportButton( + context: context, + task: task, + completed: completed.toInt(), + refreshCallback: _refreshData, + ), + const SizedBox(width: 4), + ] else if (task.approvedBy == null && + permissionController.hasPermission( + Permissions.approveTask)) ...[ + TaskActionButtons.reportActionButton( + context: context, + task: task, + parentTaskID: parentTaskID, + workAreaId: workAreaId.toString(), + activityId: activityId.toString(), + completed: completed.toInt(), + refreshCallback: _refreshData, + ), + const SizedBox(width: 5), + ], + TaskActionButtons.commentButton( + context: context, + task: task, + parentTaskID: parentTaskID, + workAreaId: workAreaId.toString(), + activityId: activityId.toString(), + refreshCallback: _refreshData, + ), + ], + ), + ), + ], + ), + ), + ); + }).toList(), + ); + }), + ], + ), + ), + ); + }), + + // 🔻 Loading More Indicator + Obx(() => dailyTaskController.isLoadingMore.value + ? const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Center(child: CircularProgressIndicator()), + ) + : const SizedBox.shrink()), + ], + ); + }); + } +} diff --git a/lib/view/taskPlanning/daily_task_planning.dart b/lib/view/taskPlanning/daily_task_planning.dart index ebf4b3d..8e0cfea 100644 --- a/lib/view/taskPlanning/daily_task_planning.dart +++ b/lib/view/taskPlanning/daily_task_planning.dart @@ -48,8 +48,7 @@ class _DailyTaskPlanningScreenState extends State (newProjectId) { if (newProjectId.isNotEmpty) { dailyTaskPlanningController.fetchTaskData(newProjectId); - serviceController - .fetchServices(newProjectId); + serviceController.fetchServices(newProjectId); } }, ); @@ -184,7 +183,7 @@ class _DailyTaskPlanningScreenState extends State Widget dailyProgressReportTab() { return Obx(() { - final isLoading = dailyTaskPlanningController.isLoading.value; + final isLoading = dailyTaskPlanningController.isFetchingTasks.value; final dailyTasks = dailyTaskPlanningController.dailyTasks; if (isLoading) {