added uptime monitoring

This commit is contained in:
2026-06-24 00:31:01 +02:00
parent e89c711d00
commit 1866e60518
16 changed files with 2220 additions and 19 deletions

View 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);
}
}
}

View File

@@ -0,0 +1,118 @@
class ManagedUserModel {
final String id;
final String createdAt;
final String updatedAt;
final bool deleted;
final String username;
final String email;
final String phone;
final int workflowId;
final String firstName;
final String lastName;
final bool enabled;
final bool accountNonExpired;
final bool accountNonLocked;
final bool credentialsNonExpired;
final List<ManagedUserAuthority> authorities;
ManagedUserModel({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.deleted,
required this.username,
required this.email,
required this.phone,
required this.workflowId,
required this.firstName,
required this.lastName,
required this.enabled,
required this.accountNonExpired,
required this.accountNonLocked,
required this.credentialsNonExpired,
required this.authorities,
});
factory ManagedUserModel.fromJson(Map<String, dynamic> json) {
return ManagedUserModel(
id: json['id'] as String,
createdAt: json['createdAt'] as String? ?? '',
updatedAt: json['updatedAt'] as String? ?? '',
deleted: json['deleted'] as bool? ?? false,
username: json['username'] as String? ?? '',
email: json['email'] as String? ?? '',
phone: json['phone'] as String? ?? '',
workflowId: json['workflowId'] as int? ?? 0,
firstName: json['firstName'] as String? ?? '',
lastName: json['lastName'] as String? ?? '',
enabled: json['enabled'] as bool? ?? true,
accountNonExpired: json['accountNonExpired'] as bool? ?? true,
accountNonLocked: json['accountNonLocked'] as bool? ?? true,
credentialsNonExpired: json['credentialsNonExpired'] as bool? ?? true,
authorities: (json['authorities'] as List<dynamic>?)
?.map((e) =>
ManagedUserAuthority.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'createdAt': createdAt,
'updatedAt': updatedAt,
'deleted': deleted,
'username': username,
'email': email,
'phone': phone,
'workflowId': workflowId,
'firstName': firstName,
'lastName': lastName,
'enabled': enabled,
'accountNonExpired': accountNonExpired,
'accountNonLocked': accountNonLocked,
'credentialsNonExpired': credentialsNonExpired,
'authorities': authorities.map((e) => e.toJson()).toList(),
};
}
String get displayName {
if (firstName.isNotEmpty && lastName.isNotEmpty) {
return '$firstName $lastName';
}
if (firstName.isNotEmpty) return firstName;
if (lastName.isNotEmpty) return lastName;
return username;
}
String get initials {
if (firstName.isNotEmpty && lastName.isNotEmpty) {
return '${firstName[0]}${lastName[0]}'.toUpperCase();
}
if (firstName.isNotEmpty && firstName.length >= 2) {
return firstName.substring(0, 2).toUpperCase();
}
if (username.isNotEmpty && username.length >= 2) {
return username.substring(0, 2).toUpperCase();
}
return '?';
}
String get roleNames =>
authorities.map((a) => a.authority.replaceFirst('ROLE_', '')).join(', ');
}
class ManagedUserAuthority {
final String authority;
ManagedUserAuthority({required this.authority});
factory ManagedUserAuthority.fromJson(Map<String, dynamic> json) {
return ManagedUserAuthority(authority: json['authority'] as String? ?? '');
}
Map<String, dynamic> toJson() {
return {'authority': authority};
}
}

File diff suppressed because it is too large Load Diff