import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:pshared/models/payment/payment.dart'; import 'package:pshared/provider/organizations.dart'; import 'package:pshared/provider/resource.dart'; import 'package:pshared/service/payment/service.dart'; import 'package:pshared/utils/exception.dart'; class PaymentsProvider with ChangeNotifier { OrganizationsProvider? _organizations; String? _loadedOrganizationRef; Resource> _resource = Resource(data: []); bool _isLoaded = false; bool _isLoadingMore = false; String? _nextCursor; Timer? _autoRefreshTimer; int _autoRefreshRefs = 0; Duration _autoRefreshInterval = const Duration(seconds: 15); int? _limit; String? _sourceRef; String? _destinationRef; List? _states; int _opSeq = 0; Resource> get resource => _resource; List get payments => _resource.data ?? []; bool get isLoading => _resource.isLoading; Exception? get error => _resource.error; bool get isReady => _isLoaded && !_resource.isLoading && _resource.error == null; bool get isLoadingMore => _isLoadingMore; String? get nextCursor => _nextCursor; bool get canLoadMore => _nextCursor != null && _nextCursor!.isNotEmpty; void beginAutoRefresh({Duration interval = const Duration(seconds: 15)}) { _autoRefreshRefs += 1; if (interval < _autoRefreshInterval) { _autoRefreshInterval = interval; _restartAutoRefreshTimer(); } _ensureAutoRefreshTimer(); } void endAutoRefresh() { if (_autoRefreshRefs == 0) return; _autoRefreshRefs -= 1; if (_autoRefreshRefs == 0) { _stopAutoRefreshTimer(); } } void update(OrganizationsProvider organizations) { _organizations = organizations; if (!organizations.isOrganizationSet) { reset(); return; } if (_autoRefreshRefs > 0) { _ensureAutoRefreshTimer(); } final orgRef = organizations.current.id; if (_loadedOrganizationRef != orgRef) { _loadedOrganizationRef = orgRef; unawaited(refresh()); } } Future refresh({ int? limit, String? sourceRef, String? destinationRef, List? states, }) async { await _refresh( limit: limit, sourceRef: sourceRef, destinationRef: destinationRef, states: states, showLoading: true, updateError: true, ); } Future refreshSilently({ int? limit, String? sourceRef, String? destinationRef, List? states, }) async { await _refresh( limit: limit, sourceRef: sourceRef, destinationRef: destinationRef, states: states, showLoading: false, updateError: false, ); } Future _refresh({ int? limit, String? sourceRef, String? destinationRef, List? states, required bool showLoading, required bool updateError, }) async { final org = _organizations; if (org == null || !org.isOrganizationSet) return; _limit = limit; _sourceRef = _normalize(sourceRef); _destinationRef = _normalize(destinationRef); _states = _normalizeStates(states); _nextCursor = null; _isLoadingMore = false; final seq = ++_opSeq; if (showLoading) { _applyResource(_resource.copyWith(isLoading: true, error: null), notify: true); } try { final page = await PaymentService.listPage( org.current.id, limit: _limit, cursor: null, sourceRef: _sourceRef, destinationRef: _destinationRef, states: _states, ); if (seq != _opSeq) return; _isLoaded = true; _nextCursor = _normalize(page.nextCursor); _applyResource( Resource( data: page.items, isLoading: false, error: null, ), notify: true, ); } catch (e) { if (seq != _opSeq) return; if (updateError) { _applyResource( _resource.copyWith(isLoading: false, error: toException(e)), notify: true, ); } else if (showLoading) { _applyResource( _resource.copyWith(isLoading: false), notify: true, ); } } } Future loadMore() async { final org = _organizations; if (org == null || !org.isOrganizationSet) return; if (_isLoadingMore || _resource.isLoading) return; final cursor = _normalize(_nextCursor); if (cursor == null) return; final seq = _opSeq; _isLoadingMore = true; _applyResource(_resource.copyWith(error: null), notify: false); notifyListeners(); try { final page = await PaymentService.listPage( org.current.id, limit: _limit, cursor: cursor, sourceRef: _sourceRef, destinationRef: _destinationRef, states: _states, ); if (seq != _opSeq) return; final combined = List.from(payments)..addAll(page.items); _nextCursor = _normalize(page.nextCursor); _applyResource( _resource.copyWith(data: combined, error: null), notify: false, ); } catch (e) { if (seq != _opSeq) return; _applyResource( _resource.copyWith(error: toException(e)), notify: false, ); } finally { if (seq == _opSeq) { _isLoadingMore = false; notifyListeners(); } } } void reset() { _opSeq++; _isLoaded = false; _isLoadingMore = false; _nextCursor = null; _limit = null; _sourceRef = null; _destinationRef = null; _states = null; _resource = Resource(data: []); _pauseAutoRefreshTimer(); notifyListeners(); } void _applyResource(Resource> newResource, {required bool notify}) { _resource = newResource; if (notify) notifyListeners(); } String? _normalize(String? value) { final trimmed = value?.trim(); if (trimmed == null || trimmed.isEmpty) return null; return trimmed; } List? _normalizeStates(List? states) { if (states == null || states.isEmpty) return null; final normalized = states .map((state) => state.trim()) .where((state) => state.isNotEmpty) .toList(); if (normalized.isEmpty) return null; return normalized; } void _ensureAutoRefreshTimer() { if (_autoRefreshTimer != null) return; _autoRefreshTimer = Timer.periodic(_autoRefreshInterval, (_) { if (_resource.isLoading || _isLoadingMore) return; unawaited(refreshSilently()); }); } void _restartAutoRefreshTimer() { if (_autoRefreshTimer == null) return; _autoRefreshTimer?.cancel(); _autoRefreshTimer = null; _ensureAutoRefreshTimer(); } void _stopAutoRefreshTimer() { _autoRefreshTimer?.cancel(); _autoRefreshTimer = null; _autoRefreshRefs = 0; _autoRefreshInterval = const Duration(seconds: 15); } void _pauseAutoRefreshTimer() { _autoRefreshTimer?.cancel(); _autoRefreshTimer = null; } }