Enhance attendance functionality by adding optional markTime parameter in uploadAttendanceImage method, and refactor attendance button logic using AttendanceButtonHelper for improved readability and maintainability.

This commit is contained in:
Vaibhav Surve 2025-05-10 15:19:36 +05:30
parent 4ce22b149f
commit 23de99432a
5 changed files with 339 additions and 162 deletions

View File

@ -128,6 +128,7 @@ class AttendanceController extends GetxController {
String comment = "Marked via mobile app", String comment = "Marked via mobile app",
required int action, required int action,
bool imageCapture = true, bool imageCapture = true,
String? markTime,
}) async { }) async {
try { try {
uploadingStates[employeeId]?.value = true; uploadingStates[employeeId]?.value = true;
@ -170,6 +171,7 @@ class AttendanceController extends GetxController {
comment: comment, comment: comment,
action: action, action: action,
imageCapture: imageCapture, imageCapture: imageCapture,
markTime: markTime,
); );
log.i("Attendance uploaded for $employeeId, action: $action"); log.i("Attendance uploaded for $employeeId, action: $action");

View File

@ -14,4 +14,7 @@ class ApiEndpoints {
static const String getAllEmployees = "/employee/list"; static const String getAllEmployees = "/employee/list";
static const String getRoles = "/roles/jobrole"; static const String getRoles = "/roles/jobrole";
static const String createEmployee = "/employee/manage-mobile"; static const String createEmployee = "/employee/manage-mobile";
// Daily Task Screen API Endpoints
static const String getDailyTask = "/task/list";
} }

View File

@ -152,65 +152,66 @@ class ApiService {
// ===== Upload Attendance Image ===== // ===== Upload Attendance Image =====
static Future<bool> uploadAttendanceImage( static Future<bool> uploadAttendanceImage(
String id, String id,
String employeeId, String employeeId,
XFile? imageFile, XFile? imageFile,
double latitude, double latitude,
double longitude, { double longitude, {
required String imageName, required String imageName,
required String projectId, required String projectId,
String comment = "", String comment = "",
required int action, required int action,
bool imageCapture = true, bool imageCapture = true,
}) async { String? markTime, // <-- Optional markTime parameter
final now = DateTime.now(); }) async {
final body = { final now = DateTime.now();
"id": id, final body = {
"employeeId": employeeId, "id": id,
"projectId": projectId, "employeeId": employeeId,
"markTime": DateFormat('hh:mm a').format(now), "projectId": projectId,
"comment": comment, "markTime": markTime ?? DateFormat('hh:mm a').format(now),
"action": action, "comment": comment,
"date": DateFormat('yyyy-MM-dd').format(now), "action": action,
if (imageCapture) "latitude": '$latitude', "date": DateFormat('yyyy-MM-dd').format(now),
if (imageCapture) "longitude": '$longitude', if (imageCapture) "latitude": '$latitude',
}; if (imageCapture) "longitude": '$longitude',
};
if (imageCapture && imageFile != null) { if (imageCapture && imageFile != null) {
try { try {
final bytes = await imageFile.readAsBytes(); final bytes = await imageFile.readAsBytes();
final base64Image = base64Encode(bytes); final base64Image = base64Encode(bytes);
final fileSize = await imageFile.length(); final fileSize = await imageFile.length();
final contentType = "image/${imageFile.path.split('.').last}"; final contentType = "image/${imageFile.path.split('.').last}";
body["image"] = { body["image"] = {
"fileName": imageName, "fileName": imageName,
"contentType": contentType, "contentType": contentType,
"fileSize": fileSize, "fileSize": fileSize,
"description": "Employee attendance photo", "description": "Employee attendance photo",
"base64Data": base64Image, "base64Data": base64Image,
}; };
} catch (e) { } catch (e) {
_log("Image encoding error: $e"); _log("Image encoding error: $e");
return false; return false;
}
} }
}
final response = final response =
await _postRequest(ApiEndpoints.uploadAttendanceImage, body); await _postRequest(ApiEndpoints.uploadAttendanceImage, body);
if (response == null) return false; if (response == null) return false;
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
if (response.statusCode == 200 && json['success'] == true) { if (response.statusCode == 200 && json['success'] == true) {
return true; return true;
} else { } else {
_log("Failed to upload image: ${json['message'] ?? 'Unknown error'}"); _log("Failed to upload image: ${json['message'] ?? 'Unknown error'}");
}
return false;
} }
return false;
}
// ===== Utilities ===== // ===== Utilities =====
static String generateImageName(String employeeId, int count) { static String generateImageName(String employeeId, int count) {
@ -289,4 +290,20 @@ class ApiService {
return false; return false;
} }
} }
// ===== Daily Tasks API Calls =====
static Future<List<dynamic>?> 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;
}
} }

View File

@ -1,6 +1,9 @@
import 'package:flutter/material.dart'; 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 { class ButtonActions {
static const String checkIn = "Check In"; static const String checkIn = "Check In";
static const String checkOut = "Check Out"; static const String checkOut = "Check Out";
@ -12,7 +15,7 @@ class ButtonActions {
static const String reject = "Reject"; static const String reject = "Reject";
} }
// Map action texts to colors /// Action colors mapping
class AttendanceActionColors { class AttendanceActionColors {
static const Map<String, Color> colors = { static const Map<String, Color> colors = {
ButtonActions.checkIn: Colors.green, ButtonActions.checkIn: Colors.green,
@ -22,6 +25,129 @@ class AttendanceActionColors {
ButtonActions.approved: Colors.green, ButtonActions.approved: Colors.green,
ButtonActions.requested: Colors.yellow, ButtonActions.requested: Colors.yellow,
ButtonActions.approve: Colors.blueAccent, 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;
}
}

View File

@ -624,30 +624,26 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
), ),
DataCell( DataCell(
Obx(() { Obx(() {
final uniqueLogKey = '${log.employeeId}_${log.id}'; final uniqueLogKey =
AttendanceButtonHelper.getUniqueKey(log.employeeId, log.id);
final isUploading = final isUploading =
attendanceController.uploadingStates[uniqueLogKey]?.value ?? attendanceController.uploadingStates[uniqueLogKey]?.value ??
false; false;
final isYesterday = log.checkIn != null && final isYesterday = AttendanceButtonHelper.isLogFromYesterday(
log.checkOut != null && log.checkIn, log.checkOut);
DateUtils.isSameDay(log.checkIn!, final isTodayApproved = AttendanceButtonHelper.isTodayApproved(
DateTime.now().subtract(Duration(days: 1))) && log.activity, log.checkIn);
DateUtils.isSameDay(log.checkOut!,
DateTime.now().subtract(Duration(days: 1)));
final isTodayApproved = log.activity == 4 &&
DateUtils.isSameDay(
log.checkIn ?? DateTime(2000), DateTime.now());
final isApprovedButNotToday = final isApprovedButNotToday =
log.activity == 4 && !isTodayApproved; AttendanceButtonHelper.isApprovedButNotToday(
log.activity, isTodayApproved);
final isButtonDisabled = isUploading || final isButtonDisabled = AttendanceButtonHelper.isButtonDisabled(
isYesterday || isUploading: isUploading,
log.activity == 2 || isYesterday: isYesterday,
log.activity == 5 || activity: log.activity,
isApprovedButNotToday; isApprovedButNotToday: isApprovedButNotToday,
);
return SizedBox( return SizedBox(
width: 90, width: 90,
@ -662,8 +658,8 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
if (attendanceController.selectedProjectId == null) { if (attendanceController.selectedProjectId == null) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text("Please select a project first"), content:
), Text("Please select a project first")),
); );
attendanceController.uploadingStates[uniqueLogKey] = attendanceController.uploadingStates[uniqueLogKey] =
RxBool(false); RxBool(false);
@ -674,55 +670,88 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
String actionText; String actionText;
bool imageCapture = true; bool imageCapture = true;
if (log.activity == 0) { switch (log.activity) {
updatedAction = 0; case 0:
actionText = "Check In"; updatedAction = 0;
} else if (log.activity == 1) { actionText = ButtonActions.checkIn;
final twoDaysAgo = break;
DateTime.now().subtract(Duration(days: 2)); case 1:
if (log.checkOut == null &&
if (log.checkOut == null && AttendanceButtonHelper.isOlderThanDays(
log.checkIn != null && log.checkIn, 2)) {
log.checkIn!.isBefore(twoDaysAgo)) { 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; updatedAction = 2;
actionText = "Request Regularize"; actionText = ButtonActions.requestRegularize;
imageCapture = false; break;
} else if (log.checkOut != null && case 4:
log.checkOut!.isBefore(twoDaysAgo)) { updatedAction = isTodayApproved ? 0 : 0;
updatedAction = 2; actionText = ButtonActions.checkIn;
actionText = "Request Regularize"; break;
} else { default:
updatedAction = 1; updatedAction = 0;
actionText = "Check Out"; actionText = "Unknown Action";
} break;
} else if (log.activity == 2) {
updatedAction = 2;
actionText = "Request Regularize";
} else if (isTodayApproved) {
updatedAction = 0;
actionText = "Check In";
} else {
updatedAction = 0;
actionText = "Unknown Action";
} }
final success = await attendanceController bool success = false;
.captureAndUploadAttendance( if (actionText == ButtonActions.requestRegularize) {
log.id, final selectedTime =
log.employeeId, await showTimePickerForRegularization(
attendanceController.selectedProjectId!, context: context,
comment: actionText, checkInTime: log.checkIn!,
action: updatedAction, );
imageCapture: imageCapture, if (selectedTime != null) {
); final formattedSelectedTime =
DateFormat("hh:mm a").format(selectedTime);
ScaffoldMessenger.of(context).showSnackBar( success = await attendanceController
SnackBar( .captureAndUploadAttendance(
content: Text(success log.id,
? 'Attendance marked successfully!' log.employeeId,
: 'Failed to mark attendance.'), 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] = attendanceController.uploadingStates[uniqueLogKey] =
RxBool(false); RxBool(false);
@ -740,13 +769,11 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
} }
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: isYesterday backgroundColor: AttendanceButtonHelper.getButtonColor(
? Colors.grey isYesterday: isYesterday,
: isTodayApproved isTodayApproved: isTodayApproved,
? Colors.green activity: log.activity,
: AttendanceActionColors.colors[(log.activity == 0) ),
? ButtonActions.checkIn
: ButtonActions.checkOut],
padding: padding:
const EdgeInsets.symmetric(vertical: 4, horizontal: 6), const EdgeInsets.symmetric(vertical: 4, horizontal: 6),
textStyle: const TextStyle(fontSize: 12), textStyle: const TextStyle(fontSize: 12),
@ -762,42 +789,12 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
), ),
) )
: Text( : Text(
log.activity == 5 AttendanceButtonHelper.getButtonText(
? ButtonActions.rejected activity: log.activity,
: isTodayApproved checkIn: log.checkIn,
? ButtonActions.checkIn checkOut: log.checkOut,
: log.activity == 4 isTodayApproved: isTodayApproved,
? 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,
), ),
), ),
); );
@ -1076,4 +1073,36 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
], ],
); );
} }
Future<DateTime?> 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;
}
} }