import 'package:flutter/material.dart'; import 'package:flutter_lucide/flutter_lucide.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/permission_constants.dart'; import 'package:marco/helpers/utils/attendance_actions.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_list_extension.dart'; import 'package:marco/helpers/widgets/my_loading_component.dart'; import 'package:marco/helpers/widgets/my_refresh_wrapper.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/utils/my_shadow.dart'; import 'package:marco/model/my_paginated_table.dart'; import 'package:marco/view/layouts/layout.dart'; import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; import 'package:intl/intl.dart'; import 'package:marco/controller/permission_controller.dart'; import 'package:url_launcher/url_launcher.dart'; class AttendanceScreen extends StatefulWidget { const 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()); @override Widget build(BuildContext context) { return Layout( child: MyRefreshWrapper( onRefresh: () async { if (attendanceController.selectedProjectId != null) { await attendanceController.fetchEmployeesByProject( attendanceController.selectedProjectId!); await attendanceController .fetchAttendanceLogs(attendanceController.selectedProjectId!); await attendanceController .fetchProjectData(attendanceController.selectedProjectId!); await attendanceController .fetchProjectData(attendanceController.selectedProjectId!); attendanceController.update(); } else { await attendanceController.fetchProjects(); } }, child: GetBuilder( init: attendanceController, tag: 'attendance_dashboard_controller', builder: (controller) { return LoadingComponent( isLoading: controller.isLoading.value, loadingText: 'Loading Attendance...', child: 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), Padding( padding: MySpacing.x(flexSpacing / 2), child: MyFlex( children: [ // Project Selection Dropdown MyFlexItem( sizes: 'lg-12', child: MyContainer.bordered( padding: MySpacing.xy(8, 8), child: PopupMenuButton( onSelected: (value) async { attendanceController.selectedProjectId = value; await attendanceController .fetchEmployeesByProject(value); await attendanceController .fetchAttendanceLogs(value); await attendanceController .fetchRegularizationLogs(value); await attendanceController .fetchProjectData(value); attendanceController.update(); }, itemBuilder: (BuildContext context) { if (attendanceController.projects.isEmpty) { return [ PopupMenuItem( value: '', child: MyText.bodySmall('No Data', fontWeight: 600), ) ]; } // Filter projects based on permissions final accessibleProjects = attendanceController .projects .where((project) => permissionController .isUserAssignedToProject( project.id.toString())) .toList(); if (accessibleProjects.isEmpty) { return [ PopupMenuItem( value: '', child: MyText.bodySmall( 'No Projects Assigned', fontWeight: 600), ) ]; } return accessibleProjects.map((project) { return PopupMenuItem( value: project.id.toString(), height: 32, child: MyText.bodySmall( project.name, color: theme.colorScheme.onSurface, fontWeight: 600, ), ); }).toList(); }, color: theme.cardTheme.color, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ MyText.labelSmall( attendanceController.selectedProjectId != null ? attendanceController.projects .firstWhereOrNull((proj) => proj.id == attendanceController .selectedProjectId) ?.name ?? 'Select a Project' : 'Select a Project', color: theme.colorScheme.onSurface, ), Icon(LucideIcons.chevron_down, size: 20, color: theme.colorScheme.onSurface), ], ), ), ), ), // Tab Section MyFlexItem( sizes: 'lg-12', child: Obx(() { bool hasRegularizationPermission = permissionController.hasPermission( Permissions.regularizeAttendance); final tabs = [ const Tab(text: 'Employee List'), const Tab(text: 'Logs'), if (hasRegularizationPermission) const Tab(text: 'Regularization'), ]; final views = [ employeeListTab(), reportsTab(context), if (hasRegularizationPermission) regularizationTab(context), ]; return DefaultTabController( length: tabs.length, child: MyCard.bordered( borderRadiusAll: 4, border: Border.all( color: Colors.grey.withAlpha(50)), shadow: MyShadow( elevation: 1, position: MyShadowPosition.bottom), paddingAll: 10, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TabBar( labelColor: theme.colorScheme.primary, unselectedLabelColor: theme .colorScheme.onSurface .withAlpha(150), tabs: tabs, ), MySpacing.height(16), SizedBox( height: 550, child: TabBarView(children: views), ), ], ), ), ); }), ), ], ), ), ], ), ); }, ), ), ); } Widget employeeListTab() { if (attendanceController.employees.isEmpty) { return Center( child: MyText.bodySmall("No Employees Assigned to This Project", fontWeight: 600), ); } final columns = [ DataColumn( label: MyText.labelLarge('Name', color: contentTheme.primary), ), DataColumn( label: MyText.labelLarge('Actions', color: contentTheme.primary), ), ]; final rows = attendanceController.employees.mapIndexed((index, employee) { int? activity = employee.activity; String buttonText = (activity == 0 || activity == 4) ? ButtonActions.checkIn : ButtonActions.checkOut; return DataRow(cells: [ DataCell( Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ MyText.bodyMedium(employee.name, fontWeight: 600), SizedBox(height: 2), MyText.bodySmall(employee.designation, color: Colors.grey), ], ), ), DataCell( Obx(() { final isUploading = attendanceController .uploadingStates[employee.employeeId]?.value ?? false; final controller = attendanceController; return SizedBox( width: 90, height: 25, child: ElevatedButton( onPressed: isUploading ? null : () async { controller.uploadingStates[employee.employeeId] = RxBool(true); if (controller.selectedProjectId == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Please select a project first")), ); controller.uploadingStates[employee.employeeId] = RxBool(false); return; } final updatedAction = (activity == 0 || activity == 4) ? 0 : 1; final actionText = (updatedAction == 0) ? ButtonActions.checkIn : ButtonActions.checkOut; final success = await controller.captureAndUploadAttendance( employee.id, employee.employeeId, controller.selectedProjectId!, comment: actionText, action: updatedAction, ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(success ? 'Attendance marked successfully!' : 'Image upload failed.'), ), ); controller.uploadingStates[employee.employeeId] = RxBool(false); if (success) { await Future.wait([ controller.fetchEmployeesByProject( controller.selectedProjectId!), controller.fetchAttendanceLogs( controller.selectedProjectId!), controller.fetchProjectData( controller.selectedProjectId!), ]); controller.update(); } }, style: ElevatedButton.styleFrom( backgroundColor: AttendanceActionColors.colors[buttonText], textStyle: const TextStyle(fontSize: 12), ), child: isUploading ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : Text(buttonText), ), ); }), ), ]); }).toList(); return Padding( padding: const EdgeInsets.all(0.0), child: SingleChildScrollView( child: MyPaginatedTable( columns: columns, rows: rows, ), ), ); } Widget reportsTab(BuildContext context) { final attendanceController = Get.find(); final groupedLogs = attendanceController.groupLogsByCheckInDate(); final columns = [ DataColumn(label: MyText.labelLarge('Name', color: contentTheme.primary)), DataColumn( label: MyText.labelLarge('Check-In', color: contentTheme.primary)), DataColumn( label: MyText.labelLarge('Check-Out', color: contentTheme.primary)), DataColumn(label: MyText.labelLarge('View', color: contentTheme.primary)), DataColumn( label: MyText.labelLarge('Action', color: contentTheme.primary)), ]; List rows = []; // Iterate over grouped logs groupedLogs.forEach((checkInDate, logs) { // Add a row for the check-in date as a header rows.add(DataRow(cells: [ DataCell(MyText.bodyLarge(checkInDate, fontWeight: 600)), DataCell(MyText.bodyMedium('')), DataCell(MyText.bodyMedium('')), DataCell(MyText.bodyMedium('')), DataCell(MyText.bodyMedium('')), ])); // Add rows for each log in this group for (var log in logs) { rows.add(DataRow(cells: [ DataCell( Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ MyText.bodyMedium(log.name, fontWeight: 600), SizedBox(height: 2), MyText.bodySmall(log.role, color: Colors.grey), ], ), ), DataCell( Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodyMedium( log.checkIn != null ? DateFormat('hh:mm a').format(log.checkIn!) : '', fontWeight: 600, ), ], ), ), DataCell( Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodyMedium( log.checkOut != null ? DateFormat('hh:mm a').format(log.checkOut!) : '', fontWeight: 600, ), ], ), ), DataCell( IconButton( icon: const Icon(Icons.visibility, size: 18), onPressed: () async { await attendanceController.fetchLogsView(log.id.toString()); showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), backgroundColor: Theme.of(context).cardColor, builder: (context) { return Padding( padding: EdgeInsets.only( left: 16, right: 16, top: 16, bottom: MediaQuery.of(context).viewInsets.bottom + 16, ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ MyText.titleMedium( "Attendance Log Details", fontWeight: 700, ), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context), ), ], ), const SizedBox(height: 16), if (attendanceController .attendenceLogsView.isNotEmpty) ...[ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: MyText.bodyMedium("Date", fontWeight: 600)), Expanded( child: MyText.bodyMedium("Time", fontWeight: 600)), Expanded( child: MyText.bodyMedium("Description", fontWeight: 600)), Expanded( child: MyText.bodyMedium("Image", fontWeight: 600)), ], ), const Divider(thickness: 1, height: 24), ], if (attendanceController .attendenceLogsView.isNotEmpty) ...attendanceController.attendenceLogsView .map((log) => Padding( padding: const EdgeInsets.symmetric( vertical: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: MyText.bodyMedium( log.formattedDate ?? '-', fontWeight: 600)), Expanded( child: MyText.bodyMedium( log.formattedTime ?? '-', fontWeight: 600)), Expanded( child: Row( children: [ if (log.latitude != null && log.longitude != null) GestureDetector( onTap: () async { final url = 'https://www.google.com/maps/search/?api=1&query=${log.latitude},${log.longitude}'; if (await canLaunchUrl( Uri.parse(url))) { await launchUrl( Uri.parse(url), mode: LaunchMode .externalApplication); } else { ScaffoldMessenger.of( context) .showSnackBar( const SnackBar( content: Text( 'Could not open Google Maps')), ); } }, child: const Padding( padding: EdgeInsets.only( right: 4.0), child: Icon( Icons.location_on, size: 18, color: Colors.blue), ), ), Expanded( child: MyText.bodyMedium( log.comment ?? '-', fontWeight: 600, ), ), ], ), ), Expanded( child: GestureDetector( onTap: () { if (log.preSignedUrl != null) { showDialog( context: context, builder: (_) => Dialog( child: Image.network( log.preSignedUrl!, fit: BoxFit.cover, height: 400, errorBuilder: (context, error, stackTrace) { return const Icon( Icons .broken_image, size: 50, color: Colors .grey); }, ), ), ); } }, child: log.thumbPreSignedUrl != null ? Image.network( log.thumbPreSignedUrl!, height: 40, width: 40, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return const Icon( Icons .broken_image, size: 40, color: Colors.grey); }, ) : const Icon( Icons.broken_image, size: 40, color: Colors.grey), ), ), ], ), )), ], ), ), ); }, ); }, ), ), DataCell( Obx(() { final uniqueLogKey = '${log.employeeId}_${log.id}'; final isUploading = attendanceController.uploadingStates[uniqueLogKey]?.value ?? false; final isYesterday = log.checkIn != null && log.checkOut != null && DateUtils.isSameDay(log.checkIn!, DateTime.now().subtract(Duration(days: 1))) && DateUtils.isSameDay(log.checkOut!, DateTime.now().subtract(Duration(days: 1))); final isTodayApproved = log.activity == 4 && DateUtils.isSameDay( log.checkIn ?? DateTime(2000), DateTime.now()); final isApprovedButNotToday = log.activity == 4 && !isTodayApproved; final isButtonDisabled = isUploading || isYesterday || log.activity == 2 || log.activity == 5 || isApprovedButNotToday; return SizedBox( width: 90, height: 25, child: ElevatedButton( onPressed: isButtonDisabled ? null : () async { attendanceController.uploadingStates[uniqueLogKey] = RxBool(true); if (attendanceController.selectedProjectId == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Please select a project first"), ), ); attendanceController.uploadingStates[uniqueLogKey] = RxBool(false); return; } int updatedAction; String actionText; bool imageCapture = true; if (log.activity == 0) { updatedAction = 0; actionText = "Check In"; } else if (log.activity == 1) { final twoDaysAgo = DateTime.now().subtract(Duration(days: 2)); if (log.checkOut == null && log.checkIn != null && log.checkIn!.isBefore(twoDaysAgo)) { updatedAction = 2; actionText = "Request Regularize"; imageCapture = false; } else if (log.checkOut != null && log.checkOut!.isBefore(twoDaysAgo)) { updatedAction = 2; actionText = "Request Regularize"; } else { updatedAction = 1; actionText = "Check Out"; } } else if (log.activity == 2) { updatedAction = 2; actionText = "Request Regularize"; } else if (isTodayApproved) { updatedAction = 0; actionText = "Check In"; } else { updatedAction = 0; actionText = "Unknown Action"; } final success = await attendanceController .captureAndUploadAttendance( log.id, log.employeeId, attendanceController.selectedProjectId!, comment: actionText, action: updatedAction, imageCapture: imageCapture, ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(success ? 'Attendance marked successfully!' : 'Failed to mark attendance.'), ), ); attendanceController.uploadingStates[uniqueLogKey] = RxBool(false); if (success) { attendanceController.fetchEmployeesByProject( attendanceController.selectedProjectId!); attendanceController.fetchAttendanceLogs( attendanceController.selectedProjectId!); await attendanceController.fetchRegularizationLogs( attendanceController.selectedProjectId!); await attendanceController.fetchProjectData( attendanceController.selectedProjectId!); attendanceController.update(); } }, style: ElevatedButton.styleFrom( backgroundColor: isYesterday ? Colors.grey : isTodayApproved ? Colors.green : AttendanceActionColors.colors[(log.activity == 0) ? ButtonActions.checkIn : ButtonActions.checkOut], padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6), textStyle: const TextStyle(fontSize: 12), ), child: isUploading ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : Text( log.activity == 5 ? ButtonActions.rejected : isTodayApproved ? ButtonActions.checkIn : log.activity == 4 ? ButtonActions.approved : log.activity == 2 ? ButtonActions.requested : (log.activity == 0 && !(log.checkIn != null && log.checkOut != null && !DateUtils.isSameDay( log.checkIn!, DateTime.now()))) ? ButtonActions.checkIn : (log.activity == 1 && log.checkOut != null && DateTime.now() .difference( log.checkOut!) .inDays <= 2) ? ButtonActions.checkOut : (log.activity == 1 && log.checkOut == null && log.checkIn != null && log.checkIn!.isBefore( DateTime.now() .subtract( Duration( days: 2)))) ? ButtonActions .requestRegularize : ButtonActions.checkOut, ), ), ); }), ) ])); } }); return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(8.0), child: TextButton.icon( icon: const Icon(Icons.date_range), label: const Text("Select Date Range for Attendance"), onPressed: () => attendanceController .selectDateRangeForAttendance(context, attendanceController), ), ), if (attendanceController.attendanceLogs.isEmpty) Align( alignment: Alignment.center, child: Padding( padding: const EdgeInsets.symmetric(vertical: 40), child: MyText.bodySmall( "No Attendance Records Found", fontWeight: 600, ), ), ) else SingleChildScrollView( child: MyPaginatedTable( columns: columns, rows: rows, ), ), ], ), ); } Widget regularizationTab(BuildContext context) { final attendanceController = Get.find(); final columns = [ DataColumn(label: MyText.labelLarge('Name', color: contentTheme.primary)), DataColumn( label: MyText.labelLarge('Check-In', color: contentTheme.primary)), DataColumn( label: MyText.labelLarge('Check-Out', color: contentTheme.primary)), DataColumn( label: MyText.labelLarge('Action', color: contentTheme.primary)), ]; final rows = attendanceController.regularizationLogs.mapIndexed((index, log) { final uniqueLogKey = '${log.id}-${log.employeeId}'; // Unique key for each log final isUploading = attendanceController.uploadingStates[uniqueLogKey]?.value ?? false; // Check the upload state return DataRow(cells: [ DataCell( Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ MyText.bodyMedium(log.name, fontWeight: 600), SizedBox(height: 2), MyText.bodySmall(log.role, color: Colors.grey), ], ), ), DataCell( Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodyMedium( log.checkIn != null ? DateFormat('dd/MMM/yyyy').format(log.checkIn!) : '-', fontWeight: 600, ), MyText.bodyMedium( log.checkIn != null ? DateFormat('hh:mm a').format(log.checkIn!) : '', fontWeight: 600, ), ], ), ), DataCell( Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodyMedium( log.checkOut != null ? DateFormat('dd MMM yyyy').format(log.checkOut!) : '-', fontWeight: 600, ), MyText.bodyMedium( log.checkOut != null ? DateFormat('hh:mm a').format(log.checkOut!) : '', fontWeight: 600, ), ], ), ), DataCell( Row( children: [ // Approve Button ElevatedButton( onPressed: isUploading // Disable button if uploading ? null : () async { if (attendanceController.selectedProjectId == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Please select a project first")), ); return; } attendanceController.uploadingStates[uniqueLogKey] ?.value = true; // Start loading final success = await attendanceController .captureAndUploadAttendance( log.id, log.employeeId, attendanceController.selectedProjectId!, comment: "Accepted", action: 4, // Approve action imageCapture: false, ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(success ? 'Approval marked successfully!' : 'Failed to mark approval.')), ); if (success) { attendanceController.fetchEmployeesByProject( attendanceController.selectedProjectId!); attendanceController.fetchAttendanceLogs( attendanceController.selectedProjectId!); await attendanceController.fetchRegularizationLogs( attendanceController.selectedProjectId!); await attendanceController.fetchProjectData( attendanceController.selectedProjectId!); } attendanceController.uploadingStates[uniqueLogKey] ?.value = false; // End loading }, style: ElevatedButton.styleFrom( backgroundColor: AttendanceActionColors.colors[ButtonActions.approve], padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6), minimumSize: const Size(60, 20), textStyle: const TextStyle(fontSize: 12), ), child: isUploading ? const CircularProgressIndicator( strokeWidth: 2) // Show loading indicator while uploading : const Text("Approve"), ), // Space between buttons const SizedBox(width: 8), // Reject Button ElevatedButton( onPressed: isUploading // Disable button if uploading ? null : () async { if (attendanceController.selectedProjectId == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Please select a project first")), ); return; } attendanceController.uploadingStates[uniqueLogKey] ?.value = true; // Start loading final success = await attendanceController .captureAndUploadAttendance( log.id, log.employeeId, attendanceController.selectedProjectId!, comment: "Rejected", action: 5, // Reject action imageCapture: false, ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(success ? 'Attendance marked as Rejected!' : 'Failed to mark attendance.')), ); if (success) { attendanceController.fetchEmployeesByProject( attendanceController.selectedProjectId!); attendanceController.fetchAttendanceLogs( attendanceController.selectedProjectId!); await attendanceController.fetchRegularizationLogs( attendanceController.selectedProjectId!); await attendanceController.fetchProjectData( attendanceController.selectedProjectId!); } attendanceController.uploadingStates[uniqueLogKey] ?.value = false; // End loading }, style: ElevatedButton.styleFrom( backgroundColor: AttendanceActionColors.colors[ButtonActions.reject], padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6), minimumSize: const Size(60, 20), textStyle: const TextStyle(fontSize: 12), ), child: isUploading ? const CircularProgressIndicator( strokeWidth: 2) // Show loading indicator while uploading : const Text("Reject"), ), ], ), ), ]); }).toList(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(8.0), ), if (attendanceController.regularizationLogs.isEmpty) Expanded( child: Center( child: MyText.bodySmall("No Regularization Records Found", fontWeight: 600), ), ) else Expanded( child: SingleChildScrollView( child: MyPaginatedTable( columns: columns, rows: rows, columnSpacing: 15.0, ), ), ), ], ); } }