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:logger/logger.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/widgets/my_image_compressor.dart'; import 'package:marco/model/attendance_model.dart'; import 'package:marco/model/project_model.dart'; import 'package:marco/model/employee_model.dart'; import 'package:marco/model/attendance_log_model.dart'; import 'package:marco/model/regularization_log_model.dart'; import 'package:marco/model/attendance_log_view_model.dart'; final Logger log = Logger(); class AttendanceController extends GetxController { // Data lists List attendances = []; List projects = []; List employees = []; List attendanceLogs = []; List regularizationLogs = []; List attendenceLogsView = []; // Selected values String? selectedProjectId; String selectedTab = 'Employee List'; // Date range for attendance filtering DateTime? startDateAttendance; DateTime? endDateAttendance; // Loading states RxBool isLoading = true.obs; RxBool isLoadingProjects = true.obs; RxBool isLoadingEmployees = true.obs; RxBool isLoadingAttendanceLogs = true.obs; RxBool isLoadingRegularizationLogs = true.obs; RxBool isLoadingLogView = true.obs; // Uploading state per employee (keyed by employeeId) RxMap uploadingStates = {}.obs; @override void onInit() { super.onInit(); _initializeDefaults(); } void _initializeDefaults() { _setDefaultDateRange(); fetchProjects(); } void _setDefaultDateRange() { final today = DateTime.now(); startDateAttendance = today.subtract(const Duration(days: 7)); endDateAttendance = today.subtract(const Duration(days: 1)); log.i("Default date range set: $startDateAttendance to $endDateAttendance"); } /// Checks and requests location permission, returns true if granted. Future _handleLocationPermission() async { LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { log.w('Location permissions are denied'); return false; } } if (permission == LocationPermission.deniedForever) { log.e('Location permissions are permanently denied'); return false; } return true; } /// Fetches projects and initializes selected project. Future fetchProjects() async { isLoadingProjects.value = true; isLoading.value = true; final response = await ApiService.getProjects(); if (response != null && response.isNotEmpty) { projects = response.map((json) => ProjectModel.fromJson(json)).toList(); log.i("Projects fetched: ${projects.length}"); } else { log.e("Failed to fetch projects or no projects available."); projects = []; } isLoadingProjects.value = false; isLoading.value = false; update(['attendance_dashboard_controller']); } Future loadAttendanceData(String projectId) async { await fetchEmployeesByProject(projectId); await fetchAttendanceLogs(projectId); await fetchRegularizationLogs(projectId); await fetchProjectData(projectId); } /// Fetches employees, attendance logs and regularization logs for a project. Future fetchProjectData(String? projectId) async { if (projectId == null) return; isLoading.value = true; await Future.wait([ fetchEmployeesByProject(projectId), fetchAttendanceLogs(projectId, dateFrom: startDateAttendance, dateTo: endDateAttendance), fetchRegularizationLogs(projectId), ]); isLoading.value = false; log.i("Project data fetched for project ID: $projectId"); } /// Fetches employees for the given project. Future fetchEmployeesByProject(String? projectId) async { if (projectId == null) return; isLoadingEmployees.value = true; final response = await ApiService.getEmployeesByProject(projectId); if (response != null) { employees = response.map((json) => EmployeeModel.fromJson(json)).toList(); // Initialize uploading states for employees for (var emp in employees) { uploadingStates[emp.id] = false.obs; } log.i("Employees fetched: ${employees.length} for project $projectId"); update(); } else { log.e("Failed to fetch employees for project $projectId"); } isLoadingEmployees.value = false; } /// Captures image, gets location, and uploads attendance data. /// Returns true on success. Future captureAndUploadAttendance( String id, String employeeId, String projectId, { String comment = "Marked via mobile app", required int action, bool imageCapture = true, String? markTime, }) async { try { uploadingStates[employeeId]?.value = true; XFile? image; if (imageCapture) { image = await ImagePicker().pickImage( source: ImageSource.camera, imageQuality: 80, ); if (image == null) { log.w("Image capture cancelled."); uploadingStates[employeeId]?.value = false; return false; } final compressedBytes = await compressImageToUnder100KB(File(image.path)); if (compressedBytes == null) { log.e("Image compression failed."); uploadingStates[employeeId]?.value = false; return false; } final compressedFile = await saveCompressedImageToFile(compressedBytes); image = XFile(compressedFile.path); } final hasLocationPermission = await _handleLocationPermission(); if (!hasLocationPermission) { uploadingStates[employeeId]?.value = false; return false; } final position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high, ); final imageName = imageCapture ? ApiService.generateImageName(employeeId, employees.length + 1) : ""; final result = await ApiService.uploadAttendanceImage( id, employeeId, image, position.latitude, position.longitude, imageName: imageName, projectId: projectId, comment: comment, action: action, imageCapture: imageCapture, markTime: markTime, ); log.i("Attendance uploaded for $employeeId, action: $action"); return result; } catch (e, stacktrace) { log.e("Error uploading attendance", error: e, stackTrace: stacktrace); return false; } finally { uploadingStates[employeeId]?.value = false; } } /// Opens a date range picker for attendance filtering and fetches logs on selection. Future selectDateRangeForAttendance( BuildContext context, AttendanceController controller, ) async { final today = DateTime.now(); final todayDateOnly = DateTime(today.year, today.month, today.day); final picked = await showDateRangePicker( context: context, firstDate: DateTime(2022), lastDate: todayDateOnly.subtract(const Duration(days: 1)), initialDateRange: DateTimeRange( start: startDateAttendance ?? today.subtract(const Duration(days: 7)), end: endDateAttendance ?? todayDateOnly.subtract(const Duration(days: 1)), ), builder: (BuildContext context, Widget? child) { return Center( child: SizedBox( width: 400, height: 500, child: Theme( data: Theme.of(context).copyWith( colorScheme: ColorScheme.light( primary: const Color.fromARGB(255, 95, 132, 255), onPrimary: Colors.white, onSurface: Colors.teal.shade800, ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom(foregroundColor: Colors.teal), ), dialogTheme: const DialogTheme(backgroundColor: Colors.white), ), child: child!, ), ), ); }, ); if (picked != null) { startDateAttendance = picked.start; endDateAttendance = picked.end; log.i("Date range selected: $startDateAttendance to $endDateAttendance"); await controller.fetchAttendanceLogs( controller.selectedProjectId, dateFrom: picked.start, dateTo: picked.end, ); } } /// Fetches attendance logs filtered by project and date range. Future fetchAttendanceLogs( String? projectId, { DateTime? dateFrom, DateTime? dateTo, }) async { if (projectId == null) return; isLoadingAttendanceLogs.value = true; isLoading.value = true; final response = await ApiService.getAttendanceLogs( projectId, dateFrom: dateFrom, dateTo: dateTo, ); if (response != null) { attendanceLogs = response.map((json) => AttendanceLogModel.fromJson(json)).toList(); log.i("Attendance logs fetched: ${attendanceLogs.length}"); update(); } else { log.e("Failed to fetch attendance logs for project $projectId"); } isLoadingAttendanceLogs.value = false; isLoading.value = false; } /// Groups attendance logs by check-in date formatted as 'dd MMM yyyy'. 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, () => []); groupedLogs[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); }); final sortedMap = Map>.fromEntries(sortedEntries); log.i("Logs grouped and sorted by check-in date."); return sortedMap; } /// Fetches regularization logs for a project. Future fetchRegularizationLogs( String? projectId, { DateTime? dateFrom, DateTime? dateTo, }) async { if (projectId == null) return; isLoadingRegularizationLogs.value = true; isLoading.value = true; final response = await ApiService.getRegularizationLogs(projectId); if (response != null) { regularizationLogs = response .map((json) => RegularizationLogModel.fromJson(json)) .toList(); log.i("Regularization logs fetched: ${regularizationLogs.length}"); update(); } else { log.e("Failed to fetch regularization logs for project $projectId"); } isLoadingRegularizationLogs.value = false; isLoading.value = false; } /// Fetches detailed attendance log view for a specific ID. Future fetchLogsView(String? id) async { if (id == null) return; isLoadingLogView.value = true; isLoading.value = true; final response = await ApiService.getAttendanceLogView(id); if (response != null) { attendenceLogsView = response .map((json) => AttendanceLogViewModel.fromJson(json)) .toList(); attendenceLogsView.sort((a, b) { if (a.activityTime == null || b.activityTime == null) return 0; return b.activityTime!.compareTo(a.activityTime!); }); log.i("Attendance log view fetched for ID: $id"); update(); } else { log.e("Failed to fetch attendance log view for ID $id"); } isLoadingLogView.value = false; isLoading.value = false; } }