From d6a3a0cc5b3f967cc33968d8579b8557d346ce7e Mon Sep 17 00:00:00 2001 From: Arseni Date: Thu, 5 Mar 2026 15:48:52 +0300 Subject: [PATCH] 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(); + } } } }