Refactor code structure for improved readability and maintainability, Changed the Attendance Screen UI #31

Merged
vaibhav.surve merged 1 commits from Vaibhav_Enhancement-#312 into main 2025-05-21 12:27:30 +00:00
12 changed files with 2873 additions and 1186 deletions

View 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),
),
),
],
),
),
);
}
}

View 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),
],
);
}),
),
),
);
}
}

View 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,
),
),
),
),
);
}
}

View File

@ -6,6 +6,9 @@ class AttendanceLogModel {
final DateTime? checkIn;
final DateTime? checkOut;
final int activity;
final String firstName;
final String lastName;
final String designation;
AttendanceLogModel({
required this.id,
@ -15,6 +18,9 @@ class AttendanceLogModel {
this.checkIn,
this.checkOut,
required this.activity,
required this.firstName,
required this.lastName,
required this.designation,
});
factory AttendanceLogModel.fromJson(Map<String, dynamic> json) {
@ -30,6 +36,9 @@ class AttendanceLogModel {
? DateTime.tryParse(json['checkOutTime'])
: null,
activity: json['activity'] ?? 0,
firstName: json['firstName'] ?? '',
lastName: json['lastName'] ?? '',
designation: json['jobRoleName'] ?? '',
);
}

View File

@ -1,4 +1,5 @@
import 'package:intl/intl.dart';
class AttendanceLogViewModel {
final DateTime? activityTime;
final String? imageUrl;
@ -7,6 +8,7 @@ class AttendanceLogViewModel {
final String? preSignedUrl;
final String? longitude;
final String? latitude;
final int? activity;
AttendanceLogViewModel({
this.activityTime,
@ -16,6 +18,7 @@ class AttendanceLogViewModel {
this.preSignedUrl,
this.longitude,
this.latitude,
required this.activity,
});
factory AttendanceLogViewModel.fromJson(Map<String, dynamic> json) {
@ -29,6 +32,7 @@ class AttendanceLogViewModel {
preSignedUrl: json['preSignedUrl']?.toString(),
longitude: json['longitude']?.toString(),
latitude: json['latitude']?.toString(),
activity: json['activity'] ?? 0,
);
}
@ -41,12 +45,14 @@ class AttendanceLogViewModel {
'preSignedUrl': preSignedUrl,
'longitude': longitude,
'latitude': latitude,
'activity': activity,
};
}
String? get formattedDate =>
activityTime != null ? DateFormat('yyyy-MM-dd').format(activityTime!) : null;
String? get formattedDate => activityTime != null
? DateFormat('yyyy-MM-dd').format(activityTime!)
: null;
String? get formattedTime =>
activityTime != null ? DateFormat('hh:mm a').format(activityTime!) : null;
}
}

View File

@ -4,7 +4,7 @@ class EmployeeInfo {
final String lastName;
final String gender;
final String birthDate;
final String joiningDate;
final String joiningDate;
final String currentAddress;
final String phoneNumber;
final String emergencyPhoneNumber;

View File

@ -3,8 +3,10 @@ class EmployeeModel {
final String employeeId;
final String name;
final String designation;
final String checkIn;
final String checkOut;
final DateTime? checkIn;
final DateTime? checkOut;
final String firstName;
final String lastName;
final int activity;
int action;
final String jobRole;
@ -16,8 +18,10 @@ class EmployeeModel {
required this.employeeId,
required this.name,
required this.designation,
required this.checkIn,
required this.checkOut,
this.checkIn,
this.checkOut,
required this.firstName,
required this.lastName,
required this.activity,
required this.action,
required this.jobRole,
@ -31,13 +35,19 @@ class EmployeeModel {
employeeId: json['employeeId']?.toString() ?? '',
name: '${json['firstName'] ?? ''} ${json['lastName'] ?? ''}'.trim(),
designation: json['jobRoleName'] ?? '',
checkIn: json['checkIn']?.toString() ?? '-',
checkOut: json['checkOut']?.toString() ?? '-',
checkIn: json['checkInTime'] != null
? DateTime.tryParse(json['checkInTime'])
: null,
checkOut: json['checkOutTime'] != null
? DateTime.tryParse(json['checkOutTime'])
: null,
action: json['action'] ?? 0,
activity: json['activity'] ?? 0,
jobRole: json['jobRole']?.toString() ?? '-',
email: json['email']?.toString() ?? '-',
phoneNumber: json['phoneNumber']?.toString() ?? '-',
email: json['email']?.toString() ?? '-',
phoneNumber: json['phoneNumber']?.toString() ?? '-',
firstName: json['firstName'] ?? '',
lastName: json['lastName'] ?? '',
);
}
@ -48,8 +58,8 @@ class EmployeeModel {
'firstName': name.split(' ').first,
'lastName': name.split(' ').length > 1 ? name.split(' ').last : '',
'jobRoleName': designation,
'checkIn': checkIn,
'checkOut': checkOut,
'checkInTime': checkIn?.toIso8601String(),
'checkOutTime': checkOut?.toIso8601String(),
'action': action,
'activity': activity,
'jobRole': jobRole.isEmpty ? '-' : jobRole,

View File

@ -6,6 +6,8 @@ class RegularizationLogModel {
final DateTime? checkIn;
final DateTime? checkOut;
final int activity;
final String firstName;
final String lastName;
RegularizationLogModel({
required this.id,
@ -15,6 +17,8 @@ class RegularizationLogModel {
this.checkIn,
this.checkOut,
required this.activity,
required this.firstName,
required this.lastName,
});
factory RegularizationLogModel.fromJson(Map<String, dynamic> json) {
@ -30,6 +34,8 @@ class RegularizationLogModel {
? DateTime.tryParse(json['checkOutTime'].toString())
: null,
activity: json['activity'] ?? 0,
firstName: json['firstName'] ?? '',
lastName: json['lastName'] ?? '',
);
}

View File

@ -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_500_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/add_employee_screen.dart';
import 'package:marco/view/dashboard/employee_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/comment_task_screen.dart';
import 'package:marco/view/dashboard/Attendence/attendance_screen.dart';
class AuthMiddleware extends GetMiddleware {
@override
@ -29,7 +30,7 @@ getPageRoute() {
var routes = [
GetPage(
name: '/',
page: () => const AttendanceScreen(),
page: () => AttendanceScreen(),
middlewares: [AuthMiddleware()]),
// Dashboard

View 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,
),
],
);
}),
),
);
}),
],
);
}
}

View 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