marco.pms.mobileapp/lib/model/attendance/attendence_action_button.dart

418 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
import 'package:marco/helpers/utils/attendance_actions.dart';
import 'package:marco/controller/project_controller.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
class AttendanceActionButton extends StatefulWidget {
final dynamic employee;
final AttendanceController attendanceController;
const AttendanceActionButton({
super.key,
required this.employee,
required this.attendanceController,
});
@override
State<AttendanceActionButton> createState() => _AttendanceActionButtonState();
}
Future<String?> _showCommentBottomSheet(
BuildContext context, String actionText) async {
final TextEditingController commentController = TextEditingController();
String? errorText;
return showModalBottomSheet<String>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) {
return StatefulBuilder(
builder: (context, setModalState) {
void submit() {
final comment = commentController.text.trim();
if (comment.isEmpty) {
setModalState(() => errorText = 'Comment cannot be empty.');
return;
}
Navigator.of(context).pop(comment);
}
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: BaseBottomSheet(
title: 'Add Comment for ${capitalizeFirstLetter(actionText)}',
onCancel: () => Navigator.of(context).pop(),
onSubmit: submit,
isSubmitting: false,
submitText: 'Submit',
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: commentController,
maxLines: 4,
decoration: InputDecoration(
hintText: 'Type your comment here...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade100,
errorText: errorText,
),
onChanged: (_) {
if (errorText != null) {
setModalState(() => errorText = null);
}
},
),
],
),
),
);
},
);
},
);
}
String capitalizeFirstLetter(String text) {
if (text.isEmpty) return text;
return text[0].toUpperCase() + text.substring(1);
}
class _AttendanceActionButtonState extends State<AttendanceActionButton> {
late final String uniqueLogKey;
@override
void initState() {
super.initState();
uniqueLogKey = AttendanceButtonHelper.getUniqueKey(
widget.employee.employeeId,
widget.employee.id,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.attendanceController.uploadingStates.putIfAbsent(
uniqueLogKey,
() => false.obs,
);
});
}
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,
);
final now = DateTime.now();
if (selectedDateTime.isBefore(checkInTime)) {
showAppSnackbar(
title: "Invalid Time",
message: "Time must be after check-in.",
type: SnackbarType.warning,
);
return null;
} else if (selectedDateTime.isAfter(now)) {
showAppSnackbar(
title: "Invalid Time",
message: "Future time is not allowed.",
type: SnackbarType.warning,
);
return null;
}
return selectedDateTime;
}
return null;
}
void _handleButtonPressed(BuildContext context) async {
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = true;
final projectController = Get.find<ProjectController>();
final selectedProjectId = projectController.selectedProject?.id;
if (selectedProjectId == null) {
showAppSnackbar(
title: "Project Required",
message: "Please select a project first",
type: SnackbarType.error,
);
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = false;
return;
}
int updatedAction;
String actionText;
bool imageCapture = true;
switch (widget.employee.activity) {
case 0:
updatedAction = 0;
actionText = ButtonActions.checkIn;
break;
case 1:
if (widget.employee.checkOut == null &&
AttendanceButtonHelper.isOlderThanDays(
widget.employee.checkIn, 2)) {
updatedAction = 2;
actionText = ButtonActions.requestRegularize;
imageCapture = false;
} else if (widget.employee.checkOut != null &&
AttendanceButtonHelper.isOlderThanDays(
widget.employee.checkOut, 2)) {
updatedAction = 2;
actionText = ButtonActions.requestRegularize;
} else {
updatedAction = 1;
actionText = ButtonActions.checkOut;
}
break;
case 2:
updatedAction = 2;
actionText = ButtonActions.requestRegularize;
break;
case 4:
updatedAction = 0;
actionText = ButtonActions.checkIn;
break;
default:
updatedAction = 0;
actionText = "Unknown Action";
break;
}
DateTime? selectedTime;
final isYesterdayCheckIn = widget.employee.checkIn != null &&
DateUtils.isSameDay(
widget.employee.checkIn,
DateTime.now().subtract(const Duration(days: 1)),
);
if (isYesterdayCheckIn &&
widget.employee.checkOut == null &&
actionText == ButtonActions.checkOut) {
selectedTime = await showTimePickerForRegularization(
context: context,
checkInTime: widget.employee.checkIn!,
);
if (selectedTime == null) {
widget.attendanceController.uploadingStates[uniqueLogKey]?.value =
false;
return;
}
}
final userComment = await _showCommentBottomSheet(context, actionText);
if (userComment == null || userComment.isEmpty) {
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = false;
return;
}
bool success = false;
if (actionText == ButtonActions.requestRegularize) {
final regularizeTime = selectedTime ??
await showTimePickerForRegularization(
context: context,
checkInTime: widget.employee.checkIn!,
);
if (regularizeTime != null) {
final formattedTime = DateFormat("hh:mm a").format(regularizeTime);
success = await widget.attendanceController.captureAndUploadAttendance(
widget.employee.id,
widget.employee.employeeId,
selectedProjectId,
comment: userComment,
action: updatedAction,
imageCapture: imageCapture,
markTime: formattedTime,
);
}
} else if (selectedTime != null) {
final formattedTime = DateFormat("hh:mm a").format(selectedTime);
success = await widget.attendanceController.captureAndUploadAttendance(
widget.employee.id,
widget.employee.employeeId,
selectedProjectId,
comment: userComment,
action: updatedAction,
imageCapture: imageCapture,
markTime: formattedTime,
);
} else {
success = await widget.attendanceController.captureAndUploadAttendance(
widget.employee.id,
widget.employee.employeeId,
selectedProjectId,
comment: userComment,
action: updatedAction,
imageCapture: imageCapture,
);
}
showAppSnackbar(
title: success ? '${capitalizeFirstLetter(actionText)} Success' : 'Error',
message: success
? '${capitalizeFirstLetter(actionText)} marked successfully!'
: 'Failed to ${actionText.toLowerCase()}',
type: success ? SnackbarType.success : SnackbarType.error,
);
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = false;
if (success) {
widget.attendanceController.fetchEmployeesByProject(selectedProjectId);
widget.attendanceController.fetchAttendanceLogs(selectedProjectId);
await widget.attendanceController.fetchRegularizationLogs(selectedProjectId);
await widget.attendanceController.fetchProjectData(selectedProjectId);
widget.attendanceController.update();
}
}
@override
Widget build(BuildContext context) {
return Obx(() {
final isUploading =
widget.attendanceController.uploadingStates[uniqueLogKey]?.value ??
false;
final isYesterday = AttendanceButtonHelper.isLogFromYesterday(
widget.employee.checkIn,
widget.employee.checkOut,
);
final isTodayApproved = AttendanceButtonHelper.isTodayApproved(
widget.employee.activity,
widget.employee.checkIn,
);
final isApprovedButNotToday = AttendanceButtonHelper.isApprovedButNotToday(
widget.employee.activity,
isTodayApproved,
);
final isButtonDisabled = AttendanceButtonHelper.isButtonDisabled(
isUploading: isUploading,
isYesterday: isYesterday,
activity: widget.employee.activity,
isApprovedButNotToday: isApprovedButNotToday,
);
final buttonText = AttendanceButtonHelper.getButtonText(
activity: widget.employee.activity,
checkIn: widget.employee.checkIn,
checkOut: widget.employee.checkOut,
isTodayApproved: isTodayApproved,
);
final buttonColor = AttendanceButtonHelper.getButtonColor(
isYesterday: isYesterday,
isTodayApproved: isTodayApproved,
activity: widget.employee.activity,
);
return AttendanceActionButtonUI(
isUploading: isUploading,
isButtonDisabled: isButtonDisabled,
buttonText: buttonText,
buttonColor: buttonColor,
onPressed: isButtonDisabled ? null : () => _handleButtonPressed(context),
);
});
}
}
class AttendanceActionButtonUI extends StatelessWidget {
final bool isUploading;
final bool isButtonDisabled;
final String buttonText;
final Color buttonColor;
final VoidCallback? onPressed;
const AttendanceActionButtonUI({
super.key,
required this.isUploading,
required this.isButtonDisabled,
required this.buttonText,
required this.buttonColor,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 30,
child: ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: buttonColor,
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
textStyle: const TextStyle(fontSize: 12),
),
child: isUploading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (buttonText.toLowerCase() == 'approved') ...[
const Icon(Icons.check, size: 16, color: Colors.green),
const SizedBox(width: 4),
] else if (buttonText.toLowerCase() == 'rejected') ...[
const Icon(Icons.close, size: 16, color: Colors.red),
const SizedBox(width: 4),
] else if (buttonText.toLowerCase() == 'requested') ...[
const Icon(Icons.hourglass_top,
size: 16, color: Colors.orange),
const SizedBox(width: 4),
],
Flexible(
child: Text(
buttonText,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12),
),
),
],
),
),
);
}
}