reports page #518

Merged
tech merged 2 commits from SEND053 into main 2026-02-17 09:21:39 +00:00
13 changed files with 120 additions and 58 deletions
Showing only changes of commit e2e2257167 - Show all commits

View File

@@ -1,13 +1,16 @@
import 'package:pshared/models/wallet/wallet.dart' as domain;
import 'package:pshared/models/payment/wallet.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/money.dart';
extension WalletUiMapper on domain.WalletModel {
Wallet toUi() => Wallet(
id: walletRef,
walletUserID: walletRef,
balance: double.tryParse(availableMoney?.amount ?? balance?.available?.amount ?? '0') ?? 0,
balance: parseMoneyAmount(
availableMoney?.amount ?? balance?.available?.amount,
),
currency: currencyStringToCode(asset.tokenSymbol),
calculatedAt: balance?.calculatedAt ?? DateTime.now(),
depositAddress: depositAddress,

View File

@@ -3,6 +3,7 @@ import 'package:pshared/models/describable.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pshared/models/wallet/chain_asset.dart';
import 'package:pshared/service/wallet.dart' as shared_wallet_service;
import 'package:pshared/utils/money.dart';
abstract class WalletsService {
@@ -29,8 +30,7 @@ class ApiWalletsService implements WalletsService {
organizationRef: organizationRef,
walletRef: walletRef,
);
final amount = balance.available?.amount;
return amount == null ? 0 : double.tryParse(amount) ?? 0;
return parseMoneyAmount(balance.available?.amount);
}
@override

View File

@@ -0,0 +1,12 @@
import 'package:pshared/models/money.dart';
double parseMoneyAmount(String? raw, {double fallback = 0}) {
final trimmed = raw?.trim();
if (trimmed == null || trimmed.isEmpty) return fallback;
return double.tryParse(trimmed) ?? fallback;
}
extension MoneyAmountX on Money {
double get amountValue => parseMoneyAmount(amount);
}

View File

@@ -17,17 +17,11 @@ class RecentPaymentsController extends ChangeNotifier {
void update(PaymentsProvider provider) {
if (!identical(_payments, provider)) {
_payments?.removeListener(_onPaymentsChanged);
_payments = provider;
Review

по идее должно работать без ручного управления подпиской. update должен вызываться при любом обновлении провайдера. Соответственно, можно просто из update делать rebuild. Так не работает?

по идее должно работать без ручного управления подпиской. update должен вызываться при любом обновлении провайдера. Соответственно, можно просто из update делать rebuild. Так не работает?
_payments?.addListener(_onPaymentsChanged);
}
_rebuild();
}
void _onPaymentsChanged() {
_rebuild();
}
void _rebuild() {
final operations = (_payments?.payments ?? const [])
.map(mapPaymentToOperation)
@@ -36,9 +30,4 @@ class RecentPaymentsController extends ChangeNotifier {
notifyListeners();
}
@override
void dispose() {
_payments?.removeListener(_onPaymentsChanged);
super.dispose();
}
}

View File

@@ -28,9 +28,7 @@ class ReportOperationsController extends ChangeNotifier {
void update(PaymentsProvider provider) {
if (!identical(_payments, provider)) {
_payments?.removeListener(_onPaymentsChanged);
_payments = provider;
Review

то же самое

то же самое
_payments?.addListener(_onPaymentsChanged);
}
_rebuildOperations();
}
@@ -61,10 +59,6 @@ class ReportOperationsController extends ChangeNotifier {
await _payments?.refresh();
}
void _onPaymentsChanged() {
_rebuildOperations();
}
void _rebuildOperations() {
final items = _payments?.payments ?? const [];
_operations = items.map(mapPaymentToOperation).toList();
@@ -107,9 +101,4 @@ class ReportOperationsController extends ChangeNotifier {
left.end.isAtSameMomentAs(right.end);
}
@override
void dispose() {
_payments?.removeListener(_onPaymentsChanged);
super.dispose();
}
}

View File

@@ -0,0 +1,27 @@
enum PaymentState {
success,
failed,
cancelled,
processing,
unknown,
}
PaymentState paymentStateFromRaw(String? raw) {
final trimmed = (raw ?? '').trim().toUpperCase();
final normalized = trimmed.startsWith('PAYMENT_STATE_')
? trimmed.substring('PAYMENT_STATE_'.length)
: trimmed;
switch (normalized) {
case 'SUCCESS':
return PaymentState.success;
case 'FAILED':
return PaymentState.failed;
case 'CANCELLED':
return PaymentState.cancelled;
case 'PROCESSING':
return PaymentState.processing;
default:
return PaymentState.unknown;
}
}

View File

@@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
import 'package:pshared/controllers/balance_mask/ledger_accounts.dart';
import 'package:pshared/models/ledger/account.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/money.dart';
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
import 'package:pweb/pages/dashboard/buttons/balance/header.dart';
@@ -25,8 +26,8 @@ class LedgerAccountCard extends StatelessWidget {
final money = account.balance?.balance;
if (money == null) return '--';
final amount = double.tryParse(money.amount);
if (amount == null) {
final amount = parseMoneyAmount(money.amount, fallback: double.nan);
if (amount.isNaN) {
return '${money.amount} ${money.currency}';
}

View File

@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import 'package:pshared/provider/payment/amount.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/money.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -32,7 +33,13 @@ class _PaymentAmountWidgetState extends State<PaymentAmountWidget> {
super.dispose();
}
double? _parseAmount(String value) => double.tryParse(value.replaceAll(',', '.'));
double? _parseAmount(String value) {
final parsed = parseMoneyAmount(
value.replaceAll(',', '.'),
fallback: double.nan,
);
return parsed.isNaN ? null : parsed;
}
void _syncTextWithAmount(double amount) {
final parsedText = _parseAmount(_controller.text);

View File

@@ -1,14 +1,15 @@
import 'package:pshared/models/asset.dart';
import 'package:pshared/models/money.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/money.dart';
import 'package:pweb/controllers/multiple_payouts.dart';
String moneyLabel(Money? money) {
if (money == null) return 'N/A';
final amount = double.tryParse(money.amount);
if (amount == null) return '${money.amount} ${money.currency}';
final amount = parseMoneyAmount(money.amount, fallback: double.nan);
if (amount.isNaN) return '${money.amount} ${money.currency}';
try {
return assetToString(
Asset(

View File

@@ -8,6 +8,7 @@ import 'package:pshared/provider/payment/multiple/provider.dart';
import 'package:pshared/provider/payment/multiple/quotation.dart';
import 'package:pshared/provider/payment/payments.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/money.dart';
import 'package:pweb/models/multiple_payouts/csv_row.dart';
import 'package:pweb/models/multiple_payouts/state.dart';
@@ -93,8 +94,8 @@ class MultiplePayoutsProvider extends ChangeNotifier {
double total = 0;
for (final row in _rows) {
final value = double.tryParse(row.amount);
if (value == null) return null;
final value = parseMoneyAmount(row.amount, fallback: double.nan);
if (value.isNaN) return null;
total += value;
}
return Money(amount: amountToString(total), currency: currency);
@@ -121,10 +122,10 @@ class MultiplePayoutsProvider extends ChangeNotifier {
final fee = aggregateFeeAmountFor(sourceWallet);
if (debit == null || fee == null) return null;
final debitValue = double.tryParse(debit.amount);
final feeValue = double.tryParse(fee.amount);
final debitValue = parseMoneyAmount(debit.amount, fallback: double.nan);
final feeValue = parseMoneyAmount(fee.amount, fallback: double.nan);
if (debit.currency.toUpperCase() != fee.currency.toUpperCase()) return null;
if (debitValue == null || feeValue == null || debitValue <= 0) return null;
if (debitValue.isNaN || feeValue.isNaN || debitValue <= 0) return null;
return (feeValue / debitValue) * 100;
}

View File

@@ -1,3 +1,5 @@
import 'package:pshared/utils/money.dart';
import 'package:pweb/models/multiple_payouts/csv_row.dart';
@@ -79,8 +81,8 @@ class MultipleCsvParser {
throw FormatException('CSV row ${i + 1}: amount is required');
}
final parsedAmount = double.tryParse(amount);
if (parsedAmount == null || parsedAmount <= 0) {
final parsedAmount = parseMoneyAmount(amount, fallback: double.nan);
if (parsedAmount.isNaN || parsedAmount <= 0) {
throw FormatException(
'CSV row ${i + 1}: amount must be greater than 0',
);

View File

@@ -1,6 +1,9 @@
import 'package:pshared/models/payment/operation.dart';
import 'package:pshared/models/payment/payment.dart';
import 'package:pshared/models/payment/status.dart';
import 'package:pshared/utils/money.dart';
import 'package:pweb/models/payment_state.dart';
OperationItem mapPaymentToOperation(Payment payment) {
@@ -8,9 +11,11 @@ OperationItem mapPaymentToOperation(Payment payment) {
final settlement = payment.lastQuote?.expectedSettlementAmount;
final amountMoney = debit ?? settlement;
final amount = _parseAmount(amountMoney?.amount);
final amount = parseMoneyAmount(amountMoney?.amount);
final currency = amountMoney?.currency ?? '';
final toAmount = settlement == null ? amount : _parseAmount(settlement.amount);
final toAmount = settlement == null
? amount
: parseMoneyAmount(settlement.amount);
final toCurrency = settlement?.currency ?? currency;
final payId = _firstNonEmpty([
@@ -48,14 +53,15 @@ OperationItem mapPaymentToOperation(Payment payment) {
}
OperationStatus statusFromPayment(Payment payment) {
final normalized = _normalizePaymentState(payment.state);
switch (normalized) {
case 'SUCCESS':
final state = paymentStateFromRaw(payment.state);
switch (state) {
case PaymentState.success:
return OperationStatus.success;
case 'FAILED':
case 'CANCELLED':
case PaymentState.failed:
case PaymentState.cancelled:
return OperationStatus.error;
default:
case PaymentState.processing:
case PaymentState.unknown:
return OperationStatus.processing;
}
}
@@ -102,16 +108,3 @@ String? _firstNonEmpty(List<String?> values) {
}
return null;
}
String _normalizePaymentState(String? raw) {
final trimmed = (raw ?? '').trim().toUpperCase();
if (trimmed.startsWith('PAYMENT_STATE_')) {
return trimmed.substring('PAYMENT_STATE_'.length);
}
return trimmed;
}
double _parseAmount(String? amount) {
if (amount == null || amount.trim().isEmpty) return 0;
return double.tryParse(amount) ?? 0;
}

View File

@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/money.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/localization.dart';
import 'package:intl/intl.dart';
String formatMoney(Money? money, {String fallback = '-'}) {
final amount = money?.amount.trim();
if (amount == null || amount.isEmpty) return fallback;
return '$amount ${money!.currency}';
}
String formatAmount(double amount, String currency, {String fallback = '-'}) {
final trimmed = currency.trim();
if (trimmed.isEmpty) return amountToString(amount);
final symbol = currencySymbolFromCode(trimmed);
final suffix = symbol ?? trimmed;
return '${amountToString(amount)} $suffix';
}
String formatDateLabel(BuildContext context, DateTime? date, {String fallback = '-'}) {
if (date == null || date.millisecondsSinceEpoch == 0) return fallback;
return dateTimeToLocalFormat(context, date.toLocal());
}
String formatLongDate(BuildContext context, DateTime? date, {String fallback = '-'}) {
if (date == null || date.millisecondsSinceEpoch == 0) return fallback;
final locale = Localizations.localeOf(context).toString();
final formatter = DateFormat('d MMMM y', locale);
return formatter.format(date.toLocal());
}
String collapseWhitespace(String value) {
return value.replaceAll(RegExp(r'\s+'), ' ').trim();
}