- Added a floating action button to the Layout widget for better accessibility. - Updated the left bar navigation items for clarity and consistency. - Introduced Daily Progress Report and Daily Task Planning screens with comprehensive UI. - Implemented filtering and refreshing functionalities in task planning. - Improved user experience with better spacing and layout adjustments. - Updated pubspec.yaml to include new dependencies for image handling and path management.
385 lines
12 KiB
Dart
385 lines
12 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';
|
|
|
|
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;
|
|
|
|
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,
|
|
);
|
|
|
|
if (selectedDateTime.isAfter(checkInTime)) {
|
|
return selectedDateTime;
|
|
} else {
|
|
showAppSnackbar(
|
|
title: "Invalid Time",
|
|
message: "Please select a time after check-in time.",
|
|
type: SnackbarType.warning,
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
void _handleButtonPressed(BuildContext context) async {
|
|
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = true;
|
|
|
|
if (widget.attendanceController.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;
|
|
}
|
|
|
|
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 selectedTime = await showTimePickerForRegularization(
|
|
context: context,
|
|
checkInTime: widget.employee.checkIn!,
|
|
);
|
|
if (selectedTime != null) {
|
|
final formattedSelectedTime =
|
|
DateFormat("hh:mm a").format(selectedTime);
|
|
success = await widget.attendanceController.captureAndUploadAttendance(
|
|
widget.employee.id,
|
|
widget.employee.employeeId,
|
|
widget.attendanceController.selectedProjectId!,
|
|
comment: userComment,
|
|
action: updatedAction,
|
|
imageCapture: imageCapture,
|
|
markTime: formattedSelectedTime,
|
|
);
|
|
}
|
|
} else {
|
|
success = await widget.attendanceController.captureAndUploadAttendance(
|
|
widget.employee.id,
|
|
widget.employee.employeeId,
|
|
widget.attendanceController.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(
|
|
widget.attendanceController.selectedProjectId!);
|
|
widget.attendanceController
|
|
.fetchAttendanceLogs(widget.attendanceController.selectedProjectId!);
|
|
await widget.attendanceController.fetchRegularizationLogs(
|
|
widget.attendanceController.selectedProjectId!);
|
|
await widget.attendanceController
|
|
.fetchProjectData(widget.attendanceController.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),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|