Files
velocity-pay-flutter/lib/screens/users/controllers/user_controller.dart
2026-06-24 00:31:01 +02:00

325 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:qpay/http/http.dart';
import 'package:qpay/models/api_response.dart';
import 'package:qpay/models/pageable_model.dart';
import 'package:qpay/screens/users/models/managed_user_model.dart';
class UserScreenModel {
List<ManagedUserModel> users = [];
bool isLoading = false;
bool isLoadingMore = false;
String? errorMessage;
int currentPage = 0;
int totalPages = 0;
int totalElements = 0;
String searchQuery = '';
bool selectMode = false;
Set<String> selectedUserIds = {};
/// Active filter values (null = not filtered)
String? filterEmail;
String? filterPhone;
}
class UserController extends ChangeNotifier {
final UserScreenModel model = UserScreenModel();
final Http http = Http();
BuildContext? context;
bool _disposed = false;
UserController(this.context);
/// Creates a controller without a [BuildContext] — useful when a
/// bottom sheet or dialog needs to drive API calls through the
/// controller without participating in the widget tree.
UserController.forSheet() : context = null;
@override
void dispose() {
_disposed = true;
super.dispose();
}
dynamic _unwrap(dynamic response) {
if (response is Map && response.containsKey('body')) {
return response['body'];
}
return response;
}
String buildQueryParameters(Map<String, String?> params) {
final filtered = <String, String>{};
params.forEach((key, value) {
if (value != null && value.isNotEmpty) {
filtered[key] = value;
}
});
if (filtered.isEmpty) return '';
return filtered.entries.map((e) => '${e.key}=${e.value}').join('&');
}
// ────────────────────────────────────────────────────────────────
// 1. LIST USERS (GET /api/users?username=…&email=…&phone=…)
// ────────────────────────────────────────────────────────────────
Future<ApiResponse<PageableModel>> listUsers({
int page = 0,
int size = 20,
String? username,
String? email,
String? phone,
}) async {
if (page == 0) {
model.isLoading = true;
model.users = [];
model.currentPage = 0;
} else {
model.isLoadingMore = true;
}
if (!_disposed) notifyListeners();
try {
final params = <String, String?>{
'page': page.toString(),
'size': size.toString(),
'username': username,
'email': email,
'phone': phone,
};
final raw = await http.get('/users?${buildQueryParameters(params)}');
final response = _unwrap(raw);
final pageableModel = PageableModel.fromJson(
response as Map<String, dynamic>,
);
final newUsers = pageableModel.content
.map((e) => ManagedUserModel.fromJson(e as Map<String, dynamic>))
.toList();
if (page == 0) {
model.users = newUsers;
} else {
model.users.addAll(newUsers);
}
model.currentPage = pageableModel.number;
model.totalPages = pageableModel.totalPages;
model.totalElements = pageableModel.totalElements;
model.isLoading = false;
model.isLoadingMore = false;
if (!_disposed) notifyListeners();
return ApiResponse.success(pageableModel);
} catch (e) {
logger.e(e);
model.errorMessage = e.toString();
if (page == 0) {
model.users = [];
}
model.isLoading = false;
model.isLoadingMore = false;
if (!_disposed) notifyListeners();
return ApiResponse.failure('Failed to load users');
}
}
// ────────────────────────────────────────────────────────────────
// 2. LIST WORKSPACE USERS (GET /api/users/workspace?workspaceId=…)
// ────────────────────────────────────────────────────────────────
Future<ApiResponse<PageableModel>> listWorkspaceUsers({
required String workspaceId,
int page = 0,
int size = 20,
String? username,
String? email,
String? phone,
}) async {
if (page == 0) {
model.isLoading = true;
model.users = [];
model.currentPage = 0;
} else {
model.isLoadingMore = true;
}
if (!_disposed) notifyListeners();
try {
final params = <String, String?>{
'workspaceId': workspaceId,
'page': page.toString(),
'size': size.toString(),
'username': username,
'email': email,
'phone': phone,
};
final raw = await http.get(
'/users/workspace?${buildQueryParameters(params)}',
);
final response = _unwrap(raw);
final pageableModel = PageableModel.fromJson(
response as Map<String, dynamic>,
);
final newUsers = pageableModel.content
.map((e) => ManagedUserModel.fromJson(e as Map<String, dynamic>))
.toList();
if (page == 0) {
model.users = newUsers;
} else {
model.users.addAll(newUsers);
}
model.currentPage = pageableModel.number;
model.totalPages = pageableModel.totalPages;
model.totalElements = pageableModel.totalElements;
model.isLoading = false;
model.isLoadingMore = false;
if (!_disposed) notifyListeners();
return ApiResponse.success(pageableModel);
} catch (e) {
logger.e(e);
model.errorMessage = e.toString();
if (page == 0) {
model.users = [];
}
model.isLoading = false;
model.isLoadingMore = false;
if (!_disposed) notifyListeners();
return ApiResponse.failure('Failed to load workspace users');
}
}
// ────────────────────────────────────────────────────────────────
// 3. ASSOCIATE USER TO WORKSPACE
// POST /api/workspaces/{workspaceId}/users/{userId}
// ────────────────────────────────────────────────────────────────
Future<ApiResponse<void>> associateUser({
required String workspaceId,
required String userId,
}) async {
try {
await http.postRaw(
'/workspaces/$workspaceId/users/$userId',
);
// Refresh the list after association so the UI stays in sync
await listWorkspaceUsers(workspaceId: workspaceId, page: 0);
return ApiResponse.success(null);
} catch (e) {
logger.e(e);
model.errorMessage = e.toString();
if (!_disposed) notifyListeners();
return ApiResponse.failure('Failed to associate user to workspace');
}
}
// ────────────────────────────────────────────────────────────────
// 4. DISSOCIATE USER FROM WORKSPACE
// DELETE /api/workspaces/{workspaceId}/users/{userId}
// ────────────────────────────────────────────────────────────────
Future<ApiResponse<void>> dissociateUser({
required String workspaceId,
required String userId,
}) async {
try {
await http.delete('/workspaces/$workspaceId/users/$userId');
// Refresh the workspace users list after dissociation
await listWorkspaceUsers(workspaceId: workspaceId, page: 0);
return ApiResponse.success(null);
} catch (e) {
logger.e(e);
model.errorMessage = e.toString();
if (!_disposed) notifyListeners();
return ApiResponse.failure('Failed to dissociate user from workspace');
}
}
// ────────────────────────────────────────────────────────────────
// Convenience helpers
// ────────────────────────────────────────────────────────────────
Future<void> searchUsers({required String workspaceId, String? name}) async {
model.searchQuery = name ?? '';
await listWorkspaceUsers(
workspaceId: workspaceId,
page: 0,
username: name,
email: model.filterEmail,
phone: model.filterPhone,
);
}
Future<void> applyFilters({
required String workspaceId,
String? email,
String? phone,
}) async {
model.filterEmail = email;
model.filterPhone = phone;
await listWorkspaceUsers(
workspaceId: workspaceId,
page: 0,
username: model.searchQuery.isNotEmpty ? model.searchQuery : null,
email: email,
phone: phone,
);
}
void clearFilters({required String workspaceId}) {
model.filterEmail = null;
model.filterPhone = null;
if (!_disposed) notifyListeners();
listWorkspaceUsers(
workspaceId: workspaceId,
page: 0,
username: model.searchQuery.isNotEmpty ? model.searchQuery : null,
);
}
Future<void> loadNextPage({required String workspaceId}) async {
if (model.isLoadingMore || model.currentPage + 1 >= model.totalPages) {
return;
}
await listWorkspaceUsers(
workspaceId: workspaceId,
page: model.currentPage + 1,
username: model.searchQuery.isNotEmpty ? model.searchQuery : null,
email: model.filterEmail,
phone: model.filterPhone,
);
}
// ────────────────────────────────────────────────────────────────
// Selection helpers for bulk actions
// ────────────────────────────────────────────────────────────────
void toggleSelectMode() {
model.selectMode = !model.selectMode;
if (!model.selectMode) {
model.selectedUserIds.clear();
}
if (!_disposed) notifyListeners();
}
void toggleUserSelection(String userId) {
if (model.selectedUserIds.contains(userId)) {
model.selectedUserIds.remove(userId);
} else {
model.selectedUserIds.add(userId);
}
if (!_disposed) notifyListeners();
}
Future<void> dissociateSelectedUsers({
required String workspaceId,
}) async {
final idsToProcess = List<String>.from(model.selectedUserIds);
model.selectMode = false;
model.selectedUserIds.clear();
if (!_disposed) notifyListeners();
for (final id in idsToProcess) {
await dissociateUser(workspaceId: workspaceId, userId: id);
}
}
}