From f2c125172df3476fd4118488417d79f97246354a Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 5 May 2025 15:49:46 +0530 Subject: [PATCH] grouped the attendence logs --- .../attendance_screen_controller.dart | 18 +- lib/view/dashboard/attendanceScreen.dart | 681 +++++++++--------- 2 files changed, 365 insertions(+), 334 deletions(-) diff --git a/lib/controller/dashboard/attendance_screen_controller.dart b/lib/controller/dashboard/attendance_screen_controller.dart index 174c60d..e211243 100644 --- a/lib/controller/dashboard/attendance_screen_controller.dart +++ b/lib/controller/dashboard/attendance_screen_controller.dart @@ -2,7 +2,7 @@ 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/api_service.dart'; import 'package:marco/model/attendance_model.dart'; import 'package:marco/model/project_model.dart'; @@ -182,6 +182,22 @@ class AttendanceController extends GetxController { print("Failed to fetch attendance logs for project $projectId."); } } +Map> groupLogsByCheckInDate() { + final groupedLogs = >{}; + + for (var log in attendanceLogs) { + final checkInDate = log.checkIn != null + ? DateFormat('dd MMM yyyy').format(log.checkIn!) + : 'Unknown'; + + if (!groupedLogs.containsKey(checkInDate)) { + groupedLogs[checkInDate] = []; + } + groupedLogs[checkInDate]!.add(log); + } + + return groupedLogs; +} Future fetchRegularizationLogs( String? projectId, { diff --git a/lib/view/dashboard/attendanceScreen.dart b/lib/view/dashboard/attendanceScreen.dart index 0e485f7..08b2b92 100644 --- a/lib/view/dashboard/attendanceScreen.dart +++ b/lib/view/dashboard/attendanceScreen.dart @@ -340,6 +340,7 @@ class _AttendanceScreenState extends State with UIMixin { Widget reportsTab(BuildContext context) { final attendanceController = Get.find(); + final groupedLogs = attendanceController.groupLogsByCheckInDate(); final columns = [ DataColumn(label: MyText.labelLarge('Name', color: contentTheme.primary)), @@ -352,360 +353,374 @@ class _AttendanceScreenState extends State with UIMixin { label: MyText.labelLarge('Action', color: contentTheme.primary)), ]; - final rows = attendanceController.attendanceLogs.reversed - .toList() - .asMap() - .entries - .map((entry) { - var log = entry.value; - 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), - ], + 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.bodyMedium(checkInDate, fontWeight: 600)), + DataCell(MyText.bodyMedium('')), // Placeholder for other columns + DataCell(MyText.bodyMedium('')), // Placeholder for other columns + DataCell(MyText.bodyMedium('')), // Placeholder for other columns + DataCell(MyText.bodyMedium('')), // Placeholder for other columns + ])); + + // 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('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( - IconButton( - icon: const Icon(Icons.visibility, size: 18), - onPressed: () async { - await attendanceController.fetchLogsView(log.id.toString()); - showModalBottomSheet( - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + 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, ), - backgroundColor: Theme.of(context).cardColor, - builder: (context) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.titleMedium("Attendance Log Details", - fontWeight: 700), - 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) => 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), + ], + ), + ), + 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( + IconButton( + icon: const Icon(Icons.visibility, size: 18), + onPressed: () async { + await attendanceController.fetchLogsView(log.id.toString()); + showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: + BorderRadius.vertical(top: Radius.circular(16)), + ), + backgroundColor: Theme.of(context).cardColor, + builder: (context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleMedium("Attendance Log Details", + fontWeight: 700), + 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) => 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: MyText.bodyMedium( - log.comment ?? '-', - fontWeight: 600, - ), - ), - ], + ], + ), ), - ), - Expanded( - child: GestureDetector( - onTap: () { - if (log.preSignedUrl != null) { - showDialog( - context: context, - builder: (_) => Dialog( - child: Image.network( - log.preSignedUrl!, + 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 Icon( + Icons.broken_image, + size: 50, + color: Colors.grey); + }, + ), + ), + ); + } + }, + child: log.thumbPreSignedUrl != null + ? Image.network( + log.thumbPreSignedUrl!, + height: 40, + width: 40, fit: BoxFit.cover, - height: 400, errorBuilder: (context, error, stackTrace) { return Icon( Icons.broken_image, - size: 50, + size: 40, color: Colors.grey); }, - ), - ), - ); - } - }, - child: log.thumbPreSignedUrl != null - ? Image.network( - log.thumbPreSignedUrl!, - height: 40, - width: 40, - fit: BoxFit.cover, - errorBuilder: (context, error, - stackTrace) { - return Icon( - Icons.broken_image, - size: 40, - color: Colors.grey); - }, - ) - : Icon(Icons.broken_image, - size: 40, color: Colors.grey), + ) + : Icon(Icons.broken_image, + size: 40, + color: Colors.grey), + ), ), - ), - ], - )), - Align( - alignment: Alignment.centerRight, - child: ElevatedButton( - onPressed: () => Navigator.pop(context), - child: const Text("Close"), + ], + )), + Align( + alignment: Alignment.centerRight, + child: ElevatedButton( + onPressed: () => Navigator.pop(context), + child: const Text("Close"), + ), ), - ), - ], - ), - ); - }, - ); - }, - ), - ), - DataCell( - ElevatedButton( - onPressed: (log.activity == 5 || - log.activity == 2 || // Add this condition for activity 2 - (log.activity == 4 && - !(log.checkOut != null && - log.checkIn != null && - DateTime.now().difference(log.checkIn!).inDays <= - 2))) - ? null - : () async { - if (attendanceController.selectedProjectId == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Please select a project first"), - ), - ); - return; - } - - int updatedAction; - String actionText; - bool imageCapture = true; - if (log.activity == 0) { - updatedAction = 0; - actionText = "Check In"; - } else if (log.activity == 1) { - DateTime currentDate = DateTime.now(); - DateTime twoDaysAgo = - currentDate.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 (log.activity == 4 && - log.checkOut != null && - log.checkIn != null && - DateTime.now().difference(log.checkIn!).inDays <= 2) { - 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!' - : 'Image upload failed.'), + ], ), ); - - 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: (log.activity == 4 && - log.checkOut != null && - log.checkIn != null && - DateTime.now().difference(log.checkIn!).inDays <= 2) - ? Colors.green - : AttendanceActionColors.colors[(log.activity == 0) - ? ButtonActions.checkIn - : ButtonActions.checkOut], - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6), - minimumSize: const Size(60, 20), - textStyle: const TextStyle(fontSize: 12), - ), - child: Text( - (log.activity == 5) - ? ButtonActions.rejected - : (log.activity == 4 && - log.checkOut != null && - log.checkIn != null && - DateTime.now().difference(log.checkIn!).inDays <= 2) - ? ButtonActions.checkIn - : (log.activity == 2) // Change text when activity is 2 - ? "Requested" - : (log.activity == 4) - ? ButtonActions.approved - : (log.activity == 0) - ? ButtonActions.checkIn - : (log.activity == 1 && - log.checkOut != null && - DateTime.now() - .difference(log.checkOut!) - .inDays <= - 2) - ? ButtonActions.checkOut - : (log.activity == 2 || - (log.activity == 1 && - log.checkOut == null && - log.checkIn != null && - log.checkIn!.isBefore( - DateTime.now().subtract( - Duration(days: 2))))) - ? ButtonActions.requestRegularize - : ButtonActions.checkOut, + ); + }, ), ), - ) - ]); - }).toList(); + DataCell( + ElevatedButton( + onPressed: (log.activity == 5 || + log.activity == 2 || // Add this condition for activity 2 + (log.activity == 4 && + !(log.checkOut != null && + log.checkIn != null && + DateTime.now().difference(log.checkIn!).inDays <= + 2))) + ? null + : () async { + if (attendanceController.selectedProjectId == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Please select a project first"), + ), + ); + return; + } + + int updatedAction; + String actionText; + bool imageCapture = true; + if (log.activity == 0) { + updatedAction = 0; + actionText = "Check In"; + } else if (log.activity == 1) { + DateTime currentDate = DateTime.now(); + DateTime twoDaysAgo = + currentDate.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 (log.activity == 4 && + log.checkOut != null && + log.checkIn != null && + DateTime.now().difference(log.checkIn!).inDays <= 2) { + 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!' + : 'Image upload failed.'), + ), + ); + + 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: (log.activity == 4 && + log.checkOut != null && + log.checkIn != null && + DateTime.now().difference(log.checkIn!).inDays <= 2) + ? Colors.green + : AttendanceActionColors.colors[(log.activity == 0) + ? ButtonActions.checkIn + : ButtonActions.checkOut], + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6), + minimumSize: const Size(60, 20), + textStyle: const TextStyle(fontSize: 12), + ), + child: Text( + (log.activity == 5) + ? ButtonActions.rejected + : (log.activity == 4 && + log.checkOut != null && + log.checkIn != null && + DateTime.now().difference(log.checkIn!).inDays <= 2) + ? ButtonActions.checkIn + : (log.activity == 2) // Change text when activity is 2 + ? "Requested" + : (log.activity == 4) + ? ButtonActions.approved + : (log.activity == 0) + ? ButtonActions.checkIn + : (log.activity == 1 && + log.checkOut != null && + DateTime.now() + .difference(log.checkOut!) + .inDays <= + 2) + ? ButtonActions.checkOut + : (log.activity == 2 || + (log.activity == 1 && + log.checkOut == null && + log.checkIn != null && + log.checkIn!.isBefore( + DateTime.now().subtract( + Duration( + days: 2))))) + ? ButtonActions.requestRegularize + : ButtonActions.checkOut, + ), + ), + ), + ])); + } + }); return SingleChildScrollView( - // Wrap the Column in SingleChildScrollView to handle overflow child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [