feat: Implement organization selection functionality and integrate with employee fetching logic

This commit is contained in:
Vaibhav Surve 2025-09-22 15:54:32 +05:30
parent 17c7b9f10d
commit 83a8abbb87
5 changed files with 247 additions and 54 deletions

View File

@ -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<ProjectController>().selectedProject?.id;
if (projectId != null) {
@ -66,21 +66,26 @@ class EmployeesScreenController extends GetxController {
update(['employee_screen_controller']);
}
Future<void> fetchAllEmployees() async {
Future<void> 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<void> fetchEmployeesByProject(String? projectId) async {
if (projectId == null || projectId.isEmpty) {
logSafe("Project ID is required but was null or empty.",
level: LogLevel.error);
return;
}
Future<void> 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;

View File

@ -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<Organization> organizations = [];
Organization? selectedOrganization;
final isLoadingOrganizations = false.obs;
Future<void> 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";
}

View File

@ -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<List<dynamic>?> getAllEmployeesByProject(
String projectId) async {
static Future<List<dynamic>?> 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<List<dynamic>?> getAllEmployees() async =>
_getRequest(ApiEndpoints.getAllEmployees).then((res) =>
res != null ? _parseResponse(res, label: 'All Employees') : null);
static Future<List<dynamic>?> 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<List<dynamic>?> getRoles() async =>
_getRequest(ApiEndpoints.getRoles).then(

View File

@ -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<void> 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<String> items,
}) {
return PopupMenuButton<String>(
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<String>(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,
);
});
}
}

View File

@ -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<EmployeesScreen> with UIMixin {
Get.find<PermissionController>();
final TextEditingController _searchController = TextEditingController();
final RxList<EmployeeModel> _filteredEmployees = <EmployeeModel>[].obs;
final OrganizationController _organizationController =
Get.put(OrganizationController());
@override
void initState() {
@ -44,13 +48,19 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
Future<void> _initEmployees() async {
final projectId = Get.find<ProjectController>().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<EmployeesScreen> with UIMixin {
Future<void> _refreshEmployees() async {
try {
final projectId = Get.find<ProjectController>().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<EmployeesScreen> 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<ProjectController>().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),
],
),
);