import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pshared/models/payment/operation.dart'; import 'package:pshared/models/payment/payment.dart'; import 'package:pshared/models/payment/status.dart'; import 'package:pshared/provider/payment/payments.dart'; import 'package:pweb/pages/report/charts/distribution.dart'; import 'package:pweb/pages/report/charts/status.dart'; import 'package:pweb/pages/report/table/filters.dart'; import 'package:pweb/pages/report/table/widget.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; class OperationHistoryPage extends StatefulWidget { const OperationHistoryPage({super.key}); @override State createState() => _OperationHistoryPageState(); } class _OperationHistoryPageState extends State { DateTimeRange? _pendingRange; DateTimeRange? _appliedRange; final Set _pendingStatuses = {}; Set _appliedStatuses = {}; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { final provider = context.read(); if (!provider.isReady && !provider.isLoading) { provider.refresh(); } }); } Future _pickRange() async { final now = DateTime.now(); final initial = _pendingRange ?? DateTimeRange( start: now.subtract(const Duration(days: 30)), end: now, ); final picked = await showDateRangePicker( context: context, firstDate: DateTime(2000), lastDate: now.add(const Duration(days: 1)), initialDateRange: initial, ); if (picked != null) { setState(() { _pendingRange = picked; }); } } void _toggleStatus(OperationStatus status) { setState(() { if (_pendingStatuses.contains(status)) { _pendingStatuses.remove(status); } else { _pendingStatuses.add(status); } }); } void _applyFilters() { setState(() { _appliedRange = _pendingRange; _appliedStatuses = {..._pendingStatuses}; }); } List _mapPayments(List payments) { return payments.map(_mapPayment).toList(); } OperationItem _mapPayment(Payment payment) { final debit = payment.lastQuote?.debitAmount; final settlement = payment.lastQuote?.expectedSettlementAmount; final amountMoney = debit ?? settlement; final amount = _parseAmount(amountMoney?.amount); final currency = amountMoney?.currency ?? ''; final toAmount = settlement == null ? amount : _parseAmount(settlement.amount); final toCurrency = settlement?.currency ?? currency; final payId = _firstNonEmpty([payment.paymentRef, payment.idempotencyKey]) ?? '-'; final name = _firstNonEmpty([payment.lastQuote?.quoteRef, payment.paymentRef, payment.idempotencyKey]) ?? '-'; final comment = _firstNonEmpty([payment.failureReason, payment.failureCode, payment.state]) ?? ''; return OperationItem( status: _statusFromPaymentState(payment.state), fileName: null, amount: amount, currency: currency, toAmount: toAmount, toCurrency: toCurrency, payId: payId, paymentRef: payment.paymentRef, cardNumber: null, name: name, date: _resolvePaymentDate(payment), comment: comment, ); } List _filterOperations(List operations) { if (_appliedRange == null && _appliedStatuses.isEmpty) { return operations; } return operations.where((op) { final statusMatch = _appliedStatuses.isEmpty || _appliedStatuses.contains(op.status); final dateMatch = _appliedRange == null || _isUnknownDate(op.date) || (op.date.isAfter(_appliedRange!.start.subtract(const Duration(seconds: 1))) && op.date.isBefore(_appliedRange!.end.add(const Duration(seconds: 1)))); return statusMatch && dateMatch; }).toList(); } OperationStatus _statusFromPaymentState(String? raw) { final state = raw?.trim().toLowerCase(); switch (state) { case 'accepted': case 'funds_reserved': case 'submitted': case 'unspecified': case null: return OperationStatus.processing; case 'settled': case 'success': return OperationStatus.success; case 'failed': case 'cancelled': return OperationStatus.error; default: // Future-proof: any new backend state is treated as processing return OperationStatus.processing; } } DateTime _resolvePaymentDate(Payment payment) { final expiresAt = payment.lastQuote?.fxQuote?.expiresAtUnixMs; if (expiresAt != null && expiresAt > 0) { return DateTime.fromMillisecondsSinceEpoch(expiresAt, isUtc: true); } return DateTime.fromMillisecondsSinceEpoch(0, isUtc: true); } double _parseAmount(String? amount) { if (amount == null || amount.trim().isEmpty) return 0; return double.tryParse(amount) ?? 0; } String? _firstNonEmpty(List values) { for (final value in values) { final trimmed = value?.trim(); if (trimmed != null && trimmed.isNotEmpty) return trimmed; } return null; } bool _isUnknownDate(DateTime date) => date.millisecondsSinceEpoch == 0; @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context)!; return Consumer( builder: (context, provider, child) { if (provider.isLoading) { return const Center(child: CircularProgressIndicator()); } if (provider.error != null) { final message = provider.error?.toString() ?? loc.noErrorInformation; return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(loc.notificationError(message)), ElevatedButton( onPressed: () => provider.refresh(), child: Text(loc.retry), ), ], ), ); } final operations = _mapPayments(provider.payments); final filteredOperations = _filterOperations(operations); final hasFileName = operations.any( (operation) => (operation.fileName ?? '').trim().isNotEmpty, ); return Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, spacing: 16, children: [ SizedBox( height: 200, child: Row( spacing: 16, children: [ Expanded( child: StatusChart(operations: operations), ), Expanded( child: PayoutDistributionChart( operations: operations, ), ), ], ), ), OperationFilters( selectedRange: _pendingRange, selectedStatuses: _pendingStatuses, onPickRange: _pickRange, onToggleStatus: _toggleStatus, onApply: _applyFilters, ), OperationsTable( operations: filteredOperations, showFileNameColumn: hasFileName, ), ], ), ); }, ); } }