Merge pull request 'Task-#188' (#12) from Task-#188 into main
Reviewed-on: #12
This commit is contained in:
commit
eab722d5c0
@ -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 {
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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: MyContainer.bordered(
|
child: ConstrainedBox(
|
||||||
borderColor: Colors.black.withAlpha(40),
|
constraints: BoxConstraints(minWidth: constraints.maxWidth),
|
||||||
padding: EdgeInsets.zero,
|
child: MyContainer.bordered(
|
||||||
child: DataTable(
|
borderColor: Colors.black.withAlpha(40),
|
||||||
columns: widget.columns,
|
padding: const EdgeInsets.all(10),
|
||||||
rows: visibleRows,
|
child: DataTable(
|
||||||
columnSpacing: spacing,
|
columns: widget.columns,
|
||||||
horizontalMargin: widget.horizontalMargin,
|
rows: visibleRows,
|
||||||
|
columnSpacing: spacing,
|
||||||
|
horizontalMargin: widget.horizontalMargin,
|
||||||
|
headingRowHeight: 48,
|
||||||
|
dataRowHeight: 44,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
MySpacing.height(8), // Standard height spacing after table
|
const SizedBox(height: 8),
|
||||||
PaginatedFooter(
|
widget.footer ??
|
||||||
currentPage: currentPage,
|
PaginatedFooter(
|
||||||
totalPages: totalPages,
|
currentPage: currentPage,
|
||||||
onPrevious: () {
|
totalPages: totalPages,
|
||||||
setState(() {
|
totalRows: totalRows,
|
||||||
_start = (_start - _rowsPerPage).clamp(0, totalRows - _rowsPerPage);
|
onPrevious: _handlePrevious,
|
||||||
});
|
onNext: _handleNext,
|
||||||
},
|
onPageChanged: _handlePageChanged,
|
||||||
onNext: () {
|
onPageSizeChanged: _handlePageSizeChanged,
|
||||||
setState(() {
|
),
|
||||||
_start = (_start + _rowsPerPage).clamp(0, totalRows - _rowsPerPage);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onPageSizeChanged: (newRowsPerPage) {
|
|
||||||
setState(() {
|
|
||||||
_rowsPerPage = newRowsPerPage;
|
|
||||||
_start = 0;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
double _calculateSmartSpacing(double maxWidth) {
|
||||||
int columnCount = widget.columns.length;
|
final columnCount = widget.columns.length;
|
||||||
double horizontalPadding = widget.horizontalMargin * 2;
|
final horizontalPadding = widget.horizontalMargin * 2;
|
||||||
double availableWidth = maxWidth - horizontalPadding;
|
final availableWidth = maxWidth - horizontalPadding;
|
||||||
|
|
||||||
// Desired min/max column spacing
|
|
||||||
const double minSpacing = 16;
|
const double minSpacing = 16;
|
||||||
const double maxSpacing = 80;
|
const double maxSpacing = 64;
|
||||||
|
|
||||||
// Total width assuming minimal spacing
|
double spacing = (availableWidth / columnCount) - 40;
|
||||||
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
|
|
||||||
return spacing.clamp(minSpacing, maxSpacing);
|
return spacing.clamp(minSpacing, maxSpacing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -117,62 +142,102 @@ 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(
|
child: Row(
|
||||||
scrollDirection: Axis.horizontal,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
child: Row(
|
children: [
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
IconButton(
|
||||||
children: [
|
onPressed: currentPage > 1 ? onPrevious : null,
|
||||||
if (currentPage > 1)
|
icon: const Icon(Icons.chevron_left),
|
||||||
IconButton(
|
tooltip: 'Previous',
|
||||||
onPressed: onPrevious,
|
),
|
||||||
icon: Icon(Icons.chevron_left),
|
..._buildPageButtons(),
|
||||||
),
|
IconButton(
|
||||||
Text(
|
onPressed: currentPage < totalPages ? onNext : null,
|
||||||
'Page $currentPage of $totalPages',
|
icon: const Icon(Icons.chevron_right),
|
||||||
style: TextStyle(
|
tooltip: 'Next',
|
||||||
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();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user