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:
commit
809c048de6
@ -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");
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user