Merge pull request 'Task-#188' (#12) from Task-#188 into main

Reviewed-on: #12
This commit is contained in:
vaibhav.surve 2025-05-06 12:26:54 +00:00
commit eab722d5c0
4 changed files with 783 additions and 580 deletions

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:logger/logger.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/attendance_model.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/regularization_log_model.dart';
import 'package:marco/model/attendance_log_view_model.dart'; import 'package:marco/model/attendance_log_view_model.dart';
import 'package:logger/logger.dart';
final Logger log = Logger(); final Logger log = Logger();
class AttendanceController extends GetxController { class AttendanceController extends GetxController {
@ -30,6 +29,7 @@ class AttendanceController extends GetxController {
List<AttendanceLogViewModel> attendenceLogsView = []; List<AttendanceLogViewModel> attendenceLogsView = [];
RxBool isLoading = false.obs; RxBool isLoading = false.obs;
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
@override @override
void onInit() { void onInit() {
@ -88,8 +88,13 @@ class AttendanceController extends GetxController {
if (response != null) { if (response != null) {
employees = response.map((json) => EmployeeModel.fromJson(json)).toList(); 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(); update();
} else { } else {
log.e("Failed to fetch employees for project $projectId"); log.e("Failed to fetch employees for project $projectId");
@ -105,6 +110,8 @@ class AttendanceController extends GetxController {
bool imageCapture = true, bool imageCapture = true,
}) async { }) async {
try { try {
uploadingStates[employeeId]?.value = true;
XFile? image; XFile? image;
if (imageCapture) { if (imageCapture) {
image = await ImagePicker().pickImage( image = await ImagePicker().pickImage(
@ -113,6 +120,7 @@ class AttendanceController extends GetxController {
); );
if (image == null) { if (image == null) {
log.w("Image capture cancelled."); log.w("Image capture cancelled.");
uploadingStates[employeeId]?.value = false;
return false; return false;
} }
} }
@ -143,6 +151,8 @@ class AttendanceController extends GetxController {
} catch (e, stacktrace) { } catch (e, stacktrace) {
log.e("Error uploading attendance", error: e, stackTrace: stacktrace); log.e("Error uploading attendance", error: e, stackTrace: stacktrace);
return false; return false;
} finally {
uploadingStates[employeeId]?.value = false;
} }
} }
@ -212,8 +222,21 @@ class AttendanceController extends GetxController {
groupedLogs[checkInDate]!.add(logItem); groupedLogs[checkInDate]!.add(logItem);
} }
log.i("Logs grouped by check-in date."); // Sort by date descending
return groupedLogs; 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<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
log.i("Logs grouped and sorted by check-in date.");
return sortedMap;
} }
Future<void> fetchRegularizationLogs( Future<void> fetchRegularizationLogs(
@ -228,9 +251,8 @@ class AttendanceController extends GetxController {
isLoading.value = false; isLoading.value = false;
if (response != null) { if (response != null) {
regularizationLogs = response regularizationLogs =
.map((json) => RegularizationLogModel.fromJson(json)) response.map((json) => RegularizationLogModel.fromJson(json)).toList();
.toList();
log.i("Regularization logs fetched: ${regularizationLogs.length}"); log.i("Regularization logs fetched: ${regularizationLogs.length}");
update(); update();
} else { } else {
@ -246,9 +268,8 @@ class AttendanceController extends GetxController {
isLoading.value = false; isLoading.value = false;
if (response != null) { if (response != null) {
attendenceLogsView = response attendenceLogsView =
.map((json) => AttendanceLogViewModel.fromJson(json)) response.map((json) => AttendanceLogViewModel.fromJson(json)).toList();
.toList();
log.i("Attendance log view fetched for ID: $id"); log.i("Attendance log view fetched for ID: $id");
update(); update();
} else { } else {

View File

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
class ButtonActions { class ButtonActions {
static const String checkIn = "Check In"; static const String checkIn = "Check In";
static const String checkOut = "Check Out"; 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 rejected = "Rejected";
static const String approved = "Approved"; static const String approved = "Approved";
static const String requested = "Requested"; static const String requested = "Requested";

View File

@ -1,3 +1,4 @@
import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
@ -9,18 +10,22 @@ class MyPaginatedTable extends StatefulWidget {
final List<DataRow> rows; final List<DataRow> rows;
final double columnSpacing; final double columnSpacing;
final double horizontalMargin; final double horizontalMargin;
final bool isLoading;
final Widget? footer;
const MyPaginatedTable({ const MyPaginatedTable({
super.key, super.key,
this.title, this.title,
required this.columns, required this.columns,
required this.rows, required this.rows,
this.columnSpacing = 23, this.columnSpacing = 20,
this.horizontalMargin = 35, this.horizontalMargin = 0,
this.isLoading = false,
this.footer,
}); });
@override @override
_MyPaginatedTableState createState() => _MyPaginatedTableState(); State<MyPaginatedTable> createState() => _MyPaginatedTableState();
} }
class _MyPaginatedTableState extends State<MyPaginatedTable> { class _MyPaginatedTableState extends State<MyPaginatedTable> {
@ -29,22 +34,30 @@ class _MyPaginatedTableState extends State<MyPaginatedTable> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final visibleRows = widget.rows.skip(_start).take(_rowsPerPage).toList();
final totalRows = widget.rows.length; final totalRows = widget.rows.length;
final totalPages = (totalRows / _rowsPerPage).ceil(); 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( return Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (widget.title != null) if (widget.title != null)
Padding( Padding(
padding: MySpacing.xy(8, 6), // Using standard spacing for title padding: MySpacing.xy(8, 6),
child: MyText.titleMedium(widget.title!, fontWeight: 600, fontSize: 20), child: MyText.titleMedium(
widget.title!,
fontWeight: 600,
fontSize: 20,
),
), ),
if (widget.rows.isEmpty) if (widget.rows.isEmpty)
Padding( Padding(
padding: MySpacing.all(16), // Standard padding for empty state padding: MySpacing.all(16),
child: MyText.bodySmall('No data available'), child: MyText.bodySmall('No data available'),
), ),
if (widget.rows.isNotEmpty) if (widget.rows.isNotEmpty)
@ -53,63 +66,75 @@ class _MyPaginatedTableState extends State<MyPaginatedTable> {
final spacing = _calculateSmartSpacing(constraints.maxWidth); final spacing = _calculateSmartSpacing(constraints.maxWidth);
return SingleChildScrollView( return SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: constraints.maxWidth),
child: MyContainer.bordered( child: MyContainer.bordered(
borderColor: Colors.black.withAlpha(40), borderColor: Colors.black.withAlpha(40),
padding: EdgeInsets.zero, padding: const EdgeInsets.all(10),
child: DataTable( child: DataTable(
columns: widget.columns, columns: widget.columns,
rows: visibleRows, rows: visibleRows,
columnSpacing: spacing, columnSpacing: spacing,
horizontalMargin: widget.horizontalMargin, horizontalMargin: widget.horizontalMargin,
headingRowHeight: 48,
dataRowHeight: 44,
),
), ),
), ),
); );
}, },
), ),
MySpacing.height(8), // Standard height spacing after table const SizedBox(height: 8),
widget.footer ??
PaginatedFooter( PaginatedFooter(
currentPage: currentPage, currentPage: currentPage,
totalPages: totalPages, totalPages: totalPages,
onPrevious: () { totalRows: totalRows,
setState(() { onPrevious: _handlePrevious,
_start = (_start - _rowsPerPage).clamp(0, totalRows - _rowsPerPage); onNext: _handleNext,
}); onPageChanged: _handlePageChanged,
}, onPageSizeChanged: _handlePageSizeChanged,
onNext: () {
setState(() {
_start = (_start + _rowsPerPage).clamp(0, totalRows - _rowsPerPage);
});
},
onPageSizeChanged: (newRowsPerPage) {
setState(() {
_rowsPerPage = newRowsPerPage;
_start = 0;
});
},
), ),
], ],
); );
} }
double _calculateSmartSpacing(double maxWidth) { void _handlePrevious() {
int columnCount = widget.columns.length; setState(() {
double horizontalPadding = widget.horizontalMargin * 2; _start = (_start - _rowsPerPage)
double availableWidth = maxWidth - horizontalPadding; .clamp(0, math.max(0, widget.rows.length - _rowsPerPage));
});
// Desired min/max column spacing
const double minSpacing = 16;
const double maxSpacing = 80;
// 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 void _handleNext() {
double spacing = (availableWidth / columnCount) - 40; // 40 for estimated cell content width 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) {
final columnCount = widget.columns.length;
final horizontalPadding = widget.horizontalMargin * 2;
final availableWidth = maxWidth - horizontalPadding;
const double minSpacing = 16;
const double maxSpacing = 64;
double spacing = (availableWidth / columnCount) - 40;
return spacing.clamp(minSpacing, maxSpacing); return spacing.clamp(minSpacing, maxSpacing);
} }
} }
@ -117,63 +142,103 @@ class _MyPaginatedTableState extends State<MyPaginatedTable> {
class PaginatedFooter extends StatelessWidget { class PaginatedFooter extends StatelessWidget {
final int currentPage; final int currentPage;
final int totalPages; final int totalPages;
final int totalRows;
final VoidCallback onPrevious; final VoidCallback onPrevious;
final VoidCallback onNext; final VoidCallback onNext;
final Function(int) onPageChanged;
final Function(int) onPageSizeChanged; final Function(int) onPageSizeChanged;
const PaginatedFooter({ const PaginatedFooter({
super.key,
required this.currentPage, required this.currentPage,
required this.totalPages, required this.totalPages,
required this.totalRows,
required this.onPrevious, required this.onPrevious,
required this.onNext, required this.onNext,
required this.onPageChanged,
required this.onPageSizeChanged, required this.onPageSizeChanged,
}); });
List<Widget> _buildPageButtons() {
List<Widget> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: MySpacing.x(16), // Standard horizontal spacing for footer padding: MySpacing.all(0),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
if (currentPage > 1)
IconButton( IconButton(
onPressed: onPrevious, onPressed: currentPage > 1 ? onPrevious : null,
icon: Icon(Icons.chevron_left), icon: const Icon(Icons.chevron_left),
tooltip: 'Previous',
), ),
Text( ..._buildPageButtons(),
'Page $currentPage of $totalPages',
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onBackground,
),
),
SizedBox(width: 8),
if (currentPage < totalPages)
IconButton( IconButton(
onPressed: onNext, onPressed: currentPage < totalPages ? onNext : null,
icon: Icon(Icons.chevron_right), icon: const Icon(Icons.chevron_right),
), tooltip: 'Next',
SizedBox(width: 16),
PopupMenuButton<int>(
icon: Icon(Icons.more_vert),
onSelected: (value) {
onPageSizeChanged(value);
},
itemBuilder: (BuildContext context) {
return [5, 10, 20, 50].map((e) {
return PopupMenuItem<int>(
value: e,
child: Text('$e rows per page'),
);
}).toList();
},
), ),
], ],
), ),
),
); );
} }
} }

View File

@ -241,7 +241,8 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
Widget employeeListTab() { Widget employeeListTab() {
if (attendanceController.employees.isEmpty) { if (attendanceController.employees.isEmpty) {
return Center( return Center(
child: MyText.bodySmall("No Employees Found", fontWeight: 600), child: MyText.bodySmall("No Employees Assigned to This Project",
fontWeight: 600),
); );
} }
@ -273,30 +274,42 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
), ),
), ),
DataCell( DataCell(
ElevatedButton( Obx(() {
onPressed: () async { final isUploading = attendanceController
if (attendanceController.selectedProjectId == null) { .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( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text("Please select a project first")), content: Text("Please select a project first")),
); );
controller.uploadingStates[employee.employeeId] =
RxBool(false);
return; return;
} }
final updatedAction =
int updatedAction = (activity == 0 || activity == 4) ? 0 : 1; (activity == 0 || activity == 4) ? 0 : 1;
String actionText = (updatedAction == 0) final actionText = (updatedAction == 0)
? ButtonActions.checkIn ? ButtonActions.checkIn
: ButtonActions.checkOut; : ButtonActions.checkOut;
final success = final success =
await attendanceController.captureAndUploadAttendance( await controller.captureAndUploadAttendance(
employee.id, employee.id,
employee.employeeId, employee.employeeId,
attendanceController.selectedProjectId!, controller.selectedProjectId!,
comment: actionText, comment: actionText,
action: updatedAction, action: updatedAction,
); );
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(success content: Text(success
@ -305,30 +318,45 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
), ),
); );
controller.uploadingStates[employee.employeeId] =
RxBool(false);
if (success) { if (success) {
attendanceController.fetchEmployeesByProject( await Future.wait([
attendanceController.selectedProjectId!); controller.fetchEmployeesByProject(
attendanceController.fetchAttendanceLogs( controller.selectedProjectId!),
attendanceController.selectedProjectId!); controller.fetchAttendanceLogs(
attendanceController controller.selectedProjectId!),
.fetchProjectData(attendanceController.selectedProjectId!); controller.fetchProjectData(
attendanceController.update(); controller.selectedProjectId!),
]);
controller.update();
} }
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AttendanceActionColors.colors[buttonText], backgroundColor: AttendanceActionColors.colors[buttonText],
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6),
minimumSize: const Size(60, 20),
textStyle: const TextStyle(fontSize: 12), textStyle: const TextStyle(fontSize: 12),
), ),
child: Text(buttonText), child: isUploading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(Colors.white),
), ),
)
: Text(buttonText),
),
);
}),
), ),
]); ]);
}).toList(); }).toList();
return Padding( return Padding(
padding: const EdgeInsets.all(8.0), // You can adjust this as needed padding: const EdgeInsets.all(0.0),
child: SingleChildScrollView( child: SingleChildScrollView(
child: MyPaginatedTable( child: MyPaginatedTable(
columns: columns, columns: columns,
@ -360,10 +388,10 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
// Add a row for the check-in date as a header // Add a row for the check-in date as a header
rows.add(DataRow(cells: [ rows.add(DataRow(cells: [
DataCell(MyText.bodyMedium(checkInDate, fontWeight: 600)), DataCell(MyText.bodyMedium(checkInDate, fontWeight: 600)),
DataCell(MyText.bodyMedium('')), // Placeholder for other columns DataCell(MyText.bodyMedium('')),
DataCell(MyText.bodyMedium('')), // Placeholder for other columns DataCell(MyText.bodyMedium('')),
DataCell(MyText.bodyMedium('')), // Placeholder for other columns DataCell(MyText.bodyMedium('')),
DataCell(MyText.bodyMedium('')), // Placeholder for other columns DataCell(MyText.bodyMedium('')),
])); ]));
// Add rows for each log in this group // Add rows for each log in this group
@ -385,12 +413,6 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// MyText.bodyMedium(
// log.checkIn != null
// ? DateFormat('dd MMM yyyy').format(log.checkIn!)
// : '-',
// fontWeight: 600,
// ),
MyText.bodyMedium( MyText.bodyMedium(
log.checkIn != null log.checkIn != null
? DateFormat('hh:mm a').format(log.checkIn!) ? DateFormat('hh:mm a').format(log.checkIn!)
@ -405,12 +427,6 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// MyText.bodyMedium(
// log.checkOut != null
// ? DateFormat('dd MMM yyyy').format(log.checkOut!)
// : '-',
// fontWeight: 600,
// ),
MyText.bodyMedium( MyText.bodyMedium(
log.checkOut != null log.checkOut != null
? DateFormat('hh:mm a').format(log.checkOut!) ? DateFormat('hh:mm a').format(log.checkOut!)
@ -427,6 +443,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
await attendanceController.fetchLogsView(log.id.toString()); await attendanceController.fetchLogsView(log.id.toString());
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: borderRadius:
BorderRadius.vertical(top: Radius.circular(16)), BorderRadius.vertical(top: Radius.circular(16)),
@ -434,7 +451,13 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
backgroundColor: Theme.of(context).cardColor, backgroundColor: Theme.of(context).cardColor,
builder: (context) { builder: (context) {
return Padding( return Padding(
padding: const EdgeInsets.all(16.0), padding: EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: MediaQuery.of(context).viewInsets.bottom + 16,
),
child: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -445,7 +468,8 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
if (attendanceController if (attendanceController
.attendenceLogsView.isNotEmpty) ...[ .attendenceLogsView.isNotEmpty) ...[
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded( Expanded(
child: MyText.bodyMedium("Date", child: MyText.bodyMedium("Date",
@ -466,7 +490,10 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
if (attendanceController if (attendanceController
.attendenceLogsView.isNotEmpty) .attendenceLogsView.isNotEmpty)
...attendanceController.attendenceLogsView ...attendanceController.attendenceLogsView
.map((log) => Row( .map((log) => Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0),
child: Row(
mainAxisAlignment: mainAxisAlignment:
MainAxisAlignment.spaceBetween, MainAxisAlignment.spaceBetween,
children: [ children: [
@ -504,7 +531,8 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
} }
}, },
child: const Padding( child: const Padding(
padding: EdgeInsets.only( padding:
EdgeInsets.only(
right: 4.0), right: 4.0),
child: Icon( child: Icon(
Icons.location_on, Icons.location_on,
@ -524,7 +552,8 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
Expanded( Expanded(
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
if (log.preSignedUrl != null) { if (log.preSignedUrl !=
null) {
showDialog( showDialog(
context: context, context: context,
builder: (_) => Dialog( builder: (_) => Dialog(
@ -532,19 +561,23 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
log.preSignedUrl!, log.preSignedUrl!,
fit: BoxFit.cover, fit: BoxFit.cover,
height: 400, height: 400,
errorBuilder: (context, errorBuilder:
error, stackTrace) { (context, error,
return Icon( stackTrace) {
Icons.broken_image, return const Icon(
Icons
.broken_image,
size: 50, size: 50,
color: Colors.grey); color: Colors
.grey);
}, },
), ),
), ),
); );
} }
}, },
child: log.thumbPreSignedUrl != null child: log.thumbPreSignedUrl !=
null
? Image.network( ? Image.network(
log.thumbPreSignedUrl!, log.thumbPreSignedUrl!,
height: 40, height: 40,
@ -552,19 +585,24 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
fit: BoxFit.cover, fit: BoxFit.cover,
errorBuilder: (context, errorBuilder: (context,
error, stackTrace) { error, stackTrace) {
return Icon( return const Icon(
Icons.broken_image, Icons
.broken_image,
size: 40, size: 40,
color: Colors.grey); color:
Colors.grey);
}, },
) )
: Icon(Icons.broken_image, : const Icon(
Icons.broken_image,
size: 40, size: 40,
color: Colors.grey), color: Colors.grey),
), ),
), ),
], ],
),
)), )),
const SizedBox(height: 16),
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: ElevatedButton( child: ElevatedButton(
@ -574,6 +612,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
), ),
], ],
), ),
),
); );
}, },
); );
@ -581,25 +620,50 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
), ),
), ),
DataCell( DataCell(
ElevatedButton( Obx(() {
onPressed: (log.activity == 5 || // Check if any record for this employee is uploading
log.activity == 2 || // Add this condition for activity 2 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 && (log.activity == 4 &&
!(log.checkOut != null && !(DateUtils.isSameDay(
log.checkIn != null && log.checkIn ?? DateTime(2000),
DateTime.now().difference(log.checkIn!).inDays <= DateTime.now())))
2)))
? null ? null
: () async { : () async {
// Set the uploading state for the employee when the action starts
attendanceController.uploadingStates[uniqueLogKey] =
RxBool(true);
if (attendanceController.selectedProjectId == null) { if (attendanceController.selectedProjectId == null) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text("Please select a project first"), content: Text("Please select a project first"),
), ),
); );
attendanceController.uploadingStates[uniqueLogKey] =
RxBool(false);
return; return;
} }
// Existing logic for updating action
int updatedAction; int updatedAction;
String actionText; String actionText;
bool imageCapture = true; bool imageCapture = true;
@ -631,7 +695,8 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
} else if (log.activity == 4 && } else if (log.activity == 4 &&
log.checkOut != null && log.checkOut != null &&
log.checkIn != null && log.checkIn != null &&
DateTime.now().difference(log.checkIn!).inDays <= 2) { DateTime.now().difference(log.checkIn!).inDays <=
2) {
updatedAction = 0; updatedAction = 0;
actionText = "Check In"; actionText = "Check In";
} else { } else {
@ -639,8 +704,9 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
actionText = "Unknown Action"; actionText = "Unknown Action";
} }
final success = // Proceed with capturing and uploading attendance
await attendanceController.captureAndUploadAttendance( final success = await attendanceController
.captureAndUploadAttendance(
log.id, log.id,
log.employeeId, log.employeeId,
attendanceController.selectedProjectId!, attendanceController.selectedProjectId!,
@ -649,15 +715,21 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
imageCapture: imageCapture, imageCapture: imageCapture,
); );
// Show result in SnackBar
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(success content: Text(success
? 'Attendance marked successfully!' ? 'Attendance marked successfully!'
: 'Image upload failed.'), : 'Failed to mark attendance.'),
), ),
); );
// Reset the uploading state after the action is complete
attendanceController.uploadingStates[uniqueLogKey] =
RxBool(false);
if (success) { if (success) {
// Update the UI with the new data
attendanceController.fetchEmployeesByProject( attendanceController.fetchEmployeesByProject(
attendanceController.selectedProjectId!); attendanceController.selectedProjectId!);
attendanceController.fetchAttendanceLogs( attendanceController.fetchAttendanceLogs(
@ -670,52 +742,83 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
} }
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: (log.activity == 4 && backgroundColor: isYesterday
? Colors
.grey // Button color for the disabled state (Yesterday's date)
: (log.activity == 4 &&
log.checkOut != null && log.checkOut != null &&
log.checkIn != null && log.checkIn != null &&
DateTime.now().difference(log.checkIn!).inDays <= 2) DateTime.now()
.difference(log.checkIn!)
.inDays <=
2)
? Colors.green ? Colors.green
: AttendanceActionColors.colors[(log.activity == 0) : AttendanceActionColors.colors[(log.activity == 0)
? ButtonActions.checkIn ? ButtonActions.checkIn
: ButtonActions.checkOut], : ButtonActions.checkOut],
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6), padding:
minimumSize: const Size(60, 20), const EdgeInsets.symmetric(vertical: 4, horizontal: 6),
textStyle: const TextStyle(fontSize: 12), textStyle: const TextStyle(fontSize: 12),
), ),
child: Text( child: isUploading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text(
(log.activity == 5) (log.activity == 5)
? ButtonActions.rejected ? ButtonActions.rejected
: (log.activity == 4 && : (log.activity == 4 &&
log.checkOut != null && log.checkOut != null &&
log.checkIn != null && log.checkIn != null &&
DateTime.now().difference(log.checkIn!).inDays <= 2) DateTime.now()
.difference(log.checkIn!)
.inDays <=
2)
? ButtonActions.checkIn ? ButtonActions.checkIn
: (log.activity == 2) // Change text when activity is 2 : (log.activity == 2)
? "Requested" ? "Requested"
: (log.activity == 4) : (log.activity == 4)
? ButtonActions.approved ? ButtonActions.approved
: (log.activity == 0) : (log.activity == 0 &&
!(log.checkIn != null &&
log.checkOut != null &&
!DateUtils.isSameDay(
log.checkIn!,
DateTime.now())))
? ButtonActions.checkIn ? ButtonActions.checkIn
: (log.activity == 1 && : (log.activity == 1 &&
log.checkOut != null && log.checkOut != null &&
DateTime.now() DateTime.now()
.difference(log.checkOut!) .difference(
log.checkOut!)
.inDays <= .inDays <=
2) 2)
? ButtonActions.checkOut ? ButtonActions.checkOut
: (log.activity == 2 || : (log.activity == 2 ||
(log.activity == 1 && (log.activity == 1 &&
log.checkOut == null && log.checkOut ==
log.checkIn != null && null &&
log.checkIn !=
null &&
log.checkIn!.isBefore( log.checkIn!.isBefore(
DateTime.now().subtract( DateTime.now()
Duration( .subtract(Duration(
days: 2))))) days:
? ButtonActions.requestRegularize 2)))))
? ButtonActions
.requestRegularize
: ButtonActions.checkOut, : ButtonActions.checkOut,
), ),
), ),
), );
}),
)
])); ]));
} }
}); });
@ -749,7 +852,6 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
child: MyPaginatedTable( child: MyPaginatedTable(
columns: columns, columns: columns,
rows: rows, rows: rows,
columnSpacing: 8.0,
), ),
), ),
], ],
@ -771,7 +873,11 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
]; ];
final rows = attendanceController.regularizationLogs final rows = attendanceController.regularizationLogs
.mapIndexed((index, log) => DataRow(cells: [ .mapIndexed((index, log) {
final uniqueLogKey = '${log.id}-${log.employeeId}'; // Unique key for each log
final isUploading = attendanceController.uploadingStates[uniqueLogKey]?.value ?? false; // Check the upload state
return DataRow(cells: [
DataCell( DataCell(
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -828,17 +934,18 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
children: [ children: [
// Approve Button // Approve Button
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: isUploading // Disable button if uploading
? null
: () async {
if (attendanceController.selectedProjectId == null) { if (attendanceController.selectedProjectId == null) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(content: Text("Please select a project first")),
content: Text("Please select a project first")),
); );
return; return;
} }
final success = await attendanceController attendanceController.uploadingStates[uniqueLogKey]?.value = true; // Start loading
.captureAndUploadAttendance( final success = await attendanceController.captureAndUploadAttendance(
log.id, log.id,
log.employeeId, log.employeeId,
attendanceController.selectedProjectId!, attendanceController.selectedProjectId!,
@ -864,6 +971,8 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
await attendanceController.fetchProjectData( await attendanceController.fetchProjectData(
attendanceController.selectedProjectId!); attendanceController.selectedProjectId!);
} }
attendanceController.uploadingStates[uniqueLogKey]?.value = false; // End loading
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AttendanceActionColors backgroundColor: AttendanceActionColors
@ -873,7 +982,9 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
minimumSize: const Size(60, 20), minimumSize: const Size(60, 20),
textStyle: const TextStyle(fontSize: 12), textStyle: const TextStyle(fontSize: 12),
), ),
child: const Text("Approve"), child: isUploading
? const CircularProgressIndicator(strokeWidth: 2) // Show loading indicator while uploading
: const Text("Approve"),
), ),
// Space between buttons // Space between buttons
@ -881,17 +992,19 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
// Reject Button // Reject Button
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: isUploading // Disable button if uploading
? null
: () async {
if (attendanceController.selectedProjectId == null) { if (attendanceController.selectedProjectId == null) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(content: Text("Please select a project first")),
content: Text("Please select a project first")),
); );
return; return;
} }
final success = await attendanceController attendanceController.uploadingStates[uniqueLogKey]?.value = true; // Start loading
.captureAndUploadAttendance(
final success = await attendanceController.captureAndUploadAttendance(
log.id, log.id,
log.employeeId, log.employeeId,
attendanceController.selectedProjectId!, attendanceController.selectedProjectId!,
@ -917,6 +1030,8 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
await attendanceController.fetchProjectData( await attendanceController.fetchProjectData(
attendanceController.selectedProjectId!); attendanceController.selectedProjectId!);
} }
attendanceController.uploadingStates[uniqueLogKey]?.value = false; // End loading
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor:
@ -926,12 +1041,15 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
minimumSize: const Size(60, 20), minimumSize: const Size(60, 20),
textStyle: const TextStyle(fontSize: 12), textStyle: const TextStyle(fontSize: 12),
), ),
child: const Text("Reject"), child: isUploading
? const CircularProgressIndicator(strokeWidth: 2) // Show loading indicator while uploading
: const Text("Reject"),
), ),
], ],
), ),
), ),
])) ]);
})
.toList(); .toList();
return Column( return Column(
@ -951,7 +1069,6 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
child: MyPaginatedTable( child: MyPaginatedTable(
// Use MyPaginatedTable here for pagination
columns: columns, columns: columns,
rows: rows, rows: rows,
columnSpacing: 15.0, columnSpacing: 15.0,