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 users = []; bool isLoading = false; bool isLoadingMore = false; String? errorMessage; int currentPage = 0; int totalPages = 0; int totalElements = 0; String searchQuery = ''; bool selectMode = false; Set 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 params) { final filtered = {}; 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> 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 = { '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, ); final newUsers = pageableModel.content .map((e) => ManagedUserModel.fromJson(e as Map)) .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> 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 = { '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, ); final newUsers = pageableModel.content .map((e) => ManagedUserModel.fromJson(e as Map)) .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> 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> 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 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 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 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 dissociateSelectedUsers({ required String workspaceId, }) async { final idsToProcess = List.from(model.selectedUserIds); model.selectMode = false; model.selectedUserIds.clear(); if (!_disposed) notifyListeners(); for (final id in idsToProcess) { await dissociateUser(workspaceId: workspaceId, userId: id); } } }