import 'package:get/get.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/model/service_project/service_projects_details_model.dart'; import 'package:marco/model/service_project/job_list_model.dart'; import 'package:marco/model/service_project/service_project_job_detail_model.dart'; import 'package:geolocator/geolocator.dart'; import 'package:marco/model/service_project/job_attendance_logs_model.dart'; import 'package:marco/model/service_project/job_allocation_model.dart'; import 'dart:convert'; import 'dart:io'; import 'package:mime/mime.dart'; class ServiceProjectDetailsController extends GetxController { // -------------------- Observables -------------------- var projectId = ''.obs; var projectDetail = Rxn(); var jobList = [].obs; var jobDetail = Rxn(); // Loading states var isLoading = false.obs; var isJobLoading = false.obs; var isJobDetailLoading = false.obs; // Error messages var errorMessage = ''.obs; var jobErrorMessage = ''.obs; var jobDetailErrorMessage = ''.obs; // Pagination var pageNumber = 1; final int pageSize = 20; var hasMoreJobs = true.obs; var isTagging = false.obs; var attendanceMessage = ''.obs; var attendanceLog = Rxn(); var teamList = [].obs; var isTeamLoading = false.obs; var teamErrorMessage = ''.obs; // -------------------- Lifecycle -------------------- @override void onInit() { super.onInit(); fetchProjectJobs(); // always load jobs even without projectId } // -------------------- Project -------------------- void setProjectId(String id) { projectId.value = id; fetchProjectDetail(); pageNumber = 1; hasMoreJobs.value = true; fetchProjectJobs(); // no initialLoad } Future fetchProjectTeams() async { if (projectId.value.isEmpty) { teamErrorMessage.value = "Invalid project ID"; return; } isTeamLoading.value = true; teamErrorMessage.value = ''; try { final result = await ApiService.getServiceProjectAllocationList( projectId: projectId.value, isActive: true, ); if (result != null) { teamList.value = result; } else { teamErrorMessage.value = "No teams found"; } } catch (e) { teamErrorMessage.value = "Error fetching teams: $e"; } finally { isTeamLoading.value = false; } } Future fetchProjectDetail() async { if (projectId.value.isEmpty) { errorMessage.value = "Invalid project ID"; return; } isLoading.value = true; errorMessage.value = ''; try { final result = await ApiService.getServiceProjectDetailApi(projectId.value); if (result != null && result.data != null) { projectDetail.value = result.data!; } else { errorMessage.value = result?.message ?? "Failed to fetch project details"; } } catch (e) { errorMessage.value = "Error: $e"; } finally { isLoading.value = false; } } Future fetchJobAttendanceLog(String attendanceId) async { if (attendanceId.isEmpty) { attendanceMessage.value = "Invalid attendance ID"; return; } isJobDetailLoading.value = true; attendanceMessage.value = ''; try { final result = await ApiService.getJobAttendanceLog(attendanceId: attendanceId); if (result != null) { attendanceLog.value = result; } else { attendanceMessage.value = "Attendance log not found or empty"; } } catch (e) { attendanceMessage.value = "Error fetching attendance log: $e"; } finally { isJobDetailLoading.value = false; } } // -------------------- Job List (modified to always load) -------------------- Future fetchProjectJobs() async { if (!hasMoreJobs.value) return; isJobLoading.value = true; jobErrorMessage.value = ''; try { final result = await ApiService.getServiceProjectJobListApi( projectId: projectId.value, // allows empty projectId pageNumber: pageNumber, pageSize: pageSize, isActive: true, ); if (result != null && result.data != null) { final newJobs = result.data?.data ?? []; if (pageNumber == 1) { jobList.value = newJobs; } else { jobList.addAll(newJobs); } hasMoreJobs.value = newJobs.length == pageSize; if (hasMoreJobs.value) pageNumber++; } else { jobErrorMessage.value = result?.message ?? "Failed to fetch job list"; } } catch (e) { jobErrorMessage.value = "Error fetching jobs: $e"; } finally { isJobLoading.value = false; } } Future fetchMoreJobs() async => fetchProjectJobs(); // -------------------- Manual Refresh -------------------- Future refresh() async { pageNumber = 1; hasMoreJobs.value = true; await Future.wait([ fetchProjectDetail(), fetchProjectJobs(), ]); } // -------------------- Job Detail -------------------- Future fetchJobDetail(String jobId) async { if (jobId.isEmpty) { jobDetailErrorMessage.value = "Invalid job ID"; return; } isJobDetailLoading.value = true; jobDetailErrorMessage.value = ''; try { final result = await ApiService.getServiceProjectJobDetailApi(jobId); if (result != null) { jobDetail.value = result; } else { jobDetailErrorMessage.value = "Failed to fetch job details"; } } catch (e) { jobDetailErrorMessage.value = "Error fetching job details: $e"; } finally { isJobDetailLoading.value = false; } } Future _getCurrentLocation() async { try { bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { attendanceMessage.value = "Location services are disabled."; return null; } LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { attendanceMessage.value = "Location permission denied"; return null; } } if (permission == LocationPermission.deniedForever) { attendanceMessage.value = "Location permission permanently denied. Enable it from settings."; return null; } return await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high); } catch (e) { attendanceMessage.value = "Failed to get location: $e"; return null; } } /// Tag In / Tag Out for a job with proper payload Future updateJobAttendance({ required String jobId, required int action, String comment = "Updated via app", File? attachment, }) async { if (jobId.isEmpty) return; isTagging.value = true; attendanceMessage.value = ''; try { final position = await _getCurrentLocation(); if (position == null) { isTagging.value = false; return; } Map? attachmentPayload; if (attachment != null) { final bytes = await attachment.readAsBytes(); final base64Data = base64Encode(bytes); final mimeType = lookupMimeType(attachment.path) ?? 'application/octet-stream'; attachmentPayload = { "documentId": jobId, "fileName": attachment.path.split('/').last, "base64Data": base64Data, "contentType": mimeType, "fileSize": bytes.length, "description": "Attached via app", "isActive": true, }; } final payload = { "jobTcketId": jobId, "action": action, "latitude": position.latitude.toString(), "longitude": position.longitude.toString(), "comment": comment, "attachment": attachmentPayload, }; final success = await ApiService.updateServiceProjectJobAttendance( payload: payload, ); if (success) { attendanceMessage.value = action == 0 ? "Tagged In successfully" : "Tagged Out successfully"; await fetchJobDetail(jobId); } else { attendanceMessage.value = "Failed to update attendance"; } } catch (e) { attendanceMessage.value = "Error updating attendance: $e"; } finally { isTagging.value = false; } } // ------------------------------------------------------------ // 🔥 AUTO REFRESH JOB LIST AFTER ADDING A JOB // ------------------------------------------------------------ Future refreshJobsAfterAdd() async { pageNumber = 1; hasMoreJobs.value = true; await fetchProjectJobs(); } }