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/create_group_from_file_response.dart'; import 'package:qpay/screens/groups/models/recipient_group_model.dart'; class GroupScreenModel { List groups = []; List members = []; PageableModel? membersPage; 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 workspaceId, 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 = { 'workspaceId': workspaceId, 'page': page.toString(), 'size': size.toString(), }; if (name != null && name.isNotEmpty) { params['name'] = name; } final raw = await http.get( '/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 workspaceId, String? name}) async { model.searchQuery = name ?? ''; await listGroups(workspaceId: workspaceId, page: 0, name: name); } Future loadNextPage({required String workspaceId}) async { if (model.isLoadingMore || model.currentPage + 1 >= model.totalPages) { return; } await listGroups( workspaceId: workspaceId, 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( '/recipient-groups/members?recipientGroupId=$groupId', ); final response = _unwrap(raw); model.membersPage = PageableModel.fromJson(response); 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('/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 workspaceId, }) async { try { await http.delete( '/recipient-groups/delete/$groupId?workspaceId=$workspaceId', ); 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 workspaceId, }) async { try { await http.delete( '/recipient-groups/$groupId/members/$recipientId?workspaceId=$workspaceId', ); 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 workspaceId}) 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, workspaceId: workspaceId); } } Future> createGroupFromFile({ required String groupName, required String workspaceId, required String userId, String? description, required String currency, required List fileBytes, required String fileName, }) async { model.isLoading = true; if (!_disposed) notifyListeners(); try { final fields = { 'groupName': groupName, 'workspaceId': workspaceId, 'userId': userId, 'currency': currency, }; if (description != null && description.isNotEmpty) { fields['description'] = description; } final raw = await http.multipartPost( '/recipient-groups/create-from-file', fields, 'file', fileBytes, fileName, ); final response = _unwrap(raw); final result = CreateGroupFromFileResponse.fromJson( response as Map, ); model.isLoading = false; if (!_disposed) notifyListeners(); return ApiResponse.success(result); } catch (e) { logger.e(e); model.errorMessage = e.toString(); model.isLoading = false; if (!_disposed) notifyListeners(); return ApiResponse.failure('Failed to create group from file'); } } }