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: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 {

View File

@ -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";

View File

@ -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',
),
],
),
);
}

File diff suppressed because it is too large Load Diff