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/groups/models/recipient_group_model.dart'; class GroupScreenModel { List groups = []; List members = []; bool isLoading = false; bool isLoadingMore = false; String? errorMessage; int currentPage = 0; int totalPages = 0; int totalElements = 0; String searchQuery = ''; bool selectMode = false; Set selectedGroupIds = {}; } class GroupController extends ChangeNotifier { final GroupScreenModel model = GroupScreenModel(); final Http http = Http(); BuildContext context; bool _disposed = false; GroupController(this.context); @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) { if (params.isEmpty) { return ''; } String query = ''; params.forEach((key, value) { query += '$key=$value&'; }); return query.substring(0, query.length - 1); } Future> listGroups({ required String userId, int page = 0, int size = 20, String? name, }) async { if (page == 0) { model.isLoading = true; model.groups = []; model.currentPage = 0; } else { model.isLoadingMore = true; } if (!_disposed) notifyListeners(); try { final params = { 'userId': userId, 'page': page.toString(), 'size': size.toString(), }; if (name != null && name.isNotEmpty) { params['name'] = name; } final raw = await http.get( '/public/recipient-groups?${buildQueryParameters(params)}', ); final response = _unwrap(raw); final pageableModel = PageableModel.fromJson(response as Map); final newGroups = pageableModel.content .map((e) => RecipientGroup.fromJson(e as Map)) .toList(); if (page == 0) { model.groups = newGroups; } else { model.groups.addAll(newGroups); } 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.groups = []; } model.isLoading = false; model.isLoadingMore = false; if (!_disposed) notifyListeners(); return ApiResponse.failure('Failed to load groups'); } } Future searchGroups({ required String userId, String? name, }) async { model.searchQuery = name ?? ''; await listGroups(userId: userId, page: 0, name: name); } Future loadNextPage({required String userId}) async { if (model.isLoadingMore || model.currentPage + 1 >= model.totalPages) { return; } await listGroups( userId: userId, page: model.currentPage + 1, name: model.searchQuery.isNotEmpty ? model.searchQuery : null, ); } Future>> listMembers( String groupId, ) async { model.isLoading = true; if (!_disposed) notifyListeners(); try { final raw = await http.get( '/public/recipient-groups/members?recipientGroupId=$groupId', ); final response = _unwrap(raw); final List content; if (response is List) { content = response; } else if (response is Map && response.containsKey('content')) { content = response['content'] as List; } else { content = []; } model.members = content .map((e) => RecipientGroupMember.fromJson(e as Map)) .toList(); model.isLoading = false; if (!_disposed) notifyListeners(); return ApiResponse.success(model.members); } catch (e, s) { logger.e(e); logger.e(s); model.errorMessage = e.toString(); model.members = []; model.isLoading = false; if (!_disposed) notifyListeners(); return ApiResponse.failure('Failed to load members'); } } Future> createGroup( CreateGroupRequest req, ) async { model.isLoading = true; if (!_disposed) notifyListeners(); try { final raw = await http.post('/public/recipient-groups', req.toJson()); final response = _unwrap(raw); final group = RecipientGroup.fromJson(response as Map); model.groups = [group, ...model.groups]; model.isLoading = false; if (!_disposed) notifyListeners(); return ApiResponse.success(group); } catch (e) { logger.e(e); model.errorMessage = e.toString(); model.isLoading = false; if (!_disposed) notifyListeners(); return ApiResponse.failure('Failed to create group'); } } Future deleteGroup({ required String groupId, required String userId, }) async { try { await http.delete( '/public/recipient-groups/delete/$groupId?userId=$userId', ); model.groups.removeWhere((g) => g.id == groupId); model.selectedGroupIds.remove(groupId); if (!_disposed) notifyListeners(); } catch (e) { logger.e(e); model.errorMessage = e.toString(); if (!_disposed) notifyListeners(); } } void toggleSelectMode() { model.selectMode = !model.selectMode; if (!model.selectMode) { model.selectedGroupIds.clear(); } if (!_disposed) notifyListeners(); } void toggleGroupSelection(String groupId) { if (model.selectedGroupIds.contains(groupId)) { model.selectedGroupIds.remove(groupId); } else { model.selectedGroupIds.add(groupId); } if (!_disposed) notifyListeners(); } Future removeMember({ required String groupId, required String recipientId, required String userId, }) async { try { await http.delete( '/public/recipient-groups/$groupId/members/$recipientId?userId=$userId', ); model.members.removeWhere((m) => m.id == recipientId); if (!_disposed) notifyListeners(); } catch (e) { logger.e(e); model.errorMessage = e.toString(); if (!_disposed) notifyListeners(); } } Future deleteSelectedGroups({required String userId}) async { final idsToDelete = List.from(model.selectedGroupIds); model.selectMode = false; model.selectedGroupIds.clear(); if (!_disposed) notifyListeners(); for (final id in idsToDelete) { await deleteGroup(groupId: id, userId: userId); } } }