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; int? _limit; String? _quotationRef; DateTime? _createdFrom; DateTime? _createdTo; 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 update(OrganizationsProvider organizations) { _organizations = organizations; if (!organizations.isOrganizationSet) { reset(); return; } final orgRef = organizations.current.id; if (_loadedOrganizationRef != orgRef) { _loadedOrganizationRef = orgRef; unawaited(refresh()); } } Future refresh({ int? limit, String? quotationRef, DateTime? createdFrom, DateTime? createdTo, List? states, }) async { await _refresh( limit: limit, quotationRef: quotationRef, createdFrom: createdFrom, createdTo: createdTo, states: states, showLoading: true, updateError: true, ); } Future refreshSilently({ int? limit, String? quotationRef, DateTime? createdFrom, DateTime? createdTo, List? states, }) async { await _refresh( limit: limit, quotationRef: quotationRef, createdFrom: createdFrom, createdTo: createdTo, states: states, showLoading: false, updateError: false, ); } Future refreshCurrentQuerySilently() async { await _refresh( limit: _limit, quotationRef: _quotationRef, createdFrom: _createdFrom, createdTo: _createdTo, states: _states, showLoading: false, updateError: false, ); } void mergePayments(List incoming) { if (incoming.isEmpty) return; final existing = List.from(_resource.data ?? const []); final combined = [...incoming, ...existing]; final seen = {}; final merged = []; for (final payment in combined) { final key = _paymentKey(payment); if (key == null || key.isEmpty) { merged.add(payment); continue; } if (seen.contains(key)) continue; seen.add(key); merged.add(payment); } _applyResource(_resource.copyWith(data: merged), notify: true); } Future _refresh({ int? limit, String? quotationRef, DateTime? createdFrom, DateTime? createdTo, List? states, required bool showLoading, required bool updateError, }) async { final org = _organizations; if (org == null || !org.isOrganizationSet) return; _limit = limit; _quotationRef = _normalize(quotationRef); _createdFrom = createdFrom?.toUtc(); _createdTo = createdTo?.toUtc(); _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, quotationRef: _quotationRef, createdFrom: _createdFrom, createdTo: _createdTo, 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, quotationRef: _quotationRef, createdFrom: _createdFrom, createdTo: _createdTo, 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; _quotationRef = null; _createdFrom = null; _createdTo = null; _states = null; _resource = Resource(data: []); 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; } String? _paymentKey(Payment payment) { final ref = _normalize(payment.paymentRef); if (ref != null) return ref; return _normalize(payment.idempotencyKey); } 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; } }