diff --git a/lib/controller/attendance/attendance_screen_controller.dart b/lib/controller/attendance/attendance_screen_controller.dart index 9d6ecb7..501a94f 100644 --- a/lib/controller/attendance/attendance_screen_controller.dart +++ b/lib/controller/attendance/attendance_screen_controller.dart @@ -15,7 +15,7 @@ import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/model/attendance/attendance_log_model.dart'; import 'package:marco/model/regularization_log_model.dart'; import 'package:marco/model/attendance/attendance_log_view_model.dart'; - +import 'package:marco/model/attendance/organization_per_project_list_model.dart'; import 'package:marco/controller/project_controller.dart'; class AttendanceController extends GetxController { @@ -26,9 +26,13 @@ class AttendanceController extends GetxController { List attendanceLogs = []; List regularizationLogs = []; List attendenceLogsView = []; + // ------------------ Organizations ------------------ + List organizations = []; + Organization? selectedOrganization; + final isLoadingOrganizations = false.obs; // States - String selectedTab = 'Employee List'; +String selectedTab = 'todaysAttendance'; DateTime? startDateAttendance; DateTime? endDateAttendance; @@ -45,11 +49,16 @@ class AttendanceController extends GetxController { void onInit() { super.onInit(); _initializeDefaults(); + + // 🔹 Fetch organizations for the selected project + final projectId = Get.find().selectedProject?.id; + if (projectId != null) { + fetchOrganizations(projectId); + } } void _initializeDefaults() { _setDefaultDateRange(); - fetchProjects(); } void _setDefaultDateRange() { @@ -104,29 +113,15 @@ class AttendanceController extends GetxController { .toList(); } - Future fetchProjects() async { - isLoadingProjects.value = true; - - final response = await ApiService.getProjects(); - if (response != null && response.isNotEmpty) { - projects = response.map((e) => ProjectModel.fromJson(e)).toList(); - logSafe("Projects fetched: ${projects.length}"); - } else { - projects = []; - logSafe("Failed to fetch projects or no projects available.", - level: LogLevel.error); - } - - isLoadingProjects.value = false; - update(['attendance_dashboard_controller']); - } - - Future fetchEmployeesByProject(String? projectId) async { + Future fetchTodaysAttendance(String? projectId) async { if (projectId == null) return; isLoadingEmployees.value = true; - final response = await ApiService.getEmployeesByProject(projectId); + final response = await ApiService.getTodaysAttendance( + projectId, + organizationId: selectedOrganization?.id, + ); if (response != null) { employees = response.map((e) => EmployeeModel.fromJson(e)).toList(); for (var emp in employees) { @@ -141,6 +136,20 @@ class AttendanceController extends GetxController { update(); } + Future fetchOrganizations(String projectId) async { + isLoadingOrganizations.value = true; + final response = await ApiService.getAssignedOrganizations(projectId); + if (response != null) { + organizations = response.data; + logSafe("Organizations fetched: ${organizations.length}"); + } else { + logSafe("Failed to fetch organizations for project $projectId", + level: LogLevel.error); + } + isLoadingOrganizations.value = false; + update(); + } + // ------------------ Attendance Capture ------------------ Future captureAndUploadAttendance( @@ -262,8 +271,12 @@ class AttendanceController extends GetxController { isLoadingAttendanceLogs.value = true; - final response = await ApiService.getAttendanceLogs(projectId, - dateFrom: dateFrom, dateTo: dateTo); + final response = await ApiService.getAttendanceLogs( + projectId, + dateFrom: dateFrom, + dateTo: dateTo, + organizationId: selectedOrganization?.id, + ); if (response != null) { attendanceLogs = response.map((e) => AttendanceLogModel.fromJson(e)).toList(); @@ -306,7 +319,10 @@ class AttendanceController extends GetxController { isLoadingRegularizationLogs.value = true; - final response = await ApiService.getRegularizationLogs(projectId); + final response = await ApiService.getRegularizationLogs( + projectId, + organizationId: selectedOrganization?.id, + ); if (response != null) { regularizationLogs = response.map((e) => RegularizationLogModel.fromJson(e)).toList(); @@ -354,14 +370,28 @@ class AttendanceController extends GetxController { Future fetchProjectData(String? projectId) async { if (projectId == null) return; - await Future.wait([ - fetchEmployeesByProject(projectId), - fetchAttendanceLogs(projectId, - dateFrom: startDateAttendance, dateTo: endDateAttendance), - fetchRegularizationLogs(projectId), - ]); + await fetchOrganizations(projectId); - logSafe("Project data fetched for project ID: $projectId"); + // Call APIs depending on the selected tab only + switch (selectedTab) { + case 'todaysAttendance': + await fetchTodaysAttendance(projectId); + break; + case 'attendanceLogs': + await fetchAttendanceLogs( + projectId, + dateFrom: startDateAttendance, + dateTo: endDateAttendance, + ); + break; + case 'regularizationRequests': + await fetchRegularizationLogs(projectId); + break; + } + + logSafe( + "Project data fetched for project ID: $projectId, tab: $selectedTab"); + update(); } // ------------------ UI Interaction ------------------ diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index eee0216..f5588c1 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -14,7 +14,7 @@ class ApiEndpoints { // Attendance Module API Endpoints static const String getProjects = "/project/list"; static const String getGlobalProjects = "/project/list/basic"; - static const String getEmployeesByProject = "/attendance/project/team"; + static const String getTodaysAttendance = "/attendance/project/team"; static const String getAttendanceLogs = "/attendance/project/log"; static const String getAttendanceLogView = "/attendance/log/attendance"; static const String getRegularizationLogs = "/attendance/regularize"; @@ -90,4 +90,7 @@ class ApiEndpoints { /// Logs Module API Endpoints static const String uploadLogs = "/log"; + + static const String getAssignedOrganizations = + "/project/get/assigned/organization"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index b660075..6c8d98d 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -18,6 +18,7 @@ import 'package:marco/model/document/master_document_tags.dart'; import 'package:marco/model/document/master_document_type_model.dart'; import 'package:marco/model/document/document_details_model.dart'; import 'package:marco/model/document/document_version_model.dart'; +import 'package:marco/model/attendance/organization_per_project_list_model.dart'; class ApiService { static const Duration timeout = Duration(seconds: 30); @@ -247,6 +248,36 @@ class ApiService { } } + /// Get Organizations assigned to a Project + static Future getAssignedOrganizations( + String projectId) async { + final endpoint = "${ApiEndpoints.getAssignedOrganizations}/$projectId"; + logSafe("Fetching organizations assigned to projectId: $projectId"); + + try { + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe("Assigned Organizations request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = + _parseResponseForAllData(response, label: "Assigned Organizations"); + + if (jsonResponse != null) { + return OrganizationListResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getAssignedOrganizations: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + static Future postLogsApi(List> logs) async { const endpoint = "${ApiEndpoints.uploadLogs}"; logSafe("Posting logs... count=${logs.length}"); @@ -1733,23 +1764,49 @@ class ApiService { _getRequest(ApiEndpoints.getGlobalProjects).then((res) => res != null ? _parseResponse(res, label: 'Global Projects') : null); - static Future?> getEmployeesByProject(String projectId) async => - _getRequest(ApiEndpoints.getEmployeesByProject, - queryParams: {"projectId": projectId}) - .then((res) => - res != null ? _parseResponse(res, label: 'Employees') : null); + static Future?> getTodaysAttendance( + String projectId, { + String? organizationId, + }) async { + final query = { + "projectId": projectId, + if (organizationId != null) "organizationId": organizationId, + }; + + return _getRequest(ApiEndpoints.getTodaysAttendance, queryParams: query) + .then((res) => + res != null ? _parseResponse(res, label: 'Employees') : null); + } + + static Future?> getRegularizationLogs( + String projectId, { + String? organizationId, + }) async { + final query = { + "projectId": projectId, + if (organizationId != null) "organizationId": organizationId, + }; + + return _getRequest(ApiEndpoints.getRegularizationLogs, queryParams: query) + .then((res) => res != null + ? _parseResponse(res, label: 'Regularization Logs') + : null); + } static Future?> getAttendanceLogs( String projectId, { DateTime? dateFrom, DateTime? dateTo, + String? organizationId, }) async { final query = { "projectId": projectId, if (dateFrom != null) "dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom), if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo), + if (organizationId != null) "organizationId": organizationId, }; + return _getRequest(ApiEndpoints.getAttendanceLogs, queryParams: query).then( (res) => res != null ? _parseResponse(res, label: 'Attendance Logs') : null); @@ -1759,13 +1816,6 @@ class ApiService { _getRequest("${ApiEndpoints.getAttendanceLogView}/$id").then((res) => res != null ? _parseResponse(res, label: 'Log Details') : null); - static Future?> getRegularizationLogs(String projectId) async => - _getRequest(ApiEndpoints.getRegularizationLogs, - queryParams: {"projectId": projectId}) - .then((res) => res != null - ? _parseResponse(res, label: 'Regularization Logs') - : null); - static Future uploadAttendanceImage( String id, String employeeId, diff --git a/lib/model/attendance/attendence_action_button.dart b/lib/model/attendance/attendence_action_button.dart index 40c398e..7fc49b1 100644 --- a/lib/model/attendance/attendence_action_button.dart +++ b/lib/model/attendance/attendence_action_button.dart @@ -193,7 +193,7 @@ class _AttendanceActionButtonState extends State { controller.uploadingStates[uniqueLogKey]?.value = false; if (success) { - await controller.fetchEmployeesByProject(selectedProjectId); + await controller.fetchTodaysAttendance(selectedProjectId); await controller.fetchAttendanceLogs(selectedProjectId); await controller.fetchRegularizationLogs(selectedProjectId); await controller.fetchProjectData(selectedProjectId); diff --git a/lib/model/attendance/attendence_filter_sheet.dart b/lib/model/attendance/attendence_filter_sheet.dart index bbf7b2b..24c5360 100644 --- a/lib/model/attendance/attendence_filter_sheet.dart +++ b/lib/model/attendance/attendence_filter_sheet.dart @@ -5,6 +5,7 @@ import 'package:marco/controller/attendance/attendance_screen_controller.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; +import 'package:get/get.dart'; class AttendanceFilterBottomSheet extends StatefulWidget { final AttendanceController controller; @@ -44,6 +45,59 @@ class _AttendanceFilterBottomSheetState return "Date Range"; } + Widget _popupSelector({ + required String currentValue, + required List items, + required ValueChanged onSelected, + }) { + return PopupMenuButton( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + onSelected: onSelected, + itemBuilder: (context) => items + .map((e) => PopupMenuItem( + value: e, + child: MyText(e), + )) + .toList(), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: MyText( + currentValue, + style: const TextStyle(color: Colors.black87), + overflow: TextOverflow.ellipsis, + ), + ), + const Icon(Icons.arrow_drop_down, color: Colors.grey), + ], + ), + ), + ); + } + + Widget _buildOrganizationSelector(BuildContext context) { + return _popupSelector( + currentValue: + widget.controller.selectedOrganization?.name ?? "Select Organization", + items: widget.controller.organizations.map((e) => e.name).toList(), + onSelected: (name) { + final selectedOrg = widget.controller.organizations + .firstWhere((org) => org.name == name); + setState(() { + widget.controller.selectedOrganization = selectedOrg; + }); + }, + ); + } + List buildMainFilters() { final hasRegularizationPermission = widget.permissionController .hasPermission(Permissions.regularizeAttendance); @@ -61,7 +115,7 @@ class _AttendanceFilterBottomSheetState final List widgets = [ Padding( - padding: EdgeInsets.only(bottom: 4), + padding: const EdgeInsets.only(bottom: 4), child: Align( alignment: Alignment.centerLeft, child: MyText.titleSmall("View", fontWeight: 600), @@ -82,11 +136,41 @@ class _AttendanceFilterBottomSheetState }), ]; + // 🔹 Organization filter + widgets.addAll([ + const Divider(), + Padding( + padding: const EdgeInsets.only(top: 12, bottom: 12), + child: Align( + alignment: Alignment.centerLeft, + child: MyText.titleSmall("Select Organization", fontWeight: 600), + ), + ), + Obx(() { + if (widget.controller.isLoadingOrganizations.value) { + return const Center(child: CircularProgressIndicator()); + } else if (widget.controller.organizations.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: MyText.bodyMedium( + "No organizations found", + fontWeight: 500, + color: Colors.grey, + ), + ), + ); + } + return _buildOrganizationSelector(context); + }), + ]); + + // 🔹 Date Range only for attendanceLogs if (tempSelectedTab == 'attendanceLogs') { widgets.addAll([ const Divider(), Padding( - padding: EdgeInsets.only(top: 12, bottom: 4), + padding: const EdgeInsets.only(top: 12, bottom: 4), child: Align( alignment: Alignment.centerLeft, child: MyText.titleSmall("Date Range", fontWeight: 600), @@ -99,7 +183,7 @@ class _AttendanceFilterBottomSheetState context, widget.controller, ); - setState(() {}); // rebuild UI after date range is updated + setState(() {}); }, child: Ink( decoration: BoxDecoration( @@ -139,6 +223,7 @@ class _AttendanceFilterBottomSheetState onCancel: () => Navigator.pop(context), onSubmit: () => Navigator.pop(context, { 'selectedTab': tempSelectedTab, + 'selectedOrganization': widget.controller.selectedOrganization?.id, }), child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/model/attendance/organization_per_project_list_model.dart b/lib/model/attendance/organization_per_project_list_model.dart new file mode 100644 index 0000000..8149191 --- /dev/null +++ b/lib/model/attendance/organization_per_project_list_model.dart @@ -0,0 +1,106 @@ +class OrganizationListResponse { + final bool success; + final String message; + final List data; + final dynamic errors; + final int statusCode; + final String timestamp; + + OrganizationListResponse({ + required this.success, + required this.message, + required this.data, + this.errors, + required this.statusCode, + required this.timestamp, + }); + + factory OrganizationListResponse.fromJson(Map json) { + return OrganizationListResponse( + success: json['success'] ?? false, + message: json['message'] ?? '', + data: (json['data'] as List?) + ?.map((e) => Organization.fromJson(e)) + .toList() ?? + [], + errors: json['errors'], + statusCode: json['statusCode'] ?? 0, + timestamp: json['timestamp'] ?? '', + ); + } + + Map toJson() { + return { + 'success': success, + 'message': message, + 'data': data.map((e) => e.toJson()).toList(), + 'errors': errors, + 'statusCode': statusCode, + 'timestamp': timestamp, + }; + } +} + +class Organization { + final String id; + final String name; + final String email; + final String contactPerson; + final String address; + final String contactNumber; + final int sprid; + final String createdAt; + final dynamic createdBy; + final dynamic updatedBy; + final dynamic updatedAt; + final bool isActive; + + Organization({ + required this.id, + required this.name, + required this.email, + required this.contactPerson, + required this.address, + required this.contactNumber, + required this.sprid, + required this.createdAt, + this.createdBy, + this.updatedBy, + this.updatedAt, + required this.isActive, + }); + + factory Organization.fromJson(Map json) { + return Organization( + id: json['id'] ?? '', + name: json['name'] ?? '', + email: json['email'] ?? '', + contactPerson: json['contactPerson'] ?? '', + address: json['address'] ?? '', + contactNumber: json['contactNumber'] ?? '', + sprid: json['sprid'] ?? 0, + createdAt: json['createdAt'] ?? '', + createdBy: json['createdBy'], + updatedBy: json['updatedBy'], + updatedAt: json['updatedAt'], + isActive: json['isActive'] ?? false, + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + 'email': email, + 'contactPerson': contactPerson, + 'address': address, + 'contactNumber': contactNumber, + 'sprid': sprid, + 'createdAt': createdAt, + 'createdBy': createdBy, + 'updatedBy': updatedBy, + 'updatedAt': updatedAt, + 'isActive': isActive, + }; + } +} diff --git a/lib/view/Attendence/attendance_screen.dart b/lib/view/Attendence/attendance_screen.dart index c79b20c..32d3d8d 100644 --- a/lib/view/Attendence/attendance_screen.dart +++ b/lib/view/Attendence/attendance_screen.dart @@ -47,6 +47,7 @@ class _AttendanceScreenState extends State with UIMixin { Future _loadData(String projectId) async { try { + attendanceController.selectedTab = 'todaysAttendance'; await attendanceController.loadAttendanceData(projectId); attendanceController.update(['attendance_dashboard_controller']); } catch (e) { @@ -56,7 +57,24 @@ class _AttendanceScreenState extends State with UIMixin { Future _refreshData() async { final projectId = projectController.selectedProjectId.value; - if (projectId.isNotEmpty) await _loadData(projectId); + if (projectId.isEmpty) return; + + // Call only the relevant API for current tab + switch (selectedTab) { + case 'todaysAttendance': + await attendanceController.fetchTodaysAttendance(projectId); + break; + case 'attendanceLogs': + await attendanceController.fetchAttendanceLogs( + projectId, + dateFrom: attendanceController.startDateAttendance, + dateTo: attendanceController.endDateAttendance, + ); + break; + case 'regularizationRequests': + await attendanceController.fetchRegularizationLogs(projectId); + break; + } } Widget _buildAppBar() { @@ -195,15 +213,26 @@ class _AttendanceScreenState extends State with UIMixin { final selectedProjectId = projectController.selectedProjectId.value; final selectedView = result['selectedTab'] as String?; + final selectedOrgId = + result['selectedOrganization'] as String?; + + if (selectedOrgId != null) { + attendanceController.selectedOrganization = + attendanceController.organizations + .firstWhere((o) => o.id == selectedOrgId); + } if (selectedProjectId.isNotEmpty) { try { - await attendanceController - .fetchEmployeesByProject(selectedProjectId); - await attendanceController - .fetchAttendanceLogs(selectedProjectId); - await attendanceController - .fetchRegularizationLogs(selectedProjectId); + await attendanceController.fetchTodaysAttendance( + selectedProjectId, + ); + await attendanceController.fetchAttendanceLogs( + selectedProjectId, + ); + await attendanceController.fetchRegularizationLogs( + selectedProjectId, + ); await attendanceController .fetchProjectData(selectedProjectId); } catch (_) {} @@ -214,6 +243,11 @@ class _AttendanceScreenState extends State with UIMixin { if (selectedView != null && selectedView != selectedTab) { setState(() => selectedTab = selectedView); + attendanceController.selectedTab = selectedView; + if (selectedProjectId.isNotEmpty) { + await attendanceController + .fetchProjectData(selectedProjectId); + } } } }, diff --git a/lib/view/expense/expense_filter_bottom_sheet.dart b/lib/view/expense/expense_filter_bottom_sheet.dart index 97c9c2c..31b0306 100644 --- a/lib/view/expense/expense_filter_bottom_sheet.dart +++ b/lib/view/expense/expense_filter_bottom_sheet.dart @@ -408,4 +408,5 @@ class ExpenseFilterBottomSheet extends StatelessWidget { ], ); } + }