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/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/employee_model.dart'; // Assuming you have an EmployeeModel for the employees.
import 'package:marco/model/AttendanceLogModel.dart'; import 'package:marco/model/AttendanceLogModel.dart';
import 'package:flutter/material.dart';
class AttendanceController extends GetxController { class AttendanceController extends GetxController {
List<AttendanceModel> attendances = []; List<AttendanceModel> attendances = [];
@ -17,6 +18,8 @@ class AttendanceController extends GetxController {
void onInit() { void onInit() {
super.onInit(); super.onInit();
fetchProjects(); // Fetch projects when initializing fetchProjects(); // Fetch projects when initializing
// fetchAttendanceLogs(selectedProjectId);
// fetchAttendanceLogs(selectedProjectId);
} }
// Fetch projects from API // Fetch projects from API
@ -92,19 +95,52 @@ class AttendanceController extends GetxController {
} }
} }
List<AttendanceLogModel> attendanceLogs = []; DateTime? startDate;
Future<void> fetchAttendanceLogs(String? projectId) async { DateTime? endDate;
if (projectId == null) return;
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) { if (picked != null) {
attendanceLogs = response startDate = picked.start;
.map<AttendanceLogModel>((json) => AttendanceLogModel.fromJson(json)) endDate = picked.end;
.toList();
update(); await controller.fetchAttendanceLogs(
} else { controller.selectedProjectId,
print("Failed to fetch logs for project $projectId."); 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) { if (response.statusCode == 200) {
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
print("Response body: ${response.body}");
if (json['success'] == true) { if (json['success'] == true) {
return json['data']; // Return the data if success is true return json['data']; // Return the data if success is true
} else { } else {
@ -34,7 +33,6 @@ class ApiService {
} }
} else { } else {
print("Error fetching projects: ${response.statusCode}"); print("Error fetching projects: ${response.statusCode}");
print("Response body: ${response.body}");
} }
} catch (e) { } catch (e) {
print("Exception while fetching projects: $e"); print("Exception while fetching projects: $e");
@ -62,7 +60,6 @@ class ApiService {
if (response.statusCode == 200) { if (response.statusCode == 200) {
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
print("Response body: ${response.body}");
if (json['success'] == true) { if (json['success'] == true) {
return json['data']; // Return employee data return json['data']; // Return employee data
} else { } else {
@ -70,7 +67,6 @@ class ApiService {
} }
} else { } else {
print("Error fetching employees: ${response.statusCode}"); print("Error fetching employees: ${response.statusCode}");
print("Response body: ${response.body}");
} }
} catch (e) { } catch (e) {
print("Exception while fetching employees: $e"); print("Exception while fetching employees: $e");
@ -138,24 +134,12 @@ class ApiService {
"image": [imageObject], // Directly included in the body "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'); print('uploadAttendanceImage: $baseUrl/attendance/record');
if (response.statusCode == 200) { if (response.statusCode == 200) {
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
return json['success'] == true; return json['success'] == true;
} else { } else {
print("Error uploading image: ${response.statusCode}"); print("Error uploading image: ${response.statusCode}");
print("Response: ${response.body}");
} }
} catch (e) { } catch (e) {
print("Exception during image upload: $e"); print("Exception during image upload: $e");
@ -163,7 +147,11 @@ class ApiService {
return false; return false;
} }
static Future<List<dynamic>?> getAttendanceLogs(int projectId) async { static Future<List<dynamic>?> getAttendanceLogs(
int projectId, {
DateTime? dateFrom,
DateTime? dateTo,
}) async {
try { try {
String? jwtToken = LocalStorage.getJwtToken(); String? jwtToken = LocalStorage.getJwtToken();
if (jwtToken == null) { if (jwtToken == null) {
@ -171,9 +159,18 @@ class ApiService {
return null; 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( final response = await http.get(
Uri.parse( uri,
"$baseUrl/attendance/project/team?projectId=$projectId"),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer $jwtToken', 'Authorization': 'Bearer $jwtToken',
@ -182,8 +179,10 @@ class ApiService {
if (response.statusCode == 200) { if (response.statusCode == 200) {
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
print("Response body: ${response.body}");
if (json['success'] == true) { if (json['success'] == true) {
return json['data']; return json['data'];
} else { } else {
print("Error: ${json['message']}"); 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/theme/app_theme.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/utils/my_shadow.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.dart';
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart'; import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
import 'package:marco/helpers/widgets/my_card.dart'; import 'package:marco/helpers/widgets/my_card.dart';
@ -60,6 +59,76 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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), MySpacing.height(flexSpacing),
MyFlex( MyFlex(
children: [ children: [
@ -93,7 +162,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
child: TabBarView( child: TabBarView(
children: [ children: [
employeeListTab(), employeeListTab(),
reportsTab(), reportsTab(context),
], ],
), ),
), ),
@ -115,135 +184,10 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
} }
Widget employeeListTab() { Widget employeeListTab() {
return Column( if (attendanceController.employees.isEmpty) {
crossAxisAlignment: CrossAxisAlignment.start, return Center(
children: [ child: MyText.bodySmall("No Employees Found", fontWeight: 600),
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());
} }
return SingleChildScrollView( return SingleChildScrollView(
@ -266,42 +210,136 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
DataColumn( DataColumn(
label: MyText.labelLarge('Name', color: contentTheme.primary)), label: MyText.labelLarge('Name', color: contentTheme.primary)),
DataColumn( DataColumn(
label: MyText.labelLarge('Role', color: contentTheme.primary)), label: MyText.labelLarge('Designation',
color: contentTheme.primary)),
DataColumn( DataColumn(
label: label: MyText.labelLarge('Actions', color: contentTheme.primary)),
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 rows: attendanceController.employees
.mapIndexed((index, log) => DataRow(cells: [ .mapIndexed((index, employee) => DataRow(cells: [
DataCell(MyText.bodyMedium(log.name, fontWeight: 600)), DataCell(MyText.bodyMedium(employee.name, fontWeight: 600)),
DataCell(MyText.bodyMedium(log.role, fontWeight: 600)), DataCell(
DataCell(MyText.bodyMedium( MyText.bodyMedium(employee.designation, fontWeight: 600)),
log.checkIn != null DataCell(
? DateFormat('dd MMM yyyy hh:mm a').format(log.checkIn!) ElevatedButton(
: '-', onPressed: () async {
fontWeight: 600, final success = await attendanceController
)), .captureAndUploadAttendance(
DataCell(MyText.bodyMedium( employee.id,
log.checkOut != null int.parse(
? DateFormat('dd MMM yyyy hh:mm a') attendanceController.selectedProjectId ?? "0"),
.format(log.checkOut!) comment: "Checked in via app",
: '-', );
fontWeight: 600,
)), ScaffoldMessenger.of(context).showSnackBar(
DataCell(IconButton( SnackBar(
icon: Icon(Icons.info_outline, color: contentTheme.primary), content: Text(
onPressed: () { success
// Action logic here ? 'Image uploaded successfully!'
}, : 'Image upload failed.',
)), ),
),
);
},
child: const Text('Check In'),
),
),
])) ]))
.toList(), .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(),
),
),
),
],
);
}
} }