Refactor code structure for improved readability and maintainability, Changed the Attendance Screen UI
This commit is contained in:
parent
5c2c2995ef
commit
8408b67aa0
293
lib/model/attendance/attendence_action_button.dart
Normal file
293
lib/model/attendance/attendence_action_button.dart
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:intl/intl.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();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AttendanceActionButtonState extends State<AttendanceActionButton> {
|
||||||
|
late final String uniqueLogKey;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
uniqueLogKey = AttendanceButtonHelper.getUniqueKey(
|
||||||
|
widget.employee.employeeId, widget.employee.id);
|
||||||
|
|
||||||
|
// Defer the Rx initialization after first frame to avoid setState during build
|
||||||
|
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 {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text("Please select a time after check-in time.")),
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleButtonPressed(BuildContext context) async {
|
||||||
|
// Set uploading state true safely
|
||||||
|
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = true;
|
||||||
|
|
||||||
|
if (widget.attendanceController.selectedProjectId == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text("Please select a project first"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: 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 widget.attendanceController.captureAndUploadAttendance(
|
||||||
|
widget.employee.id,
|
||||||
|
widget.employee.employeeId,
|
||||||
|
widget.attendanceController.selectedProjectId!,
|
||||||
|
comment: actionText,
|
||||||
|
action: updatedAction,
|
||||||
|
imageCapture: imageCapture,
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(success
|
||||||
|
? '${actionText.toLowerCase()} marked successfully!'
|
||||||
|
: 'Failed to ${actionText.toLowerCase()}'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
158
lib/model/attendance/info_card.dart
Normal file
158
lib/model/attendance/info_card.dart
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
typedef ItemWidgetBuilder<T> = Widget Function(BuildContext context, T item, int index);
|
||||||
|
typedef ButtonActionCallback<T> = Future<bool> Function(T item);
|
||||||
|
|
||||||
|
class ReusableListCard<T> extends StatelessWidget {
|
||||||
|
final List<T> items;
|
||||||
|
final Widget emptyWidget;
|
||||||
|
final ItemWidgetBuilder<T> itemBuilder;
|
||||||
|
final ButtonActionCallback<T> onActionPressed;
|
||||||
|
final ButtonActionCallback<T> onViewPressed;
|
||||||
|
final String Function(T item) getPrimaryText;
|
||||||
|
final String Function(T item) getSecondaryText;
|
||||||
|
final String Function(T item)? getInTimeText;
|
||||||
|
final String Function(T item)? getOutTimeText;
|
||||||
|
final bool Function(T item)? isUploading;
|
||||||
|
final String Function(T item)? getButtonText;
|
||||||
|
final Color Function(String buttonText)? getButtonColor;
|
||||||
|
|
||||||
|
const ReusableListCard({
|
||||||
|
Key? key,
|
||||||
|
required this.items,
|
||||||
|
required this.emptyWidget,
|
||||||
|
required this.itemBuilder,
|
||||||
|
required this.onActionPressed,
|
||||||
|
required this.onViewPressed,
|
||||||
|
required this.getPrimaryText,
|
||||||
|
required this.getSecondaryText,
|
||||||
|
this.getInTimeText,
|
||||||
|
this.getOutTimeText,
|
||||||
|
this.isUploading,
|
||||||
|
this.getButtonText,
|
||||||
|
this.getButtonColor,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||||
|
elevation: 1,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: items.isEmpty
|
||||||
|
? emptyWidget
|
||||||
|
: Column(
|
||||||
|
children: List.generate(items.length, (index) {
|
||||||
|
final item = items[index];
|
||||||
|
final buttonText = getButtonText?.call(item) ?? 'Action';
|
||||||
|
final uploading = isUploading?.call(item) ?? false;
|
||||||
|
final buttonColor =
|
||||||
|
getButtonColor?.call(buttonText) ?? Theme.of(context).primaryColor;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
color: Theme.of(context).cardColor,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Custom item widget builder for avatar or image
|
||||||
|
itemBuilder(context, item, index),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
getPrimaryText(item),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
getSecondaryText(item),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (getInTimeText != null && getOutTimeText != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Text("In: ", style: TextStyle(fontWeight: FontWeight.w600)),
|
||||||
|
Text(getInTimeText!(item), style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
const Text("Out: ", style: TextStyle(fontWeight: FontWeight.w600)),
|
||||||
|
Text(getOutTimeText!(item), style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 30,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => onViewPressed(item),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.blueGrey,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
textStyle: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
child: const Text('View', style: TextStyle(fontSize: 12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
SizedBox(
|
||||||
|
height: 30,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: uploading
|
||||||
|
? null
|
||||||
|
: () => onActionPressed(item),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: buttonColor,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
textStyle: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
child: uploading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(buttonText, style: const TextStyle(fontSize: 12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (index != items.length - 1)
|
||||||
|
Divider(color: Colors.grey.withOpacity(0.3), thickness: 1, height: 1),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
126
lib/model/attendance/regualrize_action_button.dart
Normal file
126
lib/model/attendance/regualrize_action_button.dart
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:marco/helpers/utils/attendance_actions.dart';
|
||||||
|
|
||||||
|
enum ButtonActions { approve, reject }
|
||||||
|
|
||||||
|
class RegularizeActionButton extends StatefulWidget {
|
||||||
|
final dynamic attendanceController; // Replace dynamic with your controller's type
|
||||||
|
final dynamic log; // Replace dynamic with your log model type
|
||||||
|
final String uniqueLogKey;
|
||||||
|
final ButtonActions action;
|
||||||
|
|
||||||
|
const RegularizeActionButton({
|
||||||
|
Key? key,
|
||||||
|
required this.attendanceController,
|
||||||
|
required this.log,
|
||||||
|
required this.uniqueLogKey,
|
||||||
|
required this.action,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RegularizeActionButton> createState() => _RegularizeActionButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RegularizeActionButtonState extends State<RegularizeActionButton> {
|
||||||
|
bool isUploading = false;
|
||||||
|
|
||||||
|
static const Map<ButtonActions, String> _buttonTexts = {
|
||||||
|
ButtonActions.approve: "Approve",
|
||||||
|
ButtonActions.reject: "Reject",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const Map<ButtonActions, String> _buttonComments = {
|
||||||
|
ButtonActions.approve: "Accepted",
|
||||||
|
ButtonActions.reject: "Rejected",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const Map<ButtonActions, int> _buttonActionCodes = {
|
||||||
|
ButtonActions.approve: 4,
|
||||||
|
ButtonActions.reject: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
Color get backgroundColor {
|
||||||
|
// Use string keys for correct color lookup
|
||||||
|
return AttendanceActionColors.colors[_buttonTexts[widget.action]!] ?? Colors.grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handlePress() async {
|
||||||
|
if (widget.attendanceController.selectedProjectId == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text("Please select a project first")),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
isUploading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
widget.attendanceController.uploadingStates[widget.uniqueLogKey]?.value = true;
|
||||||
|
|
||||||
|
final success = await widget.attendanceController.captureAndUploadAttendance(
|
||||||
|
widget.log.id,
|
||||||
|
widget.log.employeeId,
|
||||||
|
widget.attendanceController.selectedProjectId!,
|
||||||
|
comment: _buttonComments[widget.action]!,
|
||||||
|
action: _buttonActionCodes[widget.action]!,
|
||||||
|
imageCapture: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(success
|
||||||
|
? '${_buttonTexts[widget.action]} marked successfully!'
|
||||||
|
: 'Failed to mark ${_buttonTexts[widget.action]}.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
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.uploadingStates[widget.uniqueLogKey]?.value = false;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
isUploading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final buttonText = _buttonTexts[widget.action]!;
|
||||||
|
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 70, maxWidth: 120),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 30,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: isUploading ? null : _handlePress,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
foregroundColor: Colors.white, // Ensures visibility on all backgrounds
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6),
|
||||||
|
minimumSize: const Size(60, 20),
|
||||||
|
textStyle: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
child: isUploading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Text(
|
||||||
|
buttonText,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,9 @@ class AttendanceLogModel {
|
|||||||
final DateTime? checkIn;
|
final DateTime? checkIn;
|
||||||
final DateTime? checkOut;
|
final DateTime? checkOut;
|
||||||
final int activity;
|
final int activity;
|
||||||
|
final String firstName;
|
||||||
|
final String lastName;
|
||||||
|
final String designation;
|
||||||
|
|
||||||
AttendanceLogModel({
|
AttendanceLogModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
@ -15,6 +18,9 @@ class AttendanceLogModel {
|
|||||||
this.checkIn,
|
this.checkIn,
|
||||||
this.checkOut,
|
this.checkOut,
|
||||||
required this.activity,
|
required this.activity,
|
||||||
|
required this.firstName,
|
||||||
|
required this.lastName,
|
||||||
|
required this.designation,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory AttendanceLogModel.fromJson(Map<String, dynamic> json) {
|
factory AttendanceLogModel.fromJson(Map<String, dynamic> json) {
|
||||||
@ -30,6 +36,9 @@ class AttendanceLogModel {
|
|||||||
? DateTime.tryParse(json['checkOutTime'])
|
? DateTime.tryParse(json['checkOutTime'])
|
||||||
: null,
|
: null,
|
||||||
activity: json['activity'] ?? 0,
|
activity: json['activity'] ?? 0,
|
||||||
|
firstName: json['firstName'] ?? '',
|
||||||
|
lastName: json['lastName'] ?? '',
|
||||||
|
designation: json['jobRoleName'] ?? '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
class AttendanceLogViewModel {
|
class AttendanceLogViewModel {
|
||||||
final DateTime? activityTime;
|
final DateTime? activityTime;
|
||||||
final String? imageUrl;
|
final String? imageUrl;
|
||||||
@ -7,6 +8,7 @@ class AttendanceLogViewModel {
|
|||||||
final String? preSignedUrl;
|
final String? preSignedUrl;
|
||||||
final String? longitude;
|
final String? longitude;
|
||||||
final String? latitude;
|
final String? latitude;
|
||||||
|
final int? activity;
|
||||||
|
|
||||||
AttendanceLogViewModel({
|
AttendanceLogViewModel({
|
||||||
this.activityTime,
|
this.activityTime,
|
||||||
@ -16,6 +18,7 @@ class AttendanceLogViewModel {
|
|||||||
this.preSignedUrl,
|
this.preSignedUrl,
|
||||||
this.longitude,
|
this.longitude,
|
||||||
this.latitude,
|
this.latitude,
|
||||||
|
required this.activity,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory AttendanceLogViewModel.fromJson(Map<String, dynamic> json) {
|
factory AttendanceLogViewModel.fromJson(Map<String, dynamic> json) {
|
||||||
@ -29,6 +32,7 @@ class AttendanceLogViewModel {
|
|||||||
preSignedUrl: json['preSignedUrl']?.toString(),
|
preSignedUrl: json['preSignedUrl']?.toString(),
|
||||||
longitude: json['longitude']?.toString(),
|
longitude: json['longitude']?.toString(),
|
||||||
latitude: json['latitude']?.toString(),
|
latitude: json['latitude']?.toString(),
|
||||||
|
activity: json['activity'] ?? 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,11 +45,13 @@ class AttendanceLogViewModel {
|
|||||||
'preSignedUrl': preSignedUrl,
|
'preSignedUrl': preSignedUrl,
|
||||||
'longitude': longitude,
|
'longitude': longitude,
|
||||||
'latitude': latitude,
|
'latitude': latitude,
|
||||||
|
'activity': activity,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
String? get formattedDate =>
|
String? get formattedDate => activityTime != null
|
||||||
activityTime != null ? DateFormat('yyyy-MM-dd').format(activityTime!) : null;
|
? DateFormat('yyyy-MM-dd').format(activityTime!)
|
||||||
|
: null;
|
||||||
|
|
||||||
String? get formattedTime =>
|
String? get formattedTime =>
|
||||||
activityTime != null ? DateFormat('hh:mm a').format(activityTime!) : null;
|
activityTime != null ? DateFormat('hh:mm a').format(activityTime!) : null;
|
||||||
|
|||||||
@ -3,8 +3,10 @@ class EmployeeModel {
|
|||||||
final String employeeId;
|
final String employeeId;
|
||||||
final String name;
|
final String name;
|
||||||
final String designation;
|
final String designation;
|
||||||
final String checkIn;
|
final DateTime? checkIn;
|
||||||
final String checkOut;
|
final DateTime? checkOut;
|
||||||
|
final String firstName;
|
||||||
|
final String lastName;
|
||||||
final int activity;
|
final int activity;
|
||||||
int action;
|
int action;
|
||||||
final String jobRole;
|
final String jobRole;
|
||||||
@ -16,8 +18,10 @@ class EmployeeModel {
|
|||||||
required this.employeeId,
|
required this.employeeId,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.designation,
|
required this.designation,
|
||||||
required this.checkIn,
|
this.checkIn,
|
||||||
required this.checkOut,
|
this.checkOut,
|
||||||
|
required this.firstName,
|
||||||
|
required this.lastName,
|
||||||
required this.activity,
|
required this.activity,
|
||||||
required this.action,
|
required this.action,
|
||||||
required this.jobRole,
|
required this.jobRole,
|
||||||
@ -31,13 +35,19 @@ class EmployeeModel {
|
|||||||
employeeId: json['employeeId']?.toString() ?? '',
|
employeeId: json['employeeId']?.toString() ?? '',
|
||||||
name: '${json['firstName'] ?? ''} ${json['lastName'] ?? ''}'.trim(),
|
name: '${json['firstName'] ?? ''} ${json['lastName'] ?? ''}'.trim(),
|
||||||
designation: json['jobRoleName'] ?? '',
|
designation: json['jobRoleName'] ?? '',
|
||||||
checkIn: json['checkIn']?.toString() ?? '-',
|
checkIn: json['checkInTime'] != null
|
||||||
checkOut: json['checkOut']?.toString() ?? '-',
|
? DateTime.tryParse(json['checkInTime'])
|
||||||
|
: null,
|
||||||
|
checkOut: json['checkOutTime'] != null
|
||||||
|
? DateTime.tryParse(json['checkOutTime'])
|
||||||
|
: null,
|
||||||
action: json['action'] ?? 0,
|
action: json['action'] ?? 0,
|
||||||
activity: json['activity'] ?? 0,
|
activity: json['activity'] ?? 0,
|
||||||
jobRole: json['jobRole']?.toString() ?? '-',
|
jobRole: json['jobRole']?.toString() ?? '-',
|
||||||
email: json['email']?.toString() ?? '-',
|
email: json['email']?.toString() ?? '-',
|
||||||
phoneNumber: json['phoneNumber']?.toString() ?? '-',
|
phoneNumber: json['phoneNumber']?.toString() ?? '-',
|
||||||
|
firstName: json['firstName'] ?? '',
|
||||||
|
lastName: json['lastName'] ?? '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,8 +58,8 @@ class EmployeeModel {
|
|||||||
'firstName': name.split(' ').first,
|
'firstName': name.split(' ').first,
|
||||||
'lastName': name.split(' ').length > 1 ? name.split(' ').last : '',
|
'lastName': name.split(' ').length > 1 ? name.split(' ').last : '',
|
||||||
'jobRoleName': designation,
|
'jobRoleName': designation,
|
||||||
'checkIn': checkIn,
|
'checkInTime': checkIn?.toIso8601String(),
|
||||||
'checkOut': checkOut,
|
'checkOutTime': checkOut?.toIso8601String(),
|
||||||
'action': action,
|
'action': action,
|
||||||
'activity': activity,
|
'activity': activity,
|
||||||
'jobRole': jobRole.isEmpty ? '-' : jobRole,
|
'jobRole': jobRole.isEmpty ? '-' : jobRole,
|
||||||
|
|||||||
@ -6,6 +6,8 @@ class RegularizationLogModel {
|
|||||||
final DateTime? checkIn;
|
final DateTime? checkIn;
|
||||||
final DateTime? checkOut;
|
final DateTime? checkOut;
|
||||||
final int activity;
|
final int activity;
|
||||||
|
final String firstName;
|
||||||
|
final String lastName;
|
||||||
|
|
||||||
RegularizationLogModel({
|
RegularizationLogModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
@ -15,6 +17,8 @@ class RegularizationLogModel {
|
|||||||
this.checkIn,
|
this.checkIn,
|
||||||
this.checkOut,
|
this.checkOut,
|
||||||
required this.activity,
|
required this.activity,
|
||||||
|
required this.firstName,
|
||||||
|
required this.lastName,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory RegularizationLogModel.fromJson(Map<String, dynamic> json) {
|
factory RegularizationLogModel.fromJson(Map<String, dynamic> json) {
|
||||||
@ -30,6 +34,8 @@ class RegularizationLogModel {
|
|||||||
? DateTime.tryParse(json['checkOutTime'].toString())
|
? DateTime.tryParse(json['checkOutTime'].toString())
|
||||||
: null,
|
: null,
|
||||||
activity: json['activity'] ?? 0,
|
activity: json['activity'] ?? 0,
|
||||||
|
firstName: json['firstName'] ?? '',
|
||||||
|
lastName: json['lastName'] ?? '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,13 +10,14 @@ import 'package:marco/view/error_pages/coming_soon_screen.dart';
|
|||||||
import 'package:marco/view/error_pages/error_404_screen.dart';
|
import 'package:marco/view/error_pages/error_404_screen.dart';
|
||||||
import 'package:marco/view/error_pages/error_500_screen.dart';
|
import 'package:marco/view/error_pages/error_500_screen.dart';
|
||||||
// import 'package:marco/view/dashboard/attendance_screen.dart';
|
// import 'package:marco/view/dashboard/attendance_screen.dart';
|
||||||
import 'package:marco/view/dashboard/attendanceScreen.dart';
|
// import 'package:marco/view/dashboard/attendanceScreen.dart';
|
||||||
import 'package:marco/view/dashboard/dashboard_screen.dart';
|
import 'package:marco/view/dashboard/dashboard_screen.dart';
|
||||||
import 'package:marco/view/dashboard/add_employee_screen.dart';
|
import 'package:marco/view/dashboard/add_employee_screen.dart';
|
||||||
import 'package:marco/view/dashboard/employee_screen.dart';
|
import 'package:marco/view/dashboard/employee_screen.dart';
|
||||||
import 'package:marco/view/dashboard/daily_task_screen.dart';
|
import 'package:marco/view/dashboard/daily_task_screen.dart';
|
||||||
import 'package:marco/view/taskPlaning/report_task_screen.dart';
|
import 'package:marco/view/taskPlaning/report_task_screen.dart';
|
||||||
import 'package:marco/view/taskPlaning/comment_task_screen.dart';
|
import 'package:marco/view/taskPlaning/comment_task_screen.dart';
|
||||||
|
import 'package:marco/view/dashboard/Attendence/attendance_screen.dart';
|
||||||
|
|
||||||
class AuthMiddleware extends GetMiddleware {
|
class AuthMiddleware extends GetMiddleware {
|
||||||
@override
|
@override
|
||||||
@ -29,7 +30,7 @@ getPageRoute() {
|
|||||||
var routes = [
|
var routes = [
|
||||||
GetPage(
|
GetPage(
|
||||||
name: '/',
|
name: '/',
|
||||||
page: () => const AttendanceScreen(),
|
page: () => AttendanceScreen(),
|
||||||
middlewares: [AuthMiddleware()]),
|
middlewares: [AuthMiddleware()]),
|
||||||
|
|
||||||
// Dashboard
|
// Dashboard
|
||||||
|
|||||||
748
lib/view/dashboard/Attendence/attendance_screen.dart
Normal file
748
lib/view/dashboard/Attendence/attendance_screen.dart
Normal file
@ -0,0 +1,748 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:marco/helpers/theme/app_theme.dart';
|
||||||
|
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||||
|
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_card.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_container.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_flex.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_flex_item.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
|
import 'package:marco/view/layouts/layout.dart';
|
||||||
|
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
|
||||||
|
import 'package:marco/controller/permission_controller.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:marco/helpers/widgets/avatar.dart';
|
||||||
|
import 'package:marco/model/attendance/log_details_view.dart';
|
||||||
|
import 'package:marco/model/attendance/attendence_action_button.dart';
|
||||||
|
import 'package:marco/model/attendance/regualrize_action_button.dart';
|
||||||
|
import 'package:marco/model/attendance/attendence_filter_sheet.dart';
|
||||||
|
|
||||||
|
class AttendanceScreen extends StatefulWidget {
|
||||||
|
AttendanceScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AttendanceScreen> createState() => _AttendanceScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||||
|
final AttendanceController attendanceController =
|
||||||
|
Get.put(AttendanceController());
|
||||||
|
final PermissionController permissionController =
|
||||||
|
Get.put(PermissionController());
|
||||||
|
|
||||||
|
String selectedTab = 'todaysAttendance';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Layout(
|
||||||
|
child: GetBuilder<AttendanceController>(
|
||||||
|
init: attendanceController,
|
||||||
|
tag: 'attendance_dashboard_controller',
|
||||||
|
builder: (controller) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: MySpacing.x(flexSpacing),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
MyText.titleMedium("Attendance",
|
||||||
|
fontSize: 18, fontWeight: 600),
|
||||||
|
MyBreadcrumb(
|
||||||
|
children: [
|
||||||
|
MyBreadcrumbItem(name: 'Dashboard'),
|
||||||
|
MyBreadcrumbItem(name: 'Attendance', active: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.height(flexSpacing),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
MyText.bodyMedium(
|
||||||
|
"Filter",
|
||||||
|
fontWeight: 600,
|
||||||
|
),
|
||||||
|
// Wrap with Tooltip and InkWell for interactive feedback
|
||||||
|
Tooltip(
|
||||||
|
message: 'Filter Project',
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
onTap: () async {
|
||||||
|
final result =
|
||||||
|
await showModalBottomSheet<Map<String, dynamic>>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.vertical(top: Radius.circular(12)),
|
||||||
|
),
|
||||||
|
builder: (context) => AttendanceFilterBottomSheet(
|
||||||
|
controller: attendanceController,
|
||||||
|
permissionController: permissionController,
|
||||||
|
selectedTab: selectedTab,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
final selectedProjectId =
|
||||||
|
result['projectId'] as String?;
|
||||||
|
final selectedView = result['selectedTab'] as String?;
|
||||||
|
|
||||||
|
if (selectedProjectId != null &&
|
||||||
|
selectedProjectId !=
|
||||||
|
attendanceController.selectedProjectId) {
|
||||||
|
attendanceController.selectedProjectId =
|
||||||
|
selectedProjectId;
|
||||||
|
try {
|
||||||
|
await attendanceController
|
||||||
|
.fetchEmployeesByProject(selectedProjectId);
|
||||||
|
await attendanceController
|
||||||
|
.fetchAttendanceLogs(selectedProjectId);
|
||||||
|
await attendanceController
|
||||||
|
.fetchRegularizationLogs(selectedProjectId);
|
||||||
|
await attendanceController
|
||||||
|
.fetchProjectData(selectedProjectId);
|
||||||
|
} catch (_) {}
|
||||||
|
attendanceController
|
||||||
|
.update(['attendance_dashboard_controller']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedView != null &&
|
||||||
|
selectedView != selectedTab) {
|
||||||
|
setState(() {
|
||||||
|
selectedTab = selectedView;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Icon(
|
||||||
|
Icons.filter_list_alt,
|
||||||
|
color: Colors.blueAccent,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
MyText.bodyMedium(
|
||||||
|
"Refresh",
|
||||||
|
fontWeight: 600,
|
||||||
|
),
|
||||||
|
Tooltip(
|
||||||
|
message: 'Refresh Data',
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
onTap: () async {
|
||||||
|
final projectId =
|
||||||
|
attendanceController.selectedProjectId;
|
||||||
|
if (projectId != null && projectId.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
await attendanceController
|
||||||
|
.fetchEmployeesByProject(projectId);
|
||||||
|
await attendanceController
|
||||||
|
.fetchAttendanceLogs(projectId);
|
||||||
|
await attendanceController
|
||||||
|
.fetchRegularizationLogs(projectId);
|
||||||
|
await attendanceController
|
||||||
|
.fetchProjectData(projectId);
|
||||||
|
attendanceController
|
||||||
|
.update(['attendance_dashboard_controller']);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error refreshing data: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Icon(
|
||||||
|
Icons.refresh,
|
||||||
|
color: Colors.green,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
MySpacing.height(flexSpacing),
|
||||||
|
Padding(
|
||||||
|
padding: MySpacing.x(flexSpacing / 2),
|
||||||
|
child: MyFlex(children: [
|
||||||
|
MyFlexItem(
|
||||||
|
sizes: 'lg-12 md-12 sm-12',
|
||||||
|
child: selectedTab == 'todaysAttendance'
|
||||||
|
? employeeListTab()
|
||||||
|
: selectedTab == 'attendanceLogs'
|
||||||
|
? employeeLog()
|
||||||
|
: regularizationScreen(),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget employeeListTab() {
|
||||||
|
return Obx(() {
|
||||||
|
final isLoading = attendanceController.isLoadingEmployees.value;
|
||||||
|
final employees = attendanceController.employees;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: MyText.titleMedium(
|
||||||
|
"Today's Attendance",
|
||||||
|
fontWeight: 600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MyCard.bordered(
|
||||||
|
borderRadiusAll: 4,
|
||||||
|
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||||
|
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||||
|
paddingAll: 8,
|
||||||
|
child: isLoading
|
||||||
|
? Center(child: CircularProgressIndicator())
|
||||||
|
: employees.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: MyText.bodySmall(
|
||||||
|
"No Employees Assigned to This Project",
|
||||||
|
fontWeight: 600,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
children: List.generate(employees.length, (index) {
|
||||||
|
final employee = employees[index];
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 8),
|
||||||
|
child: MyContainer(
|
||||||
|
paddingAll: 5,
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Avatar(
|
||||||
|
firstName: employee.firstName,
|
||||||
|
lastName: employee.lastName,
|
||||||
|
size: 31,
|
||||||
|
),
|
||||||
|
MySpacing.width(16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
MyText.bodyMedium(
|
||||||
|
employee.name,
|
||||||
|
fontWeight: 600,
|
||||||
|
overflow:
|
||||||
|
TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
MySpacing.width(6),
|
||||||
|
MyText.bodySmall(
|
||||||
|
'(${employee.designation})',
|
||||||
|
fontWeight: 600,
|
||||||
|
overflow:
|
||||||
|
TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
color: Colors.grey[
|
||||||
|
700], // optional styling
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
MySpacing.height(8),
|
||||||
|
(employee.checkIn != null ||
|
||||||
|
employee.checkOut != null)
|
||||||
|
? Row(
|
||||||
|
children: [
|
||||||
|
if (employee.checkIn !=
|
||||||
|
null) ...[
|
||||||
|
Icon(
|
||||||
|
Icons
|
||||||
|
.arrow_circle_right,
|
||||||
|
size: 16,
|
||||||
|
color:
|
||||||
|
Colors.green),
|
||||||
|
MySpacing.width(4),
|
||||||
|
Expanded(
|
||||||
|
child:
|
||||||
|
MyText.bodySmall(
|
||||||
|
DateFormat(
|
||||||
|
'hh:mm a')
|
||||||
|
.format(employee
|
||||||
|
.checkIn!),
|
||||||
|
fontWeight: 600,
|
||||||
|
overflow:
|
||||||
|
TextOverflow
|
||||||
|
.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.width(16),
|
||||||
|
],
|
||||||
|
if (employee.checkOut !=
|
||||||
|
null) ...[
|
||||||
|
Icon(
|
||||||
|
Icons
|
||||||
|
.arrow_circle_left,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.red),
|
||||||
|
MySpacing.width(4),
|
||||||
|
Expanded(
|
||||||
|
child:
|
||||||
|
MyText.bodySmall(
|
||||||
|
DateFormat(
|
||||||
|
'hh:mm a')
|
||||||
|
.format(employee
|
||||||
|
.checkOut!),
|
||||||
|
fontWeight: 600,
|
||||||
|
overflow:
|
||||||
|
TextOverflow
|
||||||
|
.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: SizedBox.shrink(),
|
||||||
|
MySpacing.height(12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
AttendanceActionButton(
|
||||||
|
employee: employee,
|
||||||
|
attendanceController:
|
||||||
|
attendanceController,
|
||||||
|
),
|
||||||
|
if (employee.checkIn !=
|
||||||
|
null) ...[
|
||||||
|
MySpacing.width(8),
|
||||||
|
AttendanceLogViewButton(
|
||||||
|
employee: employee,
|
||||||
|
attendanceController:
|
||||||
|
attendanceController,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (index != employees.length - 1)
|
||||||
|
Divider(
|
||||||
|
color: Colors.grey.withOpacity(0.3),
|
||||||
|
thickness: 1,
|
||||||
|
height: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget employeeLog() {
|
||||||
|
return Obx(() {
|
||||||
|
final logs = List.of(attendanceController.attendanceLogs);
|
||||||
|
logs.sort((a, b) {
|
||||||
|
final aDate = a.checkIn ?? DateTime(0);
|
||||||
|
final bDate = b.checkIn ?? DateTime(0);
|
||||||
|
return bDate.compareTo(aDate);
|
||||||
|
});
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: MyText.titleMedium(
|
||||||
|
"Attendance Logs",
|
||||||
|
fontWeight: 600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MyCard.bordered(
|
||||||
|
borderRadiusAll: 4,
|
||||||
|
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||||
|
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||||
|
paddingAll: 8,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (attendanceController.isLoadingAttendanceLogs.value)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 32),
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
)
|
||||||
|
else if (logs.isEmpty)
|
||||||
|
MyText.bodySmall(
|
||||||
|
"No Attendance Logs Found for this Project",
|
||||||
|
fontWeight: 600,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Column(
|
||||||
|
children: List.generate(logs.length, (index) {
|
||||||
|
final employee = logs[index];
|
||||||
|
final currentDate = employee.checkIn != null
|
||||||
|
? DateFormat('dd MMM yyyy').format(employee.checkIn!)
|
||||||
|
: '';
|
||||||
|
final previousDate =
|
||||||
|
index > 0 && logs[index - 1].checkIn != null
|
||||||
|
? DateFormat('dd MMM yyyy')
|
||||||
|
.format(logs[index - 1].checkIn!)
|
||||||
|
: '';
|
||||||
|
|
||||||
|
final showDateHeader =
|
||||||
|
index == 0 || currentDate != previousDate;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (showDateHeader)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: MyText.bodyMedium(
|
||||||
|
currentDate,
|
||||||
|
fontWeight: 700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 8),
|
||||||
|
child: MyContainer(
|
||||||
|
paddingAll: 8,
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Avatar(
|
||||||
|
firstName: employee.firstName,
|
||||||
|
lastName: employee.lastName,
|
||||||
|
size: 31,
|
||||||
|
),
|
||||||
|
MySpacing.width(16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: MyText.bodyMedium(
|
||||||
|
employee.name,
|
||||||
|
fontWeight: 600,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.width(6),
|
||||||
|
Flexible(
|
||||||
|
child: MyText.bodySmall(
|
||||||
|
'(${employee.designation})',
|
||||||
|
fontWeight: 600,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
MySpacing.height(8),
|
||||||
|
(employee.checkIn != null ||
|
||||||
|
employee.checkOut != null)
|
||||||
|
? Row(
|
||||||
|
children: [
|
||||||
|
if (employee.checkIn !=
|
||||||
|
null) ...[
|
||||||
|
Icon(
|
||||||
|
Icons
|
||||||
|
.arrow_circle_right,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.green),
|
||||||
|
MySpacing.width(4),
|
||||||
|
Expanded(
|
||||||
|
child: MyText.bodySmall(
|
||||||
|
DateFormat('hh:mm a')
|
||||||
|
.format(employee
|
||||||
|
.checkIn!),
|
||||||
|
fontWeight: 600,
|
||||||
|
overflow: TextOverflow
|
||||||
|
.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.width(16),
|
||||||
|
],
|
||||||
|
if (employee.checkOut !=
|
||||||
|
null) ...[
|
||||||
|
Icon(
|
||||||
|
Icons.arrow_circle_left,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.red),
|
||||||
|
MySpacing.width(4),
|
||||||
|
Expanded(
|
||||||
|
child: MyText.bodySmall(
|
||||||
|
DateFormat('hh:mm a')
|
||||||
|
.format(employee
|
||||||
|
.checkOut!),
|
||||||
|
fontWeight: 600,
|
||||||
|
overflow: TextOverflow
|
||||||
|
.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: SizedBox.shrink(),
|
||||||
|
MySpacing.height(12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: AttendanceActionButton(
|
||||||
|
employee: employee,
|
||||||
|
attendanceController:
|
||||||
|
attendanceController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.width(8),
|
||||||
|
Flexible(
|
||||||
|
child: AttendanceLogViewButton(
|
||||||
|
employee: employee,
|
||||||
|
attendanceController:
|
||||||
|
attendanceController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (index != logs.length - 1)
|
||||||
|
Divider(
|
||||||
|
color: Colors.grey.withOpacity(0.3),
|
||||||
|
thickness: 1,
|
||||||
|
height: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget regularizationScreen() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 4.0),
|
||||||
|
child: MyText.titleMedium(
|
||||||
|
"Regularization Requests",
|
||||||
|
fontWeight: 600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Obx(() {
|
||||||
|
final employees = attendanceController.regularizationLogs;
|
||||||
|
|
||||||
|
return MyCard.bordered(
|
||||||
|
borderRadiusAll: 4,
|
||||||
|
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||||
|
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||||
|
paddingAll: 8,
|
||||||
|
child: attendanceController.isLoadingRegularizationLogs.value
|
||||||
|
? const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 32.0),
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
)
|
||||||
|
: employees.isEmpty
|
||||||
|
? MyText.bodySmall(
|
||||||
|
"No Regularization Requests Found for this Project",
|
||||||
|
fontWeight: 600,
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
children: List.generate(employees.length, (index) {
|
||||||
|
final employee = employees[index];
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 8),
|
||||||
|
child: MyContainer(
|
||||||
|
paddingAll: 8,
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Avatar(
|
||||||
|
firstName: employee.firstName,
|
||||||
|
lastName: employee.lastName,
|
||||||
|
size: 31,
|
||||||
|
),
|
||||||
|
MySpacing.width(16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: MyText.bodyMedium(
|
||||||
|
employee.name,
|
||||||
|
fontWeight: 600,
|
||||||
|
overflow:
|
||||||
|
TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.width(6),
|
||||||
|
Flexible(
|
||||||
|
child: MyText.bodySmall(
|
||||||
|
'(${employee.role})',
|
||||||
|
fontWeight: 600,
|
||||||
|
overflow:
|
||||||
|
TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
MySpacing.height(8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (employee.checkIn !=
|
||||||
|
null) ...[
|
||||||
|
Icon(Icons.arrow_circle_right,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.green),
|
||||||
|
MySpacing.width(4),
|
||||||
|
Expanded(
|
||||||
|
child: MyText.bodySmall(
|
||||||
|
DateFormat('hh:mm a')
|
||||||
|
.format(employee
|
||||||
|
.checkIn!),
|
||||||
|
fontWeight: 600,
|
||||||
|
overflow:
|
||||||
|
TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.width(16),
|
||||||
|
],
|
||||||
|
if (employee.checkOut !=
|
||||||
|
null) ...[
|
||||||
|
Icon(Icons.arrow_circle_left,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.red),
|
||||||
|
MySpacing.width(4),
|
||||||
|
Expanded(
|
||||||
|
child: MyText.bodySmall(
|
||||||
|
DateFormat('hh:mm a')
|
||||||
|
.format(employee
|
||||||
|
.checkOut!),
|
||||||
|
fontWeight: 600,
|
||||||
|
overflow:
|
||||||
|
TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
MySpacing.height(12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
RegularizeActionButton(
|
||||||
|
attendanceController:
|
||||||
|
attendanceController,
|
||||||
|
log: employee,
|
||||||
|
uniqueLogKey:
|
||||||
|
employee.employeeId,
|
||||||
|
action: ButtonActions.approve,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
RegularizeActionButton(
|
||||||
|
attendanceController:
|
||||||
|
attendanceController,
|
||||||
|
log: employee,
|
||||||
|
uniqueLogKey:
|
||||||
|
employee.employeeId,
|
||||||
|
action: ButtonActions.reject,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (index != employees.length - 1)
|
||||||
|
Divider(
|
||||||
|
color: Colors.grey.withOpacity(0.3),
|
||||||
|
thickness: 1,
|
||||||
|
height: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
227
lib/view/dashboard/Attendence/attendence_log_screen.dart
Normal file
227
lib/view/dashboard/Attendence/attendence_log_screen.dart
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:marco/helpers/theme/app_theme.dart';
|
||||||
|
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||||
|
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_card.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_container.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_flex.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_flex_item.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
|
import 'package:marco/view/layouts/layout.dart';
|
||||||
|
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
|
||||||
|
import 'package:marco/controller/permission_controller.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:marco/model/attendance/attendence_action_button.dart';
|
||||||
|
import 'package:marco/helpers/widgets/avatar.dart';
|
||||||
|
import 'package:marco/model/attendance/log_details_view.dart';
|
||||||
|
|
||||||
|
class AttendenceLogScreen extends StatefulWidget {
|
||||||
|
const AttendenceLogScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AttendenceLogScreen> createState() => _AttendenceLogScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AttendenceLogScreenState extends State<AttendenceLogScreen>
|
||||||
|
with UIMixin {
|
||||||
|
final AttendanceController attendanceController =
|
||||||
|
Get.put(AttendanceController());
|
||||||
|
final PermissionController permissionController =
|
||||||
|
Get.put(PermissionController());
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Layout(
|
||||||
|
child: GetBuilder(
|
||||||
|
init: attendanceController,
|
||||||
|
tag: 'attendence_controller',
|
||||||
|
builder: (controller) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: MySpacing.x(flexSpacing),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
MyText.titleMedium("Attendence",
|
||||||
|
fontSize: 18, fontWeight: 600),
|
||||||
|
MyBreadcrumb(
|
||||||
|
children: [
|
||||||
|
MyBreadcrumbItem(name: 'Dashboard'),
|
||||||
|
MyBreadcrumbItem(name: 'Attendence', active: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.height(flexSpacing),
|
||||||
|
Padding(
|
||||||
|
padding: MySpacing.x(flexSpacing / 2),
|
||||||
|
child: MyFlex(children: [
|
||||||
|
MyFlexItem(sizes: 'lg-12 md-12 sm-12', child: employeeLog()),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget employeeLog() {
|
||||||
|
return MyCard.bordered(
|
||||||
|
borderRadiusAll: 4,
|
||||||
|
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||||
|
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||||
|
paddingAll: 8,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
attendanceController.employees.isEmpty
|
||||||
|
? MyText.bodySmall("No Employees Assigned to This Project",
|
||||||
|
fontWeight: 600)
|
||||||
|
: Column(
|
||||||
|
children: List.generate(attendanceController.employees.length,
|
||||||
|
(index) {
|
||||||
|
final employee = attendanceController.employees[index];
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
|
child: MyContainer(
|
||||||
|
paddingAll: 12,
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Avatar(
|
||||||
|
firstName: employee.firstName,
|
||||||
|
lastName: employee.lastName,
|
||||||
|
size: 31,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
MySpacing.width(16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: MyText.bodyMedium(
|
||||||
|
'${employee.name} '
|
||||||
|
' (${employee.designation})',
|
||||||
|
fontWeight: 600,
|
||||||
|
maxLines: null,
|
||||||
|
overflow: TextOverflow.visible,
|
||||||
|
softWrap: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
MySpacing.height(8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
MyText.bodySmall("In: ",
|
||||||
|
fontWeight: 600),
|
||||||
|
MyText.bodySmall(
|
||||||
|
employee.checkIn != null
|
||||||
|
? DateFormat('hh:mm a')
|
||||||
|
.format(employee.checkIn!)
|
||||||
|
: '--',
|
||||||
|
fontWeight: 600,
|
||||||
|
),
|
||||||
|
MySpacing.width(16),
|
||||||
|
MyText.bodySmall("Out: ",
|
||||||
|
fontWeight: 600),
|
||||||
|
MyText.bodySmall(
|
||||||
|
employee.checkOut != null
|
||||||
|
? DateFormat('hh:mm a')
|
||||||
|
.format(employee.checkOut!)
|
||||||
|
: '--',
|
||||||
|
fontWeight: 600,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
MySpacing.height(12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
AttendanceLogViewButton(
|
||||||
|
employee: employee,
|
||||||
|
attendanceController:
|
||||||
|
attendanceController,
|
||||||
|
),
|
||||||
|
MySpacing.width(8),
|
||||||
|
AttendanceActionButton(
|
||||||
|
employee: employee,
|
||||||
|
attendanceController:
|
||||||
|
attendanceController,
|
||||||
|
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (index != attendanceController.employees.length - 1)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(),
|
||||||
|
child: Divider(
|
||||||
|
color: Colors.grey.withOpacity(0.3),
|
||||||
|
thickness: 1,
|
||||||
|
height: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user