Add attendance log and regularization log models; enhance attendance screen with date range selection and regularization tab

This commit is contained in:
Vaibhav Surve 2025-04-24 16:49:33 +05:30
parent fd14243f5a
commit 7936072b74
6 changed files with 496 additions and 259 deletions

View File

@ -1,85 +1,102 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/attendance_model.dart';
import 'package:marco/model/project_model.dart'; // Assuming you have a ProjectModel for the projects.
import 'package:marco/model/employee_model.dart'; // Assuming you have an EmployeeModel for the employees.
import 'package:marco/model/AttendanceLogModel.dart';
import 'package:flutter/material.dart';
import 'package:marco/model/project_model.dart';
import 'package:marco/model/employee_model.dart';
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';
class AttendanceController extends GetxController {
List<AttendanceModel> attendances = [];
List<ProjectModel> projects = []; // List of projects
String? selectedProjectId; // Currently selected project ID
List<EmployeeModel> employees = []; // Employees of the selected project
List<ProjectModel> projects = [];
String? selectedProjectId;
List<EmployeeModel> employees = [];
DateTime? startDateAttendance;
DateTime? endDateAttendance;
List<AttendanceLogModel> attendanceLogs = [];
List<RegularizationLogModel> regularizationLogs = [];
List<AttendanceLogViewModel> attendenceLogsView = [];
@override
void onInit() {
super.onInit();
fetchProjects(); // Fetch projects when initializing
// fetchAttendanceLogs(selectedProjectId);
// fetchAttendanceLogs(selectedProjectId);
_initializeDefaults();
}
void _initializeDefaults() {
_setDefaultDateRange();
fetchProjects();
}
void _setDefaultDateRange() {
final today = DateTime.now();
startDateAttendance = today.subtract(const Duration(days: 7));
endDateAttendance = today;
}
// Fetch projects from API
Future<void> fetchProjects() async {
var response = await ApiService.getProjects(); // Call the project API
final response = await ApiService.getProjects();
if (response != null) {
projects = response
.map<ProjectModel>((json) => ProjectModel.fromJson(json))
.toList();
// Set default to the first project if available
if (projects.isNotEmpty) {
selectedProjectId = projects.first.id.toString();
await fetchEmployeesByProject(
selectedProjectId); // Fetch employees for the first project
}
update([
'attendance_dashboard_controller'
]); // Notify GetBuilder with your tag
if (response != null && response.isNotEmpty) {
projects = response.map((json) => ProjectModel.fromJson(json)).toList();
selectedProjectId = projects.first.id.toString();
await _fetchProjectData(selectedProjectId);
update(['attendance_dashboard_controller']);
} else {
print("No projects data found or failed to fetch data.");
}
}
// Fetch employees by project ID
Future<void> _fetchProjectData(String? projectId) async {
if (projectId == null) return;
await Future.wait([
fetchEmployeesByProject(projectId),
fetchAttendanceLogs(projectId,
dateFrom: startDateAttendance, dateTo: endDateAttendance),
fetchRegularizationLogs(projectId),
]);
}
Future<void> fetchEmployeesByProject(String? projectId) async {
if (projectId == null) return;
var response = await ApiService.getEmployeesByProject(int.parse(projectId));
final response =
await ApiService.getEmployeesByProject(int.parse(projectId));
if (response != null) {
employees = response
.map<EmployeeModel>((json) => EmployeeModel.fromJson(json))
.toList();
update(); // Trigger UI rebuild
employees = response.map((json) => EmployeeModel.fromJson(json)).toList();
update();
} else {
print("Failed to fetch employees for project $projectId.");
}
}
Future<bool> captureAndUploadAttendance(int employeeId, int projectId,
{String comment = "Marked via mobile app"}) async {
Future<bool> captureAndUploadAttendance(
int employeeId,
int projectId, {
String comment = "Marked via mobile app",
}) async {
try {
final XFile? image = await ImagePicker().pickImage(
final image = await ImagePicker().pickImage(
source: ImageSource.camera,
imageQuality: 80,
);
if (image == null) return false;
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
String imageName = ApiService.generateImageName(
employeeId,
employees.length + 1,
desiredAccuracy: LocationAccuracy.high,
);
final imageName =
ApiService.generateImageName(employeeId, employees.length + 1);
return await ApiService.uploadAttendanceImage(
employeeId,
image,
@ -90,57 +107,94 @@ class AttendanceController extends GetxController {
comment: comment,
);
} catch (e) {
print("Error capturing or uploading: $e");
print("Error capturing or uploading attendance: $e");
return false;
}
}
DateTime? startDate;
DateTime? endDate;
Future<void> selectDateRange(BuildContext context, AttendanceController controller) async {
final DateTimeRange? picked = await showDateRangePicker(
context: context,
firstDate: DateTime(2022),
lastDate: DateTime.now(),
initialDateRange: DateTimeRange(
start: startDate ?? DateTime.now().subtract(Duration(days: 7)),
end: endDate ?? DateTime.now(),
),
);
if (picked != null) {
startDate = picked.start;
endDate = picked.end;
await controller.fetchAttendanceLogs(
controller.selectedProjectId,
dateFrom: startDate,
dateTo: endDate,
Future<void> selectDateRangeForAttendance(
BuildContext context,
AttendanceController controller,
) async {
final picked = await showDateRangePicker(
context: context,
firstDate: DateTime(2022),
lastDate: DateTime.now(),
initialDateRange: DateTimeRange(
start: startDateAttendance ??
DateTime.now().subtract(const Duration(days: 7)),
end: endDateAttendance ?? DateTime.now(),
),
);
if (picked != null) {
startDateAttendance = picked.start;
endDateAttendance = picked.end;
await controller.fetchAttendanceLogs(
controller.selectedProjectId,
dateFrom: picked.start,
dateTo: picked.end,
);
}
}
Future<void> fetchAttendanceLogs(
String? projectId, {
DateTime? dateFrom,
DateTime? dateTo,
}) async {
if (projectId == null) return;
final response = await ApiService.getAttendanceLogs(
int.parse(projectId),
dateFrom: dateFrom,
dateTo: dateTo,
);
if (response != null) {
attendanceLogs =
response.map((json) => AttendanceLogModel.fromJson(json)).toList();
print("Attendance logs fetched: ${response}");
update();
} else {
print("Failed to fetch attendance logs for project $projectId.");
}
}
Future<void> fetchRegularizationLogs(
String? projectId, {
DateTime? dateFrom,
DateTime? dateTo,
}) async {
if (projectId == null) return;
final response =
await ApiService.getRegularizationLogs(int.parse(projectId));
if (response != null) {
regularizationLogs = response
.map((json) => RegularizationLogModel.fromJson(json))
.toList();
update();
} else {
print("Failed to fetch regularization logs for project $projectId.");
}
}
Future<void> fetchLogsView(String? id) async {
if (id == null) return;
final response =
await ApiService.getAttendanceLogView(int.parse(id));
if (response != null) {
attendenceLogsView = response
.map((json) => AttendanceLogViewModel.fromJson(json))
.toList();
update();
} else {
print("Failed to fetch regularization logs for project $id.");
}
}
}
List<AttendanceLogModel> attendanceLogs = [];
Future<void> fetchAttendanceLogs(String? projectId,
{DateTime? dateFrom, DateTime? dateTo}) async {
if (projectId == null) return;
var response = await ApiService.getAttendanceLogs(
int.parse(projectId),
dateFrom: dateFrom,
dateTo: dateTo,
);
if (response != null) {
attendanceLogs = response
.map<AttendanceLogModel>((json) => AttendanceLogModel.fromJson(json))
.toList();
update();
} else {
print("Failed to fetch logs for project $projectId.");
}
}
}

View File

@ -4,147 +4,69 @@ import 'package:image_picker/image_picker.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:intl/intl.dart';
class ApiService {
static const String baseUrl = "https://api.marcoaiot.com/api";
// Fetch the list of projects
static Future<List<dynamic>?> getProjects() async {
try {
String? jwtToken = LocalStorage.getJwtToken();
if (jwtToken == null) {
print("No JWT token found. Please log in.");
return null;
}
// ===== Common Helpers =====
final response = await http.get(
Uri.parse("$baseUrl/project/list"),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $jwtToken', // Add Authorization header
},
);
if (response.statusCode == 200) {
final json = jsonDecode(response.body);
if (json['success'] == true) {
return json['data']; // Return the data if success is true
} else {
print("Error: ${json['message']}");
}
} else {
print("Error fetching projects: ${response.statusCode}");
}
} catch (e) {
print("Exception while fetching projects: $e");
static Future<String?> _getToken() async {
final token = LocalStorage.getJwtToken();
if (token == null) {
print("No JWT token found. Please log in.");
}
return null;
return token;
}
// Fetch employees by project ID
static Future<List<dynamic>?> getEmployeesByProject(int projectId) async {
try {
String? jwtToken = LocalStorage.getJwtToken();
if (jwtToken == null) {
print("No JWT token found. Please log in.");
return null;
}
final response = await http.get(
Uri.parse(
"$baseUrl/attendance/project/team?projectId=$projectId"), // Ensure correct endpoint
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $jwtToken',
},
);
if (response.statusCode == 200) {
final json = jsonDecode(response.body);
if (json['success'] == true) {
return json['data']; // Return employee data
} else {
print("Error: ${json['message']}");
}
} else {
print("Error fetching employees: ${response.statusCode}");
}
} catch (e) {
print("Exception while fetching employees: $e");
}
return null;
}
static String generateImageName(int employeeId, int count) {
final now = DateTime.now();
final formattedDate = "${now.year.toString().padLeft(4, '0')}"
"${now.month.toString().padLeft(2, '0')}"
"${now.day.toString().padLeft(2, '0')}_"
"${now.hour.toString().padLeft(2, '0')}"
"${now.minute.toString().padLeft(2, '0')}"
"${now.second.toString().padLeft(2, '0')}";
final imageNumber = count.toString().padLeft(3, '0');
return "${employeeId}_${formattedDate}_$imageNumber.jpg";
}
static Future<bool> uploadAttendanceImage(
int employeeId, XFile imageFile, double latitude, double longitude,
{required String imageName,
required int projectId,
String comment = "",
int action = 0}) async {
try {
String? jwtToken = LocalStorage.getJwtToken();
if (jwtToken == null) {
print("No JWT token found. Please log in.");
return false;
}
final bytes = await imageFile.readAsBytes();
final base64Image = base64Encode(bytes);
final fileSize = await imageFile.length();
final contentType = "image/${imageFile.path.split('.').last}";
final imageObject = {
"FileName": imageName,
"Base64Data": base64Image,
"ContentType": contentType,
"FileSize": fileSize,
"Description": "Employee attendance photo"
static Map<String, String> _headers(String token) => {
'Content-Type': 'application/json',
'Authorization': 'Bearer $token',
};
final now = DateTime.now();
static Future<http.Response?> _getRequest(String endpoint,
{Map<String, String>? queryParams}) async {
final token = await _getToken();
if (token == null) return null;
// You can now include the attendance record directly in the main body
final response = await http.post(
Uri.parse("$baseUrl/attendance/record"),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $jwtToken',
},
body: jsonEncode({
"ID": null,
"employeeId": employeeId,
"projectId": projectId,
"markTime": DateFormat('hh:mm a').format(now),
"comment": comment,
"action": action,
"date": DateFormat('yyyy-MM-dd').format(now),
"latitude": latitude,
"longitude": longitude,
"image": [imageObject], // Directly included in the body
}),
);
print('uploadAttendanceImage: $baseUrl/attendance/record');
if (response.statusCode == 200) {
final json = jsonDecode(response.body);
return json['success'] == true;
} else {
print("Error uploading image: ${response.statusCode}");
}
final uri =
Uri.parse("$baseUrl$endpoint").replace(queryParameters: queryParams);
print('GET request: $uri');
try {
return await http.get(uri, headers: _headers(token));
} catch (e) {
print("Exception during image upload: $e");
print("HTTP GET Exception: $e");
return null;
}
return false;
}
static dynamic _parseResponse(http.Response response, {String label = ''}) {
print("$label Response: ${response.body}");
if (response.statusCode == 200) {
final json = jsonDecode(response.body);
if (json['success'] == true) return json['data'];
print("API Error: ${json['message']}");
} else {
print("HTTP Error [$label]: ${response.statusCode}");
}
return null;
}
// ===== API Calls =====
static Future<List<dynamic>?> getProjects() async {
final response = await _getRequest("/project/list");
return response != null
? _parseResponse(response, label: 'Projects')
: null;
}
static Future<List<dynamic>?> getEmployeesByProject(int projectId) async {
final response = await _getRequest("/attendance/project/team",
queryParams: {"projectId": "$projectId"});
return response != null
? _parseResponse(response, label: 'Employees')
: null;
}
static Future<List<dynamic>?> getAttendanceLogs(
@ -152,46 +74,101 @@ class ApiService {
DateTime? dateFrom,
DateTime? dateTo,
}) async {
try {
String? jwtToken = LocalStorage.getJwtToken();
if (jwtToken == null) {
print("No JWT token found. Please log in.");
return null;
}
final query = {
"projectId": "$projectId",
if (dateFrom != null)
"dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom),
if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo),
};
final queryParameters = {
"projectId": projectId.toString(),
if (dateFrom != null)
"dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom),
if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo),
final response =
await _getRequest("/attendance/project/team", queryParams: query);
return response != null
? _parseResponse(response, label: 'Attendance Logs')
: null;
}
static Future<List<dynamic>?> getAttendanceLogView(int id) async {
final response = await _getRequest("/attendance/log/attendance/$id");
return response != null
? _parseResponse(response, label: 'Attendance Log Details')
: null;
}
static Future<List<dynamic>?> getRegularizationLogs(int projectId) async {
final response = await _getRequest("/attendance/regularize",
queryParams: {"projectId": "$projectId"});
return response != null
? _parseResponse(response, label: 'Regularization')
: null;
}
// ===== Upload Image =====
static Future<bool> uploadAttendanceImage(
int employeeId,
XFile imageFile,
double latitude,
double longitude, {
required String imageName,
required int projectId,
String comment = "",
int action = 0,
}) async {
final token = await _getToken();
if (token == null) return false;
try {
final bytes = await imageFile.readAsBytes();
final base64Image = base64Encode(bytes);
final fileSize = await imageFile.length();
final contentType = "image/${imageFile.path.split('.').last}";
final imageObject = {
"fileName": '$imageName',
"contentType": '$contentType',
"fileSize": fileSize,
"description": "Employee attendance photo",
"base64Data": '$base64Image',
};
final uri = Uri.parse("$baseUrl/attendance/project/team")
.replace(queryParameters: queryParameters);
print('uri: $uri');
final response = await http.get(
uri,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $jwtToken',
},
final now = DateTime.now();
final body = {
"id": null,
"employeeId": employeeId,
"projectId": projectId,
"markTime": DateFormat('hh:mm a').format(now),
"comment": comment,
"action": action,
"date": DateFormat('yyyy-MM-dd').format(now),
"latitude": '$latitude',
"longitude": '$longitude',
"image": imageObject
};
print("Upload body Image: $body");
final response = await http.post(
Uri.parse("$baseUrl/attendance/record-image"),
headers: _headers(token),
body: jsonEncode(body),
);
if (response.statusCode == 200) {
final json = jsonDecode(response.body);
print("Response body: ${response.body}");
if (json['success'] == true) {
return json['data'];
} else {
print("Error: ${json['message']}");
}
} else {
print("Error fetching logs: ${response.statusCode}");
}
print('Upload request: $baseUrl/attendance/record-image');
final json = jsonDecode(response.body);
print("Upload Image response: ${response.body}");
return response.statusCode == 200 && json['success'] == true;
} catch (e) {
print("Exception while fetching logs: $e");
print("Exception during image upload: $e");
return false;
}
return null;
}
// ===== Utilities =====
static String generateImageName(int employeeId, int count) {
final now = DateTime.now();
final dateStr = DateFormat('yyyyMMdd_HHmmss').format(now);
final imageNumber = count.toString().padLeft(3, '0');
return "${employeeId}_${dateStr}_$imageNumber.jpg";
}
}

View File

@ -4,6 +4,7 @@ class AttendanceLogModel {
final DateTime? checkIn;
final DateTime? checkOut;
final int activity;
final int id;
AttendanceLogModel({
required this.name,
@ -11,6 +12,7 @@ class AttendanceLogModel {
this.checkIn,
this.checkOut,
required this.activity,
required this.id,
});
factory AttendanceLogModel.fromJson(Map<String, dynamic> json) {
@ -20,6 +22,7 @@ class AttendanceLogModel {
checkIn: json['checkInTime'] != null ? DateTime.tryParse(json['checkInTime']) : null,
checkOut: json['checkOutTime'] != null ? DateTime.tryParse(json['checkOutTime']) : null,
activity: json['activity'] ?? 0,
id: json['id'] != null ? json['id'] : null,
);
}
}

View File

@ -0,0 +1,24 @@
import 'package:intl/intl.dart';
class AttendanceLogViewModel {
final DateTime? activityTime;
final String? imageUrl;
final String? description;
AttendanceLogViewModel({
this.activityTime,
this.imageUrl,
this.description,
});
factory AttendanceLogViewModel.fromJson(Map<String, dynamic> json) {
return AttendanceLogViewModel(
activityTime: json['activityTime'] != null ? DateTime.tryParse(json['activityTime']) : null,
imageUrl: json['imageUrl'],
description: json['description'],
);
}
String? get formattedDate => activityTime != null ? DateFormat('yyyy-MM-dd').format(activityTime!) : null;
String? get formattedTime => activityTime != null ? DateFormat('hh:mm a').format(activityTime!) : null;
}

View File

@ -0,0 +1,25 @@
class RegularizationLogModel {
final String name;
final String role;
final DateTime? checkIn;
final DateTime? checkOut;
final int activity;
RegularizationLogModel({
required this.name,
required this.role,
this.checkIn,
this.checkOut,
required this.activity,
});
factory RegularizationLogModel.fromJson(Map<String, dynamic> json) {
return RegularizationLogModel(
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,
activity: json['activity'] ?? 0,
);
}
}

View File

@ -76,6 +76,8 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
.fetchEmployeesByProject(value);
attendanceController
.fetchAttendanceLogs(value);
attendanceController
.fetchAttendanceLogs(value);
});
},
itemBuilder: (BuildContext context) {
@ -134,7 +136,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
children: [
MyFlexItem(
child: DefaultTabController(
length: 2,
length: 3,
child: MyCard.bordered(
borderRadiusAll: 4,
border:
@ -154,6 +156,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
tabs: const [
Tab(text: 'Employee List'),
Tab(text: 'Logs'),
Tab(text: 'Regularization'),
],
),
MySpacing.height(16),
@ -163,6 +166,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
children: [
employeeListTab(),
reportsTab(context),
regularizationTab(context),
],
),
),
@ -235,7 +239,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
SnackBar(
content: Text(
success
? 'Image uploaded successfully!'
? 'Attendence Marked successfully!'
: 'Image upload failed.',
),
),
@ -260,9 +264,9 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
padding: const EdgeInsets.all(8.0),
child: TextButton.icon(
icon: Icon(Icons.date_range),
label: Text("Select Date Range"),
onPressed: () => attendanceController.selectDateRange(context, attendanceController),
label: Text("Select Date Range for Attendance"),
onPressed: () => attendanceController.selectDateRangeForAttendance(
context, attendanceController),
),
),
if (attendanceController.attendanceLogs.isEmpty)
@ -308,6 +312,156 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
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(
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(
ElevatedButton(
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
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(16)),
),
backgroundColor: theme.cardTheme.color,
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),
// Display the log details
if (attendanceController
.attendenceLogsView.isNotEmpty)
...attendanceController
.attendenceLogsView
.map((log) => Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
children: [
MyText.bodyMedium(
"Date: ${log.formattedDate ?? '-'}",
fontWeight: 600,
),
MyText.bodyMedium(
"Time: ${log.formattedTime ?? '-'}",
fontWeight: 600,
),
MyText.bodyMedium(
"Description: ${log.description ?? '-'}",
fontWeight: 600,
),
const Divider(
thickness: 1,
height: 24),
],
)),
Align(
alignment: Alignment.centerRight,
child: ElevatedButton(
onPressed: () =>
Navigator.pop(context),
child: const Text("Close"),
),
)
],
),
);
},
);
},
child: const Text('View'),
),
)
]))
.toList(),
),
),
),
],
);
}
Widget regularizationTab(BuildContext context) {
final attendanceController = Get.find<AttendanceController>();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
),
if (attendanceController.regularizationLogs.isEmpty)
Expanded(
child: Center(
child: MyText.bodySmall("No Regularization 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.regularizationLogs
.mapIndexed((index, log) => DataRow(cells: [
DataCell(
MyText.bodyMedium(log.name, fontWeight: 600)),