432 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			432 lines
		
	
	
		
			14 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';
 | |
| 
 | |
| class AttendanceActionButton extends StatefulWidget {
 | |
|   final dynamic employee;
 | |
|   final AttendanceController attendanceController;
 | |
| 
 | |
|   const AttendanceActionButton({
 | |
|     Key? key,
 | |
|     required this.employee,
 | |
|     required this.attendanceController,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<AttendanceActionButton> createState() => _AttendanceActionButtonState();
 | |
| }
 | |
| 
 | |
| Future<String?> _showCommentBottomSheet(
 | |
|     BuildContext context, String actionText) async {
 | |
|   final TextEditingController commentController = TextEditingController();
 | |
|   String? errorText;
 | |
|   Get.find<ProjectController>().selectedProject?.id;
 | |
|   return showModalBottomSheet<String>(
 | |
|     context: context,
 | |
|     isScrollControlled: true,
 | |
|     backgroundColor: Colors.white,
 | |
|     shape: const RoundedRectangleBorder(
 | |
|       borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
 | |
|     ),
 | |
|     builder: (context) {
 | |
|       return StatefulBuilder(
 | |
|         builder: (context, setModalState) {
 | |
|           return Padding(
 | |
|             padding: EdgeInsets.only(
 | |
|               left: 16,
 | |
|               right: 16,
 | |
|               top: 24,
 | |
|               bottom: MediaQuery.of(context).viewInsets.bottom + 24,
 | |
|             ),
 | |
|             child: Column(
 | |
|               mainAxisSize: MainAxisSize.min,
 | |
|               children: [
 | |
|                 Text(
 | |
|                   'Add Comment for ${capitalizeFirstLetter(actionText)}',
 | |
|                   style: const TextStyle(
 | |
|                     fontWeight: FontWeight.bold,
 | |
|                     fontSize: 16,
 | |
|                   ),
 | |
|                 ),
 | |
|                 const SizedBox(height: 16),
 | |
|                 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);
 | |
|                     }
 | |
|                   },
 | |
|                 ),
 | |
|                 const SizedBox(height: 16),
 | |
|                 Row(
 | |
|                   children: [
 | |
|                     Expanded(
 | |
|                       child: OutlinedButton(
 | |
|                         onPressed: () => Navigator.of(context).pop(),
 | |
|                         child: const Text('Cancel'),
 | |
|                       ),
 | |
|                     ),
 | |
|                     const SizedBox(width: 12),
 | |
|                     Expanded(
 | |
|                       child: ElevatedButton(
 | |
|                         onPressed: () {
 | |
|                           final comment = commentController.text.trim();
 | |
|                           if (comment.isEmpty) {
 | |
|                             setModalState(() {
 | |
|                               errorText = 'Comment cannot be empty.';
 | |
|                             });
 | |
|                             return;
 | |
|                           }
 | |
|                           Navigator.of(context).pop(comment);
 | |
|                         },
 | |
|                         child: const Text('Submit'),
 | |
|                       ),
 | |
|                     ),
 | |
|                   ],
 | |
|                 ),
 | |
|               ],
 | |
|             ),
 | |
|           );
 | |
|         },
 | |
|       );
 | |
|     },
 | |
|   );
 | |
| }
 | |
| 
 | |
| 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((_) {
 | |
|       if (!widget.attendanceController.uploadingStates
 | |
|           .containsKey(uniqueLogKey)) {
 | |
|         widget.attendanceController.uploadingStates[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;
 | |
| 
 | |
|     // ✅ New condition: Yesterday Check-In + CheckOut action
 | |
|     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 formattedSelectedTime =
 | |
|             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: formattedSelectedTime,
 | |
|         );
 | |
|       }
 | |
|     } else if (selectedTime != null) {
 | |
|       // ✅ If selectedTime was picked in the new condition
 | |
|       final formattedSelectedTime = 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: formattedSelectedTime,
 | |
|       );
 | |
|     } 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({
 | |
|     Key? key,
 | |
|     required this.isUploading,
 | |
|     required this.isButtonDisabled,
 | |
|     required this.buttonText,
 | |
|     required this.buttonColor,
 | |
|     required this.onPressed,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return SizedBox(
 | |
|       height: 30,
 | |
|       child: ElevatedButton(
 | |
|         onPressed: isButtonDisabled ? null : 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),
 | |
|                     ),
 | |
|                   ),
 | |
|                 ],
 | |
|               ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | 
