import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:geolocator/geolocator.dart'; import 'package:intl/intl.dart'; import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/widgets/my_image_compressor.dart'; import 'package:marco/model/attendance/attendance_model.dart'; import 'package:marco/model/project_model.dart'; import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/model/attendance/attendance_log_model.dart'; import 'package:marco/model/regularization_log_model.dart'; import 'package:marco/model/attendance/attendance_log_view_model.dart'; import 'package:marco/model/attendance/organization_per_project_list_model.dart'; import 'package:marco/controller/project_controller.dart'; class AttendanceController extends GetxController { // Data models List attendances = []; List projects = []; List employees = []; List attendanceLogs = []; List regularizationLogs = []; List attendenceLogsView = []; // ------------------ Organizations ------------------ List organizations = []; Organization? selectedOrganization; final isLoadingOrganizations = false.obs; // States String selectedTab = 'todaysAttendance'; DateTime? startDateAttendance; DateTime? endDateAttendance; final isLoading = true.obs; final isLoadingProjects = true.obs; final isLoadingEmployees = true.obs; final isLoadingAttendanceLogs = true.obs; final isLoadingRegularizationLogs = true.obs; final isLoadingLogView = true.obs; final uploadingStates = {}.obs; var showPendingOnly = false.obs; @override void onInit() { super.onInit(); _initializeDefaults(); // 🔹 Fetch organizations for the selected project final projectId = Get.find().selectedProject?.id; if (projectId != null) { fetchOrganizations(projectId); } } void _initializeDefaults() { _setDefaultDateRange(); } void _setDefaultDateRange() { final today = DateTime.now(); startDateAttendance = today.subtract(const Duration(days: 7)); endDateAttendance = today.subtract(const Duration(days: 1)); logSafe( "Default date range set: $startDateAttendance to $endDateAttendance"); } // ------------------ Project & Employee ------------------ /// Called when a notification says attendance has been updated Future refreshDataFromNotification({String? projectId}) async { projectId ??= Get.find().selectedProject?.id; if (projectId == null) { logSafe("No project selected for attendance refresh from notification", level: LogLevel.warning); return; } await fetchProjectData(projectId); logSafe( "Attendance data refreshed from notification for project $projectId"); } // 🔍 Search query final searchQuery = ''.obs; // Computed filtered employees List get filteredEmployees { if (searchQuery.value.isEmpty) return employees; return employees .where((e) => e.name.toLowerCase().contains(searchQuery.value.toLowerCase())) .toList(); } // Computed filtered logs List get filteredLogs { if (searchQuery.value.isEmpty) return attendanceLogs; return attendanceLogs .where((log) => (log.name).toLowerCase().contains(searchQuery.value.toLowerCase())) .toList(); } // Computed filtered regularization logs List get filteredRegularizationLogs { if (searchQuery.value.isEmpty) return regularizationLogs; return regularizationLogs .where((log) => log.name.toLowerCase().contains(searchQuery.value.toLowerCase())) .toList(); } Future fetchTodaysAttendance(String? projectId) async { if (projectId == null) return; isLoadingEmployees.value = true; final response = await ApiService.getTodaysAttendance( projectId, organizationId: selectedOrganization?.id, ); if (response != null) { employees = response.map((e) => EmployeeModel.fromJson(e)).toList(); for (var emp in employees) { uploadingStates[emp.id] = false.obs; } logSafe("Employees fetched: ${employees.length} for project $projectId"); } else { logSafe("Failed to fetch employees for project $projectId", level: LogLevel.error); } isLoadingEmployees.value = false; update(); } Future fetchOrganizations(String projectId) async { isLoadingOrganizations.value = true; final response = await ApiService.getAssignedOrganizations(projectId); if (response != null) { organizations = response.data; logSafe("Organizations fetched: ${organizations.length}"); } else { logSafe("Failed to fetch organizations for project $projectId", level: LogLevel.error); } isLoadingOrganizations.value = false; update(); } // ------------------ Attendance Capture ------------------ Future captureAndUploadAttendance( String id, String employeeId, String projectId, { String comment = "Marked via mobile app", required int action, bool imageCapture = true, String? markTime, // still optional in controller String? date, // new optional param }) async { try { uploadingStates[employeeId]?.value = true; XFile? image; if (imageCapture) { image = await ImagePicker() .pickImage(source: ImageSource.camera, imageQuality: 80); if (image == null) { logSafe("Image capture cancelled.", level: LogLevel.warning); return false; } final compressedBytes = await compressImageToUnder100KB(File(image.path)); if (compressedBytes == null) { logSafe("Image compression failed.", level: LogLevel.error); return false; } final compressedFile = await saveCompressedImageToFile(compressedBytes); image = XFile(compressedFile.path); } if (!await _handleLocationPermission()) return false; final position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high); final imageName = imageCapture ? ApiService.generateImageName(employeeId, employees.length + 1) : ""; // ---------------- DATE / TIME LOGIC ---------------- final now = DateTime.now(); // Default effectiveDate = now DateTime effectiveDate = now; if (action == 1) { // Checkout // Try to find today's open log for this employee final log = attendanceLogs.firstWhereOrNull( (log) => log.employeeId == employeeId && log.checkOut == null, ); if (log?.checkIn != null) { effectiveDate = log!.checkIn!; // use check-in date } } final formattedMarkTime = markTime ?? DateFormat('hh:mm a').format(now); final formattedDate = date ?? DateFormat('yyyy-MM-dd').format(effectiveDate); // ---------------- API CALL ---------------- final result = await ApiService.uploadAttendanceImage( id, employeeId, image, position.latitude, position.longitude, imageName: imageName, projectId: projectId, comment: comment, action: action, imageCapture: imageCapture, markTime: formattedMarkTime, date: formattedDate, ); logSafe( "Attendance uploaded for $employeeId, action: $action, date: $formattedDate"); return result; } catch (e, stacktrace) { logSafe("Error uploading attendance", level: LogLevel.error, error: e, stackTrace: stacktrace); return false; } finally { uploadingStates[employeeId]?.value = false; } } Future _handleLocationPermission() async { LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { logSafe('Location permissions are denied', level: LogLevel.warning); return false; } } if (permission == LocationPermission.deniedForever) { logSafe('Location permissions are permanently denied', level: LogLevel.error); return false; } return true; } // ------------------ Attendance Logs ------------------ Future fetchAttendanceLogs(String? projectId, {DateTime? dateFrom, DateTime? dateTo}) async { if (projectId == null) return; isLoadingAttendanceLogs.value = true; final response = await ApiService.getAttendanceLogs( projectId, dateFrom: dateFrom, dateTo: dateTo, organizationId: selectedOrganization?.id, ); if (response != null) { attendanceLogs = response.map((e) => AttendanceLogModel.fromJson(e)).toList(); logSafe("Attendance logs fetched: ${attendanceLogs.length}"); } else { logSafe("Failed to fetch attendance logs for project $projectId", level: LogLevel.error); } isLoadingAttendanceLogs.value = false; update(); } Map> groupLogsByCheckInDate() { final groupedLogs = >{}; for (var logItem in attendanceLogs) { final checkInDate = logItem.checkIn != null ? DateFormat('dd MMM yyyy').format(logItem.checkIn!) : 'Unknown'; groupedLogs.putIfAbsent(checkInDate, () => []).add(logItem); } final sortedEntries = groupedLogs.entries.toList() ..sort((a, b) { if (a.key == 'Unknown') return 1; if (b.key == 'Unknown') return -1; final dateA = DateFormat('dd MMM yyyy').parse(a.key); final dateB = DateFormat('dd MMM yyyy').parse(b.key); return dateB.compareTo(dateA); }); return Map>.fromEntries(sortedEntries); } // ------------------ Regularization Logs ------------------ Future fetchRegularizationLogs(String? projectId) async { if (projectId == null) return; isLoadingRegularizationLogs.value = true; final response = await ApiService.getRegularizationLogs( projectId, organizationId: selectedOrganization?.id, ); if (response != null) { regularizationLogs = response.map((e) => RegularizationLogModel.fromJson(e)).toList(); logSafe("Regularization logs fetched: ${regularizationLogs.length}"); } else { logSafe("Failed to fetch regularization logs for project $projectId", level: LogLevel.error); } isLoadingRegularizationLogs.value = false; update(); } // ------------------ Attendance Log View ------------------ Future fetchLogsView(String? id) async { if (id == null) return; isLoadingLogView.value = true; final response = await ApiService.getAttendanceLogView(id); if (response != null) { attendenceLogsView = response.map((e) => AttendanceLogViewModel.fromJson(e)).toList(); attendenceLogsView.sort((a, b) => (b.activityTime ?? DateTime(2000)) .compareTo(a.activityTime ?? DateTime(2000))); logSafe("Attendance log view fetched for ID: $id"); } else { logSafe("Failed to fetch attendance log view for ID $id", level: LogLevel.error); } isLoadingLogView.value = false; update(); } // ------------------ Combined Load ------------------ Future loadAttendanceData(String projectId) async { isLoading.value = true; await fetchProjectData(projectId); isLoading.value = false; } Future fetchProjectData(String? projectId) async { if (projectId == null) return; await fetchOrganizations(projectId); // Call APIs depending on the selected tab only switch (selectedTab) { case 'todaysAttendance': await fetchTodaysAttendance(projectId); break; case 'attendanceLogs': await fetchAttendanceLogs( projectId, dateFrom: startDateAttendance, dateTo: endDateAttendance, ); break; case 'regularizationRequests': await fetchRegularizationLogs(projectId); break; } logSafe( "Project data fetched for project ID: $projectId, tab: $selectedTab"); update(); } // ------------------ UI Interaction ------------------ Future selectDateRangeForAttendance( BuildContext context, AttendanceController controller) async { final today = DateTime.now(); final picked = await showDateRangePicker( context: context, firstDate: DateTime(2022), lastDate: today.subtract(const Duration(days: 1)), initialDateRange: DateTimeRange( start: startDateAttendance ?? today.subtract(const Duration(days: 7)), end: endDateAttendance ?? today.subtract(const Duration(days: 1)), ), ); if (picked != null) { startDateAttendance = picked.start; endDateAttendance = picked.end; logSafe( "Date range selected: $startDateAttendance to $endDateAttendance"); await controller.fetchAttendanceLogs( Get.find().selectedProject?.id, dateFrom: picked.start, dateTo: picked.end, ); } } }