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_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/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'; import 'package:marco/controller/project_controller.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 void initState() { super.initState(); final projectController = Get.find(); final attendanceController = Get.find(); WidgetsBinding.instance.addPostFrameCallback((_) async { // Listen for future changes in selected project ever(projectController.selectedProjectId!, (projectId) async { if (projectId != null && projectId.isNotEmpty) { try { await attendanceController.loadAttendanceData(projectId); attendanceController.update(['attendance_dashboard_controller']); } catch (e) { debugPrint("Error updating data on project change: $e"); } } }); // Load data initially if project is already selected final initialProjectId = projectController.selectedProjectId?.value; if (initialProjectId != null && initialProjectId.isNotEmpty) { try { await attendanceController.loadAttendanceData(initialProjectId); attendanceController.update(['attendance_dashboard_controller']); } catch (e) { debugPrint("Error loading initial data: $e"); } } }); } @override Widget build(BuildContext context) { return Scaffold( appBar: PreferredSize( preferredSize: const Size.fromHeight(80), child: AppBar( backgroundColor: const Color(0xFFF5F5F5), elevation: 0.5, foregroundColor: Colors.black, titleSpacing: 0, leading: IconButton( icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black, size: 20), onPressed: () { Get.offNamed('/dashboard'); }, ), title: Padding( padding: const EdgeInsets.only(top: 12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ MyText.titleLarge( 'Attendance', fontWeight: 700, color: Colors.black, ), const SizedBox(height: 4), GetBuilder( builder: (projectController) { final projectName = projectController.selectedProject?.name ?? 'Select Project'; return MyText.bodySmall( projectName, fontWeight: 600, maxLines: 1, overflow: TextOverflow.ellipsis, color: Colors.grey[700], ); }, ), ], ), ), ), ), body: SafeArea( child: SingleChildScrollView( padding: MySpacing.x(0), child: GetBuilder( init: attendanceController, tag: 'attendance_dashboard_controller', builder: (controller) { final selectedProjectId = Get.find().selectedProjectId?.value; final bool noProjectSelected = selectedProjectId == null || selectedProjectId.isEmpty; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MySpacing.height(flexSpacing), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ MyText.bodyMedium("Filter", fontWeight: 600), Tooltip( message: 'Filter Project', child: InkWell( borderRadius: BorderRadius.circular(24), onTap: () async { final result = await showModalBottomSheet< Map>( 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 = Get.find() .selectedProjectId ?.value; final selectedView = result['selectedTab'] as String?; if (selectedProjectId != null) { 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 = Get.find() .selectedProjectId ?.value; if (projectId != null && projectId.isNotEmpty) { try { await attendanceController .loadAttendanceData(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), MyFlex(children: [ MyFlexItem( sizes: 'lg-12 md-12 sm-12', child: noProjectSelected ? Center( child: Padding( padding: const EdgeInsets.all(24.0), child: MyText.titleMedium( 'No Records Found', fontWeight: 600, color: Colors.grey[600], ), ), ) : selectedTab == 'todaysAttendance' ? employeeListTab() : selectedTab == 'attendanceLogs' ? employeeLog() : regularizationScreen(), ), ]), ], ); }, ), ), ), ); } String _formatDate(DateTime date) { return "${date.day}/${date.month}/${date.year}"; } 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, ), ), MyText.bodySmall( _formatDate(DateTime.now()), fontWeight: 600, color: Colors.grey[700], overflow: TextOverflow.ellipsis, ), ], ), ), if (isLoading) const SizedBox( height: 120, child: Center(child: CircularProgressIndicator()), ) else if (employees.isEmpty) SizedBox( height: 120, child: Center( child: MyText.bodySmall( "No Employees Assigned to This Project", fontWeight: 600, ), ), ) else MyCard.bordered( borderRadiusAll: 4, border: Border.all(color: Colors.grey.withOpacity(0.2)), shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom), paddingAll: 8, child: Column( children: List.generate(employees.length, (index) { final employee = employees[index]; return Column( children: [ Padding( padding: const 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: [ Wrap( crossAxisAlignment: WrapCrossAlignment.center, spacing: 6, // spacing between name and designation children: [ MyText.bodyMedium( employee.name, fontWeight: 600, overflow: TextOverflow.visible, maxLines: null, ), MyText.bodySmall( '(${employee.designation})', fontWeight: 600, overflow: TextOverflow.visible, maxLines: null, color: Colors.grey[700], ), ], ), MySpacing.height(8), (employee.checkIn != null || employee.checkOut != null) ? Row( children: [ if (employee.checkIn != null) ...[ const 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) ...[ const 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, ), ), ], ], ) : const 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, ); }), ], ), ), if (attendanceController.isLoadingAttendanceLogs.value) const SizedBox( height: 120, child: Center(child: CircularProgressIndicator()), ) else if (logs.isEmpty) SizedBox( height: 120, child: Center( child: MyText.bodySmall( "No Attendance Logs Found for this Project", fontWeight: 600, ), ), ) else 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: 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: const 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) ...[ const 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) ...[ const 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, ), ), ], ], ) : const 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; if (attendanceController.isLoadingRegularizationLogs.value) { return SizedBox( height: 120, child: const Center(child: CircularProgressIndicator()), ); } if (employees.isEmpty) { return SizedBox( height: 120, child: Center( child: MyText.bodySmall( "No Regularization Requests Found for this Project", fontWeight: 600, ), ), ); } 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( children: List.generate(employees.length, (index) { final employee = employees[index]; return Column( children: [ Padding( padding: const 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) ...[ const 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) ...[ const 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, ), const SizedBox(width: 8), if (employee.checkIn != null) ...[ AttendanceLogViewButton( employee: employee, attendanceController: attendanceController, ), ], ], ), ], ), ), ], ), ), ), if (index != employees.length - 1) Divider( color: Colors.grey.withOpacity(0.3), thickness: 1, height: 1, ), ], ); }), ), ); }), ], ); } }