implemented peginated table

This commit is contained in:
Vaibhav Surve 2025-04-30 17:55:09 +05:30
parent 1007e081b9
commit daf40ab2ce
13 changed files with 1221 additions and 658 deletions

View File

@ -9,6 +9,7 @@ class LoginController extends MyController {
MyFormValidator basicValidator = MyFormValidator();
bool showPassword = false, isChecked = false;
RxBool isLoading = false.obs; // Add reactive loading state
final String _dummyEmail = "admin@marcobms.com";
final String _dummyPassword = "User@123";
@ -16,10 +17,7 @@ class LoginController extends MyController {
@override
void onInit() {
basicValidator.addField('username', required: true, label: "User_Name", validators: [MyEmailValidator()], controller: TextEditingController(text: _dummyEmail));
basicValidator.addField('password',
required: true, label: "Password", validators: [MyLengthValidator(min: 6, max: 10)], controller: TextEditingController(text: _dummyPassword));
basicValidator.addField('password', required: true, label: "Password", validators: [MyLengthValidator(min: 6, max: 10)], controller: TextEditingController(text: _dummyPassword));
super.onInit();
}
@ -35,7 +33,10 @@ class LoginController extends MyController {
Future<void> onLogin() async {
if (basicValidator.validateForm()) {
// Set loading to true
isLoading.value = true;
update();
var errors = await AuthService.loginUser(basicValidator.getData());
if (errors != null) {
basicValidator.addErrors(errors);
@ -45,6 +46,9 @@ class LoginController extends MyController {
String nextUrl = Uri.parse(ModalRoute.of(Get.context!)?.settings.name ?? "").queryParameters['next'] ?? "/home";
Get.toNamed(nextUrl);
}
// Set loading to false after the API call is complete
isLoading.value = false;
update();
}
}
@ -57,3 +61,4 @@ class LoginController extends MyController {
Get.offAndToNamed('/auth/register_account');
}
}

View File

@ -24,6 +24,8 @@ class AttendanceController extends GetxController {
List<RegularizationLogModel> regularizationLogs = [];
List<AttendanceLogViewModel> attendenceLogsView = [];
RxBool isLoading = false.obs; // Added loading flag
@override
void onInit() {
super.onInit();
@ -42,7 +44,9 @@ class AttendanceController extends GetxController {
}
Future<void> fetchProjects() async {
isLoading.value = true; // Set loading to true before API call
final response = await ApiService.getProjects();
isLoading.value = false; // Set loading to false after API call completes
if (response != null && response.isNotEmpty) {
projects = response.map((json) => ProjectModel.fromJson(json)).toList();
@ -57,19 +61,22 @@ class AttendanceController extends GetxController {
Future<void> _fetchProjectData(String? projectId) async {
if (projectId == null) return;
isLoading.value = true; // Set loading to true before API call
await Future.wait([
fetchEmployeesByProject(projectId),
fetchAttendanceLogs(projectId,
dateFrom: startDateAttendance, dateTo: endDateAttendance),
fetchAttendanceLogs(projectId, dateFrom: startDateAttendance, dateTo: endDateAttendance),
fetchRegularizationLogs(projectId),
]);
isLoading.value = false; // Set loading to false after data is fetched
}
Future<void> fetchEmployeesByProject(String? projectId) async {
if (projectId == null) return;
final response =
await ApiService.getEmployeesByProject(int.parse(projectId));
isLoading.value = true; // Set loading to true before API call
final response = await ApiService.getEmployeesByProject(int.parse(projectId));
isLoading.value = false; // Set loading to false after API call completes
if (response != null) {
employees = response.map((json) => EmployeeModel.fromJson(json)).toList();
update();
@ -84,20 +91,25 @@ class AttendanceController extends GetxController {
int projectId, {
String comment = "Marked via mobile app",
required int action,
bool imageCapture = true, // <- add this flag
}) async {
try {
final image = await ImagePicker().pickImage(
XFile? image;
if (imageCapture) {
image = await ImagePicker().pickImage(
source: ImageSource.camera,
imageQuality: 80,
);
if (image == null) return false;
}
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
final imageName =
ApiService.generateImageName(employeeId, employees.length + 1);
final imageName = imageCapture
? ApiService.generateImageName(employeeId, employees.length + 1)
: ""; // Empty or null if not capturing image
return await ApiService.uploadAttendanceImage(
id,
@ -109,6 +121,7 @@ class AttendanceController extends GetxController {
projectId: projectId,
comment: comment,
action: action,
imageCapture: imageCapture, // <- pass flag down
);
} catch (e) {
print("Error capturing or uploading attendance: $e");
@ -125,8 +138,7 @@ class AttendanceController extends GetxController {
firstDate: DateTime(2022),
lastDate: DateTime.now(),
initialDateRange: DateTimeRange(
start: startDateAttendance ??
DateTime.now().subtract(const Duration(days: 7)),
start: startDateAttendance ?? DateTime.now().subtract(const Duration(days: 7)),
end: endDateAttendance ?? DateTime.now(),
),
);
@ -150,15 +162,16 @@ class AttendanceController extends GetxController {
}) async {
if (projectId == null) return;
isLoading.value = true; // Set loading to true before API call
final response = await ApiService.getAttendanceLogs(
int.parse(projectId),
dateFrom: dateFrom,
dateTo: dateTo,
);
isLoading.value = false; // Set loading to false after API call completes
if (response != null) {
attendanceLogs =
response.map((json) => AttendanceLogModel.fromJson(json)).toList();
attendanceLogs = response.map((json) => AttendanceLogModel.fromJson(json)).toList();
print("Attendance logs fetched: ${response}");
update();
} else {
@ -173,8 +186,9 @@ class AttendanceController extends GetxController {
}) async {
if (projectId == null) return;
final response =
await ApiService.getRegularizationLogs(int.parse(projectId));
isLoading.value = true; // Set loading to true before API call
final response = await ApiService.getRegularizationLogs(int.parse(projectId));
isLoading.value = false; // Set loading to false after API call completes
if (response != null) {
regularizationLogs = response
@ -189,8 +203,9 @@ class AttendanceController extends GetxController {
Future<void> fetchLogsView(String? id) async {
if (id == null) return;
final response =
await ApiService.getAttendanceLogView(int.parse(id));
isLoading.value = true; // Set loading to true before API call
final response = await ApiService.getAttendanceLogView(int.parse(id));
isLoading.value = false; // Set loading to false after API call completes
if (response != null) {
attendenceLogsView = response

View File

@ -128,7 +128,7 @@ class ApiService {
final fileSize = await imageFile.length();
final contentType = "image/${imageFile.path.split('.').last}";
final imageObject = {
imageObject = {
"fileName": '$imageName',
"contentType": '$contentType',
"fileSize": fileSize,
@ -147,10 +147,15 @@ class ApiService {
"comment": comment,
"action": action,
"date": DateFormat('yyyy-MM-dd').format(now),
"latitude": '$latitude',
"longitude": '$longitude',
};
// Only include latitude and longitude if imageCapture is true
if (imageCapture) {
body["latitude"] = '$latitude';
body["longitude"] = '$longitude';
}
// Only add imageObject if it's not null
if (imageObject != null) {
body["image"] = imageObject;
}

View File

@ -3,7 +3,7 @@ import 'package:marco/helpers/services/localizations/language.dart';
import 'package:marco/helpers/theme/theme_customizer.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:marco/model/user_permission.dart';
import 'package:marco/model/employee_info.dart'; // Import the EmployeeInfo model
import 'package:marco/model/employee_info.dart';
import 'dart:convert';
class LocalStorage {
static const String _loggedInUserKey = "user";

View File

@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
// Define action texts
class ButtonActions {
static const String checkIn = "Check In";
static const String checkOut = "Check Out";
static const String requestRegularize = " Request Regularize";
static const String rejected = "Rejected";
static const String approved = "Approved";
static const String requested = "Requested";
static const String approve = "Approve";
static const String reject = "Reject";
}
// Map action texts to colors
class AttendanceActionColors {
static const Map<String, Color> colors = {
ButtonActions.checkIn: Colors.green,
ButtonActions.checkOut: Colors.red,
ButtonActions.requestRegularize: Colors.blue,
ButtonActions.rejected: Colors.orange,
ButtonActions.approved: Colors.green,
ButtonActions.requested: Colors.yellow,
ButtonActions.approve: Colors.blueAccent,
ButtonActions.reject: Colors.pink,
};
}

View File

@ -5,6 +5,7 @@ class AttendanceLogModel {
final DateTime? checkOut;
final int activity;
final int id;
final int employeeId;
AttendanceLogModel({
required this.name,
@ -13,6 +14,7 @@ class AttendanceLogModel {
this.checkOut,
required this.activity,
required this.id,
required this.employeeId,
});
factory AttendanceLogModel.fromJson(Map<String, dynamic> json) {
@ -23,6 +25,7 @@ class AttendanceLogModel {
checkOut: json['checkOutTime'] != null ? DateTime.tryParse(json['checkOutTime']) : null,
activity: json['activity'] ?? 0,
id: json['id'] != null ? json['id'] : null,
employeeId: json['employeeId'] != null ? json['employeeId'] : null,
);
}
}

View File

@ -2,19 +2,31 @@ import 'package:intl/intl.dart';
class AttendanceLogViewModel {
final DateTime? activityTime;
final String? imageUrl;
final String? description;
final String? comment;
final String? thumbPreSignedUrl;
final String? preSignedUrl;
final String? longitude;
final String? latitude;
AttendanceLogViewModel({
this.activityTime,
this.imageUrl,
this.description,
this.comment,
this.thumbPreSignedUrl,
this.preSignedUrl,
this.longitude,
this.latitude,
});
factory AttendanceLogViewModel.fromJson(Map<String, dynamic> json) {
return AttendanceLogViewModel(
activityTime: json['activityTime'] != null ? DateTime.tryParse(json['activityTime']) : null,
imageUrl: json['imageUrl'],
description: json['description'],
comment: json['comment'],
thumbPreSignedUrl: json['thumbPreSignedUrl'],
preSignedUrl: json['preSignedUrl'],
longitude: json['longitude'],
latitude: json['latitude'],
);
}

View File

@ -0,0 +1,179 @@
import 'package:flutter/material.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_container.dart';
class MyPaginatedTable extends StatefulWidget {
final String? title;
final List<DataColumn> columns;
final List<DataRow> rows;
final double columnSpacing;
final double horizontalMargin;
const MyPaginatedTable({
super.key,
this.title,
required this.columns,
required this.rows,
this.columnSpacing = 23,
this.horizontalMargin = 35,
});
@override
_MyPaginatedTableState createState() => _MyPaginatedTableState();
}
class _MyPaginatedTableState extends State<MyPaginatedTable> {
int _start = 0;
int _rowsPerPage = 10;
@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;
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
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),
),
if (widget.rows.isEmpty)
Padding(
padding: MySpacing.all(16), // Standard padding for empty state
child: MyText.bodySmall('No data available'),
),
if (widget.rows.isNotEmpty)
LayoutBuilder(
builder: (context, constraints) {
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,
),
),
);
},
),
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;
});
},
),
],
);
}
double _calculateSmartSpacing(double maxWidth) {
int columnCount = widget.columns.length;
double horizontalPadding = widget.horizontalMargin * 2;
double availableWidth = maxWidth - horizontalPadding;
// 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
double spacing = (availableWidth / columnCount) - 40; // 40 for estimated cell content width
return spacing.clamp(minSpacing, maxSpacing);
}
}
class PaginatedFooter extends StatelessWidget {
final int currentPage;
final int totalPages;
final VoidCallback onPrevious;
final VoidCallback onNext;
final Function(int) onPageSizeChanged;
const PaginatedFooter({
required this.currentPage,
required this.totalPages,
required this.onPrevious,
required this.onNext,
required this.onPageSizeChanged,
});
@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();
},
),
],
),
),
);
}
}

View File

@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
class PaginatedTableWidget<T> extends StatefulWidget {
final List<DataColumn> columns;
final List<DataRow> rows;
final int initialRowsPerPage;
final Function(int, int)? onPageChanged;
final double columnSpacing;
const PaginatedTableWidget({
required this.columns,
required this.rows,
this.initialRowsPerPage = 5,
this.onPageChanged,
this.columnSpacing = 16.0, // Default column spacing
});
@override
_PaginatedTableWidgetState createState() => _PaginatedTableWidgetState();
}
class _PaginatedTableWidgetState extends State<PaginatedTableWidget> {
late int rowsPerPage;
@override
void initState() {
super.initState();
rowsPerPage = widget.initialRowsPerPage;
}
@override
Widget build(BuildContext context) {
return PaginatedDataTable(
rowsPerPage: rowsPerPage,
availableRowsPerPage: [5, 10, 20, 50, 100],
onRowsPerPageChanged: (newRowsPerPage) {
setState(() {
rowsPerPage = newRowsPerPage ?? rowsPerPage;
});
},
columns: widget.columns,
source: _DataTableSource(
rows: widget.rows,
),
columnSpacing: widget.columnSpacing,
);
}
}
class _DataTableSource extends DataTableSource {
final List<DataRow> rows;
_DataTableSource({required this.rows});
@override
DataRow? getRow(int index) {
if (index >= rows.length) {
return null;
}
return rows[index];
}
@override
int get rowCount => rows.length;
@override
bool get isRowCountApproximate => false;
@override
int get selectedRowCount => 0;
}

View File

@ -1,4 +1,6 @@
class RegularizationLogModel {
final int id;
final int employeeId;
final String name;
final String role;
final DateTime? checkIn;
@ -6,6 +8,8 @@ class RegularizationLogModel {
final int activity;
RegularizationLogModel({
required this.id,
required this.employeeId,
required this.name,
required this.role,
this.checkIn,
@ -13,12 +17,19 @@ class RegularizationLogModel {
required this.activity,
});
factory RegularizationLogModel.fromJson(Map<String, dynamic> json) {
return RegularizationLogModel(
id: json['id'] ?? 0,
employeeId: json['employeeId'] ?? 0,
name: "${json['firstName'] ?? ''} ${json['lastName'] ?? ''}".trim(),
role: json['jobRoleName'] ?? '',
checkIn: json['checkInTime'] != null ? DateTime.tryParse(json['checkInTime']) : null,
checkOut: json['checkOutTime'] != null ? DateTime.tryParse(json['checkOutTime']) : null,
checkIn: json['checkInTime'] != null
? DateTime.tryParse(json['checkInTime'])
: null,
checkOut: json['checkOutTime'] != null
? DateTime.tryParse(json['checkOutTime'])
: null,
activity: json['activity'] ?? 0,
);
}

View File

@ -34,7 +34,10 @@ class _LoginScreenState extends State<LoginScreen> with UIMixin {
init: controller,
tag: 'login_controller',
builder: (controller) {
return Form(
return Obx(() {
return controller.isLoading.value
? Center(child: CircularProgressIndicator()) // Show loading spinner when isLoading is true
: Form(
key: controller.basicValidator.formKey,
child: SingleChildScrollView(
padding: MySpacing.xy(2, 40),
@ -220,6 +223,7 @@ class _LoginScreenState extends State<LoginScreen> with UIMixin {
),
),
);
});
},
),
);

View File

@ -16,6 +16,11 @@ import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/view/layouts/layout.dart';
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
import 'package:intl/intl.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/helpers/utils/permission_constants.dart';
import 'package:marco/helpers/utils/attendance_actions.dart';
import 'package:marco/helpers/widgets/my_refresh_wrapper.dart';
import 'package:marco/model/my_paginated_table.dart';
class AttendanceScreen extends StatefulWidget {
const AttendanceScreen({super.key});
@ -26,10 +31,22 @@ class AttendanceScreen extends StatefulWidget {
class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
AttendanceController attendanceController = Get.put(AttendanceController());
PermissionController permissionController = Get.put(PermissionController());
@override
Widget build(BuildContext context) {
return Layout(
child: MyRefreshWrapper(
onRefresh: () async {
if (attendanceController.selectedProjectId != null) {
await attendanceController.fetchEmployeesByProject(
attendanceController.selectedProjectId!);
await attendanceController
.fetchAttendanceLogs(attendanceController.selectedProjectId!);
} else {
await attendanceController.fetchProjects();
}
},
child: GetBuilder(
init: attendanceController,
tag: 'attendance_dashboard_controller',
@ -56,28 +73,20 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
MySpacing.height(flexSpacing),
Padding(
padding: MySpacing.x(flexSpacing / 2),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: MyFlex(
children: [
MySpacing.height(flexSpacing),
// Move project selection here
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
// Popup Menu for Project Selection
MyFlexItem(
sizes: 'lg-12',
child: MyContainer.bordered(
padding: MySpacing.xy(8, 8),
child: PopupMenuButton<String>(
onSelected: (value) {
setState(() {
attendanceController.selectedProjectId =
value;
attendanceController.selectedProjectId = value;
attendanceController
.fetchEmployeesByProject(value);
attendanceController
.fetchAttendanceLogs(value);
attendanceController
.fetchAttendanceLogs(value);
attendanceController.fetchAttendanceLogs(value);
});
},
itemBuilder: (BuildContext context) {
@ -105,12 +114,10 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
},
color: theme.cardTheme.color,
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.labelSmall(
attendanceController.selectedProjectId !=
null
attendanceController.selectedProjectId != null
? attendanceController.projects
.firstWhereOrNull((proj) =>
proj.id.toString() ==
@ -129,14 +136,31 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
),
),
),
],
),
MySpacing.height(flexSpacing),
MyFlex(
children: [
// Tabs for Employee List, Logs, and Regularization
MyFlexItem(
child: DefaultTabController(
length: 3,
sizes: 'lg-12',
child: Obx(() {
bool hasRegularizationPermission =
permissionController.hasPermission(
Permissions.regularizeAttendance);
final tabs = <Tab>[
const Tab(text: 'Employee List'),
const Tab(text: 'Logs'),
if (hasRegularizationPermission)
const Tab(text: 'Regularization'),
];
final views = <Widget>[
employeeListTab(),
reportsTab(context),
if (hasRegularizationPermission)
regularizationTab(context),
];
return DefaultTabController(
length: tabs.length,
child: MyCard.bordered(
borderRadiusAll: 4,
border:
@ -153,29 +177,18 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
unselectedLabelColor: theme
.colorScheme.onSurface
.withAlpha(150),
tabs: const [
Tab(text: 'Employee List'),
Tab(text: 'Logs'),
Tab(text: 'Regularization'),
],
tabs: tabs,
),
MySpacing.height(16),
SizedBox(
height: 500,
child: TabBarView(
children: [
employeeListTab(),
reportsTab(context),
regularizationTab(context),
],
),
height: 550,
child: TabBarView(children: views),
),
],
),
),
),
),
],
);
}),
),
],
),
@ -184,6 +197,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
);
},
),
),
);
}
@ -194,115 +208,92 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
);
}
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
sortAscending: true,
columnSpacing: 15,
headingRowColor:
WidgetStatePropertyAll(contentTheme.primary.withAlpha(40)),
dataRowMaxHeight: 60,
showBottomBorder: true,
clipBehavior: Clip.antiAliasWithSaveLayer,
border: TableBorder.all(
borderRadius: BorderRadius.circular(4),
style: BorderStyle.solid,
width: 0.4,
color: Colors.grey,
final columns = <DataColumn>[
DataColumn(
label: MyText.labelLarge('Name', color: contentTheme.primary),
),
columns: [
DataColumn(
label: MyText.labelLarge('Name', color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Designation',
color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Actions', color: contentTheme.primary)),
],
rows: attendanceController.employees.mapIndexed((index, employee) {
// Set actionText directly from employee's action
String actionText = "";
int? activity =
employee.activity; // Assuming employee has an 'action' field
label: MyText.labelLarge('Actions', color: contentTheme.primary),
),
];
// Set action text based on employee's activity value
if (activity == 1) {
actionText = "Check In";
} else if (activity == 0) {
actionText = "Check Out";
} else if (activity == 4) {
// Activity 4 logic
actionText = "Check In";
}
final rows = attendanceController.employees.mapIndexed((index, employee) {
int? activity = employee.activity;
String buttonText = (activity == 0 || activity == 4)
? ButtonActions.checkIn
: ButtonActions.checkOut;
return DataRow(cells: [
DataCell(MyText.bodyMedium(employee.name, fontWeight: 600)),
DataCell(MyText.bodyMedium(employee.designation, fontWeight: 600)),
DataCell(ElevatedButton(
DataCell(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
MyText.bodyMedium(employee.name, fontWeight: 600),
SizedBox(height: 2),
MyText.bodySmall(employee.designation, color: Colors.grey),
],
),
),
DataCell(
ElevatedButton(
onPressed: () async {
if (attendanceController.selectedProjectId == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Please select a project first")),
const SnackBar(
content: Text("Please select a project first")),
);
return;
}
// Determine the updated action based on current activity
int updatedAction;
String actionText;
int updatedAction = (activity == 0 || activity == 4) ? 0 : 1;
String actionText = (updatedAction == 0)
? ButtonActions.checkIn
: ButtonActions.checkOut;
if (activity == 0 || activity == 4) {
// The user is currently checked in (activity == 0), so they need to check out
updatedAction = 0;
actionText = "Check In";
} else {
// The user is currently checked out (activity == 1), so they need to check in
updatedAction = 1;
actionText = "Check Out";
}
// Call the method to capture attendance with the updated action
final success =
await attendanceController.captureAndUploadAttendance(
employee.id, // Pass the employee's ID
employee.id,
employee.employeeId,
int.parse(attendanceController
.selectedProjectId!), // Pass the selected project ID
comment: actionText, // Action text (Check In / Check Out)
int.parse(attendanceController.selectedProjectId!),
comment: actionText,
action: updatedAction,
);
// Show success or failure message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
success
content: Text(success
? 'Attendance marked successfully!'
: 'Image upload failed.',
),
: 'Image upload failed.'),
),
);
if (success) {
// Fetch the updated list of employees and logs after the attendance upload
attendanceController.fetchEmployeesByProject(
attendanceController.selectedProjectId!);
attendanceController.fetchAttendanceLogs(
attendanceController.selectedProjectId!);
// You can add more fetch calls if necessary, such as regularization logs.
}
},
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(
vertical: 4, horizontal: 6), // Adjust padding
minimumSize: Size(60, 20), // Adjust minimum size for the button
textStyle: TextStyle(fontSize: 12), // Smaller font size
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: Text(
activity == 0 || activity == 4 ? 'Check In' : 'Check Out'),
)),
]);
}).toList(),
}).toList();
return Padding(
padding: const EdgeInsets.all(8.0), // You can adjust this as needed
child: SingleChildScrollView(
child: MyPaginatedTable(
columns: columns,
rows: rows,
),
),
);
}
@ -310,171 +301,467 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
Widget reportsTab(BuildContext context) {
final attendanceController = Get.find<AttendanceController>();
return Column(
final columns = [
DataColumn(label: MyText.labelLarge('Name', color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Check-In', color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Check-Out', color: contentTheme.primary)),
DataColumn(label: MyText.labelLarge('View', color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Action', color: contentTheme.primary)),
];
final rows = attendanceController.attendanceLogs.mapIndexed((index, log) {
return DataRow(cells: [
DataCell(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
MyText.bodyMedium(log.name, fontWeight: 600),
SizedBox(height: 2),
MyText.bodySmall(log.role, color: Colors.grey),
],
),
),
DataCell(
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextButton.icon(
icon: Icon(Icons.date_range),
label: Text("Select Date Range for Attendance"),
onPressed: () => attendanceController.selectDateRangeForAttendance(
context, attendanceController),
),
),
if (attendanceController.attendanceLogs.isEmpty)
Expanded(
child: Center(
child: MyText.bodySmall("No Attendance Records Found",
fontWeight: 600),
),
)
else
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
sortAscending: true,
columnSpacing: 15,
headingRowColor:
WidgetStatePropertyAll(contentTheme.primary.withAlpha(40)),
dataRowMaxHeight: 60,
showBottomBorder: true,
clipBehavior: Clip.antiAliasWithSaveLayer,
border: TableBorder.all(
borderRadius: BorderRadius.circular(4),
style: BorderStyle.solid,
width: 0.4,
color: Colors.grey,
),
columns: [
DataColumn(
label: MyText.labelLarge('Name',
color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Role',
color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Check-In',
color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Check-Out',
color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Action',
color: contentTheme.primary)),
],
rows: attendanceController.attendanceLogs
.mapIndexed((index, log) => DataRow(cells: [
DataCell(
MyText.bodyMedium(log.name, fontWeight: 600)),
DataCell(
MyText.bodyMedium(log.role, fontWeight: 600)),
DataCell(MyText.bodyMedium(
MyText.bodyMedium(
log.checkIn != null
? DateFormat('dd MMM yyyy hh:mm a')
.format(log.checkIn!)
? DateFormat('dd MMM yyyy').format(log.checkIn!)
: '-',
fontWeight: 600,
)),
DataCell(MyText.bodyMedium(
log.checkOut != null
? DateFormat('dd MMM yyyy hh:mm a')
.format(log.checkOut!)
: '-',
fontWeight: 600,
)),
DataCell(
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(
vertical: 4,
horizontal: 6), // Adjust padding
minimumSize:
Size(60, 20), // Adjust minimum size
textStyle: TextStyle(
fontSize: 12), // Smaller font size
),
MyText.bodyMedium(
log.checkIn != null
? DateFormat('hh:mm a').format(log.checkIn!)
: '',
fontWeight: 600,
),
],
),
),
DataCell(
Column(
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!)
: '',
fontWeight: 600,
),
],
),
),
DataCell(
IconButton(
icon: const Icon(Icons.visibility, size: 18),
onPressed: () async {
// Call fetchLogsView to load the log data
await attendanceController.fetchLogsView(log.id
.toString()); // Assuming `log.id` is available
// Open the bottom sheet to display the log details
await attendanceController.fetchLogsView(log.id.toString());
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(16)),
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
backgroundColor: theme.cardTheme.color,
backgroundColor: Theme.of(context).cardColor,
builder: (context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleMedium(
"Attendance Log Details",
MyText.titleMedium("Attendance Log Details",
fontWeight: 700),
const SizedBox(height: 16),
// Display the log details
if (attendanceController
.attendenceLogsView.isNotEmpty)
...attendanceController
.attendenceLogsView
.map((log) => Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
.attendenceLogsView.isNotEmpty) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.bodyMedium(
"Date: ${log.formattedDate ?? '-'}",
fontWeight: 600,
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)),
],
),
MyText.bodyMedium(
"Time: ${log.formattedTime ?? '-'}",
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: 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);
},
),
),
);
}
},
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),
),
MyText.bodyMedium(
"Description: ${log.description ?? '-'}",
fontWeight: 600,
),
const Divider(
thickness: 1,
height: 24),
],
)),
Align(
alignment: Alignment.centerRight,
child: ElevatedButton(
onPressed: () =>
Navigator.pop(context),
onPressed: () => Navigator.pop(context),
child: const Text("Close"),
),
)
),
],
),
);
},
);
},
child: const Text('View'),
),
),
DataCell(
ElevatedButton(
onPressed: () async {
if (attendanceController.selectedProjectId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Please select a project first")),
);
return;
}
int updatedAction;
String actionText;
if (log.activity == 0 || log.activity == 4) {
updatedAction = 0;
actionText = "Check In";
} else if (log.activity == 1) {
updatedAction = 1;
actionText = "Check Out";
} else if (log.activity == 2) {
updatedAction = 2;
actionText = "Request Regularize";
} else {
updatedAction = 0;
actionText = "Unknown Action";
}
final success =
await attendanceController.captureAndUploadAttendance(
log.id,
log.employeeId,
int.parse(attendanceController.selectedProjectId!),
comment: actionText,
action: updatedAction,
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success
? 'Attendance marked successfully!'
: 'Image upload failed.')),
);
if (success) {
attendanceController.fetchEmployeesByProject(
attendanceController.selectedProjectId!);
attendanceController.fetchAttendanceLogs(
attendanceController.selectedProjectId!);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AttendanceActionColors.colors[
(log.activity == 0 || log.activity == 4)
? 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 == 0 || log.activity == 4)
? ButtonActions.checkIn
: (log.activity == 2)
? ButtonActions.requestRegularize
: ButtonActions.checkOut,
),
),
),
]);
}).toList();
return SingleChildScrollView(
// Wrap the Column in SingleChildScrollView to handle overflow
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextButton.icon(
icon: const Icon(Icons.date_range),
label: const Text("Select Date Range for Attendance"),
onPressed: () => attendanceController
.selectDateRangeForAttendance(context, attendanceController),
),
),
if (attendanceController.attendanceLogs.isEmpty)
Align(
alignment: Alignment.center,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 40),
child: MyText.bodySmall(
"No Attendance Records Found",
fontWeight: 600,
),
),
)
]))
.toList(),
),
else
SingleChildScrollView(
child: MyPaginatedTable(
columns: columns,
rows: rows,
columnSpacing: 8.0,
),
),
],
),
);
}
Widget regularizationTab(BuildContext context) {
final attendanceController = Get.find<AttendanceController>();
final columns = [
DataColumn(label: MyText.labelLarge('Name', color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Check-In', color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Check-Out', color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Action', color: contentTheme.primary)),
];
final rows = attendanceController.regularizationLogs
.mapIndexed((index, log) => DataRow(cells: [
DataCell(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
MyText.bodyMedium(log.name, fontWeight: 600),
SizedBox(height: 2),
MyText.bodySmall(log.role, color: Colors.grey),
],
),
),
DataCell(
Column(
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!)
: '',
fontWeight: 600,
),
],
),
),
DataCell(
Column(
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!)
: '',
fontWeight: 600,
),
],
),
),
DataCell(
Row(
children: [
// Approve Button
ElevatedButton(
onPressed: () async {
if (attendanceController.selectedProjectId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Please select a project first")),
);
return;
}
final success = await attendanceController
.captureAndUploadAttendance(
log.id,
log.employeeId,
int.parse(attendanceController.selectedProjectId!),
comment: "Accepted",
action: 4, // Approve action
imageCapture: false,
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success
? 'Approval marked successfully!'
: 'Failed to mark approval.')),
);
if (success) {
attendanceController.fetchEmployeesByProject(
attendanceController.selectedProjectId!);
attendanceController.fetchAttendanceLogs(
attendanceController.selectedProjectId!);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AttendanceActionColors
.colors[ButtonActions.approve],
padding: const EdgeInsets.symmetric(
vertical: 4, horizontal: 6),
minimumSize: const Size(60, 20),
textStyle: const TextStyle(fontSize: 12),
),
child: const Text("Approve"),
),
// Space between buttons
const SizedBox(width: 8),
// Reject Button
ElevatedButton(
onPressed: () async {
if (attendanceController.selectedProjectId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Please select a project first")),
);
return;
}
final success = await attendanceController
.captureAndUploadAttendance(
log.id,
log.employeeId,
int.parse(attendanceController.selectedProjectId!),
comment: "Rejected",
action: 5, // Reject action
imageCapture: false,
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success
? 'Attendance marked as Rejected!'
: 'Failed to mark attendance.')),
);
if (success) {
attendanceController.fetchEmployeesByProject(
attendanceController.selectedProjectId!);
attendanceController.fetchAttendanceLogs(
attendanceController.selectedProjectId!);
}
},
style: ElevatedButton.styleFrom(
backgroundColor:
AttendanceActionColors.colors[ButtonActions.reject],
padding: const EdgeInsets.symmetric(
vertical: 4, horizontal: 6),
minimumSize: const Size(60, 20),
textStyle: const TextStyle(fontSize: 12),
),
child: const Text("Reject"),
),
],
),
),
]))
.toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -491,67 +778,11 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
else
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
sortAscending: true,
columnSpacing: 15,
headingRowColor:
WidgetStatePropertyAll(contentTheme.primary.withAlpha(40)),
dataRowMaxHeight: 60,
showBottomBorder: true,
clipBehavior: Clip.antiAliasWithSaveLayer,
border: TableBorder.all(
borderRadius: BorderRadius.circular(4),
style: BorderStyle.solid,
width: 0.4,
color: Colors.grey,
),
columns: [
DataColumn(
label: MyText.labelLarge('Name',
color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Role',
color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Check-In',
color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Check-Out',
color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Action',
color: contentTheme.primary)),
],
rows: attendanceController.regularizationLogs
.mapIndexed((index, log) => DataRow(cells: [
DataCell(
MyText.bodyMedium(log.name, fontWeight: 600)),
DataCell(
MyText.bodyMedium(log.role, fontWeight: 600)),
DataCell(MyText.bodyMedium(
log.checkIn != null
? DateFormat('dd MMM yyyy hh:mm a')
.format(log.checkIn!)
: '-',
fontWeight: 600,
)),
DataCell(MyText.bodyMedium(
log.checkOut != null
? DateFormat('dd MMM yyyy hh:mm a')
.format(log.checkOut!)
: '-',
fontWeight: 600,
)),
DataCell(IconButton(
icon: Icon(Icons.info_outline,
color: contentTheme.primary),
onPressed: () {
// Add action logic
},
)),
]))
.toList(),
child: MyPaginatedTable(
// Use MyPaginatedTable here for pagination
columns: columns,
rows: rows,
columnSpacing: 15.0,
),
),
),

View File

@ -47,10 +47,10 @@ class DashboardScreen extends StatelessWidget with UIMixin {
List<Widget> _buildDashboardStats() {
final stats = [
_StatItem(LucideIcons.layout_dashboard, "Dashboard", contentTheme.primary),
_StatItem(LucideIcons.gauge, "Dashboard", contentTheme.primary),
_StatItem(LucideIcons.folder, "Projects", contentTheme.secondary),
_StatItem(LucideIcons.check, "Attendance", contentTheme.success),
_StatItem(LucideIcons.users, "Task", contentTheme.info),
_StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success),
_StatItem(LucideIcons.logs, "Task", contentTheme.info),
];
return List.generate(