feat: Implement organization selection functionality and integrate with employee fetching logic
This commit is contained in:
parent
17c7b9f10d
commit
83a8abbb87
@ -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;
|
||||
|
40
lib/controller/tenant/organization_selection_controller.dart
Normal file
40
lib/controller/tenant/organization_selection_controller.dart
Normal 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";
|
||||
}
|
@ -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(
|
||||
|
107
lib/helpers/widgets/tenant/organization_selector.dart
Normal file
107
lib/helpers/widgets/tenant/organization_selector.dart
Normal 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,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user