import 'package:get/get.dart'; import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/widgets/my_form_validator.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; import 'package:on_field_work/model/project_model.dart'; import 'package:on_field_work/model/dailyTaskPlanning/daily_task_planning_model.dart'; import 'package:on_field_work/model/employees/employee_model.dart'; class DailyTaskPlanningController extends GetxController { List projects = []; RxList employees = [].obs; RxList selectedEmployees = [].obs; List allEmployeesCache = []; List dailyTasks = []; RxMap uploadingStates = {}.obs; MyFormValidator basicValidator = MyFormValidator(); List> roles = []; RxBool isAssigningTask = false.obs; RxnString selectedRoleId = RxnString(); RxBool isFetchingTasks = true.obs; RxBool isFetchingProjects = true.obs; RxBool isFetchingEmployees = true.obs; /// New: track per-building loading and loaded state for lazy infra loading RxMap buildingLoadingStates = {}.obs; final Set buildingsWithDetails = {}; @override void onInit() { super.onInit(); fetchRoles(); } String? formFieldValidator(String? value, {required String fieldType}) { if (value == null || value.trim().isEmpty) return 'This field is required'; if (fieldType == "target" && int.tryParse(value.trim()) == null) { return 'Please enter a valid number'; } if (fieldType == "description" && value.trim().length < 5) { return 'Description must be at least 5 characters'; } return null; } void updateSelectedEmployees() { selectedEmployees.value = employees.where((e) => uploadingStates[e.id]?.value == true).toList(); logSafe("Updated selected employees", level: LogLevel.debug); } void onRoleSelected(String? roleId) { selectedRoleId.value = roleId; logSafe("Role selected", level: LogLevel.info); } Future fetchRoles() async { logSafe("Fetching roles...", level: LogLevel.info); final result = await ApiService.getRoles(); if (result != null) { roles = List>.from(result); logSafe("Roles fetched successfully", level: LogLevel.info); update(); } else { logSafe("Failed to fetch roles", level: LogLevel.error); } } Future assignDailyTask({ required String workItemId, required int plannedTask, required String description, required List taskTeam, DateTime? assignmentDate, String? organizationId, String? serviceId, }) async { isAssigningTask.value = true; logSafe("Starting assign task...", level: LogLevel.info); final response = await ApiService.assignDailyTask( workItemId: workItemId, plannedTask: plannedTask, description: description, taskTeam: taskTeam, assignmentDate: assignmentDate, organizationId: organizationId, serviceId: serviceId, ); isAssigningTask.value = false; if (response == true) { logSafe("Task assigned successfully", level: LogLevel.info); showAppSnackbar( title: "Success", message: "Task assigned successfully!", type: SnackbarType.success, ); return true; } else { logSafe("Failed to assign task", level: LogLevel.error); showAppSnackbar( title: "Error", message: "Failed to assign task.", type: SnackbarType.error, ); return false; } } /// Fetch buildings list only (no deep area/workItem calls) for initial load. Future fetchTaskData(String? projectId, {String? serviceId}) async { if (projectId == null) return; isFetchingTasks.value = true; try { final infraResponse = await ApiService.getInfraDetails( projectId, serviceId: serviceId, ); final infraData = infraResponse?['data'] as List?; if (infraData == null || infraData.isEmpty) { dailyTasks = []; return; } // Filter buildings with 0 planned & completed work final filteredBuildings = infraData.where((b) { final planned = (b['plannedWork'] as num?)?.toDouble() ?? 0; final completed = (b['completedWork'] as num?)?.toDouble() ?? 0; return planned > 0 || completed > 0; }).toList(); dailyTasks = filteredBuildings.map((buildingJson) { final building = Building( id: buildingJson['id'], name: buildingJson['buildingName'], description: buildingJson['description'], floors: [], plannedWork: (buildingJson['plannedWork'] as num?)?.toDouble() ?? 0, completedWork: (buildingJson['completedWork'] as num?)?.toDouble() ?? 0, ); return TaskPlanningDetailsModel( id: building.id, name: building.name, projectAddress: "", contactPerson: "", startDate: DateTime.now(), endDate: DateTime.now(), projectStatusId: "", buildings: [building], ); }).toList(); buildingLoadingStates.clear(); buildingsWithDetails.clear(); } catch (e, stack) { logSafe("Error fetching daily task data", level: LogLevel.error, error: e, stackTrace: stack); } finally { isFetchingTasks.value = false; update(); } } /// Fetch full infra for a single building (floors, workAreas, workItems). /// Called lazily when user expands a building in the UI. Future fetchBuildingInfra( String buildingId, String projectId, String? serviceId) async { if (buildingId.isEmpty) return; // mark loading buildingLoadingStates.putIfAbsent(buildingId, () => true.obs); buildingLoadingStates[buildingId]!.value = true; update(); try { // Re-use getInfraDetails and find the building entry for the requested buildingId final infraResponse = await ApiService.getInfraDetails(projectId, serviceId: serviceId); final infraData = infraResponse?['data'] as List? ?? []; final buildingJson = infraData.firstWhere( (b) => b['id'].toString() == buildingId.toString(), orElse: () => null, ); if (buildingJson == null) { logSafe("Building $buildingId not found in infra response", level: LogLevel.warning); return; } // Build floors & workAreas for this building final building = Building( id: buildingJson['id'], name: buildingJson['buildingName'], description: buildingJson['description'], floors: (buildingJson['floors'] as List? ?? []).map((floorJson) { return Floor( id: floorJson['id'], floorName: floorJson['floorName'], workAreas: (floorJson['workAreas'] as List? ?? []) .map((areaJson) { return WorkArea( id: areaJson['id'], areaName: areaJson['areaName'], workItems: [], // will populate later ); }).toList(), ); }).toList(), plannedWork: (buildingJson['plannedWork'] as num?)?.toDouble() ?? 0, completedWork: (buildingJson['completedWork'] as num?)?.toDouble() ?? 0, ); // For each workArea, fetch its work items and populate await Future.wait( building.floors.expand((f) => f.workAreas).map((area) async { try { final taskResponse = await ApiService.getWorkItemsByWorkArea(area.id, serviceId: serviceId); final taskData = taskResponse?['data'] as List? ?? []; area.workItems.addAll(taskData.map((taskJson) => WorkItemWrapper( workItemId: taskJson['id'], workItem: WorkItem( id: taskJson['id'], activityMaster: taskJson['activityMaster'] != null ? ActivityMaster.fromJson(taskJson['activityMaster']) : null, workCategoryMaster: taskJson['workCategoryMaster'] != null ? WorkCategoryMaster.fromJson( taskJson['workCategoryMaster']) : null, plannedWork: (taskJson['plannedWork'] as num?)?.toDouble(), completedWork: (taskJson['completedWork'] as num?)?.toDouble(), todaysAssigned: (taskJson['todaysAssigned'] as num?)?.toDouble(), description: taskJson['description'] as String?, taskDate: taskJson['taskDate'] != null ? DateTime.tryParse(taskJson['taskDate']) : null, ), ))); } catch (e, stack) { logSafe("Error fetching tasks for work area ${area.id}", level: LogLevel.error, error: e, stackTrace: stack); } })); // Merge/replace the building into dailyTasks bool merged = false; for (var t in dailyTasks) { final idx = t.buildings .indexWhere((b) => b.id.toString() == building.id.toString()); if (idx != -1) { t.buildings[idx] = building; merged = true; break; } } if (!merged) { // If not present, add a new TaskPlanningDetailsModel wrapper (fallback) dailyTasks.add(TaskPlanningDetailsModel( id: building.id, name: building.name, projectAddress: "", contactPerson: "", startDate: DateTime.now(), endDate: DateTime.now(), projectStatusId: "", buildings: [building], )); } // Mark as loaded buildingsWithDetails.add(buildingId.toString()); } catch (e, stack) { logSafe("Error fetching infra for building $buildingId", level: LogLevel.error, error: e, stackTrace: stack); } finally { buildingLoadingStates.putIfAbsent(buildingId, () => false.obs); buildingLoadingStates[buildingId]!.value = false; update(); } } Future fetchEmployeesByProjectService({ required String projectId, String? serviceId, String? organizationId, }) async { isFetchingEmployees.value = true; try { final response = await ApiService.getEmployeesByProjectService( projectId, serviceId: serviceId ?? '', organizationId: organizationId ?? '', ); if (response != null && response.isNotEmpty) { employees .assignAll(response.map((json) => EmployeeModel.fromJson(json))); if (serviceId == null && organizationId == null) { allEmployeesCache = List.from(employees); } final currentEmployeeIds = employees.map((e) => e.id).toSet(); uploadingStates .removeWhere((key, _) => !currentEmployeeIds.contains(key)); employees.forEach((emp) { uploadingStates.putIfAbsent(emp.id, () => false.obs); }); selectedEmployees .removeWhere((e) => !currentEmployeeIds.contains(e.id)); logSafe("Employees fetched: ${employees.length}", level: LogLevel.info); } else { employees.clear(); uploadingStates.clear(); selectedEmployees.clear(); logSafe( serviceId != null || organizationId != null ? "Filtered employees empty" : "No employees found", level: LogLevel.warning, ); } } catch (e, stack) { logSafe("Error fetching employees", level: LogLevel.error, error: e, stackTrace: stack); if (serviceId == null && organizationId == null && allEmployeesCache.isNotEmpty) { employees.assignAll(allEmployeesCache); final cachedEmployeeIds = employees.map((e) => e.id).toSet(); uploadingStates .removeWhere((key, _) => !cachedEmployeeIds.contains(key)); employees.forEach((emp) { uploadingStates.putIfAbsent(emp.id, () => false.obs); }); selectedEmployees.removeWhere((e) => !cachedEmployeeIds.contains(e.id)); } else { employees.clear(); uploadingStates.clear(); selectedEmployees.clear(); } } finally { isFetchingEmployees.value = false; update(); } } }