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 createState() => _AttendanceScreenState(); } class _AttendanceScreenState extends State 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( 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>( 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, ), ), ), ), ), ], ), 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( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: MyText.titleMedium( "Attendance Logs", fontWeight: 600, ), ), Obx(() { if (attendanceController.isLoading.value) { return const SizedBox( height: 20, width: 20, child: LinearProgressIndicator(), ); } final dateFormat = DateFormat('dd MMM yyyy'); final dateRangeText = attendanceController .startDateAttendance != null && attendanceController.endDateAttendance != null ? '${dateFormat.format(attendanceController.endDateAttendance!)} - ${dateFormat.format(attendanceController.startDateAttendance!)}' : 'Select date range'; return MyText.bodySmall( dateRangeText, fontWeight: 600, color: Colors.grey[700], overflow: TextOverflow.ellipsis, ); }), ], ), ), 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, ), ], ); }), ), ); }), ], ); } }