marco.pms.mobileapp/lib/controller/attendance/attendance_screen_controller.dart
Vaibhav Surve 03e3e7b5db feat: Enhance Dashboard with Attendance and Infra Projects
- Added employee attendance fetching in DashboardController.
- Introduced loading state for employees in the dashboard.
- Updated API endpoints to include attendance for the dashboard.
- Created a new InfraProjectsMainScreen with tab navigation for task planning and progress reporting.
- Improved UI components for better user experience in the dashboard.
- Refactored project selection and quick actions in the dashboard.
- Added permission constants for infrastructure projects.
2025-12-03 13:09:48 +05:30

585 lines
17 KiB
Dart

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<AttendanceModel> attendances = <AttendanceModel>[];
final List<ProjectModel> projects = <ProjectModel>[];
final List<EmployeeModel> employees = <EmployeeModel>[];
final List<AttendanceLogModel> attendanceLogs = <AttendanceLogModel>[];
final List<RegularizationLogModel> regularizationLogs =
<RegularizationLogModel>[];
final List<AttendanceLogViewModel> attendenceLogsView =
<AttendanceLogViewModel>[];
// ------------------ Organizations ------------------
final List<Organization> organizations = <Organization>[];
Organization? selectedOrganization;
final RxBool isLoadingOrganizations = false.obs;
// ------------------ States ------------------
String selectedTab = 'todaysAttendance';
// ✅ Reactive date range
final Rx<DateTime> startDateAttendance =
DateTime.now().subtract(const Duration(days: 7)).obs;
final Rx<DateTime> 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<String, RxBool> uploadingStates = <String, RxBool>{}.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<EmployeeModel> 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<AttendanceLogModel> 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<RegularizationLogModel> 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<void> refreshDataFromNotification({String? projectId}) async {
projectId ??= Get.find<ProjectController>().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<void> fetchTodaysAttendance(String? projectId) async {
if (projectId == null) return;
isLoadingEmployees.value = true;
final List<dynamic>? response = await ApiService.getTodaysAttendance(
projectId,
organizationId: selectedOrganization?.id,
);
if (response != null) {
employees
..clear()
..addAll(
response
.map<EmployeeModel>(
(dynamic e) => EmployeeModel.fromJson(
e as Map<String, dynamic>,
),
)
.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<void> 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<bool> 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<DashboardController>()) {
final DashboardController dashboardController =
Get.find<DashboardController>();
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<XFile?> _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<int>? compressedBytes =
await compressImageToUnder100KB(timestampedFile);
if (compressedBytes == null) {
logSafe(
'Image compression failed.',
level: LogLevel.error,
);
return null;
}
// FIX: convert List<int> -> Uint8List
final Uint8List compressedUint8List = Uint8List.fromList(compressedBytes);
final File compressedFile =
await saveCompressedImageToFile(compressedUint8List);
return XFile(compressedFile.path);
}
Future<Position?> _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<bool> _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<void> fetchAttendanceLogs(
String? projectId, {
DateTime? dateFrom,
DateTime? dateTo,
}) async {
if (projectId == null) return;
isLoadingAttendanceLogs.value = true;
final List<dynamic>? response = await ApiService.getAttendanceLogs(
projectId,
dateFrom: dateFrom,
dateTo: dateTo,
organizationId: selectedOrganization?.id,
);
if (response != null) {
attendanceLogs
..clear()
..addAll(
response
.map<AttendanceLogModel>(
(dynamic e) => AttendanceLogModel.fromJson(
e as Map<String, dynamic>,
),
)
.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<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
final Map<String, List<AttendanceLogModel>> groupedLogs =
<String, List<AttendanceLogModel>>{};
for (final AttendanceLogModel logItem in attendanceLogs) {
final String checkInDate = logItem.checkIn != null
? DateFormat('dd MMM yyyy').format(logItem.checkIn!)
: 'Unknown';
groupedLogs.putIfAbsent(
checkInDate,
() => <AttendanceLogModel>[],
)..add(logItem);
}
final List<MapEntry<String, List<AttendanceLogModel>>> sortedEntries =
groupedLogs.entries.toList()
..sort(
(MapEntry<String, List<AttendanceLogModel>> a,
MapEntry<String, List<AttendanceLogModel>> 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<String, List<AttendanceLogModel>>.fromEntries(
sortedEntries,
);
}
// ------------------ Regularization Logs ------------------
Future<void> fetchRegularizationLogs(String? projectId) async {
if (projectId == null) return;
isLoadingRegularizationLogs.value = true;
final List<dynamic>? response = await ApiService.getRegularizationLogs(
projectId,
organizationId: selectedOrganization?.id,
);
if (response != null) {
regularizationLogs
..clear()
..addAll(
response
.map<RegularizationLogModel>(
(dynamic e) => RegularizationLogModel.fromJson(
e as Map<String, dynamic>,
),
)
.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<void> fetchLogsView(String? id) async {
if (id == null) return;
isLoadingLogView.value = true;
final List<dynamic>? response = await ApiService.getAttendanceLogView(id);
if (response != null) {
attendenceLogsView
..clear()
..addAll(
response
.map<AttendanceLogViewModel>(
(dynamic e) => AttendanceLogViewModel.fromJson(
e as Map<String, dynamic>,
),
)
.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<void> loadAttendanceData(String projectId) async {
isLoading.value = true;
await fetchProjectData(projectId);
isLoading.value = false;
}
Future<void> 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<void> 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<ProjectController>().selectedProject?.id,
dateFrom: picked.start,
dateTo: picked.end,
);
}
}
}