diff --git a/lib/controller/dashboard/attendance_screen_controller.dart b/lib/controller/dashboard/attendance_screen_controller.dart index f477be9..339067e 100644 --- a/lib/controller/dashboard/attendance_screen_controller.dart +++ b/lib/controller/dashboard/attendance_screen_controller.dart @@ -1,22 +1,25 @@ import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:geolocator/geolocator.dart'; import 'package:intl/intl.dart'; -import 'package:marco/helpers/services/app_logger.dart'; + +import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/widgets/my_image_compressor.dart'; + import 'package:marco/model/attendance_model.dart'; import 'package:marco/model/project_model.dart'; import 'package:marco/model/employee_model.dart'; import 'package:marco/model/attendance_log_model.dart'; import 'package:marco/model/regularization_log_model.dart'; import 'package:marco/model/attendance_log_view_model.dart'; + import 'package:marco/controller/project_controller.dart'; class AttendanceController extends GetxController { + // Data models List attendances = []; List projects = []; List employees = []; @@ -24,19 +27,18 @@ class AttendanceController extends GetxController { List regularizationLogs = []; List attendenceLogsView = []; + // States String selectedTab = 'Employee List'; - DateTime? startDateAttendance; DateTime? endDateAttendance; - RxBool isLoading = true.obs; - RxBool isLoadingProjects = true.obs; - RxBool isLoadingEmployees = true.obs; - RxBool isLoadingAttendanceLogs = true.obs; - RxBool isLoadingRegularizationLogs = true.obs; - RxBool isLoadingLogView = true.obs; - - RxMap uploadingStates = {}.obs; + final isLoading = true.obs; + final isLoadingProjects = true.obs; + final isLoadingEmployees = true.obs; + final isLoadingAttendanceLogs = true.obs; + final isLoadingRegularizationLogs = true.obs; + final isLoadingLogView = true.obs; + final uploadingStates = {}.obs; @override void onInit() { @@ -56,76 +58,46 @@ class AttendanceController extends GetxController { logSafe("Default date range set: $startDateAttendance to $endDateAttendance"); } - Future _handleLocationPermission() async { - LocationPermission permission = await Geolocator.checkPermission(); - if (permission == LocationPermission.denied) { - permission = await Geolocator.requestPermission(); - if (permission == LocationPermission.denied) { - logSafe('Location permissions are denied', level: LogLevel.warning); - return false; - } - } - if (permission == LocationPermission.deniedForever) { - logSafe('Location permissions are permanently denied', level: LogLevel.error); - return false; - } - return true; - } + // ------------------ Project & Employee ------------------ Future fetchProjects() async { isLoadingProjects.value = true; - isLoading.value = true; final response = await ApiService.getProjects(); if (response != null && response.isNotEmpty) { - projects = response.map((json) => ProjectModel.fromJson(json)).toList(); + projects = response.map((e) => ProjectModel.fromJson(e)).toList(); logSafe("Projects fetched: ${projects.length}"); } else { - logSafe("Failed to fetch projects or no projects available.", level: LogLevel.error); projects = []; + logSafe("Failed to fetch projects or no projects available.", level: LogLevel.error); } isLoadingProjects.value = false; - isLoading.value = false; update(['attendance_dashboard_controller']); } - Future loadAttendanceData(String projectId) async { - await fetchEmployeesByProject(projectId); - await fetchAttendanceLogs(projectId); - await fetchRegularizationLogs(projectId); - await fetchProjectData(projectId); - } - - Future fetchProjectData(String? projectId) async { - if (projectId == null) return; - isLoading.value = true; - await Future.wait([ - fetchEmployeesByProject(projectId), - fetchAttendanceLogs(projectId, dateFrom: startDateAttendance, dateTo: endDateAttendance), - fetchRegularizationLogs(projectId), - ]); - isLoading.value = false; - logSafe("Project data fetched for project ID: $projectId"); - } - Future fetchEmployeesByProject(String? projectId) async { if (projectId == null) return; + isLoadingEmployees.value = true; + final response = await ApiService.getEmployeesByProject(projectId); if (response != null) { - employees = response.map((json) => EmployeeModel.fromJson(json)).toList(); + employees = response.map((e) => EmployeeModel.fromJson(e)).toList(); for (var emp in employees) { uploadingStates[emp.id] = false.obs; } logSafe("Employees fetched: ${employees.length} for project $projectId"); - update(); } else { logSafe("Failed to fetch employees for project $projectId", level: LogLevel.error); } + isLoadingEmployees.value = false; + update(); } + // ------------------ Attendance Capture ------------------ + Future captureAndUploadAttendance( String id, String employeeId, @@ -137,6 +109,7 @@ class AttendanceController extends GetxController { }) async { try { uploadingStates[employeeId]?.value = true; + XFile? image; if (imageCapture) { image = await ImagePicker().pickImage(source: ImageSource.camera, imageQuality: 80); @@ -144,24 +117,39 @@ class AttendanceController extends GetxController { logSafe("Image capture cancelled.", level: LogLevel.warning); return false; } + final compressedBytes = await compressImageToUnder100KB(File(image.path)); if (compressedBytes == null) { logSafe("Image compression failed.", level: LogLevel.error); return false; } + final compressedFile = await saveCompressedImageToFile(compressedBytes); image = XFile(compressedFile.path); } - final hasLocationPermission = await _handleLocationPermission(); - if (!hasLocationPermission) return false; - final position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high); - final imageName = imageCapture ? ApiService.generateImageName(employeeId, employees.length + 1) : ""; + + if (!await _handleLocationPermission()) return false; + final position = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.high); + + final imageName = imageCapture + ? ApiService.generateImageName(employeeId, employees.length + 1) + : ""; final result = await ApiService.uploadAttendanceImage( - id, employeeId, image, position.latitude, position.longitude, - imageName: imageName, projectId: projectId, comment: comment, - action: action, imageCapture: imageCapture, markTime: markTime, + id, + employeeId, + image, + position.latitude, + position.longitude, + imageName: imageName, + projectId: projectId, + comment: comment, + action: action, + imageCapture: imageCapture, + markTime: markTime, ); + logSafe("Attendance uploaded for $employeeId, action: $action"); return result; } catch (e, stacktrace) { @@ -172,8 +160,133 @@ class AttendanceController extends GetxController { } } - Future selectDateRangeForAttendance(BuildContext context, AttendanceController controller) async { + Future _handleLocationPermission() async { + LocationPermission permission = await Geolocator.checkPermission(); + + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied) { + logSafe('Location permissions are denied', level: LogLevel.warning); + return false; + } + } + + if (permission == LocationPermission.deniedForever) { + logSafe('Location permissions are permanently denied', level: LogLevel.error); + return false; + } + + return true; + } + + // ------------------ Attendance Logs ------------------ + + Future fetchAttendanceLogs(String? projectId, {DateTime? dateFrom, DateTime? dateTo}) async { + if (projectId == null) return; + + isLoadingAttendanceLogs.value = true; + + final response = await ApiService.getAttendanceLogs(projectId, dateFrom: dateFrom, dateTo: dateTo); + if (response != null) { + attendanceLogs = response.map((e) => AttendanceLogModel.fromJson(e)).toList(); + logSafe("Attendance logs fetched: ${attendanceLogs.length}"); + } else { + logSafe("Failed to fetch attendance logs for project $projectId", level: LogLevel.error); + } + + isLoadingAttendanceLogs.value = false; + update(); + } + + Map> groupLogsByCheckInDate() { + final groupedLogs = >{}; + + for (var logItem in attendanceLogs) { + final checkInDate = logItem.checkIn != null + ? DateFormat('dd MMM yyyy').format(logItem.checkIn!) + : 'Unknown'; + groupedLogs.putIfAbsent(checkInDate, () => []).add(logItem); + } + + final sortedEntries = groupedLogs.entries.toList() + ..sort((a, b) { + if (a.key == 'Unknown') return 1; + if (b.key == 'Unknown') return -1; + final dateA = DateFormat('dd MMM yyyy').parse(a.key); + final dateB = DateFormat('dd MMM yyyy').parse(b.key); + return dateB.compareTo(dateA); + }); + + return Map>.fromEntries(sortedEntries); + } + + // ------------------ Regularization Logs ------------------ + + Future fetchRegularizationLogs(String? projectId) async { + if (projectId == null) return; + + isLoadingRegularizationLogs.value = true; + + final response = await ApiService.getRegularizationLogs(projectId); + if (response != null) { + regularizationLogs = response.map((e) => RegularizationLogModel.fromJson(e)).toList(); + logSafe("Regularization logs fetched: ${regularizationLogs.length}"); + } else { + logSafe("Failed to fetch regularization logs for project $projectId", level: LogLevel.error); + } + + isLoadingRegularizationLogs.value = false; + update(); + } + + // ------------------ Attendance Log View ------------------ + + Future fetchLogsView(String? id) async { + if (id == null) return; + + isLoadingLogView.value = true; + + final response = await ApiService.getAttendanceLogView(id); + if (response != null) { + attendenceLogsView = response.map((e) => AttendanceLogViewModel.fromJson(e)).toList(); + attendenceLogsView.sort((a, b) => + (b.activityTime ?? DateTime(2000)).compareTo(a.activityTime ?? DateTime(2000))); + logSafe("Attendance log view fetched for ID: $id"); + } else { + logSafe("Failed to fetch attendance log view for ID $id", level: LogLevel.error); + } + + isLoadingLogView.value = false; + update(); + } + + // ------------------ Combined Load ------------------ + + Future loadAttendanceData(String projectId) async { + isLoading.value = true; + await fetchProjectData(projectId); + isLoading.value = false; + } + + Future fetchProjectData(String? projectId) async { + if (projectId == null) return; + + await Future.wait([ + fetchEmployeesByProject(projectId), + fetchAttendanceLogs(projectId, + dateFrom: startDateAttendance, dateTo: endDateAttendance), + fetchRegularizationLogs(projectId), + ]); + + logSafe("Project data fetched for project ID: $projectId"); + } + + // ------------------ UI Interaction ------------------ + + Future selectDateRangeForAttendance( + BuildContext context, AttendanceController controller) async { final today = DateTime.now(); + final picked = await showDateRangePicker( context: context, firstDate: DateTime(2022), @@ -190,14 +303,13 @@ class AttendanceController extends GetxController { child: Theme( data: Theme.of(context).copyWith( colorScheme: ColorScheme.light( - primary: const Color.fromARGB(255, 95, 132, 255), + primary: const Color(0xFF5F84FF), onPrimary: Colors.white, onSurface: Colors.teal.shade800, ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom(foregroundColor: Colors.teal), ), - dialogTheme: DialogThemeData(backgroundColor: Colors.white), ), child: child!, ), @@ -210,6 +322,7 @@ class AttendanceController extends GetxController { startDateAttendance = picked.start; endDateAttendance = picked.end; logSafe("Date range selected: $startDateAttendance to $endDateAttendance"); + await controller.fetchAttendanceLogs( Get.find().selectedProject?.id, dateFrom: picked.start, @@ -217,78 +330,4 @@ class AttendanceController extends GetxController { ); } } - - Future fetchAttendanceLogs(String? projectId, {DateTime? dateFrom, DateTime? dateTo}) async { - if (projectId == null) return; - isLoadingAttendanceLogs.value = true; - isLoading.value = true; - final response = await ApiService.getAttendanceLogs(projectId, dateFrom: dateFrom, dateTo: dateTo); - if (response != null) { - attendanceLogs = response.map((json) => AttendanceLogModel.fromJson(json)).toList(); - logSafe("Attendance logs fetched: ${attendanceLogs.length}"); - update(); - } else { - logSafe("Failed to fetch attendance logs for project $projectId", level: LogLevel.error); - } - isLoadingAttendanceLogs.value = false; - isLoading.value = false; - } - - Map> groupLogsByCheckInDate() { - final groupedLogs = >{}; - for (var logItem in attendanceLogs) { - final checkInDate = logItem.checkIn != null - ? DateFormat('dd MMM yyyy').format(logItem.checkIn!) - : 'Unknown'; - groupedLogs.putIfAbsent(checkInDate, () => []); - groupedLogs[checkInDate]!.add(logItem); - } - final sortedEntries = groupedLogs.entries.toList() - ..sort((a, b) { - if (a.key == 'Unknown') return 1; - if (b.key == 'Unknown') return -1; - final dateA = DateFormat('dd MMM yyyy').parse(a.key); - final dateB = DateFormat('dd MMM yyyy').parse(b.key); - return dateB.compareTo(dateA); - }); - final sortedMap = Map>.fromEntries(sortedEntries); - logSafe("Logs grouped and sorted by check-in date."); - return sortedMap; - } - - Future fetchRegularizationLogs(String? projectId, {DateTime? dateFrom, DateTime? dateTo}) async { - if (projectId == null) return; - isLoadingRegularizationLogs.value = true; - isLoading.value = true; - final response = await ApiService.getRegularizationLogs(projectId); - if (response != null) { - regularizationLogs = response.map((json) => RegularizationLogModel.fromJson(json)).toList(); - logSafe("Regularization logs fetched: ${regularizationLogs.length}"); - update(); - } else { - logSafe("Failed to fetch regularization logs for project $projectId", level: LogLevel.error); - } - isLoadingRegularizationLogs.value = false; - isLoading.value = false; - } - - Future fetchLogsView(String? id) async { - if (id == null) return; - isLoadingLogView.value = true; - isLoading.value = true; - final response = await ApiService.getAttendanceLogView(id); - if (response != null) { - attendenceLogsView = response.map((json) => AttendanceLogViewModel.fromJson(json)).toList(); - attendenceLogsView.sort((a, b) { - if (a.activityTime == null || b.activityTime == null) return 0; - return b.activityTime!.compareTo(a.activityTime!); - }); - logSafe("Attendance log view fetched for ID: $id"); - update(); - } else { - logSafe("Failed to fetch attendance log view for ID $id", level: LogLevel.error); - } - isLoadingLogView.value = false; - isLoading.value = false; - } } diff --git a/lib/model/attendance/attendence_filter_sheet.dart b/lib/model/attendance/attendence_filter_sheet.dart index 4b11265..32fbc43 100644 --- a/lib/model/attendance/attendence_filter_sheet.dart +++ b/lib/model/attendance/attendence_filter_sheet.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import 'package:marco/controller/permission_controller.dart'; import 'package:marco/controller/dashboard/attendance_screen_controller.dart'; -import 'package:intl/intl.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; @@ -19,7 +19,7 @@ class AttendanceFilterBottomSheet extends StatefulWidget { }); @override - _AttendanceFilterBottomSheetState createState() => + State createState() => _AttendanceFilterBottomSheetState(); } @@ -54,22 +54,20 @@ class _AttendanceFilterBottomSheetState {'label': 'Regularization Requests', 'value': 'regularizationRequests'}, ]; - final filteredViewOptions = viewOptions.where((item) { - if (item['value'] == 'regularizationRequests') { - return hasRegularizationPermission; - } - return true; + final filteredOptions = viewOptions.where((item) { + return item['value'] != 'regularizationRequests' || + hasRegularizationPermission; }).toList(); - List widgets = [ + final List widgets = [ Padding( - padding: const EdgeInsets.only(bottom: 4), + padding: EdgeInsets.only(bottom: 4), child: Align( alignment: Alignment.centerLeft, child: MyText.titleSmall("View", fontWeight: 600), ), ), - ...filteredViewOptions.map((item) { + ...filteredOptions.map((item) { return RadioListTile( dense: true, contentPadding: EdgeInsets.zero, @@ -81,14 +79,14 @@ class _AttendanceFilterBottomSheetState groupValue: tempSelectedTab, onChanged: (value) => setState(() => tempSelectedTab = value!), ); - }).toList(), + }), ]; if (tempSelectedTab == 'attendanceLogs') { widgets.addAll([ const Divider(), Padding( - padding: const EdgeInsets.only(top: 12, bottom: 4), + padding: EdgeInsets.only(top: 12, bottom: 4), child: Align( alignment: Alignment.centerLeft, child: MyText.titleSmall("Date Range", fontWeight: 600), diff --git a/lib/view/dashboard/Attendence/attendance_logs_tab.dart b/lib/view/dashboard/Attendence/attendance_logs_tab.dart new file mode 100644 index 0000000..f456b7d --- /dev/null +++ b/lib/view/dashboard/Attendence/attendance_logs_tab.dart @@ -0,0 +1,189 @@ +// lib/view/attendance/tabs/attendance_logs_tab.dart +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/widgets/avatar.dart'; +import 'package:marco/helpers/widgets/my_card.dart'; +import 'package:marco/helpers/widgets/my_container.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; +import 'package:marco/model/attendance/log_details_view.dart'; +import 'package:marco/model/attendance/attendence_action_button.dart'; + +class AttendanceLogsTab extends StatelessWidget { + final AttendanceController controller; + + const AttendanceLogsTab({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return Obx(() { + final logs = List.of(controller.attendanceLogs); + logs.sort((a, b) { + final aDate = a.checkIn ?? DateTime(0); + final bDate = b.checkIn ?? DateTime(0); + return bDate.compareTo(aDate); + }); + + final dateFormat = DateFormat('dd MMM yyyy'); + final dateRangeText = controller.startDateAttendance != null && + controller.endDateAttendance != null + ? '${dateFormat.format(controller.endDateAttendance!)} - ${dateFormat.format(controller.startDateAttendance!)}' + : 'Select date range'; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MyText.titleMedium("Attendance Logs", fontWeight: 600), + controller.isLoading.value + ? const SizedBox( + height: 20, width: 20, child: LinearProgressIndicator()) + : MyText.bodySmall( + dateRangeText, + fontWeight: 600, + color: Colors.grey[700], + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + if (controller.isLoadingAttendanceLogs.value) + SkeletonLoaders.employeeListSkeletonLoader() + else if (logs.isEmpty) + const SizedBox( + height: 120, + child: Center( + child: Text("No Attendance Logs Found for this Project"), + ), + ) + else + MyCard.bordered( + paddingAll: 8, + child: 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, + ), + ), + 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, + ), + ), + MySpacing.width(6), + Flexible( + child: MyText.bodySmall( + '(${employee.designation})', + fontWeight: 600, + color: Colors.grey[700], + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + MySpacing.height(8), + if (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), + MyText.bodySmall( + DateFormat('hh:mm a') + .format(employee.checkIn!), + fontWeight: 600, + ), + MySpacing.width(16), + ], + if (employee.checkOut != null) ...[ + const Icon(Icons.arrow_circle_left, + size: 16, color: Colors.red), + MySpacing.width(4), + MyText.bodySmall( + DateFormat('hh:mm a') + .format(employee.checkOut!), + fontWeight: 600, + ), + ], + ], + ), + MySpacing.height(12), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AttendanceActionButton( + employee: employee, + attendanceController: controller, + ), + MySpacing.width(8), + AttendanceLogViewButton( + employee: employee, + attendanceController: controller, + ), + ], + ), + ], + ), + ), + ], + ), + ), + if (index != logs.length - 1) + Divider(color: Colors.grey.withOpacity(0.3)), + ], + ); + }), + ), + ), + ], + ); + }); + } +} diff --git a/lib/view/dashboard/Attendence/attendance_screen.dart b/lib/view/dashboard/Attendence/attendance_screen.dart index f2fcda7..ebdd68d 100644 --- a/lib/view/dashboard/Attendence/attendance_screen.dart +++ b/lib/view/dashboard/Attendence/attendance_screen.dart @@ -2,845 +2,240 @@ 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'; -import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; +import 'package:marco/view/dashboard/Attendence/regularization_requests_tab.dart'; +import 'package:marco/view/dashboard/Attendence/attendance_logs_tab.dart'; +import 'package:marco/view/dashboard/Attendence/todays_attendance_tab.dart'; class AttendanceScreen extends StatefulWidget { - AttendanceScreen({super.key}); + 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()); + final attendanceController = Get.put(AttendanceController()); + final permissionController = Get.put(PermissionController()); + final projectController = Get.find(); 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"); - } - } + WidgetsBinding.instance.addPostFrameCallback((_) { + // Listen for future project selection changes + ever(projectController.selectedProjectId, (projectId) async { + if (projectId.isNotEmpty) await _loadData(projectId); }); - // 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"); - } - } + // Load initial data + final projectId = projectController.selectedProjectId.value; + if (projectId.isNotEmpty) _loadData(projectId); }); } + Future _loadData(String projectId) async { + try { + await attendanceController.loadAttendanceData(projectId); + attendanceController.update(['attendance_dashboard_controller']); + } catch (e) { + debugPrint("Error loading data: $e"); + } + } + + Future _refreshData() async { + final projectId = projectController.selectedProjectId.value; + if (projectId.isNotEmpty) await _loadData(projectId); + } + + Widget _buildAppBar() { + return AppBar( + backgroundColor: const Color(0xFFF5F5F5), + elevation: 0.5, + automaticallyImplyLeading: false, + titleSpacing: 0, + title: Padding( + padding: MySpacing.xy(16, 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black, size: 20), + onPressed: () => Get.offNamed('/dashboard'), + ), + MySpacing.width(8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleLarge('Attendance', fontWeight: 700, color: Colors.black), + MySpacing.height(2), + GetBuilder( + builder: (projectController) { + final projectName = projectController.selectedProject?.name ?? 'Select Project'; + return Row( + children: [ + const Icon(Icons.work_outline, size: 14, color: Colors.grey), + MySpacing.width(4), + Expanded( + child: MyText.bodySmall( + projectName, + fontWeight: 600, + overflow: TextOverflow.ellipsis, + color: Colors.grey[700], + ), + ), + ], + ); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildFilterAndRefreshRow() { + return 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>( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(12)), + ), + builder: (context) => AttendanceFilterBottomSheet( + controller: attendanceController, + permissionController: permissionController, + selectedTab: selectedTab, + ), + ); + + if (result != null) { + final selectedProjectId = projectController.selectedProjectId.value; + final selectedView = result['selectedTab'] as String?; + + if (selectedProjectId.isNotEmpty) { + 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: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon(Icons.tune, color: Colors.blueAccent, size: 20), + ), + ), + ), + const SizedBox(width: 4), + MyText.bodyMedium("Refresh", fontWeight: 600), + Tooltip( + message: 'Refresh Data', + child: InkWell( + borderRadius: BorderRadius.circular(24), + onTap: _refreshData, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon(Icons.refresh, color: Colors.green, size: 22), + ), + ), + ), + ], + ); + } + + Widget _buildNoProjectWidget() { + return Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: MyText.titleMedium( + 'No Records Found', + fontWeight: 600, + color: Colors.grey[600], + ), + ), + ); + } + + Widget _buildSelectedTabContent() { + switch (selectedTab) { + case 'attendanceLogs': + return AttendanceLogsTab(controller: attendanceController); + case 'regularizationRequests': + return RegularizationRequestsTab(controller: attendanceController); + case 'todaysAttendance': + default: + return TodaysAttendanceTab(controller: attendanceController); + } + } + @override Widget build(BuildContext context) { return Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(72), - child: AppBar( - backgroundColor: const Color(0xFFF5F5F5), - elevation: 0.5, - automaticallyImplyLeading: false, - titleSpacing: 0, - title: Padding( - padding: MySpacing.xy(16, 0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - icon: const Icon(Icons.arrow_back_ios_new, - color: Colors.black, size: 20), - onPressed: () => Get.offNamed('/dashboard'), - ), - MySpacing.width(8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - MyText.titleLarge( - 'Attendance', - fontWeight: 700, - color: Colors.black, - ), - MySpacing.height(2), - GetBuilder( - builder: (projectController) { - final projectName = - projectController.selectedProject?.name ?? - 'Select Project'; - return Row( - children: [ - const Icon(Icons.work_outline, - size: 14, color: Colors.grey), - MySpacing.width(4), - Expanded( - child: MyText.bodySmall( - projectName, - fontWeight: 600, - overflow: TextOverflow.ellipsis, - color: Colors.grey[700], - ), - ), - ], - ); - }, - ), - ], - ), - ), - ], - ), - ), - ), - ), + appBar: PreferredSize(preferredSize: const Size.fromHeight(72), child: _buildAppBar()), 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( + child: GetBuilder( + init: attendanceController, + tag: 'attendance_dashboard_controller', + builder: (controller) { + final selectedProjectId = projectController.selectedProjectId.value; + final noProjectSelected = selectedProjectId.isEmpty; + + return SingleChildScrollView( + padding: MySpacing.zero, + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MySpacing.height(flexSpacing), - Row( - mainAxisAlignment: MainAxisAlignment.end, + _buildFilterAndRefreshRow(), + MySpacing.height(flexSpacing), + MyFlex( 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.transparent, - 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.tune, - color: Colors.blueAccent, - size: 20, - ), - ), - ), - ), - ), - 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: 22, - ), - ), - ), - ), + MyFlexItem( + sizes: 'lg-12 md-12 sm-12', + child: noProjectSelected + ? _buildNoProjectWidget() + : _buildSelectedTabContent(), ), ], ), - 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) - SkeletonLoaders.employeeListSkeletonLoader() - 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) - SkeletonLoaders.employeeListSkeletonLoader() - 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 SkeletonLoaders.employeeListSkeletonLoader(); - } - - 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, - ), - ], - ); - }), - ), - ); - }), - ], - ); - } } diff --git a/lib/view/dashboard/Attendence/regularization_requests_tab.dart b/lib/view/dashboard/Attendence/regularization_requests_tab.dart new file mode 100644 index 0000000..802075c --- /dev/null +++ b/lib/view/dashboard/Attendence/regularization_requests_tab.dart @@ -0,0 +1,157 @@ +// lib/view/attendance/tabs/regularization_requests_tab.dart +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/widgets/avatar.dart'; +import 'package:marco/helpers/widgets/my_card.dart'; +import 'package:marco/helpers/widgets/my_container.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; +import 'package:marco/model/attendance/log_details_view.dart'; +import 'package:marco/model/attendance/regualrize_action_button.dart'; + +class RegularizationRequestsTab extends StatelessWidget { + final AttendanceController controller; + + const RegularizationRequestsTab({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + 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 = controller.regularizationLogs; + + if (controller.isLoadingRegularizationLogs.value) { + return SkeletonLoaders.employeeListSkeletonLoader(); + } + + if (employees.isEmpty) { + return const SizedBox( + height: 120, + child: Center( + child: Text("No Regularization Requests Found for this Project"), + ), + ); + } + + return MyCard.bordered( + paddingAll: 8, + child: Column( + children: List.generate(employees.length, (index) { + final employee = employees[index]; + return Column( + children: [ + 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, + ), + ), + MySpacing.width(6), + Flexible( + child: MyText.bodySmall( + '(${employee.role})', + fontWeight: 600, + overflow: TextOverflow.ellipsis, + color: Colors.grey[700], + ), + ), + ], + ), + MySpacing.height(8), + if (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), + MyText.bodySmall( + DateFormat('hh:mm a') + .format(employee.checkIn!), + fontWeight: 600, + ), + MySpacing.width(16), + ], + if (employee.checkOut != null) ...[ + const Icon(Icons.arrow_circle_left, + size: 16, color: Colors.red), + MySpacing.width(4), + MyText.bodySmall( + DateFormat('hh:mm a') + .format(employee.checkOut!), + fontWeight: 600, + ), + ], + ], + ), + MySpacing.height(12), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + RegularizeActionButton( + attendanceController: controller, + log: employee, + uniqueLogKey: employee.employeeId, + action: ButtonActions.approve, + ), + const SizedBox(width: 8), + RegularizeActionButton( + attendanceController: controller, + log: employee, + uniqueLogKey: employee.employeeId, + action: ButtonActions.reject, + ), + const SizedBox(width: 8), + if (employee.checkIn != null) + AttendanceLogViewButton( + employee: employee, + attendanceController: controller, + ), + ], + ), + ], + ), + ), + ], + ), + ), + if (index != employees.length - 1) + Divider(color: Colors.grey.withOpacity(0.3)), + ], + ); + }), + ), + ); + }), + ], + ); + } +} diff --git a/lib/view/dashboard/Attendence/todays_attendance_tab.dart b/lib/view/dashboard/Attendence/todays_attendance_tab.dart new file mode 100644 index 0000000..caa1f06 --- /dev/null +++ b/lib/view/dashboard/Attendence/todays_attendance_tab.dart @@ -0,0 +1,131 @@ +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/widgets/avatar.dart'; +import 'package:marco/helpers/widgets/my_card.dart'; +import 'package:marco/helpers/widgets/my_container.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; +import 'package:marco/model/attendance/log_details_view.dart'; +import 'package:marco/model/attendance/attendence_action_button.dart'; + +class TodaysAttendanceTab extends StatelessWidget { + final AttendanceController controller; + + const TodaysAttendanceTab({super.key, required this.controller}); + + String _formatDate(DateTime date) { + return "${date.day}/${date.month}/${date.year}"; + } + + @override + Widget build(BuildContext context) { + return Obx(() { + final isLoading = controller.isLoadingEmployees.value; + final employees = controller.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], + ), + ], + ), + ), + if (isLoading) + SkeletonLoaders.employeeListSkeletonLoader() + else if (employees.isEmpty) + const SizedBox(height: 120, child: Center(child: Text("No Employees Assigned"))) + else + MyCard.bordered( + paddingAll: 8, + child: Column( + children: List.generate(employees.length, (index) { + final employee = employees[index]; + return Column( + children: [ + 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( + spacing: 6, + children: [ + MyText.bodyMedium(employee.name, fontWeight: 600), + MyText.bodySmall('(${employee.designation})', fontWeight: 600, color: Colors.grey[700]), + ], + ), + MySpacing.height(8), + if (employee.checkIn != null || employee.checkOut != null) + Row( + children: [ + if (employee.checkIn != null) + Row( + children: [ + const Icon(Icons.arrow_circle_right, size: 16, color: Colors.green), + MySpacing.width(4), + Text(DateFormat('hh:mm a').format(employee.checkIn!)), + ], + ), + if (employee.checkOut != null) ...[ + MySpacing.width(16), + const Icon(Icons.arrow_circle_left, size: 16, color: Colors.red), + MySpacing.width(4), + Text(DateFormat('hh:mm a').format(employee.checkOut!)), + ], + ], + ), + MySpacing.height(12), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AttendanceActionButton( + employee: employee, + attendanceController: controller, + ), + if (employee.checkIn != null) ...[ + MySpacing.width(8), + AttendanceLogViewButton( + employee: employee, + attendanceController: controller, + ), + ], + ], + ), + ], + ), + ), + ], + ), + ), + if (index != employees.length - 1) + Divider(color: Colors.grey.withOpacity(0.3)), + ], + ); + }), + ), + ), + ], + ); + }); + } +}