made chnages into attendence screen
This commit is contained in:
parent
dd31cdafd0
commit
defd753ab0
@ -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<AttendanceLogViewModel> attendenceLogsView = [];
|
||||
|
||||
RxBool isLoading = false.obs;
|
||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.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<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
|
||||
|
||||
log.i("Logs grouped and sorted by check-in date.");
|
||||
return sortedMap;
|
||||
}
|
||||
|
||||
Future<void> 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 {
|
||||
|
@ -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";
|
||||
|
@ -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<DataRow> 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<MyPaginatedTable> createState() => _MyPaginatedTableState();
|
||||
}
|
||||
|
||||
class _MyPaginatedTableState extends State<MyPaginatedTable> {
|
||||
@ -29,22 +34,30 @@ class _MyPaginatedTableState extends State<MyPaginatedTable> {
|
||||
|
||||
@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<MyPaginatedTable> {
|
||||
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<MyPaginatedTable> {
|
||||
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<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
|
||||
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<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();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
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',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -241,7 +241,8 @@ class _AttendanceScreenState extends State<AttendanceScreen> 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<AttendanceScreen> 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<Color>(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<AttendanceScreen> 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<AttendanceScreen> 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<AttendanceScreen> 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<AttendanceScreen> 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<AttendanceScreen> 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<AttendanceScreen> 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<Color>(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<AttendanceScreen> with UIMixin {
|
||||
child: MyPaginatedTable(
|
||||
columns: columns,
|
||||
rows: rows,
|
||||
columnSpacing: 8.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -951,7 +1053,6 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: MyPaginatedTable(
|
||||
// Use MyPaginatedTable here for pagination
|
||||
columns: columns,
|
||||
rows: rows,
|
||||
columnSpacing: 15.0,
|
||||
|
Loading…
x
Reference in New Issue
Block a user