404 lines
12 KiB
Dart
404 lines
12 KiB
Dart
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<AttendanceModel> attendances = [];
|
|
List<ProjectModel> projects = [];
|
|
List<EmployeeModel> employees = [];
|
|
List<AttendanceLogModel> attendanceLogs = [];
|
|
List<RegularizationLogModel> regularizationLogs = [];
|
|
List<AttendanceLogViewModel> 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<String, RxBool> uploadingStates = <String, RxBool>{}.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<bool> _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<void> 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<void> 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<void> 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<void> 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<bool> 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<void> 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<void> 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<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
|
|
final groupedLogs = <String, List<AttendanceLogModel>>{};
|
|
|
|
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<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
|
|
|
|
log.i("Logs grouped and sorted by check-in date.");
|
|
return sortedMap;
|
|
}
|
|
|
|
/// Fetches regularization logs for a project.
|
|
Future<void> 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<void> 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;
|
|
}
|
|
}
|