import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:geolocator/geolocator.dart'; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'package:on_field_work/controller/dashboard/dashboard_controller.dart'; import 'package:on_field_work/controller/project_controller.dart'; import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/services/app_logger.dart'; import 'package:on_field_work/helpers/widgets/my_image_compressor.dart'; import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart'; import 'package:on_field_work/model/attendance/attendance_log_model.dart'; import 'package:on_field_work/model/attendance/attendance_log_view_model.dart'; import 'package:on_field_work/model/attendance/attendance_model.dart'; import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart'; import 'package:on_field_work/model/employees/employee_model.dart'; import 'package:on_field_work/model/project_model.dart'; import 'package:on_field_work/model/regularization_log_model.dart'; class AttendanceController extends GetxController { // ------------------ Data Models ------------------ final List attendances = []; final List projects = []; final List employees = []; final List attendanceLogs = []; final List regularizationLogs = []; final List attendenceLogsView = []; // ------------------ Organizations ------------------ final List organizations = []; Organization? selectedOrganization; final RxBool isLoadingOrganizations = false.obs; // ------------------ States ------------------ String selectedTab = 'todaysAttendance'; // ✅ Reactive date range final Rx startDateAttendance = DateTime.now().subtract(const Duration(days: 7)).obs; final Rx endDateAttendance = DateTime.now().subtract(const Duration(days: 1)).obs; final RxBool isLoading = true.obs; final RxBool isLoadingProjects = true.obs; final RxBool isLoadingEmployees = true.obs; final RxBool isLoadingAttendanceLogs = true.obs; final RxBool isLoadingRegularizationLogs = true.obs; final RxBool isLoadingLogView = true.obs; final RxMap uploadingStates = {}.obs; final RxBool showPendingOnly = false.obs; final RxString searchQuery = ''.obs; @override void onInit() { super.onInit(); _initializeDefaults(); } void _initializeDefaults() { _setDefaultDateRange(); } void _setDefaultDateRange() { final DateTime today = DateTime.now(); startDateAttendance.value = today.subtract(const Duration(days: 7)); endDateAttendance.value = today.subtract(const Duration(days: 1)); logSafe( 'Default date range set: ${startDateAttendance.value} to ${endDateAttendance.value}', ); } // ------------------ Computed Filters ------------------ List get filteredEmployees { final String query = searchQuery.value.trim().toLowerCase(); if (query.isEmpty) return employees; return employees .where( (EmployeeModel e) => e.name.toLowerCase().contains(query), ) .toList(); } List get filteredLogs { final String query = searchQuery.value.trim().toLowerCase(); if (query.isEmpty) return attendanceLogs; return attendanceLogs .where( (AttendanceLogModel log) => log.name.toLowerCase().contains(query), ) .toList(); } List get filteredRegularizationLogs { final String query = searchQuery.value.trim().toLowerCase(); if (query.isEmpty) return regularizationLogs; return regularizationLogs .where( (RegularizationLogModel log) => log.name.toLowerCase().contains(query), ) .toList(); } // ------------------ Project & Employee APIs ------------------ 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', ); } Future fetchTodaysAttendance(String? projectId) async { if (projectId == null) return; isLoadingEmployees.value = true; final List? response = await ApiService.getTodaysAttendance( projectId, organizationId: selectedOrganization?.id, ); if (response != null) { employees ..clear() ..addAll( response .map( (dynamic e) => EmployeeModel.fromJson( e as Map, ), ) .toList(), ); for (final EmployeeModel 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; // Keep original return type inference from your ApiService final response = await ApiService.getAssignedOrganizations(projectId); if (response != null) { organizations ..clear() ..addAll(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, String? date, }) async { try { _setUploading(employeeId, true); final XFile? image = await _captureAndPrepareImage( employeeId: employeeId, imageCapture: imageCapture, ); if (imageCapture && image == null) { return false; } final Position? position = await _getCurrentPositionSafely(); if (position == null) return false; final String imageName = imageCapture ? ApiService.generateImageName( employeeId, employees.length + 1, ) : ''; final DateTime effectiveDate = _resolveEffectiveDateForAction(action, employeeId); final DateTime now = DateTime.now(); final String formattedMarkTime = markTime ?? DateFormat('hh:mm a').format(now); final String formattedDate = date ?? DateFormat('yyyy-MM-dd').format(effectiveDate); final bool 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, ); if (result) { logSafe( 'Attendance uploaded for $employeeId, action: $action, date: $formattedDate', ); if (Get.isRegistered()) { final DashboardController dashboardController = Get.find(); await dashboardController.fetchTodaysAttendance(projectId); } } return result; } catch (e, stacktrace) { logSafe( 'Error uploading attendance', level: LogLevel.error, error: e, stackTrace: stacktrace, ); return false; } finally { _setUploading(employeeId, false); } } Future _captureAndPrepareImage({ required String employeeId, required bool imageCapture, }) async { if (!imageCapture) return null; final XFile? rawImage = await ImagePicker().pickImage( source: ImageSource.camera, imageQuality: 80, ); if (rawImage == null) { logSafe( 'Image capture cancelled.', level: LogLevel.warning, ); return null; } final File timestampedFile = await TimestampImageHelper.addTimestamp( imageFile: File(rawImage.path), ); final List? compressedBytes = await compressImageToUnder100KB(timestampedFile); if (compressedBytes == null) { logSafe( 'Image compression failed.', level: LogLevel.error, ); return null; } // FIX: convert List -> Uint8List final Uint8List compressedUint8List = Uint8List.fromList(compressedBytes); final File compressedFile = await saveCompressedImageToFile(compressedUint8List); return XFile(compressedFile.path); } Future _getCurrentPositionSafely() async { final bool permissionGranted = await _handleLocationPermission(); if (!permissionGranted) return null; return Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high, ); } DateTime _resolveEffectiveDateForAction(int action, String employeeId) { final DateTime now = DateTime.now(); if (action != 1) return now; final AttendanceLogModel? log = attendanceLogs.firstWhereOrNull( (AttendanceLogModel log) => log.employeeId == employeeId && log.checkOut == null, ); return log?.checkIn ?? now; } void _setUploading(String employeeId, bool value) { final RxBool? state = uploadingStates[employeeId]; if (state != null) { state.value = value; } else { uploadingStates[employeeId] = value.obs; } } 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 List? response = await ApiService.getAttendanceLogs( projectId, dateFrom: dateFrom, dateTo: dateTo, organizationId: selectedOrganization?.id, ); if (response != null) { attendanceLogs ..clear() ..addAll( response .map( (dynamic e) => AttendanceLogModel.fromJson( e as Map, ), ) .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 Map> groupedLogs = >{}; for (final AttendanceLogModel logItem in attendanceLogs) { final String checkInDate = logItem.checkIn != null ? DateFormat('dd MMM yyyy').format(logItem.checkIn!) : 'Unknown'; groupedLogs.putIfAbsent( checkInDate, () => [], )..add(logItem); } final List>> sortedEntries = groupedLogs.entries.toList() ..sort( (MapEntry> a, MapEntry> b) { if (a.key == 'Unknown') return 1; if (b.key == 'Unknown') return -1; final DateTime dateA = DateFormat('dd MMM yyyy').parse(a.key); final DateTime 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 List? response = await ApiService.getRegularizationLogs( projectId, organizationId: selectedOrganization?.id, ); if (response != null) { regularizationLogs ..clear() ..addAll( response .map( (dynamic e) => RegularizationLogModel.fromJson( e as Map, ), ) .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 List? response = await ApiService.getAttendanceLogView(id); if (response != null) { attendenceLogsView ..clear() ..addAll( response .map( (dynamic e) => AttendanceLogViewModel.fromJson( e as Map, ), ) .toList(), ); attendenceLogsView.sort( (AttendanceLogViewModel a, AttendanceLogViewModel 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); switch (selectedTab) { case 'todaysAttendance': await fetchTodaysAttendance(projectId); break; case 'attendanceLogs': await fetchAttendanceLogs( projectId, dateFrom: startDateAttendance.value, dateTo: endDateAttendance.value, ); 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 DateTime today = DateTime.now(); final DateTimeRange? picked = await showDateRangePicker( context: context, firstDate: DateTime(2022), lastDate: today.subtract(const Duration(days: 1)), initialDateRange: DateTimeRange( start: startDateAttendance.value, end: endDateAttendance.value, ), ); if (picked != null) { startDateAttendance.value = picked.start; endDateAttendance.value = picked.end; logSafe( 'Date range selected: ${startDateAttendance.value} to ${endDateAttendance.value}', ); await controller.fetchAttendanceLogs( Get.find().selectedProject?.id, dateFrom: picked.start, dateTo: picked.end, ); } } }