From d6a3a0cc5b3f967cc33968d8579b8557d346ce7e Mon Sep 17 00:00:00 2001 From: Arseni Date: Thu, 5 Mar 2026 15:48:52 +0300 Subject: [PATCH 1/5] solyanka iz fix for payout page design, ledger wallet now clickable --- .../pshared/lib/data/dto/payment/payment.dart | 4 +- .../data/mapper/payment/payment_response.dart | 4 +- .../pshared/lib/models/payment/payment.dart | 3 + .../pweb/lib/app/router/payout_shell.dart | 24 +-- .../operations/report_operations.dart | 57 +++++- .../operations/wallet_transactions.dart | 16 +- .../lib/controllers/payments/details.dart | 21 --- .../dashboard/buttons/balance/carousel.dart | 22 +-- .../dashboard/buttons/balance/ledger.dart | 95 +++++----- .../dashboard/buttons/balance/widget.dart | 5 + .../pweb/lib/pages/dashboard/dashboard.dart | 5 + .../pages/dashboard/payouts/amount/feild.dart | 5 +- .../lib/pages/dashboard/payouts/form.dart | 4 +- .../payouts/quote_status/widgets/card.dart | 37 ++-- .../wallet/edit/buttons/buttons.dart | 34 ++-- .../payout_page/wallet/edit/buttons/send.dart | 40 +++-- .../wallet/edit/buttons/top_up.dart | 24 +-- .../pages/payout_page/wallet/edit/fields.dart | 53 ++---- .../edit/fields/ledger/balance_formatter.dart | 44 +++++ .../edit/fields/ledger/balance_row.dart | 39 +++++ .../wallet/edit/fields/ledger/section.dart | 67 +++++++ .../fields/shared/copyable_value_row.dart | 40 +++++ .../edit/fields/wallet/wallet_section.dart | 57 ++++++ .../pages/payout_page/wallet/edit/header.dart | 19 +- .../pages/payout_page/wallet/edit/page.dart | 28 ++- .../payout_page/wallet/history/history.dart | 165 +++++++----------- .../lib/pages/report/details/content.dart | 4 +- .../pweb/lib/pages/report/details/page.dart | 11 -- .../details/sections/operations/section.dart | 4 +- .../report/details/summary_card/widget.dart | 16 +- .../lib/providers/wallet_transactions.dart | 19 +- 31 files changed, 596 insertions(+), 370 deletions(-) create mode 100644 frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart create mode 100644 frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/balance_row.dart create mode 100644 frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/section.dart create mode 100644 frontend/pweb/lib/pages/payout_page/wallet/edit/fields/shared/copyable_value_row.dart create mode 100644 frontend/pweb/lib/pages/payout_page/wallet/edit/fields/wallet/wallet_section.dart diff --git a/frontend/pshared/lib/data/dto/payment/payment.dart b/frontend/pshared/lib/data/dto/payment/payment.dart index 3c67c1fa..222d55f2 100644 --- a/frontend/pshared/lib/data/dto/payment/payment.dart +++ b/frontend/pshared/lib/data/dto/payment/payment.dart @@ -1,11 +1,11 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:pshared/data/dto/payment/operation.dart'; +import 'package:pshared/data/dto/payment/intent/payment.dart'; import 'package:pshared/data/dto/payment/payment_quote.dart'; part 'payment.g.dart'; - @JsonSerializable() class PaymentDTO { final String? paymentRef; @@ -13,6 +13,7 @@ class PaymentDTO { final String? state; final String? failureCode; final String? failureReason; + final PaymentIntentDTO? intent; final List operations; final PaymentQuoteDTO? lastQuote; final Map? metadata; @@ -24,6 +25,7 @@ class PaymentDTO { this.state, this.failureCode, this.failureReason, + this.intent, this.operations = const [], this.lastQuote, this.metadata, diff --git a/frontend/pshared/lib/data/mapper/payment/payment_response.dart b/frontend/pshared/lib/data/mapper/payment/payment_response.dart index d79fa17f..e4a7152a 100644 --- a/frontend/pshared/lib/data/mapper/payment/payment_response.dart +++ b/frontend/pshared/lib/data/mapper/payment/payment_response.dart @@ -1,10 +1,10 @@ import 'package:pshared/data/dto/payment/payment.dart'; +import 'package:pshared/data/mapper/payment/intent/payment.dart'; import 'package:pshared/data/mapper/payment/operation.dart'; import 'package:pshared/data/mapper/payment/quote.dart'; import 'package:pshared/models/payment/payment.dart'; import 'package:pshared/models/payment/state.dart'; - extension PaymentDTOMapper on PaymentDTO { Payment toDomain() => Payment( paymentRef: paymentRef, @@ -13,6 +13,7 @@ extension PaymentDTOMapper on PaymentDTO { orchestrationState: paymentOrchestrationStateFromValue(state), failureCode: failureCode, failureReason: failureReason, + intent: intent?.toDomain(), operations: operations.map((item) => item.toDomain()).toList(), lastQuote: lastQuote?.toDomain(), metadata: metadata, @@ -27,6 +28,7 @@ extension PaymentMapper on Payment { state: state ?? paymentOrchestrationStateToValue(orchestrationState), failureCode: failureCode, failureReason: failureReason, + intent: intent?.toDTO(), operations: operations.map((item) => item.toDTO()).toList(), lastQuote: lastQuote?.toDTO(), metadata: metadata, diff --git a/frontend/pshared/lib/models/payment/payment.dart b/frontend/pshared/lib/models/payment/payment.dart index 90dc2156..fa21e7b5 100644 --- a/frontend/pshared/lib/models/payment/payment.dart +++ b/frontend/pshared/lib/models/payment/payment.dart @@ -1,4 +1,5 @@ import 'package:pshared/models/payment/execution_operation.dart'; +import 'package:pshared/models/payment/intent.dart'; import 'package:pshared/models/payment/quote/quote.dart'; import 'package:pshared/models/payment/state.dart'; @@ -9,6 +10,7 @@ class Payment { final PaymentOrchestrationState orchestrationState; final String? failureCode; final String? failureReason; + final PaymentIntent? intent; final List operations; final PaymentQuote? lastQuote; final Map? metadata; @@ -21,6 +23,7 @@ class Payment { required this.orchestrationState, required this.failureCode, required this.failureReason, + this.intent, required this.operations, required this.lastQuote, required this.metadata, diff --git a/frontend/pweb/lib/app/router/payout_shell.dart b/frontend/pweb/lib/app/router/payout_shell.dart index e9cc92c6..83b50dff 100644 --- a/frontend/pweb/lib/app/router/payout_shell.dart +++ b/frontend/pweb/lib/app/router/payout_shell.dart @@ -6,6 +6,7 @@ import 'package:provider/provider.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pshared/controllers/payment/source.dart'; +import 'package:pshared/models/ledger/account.dart'; import 'package:pshared/models/payment/type.dart'; import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/provider/ledger.dart'; @@ -228,6 +229,7 @@ RouteBase payoutShellRoute() => ShellRoute( _startPayment(context, recipient: null, paymentType: type), onTopUp: (wallet) => _openWalletTopUp(context, wallet), onWalletTap: (wallet) => _openWalletEdit(context, wallet), + onLedgerTap: (account) => _openLedgerEdit(context, account), ), ), ), @@ -340,17 +342,9 @@ RouteBase payoutShellRoute() => ShellRoute( GoRoute( name: PayoutRoutes.editWallet, path: PayoutRoutes.editWalletPath, - pageBuilder: (context, state) { - final walletsProvider = context.read(); - final wallet = walletsProvider.selectedWallet; - final loc = AppLocalizations.of(context)!; - - return NoTransitionPage( - child: wallet != null - ? WalletEditPage(onBack: () => _popOrGo(context)) - : Center(child: Text(loc.noWalletSelected)), - ); - }, + pageBuilder: (context, state) => NoTransitionPage( + child: WalletEditPage(onBack: () => _popOrGo(context)), + ), ), GoRoute( name: PayoutRoutes.walletTopUp, @@ -389,10 +383,18 @@ void _openEditRecipient(BuildContext context, {required Recipient recipient}) { } void _openWalletEdit(BuildContext context, Wallet wallet) { + context.read().selectWallet(wallet); context.read().selectWallet(wallet); context.pushToEditWallet(); } +void _openLedgerEdit(BuildContext context, LedgerAccount account) { + context.read().selectLedgerByRef( + account.ledgerAccountRef, + ); + context.pushToEditWallet(); +} + void _openWalletTopUp(BuildContext context, Wallet wallet) { context.read().selectWallet(wallet); context.pushToWalletTopUp(); diff --git a/frontend/pweb/lib/controllers/operations/report_operations.dart b/frontend/pweb/lib/controllers/operations/report_operations.dart index 0ac5f1fe..e844b418 100644 --- a/frontend/pweb/lib/controllers/operations/report_operations.dart +++ b/frontend/pweb/lib/controllers/operations/report_operations.dart @@ -3,18 +3,24 @@ import 'dart:collection'; import 'package:flutter/material.dart'; import 'package:pshared/models/payment/operation.dart'; +import 'package:pshared/models/payment/payment.dart'; +import 'package:pshared/models/payment/source_type.dart'; import 'package:pshared/models/payment/status.dart'; import 'package:pshared/provider/payment/payments.dart'; import 'package:pweb/models/state/load_more_state.dart'; import 'package:pweb/utils/report/operations/operations.dart'; import 'package:pweb/utils/report/payment_mapper.dart'; +import 'package:pweb/utils/report/source_filter.dart'; class ReportOperationsController extends ChangeNotifier { PaymentsProvider? _payments; + PaymentSourceType? _sourceType; + Set _sourceRefs = const {}; DateTimeRange? _selectedRange; final Set _selectedStatuses = {}; + List _paymentItems = const []; List _operations = const []; List _filtered = const []; @@ -36,10 +42,20 @@ class ReportOperationsController extends ChangeNotifier { return LoadMoreState.hidden; } - void update(PaymentsProvider provider) { + void update( + PaymentsProvider provider, { + PaymentSourceType? sourceType, + String? sourceRef, + List? sourceRefs, + }) { if (!identical(_payments, provider)) { _payments = provider; } + _sourceType = sourceType; + final effectiveSourceRefs = + sourceRefs ?? + (sourceRef == null ? const [] : [sourceRef]); + _sourceRefs = _normalizeRefs(effectiveSourceRefs); _rebuildOperations(); } @@ -74,13 +90,16 @@ class ReportOperationsController extends ChangeNotifier { } void _rebuildOperations() { - final items = _payments?.payments ?? const []; - _operations = items.map(mapPaymentToOperation).toList(); + _paymentItems = _payments?.payments ?? const []; + _operations = _paymentItems + .where(_matchesCurrentSource) + .map(mapPaymentToOperation) + .toList(); _rebuildFiltered(notify: true); } void _rebuildFiltered({bool notify = true}) { - _filtered = _applyFilters(_operations); + _filtered = _applyFilters(sortOperations(_operations)); if (notify) { notifyListeners(); } @@ -88,13 +107,14 @@ class ReportOperationsController extends ChangeNotifier { List _applyFilters(List operations) { if (_selectedRange == null && _selectedStatuses.isEmpty) { - return sortOperations(operations); + return operations; } final filtered = operations.where((op) { final statusMatch = _selectedStatuses.isEmpty || _selectedStatuses.contains(op.status); - final dateMatch = _selectedRange == null || + final dateMatch = + _selectedRange == null || isUnknownDate(op.date) || (op.date.isAfter( _selectedRange!.start.subtract(const Duration(seconds: 1)), @@ -105,7 +125,30 @@ class ReportOperationsController extends ChangeNotifier { return statusMatch && dateMatch; }).toList(); - return sortOperations(filtered); + return filtered; + } + + bool _matchesCurrentSource(Payment payment) { + final sourceType = _sourceType; + if (sourceType == null || _sourceRefs.isEmpty) return true; + for (final sourceRef in _sourceRefs) { + if (paymentMatchesSource( + payment, + sourceType: sourceType, + sourceRef: sourceRef, + )) { + return true; + } + } + return false; + } + + Set _normalizeRefs(List refs) { + final normalized = refs + .map((value) => value.trim()) + .where((value) => value.isNotEmpty) + .toSet(); + return normalized; } bool _isSameRange(DateTimeRange? left, DateTimeRange? right) { diff --git a/frontend/pweb/lib/controllers/operations/wallet_transactions.dart b/frontend/pweb/lib/controllers/operations/wallet_transactions.dart index 2e7c5542..dac9006c 100644 --- a/frontend/pweb/lib/controllers/operations/wallet_transactions.dart +++ b/frontend/pweb/lib/controllers/operations/wallet_transactions.dart @@ -71,16 +71,24 @@ class WalletTransactionsController extends ChangeNotifier { void _rebuildFiltered({bool notify = true}) { final source = _provider?.transactions ?? const []; + final activeWalletId = _provider?.walletId; _filteredTransactions = source.where((tx) { + final walletMatch = + activeWalletId == null || tx.walletId == activeWalletId; final statusMatch = _selectedStatuses.isEmpty || _selectedStatuses.contains(tx.status); final typeMatch = _selectedTypes.isEmpty || _selectedTypes.contains(tx.type); - final dateMatch = _dateRange == null || - (tx.date.isAfter(_dateRange!.start.subtract(const Duration(seconds: 1))) && - tx.date.isBefore(_dateRange!.end.add(const Duration(seconds: 1)))); + final dateMatch = + _dateRange == null || + (tx.date.isAfter( + _dateRange!.start.subtract(const Duration(seconds: 1)), + ) && + tx.date.isBefore( + _dateRange!.end.add(const Duration(seconds: 1)), + )); - return statusMatch && typeMatch && dateMatch; + return walletMatch && statusMatch && typeMatch && dateMatch; }).toList(); if (notify) notifyListeners(); diff --git a/frontend/pweb/lib/controllers/payments/details.dart b/frontend/pweb/lib/controllers/payments/details.dart index 2258a203..71e7be0a 100644 --- a/frontend/pweb/lib/controllers/payments/details.dart +++ b/frontend/pweb/lib/controllers/payments/details.dart @@ -2,12 +2,10 @@ import 'package:flutter/foundation.dart'; import 'package:pshared/models/payment/execution_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/models/documents/operation.dart'; import 'package:pweb/utils/report/operations/document_rule.dart'; -import 'package:pweb/utils/report/payment_mapper.dart'; class PaymentDetailsController extends ChangeNotifier { PaymentDetailsController({required String paymentId}) @@ -22,25 +20,6 @@ class PaymentDetailsController extends ChangeNotifier { bool get isLoading => _payments?.isLoading ?? false; Exception? get error => _payments?.error; - bool get canDownload { - final current = _payment; - if (current == null) return false; - if (statusFromPayment(current) != OperationStatus.success) return false; - return primaryOperationDocumentRequest != null; - } - - OperationDocumentRequestModel? get primaryOperationDocumentRequest { - final current = _payment; - if (current == null) return null; - for (final operation in current.operations) { - final request = operationDocumentRequest(operation); - if (request != null) { - return request; - } - } - return null; - } - OperationDocumentRequestModel? operationDocumentRequest( PaymentExecutionOperation operation, ) { diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel.dart index da3f2dc6..84f2dd0e 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/gestures.dart'; +import 'package:pshared/models/ledger/account.dart'; import 'package:pshared/models/payment/wallet.dart'; import 'package:pweb/pages/dashboard/buttons/balance/add/card.dart'; @@ -10,13 +11,13 @@ import 'package:pweb/pages/dashboard/buttons/balance/config.dart'; import 'package:pweb/pages/dashboard/buttons/balance/indicator.dart'; import 'package:pweb/pages/dashboard/buttons/balance/ledger.dart'; - class BalanceCarousel extends StatefulWidget { final List items; final int currentIndex; final ValueChanged onIndexChanged; final ValueChanged onTopUp; final ValueChanged onWalletTap; + final ValueChanged onLedgerTap; const BalanceCarousel({ super.key, @@ -25,6 +26,7 @@ class BalanceCarousel extends StatefulWidget { required this.onIndexChanged, required this.onTopUp, required this.onWalletTap, + required this.onLedgerTap, }); @override @@ -105,7 +107,10 @@ class _BalanceCarouselState extends State { onTopUp: () => widget.onTopUp(item.wallet!), onTap: () => widget.onWalletTap(item.wallet!), ), - BalanceItemType.ledger => LedgerAccountCard(account: item.account!), + BalanceItemType.ledger => LedgerAccountCard( + account: item.account!, + onTap: () => widget.onLedgerTap(item.account!), + ), BalanceItemType.addAction => const AddBalanceCard(), }; @@ -123,19 +128,16 @@ class _BalanceCarouselState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( - onPressed: safeIndex > 0 - ? () => _goToPage(safeIndex - 1) - : null, + onPressed: safeIndex > 0 ? () => _goToPage(safeIndex - 1) : null, icon: const Icon(Icons.arrow_back), ), const SizedBox(width: 16), - CarouselIndicator( - itemCount: widget.items.length, - index: safeIndex, - ), + CarouselIndicator(itemCount: widget.items.length, index: safeIndex), const SizedBox(width: 16), IconButton( - onPressed: safeIndex < widget.items.length - 1 ? () => _goToPage(safeIndex + 1) : null, + onPressed: safeIndex < widget.items.length - 1 + ? () => _goToPage(safeIndex + 1) + : null, icon: const Icon(Icons.arrow_forward), ), ], diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger.dart index 5748c7e0..f69ffe59 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger.dart @@ -16,8 +16,9 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; class LedgerAccountCard extends StatelessWidget { final LedgerAccount account; + final VoidCallback? onTap; - const LedgerAccountCard({super.key, required this.account}); + const LedgerAccountCard({super.key, required this.account, this.onTap}); String _formatBalance() { final money = account.balance?.balance; @@ -73,50 +74,58 @@ class LedgerAccountCard extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius), ), - child: Padding( - padding: WalletCardConfig.contentPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BalanceHeader(title: title, subtitle: subtitle, badge: badge), - Row( - children: [ - Consumer( - builder: (context, controller, _) { - final isMasked = controller.isBalanceMasked( - account.ledgerAccountRef, - ); - return Row( - children: [ - Text( - isMasked ? _formatMaskedBalance() : _formatBalance(), - style: textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, + child: InkWell( + borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius), + onTap: onTap, + child: Padding( + padding: WalletCardConfig.contentPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BalanceHeader(title: title, subtitle: subtitle, badge: badge), + Row( + children: [ + Consumer( + builder: (context, controller, _) { + final isMasked = controller.isBalanceMasked( + account.ledgerAccountRef, + ); + return Row( + children: [ + Text( + isMasked + ? _formatMaskedBalance() + : _formatBalance(), + style: textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), ), - ), - const SizedBox(width: 12), - GestureDetector( - onTap: () => controller.toggleBalanceMask( - account.ledgerAccountRef, + const SizedBox(width: 12), + GestureDetector( + onTap: () => controller.toggleBalanceMask( + account.ledgerAccountRef, + ), + child: Icon( + isMasked + ? Icons.visibility_off + : Icons.visibility, + size: 24, + color: colorScheme.onSurface, + ), ), - child: Icon( - isMasked ? Icons.visibility_off : Icons.visibility, - size: 24, - color: colorScheme.onSurface, - ), - ), - ], - ); - }, - ), - const SizedBox(width: 12), - LedgerBalanceRefreshButton( - ledgerAccountRef: account.ledgerAccountRef, - ), - ], - ), - ], + ], + ); + }, + ), + const SizedBox(width: 12), + LedgerBalanceRefreshButton( + ledgerAccountRef: account.ledgerAccountRef, + ), + ], + ), + ], + ), ), ), ); diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/widget.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/widget.dart index a719d3a2..06f976e9 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/widget.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/widget.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart'; +import 'package:pshared/models/ledger/account.dart'; import 'package:pshared/provider/ledger.dart'; import 'package:pshared/models/payment/wallet.dart'; @@ -11,14 +12,17 @@ import 'package:pweb/pages/dashboard/buttons/balance/controller.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; + class BalanceWidget extends StatelessWidget { final ValueChanged onTopUp; final ValueChanged onWalletTap; + final ValueChanged onLedgerTap; const BalanceWidget({ super.key, required this.onTopUp, required this.onWalletTap, + required this.onLedgerTap, }); @override @@ -46,6 +50,7 @@ class BalanceWidget extends StatelessWidget { onIndexChanged: carousel.onPageChanged, onTopUp: onTopUp, onWalletTap: onWalletTap, + onLedgerTap: onLedgerTap, ); if (wallets.isEmpty && accounts.isEmpty) { diff --git a/frontend/pweb/lib/pages/dashboard/dashboard.dart b/frontend/pweb/lib/pages/dashboard/dashboard.dart index bc709b6d..8089857e 100644 --- a/frontend/pweb/lib/pages/dashboard/dashboard.dart +++ b/frontend/pweb/lib/pages/dashboard/dashboard.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:pshared/models/ledger/account.dart'; import 'package:pshared/models/payment/type.dart'; import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/models/payment/wallet.dart'; @@ -15,6 +16,7 @@ import 'package:pweb/pages/loader.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; + class AppSpacing { static const double small = 10; static const double medium = 16; @@ -26,6 +28,7 @@ class DashboardPage extends StatefulWidget { final void Function(PaymentType type) onGoToPaymentWithoutRecipient; final ValueChanged onTopUp; final ValueChanged onWalletTap; + final ValueChanged onLedgerTap; const DashboardPage({ super.key, @@ -33,6 +36,7 @@ class DashboardPage extends StatefulWidget { required this.onGoToPaymentWithoutRecipient, required this.onTopUp, required this.onWalletTap, + required this.onLedgerTap, }); @override @@ -87,6 +91,7 @@ class _DashboardPageState extends State { child: BalanceWidget( onTopUp: widget.onTopUp, onWalletTap: widget.onWalletTap, + onLedgerTap: widget.onLedgerTap, ), ), const SizedBox(height: AppSpacing.small), diff --git a/frontend/pweb/lib/pages/dashboard/payouts/amount/feild.dart b/frontend/pweb/lib/pages/dashboard/payouts/amount/feild.dart index 8c670ef9..70fc5cab 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/amount/feild.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/amount/feild.dart @@ -10,6 +10,7 @@ import 'package:pweb/pages/dashboard/payouts/amount/mode/selector.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; + class PaymentAmountField extends StatelessWidget { const PaymentAmountField(); @@ -37,10 +38,6 @@ class PaymentAmountField extends StatelessWidget { labelText: loc.amount, border: const OutlineInputBorder(), prefixText: symbol == null ? null : '$symbol\u00A0', - helperText: switch (ui.mode) { - PaymentAmountMode.debit => loc.debitAmountLabel, - PaymentAmountMode.settlement => loc.expectedSettlementAmountLabel, - }, ), onChanged: ui.handleChanged, ), diff --git a/frontend/pweb/lib/pages/dashboard/payouts/form.dart b/frontend/pweb/lib/pages/dashboard/payouts/form.dart index 4fa94eb8..f0ff3e93 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/form.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/form.dart @@ -102,7 +102,7 @@ class PaymentFormWidget extends StatelessWidget { children: [ detailsHeader, Row( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( flex: 3, @@ -114,7 +114,7 @@ class PaymentFormWidget extends StatelessWidget { ), const SizedBox(height: _smallSpacing), Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( flex: 3, diff --git a/frontend/pweb/lib/pages/dashboard/payouts/quote_status/widgets/card.dart b/frontend/pweb/lib/pages/dashboard/payouts/quote_status/widgets/card.dart index 738e92d8..d74537a5 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/quote_status/widgets/card.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/quote_status/widgets/card.dart @@ -26,15 +26,18 @@ class QuoteStatusCard extends StatelessWidget { }); static const double _cardRadius = 12; - static const double _cardSpacing = 12; + static const double _cardSpacing = 8; static const double _iconSize = 18; @override Widget build(BuildContext context) { final theme = Theme.of(context); + final loc = AppLocalizations.of(context)!; final foregroundColor = _resolveForegroundColor(theme, statusType); final elementColor = _resolveElementColor(theme, statusType); - final statusStyle = theme.textTheme.bodyMedium?.copyWith(color: elementColor); + final statusStyle = theme.textTheme.bodyMedium?.copyWith( + color: elementColor, + ); final helperStyle = theme.textTheme.bodySmall?.copyWith( color: foregroundColor.withValues(alpha: 0.8), ); @@ -44,12 +47,10 @@ class QuoteStatusCard extends StatelessWidget { decoration: BoxDecoration( color: _resolveCardColor(theme, statusType), borderRadius: BorderRadius.circular(_cardRadius), - border: Border.all( - color: elementColor.withValues(alpha: 0.5), - ), + border: Border.all(color: elementColor.withValues(alpha: 0.5)), ), child: Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Padding( padding: const EdgeInsets.only(top: 2), @@ -59,7 +60,9 @@ class QuoteStatusCard extends StatelessWidget { height: _iconSize, child: CircularProgressIndicator( strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(foregroundColor), + valueColor: AlwaysStoppedAnimation( + foregroundColor, + ), ), ) : Icon( @@ -81,19 +84,15 @@ class QuoteStatusCard extends StatelessWidget { ], ), ), - if (canRefresh) - Padding( - padding: const EdgeInsets.only(left: _cardSpacing), - child: showPrimaryRefresh - ? ElevatedButton( - onPressed: canRefresh ? onRefresh : null, - child: Text(AppLocalizations.of(context)!.quoteRefresh), - ) - : TextButton( - onPressed: canRefresh ? onRefresh : null, - child: Text(AppLocalizations.of(context)!.quoteRefresh), - ), + if (canRefresh) ...[ + const SizedBox(width: _cardSpacing), + IconButton( + onPressed: onRefresh, + tooltip: loc.quoteRefresh, + icon: const Icon(Icons.refresh), + color: showPrimaryRefresh ? foregroundColor : elementColor, ), + ], ], ), ); diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/buttons.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/buttons.dart index 4280a9b7..8f70a4e0 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/buttons.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/buttons.dart @@ -2,9 +2,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:pshared/controllers/payment/source.dart'; + import 'package:pweb/pages/payout_page/wallet/edit/buttons/send.dart'; import 'package:pweb/pages/payout_page/wallet/edit/buttons/top_up.dart'; -import 'package:pshared/provider/payment/wallets.dart'; class ButtonsWalletWidget extends StatelessWidget { @@ -12,25 +13,20 @@ class ButtonsWalletWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final provider = context.watch(); - - if (provider.wallets.isEmpty) return const SizedBox.shrink(); + final source = context.watch(); + if (!source.hasSources) return const SizedBox.shrink(); return Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Expanded( - child: SendPayoutButton(), - ), - VerticalDivider( - color: Theme.of(context).colorScheme.primary, - thickness: 1, - width: 10, - ), - Expanded( - child: TopUpButton(), - ), - ], + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded(child: SendPayoutButton()), + VerticalDivider( + color: Theme.of(context).colorScheme.primary, + thickness: 1, + width: 10, + ), + Expanded(child: TopUpButton()), + ], ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/send.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/send.dart index 819c2e42..2d1b17b8 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/send.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/send.dart @@ -4,7 +4,8 @@ import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/controllers/balance_mask/wallets.dart'; +import 'package:pshared/controllers/payment/source.dart'; +import 'package:pshared/models/payment/source_type.dart'; import 'package:pshared/models/payment/type.dart'; import 'package:pweb/app/router/payout_routes.dart'; @@ -18,24 +19,27 @@ class SendPayoutButton extends StatelessWidget { @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context)!; + final source = context.watch(); + + final sourceType = source.selectedType; + final paymentType = switch (sourceType) { + PaymentSourceType.wallet => PaymentType.wallet, + PaymentSourceType.ledger => PaymentType.ledger, + _ => null, + }; + return ElevatedButton( - style: ElevatedButton.styleFrom( - shadowColor: null, - elevation: 0, - ), - onPressed: () { - final wallets = context.read(); - final wallet = wallets.selectedWallet; - - if (wallet != null) { - context.pushNamed( - PayoutRoutes.payment, - queryParameters: PayoutRoutes.buildQueryParameters( - paymentType: PaymentType.wallet, - ), - ); - } - }, + style: ElevatedButton.styleFrom(shadowColor: null, elevation: 0), + onPressed: paymentType == null + ? null + : () { + context.pushNamed( + PayoutRoutes.payment, + queryParameters: PayoutRoutes.buildQueryParameters( + paymentType: paymentType, + ), + ); + }, child: Text(loc.payoutNavSendPayout), ); } diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart index 618075eb..772e766b 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart @@ -2,34 +2,26 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/controllers/balance_mask/wallets.dart'; +import 'package:pshared/controllers/payment/source.dart'; +import 'package:pshared/models/payment/source_type.dart'; import 'package:pweb/app/router/payout_routes.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; -class TopUpButton extends StatelessWidget{ +class TopUpButton extends StatelessWidget { const TopUpButton({super.key}); @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context)!; + final source = context.watch(); + final canTopUp = source.selectedType == PaymentSourceType.wallet; + return ElevatedButton( - style: ElevatedButton.styleFrom( - shadowColor: null, - elevation: 0, - ), - onPressed: () { - final wallet = context.read().selectedWallet; - if (wallet == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(loc.noWalletSelected)), - ); - return; - } - context.pushToWalletTopUp(); - }, + style: ElevatedButton.styleFrom(shadowColor: null, elevation: 0), + onPressed: canTopUp ? () => context.pushToWalletTopUp() : null, child: Text(loc.topUpBalance), ); } diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/fields.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields.dart index 530ad26e..6331396b 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/fields.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields.dart @@ -1,55 +1,30 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/controllers/balance_mask/wallets.dart'; - -import 'package:pweb/pages/dashboard/buttons/balance/amount.dart'; -import 'package:pweb/widgets/refresh_balance/wallet.dart'; +import 'package:pshared/controllers/payment/source.dart'; +import 'package:pweb/pages/payout_page/wallet/edit/fields/ledger/section.dart'; +import 'package:pweb/pages/payout_page/wallet/edit/fields/wallet/wallet_section.dart'; class WalletEditFields extends StatelessWidget { const WalletEditFields({super.key}); @override Widget build(BuildContext context) { - return Consumer( - builder: (context, controller, _) { - final wallet = controller.selectedWallet; - - if (wallet == null) { - return SizedBox.shrink(); + return Consumer( + builder: (context, sourceController, _) { + final wallet = sourceController.selectedWallet; + if (wallet != null) { + return WalletSection(wallet: wallet); } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: BalanceAmount( - wallet: wallet, - onToggleMask: () => controller.toggleBalanceMask(wallet.id), - ), - ), - WalletBalanceRefreshButton(walletRef: wallet.id), - ], - ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(wallet.walletUserID, style: Theme.of(context).textTheme.bodyLarge), - IconButton( - icon: Icon(Icons.copy), - iconSize: 18, - onPressed: () => Clipboard.setData(ClipboardData(text: wallet.walletUserID)), - ), - ], - ), - ], - ); + final ledger = sourceController.selectedLedgerAccount; + if (ledger != null) { + return LedgerSection(ledger: ledger); + } + + return const SizedBox.shrink(); }, ); } diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart new file mode 100644 index 00000000..85513ab6 --- /dev/null +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart @@ -0,0 +1,44 @@ +import 'package:pshared/models/ledger/account.dart'; +import 'package:pshared/utils/currency.dart'; +import 'package:pshared/utils/money.dart'; + + +class LedgerBalanceFormatter { + const LedgerBalanceFormatter._(); + + static String format(LedgerAccount account) { + final money = account.balance?.balance; + if (money == null) return '--'; + + final amount = parseMoneyAmount(money.amount, fallback: double.nan); + if (amount.isNaN) { + return '${money.amount} ${money.currency}'; + } + + try { + final currency = currencyStringToCode(money.currency); + final symbol = currencyCodeToSymbol(currency); + if (symbol.trim().isEmpty) { + return '${amountToString(amount)} ${money.currency}'; + } + return '${amountToString(amount)} $symbol'; + } catch (_) { + return '${amountToString(amount)} ${money.currency}'; + } + } + + static String formatMasked(LedgerAccount account) { + final currency = account.currency.trim(); + if (currency.isEmpty) return '••••'; + + try { + final symbol = currencyCodeToSymbol(currencyStringToCode(currency)); + if (symbol.trim().isEmpty) { + return '•••• $currency'; + } + return '•••• $symbol'; + } catch (_) { + return '•••• $currency'; + } + } +} diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/balance_row.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/balance_row.dart new file mode 100644 index 00000000..322c3944 --- /dev/null +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/balance_row.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + + +class LedgerBalanceRow extends StatelessWidget { + final String balance; + final bool isMasked; + final VoidCallback onToggleMask; + + const LedgerBalanceRow({ + super.key, + required this.balance, + required this.isMasked, + required this.onToggleMask, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Flexible( + child: Text( + balance, + style: Theme.of( + context, + ).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + ), + ), + const SizedBox(width: 8), + GestureDetector( + onTap: onToggleMask, + child: Icon( + isMasked ? Icons.visibility_off : Icons.visibility, + size: 22, + ), + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/section.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/section.dart new file mode 100644 index 00000000..3458dd43 --- /dev/null +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/section.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/controllers/balance_mask/ledger_accounts.dart'; +import 'package:pshared/models/ledger/account.dart'; + +import 'package:pweb/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart'; +import 'package:pweb/pages/payout_page/wallet/edit/fields/ledger/balance_row.dart'; +import 'package:pweb/pages/payout_page/wallet/edit/fields/shared/copyable_value_row.dart'; +import 'package:pweb/widgets/refresh_balance/ledger.dart'; + + +class LedgerSection extends StatelessWidget { + final LedgerAccount ledger; + + const LedgerSection({super.key, required this.ledger}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, balanceMask, _) { + final isMasked = balanceMask.isBalanceMasked(ledger.ledgerAccountRef); + final accountCode = ledger.accountCode.trim(); + final hasAccountCode = accountCode.isNotEmpty; + final balance = isMasked + ? LedgerBalanceFormatter.formatMasked(ledger) + : LedgerBalanceFormatter.format(ledger); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: LedgerBalanceRow( + balance: balance, + isMasked: isMasked, + onToggleMask: () { + balanceMask.toggleBalanceMask(ledger.ledgerAccountRef); + }, + ), + ), + LedgerBalanceRefreshButton( + ledgerAccountRef: ledger.ledgerAccountRef, + ), + ], + ), + const SizedBox(height: 8), + CopyableValueRow( + value: hasAccountCode ? accountCode : '-', + canCopy: hasAccountCode, + onCopy: hasAccountCode + ? () { + Clipboard.setData(ClipboardData(text: accountCode)); + } + : null, + overflow: TextOverflow.ellipsis, + wrapValueWithFlexible: true, + ), + ], + ); + }, + ); + } +} diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/shared/copyable_value_row.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/shared/copyable_value_row.dart new file mode 100644 index 00000000..cb2dbd9a --- /dev/null +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/shared/copyable_value_row.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + + +class CopyableValueRow extends StatelessWidget { + final String value; + final bool canCopy; + final VoidCallback? onCopy; + final TextOverflow overflow; + final bool wrapValueWithFlexible; + + const CopyableValueRow({ + super.key, + required this.value, + required this.canCopy, + required this.onCopy, + this.overflow = TextOverflow.visible, + this.wrapValueWithFlexible = false, + }); + + @override + Widget build(BuildContext context) { + final valueText = Text( + value, + style: Theme.of(context).textTheme.bodyLarge, + overflow: overflow, + ); + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (wrapValueWithFlexible) Flexible(child: valueText) else valueText, + IconButton( + icon: const Icon(Icons.copy), + iconSize: 18, + onPressed: canCopy ? onCopy : null, + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/wallet/wallet_section.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/wallet/wallet_section.dart new file mode 100644 index 00000000..fe34e23a --- /dev/null +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/wallet/wallet_section.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/controllers/balance_mask/wallets.dart'; +import 'package:pshared/models/payment/wallet.dart'; + +import 'package:pweb/pages/dashboard/buttons/balance/amount.dart'; +import 'package:pweb/pages/payout_page/wallet/edit/fields/shared/copyable_value_row.dart'; +import 'package:pweb/widgets/refresh_balance/wallet.dart'; + + +class WalletSection extends StatelessWidget { + final Wallet wallet; + + const WalletSection({super.key, required this.wallet}); + + @override + Widget build(BuildContext context) { + final depositAddress = wallet.depositAddress?.trim(); + final hasDepositAddress = + depositAddress != null && depositAddress.isNotEmpty; + final copyAddress = hasDepositAddress ? depositAddress : ''; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: BalanceAmount( + wallet: wallet, + onToggleMask: () { + context.read().toggleBalanceMask( + wallet.id, + ); + }, + ), + ), + WalletBalanceRefreshButton(walletRef: wallet.id), + ], + ), + const SizedBox(height: 8), + CopyableValueRow( + value: hasDepositAddress ? depositAddress : '-', + canCopy: hasDepositAddress, + onCopy: hasDepositAddress + ? () { + Clipboard.setData(ClipboardData(text: copyAddress)); + } + : null, + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/header.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/header.dart index a725efad..9e92bb8c 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/header.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/header.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/controllers/balance_mask/wallets.dart'; +import 'package:pshared/controllers/payment/source.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; @@ -12,15 +12,20 @@ class WalletEditHeader extends StatelessWidget { @override Widget build(BuildContext context) { - final controller = context.watch(); + final controller = context.watch(); final wallet = controller.selectedWallet; + final ledger = controller.selectedLedgerAccount; final loc = AppLocalizations.of(context)!; - if (wallet == null) { + if (wallet == null && ledger == null) { return const SizedBox.shrink(); } final theme = Theme.of(context); + final title = wallet != null + ? loc.paymentTypeCryptoWallet + : loc.paymentTypeLedger; + final subtitle = wallet?.tokenSymbol; return Row( spacing: 8, @@ -32,14 +37,14 @@ class WalletEditHeader extends StatelessWidget { spacing: 4, children: [ Text( - loc.paymentTypeCryptoWallet, - style: theme.textTheme.headlineMedium!.copyWith( + title, + style: theme.textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, ), ), - if (wallet.tokenSymbol != null) + if (subtitle != null && subtitle.trim().isNotEmpty) Text( - wallet.tokenSymbol!, + subtitle, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/page.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/page.dart index 949202d5..4328529d 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/page.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/page.dart @@ -2,17 +2,16 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/controllers/balance_mask/wallets.dart'; +import 'package:pshared/controllers/payment/source.dart'; import 'package:pweb/pages/payout_page/wallet/edit/buttons/buttons.dart'; import 'package:pweb/pages/payout_page/wallet/edit/fields.dart'; import 'package:pweb/pages/payout_page/wallet/edit/header.dart'; -import 'package:pweb/pages/payout_page/wallet/history/history.dart'; import 'package:pweb/utils/dimensions.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; - +//TODO make this page more generic and reusable class WalletEditPage extends StatelessWidget { final VoidCallback onBack; @@ -23,11 +22,11 @@ class WalletEditPage extends StatelessWidget { final dimensions = AppDimensions(); final loc = AppLocalizations.of(context)!; - return Consumer( + return Consumer( builder: (context, controller, child) { - final wallet = controller.selectedWallet; - - if (wallet == null) { + final sourceType = controller.selectedType; + + if (sourceType == null) { return Center(child: Text(loc.noWalletSelected)); } @@ -36,11 +35,15 @@ class WalletEditPage extends StatelessWidget { child: Column( children: [ ConstrainedBox( - constraints: BoxConstraints(maxWidth: dimensions.maxContentWidth), + constraints: BoxConstraints( + maxWidth: dimensions.maxContentWidth, + ), child: Material( elevation: dimensions.elevationSmall, color: Theme.of(context).colorScheme.onSecondary, - borderRadius: BorderRadius.circular(dimensions.borderRadiusMedium), + borderRadius: BorderRadius.circular( + dimensions.borderRadiusMedium, + ), child: Padding( padding: EdgeInsets.all(dimensions.paddingLarge), child: SingleChildScrollView( @@ -55,19 +58,12 @@ class WalletEditPage extends StatelessWidget { WalletEditFields(), const SizedBox(height: 24), ButtonsWalletWidget(), - const SizedBox(height: 24), ], ), ), ), ), ), - const SizedBox(height: 24), - Expanded( - child: SingleChildScrollView( - child: WalletHistory(wallet: wallet), - ), - ), ], ), ); diff --git a/frontend/pweb/lib/pages/payout_page/wallet/history/history.dart b/frontend/pweb/lib/pages/payout_page/wallet/history/history.dart index db656400..559e6b20 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/history/history.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/history/history.dart @@ -2,118 +2,87 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:pshared/models/payment/wallet.dart'; +import 'package:pshared/models/payment/source_type.dart'; +import 'package:pshared/provider/payment/payments.dart'; -import 'package:pweb/pages/payout_page/wallet/history/filters.dart'; -import 'package:pweb/pages/payout_page/wallet/history/table.dart'; -import 'package:pweb/controllers/operations/wallet_transactions.dart'; -import 'package:pweb/providers/wallet_transactions.dart'; +import 'package:pweb/controllers/operations/report_operations.dart'; +import 'package:pweb/models/state/load_more_state.dart'; +import 'package:pweb/pages/report/cards/list.dart'; +import 'package:pweb/pages/report/operations/actions.dart'; +import 'package:pweb/pages/report/operations/states/error.dart'; +import 'package:pweb/pages/report/operations/states/loading.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; -class WalletHistory extends StatefulWidget { - final Wallet wallet; +class WalletHistory extends StatelessWidget { + final String sourceRef; + final PaymentSourceType sourceType; + final List sourceRefs; - const WalletHistory({super.key, required this.wallet}); - - @override - State createState() => _WalletHistoryState(); -} - -class _WalletHistoryState extends State { - @override - void initState() { - super.initState(); - _load(); - } - - @override - void didUpdateWidget(covariant WalletHistory oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.wallet.id != widget.wallet.id) { - _load(); - } - } - - void _load() { - WidgetsBinding.instance.addPostFrameCallback((_) { - context - .read() - .load(walletId: widget.wallet.id); - }); - } - - Future _pickRange() async { - final provider = context.read(); - final now = DateTime.now(); - final initial = provider.dateRange ?? - DateTimeRange( - start: now.subtract(const Duration(days: 30)), - end: now, - ); - - final picked = await showDateRangePicker( - context: context, - firstDate: now.subtract(const Duration(days: 365)), - lastDate: now.add(const Duration(days: 1)), - initialDateRange: initial, - ); - - if (picked != null) { - provider.setDateRange(picked); - } - } + const WalletHistory({ + super.key, + required this.sourceRef, + required this.sourceType, + required this.sourceRefs, + }); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProxyProvider< + PaymentsProvider, + ReportOperationsController + >( + create: (_) => ReportOperationsController(), + update: (_, payments, controller) => controller! + ..update( + payments, + sourceType: sourceType, + sourceRef: sourceRef, + sourceRefs: sourceRefs, + ), + child: const _WalletHistoryContent(), + ); + } +} + +class _WalletHistoryContent extends StatelessWidget { + const _WalletHistoryContent(); @override Widget build(BuildContext context) { - final theme = Theme.of(context); final loc = AppLocalizations.of(context)!; - return Consumer( - builder: (context, provider, child) { - if (provider.isLoading) { - return const Padding( - padding: EdgeInsets.all(16.0), - child: Center(child: CircularProgressIndicator()), + return Consumer( + builder: (context, controller, child) { + if (controller.isLoading) { + return const OperationHistoryLoading(); + } + + if (controller.error != null) { + final message = + controller.error?.toString() ?? loc.noErrorInformation; + return OperationHistoryError( + message: loc.notificationError(message), + retryLabel: loc.retry, + onRetry: controller.refresh, ); } - if (provider.error != null) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - loc.failedToLoadHistory, - style: theme.textTheme.titleMedium! - .copyWith(color: theme.colorScheme.error), - ), - const SizedBox(height: 8), - Text(loc.notificationError(provider.error ?? loc.noErrorInformation)), - const SizedBox(height: 8), - OutlinedButton( - onPressed: _load, - child: Text(loc.retry), - ), - ], - ), - ); - } - - final transactions = provider.filteredTransactions; - - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - WalletHistoryFilters( - provider: provider, - onPickRange: _pickRange, - ), - const SizedBox(height: 12), - WalletTransactionsTable(transactions: transactions), - ], + final hasLoadMore = controller.loadMoreState != LoadMoreState.hidden; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + OperationsCardsList( + operations: controller.filteredOperations, + onTap: (operation) => openPaymentDetails(context, operation), + loadMoreState: controller.loadMoreState, + onLoadMore: hasLoadMore ? controller.loadMore : null, + ), + ], + ), ); }, ); diff --git a/frontend/pweb/lib/pages/report/details/content.dart b/frontend/pweb/lib/pages/report/details/content.dart index d882b0b6..6e280ebf 100644 --- a/frontend/pweb/lib/pages/report/details/content.dart +++ b/frontend/pweb/lib/pages/report/details/content.dart @@ -13,7 +13,6 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; class PaymentDetailsContent extends StatelessWidget { final Payment payment; final VoidCallback onBack; - final VoidCallback? onDownloadAct; final bool Function(PaymentExecutionOperation operation)? canDownloadOperationDocument; final ValueChanged? onDownloadOperationDocument; @@ -22,7 +21,6 @@ class PaymentDetailsContent extends StatelessWidget { super.key, required this.payment, required this.onBack, - this.onDownloadAct, this.canDownloadOperationDocument, this.onDownloadOperationDocument, }); @@ -37,7 +35,7 @@ class PaymentDetailsContent extends StatelessWidget { children: [ PaymentDetailsHeader(title: loc.paymentInfo, onBack: onBack), const SizedBox(height: 16), - PaymentSummaryCard(payment: payment, onDownloadAct: onDownloadAct), + PaymentSummaryCard(payment: payment), const SizedBox(height: 16), PaymentDetailsSections( payment: payment, diff --git a/frontend/pweb/lib/pages/report/details/page.dart b/frontend/pweb/lib/pages/report/details/page.dart index 998d2b3b..0e529d03 100644 --- a/frontend/pweb/lib/pages/report/details/page.dart +++ b/frontend/pweb/lib/pages/report/details/page.dart @@ -64,17 +64,6 @@ class _PaymentDetailsView extends StatelessWidget { return PaymentDetailsContent( payment: payment, onBack: () => _handleBack(context), - onDownloadAct: controller.canDownload - ? () { - final request = controller.primaryOperationDocumentRequest; - if (request == null) return; - downloadPaymentAct( - context, - gatewayService: request.gatewayService, - operationRef: request.operationRef, - ); - } - : null, canDownloadOperationDocument: controller.canDownloadOperationDocument, onDownloadOperationDocument: (operation) { diff --git a/frontend/pweb/lib/pages/report/details/sections/operations/section.dart b/frontend/pweb/lib/pages/report/details/sections/operations/section.dart index 792907cf..0f993f5b 100644 --- a/frontend/pweb/lib/pages/report/details/sections/operations/section.dart +++ b/frontend/pweb/lib/pages/report/details/sections/operations/section.dart @@ -44,12 +44,12 @@ class PaymentOperationsSection extends StatelessWidget { ); if (i < operations.length - 1) { children.addAll([ - const SizedBox(height: 8), + const SizedBox(height: 10), Divider( height: 1, color: Theme.of(context).dividerColor.withAlpha(20), ), - const SizedBox(height: 8), + const SizedBox(height: 10), ]); } } diff --git a/frontend/pweb/lib/pages/report/details/summary_card/widget.dart b/frontend/pweb/lib/pages/report/details/summary_card/widget.dart index ae3c19f8..45af884e 100644 --- a/frontend/pweb/lib/pages/report/details/summary_card/widget.dart +++ b/frontend/pweb/lib/pages/report/details/summary_card/widget.dart @@ -14,15 +14,11 @@ import 'package:pweb/utils/clipboard.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; + class PaymentSummaryCard extends StatelessWidget { final Payment payment; - final VoidCallback? onDownloadAct; - const PaymentSummaryCard({ - super.key, - required this.payment, - this.onDownloadAct, - }); + const PaymentSummaryCard({super.key, required this.payment}); @override Widget build(BuildContext context) { @@ -93,14 +89,6 @@ class PaymentSummaryCard extends StatelessWidget { text: feeText, muted: true, ), - if (onDownloadAct != null) ...[ - const SizedBox(height: 12), - OutlinedButton.icon( - onPressed: onDownloadAct, - icon: const Icon(Icons.download), - label: Text(loc.downloadAct), - ), - ], if (showPaymentId) ...[ const SizedBox(height: 16), Divider(color: theme.dividerColor.withAlpha(35), height: 1), diff --git a/frontend/pweb/lib/providers/wallet_transactions.dart b/frontend/pweb/lib/providers/wallet_transactions.dart index e9053e1a..7d153dbd 100644 --- a/frontend/pweb/lib/providers/wallet_transactions.dart +++ b/frontend/pweb/lib/providers/wallet_transactions.dart @@ -13,6 +13,7 @@ class WalletTransactionsProvider extends ChangeNotifier { bool _isLoading = false; String? _error; String? _walletId; + int _loadSeq = 0; List get transactions => List.unmodifiable(_transactions); bool get isLoading => _isLoading; @@ -20,18 +21,28 @@ class WalletTransactionsProvider extends ChangeNotifier { String? get walletId => _walletId; Future load({String? walletId}) async { + final targetWalletId = walletId ?? _walletId; + final requestSeq = ++_loadSeq; + _walletId = targetWalletId; _isLoading = true; _error = null; notifyListeners(); try { - _walletId = walletId ?? _walletId; - _transactions = await _service.fetchHistory(walletId: _walletId); + final fetched = await _service.fetchHistory(walletId: targetWalletId); + if (requestSeq != _loadSeq) return; + + _transactions = targetWalletId == null + ? fetched + : fetched.where((tx) => tx.walletId == targetWalletId).toList(); } catch (e) { + if (requestSeq != _loadSeq) return; _error = e.toString(); } finally { - _isLoading = false; - notifyListeners(); + if (requestSeq == _loadSeq) { + _isLoading = false; + notifyListeners(); + } } } } -- 2.49.1 From 97b16542c2163c0916ca0465c4c8ce9eb8362489 Mon Sep 17 00:00:00 2001 From: Arseni Date: Thu, 5 Mar 2026 21:49:23 +0300 Subject: [PATCH 2/5] ledger top up functionality and few small fixes for project architechture and design --- .../mongo/store/treasury_telegram_users.go | 87 ------------ .../models/payment/operation_document.dart} | 8 +- .../pweb/lib/app/router/payout_routes.dart | 35 +++-- .../pweb/lib/app/router/payout_shell.dart | 17 +++ .../lib/controllers/payments/details.dart | 7 +- frontend/pweb/lib/l10n/en.arb | 2 +- frontend/pweb/lib/l10n/ru.arb | 2 +- .../lib/models/dashboard/balance_item.dart | 26 ++++ .../pweb/lib/models/documents/operation.dart | 9 -- .../lib/models/payment/payment_state.dart | 27 ---- .../buttons/balance/balance_item.dart | 21 --- .../pages/dashboard/buttons/balance/card.dart | 65 +-------- .../dashboard/buttons/balance/carousel.dart | 24 ++-- .../dashboard/buttons/balance/controller.dart | 29 ++-- .../dashboard/buttons/balance/ledger.dart | 130 ++---------------- .../buttons/balance/ledger_amount.dart | 55 ++++++++ .../balance/source/actions/ledger.dart | 44 ++++++ .../balance/source/actions/wallet.dart | 42 ++++++ .../buttons/balance/source/card.dart | 99 +++++++++++++ .../buttons/balance/source/card_layout.dart | 61 ++++++++ .../dashboard/buttons/balance/widget.dart | 4 +- .../pweb/lib/pages/dashboard/dashboard.dart | 3 + .../pages/dashboard/payouts/amount/feild.dart | 1 - .../dashboard/payouts/multiple/actions.dart | 2 +- .../payouts/single/address_book/avatar.dart | 4 +- .../single/address_book/short_list.dart | 64 ++++----- .../payouts/single/address_book/widget.dart | 52 +++---- .../pweb/lib/pages/payout_page/send/page.dart | 10 +- .../lib/pages/payout_page/send/page_view.dart | 7 +- .../widgets/payment_info/manual_details.dart | 51 +++++++ .../send/widgets/payment_info/section.dart | 13 +- .../send/widgets/recipient/section.dart | 2 +- .../send/widgets/recipient_details_card.dart | 37 +++-- .../wallet/edit/buttons/top_up.dart | 32 ++++- .../pweb/lib/providers/multiple_payouts.dart | 5 +- .../csv_parser.dart} | 0 .../intent_builder.dart} | 0 .../send => utils/payment}/page_handlers.dart | 23 +++- ...ation_flow.dart => verification_flow.dart} | 0 .../pweb/lib/utils/report/payment_mapper.dart | 7 +- .../pweb/lib/utils/report/source_filter.dart | 112 +++++++++++++++ 41 files changed, 764 insertions(+), 455 deletions(-) delete mode 100644 api/gateway/tgsettle/storage/mongo/store/treasury_telegram_users.go rename frontend/{pweb/lib/models/report/operation/document.dart => pshared/lib/models/payment/operation_document.dart} (68%) create mode 100644 frontend/pweb/lib/models/dashboard/balance_item.dart delete mode 100644 frontend/pweb/lib/models/documents/operation.dart delete mode 100644 frontend/pweb/lib/models/payment/payment_state.dart delete mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/balance_item.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/ledger_amount.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/ledger.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/wallet.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/source/card.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/source/card_layout.dart create mode 100644 frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/manual_details.dart rename frontend/pweb/lib/utils/payment/{multiple_csv_parser.dart => multiple/csv_parser.dart} (100%) rename frontend/pweb/lib/utils/payment/{multiple_intent_builder.dart => multiple/intent_builder.dart} (100%) rename frontend/pweb/lib/{pages/payout_page/send => utils/payment}/page_handlers.dart (82%) rename frontend/pweb/lib/utils/payment/{payout_verification_flow.dart => verification_flow.dart} (100%) create mode 100644 frontend/pweb/lib/utils/report/source_filter.dart diff --git a/api/gateway/tgsettle/storage/mongo/store/treasury_telegram_users.go b/api/gateway/tgsettle/storage/mongo/store/treasury_telegram_users.go deleted file mode 100644 index 04c4e597..00000000 --- a/api/gateway/tgsettle/storage/mongo/store/treasury_telegram_users.go +++ /dev/null @@ -1,87 +0,0 @@ -package store - -import ( - "context" - "errors" - "strings" - - "github.com/tech/sendico/gateway/tgsettle/storage" - "github.com/tech/sendico/gateway/tgsettle/storage/model" - "github.com/tech/sendico/pkg/db/repository" - ri "github.com/tech/sendico/pkg/db/repository/index" - "github.com/tech/sendico/pkg/merrors" - "github.com/tech/sendico/pkg/mlogger" - "go.mongodb.org/mongo-driver/v2/mongo" - "go.uber.org/zap" -) - -const ( - treasuryTelegramUsersCollection = "treasury_telegram_users" - fieldTreasuryTelegramUserID = "telegramUserId" -) - -type TreasuryTelegramUsers struct { - logger mlogger.Logger - repo repository.Repository -} - -func NewTreasuryTelegramUsers(logger mlogger.Logger, db *mongo.Database) (*TreasuryTelegramUsers, error) { - if db == nil { - return nil, merrors.InvalidArgument("mongo database is nil") - } - if logger == nil { - logger = zap.NewNop() - } - logger = logger.Named("treasury_telegram_users").With(zap.String("collection", treasuryTelegramUsersCollection)) - - repo := repository.CreateMongoRepository(db, treasuryTelegramUsersCollection) - if err := repo.CreateIndex(&ri.Definition{ - Keys: []ri.Key{{Field: fieldTreasuryTelegramUserID, Sort: ri.Asc}}, - Unique: true, - }); err != nil { - logger.Error("Failed to create treasury telegram users user_id index", zap.Error(err), zap.String("index_field", fieldTreasuryTelegramUserID)) - return nil, err - } - - return &TreasuryTelegramUsers{ - logger: logger, - repo: repo, - }, nil -} - -func (t *TreasuryTelegramUsers) FindByTelegramUserID(ctx context.Context, telegramUserID string) (*model.TreasuryTelegramUser, error) { - telegramUserID = strings.TrimSpace(telegramUserID) - if telegramUserID == "" { - return nil, merrors.InvalidArgument("telegram_user_id is required", "telegram_user_id") - } - var result model.TreasuryTelegramUser - err := t.repo.FindOneByFilter(ctx, repository.Filter(fieldTreasuryTelegramUserID, telegramUserID), &result) - if errors.Is(err, merrors.ErrNoData) { - return nil, nil - } - if err != nil { - if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { - t.logger.Warn("Failed to load treasury telegram user", zap.Error(err), zap.String("telegram_user_id", telegramUserID)) - } - return nil, err - } - result.TelegramUserID = strings.TrimSpace(result.TelegramUserID) - result.LedgerAccountID = strings.TrimSpace(result.LedgerAccountID) - if len(result.AllowedChatIDs) > 0 { - normalized := make([]string, 0, len(result.AllowedChatIDs)) - for _, next := range result.AllowedChatIDs { - next = strings.TrimSpace(next) - if next == "" { - continue - } - normalized = append(normalized, next) - } - result.AllowedChatIDs = normalized - } - if result.TelegramUserID == "" || result.LedgerAccountID == "" { - return nil, nil - } - return &result, nil -} - -var _ storage.TreasuryTelegramUsersStore = (*TreasuryTelegramUsers)(nil) diff --git a/frontend/pweb/lib/models/report/operation/document.dart b/frontend/pshared/lib/models/payment/operation_document.dart similarity index 68% rename from frontend/pweb/lib/models/report/operation/document.dart rename to frontend/pshared/lib/models/payment/operation_document.dart index 9379305e..9712a046 100644 --- a/frontend/pweb/lib/models/report/operation/document.dart +++ b/frontend/pshared/lib/models/payment/operation_document.dart @@ -1,9 +1,9 @@ -class OperationDocumentInfo { - final String operationRef; +class OperationDocumentRef { final String gatewayService; + final String operationRef; - const OperationDocumentInfo({ - required this.operationRef, + const OperationDocumentRef({ required this.gatewayService, + required this.operationRef, }); } diff --git a/frontend/pweb/lib/app/router/payout_routes.dart b/frontend/pweb/lib/app/router/payout_routes.dart index f1e69a33..60543fb9 100644 --- a/frontend/pweb/lib/app/router/payout_routes.dart +++ b/frontend/pweb/lib/app/router/payout_routes.dart @@ -25,6 +25,7 @@ class PayoutRoutes { static const walletTopUp = 'payout-wallet-top-up'; static const paymentTypeQuery = 'paymentType'; + static const destinationLedgerAccountRefQuery = 'destinationLedgerAccountRef'; static const reportPaymentIdQuery = 'paymentId'; static const dashboardPath = '/dashboard'; @@ -40,7 +41,6 @@ class PayoutRoutes { static const editWalletPath = '/methods/edit'; static const walletTopUpPath = '/wallet/top-up'; - static String nameFor(PayoutDestination destination) { switch (destination) { case PayoutDestination.dashboard: @@ -126,9 +126,13 @@ class PayoutRoutes { static Map buildQueryParameters({ PaymentType? paymentType, + String? destinationLedgerAccountRef, }) { final params = { if (paymentType != null) paymentTypeQuery: paymentType.name, + if (destinationLedgerAccountRef != null && + destinationLedgerAccountRef.trim().isNotEmpty) + destinationLedgerAccountRefQuery: destinationLedgerAccountRef.trim(), }; return params; } @@ -140,35 +144,44 @@ class PayoutRoutes { ? null : PaymentType.values.firstWhereOrNull((type) => type.name == raw); + static String? destinationLedgerAccountRefFromState(GoRouterState state) => + destinationLedgerAccountRefFromRaw( + state.uri.queryParameters[destinationLedgerAccountRefQuery], + ); + + static String? destinationLedgerAccountRefFromRaw(String? raw) { + final value = raw?.trim(); + if (value == null || value.isEmpty) return null; + return value; + } } extension PayoutNavigation on BuildContext { - void goToPayout(PayoutDestination destination) => goNamed(PayoutRoutes.nameFor(destination)); + void goToPayout(PayoutDestination destination) => + goNamed(PayoutRoutes.nameFor(destination)); - void pushToPayout(PayoutDestination destination) => pushNamed(PayoutRoutes.nameFor(destination)); + void pushToPayout(PayoutDestination destination) => + pushNamed(PayoutRoutes.nameFor(destination)); void goToPayment({ PaymentType? paymentType, - }) => - goNamed( + String? destinationLedgerAccountRef, + }) => goNamed( PayoutRoutes.payment, queryParameters: PayoutRoutes.buildQueryParameters( paymentType: paymentType, + destinationLedgerAccountRef: destinationLedgerAccountRef, ), ); void goToReportPayment(String paymentId) => goNamed( PayoutRoutes.reportPayment, - queryParameters: { - PayoutRoutes.reportPaymentIdQuery: paymentId, - }, + queryParameters: {PayoutRoutes.reportPaymentIdQuery: paymentId}, ); void pushToReportPayment(String paymentId) => pushNamed( PayoutRoutes.reportPayment, - queryParameters: { - PayoutRoutes.reportPaymentIdQuery: paymentId, - }, + queryParameters: {PayoutRoutes.reportPaymentIdQuery: paymentId}, ); void pushToWalletTopUp() => pushNamed(PayoutRoutes.walletTopUp); diff --git a/frontend/pweb/lib/app/router/payout_shell.dart b/frontend/pweb/lib/app/router/payout_shell.dart index 83b50dff..768b84b2 100644 --- a/frontend/pweb/lib/app/router/payout_shell.dart +++ b/frontend/pweb/lib/app/router/payout_shell.dart @@ -228,6 +228,7 @@ RouteBase payoutShellRoute() => ShellRoute( onGoToPaymentWithoutRecipient: (type) => _startPayment(context, recipient: null, paymentType: type), onTopUp: (wallet) => _openWalletTopUp(context, wallet), + onLedgerAddFunds: (account) => _openLedgerAddFunds(context, account), onWalletTap: (wallet) => _openWalletEdit(context, wallet), onLedgerTap: (account) => _openLedgerEdit(context, account), ), @@ -306,6 +307,8 @@ RouteBase payoutShellRoute() => ShellRoute( child: PaymentPage( onBack: (_) => _popOrGo(context), initialPaymentType: PayoutRoutes.paymentTypeFromState(state), + initialDestinationLedgerAccountRef: + PayoutRoutes.destinationLedgerAccountRefFromState(state), fallbackDestination: fallbackDestination, ), ); @@ -395,6 +398,20 @@ void _openLedgerEdit(BuildContext context, LedgerAccount account) { context.pushToEditWallet(); } +void _openLedgerAddFunds(BuildContext context, LedgerAccount account) { + context.read().selectLedgerByRef( + account.ledgerAccountRef, + ); + context.read().setCurrentObject(null); + context.pushNamed( + PayoutRoutes.payment, + queryParameters: PayoutRoutes.buildQueryParameters( + paymentType: PaymentType.ledger, + destinationLedgerAccountRef: account.ledgerAccountRef, + ), + ); +} + void _openWalletTopUp(BuildContext context, Wallet wallet) { context.read().selectWallet(wallet); context.pushToWalletTopUp(); diff --git a/frontend/pweb/lib/controllers/payments/details.dart b/frontend/pweb/lib/controllers/payments/details.dart index 71e7be0a..1c31439a 100644 --- a/frontend/pweb/lib/controllers/payments/details.dart +++ b/frontend/pweb/lib/controllers/payments/details.dart @@ -1,12 +1,13 @@ import 'package:flutter/foundation.dart'; +import 'package:pshared/models/payment/operation_document.dart'; import 'package:pshared/models/payment/execution_operation.dart'; import 'package:pshared/models/payment/payment.dart'; import 'package:pshared/provider/payment/payments.dart'; -import 'package:pweb/models/documents/operation.dart'; import 'package:pweb/utils/report/operations/document_rule.dart'; + class PaymentDetailsController extends ChangeNotifier { PaymentDetailsController({required String paymentId}) : _paymentId = paymentId; @@ -20,7 +21,7 @@ class PaymentDetailsController extends ChangeNotifier { bool get isLoading => _payments?.isLoading ?? false; Exception? get error => _payments?.error; - OperationDocumentRequestModel? operationDocumentRequest( + OperationDocumentRef? operationDocumentRequest( PaymentExecutionOperation operation, ) { final current = _payment; @@ -33,7 +34,7 @@ class PaymentDetailsController extends ChangeNotifier { if (!isOperationDocumentEligible(operation.code)) return null; - return OperationDocumentRequestModel( + return OperationDocumentRef( gatewayService: gatewayService, operationRef: operationRef, ); diff --git a/frontend/pweb/lib/l10n/en.arb b/frontend/pweb/lib/l10n/en.arb index 6c37a488..dbc8fee2 100644 --- a/frontend/pweb/lib/l10n/en.arb +++ b/frontend/pweb/lib/l10n/en.arb @@ -638,7 +638,7 @@ } } }, - "noFee": "No fee", + "noFee": "None", "recipientWillReceive": "Recipient will receive: {amount}", "@recipientWillReceive": { diff --git a/frontend/pweb/lib/l10n/ru.arb b/frontend/pweb/lib/l10n/ru.arb index a1777eb1..ef8b6f04 100644 --- a/frontend/pweb/lib/l10n/ru.arb +++ b/frontend/pweb/lib/l10n/ru.arb @@ -638,7 +638,7 @@ } } }, - "noFee": "Нет комиссии", + "noFee": "Без оплаты", "recipientWillReceive": "Получатель получит: {amount}", "@recipientWillReceive": { diff --git a/frontend/pweb/lib/models/dashboard/balance_item.dart b/frontend/pweb/lib/models/dashboard/balance_item.dart new file mode 100644 index 00000000..713dc8fe --- /dev/null +++ b/frontend/pweb/lib/models/dashboard/balance_item.dart @@ -0,0 +1,26 @@ +import 'package:pshared/models/ledger/account.dart'; +import 'package:pshared/models/payment/wallet.dart'; + +sealed class BalanceItem { + const BalanceItem(); + + const factory BalanceItem.wallet(Wallet wallet) = WalletBalanceItem; + const factory BalanceItem.ledger(LedgerAccount account) = LedgerBalanceItem; + const factory BalanceItem.addAction() = AddBalanceActionItem; +} + +final class WalletBalanceItem extends BalanceItem { + final Wallet wallet; + + const WalletBalanceItem(this.wallet); +} + +final class LedgerBalanceItem extends BalanceItem { + final LedgerAccount account; + + const LedgerBalanceItem(this.account); +} + +final class AddBalanceActionItem extends BalanceItem { + const AddBalanceActionItem(); +} diff --git a/frontend/pweb/lib/models/documents/operation.dart b/frontend/pweb/lib/models/documents/operation.dart deleted file mode 100644 index c669d04c..00000000 --- a/frontend/pweb/lib/models/documents/operation.dart +++ /dev/null @@ -1,9 +0,0 @@ -class OperationDocumentRequestModel { - final String gatewayService; - final String operationRef; - - const OperationDocumentRequestModel({ - required this.gatewayService, - required this.operationRef, - }); -} diff --git a/frontend/pweb/lib/models/payment/payment_state.dart b/frontend/pweb/lib/models/payment/payment_state.dart deleted file mode 100644 index 814abf3c..00000000 --- a/frontend/pweb/lib/models/payment/payment_state.dart +++ /dev/null @@ -1,27 +0,0 @@ -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; - } -} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/balance_item.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/balance_item.dart deleted file mode 100644 index 0e992d8a..00000000 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/balance_item.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:pshared/models/ledger/account.dart'; -import 'package:pshared/models/payment/wallet.dart'; - - -enum BalanceItemType { wallet, ledger, addAction } - -class BalanceItem { - final BalanceItemType type; - final Wallet? wallet; - final LedgerAccount? account; - - const BalanceItem.wallet(this.wallet) : type = BalanceItemType.wallet, account = null; - - const BalanceItem.ledger(this.account) : type = BalanceItemType.ledger, wallet = null; - - const BalanceItem.addAction() : type = BalanceItemType.addAction, wallet = null, account = null; - - bool get isWallet => type == BalanceItemType.wallet; - bool get isLedger => type == BalanceItemType.ledger; - bool get isAdd => type == BalanceItemType.addAction; -} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/card.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/card.dart index 55602f4c..a23e98df 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/card.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/card.dart @@ -1,17 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pshared/models/payment/wallet.dart'; -import 'package:pshared/models/payment/chain_network.dart'; -import 'package:pshared/utils/l10n/chain.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/add_funds.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/amount.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/config.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/header.dart'; -import 'package:pweb/widgets/refresh_balance/wallet.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/source/card.dart'; class WalletCard extends StatelessWidget { @@ -28,56 +19,10 @@ class WalletCard extends StatelessWidget { @override Widget build(BuildContext context) { - final networkLabel = (wallet.network == null || wallet.network == ChainNetwork.unspecified) - ? null - : wallet.network!.localizedName(context); - final symbol = wallet.tokenSymbol?.trim(); - - return Card( - color: Theme.of(context).colorScheme.onSecondary, - elevation: WalletCardConfig.elevation, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius), - ), - child: InkWell( - borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius), - onTap: onTap, - child: SizedBox.expand( - child: Padding( - padding: WalletCardConfig.contentPadding, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BalanceHeader( - title: wallet.name, - subtitle: networkLabel, - badge: (symbol == null || symbol.isEmpty) ? null : symbol, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - BalanceAmount( - wallet: wallet, - onToggleMask: () { - context.read().toggleBalanceMask(wallet.id); - }, - ), - Column( - children: [ - WalletBalanceRefreshButton( - walletRef: wallet.id, - ), - BalanceAddFunds(onTopUp: onTopUp), - ], - ), - ], - ), - ], - ), - ), - ), - ), + return BalanceSourceCard.wallet( + wallet: wallet, + onTap: onTap, + onAddFunds: onTopUp, ); } } diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel.dart index 84f2dd0e..999aba51 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel.dart @@ -4,18 +4,20 @@ import 'package:flutter/gestures.dart'; import 'package:pshared/models/ledger/account.dart'; import 'package:pshared/models/payment/wallet.dart'; +import 'package:pweb/models/dashboard/balance_item.dart'; import 'package:pweb/pages/dashboard/buttons/balance/add/card.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/balance_item.dart'; import 'package:pweb/pages/dashboard/buttons/balance/card.dart'; import 'package:pweb/pages/dashboard/buttons/balance/config.dart'; import 'package:pweb/pages/dashboard/buttons/balance/indicator.dart'; import 'package:pweb/pages/dashboard/buttons/balance/ledger.dart'; + class BalanceCarousel extends StatefulWidget { final List items; final int currentIndex; final ValueChanged onIndexChanged; final ValueChanged onTopUp; + final ValueChanged onLedgerAddFunds; final ValueChanged onWalletTap; final ValueChanged onLedgerTap; @@ -25,6 +27,7 @@ class BalanceCarousel extends StatefulWidget { required this.currentIndex, required this.onIndexChanged, required this.onTopUp, + required this.onLedgerAddFunds, required this.onWalletTap, required this.onLedgerTap, }); @@ -101,17 +104,18 @@ class _BalanceCarouselState extends State { itemCount: widget.items.length, itemBuilder: (context, index) { final item = widget.items[index]; - final Widget card = switch (item.type) { - BalanceItemType.wallet => WalletCard( - wallet: item.wallet!, - onTopUp: () => widget.onTopUp(item.wallet!), - onTap: () => widget.onWalletTap(item.wallet!), + final Widget card = switch (item) { + WalletBalanceItem(:final wallet) => WalletCard( + wallet: wallet, + onTopUp: () => widget.onTopUp(wallet), + onTap: () => widget.onWalletTap(wallet), ), - BalanceItemType.ledger => LedgerAccountCard( - account: item.account!, - onTap: () => widget.onLedgerTap(item.account!), + LedgerBalanceItem(:final account) => LedgerAccountCard( + account: account, + onTap: () => widget.onLedgerTap(account), + onAddFunds: () => widget.onLedgerAddFunds(account), ), - BalanceItemType.addAction => const AddBalanceCard(), + AddBalanceActionItem() => const AddBalanceCard(), }; return Padding( diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/controller.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/controller.dart index df2f6484..88176825 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/controller.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/controller.dart @@ -3,7 +3,8 @@ import 'package:flutter/foundation.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pshared/provider/ledger.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/balance_item.dart'; +import 'package:pweb/models/dashboard/balance_item.dart'; + class BalanceCarouselController with ChangeNotifier { WalletsController? _walletsController; @@ -73,14 +74,19 @@ class BalanceCarouselController with ChangeNotifier { String? _currentWalletRef(List items, int index) { if (items.isEmpty || index < 0 || index >= items.length) return null; final current = items[index]; - if (!current.isWallet) return null; - return current.wallet?.id; + return switch (current) { + WalletBalanceItem(:final wallet) => wallet.id, + _ => null, + }; } int? _walletIndexByRef(List items, String? walletRef) { if (walletRef == null || walletRef.isEmpty) return null; final idx = items.indexWhere( - (item) => item.isWallet && item.wallet?.id == walletRef, + (item) => switch (item) { + WalletBalanceItem(:final wallet) => wallet.id == walletRef, + _ => false, + }, ); if (idx < 0) return null; return idx; @@ -97,17 +103,17 @@ class BalanceCarouselController with ChangeNotifier { for (var i = 0; i < left.length; i++) { final a = left[i]; final b = right[i]; - if (a.type != b.type) return false; + if (a.runtimeType != b.runtimeType) return false; if (_itemIdentity(a) != _itemIdentity(b)) return false; } return true; } - String _itemIdentity(BalanceItem item) => switch (item.type) { - BalanceItemType.wallet => item.wallet?.id ?? '', - BalanceItemType.ledger => item.account?.ledgerAccountRef ?? '', - BalanceItemType.addAction => 'add', + String _itemIdentity(BalanceItem item) => switch (item) { + WalletBalanceItem(:final wallet) => wallet.id, + LedgerBalanceItem(:final account) => account.ledgerAccountRef, + AddBalanceActionItem() => 'add', }; void _syncSelectedWallet() { @@ -115,9 +121,8 @@ class BalanceCarouselController with ChangeNotifier { if (walletsController == null || _items.isEmpty) return; final current = _items[_index]; - if (!current.isWallet || current.wallet == null) return; - - final wallet = current.wallet!; + if (current is! WalletBalanceItem) return; + final wallet = current.wallet; if (walletsController.selectedWallet?.id == wallet.id) return; walletsController.selectWallet(wallet); } diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger.dart index f69ffe59..69771c33 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger.dart @@ -1,133 +1,27 @@ import 'package:flutter/material.dart'; -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'; -import 'package:pweb/widgets/refresh_balance/ledger.dart'; - -import 'package:pweb/generated/i18n/app_localizations.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/source/card.dart'; class LedgerAccountCard extends StatelessWidget { final LedgerAccount account; + final VoidCallback onAddFunds; final VoidCallback? onTap; - const LedgerAccountCard({super.key, required this.account, this.onTap}); - - String _formatBalance() { - final money = account.balance?.balance; - if (money == null) return '--'; - - final amount = parseMoneyAmount(money.amount, fallback: double.nan); - if (amount.isNaN) { - return '${money.amount} ${money.currency}'; - } - - try { - final currency = currencyStringToCode(money.currency); - final symbol = currencyCodeToSymbol(currency); - if (symbol.trim().isEmpty) { - return '${amountToString(amount)} ${money.currency}'; - } - return '${amountToString(amount)} $symbol'; - } catch (_) { - return '${amountToString(amount)} ${money.currency}'; - } - } - - String _formatMaskedBalance() { - final currency = account.currency.trim(); - if (currency.isEmpty) return '••••'; - try { - final symbol = currencyCodeToSymbol(currencyStringToCode(currency)); - if (symbol.trim().isEmpty) { - return '•••• $currency'; - } - return '•••• $symbol'; - } catch (_) { - return '•••• $currency'; - } - } + const LedgerAccountCard({ + super.key, + required this.account, + required this.onAddFunds, + this.onTap, + }); @override Widget build(BuildContext context) { - final textTheme = Theme.of(context).textTheme; - final colorScheme = Theme.of(context).colorScheme; - final loc = AppLocalizations.of(context)!; - final accountName = account.name.trim(); - final accountCode = account.accountCode.trim(); - final title = accountName.isNotEmpty ? accountName : loc.paymentTypeLedger; - final subtitle = accountCode.isNotEmpty ? accountCode : null; - final badge = account.currency.trim().isEmpty - ? null - : account.currency.toUpperCase(); - - return Card( - color: colorScheme.onSecondary, - elevation: WalletCardConfig.elevation, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius), - ), - child: InkWell( - borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius), - onTap: onTap, - child: Padding( - padding: WalletCardConfig.contentPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BalanceHeader(title: title, subtitle: subtitle, badge: badge), - Row( - children: [ - Consumer( - builder: (context, controller, _) { - final isMasked = controller.isBalanceMasked( - account.ledgerAccountRef, - ); - return Row( - children: [ - Text( - isMasked - ? _formatMaskedBalance() - : _formatBalance(), - style: textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), - ), - const SizedBox(width: 12), - GestureDetector( - onTap: () => controller.toggleBalanceMask( - account.ledgerAccountRef, - ), - child: Icon( - isMasked - ? Icons.visibility_off - : Icons.visibility, - size: 24, - color: colorScheme.onSurface, - ), - ), - ], - ); - }, - ), - const SizedBox(width: 12), - LedgerBalanceRefreshButton( - ledgerAccountRef: account.ledgerAccountRef, - ), - ], - ), - ], - ), - ), - ), + return BalanceSourceCard.ledger( + account: account, + onTap: onTap ?? () {}, + onAddFunds: onAddFunds, ); } } diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger_amount.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger_amount.dart new file mode 100644 index 00000000..fe0c60c4 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger_amount.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/controllers/balance_mask/ledger_accounts.dart'; +import 'package:pshared/models/ledger/account.dart'; + +import 'package:pweb/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart'; + + +class LedgerBalanceAmount extends StatelessWidget { + final LedgerAccount account; + + const LedgerBalanceAmount({super.key, required this.account}); + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + final colorScheme = Theme.of(context).colorScheme; + + return Consumer( + builder: (context, controller, _) { + final isMasked = controller.isBalanceMasked(account.ledgerAccountRef); + final balance = isMasked + ? LedgerBalanceFormatter.formatMasked(account) + : LedgerBalanceFormatter.format(account); + + return Row( + children: [ + Flexible( + child: Text( + balance, + style: textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + ), + const SizedBox(width: 12), + GestureDetector( + onTap: () { + controller.toggleBalanceMask(account.ledgerAccountRef); + }, + child: Icon( + isMasked ? Icons.visibility_off : Icons.visibility, + size: 24, + color: colorScheme.onSurface, + ), + ), + ], + ); + }, + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/ledger.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/ledger.dart new file mode 100644 index 00000000..3a58413a --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/ledger.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/provider/ledger.dart'; + +import 'package:pweb/pages/dashboard/buttons/balance/actions/bar.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class LedgerSourceActions extends StatelessWidget { + final String ledgerAccountRef; + final VoidCallback onAddFunds; + + const LedgerSourceActions({ + super.key, + required this.ledgerAccountRef, + required this.onAddFunds, + }); + + @override + Widget build(BuildContext context) { + final ledgerProvider = context.watch(); + final loc = AppLocalizations.of(context)!; + final isBusy = + ledgerProvider.isWalletRefreshing(ledgerAccountRef) || + ledgerProvider.isLoading; + final hasTarget = ledgerProvider.accounts.any( + (a) => a.ledgerAccountRef == ledgerAccountRef, + ); + + return BalanceActionsBar( + isRefreshBusy: isBusy, + canRefresh: hasTarget, + onRefresh: () { + context.read().refreshBalance(ledgerAccountRef); + }, + onAddFunds: onAddFunds, + refreshLabel: loc.refreshBalance, + addFundsLabel: loc.addFunds, + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/wallet.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/wallet.dart new file mode 100644 index 00000000..8c872026 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/wallet.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/provider/payment/wallets.dart'; + +import 'package:pweb/pages/dashboard/buttons/balance/actions/bar.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class WalletSourceActions extends StatelessWidget { + final String walletRef; + final VoidCallback onAddFunds; + + const WalletSourceActions({ + super.key, + required this.walletRef, + required this.onAddFunds, + }); + + @override + Widget build(BuildContext context) { + final walletsProvider = context.watch(); + final loc = AppLocalizations.of(context)!; + final isBusy = + walletsProvider.isWalletRefreshing(walletRef) || + walletsProvider.isLoading; + final hasTarget = walletsProvider.wallets.any((w) => w.id == walletRef); + + return BalanceActionsBar( + isRefreshBusy: isBusy, + canRefresh: hasTarget, + onRefresh: () { + context.read().refreshBalance(walletRef); + }, + onAddFunds: onAddFunds, + refreshLabel: loc.refreshBalance, + addFundsLabel: loc.addFunds, + ); + } +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card.dart new file mode 100644 index 00000000..bf56777f --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; + +import 'package:provider/provider.dart'; + +import 'package:pshared/controllers/balance_mask/wallets.dart'; +import 'package:pshared/models/ledger/account.dart'; +import 'package:pshared/models/payment/chain_network.dart'; +import 'package:pshared/models/payment/source_type.dart'; +import 'package:pshared/models/payment/wallet.dart'; +import 'package:pshared/utils/l10n/chain.dart'; + +import 'package:pweb/pages/dashboard/buttons/balance/amount.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/ledger_amount.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/source/actions/ledger.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/source/actions/wallet.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/source/card_layout.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class BalanceSourceCard extends StatelessWidget { + final PaymentSourceType _type; + final Wallet? _wallet; + final LedgerAccount? _ledgerAccount; + final VoidCallback onTap; + final VoidCallback onAddFunds; + + const BalanceSourceCard.wallet({ + super.key, + required Wallet wallet, + required this.onTap, + required this.onAddFunds, + }) : _type = PaymentSourceType.wallet, + _wallet = wallet, + _ledgerAccount = null; + + const BalanceSourceCard.ledger({ + super.key, + required LedgerAccount account, + required this.onTap, + required this.onAddFunds, + }) : _type = PaymentSourceType.ledger, + _wallet = null, + _ledgerAccount = account; + + @override + Widget build(BuildContext context) => switch (_type) { + PaymentSourceType.wallet => _buildWalletCard(context, _wallet!), + PaymentSourceType.ledger => _buildLedgerCard(context, _ledgerAccount!), + }; + + Widget _buildWalletCard(BuildContext context, Wallet wallet) { + final networkLabel = + (wallet.network == null || wallet.network == ChainNetwork.unspecified) + ? null + : wallet.network!.localizedName(context); + final symbol = wallet.tokenSymbol?.trim(); + + return BalanceSourceCardLayout( + title: wallet.name, + subtitle: networkLabel, + badge: (symbol == null || symbol.isEmpty) ? null : symbol, + onTap: onTap, + actions: WalletSourceActions( + walletRef: wallet.id, + onAddFunds: onAddFunds, + ), + amount: BalanceAmount( + wallet: wallet, + onToggleMask: () { + context.read().toggleBalanceMask(wallet.id); + }, + ), + ); + } + + Widget _buildLedgerCard(BuildContext context, LedgerAccount account) { + final loc = AppLocalizations.of(context)!; + final accountName = account.name.trim(); + final accountCode = account.accountCode.trim(); + final title = accountName.isNotEmpty ? accountName : loc.paymentTypeLedger; + final subtitle = accountCode.isNotEmpty ? accountCode : null; + final badge = account.currency.trim().isEmpty + ? null + : account.currency.toUpperCase(); + + return BalanceSourceCardLayout( + title: title, + subtitle: subtitle, + badge: badge, + onTap: onTap, + actions: LedgerSourceActions( + ledgerAccountRef: account.ledgerAccountRef, + onAddFunds: onAddFunds, + ), + amount: LedgerBalanceAmount(account: account), + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card_layout.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card_layout.dart new file mode 100644 index 00000000..9f85c09b --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card_layout.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/pages/dashboard/buttons/balance/config.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/header.dart'; + + +class BalanceSourceCardLayout extends StatelessWidget { + final String title; + final String? subtitle; + final String? badge; + final Widget amount; + final Widget actions; + final VoidCallback onTap; + + const BalanceSourceCardLayout({ + super.key, + required this.title, + required this.subtitle, + required this.badge, + required this.amount, + required this.actions, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Card( + color: colorScheme.onSecondary, + elevation: WalletCardConfig.elevation, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius), + ), + child: InkWell( + borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius), + onTap: onTap, + child: SizedBox.expand( + child: Padding( + padding: WalletCardConfig.contentPadding, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BalanceHeader(title: title, subtitle: subtitle, badge: badge), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded(child: amount), + const SizedBox(width: 12), + actions, + ], + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/widget.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/widget.dart index 06f976e9..65027d9c 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/widget.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/widget.dart @@ -12,15 +12,16 @@ import 'package:pweb/pages/dashboard/buttons/balance/controller.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; - class BalanceWidget extends StatelessWidget { final ValueChanged onTopUp; + final ValueChanged onLedgerAddFunds; final ValueChanged onWalletTap; final ValueChanged onLedgerTap; const BalanceWidget({ super.key, required this.onTopUp, + required this.onLedgerAddFunds, required this.onWalletTap, required this.onLedgerTap, }); @@ -49,6 +50,7 @@ class BalanceWidget extends StatelessWidget { currentIndex: carousel.index, onIndexChanged: carousel.onPageChanged, onTopUp: onTopUp, + onLedgerAddFunds: onLedgerAddFunds, onWalletTap: onWalletTap, onLedgerTap: onLedgerTap, ); diff --git a/frontend/pweb/lib/pages/dashboard/dashboard.dart b/frontend/pweb/lib/pages/dashboard/dashboard.dart index 8089857e..1de0a860 100644 --- a/frontend/pweb/lib/pages/dashboard/dashboard.dart +++ b/frontend/pweb/lib/pages/dashboard/dashboard.dart @@ -27,6 +27,7 @@ class DashboardPage extends StatefulWidget { final ValueChanged onRecipientSelected; final void Function(PaymentType type) onGoToPaymentWithoutRecipient; final ValueChanged onTopUp; + final ValueChanged onLedgerAddFunds; final ValueChanged onWalletTap; final ValueChanged onLedgerTap; @@ -35,6 +36,7 @@ class DashboardPage extends StatefulWidget { required this.onRecipientSelected, required this.onGoToPaymentWithoutRecipient, required this.onTopUp, + required this.onLedgerAddFunds, required this.onWalletTap, required this.onLedgerTap, }); @@ -90,6 +92,7 @@ class _DashboardPageState extends State { BalanceWidgetProviders( child: BalanceWidget( onTopUp: widget.onTopUp, + onLedgerAddFunds: widget.onLedgerAddFunds, onWalletTap: widget.onWalletTap, onLedgerTap: widget.onLedgerTap, ), diff --git a/frontend/pweb/lib/pages/dashboard/payouts/amount/feild.dart b/frontend/pweb/lib/pages/dashboard/payouts/amount/feild.dart index 70fc5cab..6f293385 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/amount/feild.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/amount/feild.dart @@ -5,7 +5,6 @@ import 'package:provider/provider.dart'; import 'package:pshared/utils/currency.dart'; import 'package:pweb/controllers/payments/amount_field.dart'; -import 'package:pweb/models/payment/amount/mode.dart'; import 'package:pweb/pages/dashboard/payouts/amount/mode/selector.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/actions.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/actions.dart index 9030afb9..9a09c800 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/actions.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/actions.dart @@ -6,7 +6,7 @@ import 'package:pshared/provider/payment/multiple/quotation.dart'; import 'package:pweb/controllers/payouts/multiple_payouts.dart'; import 'package:pweb/controllers/payouts/payout_verification.dart'; -import 'package:pweb/utils/payment/payout_verification_flow.dart'; +import 'package:pweb/utils/payment/verification_flow.dart'; import 'package:pweb/widgets/dialogs/payment_status_dialog.dart'; diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/avatar.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/avatar.dart index db03530e..118239f2 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/avatar.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/avatar.dart @@ -23,7 +23,7 @@ class RecipientAvatar extends StatelessWidget { @override Widget build(BuildContext context) { - final textColor = Theme.of(context).colorScheme.onPrimary; + final textColor = Theme.of(context).colorScheme.onSecondary; return Column( mainAxisAlignment: MainAxisAlignment.center, @@ -31,7 +31,7 @@ class RecipientAvatar extends StatelessWidget { CircleAvatar( radius: avatarRadius, backgroundImage: avatarUrl != null ? NetworkImage(avatarUrl!) : null, - backgroundColor: Theme.of(context).colorScheme.primary, + backgroundColor: Theme.of(context).colorScheme.primaryFixed, child: avatarUrl == null ? Text( getInitials(name), diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/short_list.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/short_list.dart index 512bec73..1c281645 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/short_list.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/short_list.dart @@ -7,54 +7,56 @@ import 'package:pweb/pages/dashboard/payouts/single/address_book/avatar.dart'; class ShortListAddressBookPayout extends StatelessWidget { final List recipients; final ValueChanged onSelected; - final Widget? trailing; + final Widget? leading; const ShortListAddressBookPayout({ super.key, required this.recipients, required this.onSelected, - this.trailing, + this.leading, }); static const double _avatarRadius = 20; static const double _avatarSize = 80; - static const EdgeInsets _padding = EdgeInsets.symmetric(horizontal: 10, vertical: 8); + static const EdgeInsets _padding = EdgeInsets.symmetric( + horizontal: 10, + vertical: 8, + ); static const TextStyle _nameStyle = TextStyle(fontSize: 12); @override Widget build(BuildContext context) { - final trailingWidget = trailing; + final leadingWidget = leading; + final recipientItems = recipients.map((recipient) { + return Padding( + padding: _padding, + child: InkWell( + borderRadius: BorderRadius.circular(5), + hoverColor: Theme.of(context).colorScheme.onTertiary, + onTap: () => onSelected(recipient), + child: SizedBox( + height: _avatarSize, + width: _avatarSize, + child: RecipientAvatar( + isVisible: true, + name: recipient.name, + avatarUrl: recipient.avatarUrl, + avatarRadius: _avatarRadius, + nameStyle: _nameStyle, + ), + ), + ), + ); + }); return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( - children: - recipients.map((recipient) { - return Padding( - padding: _padding, - child: InkWell( - borderRadius: BorderRadius.circular(5), - hoverColor: Theme.of(context).colorScheme.primaryContainer, - onTap: () => onSelected(recipient), - child: SizedBox( - height: _avatarSize, - width: _avatarSize, - child: RecipientAvatar( - isVisible: true, - name: recipient.name, - avatarUrl: recipient.avatarUrl, - avatarRadius: _avatarRadius, - nameStyle: _nameStyle, - ), - ), - ), - ); - }).toList() - ..addAll( - trailingWidget == null - ? const [] - : [Padding(padding: _padding, child: trailingWidget)], - ), + children: [ + if (leadingWidget != null) + Padding(padding: _padding, child: leadingWidget), + ...recipientItems, + ], ), ); } diff --git a/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/widget.dart b/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/widget.dart index df80ca77..c6d42d95 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/widget.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/single/address_book/widget.dart @@ -21,10 +21,7 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; class AddressBookPayout extends StatefulWidget { final ValueChanged onSelected; - const AddressBookPayout({ - super.key, - required this.onSelected, - }); + const AddressBookPayout({super.key, required this.onSelected}); @override State createState() => _AddressBookPayoutState(); @@ -71,6 +68,7 @@ class _AddressBookPayoutState extends State { provider.setCurrentObject(null); context.pushNamed(PayoutRoutes.addRecipient); } + final filteredRecipients = filterRecipients( recipients: recipients, query: _query, @@ -81,16 +79,18 @@ class _AddressBookPayoutState extends State { } if (provider.error != null) { - return Center(child: Text(loc.notificationError(provider.error ?? loc.noErrorInformation))); + return Center( + child: Text( + loc.notificationError(provider.error ?? loc.noErrorInformation), + ), + ); } return SizedBox( height: _isExpanded ? _expandedHeight : _collapsedHeight, child: Card( margin: const EdgeInsets.all(_cardMargin), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), elevation: 4, color: Theme.of(context).colorScheme.onSecondary, child: Padding( @@ -105,27 +105,27 @@ class _AddressBookPayoutState extends State { const SizedBox(height: _spacingBetween), Expanded( child: recipients.isEmpty - ? Center( - child: AddRecipientTile( - label: loc.addRecipient, - onTap: onAddRecipient, - ), - ) - : _isExpanded && filteredRecipients.isEmpty + ? Center( + child: AddRecipientTile( + label: loc.addRecipient, + onTap: onAddRecipient, + ), + ) + : _isExpanded && filteredRecipients.isEmpty ? AddressBookPlaceholder(text: loc.noRecipientsFound) : _isExpanded - ? LongListAddressBookPayout( - filteredRecipients: filteredRecipients, - onSelected: widget.onSelected, - ) - : ShortListAddressBookPayout( - recipients: recipients, - onSelected: widget.onSelected, - trailing: AddRecipientTile( - label: loc.addRecipient, - onTap: onAddRecipient, - ), + ? LongListAddressBookPayout( + filteredRecipients: filteredRecipients, + onSelected: widget.onSelected, + ) + : ShortListAddressBookPayout( + recipients: recipients, + onSelected: widget.onSelected, + leading: AddRecipientTile( + label: loc.addRecipient, + onTap: onAddRecipient, ), + ), ), ], ), diff --git a/frontend/pweb/lib/pages/payout_page/send/page.dart b/frontend/pweb/lib/pages/payout_page/send/page.dart index 7118ccf8..cea285e6 100644 --- a/frontend/pweb/lib/pages/payout_page/send/page.dart +++ b/frontend/pweb/lib/pages/payout_page/send/page.dart @@ -5,19 +5,21 @@ import 'package:pshared/models/recipient/recipient.dart'; import 'package:pweb/widgets/sidebar/destinations.dart'; import 'package:pweb/controllers/payments/page_ui.dart'; -import 'package:pweb/pages/payout_page/send/page_handlers.dart'; +import 'package:pweb/utils/payment/page_handlers.dart'; import 'package:pweb/pages/payout_page/send/page_view.dart'; class PaymentPage extends StatefulWidget { final ValueChanged? onBack; final PaymentType? initialPaymentType; + final String? initialDestinationLedgerAccountRef; final PayoutDestination fallbackDestination; const PaymentPage({ super.key, this.onBack, this.initialPaymentType, + this.initialDestinationLedgerAccountRef, this.fallbackDestination = PayoutDestination.dashboard, }); @@ -34,7 +36,11 @@ class _PaymentPageState extends State { _uiController = PaymentPageUiController(); WidgetsBinding.instance.addPostFrameCallback( - (_) => initializePaymentPage(context, widget.initialPaymentType), + (_) => initializePaymentPage( + context, + widget.initialPaymentType, + destinationLedgerAccountRef: widget.initialDestinationLedgerAccountRef, + ), ); } diff --git a/frontend/pweb/lib/pages/payout_page/send/page_view.dart b/frontend/pweb/lib/pages/payout_page/send/page_view.dart index 442719c9..7915609a 100644 --- a/frontend/pweb/lib/pages/payout_page/send/page_view.dart +++ b/frontend/pweb/lib/pages/payout_page/send/page_view.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pshared/models/recipient/recipient.dart'; +import 'package:pshared/provider/payment/flow.dart'; import 'package:pshared/provider/payment/quotation/quotation.dart'; import 'package:pshared/provider/recipient/pmethods.dart'; import 'package:pshared/provider/recipient/provider.dart'; @@ -14,6 +15,7 @@ import 'package:pweb/controllers/payments/page_ui.dart'; import 'package:pweb/controllers/payouts/payout_verification.dart'; import 'package:pweb/models/state/control_state.dart'; + class PaymentPageView extends StatelessWidget { final PaymentPageUiController uiController; final ValueChanged? onBack; @@ -47,6 +49,7 @@ class PaymentPageView extends StatelessWidget { final uiController = context.watch(); final methodsProvider = context.watch(); final recipientProvider = context.watch(); + final flowProvider = context.watch(); final quotationProvider = context.watch(); final verificationController = context .watch(); @@ -58,10 +61,12 @@ class PaymentPageView extends StatelessWidget { recipients: recipientProvider.recipients, query: uiController.query, ); + final hasDestinationSelection = + flowProvider.selectedPaymentData != null; final sendState = verificationController.isCooldownActiveFor(verificationContextKey) ? ControlState.disabled - : (recipient == null + : (!hasDestinationSelection ? ControlState.disabled : ControlState.enabled); diff --git a/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/manual_details.dart b/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/manual_details.dart new file mode 100644 index 00000000..e9dfc9d7 --- /dev/null +++ b/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/manual_details.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/methods/data.dart'; +import 'package:pshared/models/recipient/payment_method_draft.dart'; + +import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel.dart'; +import 'package:pweb/pages/payout_page/send/widgets/payment_info/header.dart'; +import 'package:pweb/models/state/control_state.dart'; +import 'package:pweb/models/state/visibility.dart'; +import 'package:pweb/utils/dimensions.dart'; + + +class PaymentInfoManualDetailsSection extends StatelessWidget { + final AppDimensions dimensions; + final String title; + final VisibilityState titleVisibility; + final PaymentMethodData data; + + const PaymentInfoManualDetailsSection({ + super.key, + required this.dimensions, + required this.title, + required this.titleVisibility, + required this.data, + }); + + @override + Widget build(BuildContext context) { + final entry = RecipientMethodDraft(type: data.type, data: data); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PaymentInfoHeader( + dimensions: dimensions, + title: title, + visibility: titleVisibility, + ), + PaymentMethodPanel( + selectedType: data.type, + selectedIndex: 0, + entries: [entry], + onRemove: (_) {}, + onChanged: (_, ignored) {}, + editState: ControlState.disabled, + deleteVisibility: VisibilityState.hidden, + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/section.dart b/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/section.dart index fd4f6399..51419c09 100644 --- a/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/section.dart +++ b/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/section.dart @@ -6,6 +6,7 @@ import 'package:pshared/provider/payment/flow.dart'; import 'package:pweb/pages/payout_page/send/widgets/payment_info/methods_section.dart'; import 'package:pweb/pages/payout_page/send/widgets/payment_info/methods_state.dart'; +import 'package:pweb/pages/payout_page/send/widgets/payment_info/manual_details.dart'; import 'package:pweb/pages/payout_page/send/widgets/payment_info/no_methods.dart'; import 'package:pweb/pages/payout_page/send/widgets/payment_info/no_recipient.dart'; import 'package:pweb/models/state/visibility.dart'; @@ -35,8 +36,9 @@ class PaymentInfoSection extends StatelessWidget { Widget build(BuildContext context) { final loc = AppLocalizations.of(context)!; final flowProvider = context.watch(); + final manualData = flowProvider.manualPaymentData; - if (!flowProvider.hasRecipient) { + if (!flowProvider.hasRecipient && manualData == null) { return PaymentInfoNoRecipientSection( dimensions: dimensions, title: loc.paymentInfo, @@ -44,6 +46,15 @@ class PaymentInfoSection extends StatelessWidget { ); } + if (!flowProvider.hasRecipient && manualData != null) { + return PaymentInfoManualDetailsSection( + dimensions: dimensions, + title: loc.paymentInfo, + titleVisibility: titleVisibility, + data: manualData, + ); + } + final methods = flowProvider.methodsForRecipient; final types = visiblePaymentTypes; diff --git a/frontend/pweb/lib/pages/payout_page/send/widgets/recipient/section.dart b/frontend/pweb/lib/pages/payout_page/send/widgets/recipient/section.dart index ad5f8f0e..17094754 100644 --- a/frontend/pweb/lib/pages/payout_page/send/widgets/recipient/section.dart +++ b/frontend/pweb/lib/pages/payout_page/send/widgets/recipient/section.dart @@ -81,7 +81,7 @@ class RecipientSection extends StatelessWidget { ShortListAddressBookPayout( recipients: recipientProvider.recipients, onSelected: onRecipientSelected, - trailing: AddRecipientTile( + leading: AddRecipientTile( label: loc.addRecipient, onTap: onAddRecipient, ), diff --git a/frontend/pweb/lib/pages/payout_page/send/widgets/recipient_details_card.dart b/frontend/pweb/lib/pages/payout_page/send/widgets/recipient_details_card.dart index f5bd0cc3..971062ae 100644 --- a/frontend/pweb/lib/pages/payout_page/send/widgets/recipient_details_card.dart +++ b/frontend/pweb/lib/pages/payout_page/send/widgets/recipient_details_card.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + import 'package:pshared/models/recipient/recipient.dart'; +import 'package:pshared/provider/payment/flow.dart'; import 'package:pshared/provider/recipient/provider.dart'; import 'package:pweb/pages/payout_page/send/widgets/payment_info/section.dart'; @@ -46,24 +49,30 @@ class PaymentRecipientDetailsCard extends StatelessWidget { @override Widget build(BuildContext context) { + final flowProvider = context.watch(); + final isRecipientSelectionLocked = + !flowProvider.hasRecipient && flowProvider.manualPaymentData != null; + return PaymentSectionCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - RecipientSection( - recipient: recipient, - dimensions: dimensions, - recipientProvider: recipientProvider, - searchQuery: searchQuery, - filteredRecipients: filteredRecipients, - searchController: searchController, - searchFocusNode: searchFocusNode, - onSearchChanged: onSearchChanged, - onRecipientSelected: onRecipientSelected, - onRecipientCleared: onRecipientCleared, - onAddRecipient: onAddRecipient, - ), - SizedBox(height: dimensions.paddingMedium), + if (!isRecipientSelectionLocked) ...[ + RecipientSection( + recipient: recipient, + dimensions: dimensions, + recipientProvider: recipientProvider, + searchQuery: searchQuery, + filteredRecipients: filteredRecipients, + searchController: searchController, + searchFocusNode: searchFocusNode, + onSearchChanged: onSearchChanged, + onRecipientSelected: onRecipientSelected, + onRecipientCleared: onRecipientCleared, + onAddRecipient: onAddRecipient, + ), + SizedBox(height: dimensions.paddingMedium), + ], PaymentInfoSection( dimensions: dimensions, titleVisibility: VisibilityState.hidden, diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart index 772e766b..f70cf1f1 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/buttons/top_up.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + import 'package:provider/provider.dart'; import 'package:pshared/controllers/payment/source.dart'; import 'package:pshared/models/payment/source_type.dart'; +import 'package:pshared/models/payment/type.dart'; +import 'package:pshared/provider/recipient/provider.dart'; import 'package:pweb/app/router/payout_routes.dart'; @@ -17,11 +21,35 @@ class TopUpButton extends StatelessWidget { Widget build(BuildContext context) { final loc = AppLocalizations.of(context)!; final source = context.watch(); - final canTopUp = source.selectedType == PaymentSourceType.wallet; + final selectedType = source.selectedType; + final selectedLedger = source.selectedLedgerAccount; + final canTopUp = + selectedType == PaymentSourceType.wallet || + (selectedType == PaymentSourceType.ledger && selectedLedger != null); return ElevatedButton( style: ElevatedButton.styleFrom(shadowColor: null, elevation: 0), - onPressed: canTopUp ? () => context.pushToWalletTopUp() : null, + onPressed: !canTopUp + ? null + : () { + if (selectedType == PaymentSourceType.wallet) { + context.pushToWalletTopUp(); + return; + } + + if (selectedType == PaymentSourceType.ledger && + selectedLedger != null) { + context.read().setCurrentObject(null); + context.pushNamed( + PayoutRoutes.payment, + queryParameters: PayoutRoutes.buildQueryParameters( + paymentType: PaymentType.ledger, + destinationLedgerAccountRef: + selectedLedger.ledgerAccountRef, + ), + ); + } + }, child: Text(loc.topUpBalance), ); } diff --git a/frontend/pweb/lib/providers/multiple_payouts.dart b/frontend/pweb/lib/providers/multiple_payouts.dart index d82b8568..e3537670 100644 --- a/frontend/pweb/lib/providers/multiple_payouts.dart +++ b/frontend/pweb/lib/providers/multiple_payouts.dart @@ -13,8 +13,9 @@ import 'package:pshared/utils/payment/quote_helpers.dart'; import 'package:pweb/models/payment/multiple_payouts/csv_row.dart'; import 'package:pweb/models/payment/multiple_payouts/state.dart'; -import 'package:pweb/utils/payment/multiple_csv_parser.dart'; -import 'package:pweb/utils/payment/multiple_intent_builder.dart'; +import 'package:pweb/utils/payment/multiple/csv_parser.dart'; +import 'package:pweb/utils/payment/multiple/intent_builder.dart'; + class MultiplePayoutsProvider extends ChangeNotifier { final MultipleCsvParser _csvParser; diff --git a/frontend/pweb/lib/utils/payment/multiple_csv_parser.dart b/frontend/pweb/lib/utils/payment/multiple/csv_parser.dart similarity index 100% rename from frontend/pweb/lib/utils/payment/multiple_csv_parser.dart rename to frontend/pweb/lib/utils/payment/multiple/csv_parser.dart diff --git a/frontend/pweb/lib/utils/payment/multiple_intent_builder.dart b/frontend/pweb/lib/utils/payment/multiple/intent_builder.dart similarity index 100% rename from frontend/pweb/lib/utils/payment/multiple_intent_builder.dart rename to frontend/pweb/lib/utils/payment/multiple/intent_builder.dart diff --git a/frontend/pweb/lib/pages/payout_page/send/page_handlers.dart b/frontend/pweb/lib/utils/payment/page_handlers.dart similarity index 82% rename from frontend/pweb/lib/pages/payout_page/send/page_handlers.dart rename to frontend/pweb/lib/utils/payment/page_handlers.dart index 76dac6b4..c29a1702 100644 --- a/frontend/pweb/lib/pages/payout_page/send/page_handlers.dart +++ b/frontend/pweb/lib/utils/payment/page_handlers.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; +import 'package:pshared/models/payment/methods/ledger.dart'; import 'package:pshared/models/payment/type.dart'; import 'package:pshared/models/recipient/recipient.dart'; import 'package:pshared/provider/payment/flow.dart'; @@ -17,14 +18,30 @@ import 'package:pweb/widgets/dialogs/payment_status_dialog.dart'; import 'package:pweb/controllers/payments/page.dart'; import 'package:pweb/controllers/payments/page_ui.dart'; import 'package:pweb/controllers/payouts/payout_verification.dart'; -import 'package:pweb/utils/payment/payout_verification_flow.dart'; +import 'package:pweb/utils/payment/verification_flow.dart'; + void initializePaymentPage( BuildContext context, - PaymentType? initialPaymentType, -) { + PaymentType? initialPaymentType, { + String? destinationLedgerAccountRef, +}) { final flowProvider = context.read(); + final recipientsProvider = context.read(); + flowProvider.setPreferredType(initialPaymentType); + + final destinationRef = destinationLedgerAccountRef?.trim(); + if (destinationRef != null && destinationRef.isNotEmpty) { + recipientsProvider.setCurrentObject(null); + flowProvider.setPreferredType(PaymentType.ledger); + flowProvider.setManualPaymentData( + LedgerPaymentMethod(ledgerAccountRef: destinationRef), + ); + return; + } + + flowProvider.setManualPaymentData(null); } void handleSearchChanged(PaymentPageUiController uiController, String query) { diff --git a/frontend/pweb/lib/utils/payment/payout_verification_flow.dart b/frontend/pweb/lib/utils/payment/verification_flow.dart similarity index 100% rename from frontend/pweb/lib/utils/payment/payout_verification_flow.dart rename to frontend/pweb/lib/utils/payment/verification_flow.dart diff --git a/frontend/pweb/lib/utils/report/payment_mapper.dart b/frontend/pweb/lib/utils/report/payment_mapper.dart index f41027c2..567d2353 100644 --- a/frontend/pweb/lib/utils/report/payment_mapper.dart +++ b/frontend/pweb/lib/utils/report/payment_mapper.dart @@ -1,12 +1,13 @@ +import 'package:pshared/models/payment/operation_document.dart'; import 'package:pshared/models/payment/operation.dart'; import 'package:pshared/models/payment/payment.dart'; import 'package:pshared/models/payment/state.dart'; import 'package:pshared/models/payment/status.dart'; import 'package:pshared/utils/money.dart'; -import 'package:pweb/models/report/operation/document.dart'; import 'package:pweb/utils/report/operations/document_rule.dart'; + OperationItem mapPaymentToOperation(Payment payment) { final debit = payment.lastQuote?.amounts?.sourceDebitTotal; final settlement = payment.lastQuote?.amounts?.destinationSettlement; @@ -55,7 +56,7 @@ OperationItem mapPaymentToOperation(Payment payment) { ); } -OperationDocumentInfo? _resolveOperationDocument(Payment payment) { +OperationDocumentRef? _resolveOperationDocument(Payment payment) { for (final operation in payment.operations) { final operationRef = operation.operationRef; final gatewayService = operation.gateway; @@ -64,7 +65,7 @@ OperationDocumentInfo? _resolveOperationDocument(Payment payment) { if (!isOperationDocumentEligible(operation.code)) continue; - return OperationDocumentInfo( + return OperationDocumentRef( operationRef: operationRef, gatewayService: gatewayService, ); diff --git a/frontend/pweb/lib/utils/report/source_filter.dart b/frontend/pweb/lib/utils/report/source_filter.dart new file mode 100644 index 00000000..da7f1369 --- /dev/null +++ b/frontend/pweb/lib/utils/report/source_filter.dart @@ -0,0 +1,112 @@ +import 'package:pshared/models/payment/intent.dart'; +import 'package:pshared/models/payment/methods/data.dart'; +import 'package:pshared/models/payment/methods/ledger.dart'; +import 'package:pshared/models/payment/methods/managed_wallet.dart'; +import 'package:pshared/models/payment/methods/wallet.dart'; +import 'package:pshared/models/payment/payment.dart'; +import 'package:pshared/models/payment/source_type.dart'; + + +bool paymentMatchesSource( + Payment payment, { + required PaymentSourceType sourceType, + required String sourceRef, +}) { + final normalizedSourceRef = _normalize(sourceRef); + if (normalizedSourceRef == null) return false; + + final paymentSourceRef = _paymentSourceRef(payment, sourceType); + return paymentSourceRef != null && paymentSourceRef == normalizedSourceRef; +} + +String? _paymentSourceRef(Payment payment, PaymentSourceType sourceType) { + final fromIntent = _sourceRefFromIntent(payment.intent, sourceType); + if (fromIntent != null) return fromIntent; + return _sourceRefFromMetadata(payment.metadata, sourceType); +} + +String? _sourceRefFromIntent( + PaymentIntent? intent, + PaymentSourceType sourceType, +) { + final source = intent?.source; + if (source == null) return null; + + final fromIntentAttributes = _sourceRefFromMetadata( + intent?.attributes, + sourceType, + ); + if (fromIntentAttributes != null) return fromIntentAttributes; + + switch (sourceType) { + case PaymentSourceType.wallet: + return _walletSourceRef(source); + case PaymentSourceType.ledger: + return _ledgerSourceRef(source); + } +} + +String? _walletSourceRef(PaymentMethodData source) { + if (source is ManagedWalletPaymentMethod) { + return _normalize(source.managedWalletRef) ?? + _sourceRefFromMetadata(source.metadata, PaymentSourceType.wallet); + } + if (source is WalletPaymentMethod) { + return _normalize(source.walletId) ?? + _sourceRefFromMetadata(source.metadata, PaymentSourceType.wallet); + } + return null; +} + +String? _ledgerSourceRef(PaymentMethodData source) { + if (source is LedgerPaymentMethod) { + return _normalize(source.ledgerAccountRef) ?? + _sourceRefFromMetadata(source.metadata, PaymentSourceType.ledger); + } + return null; +} + +String? _sourceRefFromMetadata( + Map? metadata, + PaymentSourceType sourceType, +) { + if (metadata == null || metadata.isEmpty) return null; + + final keys = switch (sourceType) { + PaymentSourceType.wallet => const [ + 'source_wallet_ref', + 'managed_wallet_ref', + 'wallet_ref', + 'wallet_id', + 'source_wallet_id', + 'source_wallet_user_id', + 'wallet_user_id', + 'wallet_user_ref', + 'wallet_number', + 'source_wallet_number', + 'source_managed_wallet_ref', + 'source_ref', + ], + PaymentSourceType.ledger => const [ + 'source_ledger_account_ref', + 'ledger_account_ref', + 'source_account_code', + 'ledger_account_code', + 'account_code', + 'source_ref', + ], + }; + + for (final key in keys) { + final value = _normalize(metadata[key]); + if (value != null) return value; + } + + return null; +} + +String? _normalize(String? value) { + final normalized = value?.trim(); + if (normalized == null || normalized.isEmpty) return null; + return normalized; +} -- 2.49.1 From 281b3834d3be0bc8d3f9c7fe15305a32e6038f79 Mon Sep 17 00:00:00 2001 From: Arseni Date: Fri, 6 Mar 2026 17:48:36 +0300 Subject: [PATCH 3/5] wallet card redesign --- .../pweb/lib/app/router/payout_shell.dart | 8 +- .../dashboard/balance/actions_ui.dart | 19 +++ .../dashboard/balance/carousel.dart} | 45 +++++- .../dashboard/balance/source_actions.dart | 112 +++++++++++++ .../dashboard/balance/source_copy.dart | 35 ++++ .../buttons/balance/actions/bar.dart | 83 ++++++++++ .../hover_expandable_action_button.dart | 80 +++++++++ .../dashboard/buttons/balance/amount.dart | 13 +- .../dashboard/buttons/balance/carousel.dart | 152 ------------------ .../buttons/balance/carousel/card_item.dart | 47 ++++++ .../buttons/balance/carousel/cards_view.dart | 61 +++++++ .../buttons/balance/carousel/carousel.dart | 63 ++++++++ .../buttons/balance/carousel/navigation.dart | 38 +++++ .../dashboard/buttons/balance/config.dart | 19 ++- .../dashboard/buttons/balance/header.dart | 67 ++++---- .../buttons/balance/ledger_amount.dart | 17 +- .../dashboard/buttons/balance/providers.dart | 3 +- .../balance/source/actions/ledger.dart | 33 ++-- .../balance/source/actions/wallet.dart | 30 +--- .../buttons/balance/source/card.dart | 45 +++++- .../buttons/balance/source/card_layout.dart | 43 ++--- .../balance/{ => source/cards}/ledger.dart | 0 .../{card.dart => source/cards/wallet.dart} | 0 .../source/layout/amount_with_refresh.dart | 38 +++++ .../balance/source/layout/copyable_field.dart | 62 +++++++ .../balance/source/layout/wide_body.dart | 89 ++++++++++ .../dashboard/buttons/balance/widget.dart | 9 +- .../payouts/amount/{feild.dart => field.dart} | 0 .../dashboard/payouts/amount/widget.dart | 3 +- 29 files changed, 927 insertions(+), 287 deletions(-) create mode 100644 frontend/pweb/lib/controllers/dashboard/balance/actions_ui.dart rename frontend/pweb/lib/{pages/dashboard/buttons/balance/controller.dart => controllers/dashboard/balance/carousel.dart} (75%) create mode 100644 frontend/pweb/lib/controllers/dashboard/balance/source_actions.dart create mode 100644 frontend/pweb/lib/controllers/dashboard/balance/source_copy.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/actions/bar.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/actions/hover_expandable_action_button.dart delete mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/carousel.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/card_item.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/cards_view.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/carousel.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/navigation.dart rename frontend/pweb/lib/pages/dashboard/buttons/balance/{ => source/cards}/ledger.dart (100%) rename frontend/pweb/lib/pages/dashboard/buttons/balance/{card.dart => source/cards/wallet.dart} (100%) create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/source/layout/amount_with_refresh.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/source/layout/copyable_field.dart create mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/source/layout/wide_body.dart rename frontend/pweb/lib/pages/dashboard/payouts/amount/{feild.dart => field.dart} (100%) diff --git a/frontend/pweb/lib/app/router/payout_shell.dart b/frontend/pweb/lib/app/router/payout_shell.dart index 768b84b2..eda9c9b7 100644 --- a/frontend/pweb/lib/app/router/payout_shell.dart +++ b/frontend/pweb/lib/app/router/payout_shell.dart @@ -229,7 +229,7 @@ RouteBase payoutShellRoute() => ShellRoute( _startPayment(context, recipient: null, paymentType: type), onTopUp: (wallet) => _openWalletTopUp(context, wallet), onLedgerAddFunds: (account) => _openLedgerAddFunds(context, account), - onWalletTap: (wallet) => _openWalletEdit(context, wallet), + onWalletTap: (wallet) => _openWalletTopUp(context, wallet), onLedgerTap: (account) => _openLedgerEdit(context, account), ), ), @@ -385,12 +385,6 @@ void _openEditRecipient(BuildContext context, {required Recipient recipient}) { context.pushNamed(PayoutRoutes.editRecipient); } -void _openWalletEdit(BuildContext context, Wallet wallet) { - context.read().selectWallet(wallet); - context.read().selectWallet(wallet); - context.pushToEditWallet(); -} - void _openLedgerEdit(BuildContext context, LedgerAccount account) { context.read().selectLedgerByRef( account.ledgerAccountRef, diff --git a/frontend/pweb/lib/controllers/dashboard/balance/actions_ui.dart b/frontend/pweb/lib/controllers/dashboard/balance/actions_ui.dart new file mode 100644 index 00000000..ff772cfd --- /dev/null +++ b/frontend/pweb/lib/controllers/dashboard/balance/actions_ui.dart @@ -0,0 +1,19 @@ +import 'package:flutter/foundation.dart'; + + +class BalanceActionsUiController extends ChangeNotifier { + int? _hoveredButtonIndex; + + int? get hoveredButtonIndex => _hoveredButtonIndex; + + bool isExpanded(int index) => _hoveredButtonIndex == index; + + void onHoverChanged(int index, bool hovered) { + final next = hovered + ? index + : (_hoveredButtonIndex == index ? null : _hoveredButtonIndex); + if (next == _hoveredButtonIndex) return; + _hoveredButtonIndex = next; + notifyListeners(); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/controller.dart b/frontend/pweb/lib/controllers/dashboard/balance/carousel.dart similarity index 75% rename from frontend/pweb/lib/pages/dashboard/buttons/balance/controller.dart rename to frontend/pweb/lib/controllers/dashboard/balance/carousel.dart index 88176825..f7129118 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/controller.dart +++ b/frontend/pweb/lib/controllers/dashboard/balance/carousel.dart @@ -1,15 +1,22 @@ -import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pshared/provider/ledger.dart'; import 'package:pweb/models/dashboard/balance_item.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/config.dart'; -class BalanceCarouselController with ChangeNotifier { +class BalanceCarouselController extends ChangeNotifier { + BalanceCarouselController() + : pageController = PageController( + viewportFraction: WalletCardConfig.viewportFraction, + ); + WalletsController? _walletsController; List _items = const [BalanceItem.addAction()]; int _index = 0; + final PageController pageController; List get items => _items; int get index => _index; @@ -32,6 +39,7 @@ class BalanceCarouselController with ChangeNotifier { _items = nextItems; _index = nextIndex; + _syncPageController(); if (hasItemsChanged || hasIndexChanged) { notifyListeners(); @@ -50,9 +58,24 @@ class BalanceCarouselController with ChangeNotifier { notifyListeners(); } - void goBack() => onPageChanged(_index - 1); + void goBack() => animateTo(_index - 1); - void goForward() => onPageChanged(_index + 1); + void goForward() => animateTo(_index + 1); + + void animateTo(int index) { + final target = _clampIndex(index, _items.length); + if (!pageController.hasClients) { + onPageChanged(target); + return; + } + pageController.animateToPage( + target, + duration: const Duration(milliseconds: 220), + curve: Curves.easeOut, + ); + } + + void syncPageController() => _syncPageController(); int _resolveNextIndex( List nextItems, @@ -126,4 +149,18 @@ class BalanceCarouselController with ChangeNotifier { if (walletsController.selectedWallet?.id == wallet.id) return; walletsController.selectWallet(wallet); } + + void _syncPageController() { + if (!pageController.hasClients || _items.isEmpty) return; + final current = pageController.page?.round(); + final target = _clampIndex(_index, _items.length); + if (current == target) return; + pageController.jumpToPage(target); + } + + @override + void dispose() { + pageController.dispose(); + super.dispose(); + } } diff --git a/frontend/pweb/lib/controllers/dashboard/balance/source_actions.dart b/frontend/pweb/lib/controllers/dashboard/balance/source_actions.dart new file mode 100644 index 00000000..c055e5c6 --- /dev/null +++ b/frontend/pweb/lib/controllers/dashboard/balance/source_actions.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +import 'package:pshared/controllers/payment/source.dart'; +import 'package:pshared/models/payment/type.dart'; + +import 'package:pweb/app/router/payout_routes.dart'; + + +class BalanceActionButtonState { + final String label; + final IconData icon; + final VoidCallback onPressed; + + const BalanceActionButtonState({ + required this.label, + required this.icon, + required this.onPressed, + }); +} + +class BalanceActionsState { + final BalanceActionButtonState topLeading; + final BalanceActionButtonState topTrailing; + final BalanceActionButtonState bottom; + + const BalanceActionsState({ + required this.topLeading, + required this.topTrailing, + required this.bottom, + }); +} + +class BalanceSourceActionsController { + const BalanceSourceActionsController(); + + BalanceActionsState wallet({ + required BuildContext context, + required String walletRef, + required VoidCallback onAddFunds, + }) { + return BalanceActionsState( + topLeading: BalanceActionButtonState( + label: 'Operation History', + icon: Icons.history_rounded, + onPressed: () => _openWalletOperationHistory(context, walletRef), + ), + topTrailing: BalanceActionButtonState( + label: 'Send Payout', + icon: Icons.send_rounded, + onPressed: () => _sendWalletPayout(context, walletRef), + ), + bottom: BalanceActionButtonState( + label: 'Wallet Details / Add Funds', + icon: Icons.account_balance_wallet_rounded, + onPressed: onAddFunds, + ), + ); + } + + BalanceActionsState ledger({ + required BuildContext context, + required String ledgerAccountRef, + required VoidCallback onAddFunds, + required VoidCallback onWalletDetails, + }) { + return BalanceActionsState( + topLeading: BalanceActionButtonState( + label: 'Operation History / Wallet Details', + icon: Icons.receipt_long_rounded, + onPressed: onWalletDetails, + ), + topTrailing: BalanceActionButtonState( + label: 'Send Payout', + icon: Icons.send_rounded, + onPressed: () => _sendLedgerPayout(context, ledgerAccountRef), + ), + bottom: BalanceActionButtonState( + label: 'Add Funds', + icon: Icons.add_card_rounded, + onPressed: onAddFunds, + ), + ); + } + + void _openWalletOperationHistory(BuildContext context, String walletRef) { + context.read().selectWalletByRef(walletRef); + context.pushNamed(PayoutRoutes.editWallet); + } + + void _sendWalletPayout(BuildContext context, String walletRef) { + context.read().selectWalletByRef(walletRef); + context.pushNamed( + PayoutRoutes.payment, + queryParameters: PayoutRoutes.buildQueryParameters( + paymentType: PaymentType.wallet, + ), + ); + } + + void _sendLedgerPayout(BuildContext context, String ledgerAccountRef) { + context.read().selectLedgerByRef(ledgerAccountRef); + context.pushNamed( + PayoutRoutes.payment, + queryParameters: PayoutRoutes.buildQueryParameters( + paymentType: PaymentType.ledger, + ), + ); + } +} diff --git a/frontend/pweb/lib/controllers/dashboard/balance/source_copy.dart b/frontend/pweb/lib/controllers/dashboard/balance/source_copy.dart new file mode 100644 index 00000000..787a54c7 --- /dev/null +++ b/frontend/pweb/lib/controllers/dashboard/balance/source_copy.dart @@ -0,0 +1,35 @@ +import 'package:flutter/services.dart'; + + +class BalanceCopyState { + final String label; + final String payload; + + const BalanceCopyState({required this.label, required this.payload}); + + bool get canCopy => payload.trim().isNotEmpty; +} + +class BalanceSourceCopyController { + const BalanceSourceCopyController(); + + BalanceCopyState wallet(String? depositAddress) { + return BalanceCopyState( + label: 'Copy Deposit Address', + payload: depositAddress?.trim() ?? '', + ); + } + + BalanceCopyState ledger(String? accountCode) { + return BalanceCopyState( + label: 'Copy Deposit Address', + payload: accountCode?.trim() ?? '', + ); + } + + Future copy(BalanceCopyState state) async { + if (!state.canCopy) return false; + await Clipboard.setData(ClipboardData(text: state.payload)); + return true; + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/actions/bar.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/actions/bar.dart new file mode 100644 index 00000000..e4c4b62c --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/actions/bar.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/controllers/dashboard/balance/actions_ui.dart'; +import 'package:pweb/controllers/dashboard/balance/source_actions.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/actions/hover_expandable_action_button.dart'; + + +class BalanceActionsBar extends StatefulWidget { + final BalanceActionsState state; + + const BalanceActionsBar({super.key, required this.state}); + + @override + State createState() => _BalanceActionsBarState(); +} + +class _BalanceActionsBarState extends State { + static const double _buttonHeight = 34.0; + static const double _buttonGap = 6.0; + static const double _iconSize = 18.0; + static const double _textGap = 8.0; + static const double _horizontalPadding = 6.0; + + final BalanceActionsUiController _uiController = BalanceActionsUiController(); + + @override + void dispose() { + _uiController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textStyle = Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w400, + color: colorScheme.onSecondary, + fontSize: 14, + ); + final buttons = [ + widget.state.topLeading, + widget.state.topTrailing, + widget.state.bottom, + ]; + + return ListenableBuilder( + listenable: _uiController, + builder: (context, _) { + return Align( + alignment: Alignment.centerRight, + child: OverflowBox( + alignment: Alignment.centerRight, + minWidth: 0, + maxWidth: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + for (var i = 0; i < buttons.length; i++) ...[ + HoverExpandableActionButton( + height: _buttonHeight, + icon: buttons[i].icon, + label: buttons[i].label, + iconSize: _iconSize, + textStyle: textStyle, + expanded: _uiController.isExpanded(i), + textGap: _textGap, + horizontalPadding: _horizontalPadding, + onHoverChanged: (hovered) => + _uiController.onHoverChanged(i, hovered), + onPressed: buttons[i].onPressed, + ), + if (i != buttons.length - 1) + const SizedBox(height: _buttonGap), + ], + ], + ), + ), + ); + }, + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/actions/hover_expandable_action_button.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/actions/hover_expandable_action_button.dart new file mode 100644 index 00000000..3dba3dea --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/actions/hover_expandable_action_button.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; + + +class HoverExpandableActionButton extends StatelessWidget { + final double height; + final IconData icon; + final String label; + final double iconSize; + final TextStyle? textStyle; + final bool expanded; + final double textGap; + final double horizontalPadding; + final ValueChanged onHoverChanged; + final VoidCallback onPressed; + + const HoverExpandableActionButton({ + super.key, + required this.height, + required this.icon, + required this.label, + required this.iconSize, + required this.textStyle, + required this.expanded, + required this.textGap, + required this.horizontalPadding, + required this.onHoverChanged, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return MouseRegion( + onEnter: (_) => onHoverChanged(true), + onExit: (_) => onHoverChanged(false), + child: AnimatedContainer( + duration: const Duration(milliseconds: 220), + curve: Curves.easeOutCubic, + height: height, + decoration: BoxDecoration( + color: colorScheme.primaryFixed, + borderRadius: BorderRadius.circular(999), + ), + child: Material( + type: MaterialType.transparency, + child: InkWell( + borderRadius: BorderRadius.circular(999), + onTap: onPressed, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: iconSize, color: colorScheme.onSecondary), + AnimatedSize( + duration: const Duration(milliseconds: 220), + curve: Curves.easeOutCubic, + alignment: Alignment.centerRight, + child: expanded + ? Padding( + padding: EdgeInsets.only(left: textGap), + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.visible, + style: textStyle, + ), + ) + : const SizedBox.shrink(), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/amount.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/amount.dart index 3871379d..2eb8c096 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/amount.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/amount.dart @@ -29,12 +29,17 @@ class BalanceAmount extends StatelessWidget { final isMasked = wallets.isBalanceMasked(wallet.id); return Row( + mainAxisSize: MainAxisSize.min, children: [ Text( - isMasked ? '•••• $currencyBalance' : '${amountToString(wallet.balance)} $currencyBalance', - style: textTheme.headlineSmall?.copyWith( + isMasked + ? '•••• $currencyBalance' + : '${amountToString(wallet.balance)} $currencyBalance', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, - color: colorScheme.onSurface, + color: colorScheme.primary, ), ), const SizedBox(width: _iconSpacing), @@ -43,7 +48,7 @@ class BalanceAmount extends StatelessWidget { child: Icon( isMasked ? Icons.visibility_off : Icons.visibility, size: _iconSize, - color: colorScheme.onSurface, + color: colorScheme.primary, ), ), ], diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel.dart deleted file mode 100644 index 999aba51..00000000 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/gestures.dart'; - -import 'package:pshared/models/ledger/account.dart'; -import 'package:pshared/models/payment/wallet.dart'; - -import 'package:pweb/models/dashboard/balance_item.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/add/card.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/card.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/config.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/indicator.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/ledger.dart'; - - -class BalanceCarousel extends StatefulWidget { - final List items; - final int currentIndex; - final ValueChanged onIndexChanged; - final ValueChanged onTopUp; - final ValueChanged onLedgerAddFunds; - final ValueChanged onWalletTap; - final ValueChanged onLedgerTap; - - const BalanceCarousel({ - super.key, - required this.items, - required this.currentIndex, - required this.onIndexChanged, - required this.onTopUp, - required this.onLedgerAddFunds, - required this.onWalletTap, - required this.onLedgerTap, - }); - - @override - State createState() => _BalanceCarouselState(); -} - -class _BalanceCarouselState extends State { - late final PageController _controller; - - @override - void initState() { - super.initState(); - _controller = PageController( - initialPage: widget.currentIndex, - viewportFraction: WalletCardConfig.viewportFraction, - ); - } - - @override - void didUpdateWidget(covariant BalanceCarousel oldWidget) { - super.didUpdateWidget(oldWidget); - if (!mounted) return; - if (_controller.hasClients) { - final currentPage = _controller.page?.round(); - if (currentPage != widget.currentIndex) { - _controller.jumpToPage(widget.currentIndex); - } - } - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - void _goToPage(int index) { - if (!_controller.hasClients) return; - _controller.animateToPage( - index, - duration: const Duration(milliseconds: 220), - curve: Curves.easeOut, - ); - } - - @override - Widget build(BuildContext context) { - if (widget.items.isEmpty) { - return const SizedBox.shrink(); - } - - final safeIndex = widget.currentIndex.clamp(0, widget.items.length - 1); - final scrollBehavior = ScrollConfiguration.of(context).copyWith( - dragDevices: const { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - PointerDeviceKind.trackpad, - }, - ); - - return Column( - children: [ - SizedBox( - height: WalletCardConfig.cardHeight, - child: MouseRegion( - cursor: SystemMouseCursors.grab, - child: ScrollConfiguration( - behavior: scrollBehavior, - child: PageView.builder( - controller: _controller, - onPageChanged: widget.onIndexChanged, - itemCount: widget.items.length, - itemBuilder: (context, index) { - final item = widget.items[index]; - final Widget card = switch (item) { - WalletBalanceItem(:final wallet) => WalletCard( - wallet: wallet, - onTopUp: () => widget.onTopUp(wallet), - onTap: () => widget.onWalletTap(wallet), - ), - LedgerBalanceItem(:final account) => LedgerAccountCard( - account: account, - onTap: () => widget.onLedgerTap(account), - onAddFunds: () => widget.onLedgerAddFunds(account), - ), - AddBalanceActionItem() => const AddBalanceCard(), - }; - - return Padding( - padding: WalletCardConfig.cardPadding, - child: card, - ); - }, - ), - ), - ), - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - onPressed: safeIndex > 0 ? () => _goToPage(safeIndex - 1) : null, - icon: const Icon(Icons.arrow_back), - ), - const SizedBox(width: 16), - CarouselIndicator(itemCount: widget.items.length, index: safeIndex), - const SizedBox(width: 16), - IconButton( - onPressed: safeIndex < widget.items.length - 1 - ? () => _goToPage(safeIndex + 1) - : null, - icon: const Icon(Icons.arrow_forward), - ), - ], - ), - ], - ); - } -} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/card_item.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/card_item.dart new file mode 100644 index 00000000..d672321e --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/card_item.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/ledger/account.dart'; +import 'package:pshared/models/payment/wallet.dart'; + +import 'package:pweb/models/dashboard/balance_item.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/add/card.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/config.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/source/cards/ledger.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/source/cards/wallet.dart'; + + +class BalanceCarouselCardItem extends StatelessWidget { + final BalanceItem item; + final ValueChanged onTopUp; + final ValueChanged onLedgerAddFunds; + final ValueChanged onWalletTap; + final ValueChanged onLedgerTap; + + const BalanceCarouselCardItem({ + super.key, + required this.item, + required this.onTopUp, + required this.onLedgerAddFunds, + required this.onWalletTap, + required this.onLedgerTap, + }); + + @override + Widget build(BuildContext context) { + final card = switch (item) { + WalletBalanceItem(:final wallet) => WalletCard( + wallet: wallet, + onTopUp: () => onTopUp(wallet), + onTap: () => onWalletTap(wallet), + ), + LedgerBalanceItem(:final account) => LedgerAccountCard( + account: account, + onTap: () => onLedgerTap(account), + onAddFunds: () => onLedgerAddFunds(account), + ), + AddBalanceActionItem() => const AddBalanceCard(), + }; + + return Padding(padding: WalletCardConfig.cardPadding, child: card); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/cards_view.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/cards_view.dart new file mode 100644 index 00000000..94a5fee8 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/cards_view.dart @@ -0,0 +1,61 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +import 'package:pshared/models/ledger/account.dart'; +import 'package:pshared/models/payment/wallet.dart'; + +import 'package:pweb/controllers/dashboard/balance/carousel.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/carousel/card_item.dart'; + + +class BalanceCarouselCardsView extends StatelessWidget { + final BalanceCarouselController controller; + final ValueChanged onTopUp; + final ValueChanged onLedgerAddFunds; + final ValueChanged onWalletTap; + final ValueChanged onLedgerTap; + final double height; + + const BalanceCarouselCardsView({ + super.key, + required this.controller, + required this.onTopUp, + required this.onLedgerAddFunds, + required this.onWalletTap, + required this.onLedgerTap, + required this.height, + }); + + @override + Widget build(BuildContext context) { + final scrollBehavior = ScrollConfiguration.of(context).copyWith( + dragDevices: const { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + PointerDeviceKind.trackpad, + }, + ); + + return SizedBox( + height: height, + child: MouseRegion( + cursor: SystemMouseCursors.grab, + child: ScrollConfiguration( + behavior: scrollBehavior, + child: PageView.builder( + controller: controller.pageController, + onPageChanged: controller.onPageChanged, + itemCount: controller.items.length, + itemBuilder: (context, index) => BalanceCarouselCardItem( + item: controller.items[index], + onTopUp: onTopUp, + onLedgerAddFunds: onLedgerAddFunds, + onWalletTap: onWalletTap, + onLedgerTap: onLedgerTap, + ), + ), + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/carousel.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/carousel.dart new file mode 100644 index 00000000..52544c17 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/carousel.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/ledger/account.dart'; +import 'package:pshared/models/payment/wallet.dart'; + +import 'package:pweb/controllers/dashboard/balance/carousel.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/carousel/cards_view.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/carousel/navigation.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/config.dart'; + + +class BalanceCarousel extends StatelessWidget { + final BalanceCarouselController controller; + final ValueChanged onTopUp; + final ValueChanged onLedgerAddFunds; + final ValueChanged onWalletTap; + final ValueChanged onLedgerTap; + + const BalanceCarousel({ + super.key, + required this.controller, + required this.onTopUp, + required this.onLedgerAddFunds, + required this.onWalletTap, + required this.onLedgerTap, + }); + + @override + Widget build(BuildContext context) { + if (controller.items.isEmpty) { + return const SizedBox.shrink(); + } + + WidgetsBinding.instance.addPostFrameCallback((_) { + controller.syncPageController(); + }); + + final safeIndex = controller.index.clamp(0, controller.items.length - 1); + + return LayoutBuilder( + builder: (context, constraints) { + final cardHeight = WalletCardConfig.cardHeightForWidth( + constraints.maxWidth, + ); + + return Column( + children: [ + BalanceCarouselCardsView( + controller: controller, + onTopUp: onTopUp, + onLedgerAddFunds: onLedgerAddFunds, + onWalletTap: onWalletTap, + onLedgerTap: onLedgerTap, + height: cardHeight, + ), + const SizedBox(height: 16), + BalanceCarouselNavigation(controller: controller, index: safeIndex), + ], + ); + }, + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/navigation.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/navigation.dart new file mode 100644 index 00000000..33118d22 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/navigation.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/controllers/dashboard/balance/carousel.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/indicator.dart'; + + +class BalanceCarouselNavigation extends StatelessWidget { + final BalanceCarouselController controller; + final int index; + + const BalanceCarouselNavigation({ + super.key, + required this.controller, + required this.index, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: index > 0 ? controller.goBack : null, + icon: const Icon(Icons.arrow_back), + ), + const SizedBox(width: 16), + CarouselIndicator(itemCount: controller.items.length, index: index), + const SizedBox(width: 16), + IconButton( + onPressed: index < controller.items.length - 1 + ? controller.goForward + : null, + icon: const Icon(Icons.arrow_forward), + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/config.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/config.dart index a70b9234..269ae6c8 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/config.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/config.dart @@ -2,14 +2,21 @@ import 'package:flutter/material.dart'; abstract class WalletCardConfig { - static const double cardHeight = 145.0; static const double elevation = 4.0; static const double borderRadius = 16.0; - static const double viewportFraction = 0.9; - - static const EdgeInsets cardPadding = EdgeInsets.symmetric(horizontal: 8); - static const EdgeInsets contentPadding = EdgeInsets.all(16); - + static const double viewportFraction = 0.96; + + static const EdgeInsets cardPadding = EdgeInsets.symmetric(horizontal: 6); + static const EdgeInsets contentPadding = EdgeInsets.symmetric( + horizontal: 28, + vertical: 16, + ); + static const double dotSize = 8.0; static const EdgeInsets dotMargin = EdgeInsets.symmetric(horizontal: 4); + + static double cardHeightForWidth(double width) { + final adaptiveHeight = width * 0.18; + return adaptiveHeight.clamp(150.0, 230.0); + } } diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/header.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/header.dart index a00a2075..3e0eb4cb 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/header.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/header.dart @@ -20,46 +20,51 @@ class BalanceHeader extends StatelessWidget { final subtitleText = subtitle?.trim(); final badgeText = badge?.trim(); - return Row( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( title, - style: textTheme.titleMedium?.copyWith( - color: colorScheme.onSurface, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: textTheme.titleLarge?.copyWith( + color: colorScheme.primary, + fontWeight: FontWeight.w700, ), ), - if (subtitleText != null && subtitleText.isNotEmpty) - Text( - subtitleText, - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - fontWeight: FontWeight.w500, + ), + if (badgeText != null && badgeText.isNotEmpty) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: colorScheme.primaryFixed, + borderRadius: BorderRadius.circular(999), + ), + child: Text( + badgeText, + style: textTheme.labelSmall?.copyWith( + color: colorScheme.onSecondary, + fontWeight: FontWeight.w700, ), ), - ], - ), - ), - if (badgeText != null && badgeText.isNotEmpty) ...[ - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - decoration: BoxDecoration( - color: colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(999), - ), - child: Text( - badgeText, - style: textTheme.bodyMedium?.copyWith( - color: colorScheme.onPrimaryContainer, - fontWeight: FontWeight.w600, ), + ], + ], + ), + if (subtitleText != null && subtitleText.isNotEmpty) + Text( + subtitleText, + style: textTheme.titleSmall?.copyWith( + color: colorScheme.primaryFixed, + fontWeight: FontWeight.w500, ), + maxLines: 1, ), - ], ], ); } diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger_amount.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger_amount.dart index fe0c60c4..8922b636 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger_amount.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger_amount.dart @@ -26,14 +26,15 @@ class LedgerBalanceAmount extends StatelessWidget { : LedgerBalanceFormatter.format(account); return Row( + mainAxisSize: MainAxisSize.min, children: [ - Flexible( - child: Text( - balance, - style: textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), + Text( + balance, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.primary, ), ), const SizedBox(width: 12), @@ -44,7 +45,7 @@ class LedgerBalanceAmount extends StatelessWidget { child: Icon( isMasked ? Icons.visibility_off : Icons.visibility, size: 24, - color: colorScheme.onSurface, + color: colorScheme.primary, ), ), ], diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/providers.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/providers.dart index 1de0dac2..8f0f2b35 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/providers.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/providers.dart @@ -5,7 +5,8 @@ import 'package:provider/provider.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pshared/provider/ledger.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/controller.dart'; +import 'package:pweb/controllers/dashboard/balance/carousel.dart'; + class BalanceWidgetProviders extends StatelessWidget { final Widget child; diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/ledger.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/ledger.dart index 3a58413a..10e7711f 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/ledger.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/ledger.dart @@ -1,44 +1,31 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import 'package:pshared/provider/ledger.dart'; - +import 'package:pweb/controllers/dashboard/balance/source_actions.dart'; import 'package:pweb/pages/dashboard/buttons/balance/actions/bar.dart'; -import 'package:pweb/generated/i18n/app_localizations.dart'; - class LedgerSourceActions extends StatelessWidget { final String ledgerAccountRef; final VoidCallback onAddFunds; + final VoidCallback onWalletDetails; const LedgerSourceActions({ super.key, required this.ledgerAccountRef, required this.onAddFunds, + required this.onWalletDetails, }); @override Widget build(BuildContext context) { - final ledgerProvider = context.watch(); - final loc = AppLocalizations.of(context)!; - final isBusy = - ledgerProvider.isWalletRefreshing(ledgerAccountRef) || - ledgerProvider.isLoading; - final hasTarget = ledgerProvider.accounts.any( - (a) => a.ledgerAccountRef == ledgerAccountRef, + const controller = BalanceSourceActionsController(); + final state = controller.ledger( + context: context, + ledgerAccountRef: ledgerAccountRef, + onAddFunds: onAddFunds, + onWalletDetails: onWalletDetails, ); - return BalanceActionsBar( - isRefreshBusy: isBusy, - canRefresh: hasTarget, - onRefresh: () { - context.read().refreshBalance(ledgerAccountRef); - }, - onAddFunds: onAddFunds, - refreshLabel: loc.refreshBalance, - addFundsLabel: loc.addFunds, - ); + return BalanceActionsBar(state: state); } } diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/wallet.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/wallet.dart index 8c872026..e699e513 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/wallet.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/actions/wallet.dart @@ -1,13 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import 'package:pshared/provider/payment/wallets.dart'; - +import 'package:pweb/controllers/dashboard/balance/source_actions.dart'; import 'package:pweb/pages/dashboard/buttons/balance/actions/bar.dart'; -import 'package:pweb/generated/i18n/app_localizations.dart'; - class WalletSourceActions extends StatelessWidget { final String walletRef; @@ -21,22 +16,13 @@ class WalletSourceActions extends StatelessWidget { @override Widget build(BuildContext context) { - final walletsProvider = context.watch(); - final loc = AppLocalizations.of(context)!; - final isBusy = - walletsProvider.isWalletRefreshing(walletRef) || - walletsProvider.isLoading; - final hasTarget = walletsProvider.wallets.any((w) => w.id == walletRef); - - return BalanceActionsBar( - isRefreshBusy: isBusy, - canRefresh: hasTarget, - onRefresh: () { - context.read().refreshBalance(walletRef); - }, + const controller = BalanceSourceActionsController(); + final state = controller.wallet( + context: context, + walletRef: walletRef, onAddFunds: onAddFunds, - refreshLabel: loc.refreshBalance, - addFundsLabel: loc.addFunds, ); + + return BalanceActionsBar(state: state); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card.dart index bf56777f..5e511236 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card.dart @@ -9,11 +9,15 @@ import 'package:pshared/models/payment/source_type.dart'; import 'package:pshared/models/payment/wallet.dart'; import 'package:pshared/utils/l10n/chain.dart'; +import 'package:pweb/controllers/dashboard/balance/source_copy.dart'; +import 'package:pweb/models/state/visibility.dart'; import 'package:pweb/pages/dashboard/buttons/balance/amount.dart'; import 'package:pweb/pages/dashboard/buttons/balance/ledger_amount.dart'; import 'package:pweb/pages/dashboard/buttons/balance/source/actions/ledger.dart'; import 'package:pweb/pages/dashboard/buttons/balance/source/actions/wallet.dart'; import 'package:pweb/pages/dashboard/buttons/balance/source/card_layout.dart'; +import 'package:pweb/widgets/refresh_balance/ledger.dart'; +import 'package:pweb/widgets/refresh_balance/wallet.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; @@ -24,6 +28,8 @@ class BalanceSourceCard extends StatelessWidget { final LedgerAccount? _ledgerAccount; final VoidCallback onTap; final VoidCallback onAddFunds; + static const BalanceSourceCopyController _copyController = + BalanceSourceCopyController(); const BalanceSourceCard.wallet({ super.key, @@ -55,12 +61,29 @@ class BalanceSourceCard extends StatelessWidget { ? null : wallet.network!.localizedName(context); final symbol = wallet.tokenSymbol?.trim(); + final copyState = _copyController.wallet(wallet.depositAddress); return BalanceSourceCardLayout( title: wallet.name, subtitle: networkLabel, badge: (symbol == null || symbol.isEmpty) ? null : symbol, - onTap: onTap, + onTap: null, + copyLabel: copyState.label, + canCopy: copyState.canCopy, + onCopy: copyState.canCopy + ? () async { + final copied = await _copyController.copy(copyState); + if (!copied || !context.mounted) return; + final loc = AppLocalizations.of(context)!; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(loc.addressCopied))); + } + : null, + refreshButton: WalletBalanceRefreshButton( + walletRef: wallet.id, + iconOnly: VisibilityState.hidden, + ), actions: WalletSourceActions( walletRef: wallet.id, onAddFunds: onAddFunds, @@ -79,19 +102,35 @@ class BalanceSourceCard extends StatelessWidget { final accountName = account.name.trim(); final accountCode = account.accountCode.trim(); final title = accountName.isNotEmpty ? accountName : loc.paymentTypeLedger; - final subtitle = accountCode.isNotEmpty ? accountCode : null; final badge = account.currency.trim().isEmpty ? null : account.currency.toUpperCase(); + final copyState = _copyController.ledger(accountCode); return BalanceSourceCardLayout( title: title, - subtitle: subtitle, + subtitle: null, badge: badge, onTap: onTap, + copyLabel: copyState.label, + canCopy: copyState.canCopy, + onCopy: copyState.canCopy + ? () async { + final copied = await _copyController.copy(copyState); + if (!copied || !context.mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(loc.addressCopied))); + } + : null, + refreshButton: LedgerBalanceRefreshButton( + ledgerAccountRef: account.ledgerAccountRef, + iconOnly: VisibilityState.hidden, + ), actions: LedgerSourceActions( ledgerAccountRef: account.ledgerAccountRef, onAddFunds: onAddFunds, + onWalletDetails: onTap, ), amount: LedgerBalanceAmount(account: account), ); diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card_layout.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card_layout.dart index 9f85c09b..ef3626ef 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card_layout.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card_layout.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:pweb/pages/dashboard/buttons/balance/config.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/header.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/source/layout/wide_body.dart'; class BalanceSourceCardLayout extends StatelessWidget { @@ -9,8 +9,12 @@ class BalanceSourceCardLayout extends StatelessWidget { final String? subtitle; final String? badge; final Widget amount; + final Widget refreshButton; final Widget actions; - final VoidCallback onTap; + final VoidCallback? onTap; + final String copyLabel; + final bool canCopy; + final VoidCallback? onCopy; const BalanceSourceCardLayout({ super.key, @@ -18,40 +22,39 @@ class BalanceSourceCardLayout extends StatelessWidget { required this.subtitle, required this.badge, required this.amount, + required this.refreshButton, required this.actions, required this.onTap, + required this.copyLabel, + required this.canCopy, + required this.onCopy, }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; + final borderRadius = BorderRadius.circular(WalletCardConfig.borderRadius); return Card( color: colorScheme.onSecondary, elevation: WalletCardConfig.elevation, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius), - ), + shape: RoundedRectangleBorder(borderRadius: borderRadius), child: InkWell( - borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius), + borderRadius: borderRadius, onTap: onTap, child: SizedBox.expand( child: Padding( padding: WalletCardConfig.contentPadding, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BalanceHeader(title: title, subtitle: subtitle, badge: badge), - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Expanded(child: amount), - const SizedBox(width: 12), - actions, - ], - ), - ], + child: BalanceSourceBody( + title: title, + subtitle: subtitle, + badge: badge, + amount: amount, + refreshButton: refreshButton, + actions: actions, + copyLabel: copyLabel, + canCopy: canCopy, + onCopy: onCopy, ), ), ), diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/cards/ledger.dart similarity index 100% rename from frontend/pweb/lib/pages/dashboard/buttons/balance/ledger.dart rename to frontend/pweb/lib/pages/dashboard/buttons/balance/source/cards/ledger.dart diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/card.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/cards/wallet.dart similarity index 100% rename from frontend/pweb/lib/pages/dashboard/buttons/balance/card.dart rename to frontend/pweb/lib/pages/dashboard/buttons/balance/source/cards/wallet.dart diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/layout/amount_with_refresh.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/layout/amount_with_refresh.dart new file mode 100644 index 00000000..6d6303fb --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/layout/amount_with_refresh.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + + +class BalanceAmountWithRefresh extends StatelessWidget { + final Widget amount; + final Widget refreshButton; + + const BalanceAmountWithRefresh({ + super.key, + required this.amount, + required this.refreshButton, + }); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButtonTheme( + data: IconButtonThemeData( + style: IconButton.styleFrom( + minimumSize: const Size(30, 30), + maximumSize: const Size(40, 40), + padding: EdgeInsets.zero, + foregroundColor: colorScheme.primary, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + child: refreshButton, + ), + const SizedBox(width: 8), + amount, + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/layout/copyable_field.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/layout/copyable_field.dart new file mode 100644 index 00000000..4b709f5e --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/layout/copyable_field.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + + +class BalanceCopyableField extends StatelessWidget { + final String label; + final bool canCopy; + final VoidCallback? onCopy; + + const BalanceCopyableField({ + super.key, + required this.label, + required this.canCopy, + required this.onCopy, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + return Container( + decoration: BoxDecoration( + color: colorScheme.onSecondary, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: colorScheme.primaryFixed, width: 0.6), + ), + child: InkWell( + onTap: canCopy ? onCopy : null, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.copy_rounded, + size: 16, + color: canCopy + ? colorScheme.primaryFixed + : colorScheme.primary.withValues(alpha: 0.35), + ), + const SizedBox(width: 6), + Flexible( + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.labelMedium?.copyWith( + color: canCopy + ? colorScheme.primaryFixed + : colorScheme.primary.withValues(alpha: 0.45), + fontWeight: FontWeight.normal, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/layout/wide_body.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/layout/wide_body.dart new file mode 100644 index 00000000..648e6a61 --- /dev/null +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/layout/wide_body.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/pages/dashboard/buttons/balance/header.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/source/layout/amount_with_refresh.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/source/layout/copyable_field.dart'; + + +class BalanceSourceBody extends StatelessWidget { + final String title; + final String? subtitle; + final String? badge; + final Widget amount; + final Widget refreshButton; + final Widget actions; + final String copyLabel; + final bool canCopy; + final VoidCallback? onCopy; + + const BalanceSourceBody({ + super.key, + required this.title, + required this.subtitle, + required this.badge, + required this.amount, + required this.refreshButton, + required this.actions, + required this.copyLabel, + required this.canCopy, + required this.onCopy, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final sideMaxWidth = constraints.maxWidth * 0.30; + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Flexible( + fit: FlexFit.loose, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: sideMaxWidth), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + BalanceHeader( + title: title, + subtitle: subtitle, + badge: badge, + ), + SizedBox(height: constraints.maxHeight * 0.06), + BalanceCopyableField( + label: copyLabel, + canCopy: canCopy, + onCopy: onCopy, + ), + ], + ), + ), + ), + Expanded( + child: Align( + alignment: Alignment.center, + child: FittedBox( + fit: BoxFit.scaleDown, + child: BalanceAmountWithRefresh( + amount: amount, + refreshButton: refreshButton, + ), + ), + ), + ), + Flexible( + fit: FlexFit.loose, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: sideMaxWidth), + child: SizedBox(height: constraints.maxHeight, child: actions), + ), + ), + ], + ); + }, + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/widget.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/widget.dart index 65027d9c..ca9ce1c5 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/widget.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/widget.dart @@ -7,11 +7,12 @@ import 'package:pshared/models/ledger/account.dart'; import 'package:pshared/provider/ledger.dart'; import 'package:pshared/models/payment/wallet.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/carousel.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/controller.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/carousel/carousel.dart'; +import 'package:pweb/controllers/dashboard/balance/carousel.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; + class BalanceWidget extends StatelessWidget { final ValueChanged onTopUp; final ValueChanged onLedgerAddFunds; @@ -46,9 +47,7 @@ class BalanceWidget extends StatelessWidget { } final carouselWidget = BalanceCarousel( - items: carousel.items, - currentIndex: carousel.index, - onIndexChanged: carousel.onPageChanged, + controller: carousel, onTopUp: onTopUp, onLedgerAddFunds: onLedgerAddFunds, onWalletTap: onWalletTap, diff --git a/frontend/pweb/lib/pages/dashboard/payouts/amount/feild.dart b/frontend/pweb/lib/pages/dashboard/payouts/amount/field.dart similarity index 100% rename from frontend/pweb/lib/pages/dashboard/payouts/amount/feild.dart rename to frontend/pweb/lib/pages/dashboard/payouts/amount/field.dart diff --git a/frontend/pweb/lib/pages/dashboard/payouts/amount/widget.dart b/frontend/pweb/lib/pages/dashboard/payouts/amount/widget.dart index 4ddaa3e7..7176d668 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/amount/widget.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/amount/widget.dart @@ -6,7 +6,8 @@ import 'package:pshared/controllers/payment/source.dart'; import 'package:pshared/provider/payment/amount.dart'; import 'package:pweb/controllers/payments/amount_field.dart'; -import 'package:pweb/pages/dashboard/payouts/amount/feild.dart'; +import 'package:pweb/pages/dashboard/payouts/amount/field.dart'; + class PaymentAmountWidget extends StatelessWidget { const PaymentAmountWidget({super.key}); -- 2.49.1 From 0aceb2f441062d04689ca08735cfe9d645094737 Mon Sep 17 00:00:00 2001 From: Arseni Date: Fri, 6 Mar 2026 18:05:04 +0300 Subject: [PATCH 4/5] added missed loc instead of hardcode --- .../dashboard/balance/source_actions.dart | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/frontend/pweb/lib/controllers/dashboard/balance/source_actions.dart b/frontend/pweb/lib/controllers/dashboard/balance/source_actions.dart index c055e5c6..a2df9f83 100644 --- a/frontend/pweb/lib/controllers/dashboard/balance/source_actions.dart +++ b/frontend/pweb/lib/controllers/dashboard/balance/source_actions.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; + import 'package:provider/provider.dart'; import 'package:pshared/controllers/payment/source.dart'; @@ -8,6 +9,8 @@ import 'package:pshared/models/payment/type.dart'; import 'package:pweb/app/router/payout_routes.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class BalanceActionButtonState { final String label; @@ -41,19 +44,20 @@ class BalanceSourceActionsController { required String walletRef, required VoidCallback onAddFunds, }) { + final l10n = AppLocalizations.of(context)!; return BalanceActionsState( topLeading: BalanceActionButtonState( - label: 'Operation History', + label: l10n.operationfryTitle, icon: Icons.history_rounded, onPressed: () => _openWalletOperationHistory(context, walletRef), ), topTrailing: BalanceActionButtonState( - label: 'Send Payout', + label: l10n.send, icon: Icons.send_rounded, onPressed: () => _sendWalletPayout(context, walletRef), ), bottom: BalanceActionButtonState( - label: 'Wallet Details / Add Funds', + label: '${l10n.details} / ${l10n.addFunds}', icon: Icons.account_balance_wallet_rounded, onPressed: onAddFunds, ), @@ -66,19 +70,20 @@ class BalanceSourceActionsController { required VoidCallback onAddFunds, required VoidCallback onWalletDetails, }) { + final l10n = AppLocalizations.of(context)!; return BalanceActionsState( topLeading: BalanceActionButtonState( - label: 'Operation History / Wallet Details', + label: '${l10n.operationfryTitle} / ${l10n.details}', icon: Icons.receipt_long_rounded, onPressed: onWalletDetails, ), topTrailing: BalanceActionButtonState( - label: 'Send Payout', + label: l10n.send, icon: Icons.send_rounded, onPressed: () => _sendLedgerPayout(context, ledgerAccountRef), ), bottom: BalanceActionButtonState( - label: 'Add Funds', + label: l10n.addFunds, icon: Icons.add_card_rounded, onPressed: onAddFunds, ), -- 2.49.1 From 0172176978d720ca4eaa1a37dc1942a2cd93ba78 Mon Sep 17 00:00:00 2001 From: Arseni Date: Wed, 11 Mar 2026 18:26:21 +0300 Subject: [PATCH 5/5] fixes --- .../pshared/lib/data/dto/payment/payment.dart | 4 +- .../data/mapper/payment/payment_response.dart | 4 +- .../pshared/lib/models/payment/payment.dart | 4 +- frontend/pshared/lib/utils/currency.dart | 15 ++- frontend/pshared/lib/utils/money.dart | 29 ++++ .../dashboard/balance/source_actions.dart | 29 +++- .../dashboard/balance/source_copy.dart | 30 +++-- .../operations/report_operations.dart | 22 ++-- frontend/pweb/lib/l10n/en.arb | 5 +- frontend/pweb/lib/l10n/ru.arb | 5 +- .../lib/pages/address_book/form/view.dart | 2 +- .../form/widgets/payment_methods/panel.dart | 115 ---------------- .../widgets/payment_methods/panel/panel.dart | 83 ++++++++++++ .../panel/panel_container.dart | 29 ++++ .../panel/panel_entry_form.dart | 41 ++++++ .../payment_methods/panel/panel_header.dart | 56 ++++++++ .../dashboard/buttons/balance/amount.dart | 14 +- .../buttons/balance/carousel/card_item.dart | 6 +- .../buttons/balance/ledger_amount.dart | 2 +- .../buttons/balance/source/card.dart | 4 +- .../buttons/balance/source/cards/wallet.dart | 28 ---- .../pages/dashboard/payouts/amount/field.dart | 4 +- .../multiple/panels/source_quote/helpers.dart | 37 ------ .../multiple/panels/source_quote/summary.dart | 27 +++- .../pages/dashboard/payouts/summary/row.dart | 10 +- .../widgets/payment_info/manual_details.dart | 4 +- .../widgets/payment_info/methods_section.dart | 4 +- .../edit/fields/ledger/balance_formatter.dart | 26 +--- .../wallet/edit/fields/ledger/section.dart | 2 +- .../payout_page/wallet/history/table.dart | 91 ------------- .../pages/report/cards/operation_card.dart | 18 ++- .../charts/payout_volumes/totals_list.dart | 1 + .../lib/pages/report/details/sections/fx.dart | 50 +++---- .../report/details/sections/metadata.dart | 67 ---------- .../details/sections/operations/tile.dart | 14 +- .../report/details/summary_card/widget.dart | 18 ++- frontend/pweb/lib/pages/report/table/row.dart | 15 ++- frontend/pweb/lib/utils/money_display.dart | 96 ++++++++++++++ .../payment/method_delete_confirmation.dart | 22 ++++ .../pweb/lib/utils/payment/status_view.dart | 44 ++++--- frontend/pweb/lib/utils/report/format.dart | 35 ++--- .../pweb/lib/utils/report/source_filter.dart | 124 +++++++----------- .../pweb/lib/utils/report/utils/format.dart | 37 ------ .../balance_formatter.dart | 42 +++--- .../dropdown_items.dart | 5 +- .../selector_field.dart | 1 + 46 files changed, 678 insertions(+), 643 deletions(-) delete mode 100644 frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel.dart create mode 100644 frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel.dart create mode 100644 frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel_container.dart create mode 100644 frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel_entry_form.dart create mode 100644 frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel_header.dart delete mode 100644 frontend/pweb/lib/pages/dashboard/buttons/balance/source/cards/wallet.dart delete mode 100644 frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/helpers.dart delete mode 100644 frontend/pweb/lib/pages/payout_page/wallet/history/table.dart delete mode 100644 frontend/pweb/lib/pages/report/details/sections/metadata.dart create mode 100644 frontend/pweb/lib/utils/money_display.dart create mode 100644 frontend/pweb/lib/utils/payment/method_delete_confirmation.dart delete mode 100644 frontend/pweb/lib/utils/report/utils/format.dart diff --git a/frontend/pshared/lib/data/dto/payment/payment.dart b/frontend/pshared/lib/data/dto/payment/payment.dart index 23d7d449..f0d2d9b4 100644 --- a/frontend/pshared/lib/data/dto/payment/payment.dart +++ b/frontend/pshared/lib/data/dto/payment/payment.dart @@ -1,12 +1,12 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:pshared/data/dto/payment/operation.dart'; -import 'package:pshared/data/dto/payment/intent/payment.dart'; import 'package:pshared/data/dto/payment/payment_quote.dart'; import 'package:pshared/data/dto/payment/response_endpoint.dart'; part 'payment.g.dart'; + @JsonSerializable() class PaymentDTO { final String? paymentRef; @@ -15,7 +15,6 @@ class PaymentDTO { final PaymentResponseEndpointDTO? destination; final String? failureCode; final String? failureReason; - final PaymentIntentDTO? intent; final List operations; final PaymentQuoteDTO? lastQuote; final Map? metadata; @@ -28,7 +27,6 @@ class PaymentDTO { this.destination, this.failureCode, this.failureReason, - this.intent, this.operations = const [], this.lastQuote, this.metadata, diff --git a/frontend/pshared/lib/data/mapper/payment/payment_response.dart b/frontend/pshared/lib/data/mapper/payment/payment_response.dart index 242db1ff..7043dbdb 100644 --- a/frontend/pshared/lib/data/mapper/payment/payment_response.dart +++ b/frontend/pshared/lib/data/mapper/payment/payment_response.dart @@ -1,11 +1,11 @@ import 'package:pshared/data/dto/payment/payment.dart'; -import 'package:pshared/data/mapper/payment/intent/payment.dart'; import 'package:pshared/data/mapper/payment/operation.dart'; import 'package:pshared/data/mapper/payment/quote.dart'; import 'package:pshared/data/mapper/payment/response_endpoint.dart'; import 'package:pshared/models/payment/payment.dart'; import 'package:pshared/models/payment/state.dart'; + extension PaymentDTOMapper on PaymentDTO { Payment toDomain() => Payment( paymentRef: paymentRef, @@ -15,7 +15,6 @@ extension PaymentDTOMapper on PaymentDTO { orchestrationState: paymentOrchestrationStateFromValue(state), failureCode: failureCode, failureReason: failureReason, - intent: intent?.toDomain(), operations: operations.map((item) => item.toDomain()).toList(), lastQuote: lastQuote?.toDomain(), metadata: metadata, @@ -31,7 +30,6 @@ extension PaymentMapper on Payment { destination: destination?.toDTO(), failureCode: failureCode, failureReason: failureReason, - intent: intent?.toDTO(), operations: operations.map((item) => item.toDTO()).toList(), lastQuote: lastQuote?.toDTO(), metadata: metadata, diff --git a/frontend/pshared/lib/models/payment/payment.dart b/frontend/pshared/lib/models/payment/payment.dart index dad41559..3d89ca71 100644 --- a/frontend/pshared/lib/models/payment/payment.dart +++ b/frontend/pshared/lib/models/payment/payment.dart @@ -1,9 +1,9 @@ import 'package:pshared/models/payment/endpoint.dart'; import 'package:pshared/models/payment/execution_operation.dart'; -import 'package:pshared/models/payment/intent.dart'; import 'package:pshared/models/payment/quote/quote.dart'; import 'package:pshared/models/payment/state.dart'; + class Payment { final String? paymentRef; final String? state; @@ -12,7 +12,6 @@ class Payment { final PaymentOrchestrationState orchestrationState; final String? failureCode; final String? failureReason; - final PaymentIntent? intent; final List operations; final PaymentQuote? lastQuote; final Map? metadata; @@ -26,7 +25,6 @@ class Payment { required this.orchestrationState, required this.failureCode, required this.failureReason, - this.intent, required this.operations, required this.lastQuote, required this.metadata, diff --git a/frontend/pshared/lib/utils/currency.dart b/frontend/pshared/lib/utils/currency.dart index 26f5c722..f44580e0 100644 --- a/frontend/pshared/lib/utils/currency.dart +++ b/frontend/pshared/lib/utils/currency.dart @@ -4,6 +4,16 @@ import 'package:pshared/models/asset.dart'; import 'package:pshared/models/currency.dart'; +const nonBreakingSpace = '\u00A0'; + +String withTrailingNonBreakingSpace(String value) { + return '$value$nonBreakingSpace'; +} + +String joinWithNonBreakingSpace(String left, String right) { + return '$left$nonBreakingSpace$right'; +} + String currencyCodeToSymbol(Currency currencyCode) { switch (currencyCode) { case Currency.usd: @@ -24,7 +34,10 @@ String amountToString(double amount) { } String currencyToString(Currency currencyCode, double amount) { - return '${currencyCodeToSymbol(currencyCode)}\u00A0${amountToString(amount)}'; + return joinWithNonBreakingSpace( + currencyCodeToSymbol(currencyCode), + amountToString(amount), + ); } String assetToString(Asset asset) { diff --git a/frontend/pshared/lib/utils/money.dart b/frontend/pshared/lib/utils/money.dart index 925f5eb5..328d187f 100644 --- a/frontend/pshared/lib/utils/money.dart +++ b/frontend/pshared/lib/utils/money.dart @@ -1,4 +1,5 @@ import 'package:pshared/models/money.dart'; +import 'package:pshared/utils/currency.dart'; double parseMoneyAmount(String? raw, {double fallback = 0}) { @@ -7,6 +8,34 @@ double parseMoneyAmount(String? raw, {double fallback = 0}) { return double.tryParse(trimmed) ?? fallback; } +String formatMoneyDisplay( + Money? money, { + String fallback = '--', + String separator = ' ', + String invalidAmountFallback = '', +}) { + if (money == null) return fallback; + + final rawAmount = money.amount.trim(); + final rawCurrency = money.currency.trim(); + final parsedAmount = parseMoneyAmount(rawAmount, fallback: double.nan); + final amountToken = parsedAmount.isNaN + ? (rawAmount.isEmpty ? invalidAmountFallback : rawAmount) + : amountToString(parsedAmount); + + final symbol = currencySymbolFromCode(rawCurrency); + final normalizedSymbol = symbol?.trim() ?? ''; + final hasSymbol = normalizedSymbol.isNotEmpty; + final currencyToken = hasSymbol ? normalizedSymbol : rawCurrency; + final first = amountToken; + final second = currencyToken; + + if (first.isEmpty && second.isEmpty) return fallback; + if (first.isEmpty) return second; + if (second.isEmpty) return first; + return '$first$separator$second'; +} + extension MoneyAmountX on Money { double get amountValue => parseMoneyAmount(amount); } diff --git a/frontend/pweb/lib/controllers/dashboard/balance/source_actions.dart b/frontend/pweb/lib/controllers/dashboard/balance/source_actions.dart index a2df9f83..fdf7deb5 100644 --- a/frontend/pweb/lib/controllers/dashboard/balance/source_actions.dart +++ b/frontend/pweb/lib/controllers/dashboard/balance/source_actions.dart @@ -91,16 +91,22 @@ class BalanceSourceActionsController { } void _openWalletOperationHistory(BuildContext context, String walletRef) { - context.read().selectWalletByRef(walletRef); - context.pushNamed(PayoutRoutes.editWallet); + _withSelectedWallet( + context, + walletRef, + () => context.pushNamed(PayoutRoutes.editWallet), + ); } void _sendWalletPayout(BuildContext context, String walletRef) { - context.read().selectWalletByRef(walletRef); - context.pushNamed( - PayoutRoutes.payment, - queryParameters: PayoutRoutes.buildQueryParameters( - paymentType: PaymentType.wallet, + _withSelectedWallet( + context, + walletRef, + () => context.pushNamed( + PayoutRoutes.payment, + queryParameters: PayoutRoutes.buildQueryParameters( + paymentType: PaymentType.wallet, + ), ), ); } @@ -114,4 +120,13 @@ class BalanceSourceActionsController { ), ); } + + void _withSelectedWallet( + BuildContext context, + String walletRef, + VoidCallback action, + ) { + context.read().selectWalletByRef(walletRef); + action(); + } } diff --git a/frontend/pweb/lib/controllers/dashboard/balance/source_copy.dart b/frontend/pweb/lib/controllers/dashboard/balance/source_copy.dart index 787a54c7..9af9e4f5 100644 --- a/frontend/pweb/lib/controllers/dashboard/balance/source_copy.dart +++ b/frontend/pweb/lib/controllers/dashboard/balance/source_copy.dart @@ -1,5 +1,8 @@ +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:pweb/generated/i18n/app_localizations.dart'; + class BalanceCopyState { final String label; @@ -13,23 +16,26 @@ class BalanceCopyState { class BalanceSourceCopyController { const BalanceSourceCopyController(); - BalanceCopyState wallet(String? depositAddress) { - return BalanceCopyState( - label: 'Copy Deposit Address', - payload: depositAddress?.trim() ?? '', - ); - } + BalanceCopyState wallet(BuildContext context, String? depositAddress) => + _buildCopyAddressState(context, depositAddress); - BalanceCopyState ledger(String? accountCode) { - return BalanceCopyState( - label: 'Copy Deposit Address', - payload: accountCode?.trim() ?? '', - ); - } + BalanceCopyState ledger(BuildContext context, String? accountCode) => + _buildCopyAddressState(context, accountCode); Future copy(BalanceCopyState state) async { if (!state.canCopy) return false; await Clipboard.setData(ClipboardData(text: state.payload)); return true; } + + BalanceCopyState _buildCopyAddressState( + BuildContext context, + String? payload, + ) { + final l10n = AppLocalizations.of(context)!; + return BalanceCopyState( + label: l10n.copyAddress, + payload: payload?.trim() ?? '', + ); + } } diff --git a/frontend/pweb/lib/controllers/operations/report_operations.dart b/frontend/pweb/lib/controllers/operations/report_operations.dart index e844b418..51ded122 100644 --- a/frontend/pweb/lib/controllers/operations/report_operations.dart +++ b/frontend/pweb/lib/controllers/operations/report_operations.dart @@ -1,7 +1,7 @@ -import 'dart:collection'; - import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; + import 'package:pshared/models/payment/operation.dart'; import 'package:pshared/models/payment/payment.dart'; import 'package:pshared/models/payment/source_type.dart'; @@ -131,16 +131,14 @@ class ReportOperationsController extends ChangeNotifier { bool _matchesCurrentSource(Payment payment) { final sourceType = _sourceType; if (sourceType == null || _sourceRefs.isEmpty) return true; - for (final sourceRef in _sourceRefs) { - if (paymentMatchesSource( - payment, - sourceType: sourceType, - sourceRef: sourceRef, - )) { - return true; - } - } - return false; + return _sourceRefs.firstWhereOrNull( + (sourceRef) => paymentMatchesSource( + payment, + sourceType: sourceType, + sourceRef: sourceRef, + ), + ) != + null; } Set _normalizeRefs(List refs) { diff --git a/frontend/pweb/lib/l10n/en.arb b/frontend/pweb/lib/l10n/en.arb index dbc8fee2..1537022d 100644 --- a/frontend/pweb/lib/l10n/en.arb +++ b/frontend/pweb/lib/l10n/en.arb @@ -235,6 +235,7 @@ "avatarUpdateError": "Failed to update profile photo", "settings": "Settings", "notSet": "not set", + "valueUnavailable": "Not available", "search": "Search...", "ok": "Ok", "cancel": "Cancel", @@ -393,7 +394,7 @@ "paymentDetailsNotFound": "Payment not found", "paymentDetailsIdentifiers": "Identifiers", "paymentDetailsAmounts": "Amounts", - "paymentDetailsFx": "FX quote", + "paymentDetailsFx": "Conversion rate", "paymentDetailsFailure": "Failure", "paymentDetailsMetadata": "Metadata", "metadataUploadFileName": "Upload file name", @@ -424,7 +425,7 @@ "paymentOperationSettlement": "Settlement", "paymentOperationLedger": "Ledger", "paymentOperationActionSend": "Send", - "paymentOperationActionObserve": "Observe", + "paymentOperationActionObserve": "Confirmation", "paymentOperationActionFxConvert": "FX convert", "paymentOperationActionCredit": "Credit", "paymentOperationActionBlock": "Block", diff --git a/frontend/pweb/lib/l10n/ru.arb b/frontend/pweb/lib/l10n/ru.arb index ef8b6f04..e7a98a52 100644 --- a/frontend/pweb/lib/l10n/ru.arb +++ b/frontend/pweb/lib/l10n/ru.arb @@ -235,6 +235,7 @@ "avatarUpdateError": "Не удалось обновить фото профиля", "settings": "Настройки", "notSet": "не задано", + "valueUnavailable": "Недоступно", "search": "Поиск...", "ok": "Ок", "cancel": "Отмена", @@ -393,7 +394,7 @@ "paymentDetailsNotFound": "Платеж не найден", "paymentDetailsIdentifiers": "Идентификаторы", "paymentDetailsAmounts": "Суммы", - "paymentDetailsFx": "Курс", + "paymentDetailsFx": "Курс обмена", "paymentDetailsFailure": "Ошибка", "paymentDetailsMetadata": "Метаданные", "metadataUploadFileName": "Имя файла загрузки", @@ -424,7 +425,7 @@ "paymentOperationSettlement": "Расчётный контур", "paymentOperationLedger": "Леджер", "paymentOperationActionSend": "Отправка", - "paymentOperationActionObserve": "Проверка", + "paymentOperationActionObserve": "Подтверждение", "paymentOperationActionFxConvert": "FX-конверсия", "paymentOperationActionCredit": "Зачисление", "paymentOperationActionBlock": "Блокировка", diff --git a/frontend/pweb/lib/pages/address_book/form/view.dart b/frontend/pweb/lib/pages/address_book/form/view.dart index 27cbf49b..cac0f727 100644 --- a/frontend/pweb/lib/pages/address_book/form/view.dart +++ b/frontend/pweb/lib/pages/address_book/form/view.dart @@ -7,7 +7,7 @@ import 'package:pshared/models/recipient/payment_method_draft.dart'; import 'package:pweb/pages/address_book/form/widgets/feilds/email.dart'; import 'package:pweb/pages/address_book/form/widgets/header.dart'; import 'package:pweb/pages/address_book/form/widgets/feilds/name.dart'; -import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel.dart'; +import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel/panel.dart'; import 'package:pweb/pages/address_book/form/widgets/payment_methods/selector_row.dart'; import 'package:pweb/pages/address_book/form/widgets/save_button.dart'; diff --git a/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel.dart b/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel.dart deleted file mode 100644 index e0fe261b..00000000 --- a/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:pshared/models/payment/methods/data.dart'; -import 'package:pshared/models/payment/type.dart'; -import 'package:pshared/models/recipient/payment_method_draft.dart'; - -import 'package:pweb/pages/payment_methods/form.dart'; -import 'package:pweb/pages/payment_methods/icon.dart'; -import 'package:pweb/widgets/dialogs/confirmation_dialog.dart'; -import 'package:pweb/models/state/control_state.dart'; -import 'package:pweb/models/state/visibility.dart'; - -import 'package:pweb/generated/i18n/app_localizations.dart'; - - -class PaymentMethodPanel extends StatelessWidget { - final PaymentType selectedType; - final int selectedIndex; - final List entries; - final ValueChanged onRemove; - final void Function(int, PaymentMethodData) onChanged; - final ControlState editState; - final VisibilityState deleteVisibility; - - final double padding; - - const PaymentMethodPanel({ - super.key, - required this.selectedType, - required this.selectedIndex, - required this.entries, - required this.onRemove, - required this.onChanged, - this.editState = ControlState.enabled, - this.deleteVisibility = VisibilityState.visible, - this.padding = 16, - }); - - Future _confirmDelete(BuildContext context, VoidCallback onConfirmed) async { - final l10n = AppLocalizations.of(context)!; - final confirmed = await showConfirmationDialog( - context: context, - title: l10n.delete, - message: l10n.deletePaymentConfirmation, - confirmLabel: l10n.delete, - ); - if (confirmed) { - onConfirmed(); - } - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final l10n = AppLocalizations.of(context)!; - final label = l10n.paymentMethodDetails; - final entry = selectedIndex >= 0 && selectedIndex < entries.length - ? entries[selectedIndex] - : null; - - return AnimatedContainer( - duration: const Duration(milliseconds: 3000), - padding: EdgeInsets.all(padding), - decoration: BoxDecoration( - color: theme.colorScheme.onSecondary, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: theme.colorScheme.onSurface.withAlpha(30)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - iconForPaymentType(selectedType), - size: 18, - color: theme.colorScheme.primary, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - label, - style: theme.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - ), - if (entry != null && deleteVisibility == VisibilityState.visible) - TextButton.icon( - onPressed: () => _confirmDelete(context, () => onRemove(selectedIndex)), - icon: Icon(Icons.delete, color: theme.colorScheme.error), - label: Text( - l10n.delete, - style: TextStyle(color: theme.colorScheme.error), - ), - ), - ], - ), - const SizedBox(height: 12), - if (entry != null) - PaymentMethodForm( - key: ValueKey('${selectedType.name}-${entry.existing?.id ?? selectedIndex}-form'), - selectedType: selectedType, - initialData: entry.data, - isEditable: editState == ControlState.enabled, - onChanged: (data) { - if (data == null) return; - onChanged(selectedIndex, data); - }, - ), - ], - ), - ); - } -} diff --git a/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel.dart b/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel.dart new file mode 100644 index 00000000..4ba9aae4 --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/methods/data.dart'; +import 'package:pshared/models/payment/type.dart'; +import 'package:pshared/models/recipient/payment_method_draft.dart'; + +import 'package:pweb/models/state/control_state.dart'; +import 'package:pweb/models/state/visibility.dart'; +import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel/panel_container.dart'; +import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel/panel_entry_form.dart'; +import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel/panel_header.dart'; +import 'package:pweb/utils/payment/method_delete_confirmation.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +class PaymentMethodPanel extends StatelessWidget { + final PaymentType selectedType; + final int selectedIndex; + final List entries; + final ValueChanged? onRemove; + final void Function(int, PaymentMethodData)? onChanged; + final ControlState editState; + final VisibilityState deleteVisibility; + final double padding; + + const PaymentMethodPanel({ + super.key, + required this.selectedType, + required this.selectedIndex, + required this.entries, + this.onRemove, + this.onChanged, + this.editState = ControlState.enabled, + this.deleteVisibility = VisibilityState.visible, + this.padding = 16, + }) : assert(editState == ControlState.disabled || onChanged != null), + assert(deleteVisibility == VisibilityState.hidden || onRemove != null); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final entry = selectedIndex >= 0 && selectedIndex < entries.length + ? entries[selectedIndex] + : null; + final showDelete = + entry != null && + deleteVisibility == VisibilityState.visible && + onRemove != null; + + return PaymentMethodPanelContainer( + padding: padding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PaymentMethodPanelHeader( + selectedType: selectedType, + title: l10n.paymentMethodDetails, + deleteLabel: l10n.delete, + showDelete: showDelete, + onDelete: showDelete + ? () => confirmPaymentMethodDelete( + context, + () => onRemove!(selectedIndex), + ) + : null, + ), + const SizedBox(height: 12), + if (entry != null) + PaymentMethodPanelEntryForm( + selectedType: selectedType, + selectedIndex: selectedIndex, + entry: entry, + isEditable: editState == ControlState.enabled, + onChanged: onChanged == null + ? null + : (data) => onChanged!(selectedIndex, data), + ), + ], + ), + ); + } +} diff --git a/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel_container.dart b/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel_container.dart new file mode 100644 index 00000000..20301e76 --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel_container.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + + +class PaymentMethodPanelContainer extends StatelessWidget { + final double padding; + final Widget child; + + const PaymentMethodPanelContainer({ + super.key, + required this.padding, + required this.child, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return AnimatedContainer( + duration: const Duration(milliseconds: 3000), + padding: EdgeInsets.all(padding), + decoration: BoxDecoration( + color: theme.colorScheme.onSecondary, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: theme.colorScheme.onSurface.withAlpha(30)), + ), + child: child, + ); + } +} diff --git a/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel_entry_form.dart b/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel_entry_form.dart new file mode 100644 index 00000000..1a29ef2f --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel_entry_form.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/methods/data.dart'; +import 'package:pshared/models/payment/type.dart'; +import 'package:pshared/models/recipient/payment_method_draft.dart'; + +import 'package:pweb/pages/payment_methods/form.dart'; + + +class PaymentMethodPanelEntryForm extends StatelessWidget { + final PaymentType selectedType; + final int selectedIndex; + final RecipientMethodDraft entry; + final bool isEditable; + final ValueChanged? onChanged; + + const PaymentMethodPanelEntryForm({ + super.key, + required this.selectedType, + required this.selectedIndex, + required this.entry, + required this.isEditable, + this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return PaymentMethodForm( + key: ValueKey( + '${selectedType.name}-${entry.existing?.id ?? selectedIndex}-form', + ), + selectedType: selectedType, + initialData: entry.data, + isEditable: isEditable, + onChanged: (data) { + if (data == null) return; + onChanged?.call(data); + }, + ); + } +} diff --git a/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel_header.dart b/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel_header.dart new file mode 100644 index 00000000..46b488d3 --- /dev/null +++ b/frontend/pweb/lib/pages/address_book/form/widgets/payment_methods/panel/panel_header.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/payment/type.dart'; + +import 'package:pweb/pages/payment_methods/icon.dart'; + + +class PaymentMethodPanelHeader extends StatelessWidget { + final PaymentType selectedType; + final String title; + final String deleteLabel; + final bool showDelete; + final VoidCallback? onDelete; + + const PaymentMethodPanelHeader({ + super.key, + required this.selectedType, + required this.title, + required this.deleteLabel, + required this.showDelete, + this.onDelete, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Row( + children: [ + Icon( + iconForPaymentType(selectedType), + size: 18, + color: theme.colorScheme.primary, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + title, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ), + if (showDelete && onDelete != null) + TextButton.icon( + onPressed: onDelete, + icon: Icon(Icons.delete, color: theme.colorScheme.error), + label: Text( + deleteLabel, + style: TextStyle(color: theme.colorScheme.error), + ), + ), + ], + ); + } +} diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/amount.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/amount.dart index 2eb8c096..0f32412c 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/amount.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/amount.dart @@ -3,9 +3,12 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart'; +import 'package:pshared/models/money.dart'; import 'package:pshared/models/payment/wallet.dart'; import 'package:pshared/utils/currency.dart'; +import 'package:pweb/utils/money_display.dart'; + class BalanceAmount extends StatelessWidget { final Wallet wallet; @@ -25,6 +28,13 @@ class BalanceAmount extends StatelessWidget { final textTheme = Theme.of(context).textTheme; final colorScheme = Theme.of(context).colorScheme; final currencyBalance = currencyCodeToSymbol(wallet.currency); + final formattedBalance = formatMoneyUi( + context, + Money( + amount: amountToString(wallet.balance), + currency: currencyCodeToString(wallet.currency), + ), + ); final wallets = context.watch(); final isMasked = wallets.isBalanceMasked(wallet.id); @@ -32,9 +42,7 @@ class BalanceAmount extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - isMasked - ? '•••• $currencyBalance' - : '${amountToString(wallet.balance)} $currencyBalance', + isMasked ? '•••• $currencyBalance' : formattedBalance, maxLines: 1, overflow: TextOverflow.ellipsis, style: textTheme.headlineMedium?.copyWith( diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/card_item.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/card_item.dart index d672321e..576e935b 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/card_item.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/carousel/card_item.dart @@ -6,8 +6,8 @@ import 'package:pshared/models/payment/wallet.dart'; import 'package:pweb/models/dashboard/balance_item.dart'; import 'package:pweb/pages/dashboard/buttons/balance/add/card.dart'; import 'package:pweb/pages/dashboard/buttons/balance/config.dart'; +import 'package:pweb/pages/dashboard/buttons/balance/source/card.dart'; import 'package:pweb/pages/dashboard/buttons/balance/source/cards/ledger.dart'; -import 'package:pweb/pages/dashboard/buttons/balance/source/cards/wallet.dart'; class BalanceCarouselCardItem extends StatelessWidget { @@ -29,9 +29,9 @@ class BalanceCarouselCardItem extends StatelessWidget { @override Widget build(BuildContext context) { final card = switch (item) { - WalletBalanceItem(:final wallet) => WalletCard( + WalletBalanceItem(:final wallet) => BalanceSourceCard.wallet( wallet: wallet, - onTopUp: () => onTopUp(wallet), + onAddFunds: () => onTopUp(wallet), onTap: () => onWalletTap(wallet), ), LedgerBalanceItem(:final account) => LedgerAccountCard( diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger_amount.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger_amount.dart index 8922b636..f6fc8ffa 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger_amount.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger_amount.dart @@ -23,7 +23,7 @@ class LedgerBalanceAmount extends StatelessWidget { final isMasked = controller.isBalanceMasked(account.ledgerAccountRef); final balance = isMasked ? LedgerBalanceFormatter.formatMasked(account) - : LedgerBalanceFormatter.format(account); + : LedgerBalanceFormatter.format(context, account); return Row( mainAxisSize: MainAxisSize.min, diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card.dart index 5e511236..f71cb67c 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/card.dart @@ -61,7 +61,7 @@ class BalanceSourceCard extends StatelessWidget { ? null : wallet.network!.localizedName(context); final symbol = wallet.tokenSymbol?.trim(); - final copyState = _copyController.wallet(wallet.depositAddress); + final copyState = _copyController.wallet(context, wallet.depositAddress); return BalanceSourceCardLayout( title: wallet.name, @@ -105,7 +105,7 @@ class BalanceSourceCard extends StatelessWidget { final badge = account.currency.trim().isEmpty ? null : account.currency.toUpperCase(); - final copyState = _copyController.ledger(accountCode); + final copyState = _copyController.ledger(context, accountCode); return BalanceSourceCardLayout( title: title, diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/cards/wallet.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/source/cards/wallet.dart deleted file mode 100644 index a23e98df..00000000 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/source/cards/wallet.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:pshared/models/payment/wallet.dart'; - -import 'package:pweb/pages/dashboard/buttons/balance/source/card.dart'; - - -class WalletCard extends StatelessWidget { - final Wallet wallet; - final VoidCallback onTopUp; - final VoidCallback onTap; - - const WalletCard({ - super.key, - required this.wallet, - required this.onTopUp, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return BalanceSourceCard.wallet( - wallet: wallet, - onTap: onTap, - onAddFunds: onTopUp, - ); - } -} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/amount/field.dart b/frontend/pweb/lib/pages/dashboard/payouts/amount/field.dart index 6f293385..aeb78d7c 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/amount/field.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/amount/field.dart @@ -36,7 +36,9 @@ class PaymentAmountField extends StatelessWidget { decoration: InputDecoration( labelText: loc.amount, border: const OutlineInputBorder(), - prefixText: symbol == null ? null : '$symbol\u00A0', + prefixText: symbol == null + ? null + : withTrailingNonBreakingSpace(symbol), ), onChanged: ui.handleChanged, ), diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/helpers.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/helpers.dart deleted file mode 100644 index 47b3388c..00000000 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/helpers.dart +++ /dev/null @@ -1,37 +0,0 @@ -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/payouts/multiple_payouts.dart'; - -import 'package:pweb/generated/i18n/app_localizations.dart'; - - -String moneyLabel(Money? money) { - if (money == null) return 'N/A'; - final amount = parseMoneyAmount(money.amount, fallback: double.nan); - if (amount.isNaN) return '${money.amount} ${money.currency}'; - try { - return assetToString( - Asset(currency: currencyStringToCode(money.currency), amount: amount), - ); - } catch (_) { - return '${money.amount} ${money.currency}'; - } -} - -String sentAmountLabel(MultiplePayoutsController controller) { - final requested = controller.requestedSentAmount; - final sourceDebit = controller.aggregateDebitAmount; - - if (requested == null && sourceDebit == null) return 'N/A'; - if (sourceDebit != null) return moneyLabel(sourceDebit); - return moneyLabel(requested); -} - -String feeLabel(MultiplePayoutsController controller, AppLocalizations l10n) { - final fee = controller.aggregateFeeAmount; - if (fee == null) return l10n.noFee; - return moneyLabel(fee); -} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/summary.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/summary.dart index af9bdc6b..5ea64671 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/summary.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/summary.dart @@ -1,11 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:pshared/utils/currency.dart'; + import 'package:pweb/controllers/payouts/multiple_payouts.dart'; import 'package:pweb/models/dashboard/summary_values.dart'; -import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/helpers.dart'; import 'package:pweb/pages/dashboard/payouts/summary/widget.dart'; +import 'package:pweb/utils/money_display.dart'; + import 'package:pweb/generated/i18n/app_localizations.dart'; + class SourceQuoteSummary extends StatelessWidget { const SourceQuoteSummary({ super.key, @@ -18,12 +22,27 @@ class SourceQuoteSummary extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; return PaymentSummary( spacing: spacing, values: PaymentSummaryValues( - fee: feeLabel(controller, AppLocalizations.of(context)!), - recipientReceives: moneyLabel(controller.aggregateSettlementAmount), - total: moneyLabel(controller.aggregateDebitAmount), + fee: controller.aggregateFeeAmount == null + ? l10n.noFee + : formatMoneyUiWithL10n( + l10n, + controller.aggregateFeeAmount, + separator: nonBreakingSpace, + ), + recipientReceives: formatMoneyUiWithL10n( + l10n, + controller.aggregateSettlementAmount, + separator: nonBreakingSpace, + ), + total: formatMoneyUiWithL10n( + l10n, + controller.aggregateDebitAmount, + separator: nonBreakingSpace, + ), ), ); } diff --git a/frontend/pweb/lib/pages/dashboard/payouts/summary/row.dart b/frontend/pweb/lib/pages/dashboard/payouts/summary/row.dart index f459261d..7086b1a6 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/summary/row.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/summary/row.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:pshared/models/asset.dart'; -import 'package:pshared/utils/currency.dart'; + +import 'package:pweb/utils/money_display.dart'; class PaymentSummaryRow extends StatelessWidget { @@ -11,8 +12,8 @@ class PaymentSummaryRow extends StatelessWidget { final TextStyle? style; const PaymentSummaryRow({ - super.key, - required this.labelFactory, + super.key, + required this.labelFactory, required this.asset, this.value, this.style, @@ -20,8 +21,7 @@ class PaymentSummaryRow extends StatelessWidget { @override Widget build(BuildContext context) { - final formatted = value ?? - (asset == null ? 'N/A' : assetToString(asset!)); + final formatted = value ?? formatAssetUi(context, asset); return Text(labelFactory(formatted), style: style); } } diff --git a/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/manual_details.dart b/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/manual_details.dart index e9dfc9d7..ae741a7e 100644 --- a/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/manual_details.dart +++ b/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/manual_details.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:pshared/models/payment/methods/data.dart'; import 'package:pshared/models/recipient/payment_method_draft.dart'; +import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel/panel.dart'; -import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel.dart'; import 'package:pweb/pages/payout_page/send/widgets/payment_info/header.dart'; import 'package:pweb/models/state/control_state.dart'; import 'package:pweb/models/state/visibility.dart'; @@ -40,8 +40,6 @@ class PaymentInfoManualDetailsSection extends StatelessWidget { selectedType: data.type, selectedIndex: 0, entries: [entry], - onRemove: (_) {}, - onChanged: (_, ignored) {}, editState: ControlState.disabled, deleteVisibility: VisibilityState.hidden, ), diff --git a/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/methods_section.dart b/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/methods_section.dart index 266afcf0..3afaa1d0 100644 --- a/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/methods_section.dart +++ b/frontend/pweb/lib/pages/payout_page/send/widgets/payment_info/methods_section.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:pshared/models/payment/type.dart'; import 'package:pshared/models/recipient/payment_method_draft.dart'; +import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel/panel.dart'; -import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel.dart'; import 'package:pweb/pages/address_book/form/widgets/payment_methods/selector_row.dart'; import 'package:pweb/pages/payout_page/send/widgets/payment_info/details_builder.dart'; import 'package:pweb/pages/payout_page/send/widgets/payment_info/header.dart'; @@ -78,8 +78,6 @@ class PaymentInfoMethodsSection extends StatelessWidget { selectedType: state.selectedType, selectedIndex: state.selectedIndex!, entries: state.selectedEntries, - onRemove: (_) {}, - onChanged: (_, _) {}, editState: ControlState.disabled, deleteVisibility: VisibilityState.hidden, ), diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart index 85513ab6..a6639e1c 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart @@ -1,30 +1,16 @@ +import 'package:flutter/widgets.dart'; + import 'package:pshared/models/ledger/account.dart'; import 'package:pshared/utils/currency.dart'; -import 'package:pshared/utils/money.dart'; + +import 'package:pweb/utils/money_display.dart'; class LedgerBalanceFormatter { const LedgerBalanceFormatter._(); - static String format(LedgerAccount account) { - final money = account.balance?.balance; - if (money == null) return '--'; - - final amount = parseMoneyAmount(money.amount, fallback: double.nan); - if (amount.isNaN) { - return '${money.amount} ${money.currency}'; - } - - try { - final currency = currencyStringToCode(money.currency); - final symbol = currencyCodeToSymbol(currency); - if (symbol.trim().isEmpty) { - return '${amountToString(amount)} ${money.currency}'; - } - return '${amountToString(amount)} $symbol'; - } catch (_) { - return '${amountToString(amount)} ${money.currency}'; - } + static String format(BuildContext context, LedgerAccount account) { + return formatMoneyUi(context, account.balance?.balance); } static String formatMasked(LedgerAccount account) { diff --git a/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/section.dart b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/section.dart index 3458dd43..d53bdbb0 100644 --- a/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/section.dart +++ b/frontend/pweb/lib/pages/payout_page/wallet/edit/fields/ledger/section.dart @@ -26,7 +26,7 @@ class LedgerSection extends StatelessWidget { final hasAccountCode = accountCode.isNotEmpty; final balance = isMasked ? LedgerBalanceFormatter.formatMasked(ledger) - : LedgerBalanceFormatter.format(ledger); + : LedgerBalanceFormatter.format(context, ledger); return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/frontend/pweb/lib/pages/payout_page/wallet/history/table.dart b/frontend/pweb/lib/pages/payout_page/wallet/history/table.dart deleted file mode 100644 index bd5a6a3d..00000000 --- a/frontend/pweb/lib/pages/payout_page/wallet/history/table.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:pshared/utils/currency.dart'; - -import 'package:pweb/models/wallet/wallet_transaction.dart'; -import 'package:pweb/pages/payout_page/wallet/history/chip.dart'; -import 'package:pweb/pages/report/table/badge.dart'; - -import 'package:pweb/generated/i18n/app_localizations.dart'; - - -class WalletTransactionsTable extends StatelessWidget { - final List transactions; - - const WalletTransactionsTable({super.key, required this.transactions}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final loc = AppLocalizations.of(context)!; - - if (transactions.isEmpty) { - return Card( - color: theme.colorScheme.onSecondary, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(16), - child: Text(loc.walletHistoryEmpty), - ), - ); - } - - return Card( - color: theme.colorScheme.onSecondary, - elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: DataTable( - columnSpacing: 18, - headingTextStyle: const TextStyle(fontWeight: FontWeight.w600), - columns: [ - DataColumn(label: Text(loc.colStatus)), - DataColumn(label: Text(loc.colType)), - DataColumn(label: Text(loc.colAmount)), - DataColumn(label: Text(loc.colBalance)), - DataColumn(label: Text(loc.colCounterparty)), - DataColumn(label: Text(loc.colDate)), - DataColumn(label: Text(loc.colComment)), - ], - rows: List.generate( - transactions.length, - (index) { - final tx = transactions[index]; - final color = WidgetStateProperty.resolveWith( - (states) => index.isEven - ? theme.colorScheme.surfaceContainerHighest - : null, - ); - - return DataRow.byIndex( - index: index, - color: color, - cells: [ - DataCell(OperationStatusBadge(status: tx.status)), - DataCell(TypeChip(type: tx.type)), - DataCell(Text( - '${tx.type.sign}${amountToString(tx.amount)} ${currencyCodeToSymbol(tx.currency)}')), - DataCell(Text( - tx.balanceAfter == null - ? '-' - : '${amountToString(tx.balanceAfter!)} ${currencyCodeToSymbol(tx.currency)}', - )), - DataCell(Text(tx.counterparty ?? '-')), - DataCell(Text( - '${TimeOfDay.fromDateTime(tx.date).format(context)}\n' - '${tx.date.toLocal().toIso8601String().split("T").first}', - )), - DataCell(Text(tx.description)), - ], - ); - }, - ), - ), - ), - ), - ); - } -} diff --git a/frontend/pweb/lib/pages/report/cards/operation_card.dart b/frontend/pweb/lib/pages/report/cards/operation_card.dart index 3b57c68f..53aafd81 100644 --- a/frontend/pweb/lib/pages/report/cards/operation_card.dart +++ b/frontend/pweb/lib/pages/report/cards/operation_card.dart @@ -15,19 +15,23 @@ class OperationCard extends StatelessWidget { final OperationItem operation; final ValueChanged? onTap; - const OperationCard({ - super.key, - required this.operation, - this.onTap, - }); + const OperationCard({super.key, required this.operation, this.onTap}); @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context)!; final theme = Theme.of(context); final canOpen = onTap != null && paymentIdFromOperation(operation) != null; - final amountLabel = formatAmount(operation.amount, operation.currency); - final toAmountLabel = formatAmount(operation.toAmount, operation.toCurrency); + final amountLabel = formatAmount( + context, + operation.amount, + operation.currency, + ); + final toAmountLabel = formatAmount( + context, + operation.toAmount, + operation.toCurrency, + ); final showToAmount = shouldShowToAmount(operation); final timeLabel = formatOperationTime(context, operation.date); diff --git a/frontend/pweb/lib/pages/report/charts/payout_volumes/totals_list.dart b/frontend/pweb/lib/pages/report/charts/payout_volumes/totals_list.dart index 38abb52e..a7ba3644 100644 --- a/frontend/pweb/lib/pages/report/charts/payout_volumes/totals_list.dart +++ b/frontend/pweb/lib/pages/report/charts/payout_volumes/totals_list.dart @@ -42,6 +42,7 @@ class PayoutTotalsList extends StatelessWidget { const SizedBox(width: 8), Text( formatAmount( + context, totals[index].amount, totals[index].currency, ), diff --git a/frontend/pweb/lib/pages/report/details/sections/fx.dart b/frontend/pweb/lib/pages/report/details/sections/fx.dart index f56527bd..50a8ea2c 100644 --- a/frontend/pweb/lib/pages/report/details/sections/fx.dart +++ b/frontend/pweb/lib/pages/report/details/sections/fx.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:pshared/models/money.dart'; import 'package:pshared/models/payment/payment.dart'; import 'package:pshared/models/payment/fx/quote.dart'; import 'package:pshared/utils/currency.dart'; +import 'package:pshared/utils/money.dart'; import 'package:pweb/pages/report/details/section.dart'; import 'package:pweb/pages/report/details/sections/rows.dart'; @@ -13,26 +15,17 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; class PaymentFxSection extends StatelessWidget { final Payment payment; - const PaymentFxSection({ - super.key, - required this.payment, - }); + const PaymentFxSection({super.key, required this.payment}); @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context)!; final fx = payment.lastQuote?.fxQuote; final rows = buildDetailRows([ - DetailValue( - label: loc.fxRateLabel, - value: _formatRate(fx), - ), + DetailValue(label: loc.fxRateLabel, value: _formatRate(fx)), ]); - return DetailsSection( - title: loc.paymentDetailsFx, - children: rows, - ); + return DetailsSection(title: loc.paymentDetailsFx, children: rows); } String? _formatRate(FxQuote? fx) { @@ -40,24 +33,33 @@ class PaymentFxSection extends StatelessWidget { final price = fx.price?.trim(); if (price == null || price.isEmpty) return null; - final base = _firstNonEmpty([ - currencySymbolFromCode(fx.baseCurrency), - currencySymbolFromCode(fx.baseAmount?.currency), + final baseCurrency = _firstNonEmpty([ fx.baseCurrency, fx.baseAmount?.currency, + currencySymbolFromCode(fx.baseCurrency), + currencySymbolFromCode(fx.baseAmount?.currency), ]); - final quote = _firstNonEmpty([ - currencySymbolFromCode(fx.quoteCurrency), - currencySymbolFromCode(fx.quoteAmount?.currency), + final quoteCurrency = _firstNonEmpty([ fx.quoteCurrency, fx.quoteAmount?.currency, + currencySymbolFromCode(fx.quoteCurrency), + currencySymbolFromCode(fx.quoteAmount?.currency), ]); - if (base == null || quote == null) { - return price; - } + if (baseCurrency == null || quoteCurrency == null) return price; - return '1 $base = $price $quote'; + final baseDisplay = formatMoneyDisplay( + Money(amount: '1', currency: baseCurrency), + fallback: '1 $baseCurrency', + invalidAmountFallback: '1', + ); + final quoteDisplay = formatMoneyDisplay( + Money(amount: _normalizeAmount(price), currency: quoteCurrency), + fallback: '$price $quoteCurrency', + invalidAmountFallback: price, + ); + + return '$baseDisplay = $quoteDisplay'; } String? _firstNonEmpty(List values) { @@ -67,4 +69,8 @@ class PaymentFxSection extends StatelessWidget { } return null; } + + String _normalizeAmount(String raw) { + return raw.replaceAll(RegExp(r'\s+'), '').replaceAll(',', '.'); + } } diff --git a/frontend/pweb/lib/pages/report/details/sections/metadata.dart b/frontend/pweb/lib/pages/report/details/sections/metadata.dart deleted file mode 100644 index 63a48dc7..00000000 --- a/frontend/pweb/lib/pages/report/details/sections/metadata.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:pshared/models/payment/payment.dart'; - -import 'package:pweb/pages/report/details/row.dart'; -import 'package:pweb/pages/report/details/section.dart'; - -import 'package:pweb/generated/i18n/app_localizations.dart'; - - -class PaymentMetadataSection extends StatelessWidget { - final Payment payment; - - const PaymentMetadataSection({ - super.key, - required this.payment, - }); - - @override - Widget build(BuildContext context) { - final loc = AppLocalizations.of(context)!; - final metadata = payment.metadata ?? const {}; - const allowedKeys = {'upload_filename', 'upload_rows'}; - final filtered = Map.fromEntries( - metadata.entries.where((entry) => allowedKeys.contains(entry.key)), - ); - - if (filtered.isEmpty) { - return DetailsSection( - title: loc.paymentDetailsMetadata, - children: [ - Text( - loc.metadataEmpty, - style: Theme.of(context).textTheme.bodyMedium, - ), - ], - ); - } - - final entries = filtered.entries.toList() - ..sort((a, b) => a.key.compareTo(b.key)); - - return DetailsSection( - title: loc.paymentDetailsMetadata, - children: entries - .map( - (entry) => DetailRow( - label: _metadataLabel(loc, entry.key), - value: entry.value, - monospaced: true, - ), - ) - .toList(), - ); - } -} - -String _metadataLabel(AppLocalizations loc, String key) { - switch (key) { - case 'upload_filename': - return loc.metadataUploadFileName; - case 'upload_rows': - return loc.metadataTotalRecipients; - default: - return key; - } -} diff --git a/frontend/pweb/lib/pages/report/details/sections/operations/tile.dart b/frontend/pweb/lib/pages/report/details/sections/operations/tile.dart index 2afbf9f2..9a0dbd6c 100644 --- a/frontend/pweb/lib/pages/report/details/sections/operations/tile.dart +++ b/frontend/pweb/lib/pages/report/details/sections/operations/tile.dart @@ -27,6 +27,7 @@ class OperationHistoryTile extends StatelessWidget { final loc = AppLocalizations.of(context)!; final theme = Theme.of(context); final title = resolveOperationTitle(loc, operation.code); + final operationLabel = operation.label?.trim(); final stateView = resolveStepStateView(context, operation.state); final completedAt = formatCompletedAt(context, operation.completedAt); final canDownload = canDownloadDocument && onDownloadDocument != null; @@ -49,13 +50,24 @@ class OperationHistoryTile extends StatelessWidget { StepStateChip(view: stateView), ], ), + if (operationLabel != null && + operationLabel.isNotEmpty && + operationLabel != title) ...[ + const SizedBox(height: 4), + Text( + operationLabel, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], const SizedBox(height: 6), Text( '${loc.completedAtLabel}: $completedAt', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), - ), + ), if (canDownload) ...[ const SizedBox(height: 8), TextButton.icon( diff --git a/frontend/pweb/lib/pages/report/details/summary_card/widget.dart b/frontend/pweb/lib/pages/report/details/summary_card/widget.dart index 45af884e..ee6ade98 100644 --- a/frontend/pweb/lib/pages/report/details/summary_card/widget.dart +++ b/frontend/pweb/lib/pages/report/details/summary_card/widget.dart @@ -9,6 +9,7 @@ import 'package:pweb/pages/report/details/summary_card/info_line.dart'; import 'package:pweb/pages/report/table/badge.dart'; import 'package:pweb/utils/report/amount_parts.dart'; import 'package:pweb/utils/report/format.dart'; +import 'package:pweb/utils/money_display.dart'; import 'package:pweb/utils/report/payment_mapper.dart'; import 'package:pweb/utils/clipboard.dart'; @@ -24,6 +25,7 @@ class PaymentSummaryCard extends StatelessWidget { Widget build(BuildContext context) { final loc = AppLocalizations.of(context)!; final theme = Theme.of(context); + final unavailableValue = unavailableMoneyValue(context); final status = statusFromPayment(payment); final dateLabel = formatDateLabel(context, resolvePaymentDate(payment)); @@ -33,14 +35,16 @@ class PaymentSummaryCard extends StatelessWidget { final toAmount = payment.lastQuote?.amounts?.destinationSettlement; final fee = quoteFeeTotal(payment.lastQuote); - final amountLabel = formatMoney(primaryAmount); - final toAmountLabel = formatMoney(toAmount); - final feeLabel = formatMoney(fee); + final amountLabel = formatMoney(context, primaryAmount); + final toAmountLabel = formatMoney(context, toAmount); + final feeLabel = formatMoney(context, fee); final paymentRef = (payment.paymentRef ?? '').trim(); - final showToAmount = toAmountLabel != '-'; + final showToAmount = toAmountLabel != unavailableValue; final showFee = payment.lastQuote != null; - final feeText = feeLabel != '-' ? loc.fee(feeLabel) : loc.fee(loc.noFee); + final feeText = feeLabel != unavailableValue + ? loc.fee(feeLabel) + : loc.fee(loc.noFee); final showPaymentId = paymentRef.isNotEmpty; final amountParts = splitAmount(amountLabel); @@ -73,12 +77,12 @@ class PaymentSummaryCard extends StatelessWidget { currency: amountParts.currency, ), const SizedBox(height: 6), - if (amountLabel != '-') + if (amountLabel != unavailableValue) InfoLine( icon: Icons.send_outlined, text: loc.sentAmount(amountLabel), ), - if (showToAmount && toAmountLabel != '-') + if (showToAmount && toAmountLabel != unavailableValue) InfoLine( icon: Icons.south_east, text: loc.recipientWillReceive(toAmountLabel), diff --git a/frontend/pweb/lib/pages/report/table/row.dart b/frontend/pweb/lib/pages/report/table/row.dart index 3ee87b52..1c2eab6b 100644 --- a/frontend/pweb/lib/pages/report/table/row.dart +++ b/frontend/pweb/lib/pages/report/table/row.dart @@ -1,14 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:pshared/models/money.dart'; import 'package:pshared/models/payment/operation.dart'; import 'package:pshared/models/payment/status.dart'; import 'package:pshared/utils/currency.dart'; import 'package:pweb/pages/report/table/badge.dart'; +import 'package:pweb/utils/money_display.dart'; import 'package:pweb/utils/report/download_act.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; + class OperationRow { static DataRow build(OperationItem op, BuildContext context) { final isUnknownDate = op.date.millisecondsSinceEpoch == 0; @@ -35,13 +38,21 @@ class OperationRow { label: Text(loc.downloadAct), ) : Text(op.fileName ?? ''); + final amountLabel = formatMoneyUiWithL10n( + loc, + Money(amount: amountToString(op.amount), currency: op.currency), + ); + final toAmountLabel = formatMoneyUiWithL10n( + loc, + Money(amount: amountToString(op.toAmount), currency: op.toCurrency), + ); return DataRow( cells: [ DataCell(OperationStatusBadge(status: op.status)), DataCell(documentCell), - DataCell(Text('${amountToString(op.amount)} ${op.currency}')), - DataCell(Text('${amountToString(op.toAmount)} ${op.toCurrency}')), + DataCell(Text(amountLabel)), + DataCell(Text(toAmountLabel)), DataCell(Text(op.payId)), DataCell(Text(op.cardNumber ?? '-')), DataCell(Text(op.name)), diff --git a/frontend/pweb/lib/utils/money_display.dart b/frontend/pweb/lib/utils/money_display.dart new file mode 100644 index 00000000..3d7010f6 --- /dev/null +++ b/frontend/pweb/lib/utils/money_display.dart @@ -0,0 +1,96 @@ +import 'package:flutter/widgets.dart'; + +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/generated/i18n/app_localizations.dart'; + + +String unavailableMoneyValue(BuildContext context) { + return AppLocalizations.of(context)!.valueUnavailable; +} + +String unavailableMoneyValueFromL10n(AppLocalizations l10n) { + return l10n.valueUnavailable; +} + +String formatMoneyUi( + BuildContext context, + Money? money, { + String separator = ' ', +}) { + return formatMoneyUiWithL10n( + AppLocalizations.of(context)!, + money, + separator: separator, + ); +} + +String formatMoneyUiWithL10n( + AppLocalizations l10n, + Money? money, { + String separator = ' ', +}) { + final unavailableValue = unavailableMoneyValueFromL10n(l10n); + return formatMoneyDisplay( + money, + fallback: unavailableValue, + invalidAmountFallback: unavailableValue, + separator: separator, + ); +} + +String formatAmountUi( + BuildContext context, { + required double amount, + required String currency, + String separator = ' ', +}) { + return formatAmountUiWithL10n( + AppLocalizations.of(context)!, + amount: amount, + currency: currency, + separator: separator, + ); +} + +String formatAmountUiWithL10n( + AppLocalizations l10n, { + required double amount, + required String currency, + String separator = ' ', +}) { + return formatMoneyUiWithL10n( + l10n, + Money(amount: amountToString(amount), currency: currency), + separator: separator, + ); +} + +String formatAssetUi( + BuildContext context, + Asset? asset, { + String separator = ' ', +}) { + return formatAssetUiWithL10n( + AppLocalizations.of(context)!, + asset, + separator: separator, + ); +} + +String formatAssetUiWithL10n( + AppLocalizations l10n, + Asset? asset, { + String separator = ' ', +}) { + if (asset == null) return unavailableMoneyValueFromL10n(l10n); + return formatAmountUiWithL10n( + l10n, + amount: asset.amount, + currency: currencyCodeToString(asset.currency), + separator: separator, + ); +} diff --git a/frontend/pweb/lib/utils/payment/method_delete_confirmation.dart b/frontend/pweb/lib/utils/payment/method_delete_confirmation.dart new file mode 100644 index 00000000..f7300219 --- /dev/null +++ b/frontend/pweb/lib/utils/payment/method_delete_confirmation.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +import 'package:pweb/widgets/dialogs/confirmation_dialog.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +Future confirmPaymentMethodDelete( + BuildContext context, + VoidCallback onConfirmed, +) async { + final l10n = AppLocalizations.of(context)!; + final confirmed = await showConfirmationDialog( + context: context, + title: l10n.delete, + message: l10n.deletePaymentConfirmation, + confirmLabel: l10n.delete, + ); + if (confirmed) { + onConfirmed(); + } +} diff --git a/frontend/pweb/lib/utils/payment/status_view.dart b/frontend/pweb/lib/utils/payment/status_view.dart index 71f90e87..c0c04392 100644 --- a/frontend/pweb/lib/utils/payment/status_view.dart +++ b/frontend/pweb/lib/utils/payment/status_view.dart @@ -50,31 +50,35 @@ StatusView operationStatusViewFromToken( case 'settled': return StatusView( label: l10n.operationStatusSuccessful, - backgroundColor: scheme.tertiaryContainer, - foregroundColor: scheme.onTertiaryContainer, + backgroundColor: Colors.green, + foregroundColor: Colors.white, ); + case 'skipped': return StatusView( label: l10n.operationStepStateSkipped, - backgroundColor: scheme.secondaryContainer, - foregroundColor: scheme.onSecondaryContainer, + backgroundColor: Colors.grey, + foregroundColor: Colors.white, ); + case 'error': case 'failed': case 'rejected': case 'aborted': return StatusView( label: l10n.operationStatusUnsuccessful, - backgroundColor: scheme.errorContainer, - foregroundColor: scheme.onErrorContainer, + backgroundColor: Colors.red, + foregroundColor: Colors.white, ); + case 'cancelled': case 'canceled': return StatusView( label: l10n.paymentStatusCancelled, - backgroundColor: scheme.surfaceContainerHighest, - foregroundColor: scheme.onSurfaceVariant, + backgroundColor: Colors.blueGrey, + foregroundColor: Colors.white, ); + case 'processing': case 'running': case 'executing': @@ -82,9 +86,10 @@ StatusView operationStatusViewFromToken( case 'started': return StatusView( label: l10n.paymentStatusProcessing, - backgroundColor: scheme.primaryContainer, - foregroundColor: scheme.onPrimaryContainer, + backgroundColor: Colors.orange, + foregroundColor: Colors.white, ); + case 'pending': case 'queued': case 'waiting': @@ -92,26 +97,29 @@ StatusView operationStatusViewFromToken( case 'scheduled': return StatusView( label: l10n.operationStatusPending, - backgroundColor: scheme.secondary, - foregroundColor: scheme.onSecondary, + backgroundColor: Colors.amber, + foregroundColor: Colors.black, ); + case 'needs_attention': return StatusView( label: l10n.operationStepStateNeedsAttention, - backgroundColor: scheme.tertiary, - foregroundColor: scheme.onTertiary, + backgroundColor: Colors.grey.shade800, + foregroundColor: Colors.white, ); + case 'retrying': return StatusView( label: l10n.operationStepStateRetrying, - backgroundColor: scheme.primary, - foregroundColor: scheme.onPrimary, + backgroundColor: Colors.purple, + foregroundColor: Colors.white, ); + default: return StatusView( label: fallbackLabel ?? humanizeOperationStatusToken(token), - backgroundColor: scheme.surfaceContainerHighest, - foregroundColor: scheme.onSurfaceVariant, + backgroundColor: Colors.grey.shade400, + foregroundColor: Colors.black, ); } } diff --git a/frontend/pweb/lib/utils/report/format.dart b/frontend/pweb/lib/utils/report/format.dart index 4cde3fc9..370ab889 100644 --- a/frontend/pweb/lib/utils/report/format.dart +++ b/frontend/pweb/lib/utils/report/format.dart @@ -3,34 +3,35 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:pshared/models/money.dart'; -import 'package:pshared/utils/currency.dart'; import 'package:pshared/utils/localization.dart'; +import 'package:pweb/utils/money_display.dart'; -String formatMoney(Money? money, {String fallback = '-'}) { - if (money == null) return fallback; - final amount = money.amount.trim(); - if (amount.isEmpty) return fallback; - final symbol = currencySymbolFromCode(money.currency); - final suffix = symbol ?? money.currency; - if (suffix.trim().isEmpty) return amount; - return '$amount $suffix'; +String formatMoney(BuildContext context, Money? money) { + if (money == null || money.amount.trim().isEmpty) { + return unavailableMoneyValue(context); + } + return formatMoneyUi(context, money); } -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 formatAmount(BuildContext context, double amount, String currency) { + return formatAmountUi(context, amount: amount, currency: currency); } -String formatDateLabel(BuildContext context, DateTime? date, {String fallback = '-'}) { +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 = '-'}) { +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); diff --git a/frontend/pweb/lib/utils/report/source_filter.dart b/frontend/pweb/lib/utils/report/source_filter.dart index da7f1369..bcc81c79 100644 --- a/frontend/pweb/lib/utils/report/source_filter.dart +++ b/frontend/pweb/lib/utils/report/source_filter.dart @@ -1,4 +1,4 @@ -import 'package:pshared/models/payment/intent.dart'; +import 'package:pshared/models/payment/endpoint.dart'; import 'package:pshared/models/payment/methods/data.dart'; import 'package:pshared/models/payment/methods/ledger.dart'; import 'package:pshared/models/payment/methods/managed_wallet.dart'; @@ -7,6 +7,13 @@ import 'package:pshared/models/payment/payment.dart'; import 'package:pshared/models/payment/source_type.dart'; +typedef _SourceRefExtractor = String? Function(PaymentMethodData source); + +final Map _sourceExtractors = { + PaymentSourceType.wallet: _walletSourceRef, + PaymentSourceType.ledger: _ledgerSourceRef, +}; + bool paymentMatchesSource( Payment payment, { required PaymentSourceType sourceType, @@ -15,95 +22,54 @@ bool paymentMatchesSource( final normalizedSourceRef = _normalize(sourceRef); if (normalizedSourceRef == null) return false; - final paymentSourceRef = _paymentSourceRef(payment, sourceType); - return paymentSourceRef != null && paymentSourceRef == normalizedSourceRef; + final paymentSourceRefs = _paymentSourceRefs(payment, sourceType); + if (paymentSourceRefs.isEmpty) return false; + + return paymentSourceRefs.contains(normalizedSourceRef); } -String? _paymentSourceRef(Payment payment, PaymentSourceType sourceType) { - final fromIntent = _sourceRefFromIntent(payment.intent, sourceType); - if (fromIntent != null) return fromIntent; - return _sourceRefFromMetadata(payment.metadata, sourceType); +Set _paymentSourceRefs(Payment payment, PaymentSourceType sourceType) { + final fromSource = _sourceRefsFromEndpoint(payment.source, sourceType); + if (fromSource.isEmpty) return const {}; + return fromSource; } -String? _sourceRefFromIntent( - PaymentIntent? intent, +Set _sourceRefsFromEndpoint( + PaymentEndpoint? endpoint, PaymentSourceType sourceType, ) { - final source = intent?.source; - if (source == null) return null; + if (endpoint == null) return const {}; - final fromIntentAttributes = _sourceRefFromMetadata( - intent?.attributes, - sourceType, - ); - if (fromIntentAttributes != null) return fromIntentAttributes; - - switch (sourceType) { - case PaymentSourceType.wallet: - return _walletSourceRef(source); - case PaymentSourceType.ledger: - return _ledgerSourceRef(source); + final refs = {}; + void collect(String? value) { + final normalized = _normalize(value); + if (normalized == null) return; + refs.add(normalized); } + + final source = endpoint.method; + if (source != null) { + final fromMethod = _sourceExtractors[sourceType]?.call(source); + collect(fromMethod); + } + + collect(endpoint.paymentMethodRef); + + return refs; } -String? _walletSourceRef(PaymentMethodData source) { - if (source is ManagedWalletPaymentMethod) { - return _normalize(source.managedWalletRef) ?? - _sourceRefFromMetadata(source.metadata, PaymentSourceType.wallet); - } - if (source is WalletPaymentMethod) { - return _normalize(source.walletId) ?? - _sourceRefFromMetadata(source.metadata, PaymentSourceType.wallet); - } - return null; -} +String? _walletSourceRef(PaymentMethodData source) => switch (source) { + ManagedWalletPaymentMethod(:final managedWalletRef) => _normalize( + managedWalletRef, + ), + WalletPaymentMethod(:final walletId) => _normalize(walletId), + _ => null, +}; -String? _ledgerSourceRef(PaymentMethodData source) { - if (source is LedgerPaymentMethod) { - return _normalize(source.ledgerAccountRef) ?? - _sourceRefFromMetadata(source.metadata, PaymentSourceType.ledger); - } - return null; -} - -String? _sourceRefFromMetadata( - Map? metadata, - PaymentSourceType sourceType, -) { - if (metadata == null || metadata.isEmpty) return null; - - final keys = switch (sourceType) { - PaymentSourceType.wallet => const [ - 'source_wallet_ref', - 'managed_wallet_ref', - 'wallet_ref', - 'wallet_id', - 'source_wallet_id', - 'source_wallet_user_id', - 'wallet_user_id', - 'wallet_user_ref', - 'wallet_number', - 'source_wallet_number', - 'source_managed_wallet_ref', - 'source_ref', - ], - PaymentSourceType.ledger => const [ - 'source_ledger_account_ref', - 'ledger_account_ref', - 'source_account_code', - 'ledger_account_code', - 'account_code', - 'source_ref', - ], - }; - - for (final key in keys) { - final value = _normalize(metadata[key]); - if (value != null) return value; - } - - return null; -} +String? _ledgerSourceRef(PaymentMethodData source) => switch (source) { + LedgerPaymentMethod(:final ledgerAccountRef) => _normalize(ledgerAccountRef), + _ => null, +}; String? _normalize(String? value) { final normalized = value?.trim(); diff --git a/frontend/pweb/lib/utils/report/utils/format.dart b/frontend/pweb/lib/utils/report/utils/format.dart deleted file mode 100644 index f89d1428..00000000 --- a/frontend/pweb/lib/utils/report/utils/format.dart +++ /dev/null @@ -1,37 +0,0 @@ -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(); -} diff --git a/frontend/pweb/lib/widgets/payment/source_wallet_selector/balance_formatter.dart b/frontend/pweb/lib/widgets/payment/source_wallet_selector/balance_formatter.dart index 37e68bc1..8fda011b 100644 --- a/frontend/pweb/lib/widgets/payment/source_wallet_selector/balance_formatter.dart +++ b/frontend/pweb/lib/widgets/payment/source_wallet_selector/balance_formatter.dart @@ -1,31 +1,31 @@ +import 'package:flutter/widgets.dart'; + import 'package:pshared/models/ledger/account.dart'; +import 'package:pshared/models/money.dart'; import 'package:pshared/models/payment/wallet.dart'; import 'package:pshared/utils/currency.dart'; -import 'package:pshared/utils/money.dart'; + +import 'package:pweb/utils/money_display.dart'; -String walletBalance(Wallet wallet) { - final symbol = currencyCodeToSymbol(wallet.currency); - return '$symbol ${amountToString(wallet.balance)}'; +String walletBalance(BuildContext context, Wallet wallet) { + return formatMoneyUi( + context, + Money( + amount: amountToString(wallet.balance), + currency: currencyCodeToString(wallet.currency), + ), + ); } -String ledgerBalance(LedgerAccount account) { +String ledgerBalance(BuildContext context, LedgerAccount account) { final money = account.balance?.balance; - final rawAmount = money?.amount.trim(); - final amount = parseMoneyAmount(rawAmount, fallback: double.nan); - final amountText = amount.isNaN - ? (rawAmount == null || rawAmount.isEmpty ? '--' : rawAmount) - : amountToString(amount); + final effectiveCurrency = (money?.currency.trim().isNotEmpty ?? false) + ? money!.currency + : account.currency; - final currencyCode = (money?.currency ?? account.currency) - .trim() - .toUpperCase(); - final symbol = currencySymbolFromCode(currencyCode); - if (symbol != null && symbol.trim().isNotEmpty) { - return '$symbol $amountText'; - } - if (currencyCode.isNotEmpty) { - return '$amountText $currencyCode'; - } - return amountText; + return formatMoneyUi( + context, + Money(amount: money?.amount ?? '', currency: effectiveCurrency), + ); } diff --git a/frontend/pweb/lib/widgets/payment/source_wallet_selector/dropdown_items.dart b/frontend/pweb/lib/widgets/payment/source_wallet_selector/dropdown_items.dart index 28405370..3df4721a 100644 --- a/frontend/pweb/lib/widgets/payment/source_wallet_selector/dropdown_items.dart +++ b/frontend/pweb/lib/widgets/payment/source_wallet_selector/dropdown_items.dart @@ -11,6 +11,7 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; List> buildSourceSelectorItems({ + required BuildContext context, required List wallets, required List ledgerAccounts, required AppLocalizations l10n, @@ -20,7 +21,7 @@ List> buildSourceSelectorItems({ return DropdownMenuItem( value: walletOptionKey(wallet.id), child: Text( - '${walletDisplayName(wallet, l10n)} - ${walletBalance(wallet)}', + '${walletDisplayName(wallet, l10n)} - ${walletBalance(context, wallet)}', overflow: TextOverflow.ellipsis, ), ); @@ -29,7 +30,7 @@ List> buildSourceSelectorItems({ return DropdownMenuItem( value: ledgerOptionKey(ledger.ledgerAccountRef), child: Text( - '${ledgerDisplayName(ledger, l10n)} - ${ledgerBalance(ledger)}', + '${ledgerDisplayName(ledger, l10n)} - ${ledgerBalance(context, ledger)}', overflow: TextOverflow.ellipsis, ), ); diff --git a/frontend/pweb/lib/widgets/payment/source_wallet_selector/selector_field.dart b/frontend/pweb/lib/widgets/payment/source_wallet_selector/selector_field.dart index e897d151..e290dc10 100644 --- a/frontend/pweb/lib/widgets/payment/source_wallet_selector/selector_field.dart +++ b/frontend/pweb/lib/widgets/payment/source_wallet_selector/selector_field.dart @@ -25,6 +25,7 @@ Widget buildSourceSelectorField({ } final items = buildSourceSelectorItems( + context: context, wallets: wallets, ledgerAccounts: ledgerAccounts, l10n: l10n, -- 2.49.1