diff --git a/lib/controller/task_planning/daily_task_controller.dart b/lib/controller/task_planning/daily_task_controller.dart index f1a3501..644bc9f 100644 --- a/lib/controller/task_planning/daily_task_controller.dart +++ b/lib/controller/task_planning/daily_task_controller.dart @@ -24,8 +24,12 @@ class DailyTaskController extends GetxController { } RxBool isLoading = true.obs; + RxBool isLoadingMore = false.obs; Map> groupedDailyTasks = {}; - + // Pagination + int currentPage = 1; + int pageSize = 20; + bool hasMore = true; @override void onInit() { super.onInit(); @@ -47,48 +51,49 @@ class DailyTaskController extends GetxController { ); } - Future fetchTaskData(String? projectId) async { - if (projectId == null) { - logSafe("fetchTaskData: Skipped, projectId is null", - level: LogLevel.warning); - return; + Future fetchTaskData( + String projectId, { + List? serviceIds, + int pageNumber = 1, + int pageSize = 20, + bool isLoadMore = false, + }) async { + if (!isLoadMore) { + isLoading.value = true; + currentPage = 1; + hasMore = true; + groupedDailyTasks.clear(); + dailyTasks.clear(); + } else { + isLoadingMore.value = true; } - isLoading.value = true; - final response = await ApiService.getDailyTasks( projectId, dateFrom: startDateTask, dateTo: endDateTask, + serviceIds: serviceIds, + pageNumber: pageNumber, + pageSize: pageSize, ); - isLoading.value = false; - - if (response != null) { - groupedDailyTasks.clear(); - + if (response != null && response.isNotEmpty) { for (var taskJson in response) { final task = TaskModel.fromJson(taskJson); final assignmentDateKey = task.assignmentDate.toIso8601String().split('T')[0]; - groupedDailyTasks.putIfAbsent(assignmentDateKey, () => []).add(task); } - dailyTasks = groupedDailyTasks.values.expand((list) => list).toList(); - - logSafe( - "Daily tasks fetched and grouped: ${dailyTasks.length} for project $projectId", - level: LogLevel.info, - ); - - update(); + currentPage = pageNumber; } else { - logSafe( - "Failed to fetch daily tasks for project $projectId", - level: LogLevel.error, - ); + hasMore = false; } + + isLoading.value = false; + isLoadingMore.value = false; + + update(); } Future selectDateRangeForTaskData( @@ -119,17 +124,23 @@ class DailyTaskController extends GetxController { level: LogLevel.info, ); - await controller.fetchTaskData(controller.selectedProjectId); + // ✅ Add null check before calling fetchTaskData + final projectId = controller.selectedProjectId; + if (projectId != null && projectId.isNotEmpty) { + await controller.fetchTaskData(projectId); + } else { + logSafe("Project ID is null or empty, skipping fetchTaskData", + level: LogLevel.warning); + } } -void refreshTasksFromNotification({ - required String projectId, - required String taskAllocationId, -}) async { - // re-fetch tasks - await fetchTaskData(projectId); - - update(); // rebuilds UI -} + void refreshTasksFromNotification({ + required String projectId, + required String taskAllocationId, + }) async { + // re-fetch tasks + await fetchTaskData(projectId); + update(); // rebuilds UI + } } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index b4080c9..70ce6b2 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -2022,21 +2022,35 @@ class ApiService { // === Daily Task APIs === - static Future?> getDailyTasks( - String projectId, { - DateTime? dateFrom, - DateTime? dateTo, - }) async { - final query = { - "projectId": projectId, - if (dateFrom != null) - "dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom), - if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo), - }; - return _getRequest(ApiEndpoints.getDailyTask, queryParams: query).then( - (res) => - res != null ? _parseResponse(res, label: 'Daily Tasks') : null); - } +static Future?> getDailyTasks( + String projectId, { + DateTime? dateFrom, + DateTime? dateTo, + List? serviceIds, + int pageNumber = 1, + int pageSize = 20, +}) async { + final filterBody = { + "serviceIds": serviceIds ?? [], + }; + + 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), + }; + + final uri = Uri.parse(ApiEndpoints.getDailyTask).replace(queryParameters: query); + + final response = await _getRequest(uri.toString()); + + return response != null ? _parseResponse(response, label: 'Daily Tasks') : null; +} + static Future reportTask({ required String id, diff --git a/lib/helpers/widgets/tenant/organization_selector.dart b/lib/helpers/widgets/tenant/organization_selector.dart index cd89f90..c35afa8 100644 --- a/lib/helpers/widgets/tenant/organization_selector.dart +++ b/lib/helpers/widgets/tenant/organization_selector.dart @@ -25,17 +25,15 @@ class OrganizationSelector extends StatelessWidget { required List items, }) { return PopupMenuButton( + color: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), onSelected: (name) async { - // Determine the selected organization Organization? org = name == "All Organizations" ? null : controller.organizations.firstWhere((e) => e.name == name); - // Update controller state controller.selectOrganization(org); - // Trigger callback for post-selection logic if (onSelectionChanged != null) { await onSelectionChanged!(org); } @@ -45,9 +43,10 @@ class OrganizationSelector extends StatelessWidget { .toList(), child: Container( height: height, - padding: EdgeInsets.symmetric(horizontal: 12), + padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( - color: Colors.grey.shade100, + color: + Colors.white, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(12), ), diff --git a/lib/view/taskPlanning/daily_progress.dart b/lib/view/taskPlanning/daily_progress.dart index cfc2447..c102803 100644 --- a/lib/view/taskPlanning/daily_progress.dart +++ b/lib/view/taskPlanning/daily_progress.dart @@ -44,28 +44,50 @@ class _DailyProgressReportScreenState extends State Get.find(); 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); } - ever( - projectController.selectedProjectId, - (newProjectId) async { - if (newProjectId.isNotEmpty && - newProjectId != dailyTaskController.selectedProjectId) { - dailyTaskController.selectedProjectId = newProjectId; - await dailyTaskController.fetchTaskData(newProjectId); - dailyTaskController.update(['daily_progress_report_controller']); - } - }, - ); + // 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 @@ -158,7 +180,10 @@ class _DailyProgressReportScreenState extends State if (projectId?.isNotEmpty ?? false) { await dailyTaskController.fetchTaskData( projectId!, - // serviceId: service?.id, + serviceIds: + service != null ? [service.id] : null, + pageNumber: 1, + pageSize: 20, ); } }, @@ -321,10 +346,12 @@ class _DailyProgressReportScreenState extends State final isLoading = dailyTaskController.isLoading.value; final groupedTasks = dailyTaskController.groupedDailyTasks; - if (isLoading) { + // Initial loading skeleton + if (isLoading && dailyTaskController.currentPage == 1) { return SkeletonLoaders.dailyProgressReportSkeletonLoader(); } + // No tasks if (groupedTasks.isEmpty) { return Center( child: MyText.bodySmall( @@ -337,23 +364,33 @@ class _DailyProgressReportScreenState extends State 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.separated( + child: ListView.builder( + controller: _scrollController, shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: sortedDates.length, - separatorBuilder: (_, __) => Column( - children: [ - const SizedBox(height: 12), - Divider(color: Colors.grey.withOpacity(0.3), thickness: 1), - const SizedBox(height: 12), - ], - ), + 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); @@ -389,7 +426,6 @@ class _DailyProgressReportScreenState extends State return Column( children: tasksForDate.asMap().entries.map((entry) { final task = entry.value; - final index = entry.key; final activityName = task.workItem?.activityMaster?.activityName ?? 'N/A'; @@ -407,134 +443,121 @@ class _DailyProgressReportScreenState extends State ? (completed / planned).clamp(0.0, 1.0) : 0.0; final parentTaskID = task.id; - return Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8), - child: MyContainer( - paddingAll: 12, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + + 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: [ - 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), - ], + Container( + height: 5, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(6), ), ), - 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), ), - 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, - ), - ], ), ), ], ), - ), + 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, + ), + ], + ), + ), + ], ), - if (index != tasksForDate.length - 1) - Divider( - color: Colors.grey.withOpacity(0.2), - thickness: 1, - height: 1), - ], + ), ); }).toList(), ); diff --git a/lib/view/taskPlanning/daily_task_planning.dart b/lib/view/taskPlanning/daily_task_planning.dart index 7411d28..ebf4b3d 100644 --- a/lib/view/taskPlanning/daily_task_planning.dart +++ b/lib/view/taskPlanning/daily_task_planning.dart @@ -37,18 +37,19 @@ class _DailyTaskPlanningScreenState extends State void initState() { super.initState(); - // Initial fetch if a project is already selected final projectId = projectController.selectedProjectId.value; if (projectId.isNotEmpty) { dailyTaskPlanningController.fetchTaskData(projectId); + serviceController.fetchServices(projectId); // <-- Fetch services here } - // Reactive fetch on project ID change ever( projectController.selectedProjectId, (newProjectId) { if (newProjectId.isNotEmpty) { dailyTaskPlanningController.fetchTaskData(newProjectId); + serviceController + .fetchServices(newProjectId); } }, ); @@ -150,8 +151,7 @@ class _DailyTaskPlanningScreenState extends State Padding( padding: MySpacing.x(10), child: ServiceSelector( - controller: - serviceController, + controller: serviceController, height: 40, onSelectionChanged: (service) async { final projectId =