From fd14243f5a74c40638b8c32047832a7436fb640b Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Wed, 23 Apr 2025 16:18:44 +0530 Subject: [PATCH] Implement date range selection for attendance logs and refactor attendance screen layout --- .../attendance_screen_controller.dart | 60 ++- lib/helpers/services/api_service.dart | 37 +- lib/view/dashboard/attendanceScreen.dart | 362 ++++++++++-------- 3 files changed, 266 insertions(+), 193 deletions(-) diff --git a/lib/controller/dashboard/attendance_screen_controller.dart b/lib/controller/dashboard/attendance_screen_controller.dart index 57187bd..f06a1ad 100644 --- a/lib/controller/dashboard/attendance_screen_controller.dart +++ b/lib/controller/dashboard/attendance_screen_controller.dart @@ -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 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 attendanceLogs = []; - Future fetchAttendanceLogs(String? projectId) async { - if (projectId == null) return; +DateTime? startDate; +DateTime? endDate; - var response = await ApiService.getAttendanceLogs(int.parse(projectId)); +Future 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((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 attendanceLogs = []; +Future 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((json) => AttendanceLogModel.fromJson(json)) + .toList(); + update(); + } else { + print("Failed to fetch logs for project $projectId."); + } +} + } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 3d27902..c820a2c 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -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?> getAttendanceLogs(int projectId) async { + static Future?> 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']}"); } diff --git a/lib/view/dashboard/attendanceScreen.dart b/lib/view/dashboard/attendanceScreen.dart index 8cc8815..dcd44e7 100644 --- a/lib/view/dashboard/attendanceScreen.dart +++ b/lib/view/dashboard/attendanceScreen.dart @@ -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 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( + onSelected: (value) { + setState(() { + attendanceController.selectedProjectId = + value; + attendanceController + .fetchEmployeesByProject(value); + attendanceController + .fetchAttendanceLogs(value); + }); + }, + itemBuilder: (BuildContext context) { + if (attendanceController.projects.isEmpty) { + return [ + PopupMenuItem( + value: '', + child: MyText.bodySmall('No Data', + fontWeight: 600), + ) + ]; + } + return attendanceController.projects + .map((project) { + return PopupMenuItem( + 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 with UIMixin { child: TabBarView( children: [ employeeListTab(), - reportsTab(), + reportsTab(context), ], ), ), @@ -115,135 +184,10 @@ class _AttendanceScreenState extends State 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( - onSelected: (value) { - setState(() { - attendanceController.selectedProjectId = value; - attendanceController.fetchEmployeesByProject(value); - attendanceController.fetchAttendanceLogs(value); - }); - }, - itemBuilder: (BuildContext context) { - return attendanceController.projects.map((project) { - return PopupMenuItem( - 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 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(); + + 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(), + ), + ), + ), + ], + ); + } }