Implement date range selection for attendance logs and refactor attendance screen layout

This commit is contained in:
Vaibhav Surve 2025-04-23 16:18:44 +05:30
parent a5dd5e19fc
commit fd14243f5a
3 changed files with 266 additions and 193 deletions

View File

@ -6,6 +6,7 @@ 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';
class AttendanceController extends GetxController {
List<AttendanceModel> attendances = [];
@ -17,6 +18,8 @@ class AttendanceController extends GetxController {
void onInit() {
super.onInit();
fetchProjects(); // Fetch projects when initializing
// fetchAttendanceLogs(selectedProjectId);
// fetchAttendanceLogs(selectedProjectId);
}
// Fetch projects from API
@ -92,19 +95,52 @@ class AttendanceController extends GetxController {
}
}
List<AttendanceLogModel> attendanceLogs = [];
Future<void> fetchAttendanceLogs(String? projectId) async {
if (projectId == null) return;
DateTime? startDate;
DateTime? endDate;
var response = await ApiService.getAttendanceLogs(int.parse(projectId));
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 (response != null) {
attendanceLogs = response
.map<AttendanceLogModel>((json) => AttendanceLogModel.fromJson(json))
.toList();
update();
} else {
print("Failed to fetch logs for project $projectId.");
}
if (picked != null) {
startDate = picked.start;
endDate = picked.end;
await controller.fetchAttendanceLogs(
controller.selectedProjectId,
dateFrom: startDate,
dateTo: endDate,
);
}
}
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

@ -26,7 +26,6 @@ class ApiService {
if (response.statusCode == 200) {
final json = jsonDecode(response.body);
print("Response body: ${response.body}");
if (json['success'] == true) {
return json['data']; // Return the data if success is true
} else {
@ -34,7 +33,6 @@ class ApiService {
}
} else {
print("Error fetching projects: ${response.statusCode}");
print("Response body: ${response.body}");
}
} catch (e) {
print("Exception while fetching projects: $e");
@ -62,7 +60,6 @@ class ApiService {
if (response.statusCode == 200) {
final json = jsonDecode(response.body);
print("Response body: ${response.body}");
if (json['success'] == true) {
return json['data']; // Return employee data
} else {
@ -70,7 +67,6 @@ class ApiService {
}
} else {
print("Error fetching employees: ${response.statusCode}");
print("Response body: ${response.body}");
}
} catch (e) {
print("Exception while fetching employees: $e");
@ -138,24 +134,12 @@ class ApiService {
"image": [imageObject], // Directly included in the body
}),
);
print('body: ${jsonEncode({
"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('uploadAttendanceImage: $baseUrl/attendance/record');
if (response.statusCode == 200) {
final json = jsonDecode(response.body);
return json['success'] == true;
} else {
print("Error uploading image: ${response.statusCode}");
print("Response: ${response.body}");
}
} catch (e) {
print("Exception during image upload: $e");
@ -163,7 +147,11 @@ class ApiService {
return false;
}
static Future<List<dynamic>?> getAttendanceLogs(int projectId) async {
static Future<List<dynamic>?> getAttendanceLogs(
int projectId, {
DateTime? dateFrom,
DateTime? dateTo,
}) async {
try {
String? jwtToken = LocalStorage.getJwtToken();
if (jwtToken == null) {
@ -171,9 +159,18 @@ class ApiService {
return null;
}
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 uri = Uri.parse("$baseUrl/attendance/project/team")
.replace(queryParameters: queryParameters);
print('uri: $uri');
final response = await http.get(
Uri.parse(
"$baseUrl/attendance/project/team?projectId=$projectId"),
uri,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $jwtToken',
@ -182,8 +179,10 @@ class ApiService {
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']}");
}

View File

@ -4,7 +4,6 @@ import 'package:get/get.dart';
import 'package:marco/helpers/theme/app_theme.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/utils/my_shadow.dart';
import 'package:marco/helpers/utils/utils.dart';
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
import 'package:marco/helpers/widgets/my_card.dart';
@ -60,6 +59,76 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MySpacing.height(flexSpacing),
// Move project selection here
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: MyContainer.bordered(
padding: MySpacing.xy(4, 8),
child: PopupMenuButton<String>(
onSelected: (value) {
setState(() {
attendanceController.selectedProjectId =
value;
attendanceController
.fetchEmployeesByProject(value);
attendanceController
.fetchAttendanceLogs(value);
});
},
itemBuilder: (BuildContext context) {
if (attendanceController.projects.isEmpty) {
return [
PopupMenuItem<String>(
value: '',
child: MyText.bodySmall('No Data',
fontWeight: 600),
)
];
}
return attendanceController.projects
.map((project) {
return PopupMenuItem<String>(
value: project.id.toString(),
height: 32,
child: MyText.bodySmall(
project.name,
color: theme.colorScheme.onSurface,
fontWeight: 600,
),
);
}).toList();
},
color: theme.cardTheme.color,
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
MyText.labelSmall(
attendanceController.selectedProjectId !=
null
? attendanceController.projects
.firstWhereOrNull((proj) =>
proj.id.toString() ==
attendanceController
.selectedProjectId)
?.name ??
'Select a Project'
: 'Select a Project',
color: theme.colorScheme.onSurface,
),
Icon(LucideIcons.chevron_down,
size: 16,
color: theme.colorScheme.onSurface),
],
),
),
),
),
],
),
MySpacing.height(flexSpacing),
MyFlex(
children: [
@ -93,7 +162,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
child: TabBarView(
children: [
employeeListTab(),
reportsTab(),
reportsTab(context),
],
),
),
@ -115,135 +184,10 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
}
Widget employeeListTab() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: MyContainer.bordered(
padding: MySpacing.xy(4, 8),
child: PopupMenuButton<String>(
onSelected: (value) {
setState(() {
attendanceController.selectedProjectId = value;
attendanceController.fetchEmployeesByProject(value);
attendanceController.fetchAttendanceLogs(value);
});
},
itemBuilder: (BuildContext context) {
return attendanceController.projects.map((project) {
return PopupMenuItem<String>(
value: project.id.toString(),
height: 32,
child: MyText.bodySmall(
project.name,
color: theme.colorScheme.onSurface,
fontWeight: 600,
),
);
}).toList();
},
color: theme.cardTheme.color,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.labelSmall(
attendanceController.selectedProjectId != null
? attendanceController.projects
.firstWhereOrNull((proj) =>
proj.id.toString() ==
attendanceController.selectedProjectId)
?.name ??
'Select a Project'
: 'Select a Project',
color: theme.colorScheme.onSurface,
),
Icon(LucideIcons.chevron_down,
size: 16, color: theme.colorScheme.onSurface),
],
),
),
),
),
],
),
MySpacing.height(24),
attendanceController.employees.isEmpty
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
sortAscending: true,
columnSpacing: 15,
onSelectAll: (_) => {},
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('Designation',
color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Actions',
color: contentTheme.primary)),
],
rows: attendanceController.employees
.mapIndexed((index, employee) => DataRow(cells: [
DataCell(MyText.bodyMedium(employee.name,
fontWeight: 600)),
DataCell(MyText.bodyMedium(employee.designation,
fontWeight: 600)),
DataCell(
ElevatedButton(
onPressed: () async {
final success = await attendanceController
.captureAndUploadAttendance(
employee.id,
int.parse(attendanceController
.selectedProjectId ??
"0"),
comment: "Checked in via app",
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
success
? 'Image uploaded successfully!'
: 'Image upload failed.',
),
),
);
},
child: const Text('Check In'),
),
),
]))
.toList(),
),
),
],
);
}
Widget reportsTab() {
if (attendanceController.attendanceLogs.isEmpty) {
attendanceController
.fetchAttendanceLogs(attendanceController.selectedProjectId);
return const Center(child: CircularProgressIndicator());
if (attendanceController.employees.isEmpty) {
return Center(
child: MyText.bodySmall("No Employees Found", fontWeight: 600),
);
}
return SingleChildScrollView(
@ -266,42 +210,136 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
DataColumn(
label: MyText.labelLarge('Name', color: contentTheme.primary)),
DataColumn(
label: MyText.labelLarge('Role', color: contentTheme.primary)),
label: MyText.labelLarge('Designation',
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)),
label: MyText.labelLarge('Actions', 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(IconButton(
icon: Icon(Icons.info_outline, color: contentTheme.primary),
onPressed: () {
// Action logic here
},
)),
rows: attendanceController.employees
.mapIndexed((index, employee) => DataRow(cells: [
DataCell(MyText.bodyMedium(employee.name, fontWeight: 600)),
DataCell(
MyText.bodyMedium(employee.designation, fontWeight: 600)),
DataCell(
ElevatedButton(
onPressed: () async {
final success = await attendanceController
.captureAndUploadAttendance(
employee.id,
int.parse(
attendanceController.selectedProjectId ?? "0"),
comment: "Checked in via app",
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
success
? 'Image uploaded successfully!'
: 'Image upload failed.',
),
),
);
},
child: const Text('Check In'),
),
),
]))
.toList(),
),
);
}
Widget reportsTab(BuildContext context) {
final attendanceController = Get.find<AttendanceController>();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextButton.icon(
icon: Icon(Icons.date_range),
label: Text("Select Date Range"),
onPressed: () => attendanceController.selectDateRange(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(
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(),
),
),
),
],
);
}
}