diff --git a/lib/controller/dashboard/attendance_screen_controller.dart b/lib/controller/dashboard/attendance_screen_controller.dart index bf68af0..46d916a 100644 --- a/lib/controller/dashboard/attendance_screen_controller.dart +++ b/lib/controller/dashboard/attendance_screen_controller.dart @@ -128,6 +128,7 @@ class AttendanceController extends GetxController { String comment = "Marked via mobile app", required int action, bool imageCapture = true, + String? markTime, }) async { try { uploadingStates[employeeId]?.value = true; @@ -170,6 +171,7 @@ class AttendanceController extends GetxController { comment: comment, action: action, imageCapture: imageCapture, + markTime: markTime, ); log.i("Attendance uploaded for $employeeId, action: $action"); diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 3901ada..2d8bac8 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -14,4 +14,7 @@ class ApiEndpoints { static const String getAllEmployees = "/employee/list"; static const String getRoles = "/roles/jobrole"; static const String createEmployee = "/employee/manage-mobile"; + + // Daily Task Screen API Endpoints + static const String getDailyTask = "/task/list"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index abb7bc5..8681c90 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -152,65 +152,66 @@ class ApiService { // ===== Upload Attendance Image ===== - static Future uploadAttendanceImage( - String id, - String employeeId, - XFile? imageFile, - double latitude, - double longitude, { - required String imageName, - required String projectId, - String comment = "", - required int action, - bool imageCapture = true, - }) async { - final now = DateTime.now(); - final body = { - "id": id, - "employeeId": employeeId, - "projectId": projectId, - "markTime": DateFormat('hh:mm a').format(now), - "comment": comment, - "action": action, - "date": DateFormat('yyyy-MM-dd').format(now), - if (imageCapture) "latitude": '$latitude', - if (imageCapture) "longitude": '$longitude', - }; +static Future uploadAttendanceImage( + String id, + String employeeId, + XFile? imageFile, + double latitude, + double longitude, { + required String imageName, + required String projectId, + String comment = "", + required int action, + bool imageCapture = true, + String? markTime, // <-- Optional markTime parameter +}) async { + final now = DateTime.now(); + final body = { + "id": id, + "employeeId": employeeId, + "projectId": projectId, + "markTime": markTime ?? DateFormat('hh:mm a').format(now), + "comment": comment, + "action": action, + "date": DateFormat('yyyy-MM-dd').format(now), + if (imageCapture) "latitude": '$latitude', + if (imageCapture) "longitude": '$longitude', + }; - if (imageCapture && imageFile != null) { - try { - final bytes = await imageFile.readAsBytes(); - final base64Image = base64Encode(bytes); - final fileSize = await imageFile.length(); - final contentType = "image/${imageFile.path.split('.').last}"; + if (imageCapture && imageFile != null) { + try { + final bytes = await imageFile.readAsBytes(); + final base64Image = base64Encode(bytes); + final fileSize = await imageFile.length(); + final contentType = "image/${imageFile.path.split('.').last}"; - body["image"] = { - "fileName": imageName, - "contentType": contentType, - "fileSize": fileSize, - "description": "Employee attendance photo", - "base64Data": base64Image, - }; - } catch (e) { - _log("Image encoding error: $e"); - return false; - } + body["image"] = { + "fileName": imageName, + "contentType": contentType, + "fileSize": fileSize, + "description": "Employee attendance photo", + "base64Data": base64Image, + }; + } catch (e) { + _log("Image encoding error: $e"); + return false; } + } final response = await _postRequest(ApiEndpoints.uploadAttendanceImage, body); - if (response == null) return false; + if (response == null) return false; - final json = jsonDecode(response.body); - if (response.statusCode == 200 && json['success'] == true) { - return true; - } else { - _log("Failed to upload image: ${json['message'] ?? 'Unknown error'}"); - } - - return false; + final json = jsonDecode(response.body); + if (response.statusCode == 200 && json['success'] == true) { + return true; + } else { + _log("Failed to upload image: ${json['message'] ?? 'Unknown error'}"); } + return false; +} + // ===== Utilities ===== static String generateImageName(String employeeId, int count) { @@ -289,4 +290,20 @@ class ApiService { return false; } } + // ===== Daily Tasks API Calls ===== + static Future?> getDailyTasks(String projectId, + {DateTime? dateFrom, DateTime? dateTo}) async { + final query = { + "projectId": projectId, + if (dateFrom != null) + "dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom), + if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo), + }; + + final response = + await _getRequest(ApiEndpoints.getDailyTask, queryParams: query); + return response != null + ? _parseResponse(response, label: 'Daily Tasks') + : null; + } } diff --git a/lib/helpers/utils/attendance_actions.dart b/lib/helpers/utils/attendance_actions.dart index 3b2cdff..927d02c 100644 --- a/lib/helpers/utils/attendance_actions.dart +++ b/lib/helpers/utils/attendance_actions.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; -// Define action texts +/// Threshold for time elapsed (e.g., 48 hours) +const Duration THRESHOLD_DURATION = Duration(hours: 48); + +/// Action text labels class ButtonActions { static const String checkIn = "Check In"; static const String checkOut = "Check Out"; @@ -12,7 +15,7 @@ class ButtonActions { static const String reject = "Reject"; } -// Map action texts to colors +/// Action colors mapping class AttendanceActionColors { static const Map colors = { ButtonActions.checkIn: Colors.green, @@ -22,6 +25,129 @@ class AttendanceActionColors { ButtonActions.approved: Colors.green, ButtonActions.requested: Colors.yellow, ButtonActions.approve: Colors.blueAccent, - ButtonActions.reject: Colors.pink, + ButtonActions.reject: Colors.pink, }; } + +/// Attendance button helper utilities +class AttendanceButtonHelper { + static String getUniqueKey(String employeeId, String logId) { + return '${employeeId}_$logId'; + } + + static bool isLogFromYesterday(DateTime? checkIn, DateTime? checkOut) { + final yesterday = DateTime.now().subtract(const Duration(days: 1)); + final yesterdayOnly = + DateTime(yesterday.year, yesterday.month, yesterday.day); + + return checkIn != null && + checkOut != null && + DateUtils.isSameDay(checkIn, yesterdayOnly) && + DateUtils.isSameDay(checkOut, yesterdayOnly); + } + + static bool isTodayApproved(int activity, DateTime? checkIn) { + final today = DateTime.now(); + return activity == 4 && DateUtils.isSameDay(checkIn, today); + } + + static bool isApprovedButNotToday(int activity, bool isTodayApproved) { + return activity == 4 && !isTodayApproved; + } + + static bool isTimeElapsed(DateTime? time, + [Duration threshold = THRESHOLD_DURATION]) { + if (time == null) return false; + return DateTime.now().difference(time).compareTo(threshold) > 0; + } + + static bool isButtonDisabled({ + required bool isUploading, + required bool isYesterday, + required int activity, + required bool isApprovedButNotToday, + }) { + return isUploading || + isYesterday || + activity == 2 || + activity == 5 || + isApprovedButNotToday; + } + + static String getActionText( + int activity, DateTime? checkIn, DateTime? checkOut) { + switch (activity) { + case 0: + return ButtonActions.checkIn; + case 1: + if (checkOut == null && isTimeElapsed(checkIn)) { + return ButtonActions.requestRegularize; + } + return ButtonActions.checkOut; + case 2: + return ButtonActions.requestRegularize; + case 4: + return ButtonActions.checkIn; + case 5: + return ButtonActions.rejected; + default: + return ButtonActions.checkIn; + } + } + + static Color getButtonColor({ + required bool isYesterday, + required bool isTodayApproved, + required int activity, + }) { + if (isYesterday) return Colors.grey; + if (isTodayApproved) return Colors.green; + + return AttendanceActionColors.colors[ + activity == 0 ? ButtonActions.checkIn : ButtonActions.checkOut] ?? + Colors.grey; + } + + static bool isOlderThanDays(DateTime? date, int days) { + if (date == null) return false; + + // Get today's date with time set to midnight (ignoring the time) + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + + // Compare the date part (ignore time) of the provided date with today's date + final compareDate = DateTime(date.year, date.month, date.day); + final difference = today.difference(compareDate).inDays; + + return difference >= + days; // Return true if the difference is greater than or equal to 'days' + } + + static String getButtonText({ + required int activity, + required DateTime? checkIn, + required DateTime? checkOut, + required bool isTodayApproved, + }) { + if (activity == 5) return ButtonActions.rejected; + if (isTodayApproved) return ButtonActions.checkIn; + if (activity == 4) return ButtonActions.approved; + if (activity == 2) return ButtonActions.requested; + + if (activity == 0) { + if (isTimeElapsed(checkIn)) { + return ButtonActions.requestRegularize; + } + return ButtonActions.checkIn; + } + + if (activity == 1) { + if (checkOut == null && isTimeElapsed(checkIn)) { + return ButtonActions.requestRegularize; + } + return ButtonActions.checkOut; + } + + return ButtonActions.checkOut; + } +} diff --git a/lib/view/dashboard/attendanceScreen.dart b/lib/view/dashboard/attendanceScreen.dart index 6a40650..36d1042 100644 --- a/lib/view/dashboard/attendanceScreen.dart +++ b/lib/view/dashboard/attendanceScreen.dart @@ -624,30 +624,26 @@ class _AttendanceScreenState extends State with UIMixin { ), DataCell( Obx(() { - final uniqueLogKey = '${log.employeeId}_${log.id}'; + final uniqueLogKey = + AttendanceButtonHelper.getUniqueKey(log.employeeId, log.id); final isUploading = attendanceController.uploadingStates[uniqueLogKey]?.value ?? false; - final isYesterday = log.checkIn != null && - log.checkOut != null && - DateUtils.isSameDay(log.checkIn!, - DateTime.now().subtract(Duration(days: 1))) && - DateUtils.isSameDay(log.checkOut!, - DateTime.now().subtract(Duration(days: 1))); - - final isTodayApproved = log.activity == 4 && - DateUtils.isSameDay( - log.checkIn ?? DateTime(2000), DateTime.now()); - + final isYesterday = AttendanceButtonHelper.isLogFromYesterday( + log.checkIn, log.checkOut); + final isTodayApproved = AttendanceButtonHelper.isTodayApproved( + log.activity, log.checkIn); final isApprovedButNotToday = - log.activity == 4 && !isTodayApproved; + AttendanceButtonHelper.isApprovedButNotToday( + log.activity, isTodayApproved); - final isButtonDisabled = isUploading || - isYesterday || - log.activity == 2 || - log.activity == 5 || - isApprovedButNotToday; + final isButtonDisabled = AttendanceButtonHelper.isButtonDisabled( + isUploading: isUploading, + isYesterday: isYesterday, + activity: log.activity, + isApprovedButNotToday: isApprovedButNotToday, + ); return SizedBox( width: 90, @@ -662,8 +658,8 @@ class _AttendanceScreenState extends State with UIMixin { if (attendanceController.selectedProjectId == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text("Please select a project first"), - ), + content: + Text("Please select a project first")), ); attendanceController.uploadingStates[uniqueLogKey] = RxBool(false); @@ -674,55 +670,88 @@ class _AttendanceScreenState extends State with UIMixin { String actionText; bool imageCapture = true; - if (log.activity == 0) { - updatedAction = 0; - actionText = "Check In"; - } else if (log.activity == 1) { - final twoDaysAgo = - DateTime.now().subtract(Duration(days: 2)); - - if (log.checkOut == null && - log.checkIn != null && - log.checkIn!.isBefore(twoDaysAgo)) { + switch (log.activity) { + case 0: + updatedAction = 0; + actionText = ButtonActions.checkIn; + break; + case 1: + if (log.checkOut == null && + AttendanceButtonHelper.isOlderThanDays( + log.checkIn, 2)) { + updatedAction = 2; + actionText = ButtonActions.requestRegularize; + imageCapture = false; + } else if (log.checkOut != null && + AttendanceButtonHelper.isOlderThanDays( + log.checkOut, 2)) { + updatedAction = 2; + actionText = ButtonActions.requestRegularize; + } else { + updatedAction = 1; + actionText = ButtonActions.checkOut; + } + break; + case 2: updatedAction = 2; - actionText = "Request Regularize"; - imageCapture = false; - } else if (log.checkOut != null && - log.checkOut!.isBefore(twoDaysAgo)) { - updatedAction = 2; - actionText = "Request Regularize"; - } else { - updatedAction = 1; - actionText = "Check Out"; - } - } else if (log.activity == 2) { - updatedAction = 2; - actionText = "Request Regularize"; - } else if (isTodayApproved) { - updatedAction = 0; - actionText = "Check In"; - } else { - updatedAction = 0; - actionText = "Unknown Action"; + actionText = ButtonActions.requestRegularize; + break; + case 4: + updatedAction = isTodayApproved ? 0 : 0; + actionText = ButtonActions.checkIn; + break; + default: + updatedAction = 0; + actionText = "Unknown Action"; + break; } - final success = await attendanceController - .captureAndUploadAttendance( - log.id, - log.employeeId, - attendanceController.selectedProjectId!, - comment: actionText, - action: updatedAction, - imageCapture: imageCapture, - ); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(success - ? 'Attendance marked successfully!' - : 'Failed to mark attendance.'), - ), - ); + bool success = false; + if (actionText == ButtonActions.requestRegularize) { + final selectedTime = + await showTimePickerForRegularization( + context: context, + checkInTime: log.checkIn!, + ); + if (selectedTime != null) { + final formattedSelectedTime = + DateFormat("hh:mm a").format(selectedTime); + success = await attendanceController + .captureAndUploadAttendance( + log.id, + log.employeeId, + attendanceController.selectedProjectId!, + comment: actionText, + action: updatedAction, + imageCapture: imageCapture, + markTime: formattedSelectedTime, + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(success + ? '${actionText.toLowerCase()} marked successfully!' + : 'Failed to ${actionText.toLowerCase()}.'), + ), + ); + } + } else { + success = await attendanceController + .captureAndUploadAttendance( + log.id, + log.employeeId, + attendanceController.selectedProjectId!, + comment: actionText, + action: updatedAction, + imageCapture: imageCapture, + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(success + ? '${actionText.toLowerCase()} marked successfully!' + : 'Failed to ${actionText.toLowerCase()}.'), + ), + ); + } attendanceController.uploadingStates[uniqueLogKey] = RxBool(false); @@ -740,13 +769,11 @@ class _AttendanceScreenState extends State with UIMixin { } }, style: ElevatedButton.styleFrom( - backgroundColor: isYesterday - ? Colors.grey - : isTodayApproved - ? Colors.green - : AttendanceActionColors.colors[(log.activity == 0) - ? ButtonActions.checkIn - : ButtonActions.checkOut], + backgroundColor: AttendanceButtonHelper.getButtonColor( + isYesterday: isYesterday, + isTodayApproved: isTodayApproved, + activity: log.activity, + ), padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6), textStyle: const TextStyle(fontSize: 12), @@ -762,42 +789,12 @@ class _AttendanceScreenState extends State with UIMixin { ), ) : Text( - log.activity == 5 - ? ButtonActions.rejected - : isTodayApproved - ? ButtonActions.checkIn - : log.activity == 4 - ? ButtonActions.approved - : log.activity == 2 - ? ButtonActions.requested - : (log.activity == 0 && - !(log.checkIn != null && - log.checkOut != null && - !DateUtils.isSameDay( - log.checkIn!, - DateTime.now()))) - ? ButtonActions.checkIn - : (log.activity == 1 && - log.checkOut != null && - DateTime.now() - .difference( - log.checkOut!) - .inDays <= - 2) - ? ButtonActions.checkOut - : (log.activity == 1 && - log.checkOut == - null && - log.checkIn != null && - log.checkIn!.isBefore( - DateTime.now() - .subtract( - Duration( - days: - 2)))) - ? ButtonActions - .requestRegularize - : ButtonActions.checkOut, + AttendanceButtonHelper.getButtonText( + activity: log.activity, + checkIn: log.checkIn, + checkOut: log.checkOut, + isTodayApproved: isTodayApproved, + ), ), ), ); @@ -1076,4 +1073,36 @@ class _AttendanceScreenState extends State with UIMixin { ], ); } + + Future showTimePickerForRegularization({ + required BuildContext context, + required DateTime checkInTime, + }) async { + final pickedTime = await showTimePicker( + context: context, + initialTime: TimeOfDay.fromDateTime(DateTime.now()), + ); + + if (pickedTime != null) { + final selectedDateTime = DateTime( + checkInTime.year, + checkInTime.month, + checkInTime.day, + pickedTime.hour, + pickedTime.minute, + ); + + // Ensure selected time is after check-in time + if (selectedDateTime.isAfter(checkInTime)) { + return selectedDateTime; + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Please select a time after check-in time.")), + ); + return null; + } + } + return null; + } }