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

This commit is contained in:
Vaibhav Surve 2025-05-21 17:57:09 +05:30
parent 5c2c2995ef
commit 8408b67aa0
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? 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'] ?? '',
); );
} }

View File

@ -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,12 +45,14 @@ 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;
} }

View File

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

View File

@ -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,

View File

@ -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'] ?? '',
); );
} }

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_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

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