From 83a8abbb87481c9335ded504f0df22a2d34b9fe9 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 22 Sep 2025 15:54:32 +0530 Subject: [PATCH] feat: Implement organization selection functionality and integrate with employee fetching logic --- .../employee/employees_screen_controller.dart | 52 +++------ .../organization_selection_controller.dart | 40 +++++++ lib/helpers/services/api_service.dart | 41 +++++-- .../widgets/tenant/organization_selector.dart | 107 ++++++++++++++++++ lib/view/employees/employees_screen.dart | 61 ++++++++-- 5 files changed, 247 insertions(+), 54 deletions(-) create mode 100644 lib/controller/tenant/organization_selection_controller.dart create mode 100644 lib/helpers/widgets/tenant/organization_selector.dart diff --git a/lib/controller/employee/employees_screen_controller.dart b/lib/controller/employee/employees_screen_controller.dart index 2339796..3841a76 100644 --- a/lib/controller/employee/employees_screen_controller.dart +++ b/lib/controller/employee/employees_screen_controller.dart @@ -24,7 +24,7 @@ class EmployeesScreenController extends GetxController { @override void onInit() { super.onInit(); - isLoading.value = true; + isLoading.value = true; fetchAllProjects().then((_) { final projectId = Get.find().selectedProject?.id; if (projectId != null) { @@ -66,21 +66,26 @@ class EmployeesScreenController extends GetxController { update(['employee_screen_controller']); } - Future fetchAllEmployees() async { + Future fetchAllEmployees({String? organizationId}) async { isLoading.value = true; update(['employee_screen_controller']); await _handleApiCall( - ApiService.getAllEmployees, + () => ApiService.getAllEmployees( + organizationId: organizationId), // pass orgId to API onSuccess: (data) { employees.assignAll(data.map((json) => EmployeeModel.fromJson(json))); - logSafe("All Employees fetched: ${employees.length} employees loaded.", - level: LogLevel.info); + logSafe( + "All Employees fetched: ${employees.length} employees loaded.", + level: LogLevel.info, + ); }, onEmpty: () { employees.clear(); - logSafe("No Employee data found or API call failed.", - level: LogLevel.warning); + logSafe( + "No Employee data found or API call failed", + level: LogLevel.warning, + ); }, ); @@ -88,43 +93,22 @@ class EmployeesScreenController extends GetxController { update(['employee_screen_controller']); } - Future fetchEmployeesByProject(String? projectId) async { - if (projectId == null || projectId.isEmpty) { - logSafe("Project ID is required but was null or empty.", - level: LogLevel.error); - return; - } + Future fetchEmployeesByProject(String projectId, + {String? organizationId}) async { + if (projectId.isEmpty) return; isLoading.value = true; await _handleApiCall( - () => ApiService.getAllEmployeesByProject(projectId), + () => ApiService.getAllEmployeesByProject(projectId, + organizationId: organizationId), onSuccess: (data) { employees.assignAll(data.map((json) => EmployeeModel.fromJson(json))); - for (var emp in employees) { uploadingStates[emp.id] = false.obs; } - - logSafe( - "Employees fetched: ${employees.length} for project $projectId", - level: LogLevel.info, - ); - }, - onEmpty: () { - employees.clear(); - logSafe( - "No employees found for project $projectId.", - level: LogLevel.warning, - ); - }, - onError: (e) { - logSafe( - "Error fetching employees for project $projectId", - level: LogLevel.error, - error: e, - ); }, + onEmpty: () => employees.clear(), ); isLoading.value = false; diff --git a/lib/controller/tenant/organization_selection_controller.dart b/lib/controller/tenant/organization_selection_controller.dart new file mode 100644 index 0000000..5b189d6 --- /dev/null +++ b/lib/controller/tenant/organization_selection_controller.dart @@ -0,0 +1,40 @@ +import 'package:get/get.dart'; +import 'package:marco/helpers/services/app_logger.dart'; +import 'package:marco/helpers/services/api_service.dart'; +import 'package:marco/model/attendance/organization_per_project_list_model.dart'; + +class OrganizationController extends GetxController { + List organizations = []; + Organization? selectedOrganization; + final isLoadingOrganizations = false.obs; + + Future fetchOrganizations(String projectId) async { + try { + 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); + } + } finally { + isLoadingOrganizations.value = false; + update(); + } + } + + void selectOrganization(Organization? org) { + selectedOrganization = org; + update(); + } + + void clearSelection() { + selectedOrganization = null; + update(); + } + + String get currentSelection => + selectedOrganization?.name ?? "All Organizations"; +} diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 0cccd60..5d80efc 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -137,8 +137,9 @@ class ApiService { logSafe("Headers: ${_headers(token)}", level: LogLevel.debug); try { - final response = - await http.get(uri, headers: _headers(token)).timeout(extendedTimeout); + final response = await http + .get(uri, headers: _headers(token)) + .timeout(extendedTimeout); logSafe("Response Status: ${response.statusCode}", level: LogLevel.debug); logSafe("Response Body: ${response.body}", level: LogLevel.debug); @@ -898,8 +899,9 @@ class ApiService { logSafe("Sending DELETE request to $uri", level: LogLevel.debug); - final response = - await http.delete(uri, headers: _headers(token)).timeout(extendedTimeout); + final response = await http + .delete(uri, headers: _headers(token)) + .timeout(extendedTimeout); logSafe("DELETE expense response status: ${response.statusCode}"); logSafe("DELETE expense response body: ${response.body}"); @@ -1311,8 +1313,9 @@ class ApiService { logSafe("Sending DELETE request to $uri", level: LogLevel.debug); - final response = - await http.delete(uri, headers: _headers(token)).timeout(extendedTimeout); + final response = await http + .delete(uri, headers: _headers(token)) + .timeout(extendedTimeout); logSafe("DELETE bucket response status: ${response.statusCode}"); logSafe("DELETE bucket response body: ${response.body}"); @@ -1908,11 +1911,15 @@ class ApiService { return null; } - static Future?> getAllEmployeesByProject( - String projectId) async { + static Future?> getAllEmployeesByProject(String projectId, + {String? organizationId}) async { if (projectId.isEmpty) throw ArgumentError('projectId must not be empty'); - final endpoint = "${ApiEndpoints.getAllEmployeesByProject}/$projectId"; + // Build the endpoint with optional organizationId query + var endpoint = "${ApiEndpoints.getAllEmployeesByProject}/$projectId"; + if (organizationId != null && organizationId.isNotEmpty) { + endpoint += "?organizationId=$organizationId"; + } return _getRequest(endpoint).then( (res) => res != null @@ -1921,9 +1928,19 @@ class ApiService { ); } - static Future?> getAllEmployees() async => - _getRequest(ApiEndpoints.getAllEmployees).then((res) => - res != null ? _parseResponse(res, label: 'All Employees') : null); +static Future?> getAllEmployees({String? organizationId}) async { + var endpoint = ApiEndpoints.getAllEmployees; + + // Add organization filter if provided + if (organizationId != null && organizationId.isNotEmpty) { + endpoint += "?organizationId=$organizationId"; + } + + return _getRequest(endpoint).then( + (res) => res != null ? _parseResponse(res, label: 'All Employees') : null, + ); +} + static Future?> getRoles() async => _getRequest(ApiEndpoints.getRoles).then( diff --git a/lib/helpers/widgets/tenant/organization_selector.dart b/lib/helpers/widgets/tenant/organization_selector.dart new file mode 100644 index 0000000..cd89f90 --- /dev/null +++ b/lib/helpers/widgets/tenant/organization_selector.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/controller/tenant/organization_selection_controller.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/model/attendance/organization_per_project_list_model.dart'; + +class OrganizationSelector extends StatelessWidget { + final OrganizationController controller; + + /// Called whenever a new organization is selected (including "All Organizations"). + final Future Function(Organization?)? onSelectionChanged; + + /// Optional height for the selector. If null, uses default padding-based height. + final double? height; + + const OrganizationSelector({ + super.key, + required this.controller, + this.onSelectionChanged, + this.height, + }); + + Widget _popupSelector({ + required String currentValue, + required List items, + }) { + return PopupMenuButton( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + onSelected: (name) async { + // Determine the selected organization + Organization? org = name == "All Organizations" + ? null + : controller.organizations.firstWhere((e) => e.name == name); + + // Update controller state + controller.selectOrganization(org); + + // Trigger callback for post-selection logic + if (onSelectionChanged != null) { + await onSelectionChanged!(org); + } + }, + itemBuilder: (context) => items + .map((e) => PopupMenuItem(value: e, child: MyText(e))) + .toList(), + child: Container( + height: height, + padding: EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(12), + ), + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Text( + currentValue, + style: const TextStyle( + color: Colors.black87, + fontSize: 13, + height: 1.2, + ), + overflow: TextOverflow.ellipsis, + ), + ), + const Icon(Icons.arrow_drop_down, color: Colors.grey, size: 18), + ], + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Obx(() { + if (controller.isLoadingOrganizations.value) { + return const Center(child: CircularProgressIndicator()); + } else if (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, + ), + ), + ); + } + + final orgNames = [ + "All Organizations", + ...controller.organizations.map((e) => e.name) + ]; + + return _popupSelector( + currentValue: controller.currentSelection, + items: orgNames, + ); + }); + } +} diff --git a/lib/view/employees/employees_screen.dart b/lib/view/employees/employees_screen.dart index 2e30d03..6b6435e 100644 --- a/lib/view/employees/employees_screen.dart +++ b/lib/view/employees/employees_screen.dart @@ -16,6 +16,8 @@ import 'package:marco/controller/permission_controller.dart'; import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; import 'package:marco/view/employees/employee_profile_screen.dart'; +import 'package:marco/controller/tenant/organization_selection_controller.dart'; +import 'package:marco/helpers/widgets/tenant/organization_selector.dart'; class EmployeesScreen extends StatefulWidget { const EmployeesScreen({super.key}); @@ -31,6 +33,8 @@ class _EmployeesScreenState extends State with UIMixin { Get.find(); final TextEditingController _searchController = TextEditingController(); final RxList _filteredEmployees = [].obs; + final OrganizationController _organizationController = + Get.put(OrganizationController()); @override void initState() { @@ -44,13 +48,19 @@ class _EmployeesScreenState extends State with UIMixin { Future _initEmployees() async { final projectId = Get.find().selectedProject?.id; + final orgId = _organizationController.selectedOrganization?.id; + + if (projectId != null) { + await _organizationController.fetchOrganizations(projectId); + } if (_employeeController.isAllEmployeeSelected.value) { _employeeController.selectedProjectId = null; - await _employeeController.fetchAllEmployees(); + await _employeeController.fetchAllEmployees(organizationId: orgId); } else if (projectId != null) { _employeeController.selectedProjectId = projectId; - await _employeeController.fetchEmployeesByProject(projectId); + await _employeeController.fetchEmployeesByProject(projectId, + organizationId: orgId); } else { _employeeController.clearEmployees(); } @@ -61,14 +71,16 @@ class _EmployeesScreenState extends State with UIMixin { Future _refreshEmployees() async { try { final projectId = Get.find().selectedProject?.id; + final orgId = _organizationController.selectedOrganization?.id; final allSelected = _employeeController.isAllEmployeeSelected.value; _employeeController.selectedProjectId = allSelected ? null : projectId; if (allSelected) { - await _employeeController.fetchAllEmployees(); + await _employeeController.fetchAllEmployees(organizationId: orgId); } else if (projectId != null) { - await _employeeController.fetchEmployeesByProject(projectId); + await _employeeController.fetchEmployeesByProject(projectId, + organizationId: orgId); } else { _employeeController.clearEmployees(); } @@ -268,11 +280,44 @@ class _EmployeesScreenState extends State with UIMixin { Widget _buildSearchAndActionRow() { return Padding( padding: MySpacing.x(flexSpacing), - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded(child: _buildSearchField()), - const SizedBox(width: 8), - _buildPopupMenu(), + // Search Field Row + Row( + children: [ + Expanded(child: _buildSearchField()), + const SizedBox(width: 8), + _buildPopupMenu(), + ], + ), + + // Organization Selector Row + Row( + children: [ + Expanded( + child: OrganizationSelector( + controller: _organizationController, + height: 36, + onSelectionChanged: (org) async { + final projectId = + Get.find().selectedProject?.id; + + if (_employeeController.isAllEmployeeSelected.value) { + await _employeeController.fetchAllEmployees( + organizationId: org?.id); + } else if (projectId != null) { + await _employeeController.fetchEmployeesByProject( + projectId, + organizationId: org?.id); + } + _employeeController.update(['employee_screen_controller']); + }, + ), + ), + ], + ), + MySpacing.height(8), ], ), );