import 'package:get/get.dart'; import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:on_field_work/model/service_project/service_projects_details_model.dart'; import 'package:on_field_work/model/service_project/job_list_model.dart'; import 'package:on_field_work/model/service_project/service_project_job_detail_model.dart'; import 'package:geolocator/geolocator.dart'; import 'package:on_field_work/model/service_project/job_attendance_logs_model.dart'; import 'package:on_field_work/model/service_project/job_allocation_model.dart'; import 'package:on_field_work/model/service_project/job_status_response.dart'; import 'package:on_field_work/model/service_project/job_comments.dart'; import 'dart:convert'; import 'dart:io'; import 'package:mime/mime.dart'; import 'package:image_picker/image_picker.dart'; class ServiceProjectDetailsController extends GetxController { // -------------------- Observables -------------------- var projectId = ''.obs; var projectDetail = Rxn(); var jobList = [].obs; var jobDetail = Rxn(); var showArchivedJobs = false.obs; // true = archived, false = active // 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; final ImagePicker picker = ImagePicker(); var isProcessingAttachment = false.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; var filteredJobList = [].obs; // -------------------- Job Status -------------------- // With this: var jobStatusList = [].obs; var selectedJobStatus = Rx(null); var isJobStatusLoading = false.obs; var jobStatusErrorMessage = ''.obs; // -------------------- Job Comments -------------------- var jobComments = [].obs; var isCommentsLoading = false.obs; var commentsErrorMessage = ''.obs; // -------------------- Lifecycle -------------------- @override void onInit() { super.onInit(); fetchProjectJobs(); filteredJobList.value = jobList; } // -------------------- Project -------------------- void setProjectId(String id) { if (projectId.value == id) return; projectId.value = id; // Reset pagination and list pageNumber = 1; hasMoreJobs.value = true; jobList.clear(); filteredJobList.clear(); // Fetch project detail fetchProjectDetail(); // Always fetch jobs for this project fetchProjectJobs(refresh: true); } void updateJobSearch(String searchText) { if (searchText.isEmpty) { filteredJobList.value = jobList; } else { filteredJobList.value = jobList.where((job) { final lowerSearch = searchText.toLowerCase(); return job.title.toLowerCase().contains(lowerSearch) || (job.description.toLowerCase().contains(lowerSearch)) || (job.tags?.any( (tag) => tag.name.toLowerCase().contains(lowerSearch)) ?? false); }).toList(); } } 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 fetchJobStatus({required String statusId}) async { if (projectId.value.isEmpty) { jobStatusErrorMessage.value = "Invalid project ID"; return; } isJobStatusLoading.value = true; jobStatusErrorMessage.value = ''; try { final statuses = await ApiService.getMasterJobStatus( projectId: projectId.value, statusId: statusId, ); if (statuses != null && statuses.isNotEmpty) { jobStatusList.value = statuses; // Keep previously selected if exists, else pick first selectedJobStatus.value = statuses.firstWhere( (status) => status.id == selectedJobStatus.value?.id, orElse: () => statuses.first, ); print("Job Status List: ${jobStatusList.map((e) => e.name).toList()}"); } else { jobStatusErrorMessage.value = "No job statuses found"; } } catch (e) { jobStatusErrorMessage.value = "Error fetching job status: $e"; } finally { isJobStatusLoading.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({bool refresh = false}) async { if (projectId.value.isEmpty) return; if (refresh) pageNumber = 1; if (!hasMoreJobs.value && !refresh) return; isJobLoading.value = true; jobErrorMessage.value = ''; try { final result = await ApiService.getServiceProjectJobListApi( projectId: projectId.value, pageNumber: pageNumber, pageSize: pageSize, isActive: true, isArchive: showArchivedJobs.value, ); if (result != null && result.data != null) { final newJobs = result.data?.data ?? []; if (refresh || pageNumber == 1) { jobList.value = newJobs; } else { jobList.addAll(newJobs); } filteredJobList.value = jobList; hasMoreJobs.value = newJobs.length == pageSize; if (hasMoreJobs.value) pageNumber++; } else { jobErrorMessage.value = result?.message ?? "Failed to fetch jobs"; } } 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; } } Future fetchJobComments({bool refresh = false}) async { if (jobDetail.value?.data?.id == null) { commentsErrorMessage.value = "Invalid job ID"; return; } if (refresh) pageNumber = 1; isCommentsLoading.value = true; commentsErrorMessage.value = ''; try { final response = await ApiService.getJobCommentList( jobTicketId: jobDetail.value!.data!.id!, pageNumber: pageNumber, pageSize: pageSize, ); if (response != null && response.data != null) { final newComments = response.data?.data ?? []; if (refresh || pageNumber == 1) { jobComments.value = newComments; } else { jobComments.addAll(newComments); } hasMoreJobs.value = (response.data?.totalEntities ?? 0) > (pageNumber * pageSize); if (hasMoreJobs.value) pageNumber++; } else { commentsErrorMessage.value = response?.message ?? "Failed to fetch comments"; } } catch (e) { commentsErrorMessage.value = "Error fetching comments: $e"; } finally { isCommentsLoading.value = false; } } Future addJobComment({ required String jobId, required String comment, List? files, }) async { try { List> attachments = []; if (files != null && files.isNotEmpty) { for (final file in files) { final bytes = await file.readAsBytes(); final base64Data = base64Encode(bytes); final mimeType = lookupMimeType(file.path) ?? "application/octet-stream"; attachments.add({ "fileName": file.path.split('/').last, "base64Data": base64Data, "contentType": mimeType, "fileSize": bytes.length, "description": "", "isActive": true, }); } } final success = await ApiService.addJobComment( jobTicketId: jobId, comment: comment, attachments: attachments, ); if (success) { await fetchJobDetail(jobId); refresh(); } return success; } catch (e) { print("Error adding comment: $e"); return false; } } /// 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(); } }