import 'package:flutter/material.dart'; import 'package:qpay/http/http.dart'; import 'package:qpay/models/api_response.dart'; import 'package:qpay/screens/groups/models/group_batch_model.dart'; import 'package:qpay/models/pageable_model.dart'; import 'package:qpay/screens/home/home_controller.dart'; import 'package:qpay/screens/pay/pay_controller.dart'; import 'package:qpay/screens/transactions/transaction_model.dart'; class BatchScreenModel { List batches = []; List batchItems = []; GroupBatch? currentBatch; List providers = []; List processors = []; BillProvider? selectedProvider; PaymentProcessor? selectedProcessor; bool isLoading = false; bool isLoadingMore = false; bool isActing = false; String? errorMessage; int currentPage = 0; int totalPages = 0; int totalElements = 0; String searchQuery = ''; String? statusFilter; String? currencyFilter; bool selectMode = false; Set selectedBatchIds = {}; } class BatchController extends ChangeNotifier { final BatchScreenModel model = BatchScreenModel(); final Http http = Http(); BuildContext context; bool _disposed = false; BatchController(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>> loadProviders() async { try { final raw = await http.get( '/public/providers?currency=USD&sort=priority,asc', ); final response = _unwrap(raw); final List content; if (response is Map && response.containsKey('content')) { content = response['content'] as List; } else if (response is List) { content = response; } else { content = []; } model.providers = content .map((e) => BillProvider.fromJson(e as Map)) .toList(); if (model.providers.isNotEmpty) { model.selectedProvider = model.providers.first; } if (!_disposed) notifyListeners(); return ApiResponse.success(model.providers); } catch (e) { logger.e(e); return ApiResponse.failure('Failed to load providers'); } } Future>> loadProcessors() async { try { final raw = await http.get('/public/payment-processors'); final response = _unwrap(raw); final List content = response is List ? response : (response['content'] ?? []); model.processors = content .map((e) => PaymentProcessor.fromJson(e as Map)) .toList(); if (model.processors.isNotEmpty) { model.selectedProcessor = model.processors.first; } if (!_disposed) notifyListeners(); return ApiResponse.success(model.processors); } catch (e) { logger.e(e); return ApiResponse.failure('Failed to load processors'); } } Future>> listBatches( String groupId, { int page = 0, int size = 20, String? search, String? status, String? currency, }) async { if (page == 0) { model.isLoading = true; model.batches = []; model.currentPage = 0; } else { model.isLoadingMore = true; } if (!_disposed) notifyListeners(); try { final params = { 'recipientGroupId': groupId, 'page': page.toString(), 'size': size.toString(), 'sort': 'createdAt,desc', }; if (search != null && search.isNotEmpty) { params['description'] = search; } if (status != null && status.isNotEmpty) { params['status'] = status; } if (currency != null && currency.isNotEmpty) { params['currency'] = currency; } final raw = await http.get( '/public/group-batches?${buildQueryParameters(params)}', ); final response = _unwrap(raw); final pageableModel = PageableModel.fromJson( response as Map, ); final newBatches = pageableModel.content .map((e) => GroupBatch.fromJson(e as Map)) .toList(); if (page == 0) { model.batches = newBatches; } else { model.batches.addAll(newBatches); } model.currentPage = pageableModel.number; model.totalPages = pageableModel.totalPages; model.totalElements = pageableModel.totalElements; model.isLoading = false; model.isLoadingMore = false; if (!_disposed) notifyListeners(); return ApiResponse.success(model.batches); } catch (e) { logger.e(e); model.errorMessage = e.toString(); if (page == 0) { model.batches = []; } model.isLoading = false; model.isLoadingMore = false; if (!_disposed) notifyListeners(); return ApiResponse.failure('Failed to load batches'); } } Future searchBatches( String groupId, { String? search, String? status, String? currency, }) async { model.searchQuery = search ?? ''; model.statusFilter = status; model.currencyFilter = currency; await listBatches( groupId, page: 0, search: search, status: status, currency: currency, ); } Future loadNextPage(String groupId) async { if (model.isLoadingMore || model.currentPage + 1 >= model.totalPages) { return; } await listBatches( groupId, page: model.currentPage + 1, search: model.searchQuery.isNotEmpty ? model.searchQuery : null, status: model.statusFilter, currency: model.currencyFilter, ); } Future>> listBatchItems( String batchId, ) async { model.isLoading = true; if (!_disposed) notifyListeners(); try { final raw = await http.get( '/public/group-batches/items?groupBatchId=$batchId&sort=createdAt,desc', ); final response = _unwrap(raw); final List content; if (response is Map && response.containsKey('content')) { content = PageableModel.fromJson( response as Map, ).content; } else if (response is List) { content = response; } else { content = []; } model.batchItems = content .map((e) => GroupBatchItem.fromJson(e as Map)) .toList(); model.isLoading = false; if (!_disposed) notifyListeners(); return ApiResponse.success(model.batchItems); } catch (e) { logger.e(e); model.errorMessage = e.toString(); model.batchItems = []; model.isLoading = false; if (!_disposed) notifyListeners(); return ApiResponse.failure('Failed to load batch items'); } } Future> getBatch( String batchId, String userId, ) async { model.isActing = true; if (!_disposed) notifyListeners(); try { final raw = await http.get( '/public/group-batches/$batchId?userId=$userId', ); final response = _unwrap(raw); final batch = GroupBatch.fromJson(response as Map); model.currentBatch = batch; model.isActing = false; if (!_disposed) notifyListeners(); return ApiResponse.success(batch); } catch (e) { logger.e(e); model.isActing = false; if (!_disposed) notifyListeners(); return ApiResponse.failure('Failed to load batch'); } } Future> createBatch(CreateBatchRequest req) async { model.isActing = true; if (!_disposed) notifyListeners(); try { final raw = await http.post('/public/group-batches', req.toJson()); final response = _unwrap(raw); final batch = GroupBatch.fromJson(response as Map); model.currentBatch = batch; model.isActing = false; if (!_disposed) notifyListeners(); return ApiResponse.success(batch); } catch (e, s) { logger.e(e); logger.e(s); model.isActing = false; if (!_disposed) notifyListeners(); return ApiResponse.failure('Failed to create batch'); } } Future> updateBatch( GroupBatchUpdateRequest request, ) async { model.isActing = true; if (!_disposed) notifyListeners(); try { final raw = await http.put( '/public/group-batches/${request.id}', request.toJson(), ); final response = _unwrap(raw); final batch = GroupBatch.fromJson(response as Map); // Update the batch in the local list if present final index = model.batches.indexWhere((b) => b.id == batch.id); if (index != -1) { model.batches[index] = batch; } model.currentBatch = batch; model.isActing = false; if (!_disposed) notifyListeners(); return ApiResponse.success(batch); } catch (e, s) { logger.e(e); logger.e(s); model.isActing = false; if (!_disposed) notifyListeners(); return ApiResponse.failure('Failed to update batch'); } } Future> confirmBatch( String batchId, String userId, ) async { model.isActing = true; if (!_disposed) notifyListeners(); try { final key = 'batch-$batchId-confirm-${DateTime.now().millisecondsSinceEpoch}'; final body = BatchActionRequest(userId: userId, idempotencyKey: key); final raw = await http.post( '/public/group-batches/$batchId/confirm', body.toJson(), ); final response = _unwrap(raw); final batch = GroupBatch.fromJson(response as Map); model.currentBatch = batch; model.isActing = false; if (!_disposed) notifyListeners(); return ApiResponse.success(batch); } catch (e) { logger.e(e); model.isActing = false; if (!_disposed) notifyListeners(); return ApiResponse.failure('Failed to confirm batch'); } } Future> requestBatch( String batchId, String userId, ) async { model.isActing = true; if (!_disposed) notifyListeners(); try { final key = 'batch-$batchId-request-${DateTime.now().millisecondsSinceEpoch}'; final body = BatchActionRequest(userId: userId, idempotencyKey: key); final raw = await http.post( '/public/group-batches/$batchId/request', body.toJson(), ); final response = _unwrap(raw); final txn = TransactionModel.fromJson(response as Map); model.isActing = false; if (!_disposed) notifyListeners(); return ApiResponse.success(txn); } catch (e, s) { logger.e(e); logger.e(s); model.isActing = false; if (!_disposed) notifyListeners(); return ApiResponse.failure('Failed to request batch payment'); } } Future>> downloadBatchReport( String batchId, String userId, ) async { model.isActing = true; if (!_disposed) notifyListeners(); try { final raw = await http.get( '/public/group-batches/$batchId/report?userId=$userId', ); final response = _unwrap(raw); model.isActing = false; if (!_disposed) notifyListeners(); return ApiResponse.success(response as Map); } catch (e, s) { logger.e(e); logger.e(s); model.isActing = false; if (!_disposed) notifyListeners(); return ApiResponse.failure('Failed to download batch report'); } } Future> pollBatch( String batchId, String userId, ) async { model.isActing = true; if (!_disposed) notifyListeners(); try { final key = 'batch-$batchId-poll-${DateTime.now().millisecondsSinceEpoch}'; final body = BatchActionRequest(userId: userId, idempotencyKey: key); final raw = await http.post( '/public/group-batches/$batchId/poll', body.toJson(), ); final response = _unwrap(raw); final batchTransaction = TransactionModel.fromJson( response as Map, ); model.isActing = false; if (!_disposed) notifyListeners(); if (response['status'] != 'FAILED') { return ApiResponse.success(batchTransaction); } else { return ApiResponse.failure( response['errorMessage'] ?? "Transaction failed", ); } } catch (e, s) { logger.e(e); logger.e(s); model.isActing = false; if (!_disposed) notifyListeners(); return ApiResponse.failure('Failed to poll batch'); } } Future deleteBatch({ required String batchId, required String userId, }) async { try { await http.delete('/public/group-batches/$batchId?userId=$userId'); model.batches.removeWhere((b) => b.id == batchId); model.selectedBatchIds.remove(batchId); model.currentBatch = null; if (!_disposed) notifyListeners(); } catch (e) { logger.e(e); model.errorMessage = e.toString(); if (!_disposed) notifyListeners(); } } Future deleteSelectedBatches({ required String groupId, required String userId, }) async { final idsToDelete = List.from(model.selectedBatchIds); model.selectMode = false; model.selectedBatchIds.clear(); if (!_disposed) notifyListeners(); for (final id in idsToDelete) { await deleteBatch(batchId: id, userId: userId); } // Refresh after deletion await listBatches(groupId, page: 0); } void selectProvider(BillProvider provider) { model.selectedProvider = provider; if (!_disposed) notifyListeners(); } void selectProcessor(PaymentProcessor processor) { model.selectedProcessor = processor; if (!_disposed) notifyListeners(); } void toggleSelectMode() { model.selectMode = !model.selectMode; if (!model.selectMode) { model.selectedBatchIds.clear(); } if (!_disposed) notifyListeners(); } void toggleBatchSelection(String batchId) { if (model.selectedBatchIds.contains(batchId)) { model.selectedBatchIds.remove(batchId); } else { model.selectedBatchIds.add(batchId); } if (!_disposed) notifyListeners(); } }