added uptime monitoring
This commit is contained in:
325
lib/screens/users/controllers/user_controller.dart
Normal file
325
lib/screens/users/controllers/user_controller.dart
Normal file
@@ -0,0 +1,325 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user