diff --git a/lib/controller/dashboard/attendance_screen_controller.dart b/lib/controller/dashboard/attendance_screen_controller.dart index 33c1c48..ff69c00 100644 --- a/lib/controller/dashboard/attendance_screen_controller.dart +++ b/lib/controller/dashboard/attendance_screen_controller.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:geolocator/geolocator.dart'; import 'package:intl/intl.dart'; +import 'package:logger/logger.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/model/attendance_model.dart'; @@ -12,8 +13,6 @@ 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:logger/logger.dart'; - final Logger log = Logger(); class AttendanceController extends GetxController { @@ -30,6 +29,7 @@ class AttendanceController extends GetxController { List attendenceLogsView = []; RxBool isLoading = false.obs; + RxMap uploadingStates = {}.obs; @override void onInit() { @@ -88,8 +88,13 @@ class AttendanceController extends GetxController { if (response != null) { employees = response.map((json) => EmployeeModel.fromJson(json)).toList(); - log.i( - "Employees fetched: ${employees.length} employees for project $projectId"); + + // Initialize per-employee uploading state + for (var emp in employees) { + uploadingStates[emp.id] = false.obs; + } + + log.i("Employees fetched: ${employees.length} employees for project $projectId"); update(); } else { log.e("Failed to fetch employees for project $projectId"); @@ -105,6 +110,8 @@ class AttendanceController extends GetxController { bool imageCapture = true, }) async { try { + uploadingStates[employeeId]?.value = true; + XFile? image; if (imageCapture) { image = await ImagePicker().pickImage( @@ -113,6 +120,7 @@ class AttendanceController extends GetxController { ); if (image == null) { log.w("Image capture cancelled."); + uploadingStates[employeeId]?.value = false; return false; } } @@ -143,6 +151,8 @@ class AttendanceController extends GetxController { } catch (e, stacktrace) { log.e("Error uploading attendance", error: e, stackTrace: stacktrace); return false; + } finally { + uploadingStates[employeeId]?.value = false; } } @@ -212,8 +222,21 @@ class AttendanceController extends GetxController { groupedLogs[checkInDate]!.add(logItem); } - log.i("Logs grouped by check-in date."); - return groupedLogs; + // Sort by date descending + 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); + + log.i("Logs grouped and sorted by check-in date."); + return sortedMap; } Future fetchRegularizationLogs( @@ -228,9 +251,8 @@ class AttendanceController extends GetxController { isLoading.value = false; if (response != null) { - regularizationLogs = response - .map((json) => RegularizationLogModel.fromJson(json)) - .toList(); + regularizationLogs = + response.map((json) => RegularizationLogModel.fromJson(json)).toList(); log.i("Regularization logs fetched: ${regularizationLogs.length}"); update(); } else { @@ -246,9 +268,8 @@ class AttendanceController extends GetxController { isLoading.value = false; if (response != null) { - attendenceLogsView = response - .map((json) => AttendanceLogViewModel.fromJson(json)) - .toList(); + attendenceLogsView = + response.map((json) => AttendanceLogViewModel.fromJson(json)).toList(); log.i("Attendance log view fetched for ID: $id"); update(); } else { diff --git a/lib/helpers/utils/attendance_actions.dart b/lib/helpers/utils/attendance_actions.dart index 6abf4ea..3b2cdff 100644 --- a/lib/helpers/utils/attendance_actions.dart +++ b/lib/helpers/utils/attendance_actions.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; class ButtonActions { static const String checkIn = "Check In"; static const String checkOut = "Check Out"; - static const String requestRegularize = " Request Regularize"; + static const String requestRegularize = "Regularize"; static const String rejected = "Rejected"; static const String approved = "Approved"; static const String requested = "Requested"; diff --git a/lib/model/my_paginated_table.dart b/lib/model/my_paginated_table.dart index 762403d..d35ba28 100644 --- a/lib/model/my_paginated_table.dart +++ b/lib/model/my_paginated_table.dart @@ -1,3 +1,4 @@ +import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; @@ -9,18 +10,22 @@ class MyPaginatedTable extends StatefulWidget { final List rows; final double columnSpacing; final double horizontalMargin; + final bool isLoading; + final Widget? footer; const MyPaginatedTable({ super.key, this.title, required this.columns, required this.rows, - this.columnSpacing = 23, - this.horizontalMargin = 35, + this.columnSpacing = 20, + this.horizontalMargin = 0, + this.isLoading = false, + this.footer, }); @override - _MyPaginatedTableState createState() => _MyPaginatedTableState(); + State createState() => _MyPaginatedTableState(); } class _MyPaginatedTableState extends State { @@ -29,22 +34,30 @@ class _MyPaginatedTableState extends State { @override Widget build(BuildContext context) { - final visibleRows = widget.rows.skip(_start).take(_rowsPerPage).toList(); final totalRows = widget.rows.length; final totalPages = (totalRows / _rowsPerPage).ceil(); - final currentPage = (_start / _rowsPerPage).ceil() + 1; + final currentPage = (_start ~/ _rowsPerPage) + 1; + final visibleRows = widget.rows.skip(_start).take(_rowsPerPage).toList(); + + if (widget.isLoading) { + return const Center(child: CircularProgressIndicator()); + } return Column( - crossAxisAlignment: CrossAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (widget.title != null) Padding( - padding: MySpacing.xy(8, 6), // Using standard spacing for title - child: MyText.titleMedium(widget.title!, fontWeight: 600, fontSize: 20), + padding: MySpacing.xy(8, 6), + child: MyText.titleMedium( + widget.title!, + fontWeight: 600, + fontSize: 20, + ), ), if (widget.rows.isEmpty) Padding( - padding: MySpacing.all(16), // Standard padding for empty state + padding: MySpacing.all(16), child: MyText.bodySmall('No data available'), ), if (widget.rows.isNotEmpty) @@ -53,63 +66,75 @@ class _MyPaginatedTableState extends State { final spacing = _calculateSmartSpacing(constraints.maxWidth); return SingleChildScrollView( scrollDirection: Axis.horizontal, - child: MyContainer.bordered( - borderColor: Colors.black.withAlpha(40), - padding: EdgeInsets.zero, - child: DataTable( - columns: widget.columns, - rows: visibleRows, - columnSpacing: spacing, - horizontalMargin: widget.horizontalMargin, + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: constraints.maxWidth), + child: MyContainer.bordered( + borderColor: Colors.black.withAlpha(40), + padding: const EdgeInsets.all(10), + child: DataTable( + columns: widget.columns, + rows: visibleRows, + columnSpacing: spacing, + horizontalMargin: widget.horizontalMargin, + headingRowHeight: 48, + dataRowHeight: 44, + ), ), ), ); }, ), - MySpacing.height(8), // Standard height spacing after table - PaginatedFooter( - currentPage: currentPage, - totalPages: totalPages, - onPrevious: () { - setState(() { - _start = (_start - _rowsPerPage).clamp(0, totalRows - _rowsPerPage); - }); - }, - onNext: () { - setState(() { - _start = (_start + _rowsPerPage).clamp(0, totalRows - _rowsPerPage); - }); - }, - onPageSizeChanged: (newRowsPerPage) { - setState(() { - _rowsPerPage = newRowsPerPage; - _start = 0; - }); - }, - ), + const SizedBox(height: 8), + widget.footer ?? + PaginatedFooter( + currentPage: currentPage, + totalPages: totalPages, + totalRows: totalRows, + onPrevious: _handlePrevious, + onNext: _handleNext, + onPageChanged: _handlePageChanged, + onPageSizeChanged: _handlePageSizeChanged, + ), ], ); } + void _handlePrevious() { + setState(() { + _start = (_start - _rowsPerPage) + .clamp(0, math.max(0, widget.rows.length - _rowsPerPage)); + }); + } + + void _handleNext() { + setState(() { + _start = (_start + _rowsPerPage) + .clamp(0, math.max(0, widget.rows.length - _rowsPerPage)); + }); + } + + void _handlePageChanged(int page) { + setState(() { + _start = (page - 1) * _rowsPerPage; + }); + } + + void _handlePageSizeChanged(int newRowsPerPage) { + setState(() { + _rowsPerPage = newRowsPerPage; + _start = 0; + }); + } + double _calculateSmartSpacing(double maxWidth) { - int columnCount = widget.columns.length; - double horizontalPadding = widget.horizontalMargin * 2; - double availableWidth = maxWidth - horizontalPadding; + final columnCount = widget.columns.length; + final horizontalPadding = widget.horizontalMargin * 2; + final availableWidth = maxWidth - horizontalPadding; - // Desired min/max column spacing const double minSpacing = 16; - const double maxSpacing = 80; + const double maxSpacing = 64; - // Total width assuming minimal spacing - double minTotalWidth = (columnCount * minSpacing) + horizontalPadding; - - if (minTotalWidth >= availableWidth) { - // Not enough room — return minimal spacing - return minSpacing; - } - - // Fit evenly within the available width - double spacing = (availableWidth / columnCount) - 40; // 40 for estimated cell content width + double spacing = (availableWidth / columnCount) - 40; return spacing.clamp(minSpacing, maxSpacing); } } @@ -117,62 +142,102 @@ class _MyPaginatedTableState extends State { class PaginatedFooter extends StatelessWidget { final int currentPage; final int totalPages; + final int totalRows; final VoidCallback onPrevious; final VoidCallback onNext; + final Function(int) onPageChanged; final Function(int) onPageSizeChanged; const PaginatedFooter({ + super.key, required this.currentPage, required this.totalPages, + required this.totalRows, required this.onPrevious, required this.onNext, + required this.onPageChanged, required this.onPageSizeChanged, }); + List _buildPageButtons() { + List pages = []; + + void addPageButton(int page) { + pages.add( + Padding( + padding: const EdgeInsets.symmetric(horizontal: 2), + child: TextButton( + onPressed: () => onPageChanged(page), + style: TextButton.styleFrom( + backgroundColor: currentPage == page ? Colors.blue : null, + foregroundColor: + currentPage == page ? Colors.white : Colors.black, + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + minimumSize: const Size(32, 28), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(12), + ), + textStyle: const TextStyle(fontSize: 12), + ), + child: Text('$page'), + ), + ), + ); + } + + if (totalPages <= 5) { + for (int i = 1; i <= totalPages; i++) { + addPageButton(i); + } + } else { + addPageButton(1); + if (currentPage > 3) { + pages.add(const Padding( + padding: EdgeInsets.symmetric(horizontal: 4), + child: Text('...'), + )); + } + + for (int i = math.max(2, currentPage - 1); + i <= math.min(totalPages - 1, currentPage + 1); + i++) { + addPageButton(i); + } + + if (currentPage < totalPages - 2) { + pages.add(const Padding( + padding: EdgeInsets.symmetric(horizontal: 4), + child: Text('...'), + )); + } + + addPageButton(totalPages); + } + + return pages; + } + @override Widget build(BuildContext context) { return Padding( - padding: MySpacing.x(16), // Standard horizontal spacing for footer - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (currentPage > 1) - IconButton( - onPressed: onPrevious, - icon: Icon(Icons.chevron_left), - ), - Text( - 'Page $currentPage of $totalPages', - style: TextStyle( - fontSize: 16, - color: Theme.of(context).colorScheme.onBackground, - ), - ), - SizedBox(width: 8), - if (currentPage < totalPages) - IconButton( - onPressed: onNext, - icon: Icon(Icons.chevron_right), - ), - SizedBox(width: 16), - PopupMenuButton( - icon: Icon(Icons.more_vert), - onSelected: (value) { - onPageSizeChanged(value); - }, - itemBuilder: (BuildContext context) { - return [5, 10, 20, 50].map((e) { - return PopupMenuItem( - value: e, - child: Text('$e rows per page'), - ); - }).toList(); - }, - ), - ], - ), + padding: MySpacing.all(0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + onPressed: currentPage > 1 ? onPrevious : null, + icon: const Icon(Icons.chevron_left), + tooltip: 'Previous', + ), + ..._buildPageButtons(), + IconButton( + onPressed: currentPage < totalPages ? onNext : null, + icon: const Icon(Icons.chevron_right), + tooltip: 'Next', + ), + ], ), ); } diff --git a/lib/view/dashboard/attendanceScreen.dart b/lib/view/dashboard/attendanceScreen.dart index 08b2b92..308abea 100644 --- a/lib/view/dashboard/attendanceScreen.dart +++ b/lib/view/dashboard/attendanceScreen.dart @@ -241,7 +241,8 @@ class _AttendanceScreenState extends State with UIMixin { Widget employeeListTab() { if (attendanceController.employees.isEmpty) { return Center( - child: MyText.bodySmall("No Employees Found", fontWeight: 600), + child: MyText.bodySmall("No Employees Assigned to This Project", + fontWeight: 600), ); } @@ -273,62 +274,89 @@ class _AttendanceScreenState extends State with UIMixin { ), ), DataCell( - ElevatedButton( - onPressed: () async { - if (attendanceController.selectedProjectId == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Please select a project first")), - ); - return; - } + 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.'), + ), + ); - int updatedAction = (activity == 0 || activity == 4) ? 0 : 1; - String actionText = (updatedAction == 0) - ? ButtonActions.checkIn - : ButtonActions.checkOut; + controller.uploadingStates[employee.employeeId] = + RxBool(false); - final success = - await attendanceController.captureAndUploadAttendance( - employee.id, - employee.employeeId, - attendanceController.selectedProjectId!, - comment: actionText, - action: updatedAction, - ); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(success - ? 'Attendance marked successfully!' - : 'Image upload failed.'), + 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), ), - ); - - if (success) { - attendanceController.fetchEmployeesByProject( - attendanceController.selectedProjectId!); - attendanceController.fetchAttendanceLogs( - attendanceController.selectedProjectId!); - attendanceController - .fetchProjectData(attendanceController.selectedProjectId!); - attendanceController.update(); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: AttendanceActionColors.colors[buttonText], - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6), - minimumSize: const Size(60, 20), - textStyle: const TextStyle(fontSize: 12), - ), - child: Text(buttonText), - ), + 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(8.0), // You can adjust this as needed + padding: const EdgeInsets.all(0.0), child: SingleChildScrollView( child: MyPaginatedTable( columns: columns, @@ -360,10 +388,10 @@ class _AttendanceScreenState extends State with UIMixin { // 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 + DataCell(MyText.bodyMedium('')), + DataCell(MyText.bodyMedium('')), + DataCell(MyText.bodyMedium('')), + DataCell(MyText.bodyMedium('')), ])); // Add rows for each log in this group @@ -385,12 +413,6 @@ class _AttendanceScreenState extends State with UIMixin { 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!) @@ -405,12 +427,6 @@ class _AttendanceScreenState extends State with UIMixin { 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!) @@ -427,6 +443,7 @@ class _AttendanceScreenState extends State with UIMixin { await attendanceController.fetchLogsView(log.id.toString()); showModalBottomSheet( context: context, + isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), @@ -434,145 +451,167 @@ class _AttendanceScreenState extends State with UIMixin { 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( + 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: [ + 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) => Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + 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 Icon( - Icons.broken_image, - size: 50, - color: Colors.grey); + 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, ), ), - ); - } - }, - 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), - ), + ], + ), + ), + 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), + ), + ), + ], ), - ], - )), - Align( - alignment: Alignment.centerRight, - child: ElevatedButton( - onPressed: () => Navigator.pop(context), - child: const Text("Close"), + )), + const SizedBox(height: 16), + Align( + alignment: Alignment.centerRight, + child: ElevatedButton( + onPressed: () => Navigator.pop(context), + child: const Text("Close"), + ), ), - ), - ], + ], + ), ), ); }, @@ -581,141 +620,205 @@ class _AttendanceScreenState extends State with UIMixin { ), ), DataCell( - ElevatedButton( - onPressed: (log.activity == 5 || - log.activity == 2 || // Add this condition for activity 2 - (log.activity == 4 && - !(log.checkOut != null && + Obx(() { + // Check if any record for this employee is uploading + final uniqueLogKey = '${log.employeeId}_${log.id}'; + final isUploading = + attendanceController.uploadingStates[uniqueLogKey]?.value ?? + false; + // Check if both checkIn and checkOut exist and the date is yesterday + 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))); + + return SizedBox( + width: 90, + height: 25, + child: ElevatedButton( + onPressed: isUploading || + isYesterday || + log.activity == 2 || + log.activity == 5 || + (log.activity == 4 && + !(DateUtils.isSameDay( + log.checkIn ?? DateTime(2000), + DateTime.now()))) + ? null + : () async { + // Set the uploading state for the employee when the action starts + 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; + } + + // Existing logic for updating action + 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))) - ? null - : () async { - if (attendanceController.selectedProjectId == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Please select a project first"), + 2) { + updatedAction = 0; + actionText = "Check In"; + } else { + updatedAction = 0; + actionText = "Unknown Action"; + } + + // Proceed with capturing and uploading attendance + final success = await attendanceController + .captureAndUploadAttendance( + log.id, + log.employeeId, + attendanceController.selectedProjectId!, + comment: actionText, + action: updatedAction, + imageCapture: imageCapture, + ); + + // Show result in SnackBar + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(success + ? 'Attendance marked successfully!' + : 'Failed to mark attendance.'), + ), + ); + + // Reset the uploading state after the action is complete + attendanceController.uploadingStates[uniqueLogKey] = + RxBool(false); + + if (success) { + // Update the UI with the new data + 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 // Button color for the disabled state (Yesterday's date) + : (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), + textStyle: const TextStyle(fontSize: 12), + ), + child: isUploading + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: + AlwaysStoppedAnimation(Colors.white), ), - ); - 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.'), + ) + : 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) + ? "Requested" + : (log.activity == 4) + ? ButtonActions.approved + : (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 == 2 || + (log.activity == 1 && + log.checkOut == + null && + log.checkIn != + null && + log.checkIn!.isBefore( + DateTime.now() + .subtract(Duration( + days: + 2))))) + ? ButtonActions + .requestRegularize + : ButtonActions.checkOut, ), - ); - - 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, - ), - ), - ), + ), + ); + }), + ) ])); } }); @@ -749,7 +852,6 @@ class _AttendanceScreenState extends State with UIMixin { child: MyPaginatedTable( columns: columns, rows: rows, - columnSpacing: 8.0, ), ), ], @@ -951,7 +1053,6 @@ class _AttendanceScreenState extends State with UIMixin { Expanded( child: SingleChildScrollView( child: MyPaginatedTable( - // Use MyPaginatedTable here for pagination columns: columns, rows: rows, columnSpacing: 15.0,