Merge pull request 'Enhance attendance functionality by adding optional markTime parameter in uploadAttendanceImage method, and refactor attendance button logic using AttendanceButtonHelper for improved readability and maintainability.' (#18) from Vaibhav_Bug-#211 into main

Reviewed-on: #18
This commit is contained in:
vaibhav.surve 2025-05-10 09:50:48 +00:00
commit 809c048de6
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",
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");

View File

@ -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";
}

View File

@ -152,65 +152,66 @@ class ApiService {
// ===== Upload Attendance Image =====
static Future<bool> 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<bool> 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<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';
// 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<String, Color> 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;
}
}

View File

@ -624,30 +624,26 @@ class _AttendanceScreenState extends State<AttendanceScreen> 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<AttendanceScreen> 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<AttendanceScreen> 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<AttendanceScreen> 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<AttendanceScreen> 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<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;
}
}