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
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
fetchAllProjects().then((_) {
|
fetchAllProjects().then((_) {
|
||||||
final projectId = Get.find<ProjectController>().selectedProject?.id;
|
final projectId = Get.find<ProjectController>().selectedProject?.id;
|
||||||
if (projectId != null) {
|
if (projectId != null) {
|
||||||
@ -66,21 +66,26 @@ class EmployeesScreenController extends GetxController {
|
|||||||
update(['employee_screen_controller']);
|
update(['employee_screen_controller']);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchAllEmployees() async {
|
Future<void> fetchAllEmployees({String? organizationId}) async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
update(['employee_screen_controller']);
|
update(['employee_screen_controller']);
|
||||||
|
|
||||||
await _handleApiCall(
|
await _handleApiCall(
|
||||||
ApiService.getAllEmployees,
|
() => ApiService.getAllEmployees(
|
||||||
|
organizationId: organizationId), // pass orgId to API
|
||||||
onSuccess: (data) {
|
onSuccess: (data) {
|
||||||
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
|
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
|
||||||
logSafe("All Employees fetched: ${employees.length} employees loaded.",
|
logSafe(
|
||||||
level: LogLevel.info);
|
"All Employees fetched: ${employees.length} employees loaded.",
|
||||||
|
level: LogLevel.info,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onEmpty: () {
|
onEmpty: () {
|
||||||
employees.clear();
|
employees.clear();
|
||||||
logSafe("No Employee data found or API call failed.",
|
logSafe(
|
||||||
level: LogLevel.warning);
|
"No Employee data found or API call failed",
|
||||||
|
level: LogLevel.warning,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -88,43 +93,22 @@ class EmployeesScreenController extends GetxController {
|
|||||||
update(['employee_screen_controller']);
|
update(['employee_screen_controller']);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchEmployeesByProject(String? projectId) async {
|
Future<void> fetchEmployeesByProject(String projectId,
|
||||||
if (projectId == null || projectId.isEmpty) {
|
{String? organizationId}) async {
|
||||||
logSafe("Project ID is required but was null or empty.",
|
if (projectId.isEmpty) return;
|
||||||
level: LogLevel.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
await _handleApiCall(
|
await _handleApiCall(
|
||||||
() => ApiService.getAllEmployeesByProject(projectId),
|
() => ApiService.getAllEmployeesByProject(projectId,
|
||||||
|
organizationId: organizationId),
|
||||||
onSuccess: (data) {
|
onSuccess: (data) {
|
||||||
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
|
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
|
||||||
|
|
||||||
for (var emp in employees) {
|
for (var emp in employees) {
|
||||||
uploadingStates[emp.id] = false.obs;
|
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;
|
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);
|
logSafe("Headers: ${_headers(token)}", level: LogLevel.debug);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response =
|
final response = await http
|
||||||
await http.get(uri, headers: _headers(token)).timeout(extendedTimeout);
|
.get(uri, headers: _headers(token))
|
||||||
|
.timeout(extendedTimeout);
|
||||||
|
|
||||||
logSafe("Response Status: ${response.statusCode}", level: LogLevel.debug);
|
logSafe("Response Status: ${response.statusCode}", level: LogLevel.debug);
|
||||||
logSafe("Response Body: ${response.body}", 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);
|
logSafe("Sending DELETE request to $uri", level: LogLevel.debug);
|
||||||
|
|
||||||
final response =
|
final response = await http
|
||||||
await http.delete(uri, headers: _headers(token)).timeout(extendedTimeout);
|
.delete(uri, headers: _headers(token))
|
||||||
|
.timeout(extendedTimeout);
|
||||||
|
|
||||||
logSafe("DELETE expense response status: ${response.statusCode}");
|
logSafe("DELETE expense response status: ${response.statusCode}");
|
||||||
logSafe("DELETE expense response body: ${response.body}");
|
logSafe("DELETE expense response body: ${response.body}");
|
||||||
@ -1311,8 +1313,9 @@ class ApiService {
|
|||||||
|
|
||||||
logSafe("Sending DELETE request to $uri", level: LogLevel.debug);
|
logSafe("Sending DELETE request to $uri", level: LogLevel.debug);
|
||||||
|
|
||||||
final response =
|
final response = await http
|
||||||
await http.delete(uri, headers: _headers(token)).timeout(extendedTimeout);
|
.delete(uri, headers: _headers(token))
|
||||||
|
.timeout(extendedTimeout);
|
||||||
|
|
||||||
logSafe("DELETE bucket response status: ${response.statusCode}");
|
logSafe("DELETE bucket response status: ${response.statusCode}");
|
||||||
logSafe("DELETE bucket response body: ${response.body}");
|
logSafe("DELETE bucket response body: ${response.body}");
|
||||||
@ -1908,11 +1911,15 @@ class ApiService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<dynamic>?> getAllEmployeesByProject(
|
static Future<List<dynamic>?> getAllEmployeesByProject(String projectId,
|
||||||
String projectId) async {
|
{String? organizationId}) async {
|
||||||
if (projectId.isEmpty) throw ArgumentError('projectId must not be empty');
|
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(
|
return _getRequest(endpoint).then(
|
||||||
(res) => res != null
|
(res) => res != null
|
||||||
@ -1921,9 +1928,19 @@ class ApiService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<dynamic>?> getAllEmployees() async =>
|
static Future<List<dynamic>?> getAllEmployees({String? organizationId}) async {
|
||||||
_getRequest(ApiEndpoints.getAllEmployees).then((res) =>
|
var endpoint = ApiEndpoints.getAllEmployees;
|
||||||
res != null ? _parseResponse(res, label: 'All Employees') : null);
|
|
||||||
|
// 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 =>
|
static Future<List<dynamic>?> getRoles() async =>
|
||||||
_getRequest(ApiEndpoints.getRoles).then(
|
_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/utils/permission_constants.dart';
|
||||||
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
|
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
|
||||||
import 'package:marco/view/employees/employee_profile_screen.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 {
|
class EmployeesScreen extends StatefulWidget {
|
||||||
const EmployeesScreen({super.key});
|
const EmployeesScreen({super.key});
|
||||||
@ -31,6 +33,8 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
|
|||||||
Get.find<PermissionController>();
|
Get.find<PermissionController>();
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
final RxList<EmployeeModel> _filteredEmployees = <EmployeeModel>[].obs;
|
final RxList<EmployeeModel> _filteredEmployees = <EmployeeModel>[].obs;
|
||||||
|
final OrganizationController _organizationController =
|
||||||
|
Get.put(OrganizationController());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -44,13 +48,19 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
|
|||||||
|
|
||||||
Future<void> _initEmployees() async {
|
Future<void> _initEmployees() async {
|
||||||
final projectId = Get.find<ProjectController>().selectedProject?.id;
|
final projectId = Get.find<ProjectController>().selectedProject?.id;
|
||||||
|
final orgId = _organizationController.selectedOrganization?.id;
|
||||||
|
|
||||||
|
if (projectId != null) {
|
||||||
|
await _organizationController.fetchOrganizations(projectId);
|
||||||
|
}
|
||||||
|
|
||||||
if (_employeeController.isAllEmployeeSelected.value) {
|
if (_employeeController.isAllEmployeeSelected.value) {
|
||||||
_employeeController.selectedProjectId = null;
|
_employeeController.selectedProjectId = null;
|
||||||
await _employeeController.fetchAllEmployees();
|
await _employeeController.fetchAllEmployees(organizationId: orgId);
|
||||||
} else if (projectId != null) {
|
} else if (projectId != null) {
|
||||||
_employeeController.selectedProjectId = projectId;
|
_employeeController.selectedProjectId = projectId;
|
||||||
await _employeeController.fetchEmployeesByProject(projectId);
|
await _employeeController.fetchEmployeesByProject(projectId,
|
||||||
|
organizationId: orgId);
|
||||||
} else {
|
} else {
|
||||||
_employeeController.clearEmployees();
|
_employeeController.clearEmployees();
|
||||||
}
|
}
|
||||||
@ -61,14 +71,16 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
|
|||||||
Future<void> _refreshEmployees() async {
|
Future<void> _refreshEmployees() async {
|
||||||
try {
|
try {
|
||||||
final projectId = Get.find<ProjectController>().selectedProject?.id;
|
final projectId = Get.find<ProjectController>().selectedProject?.id;
|
||||||
|
final orgId = _organizationController.selectedOrganization?.id;
|
||||||
final allSelected = _employeeController.isAllEmployeeSelected.value;
|
final allSelected = _employeeController.isAllEmployeeSelected.value;
|
||||||
|
|
||||||
_employeeController.selectedProjectId = allSelected ? null : projectId;
|
_employeeController.selectedProjectId = allSelected ? null : projectId;
|
||||||
|
|
||||||
if (allSelected) {
|
if (allSelected) {
|
||||||
await _employeeController.fetchAllEmployees();
|
await _employeeController.fetchAllEmployees(organizationId: orgId);
|
||||||
} else if (projectId != null) {
|
} else if (projectId != null) {
|
||||||
await _employeeController.fetchEmployeesByProject(projectId);
|
await _employeeController.fetchEmployeesByProject(projectId,
|
||||||
|
organizationId: orgId);
|
||||||
} else {
|
} else {
|
||||||
_employeeController.clearEmployees();
|
_employeeController.clearEmployees();
|
||||||
}
|
}
|
||||||
@ -268,11 +280,44 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
|
|||||||
Widget _buildSearchAndActionRow() {
|
Widget _buildSearchAndActionRow() {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: MySpacing.x(flexSpacing),
|
padding: MySpacing.x(flexSpacing),
|
||||||
child: Row(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: _buildSearchField()),
|
// Search Field Row
|
||||||
const SizedBox(width: 8),
|
Row(
|
||||||
_buildPopupMenu(),
|
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