From 519a2b130451e2280c0a9b5ec2ecaec6b9c3cb60 Mon Sep 17 00:00:00 2001 From: Arseni Date: Thu, 5 Mar 2026 01:48:53 +0300 Subject: [PATCH] few fixes and made sure ledger widget displays the name of ledger wallet --- .../lib/data/dto/payment/operation.dart | 40 +--- .../pshared/lib/data/dto/payment/payment.dart | 1 - .../lib/data/mapper/ledger/account.dart | 43 +++-- .../lib/data/mapper/payment/operation.dart | 1 + .../lib/service/payment/documents.dart | 1 + .../lib/controllers/payments/details.dart | 7 +- .../dashboard/buttons/balance/ledger.dart | 28 +-- .../multiple/panels/source_quote/widget.dart | 3 +- .../send/widgets/method_selector.dart | 2 +- .../report/operations/document_rule.dart | 14 ++ .../pweb/lib/utils/report/payment_mapper.dart | 9 +- .../payment/source_wallet_selector.dart | 172 ------------------ .../balance_formatter.dart | 31 ++++ .../source_wallet_selector/display_name.dart | 19 ++ .../dropdown_items.dart | 38 ++++ .../source_wallet_selector/options.dart | 25 +++ .../selector_field.dart | 57 ++++++ .../payment/source_wallet_selector/view.dart | 51 ++++++ 18 files changed, 293 insertions(+), 249 deletions(-) create mode 100644 frontend/pweb/lib/utils/report/operations/document_rule.dart delete mode 100644 frontend/pweb/lib/widgets/payment/source_wallet_selector.dart create mode 100644 frontend/pweb/lib/widgets/payment/source_wallet_selector/balance_formatter.dart create mode 100644 frontend/pweb/lib/widgets/payment/source_wallet_selector/display_name.dart create mode 100644 frontend/pweb/lib/widgets/payment/source_wallet_selector/dropdown_items.dart create mode 100644 frontend/pweb/lib/widgets/payment/source_wallet_selector/options.dart create mode 100644 frontend/pweb/lib/widgets/payment/source_wallet_selector/selector_field.dart create mode 100644 frontend/pweb/lib/widgets/payment/source_wallet_selector/view.dart diff --git a/frontend/pshared/lib/data/dto/payment/operation.dart b/frontend/pshared/lib/data/dto/payment/operation.dart index 42b0a106..f10db2c2 100644 --- a/frontend/pshared/lib/data/dto/payment/operation.dart +++ b/frontend/pshared/lib/data/dto/payment/operation.dart @@ -1,3 +1,8 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'operation.g.dart'; + +@JsonSerializable() class PaymentOperationDTO { final String? stepRef; final String? operationRef; @@ -24,37 +29,6 @@ class PaymentOperationDTO { }); factory PaymentOperationDTO.fromJson(Map json) => - PaymentOperationDTO( - stepRef: _asString(json['stepRef'] ?? json['step_ref']), - operationRef: _asString(json['operationRef'] ?? json['operation_ref']), - gateway: _asString(json['gateway']), - code: _asString(json['code']), - state: _asString(json['state']), - label: _asString(json['label']), - failureCode: _asString(json['failureCode'] ?? json['failure_code']), - failureReason: _asString( - json['failureReason'] ?? json['failure_reason'], - ), - startedAt: _asString(json['startedAt'] ?? json['started_at']), - completedAt: _asString(json['completedAt'] ?? json['completed_at']), - ); - - Map toJson() => { - 'stepRef': stepRef, - 'operationRef': operationRef, - 'gateway': gateway, - 'code': code, - 'state': state, - 'label': label, - 'failureCode': failureCode, - 'failureReason': failureReason, - 'startedAt': startedAt, - 'completedAt': completedAt, - }; -} - -String? _asString(Object? value) { - final text = value?.toString().trim(); - if (text == null || text.isEmpty) return null; - return text; + _$PaymentOperationDTOFromJson(json); + Map toJson() => _$PaymentOperationDTOToJson(this); } diff --git a/frontend/pshared/lib/data/dto/payment/payment.dart b/frontend/pshared/lib/data/dto/payment/payment.dart index c3f4cddd..3c67c1fa 100644 --- a/frontend/pshared/lib/data/dto/payment/payment.dart +++ b/frontend/pshared/lib/data/dto/payment/payment.dart @@ -13,7 +13,6 @@ class PaymentDTO { final String? state; final String? failureCode; final String? failureReason; - @JsonKey(defaultValue: []) final List operations; final PaymentQuoteDTO? lastQuote; final Map? metadata; diff --git a/frontend/pshared/lib/data/mapper/ledger/account.dart b/frontend/pshared/lib/data/mapper/ledger/account.dart index f49130ea..6197c936 100644 --- a/frontend/pshared/lib/data/mapper/ledger/account.dart +++ b/frontend/pshared/lib/data/mapper/ledger/account.dart @@ -9,22 +9,33 @@ import 'package:pshared/models/ledger/account.dart'; extension LedgerAccountDTOMapper on LedgerAccountDTO { - LedgerAccount toDomain() => LedgerAccount( - ledgerAccountRef: ledgerAccountRef, - organizationRef: organizationRef, - ownerRef: ownerRef, - accountCode: accountCode, - accountType: accountType.toDomain(), - currency: currency, - status: status.toDomain(), - allowNegative: allowNegative, - role: role.toDomain(), - metadata: metadata, - createdAt: createdAt, - updatedAt: updatedAt, - describable: describable?.toDomain() ?? newDescribable(name: '', description: null), - balance: balance?.toDomain(), - ); + LedgerAccount toDomain() { + final mappedDescribable = describable?.toDomain(); + final fallbackName = metadata?['name']?.trim() ?? ''; + final name = mappedDescribable?.name.trim().isNotEmpty == true + ? mappedDescribable!.name + : fallbackName; + + return LedgerAccount( + ledgerAccountRef: ledgerAccountRef, + organizationRef: organizationRef, + ownerRef: ownerRef, + accountCode: accountCode, + accountType: accountType.toDomain(), + currency: currency, + status: status.toDomain(), + allowNegative: allowNegative, + role: role.toDomain(), + metadata: metadata, + createdAt: createdAt, + updatedAt: updatedAt, + describable: newDescribable( + name: name, + description: mappedDescribable?.description, + ), + balance: balance?.toDomain(), + ); + } } extension LedgerAccountModelMapper on LedgerAccount { diff --git a/frontend/pshared/lib/data/mapper/payment/operation.dart b/frontend/pshared/lib/data/mapper/payment/operation.dart index 4ab8e3fd..0b086d6f 100644 --- a/frontend/pshared/lib/data/mapper/payment/operation.dart +++ b/frontend/pshared/lib/data/mapper/payment/operation.dart @@ -1,6 +1,7 @@ import 'package:pshared/data/dto/payment/operation.dart'; import 'package:pshared/models/payment/execution_operation.dart'; + extension PaymentOperationDTOMapper on PaymentOperationDTO { PaymentExecutionOperation toDomain() => PaymentExecutionOperation( stepRef: stepRef, diff --git a/frontend/pshared/lib/service/payment/documents.dart b/frontend/pshared/lib/service/payment/documents.dart index 37c0b1df..ec05f41e 100644 --- a/frontend/pshared/lib/service/payment/documents.dart +++ b/frontend/pshared/lib/service/payment/documents.dart @@ -4,6 +4,7 @@ import 'package:pshared/models/file/downloaded_file.dart'; import 'package:pshared/service/authorization/service.dart'; import 'package:pshared/service/services.dart'; + class PaymentDocumentsService { static final _logger = Logger('service.payment_documents'); static const String _objectType = Services.payments; diff --git a/frontend/pweb/lib/controllers/payments/details.dart b/frontend/pweb/lib/controllers/payments/details.dart index 7f99a55e..2258a203 100644 --- a/frontend/pweb/lib/controllers/payments/details.dart +++ b/frontend/pweb/lib/controllers/payments/details.dart @@ -6,10 +6,9 @@ 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/payment/operation_code.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}) : _paymentId = paymentId; @@ -53,9 +52,7 @@ class PaymentDetailsController extends ChangeNotifier { final gatewayService = operation.gateway; if (gatewayService == null || gatewayService.isEmpty) return null; - final pair = parseOperationCodePair(operation.code); - if (pair == null) return null; - if (pair.operation != 'card_payout' || pair.action != 'send') return null; + if (!isOperationDocumentEligible(operation.code)) return null; return OperationDocumentRequestModel( gatewayService: gatewayService, diff --git a/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger.dart b/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger.dart index 269b3539..5748c7e0 100644 --- a/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger.dart +++ b/frontend/pweb/lib/pages/dashboard/buttons/balance/ledger.dart @@ -17,10 +17,7 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; class LedgerAccountCard extends StatelessWidget { final LedgerAccount account; - const LedgerAccountCard({ - super.key, - required this.account, - }); + const LedgerAccountCard({super.key, required this.account}); String _formatBalance() { final money = account.balance?.balance; @@ -62,8 +59,13 @@ class LedgerAccountCard extends StatelessWidget { final textTheme = Theme.of(context).textTheme; final colorScheme = Theme.of(context).colorScheme; final loc = AppLocalizations.of(context)!; - final subtitle = account.name.isNotEmpty ? account.name : account.accountCode; - final badge = account.currency.trim().isEmpty ? null : account.currency.toUpperCase(); + 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, @@ -76,16 +78,14 @@ class LedgerAccountCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - BalanceHeader( - title: loc.paymentTypeLedger, - subtitle: subtitle.isNotEmpty ? subtitle : null, - badge: badge, - ), + BalanceHeader(title: title, subtitle: subtitle, badge: badge), Row( children: [ Consumer( builder: (context, controller, _) { - final isMasked = controller.isBalanceMasked(account.ledgerAccountRef); + final isMasked = controller.isBalanceMasked( + account.ledgerAccountRef, + ); return Row( children: [ Text( @@ -97,7 +97,9 @@ class LedgerAccountCard extends StatelessWidget { ), const SizedBox(width: 12), GestureDetector( - onTap: () => controller.toggleBalanceMask(account.ledgerAccountRef), + onTap: () => controller.toggleBalanceMask( + account.ledgerAccountRef, + ), child: Icon( isMasked ? Icons.visibility_off : Icons.visibility, size: 24, diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/widget.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/widget.dart index ced0e633..238d33f0 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/widget.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/widget.dart @@ -13,14 +13,13 @@ import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/summar import 'package:pweb/pages/dashboard/payouts/multiple/widgets/quote_status.dart'; import 'package:pweb/pages/payout_page/send/widgets/send_button.dart'; import 'package:pweb/widgets/payment/source_of_funds_panel.dart'; -import 'package:pweb/widgets/payment/source_wallet_selector.dart'; +import 'package:pweb/widgets/payment/source_wallet_selector/view.dart'; import 'package:pweb/widgets/cooldown_hint.dart'; import 'package:pweb/widgets/refresh_balance/wallet.dart'; import 'package:pweb/models/state/control_state.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; - class SourceQuotePanel extends StatelessWidget { const SourceQuotePanel({super.key, required this.controller}); diff --git a/frontend/pweb/lib/pages/payout_page/send/widgets/method_selector.dart b/frontend/pweb/lib/pages/payout_page/send/widgets/method_selector.dart index 25bd0dc4..edca5186 100644 --- a/frontend/pweb/lib/pages/payout_page/send/widgets/method_selector.dart +++ b/frontend/pweb/lib/pages/payout_page/send/widgets/method_selector.dart @@ -4,7 +4,7 @@ import 'package:provider/provider.dart'; import 'package:pshared/controllers/payment/source.dart'; -import 'package:pweb/widgets/payment/source_wallet_selector.dart'; +import 'package:pweb/widgets/payment/source_wallet_selector/view.dart'; class PaymentMethodSelector extends StatelessWidget { const PaymentMethodSelector({super.key}); diff --git a/frontend/pweb/lib/utils/report/operations/document_rule.dart b/frontend/pweb/lib/utils/report/operations/document_rule.dart new file mode 100644 index 00000000..8ebe2601 --- /dev/null +++ b/frontend/pweb/lib/utils/report/operations/document_rule.dart @@ -0,0 +1,14 @@ +import 'package:pweb/utils/payment/operation_code.dart'; + +const String _documentOperation = 'card_payout'; +const String _documentAction = 'send'; + +bool isOperationDocumentEligible(String? operationCode) { + final pair = parseOperationCodePair(operationCode); + if (pair == null) return false; + return _isDocumentOperationPair(pair); +} + +bool _isDocumentOperationPair(OperationCodePair pair) { + return pair.operation == _documentOperation && pair.action == _documentAction; +} diff --git a/frontend/pweb/lib/utils/report/payment_mapper.dart b/frontend/pweb/lib/utils/report/payment_mapper.dart index 2e9a7e08..f41027c2 100644 --- a/frontend/pweb/lib/utils/report/payment_mapper.dart +++ b/frontend/pweb/lib/utils/report/payment_mapper.dart @@ -5,8 +5,7 @@ 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/payment/operation_code.dart'; - +import 'package:pweb/utils/report/operations/document_rule.dart'; OperationItem mapPaymentToOperation(Payment payment) { final debit = payment.lastQuote?.amounts?.sourceDebitTotal; @@ -63,9 +62,7 @@ OperationDocumentInfo? _resolveOperationDocument(Payment payment) { if (operationRef == null || operationRef.isEmpty) continue; if (gatewayService == null || gatewayService.isEmpty) continue; - final pair = parseOperationCodePair(operation.code); - if (pair == null) continue; - if (pair.operation != 'card_payout' || pair.action != 'send') continue; + if (!isOperationDocumentEligible(operation.code)) continue; return OperationDocumentInfo( operationRef: operationRef, @@ -133,4 +130,4 @@ String? _firstNonEmpty(List values) { if (trimmed != null && trimmed.isNotEmpty) return trimmed; } return null; -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/widgets/payment/source_wallet_selector.dart b/frontend/pweb/lib/widgets/payment/source_wallet_selector.dart deleted file mode 100644 index fc8dd7e8..00000000 --- a/frontend/pweb/lib/widgets/payment/source_wallet_selector.dart +++ /dev/null @@ -1,172 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:pshared/controllers/payment/source.dart'; -import 'package:pshared/models/ledger/account.dart'; -import 'package:pshared/models/payment/source_type.dart'; -import 'package:pshared/models/payment/wallet.dart'; -import 'package:pshared/utils/currency.dart'; -import 'package:pshared/utils/money.dart'; - -import 'package:pweb/generated/i18n/app_localizations.dart'; - -typedef _SourceOptionKey = ({PaymentSourceType type, String ref}); - -class SourceWalletSelector extends StatelessWidget { - const SourceWalletSelector({ - super.key, - required this.sourceController, - this.isBusy = false, - this.onChanged, - }); - - final PaymentSourceController sourceController; - final bool isBusy; - final ValueChanged? onChanged; - - @override - Widget build(BuildContext context) { - final source = sourceController; - final selectedWallet = source.selectedWallet; - final selectedLedger = source.selectedLedgerAccount; - final selectedValue = switch (source.selectedType) { - PaymentSourceType.wallet => - selectedWallet == null ? null : _walletKey(selectedWallet.id), - PaymentSourceType.ledger => - selectedLedger == null - ? null - : _ledgerKey(selectedLedger.ledgerAccountRef), - null => null, - }; - - return _buildSourceSelector( - context: context, - wallets: source.wallets, - ledgerAccounts: source.ledgerAccounts, - selectedValue: selectedValue, - onChanged: (value) { - if (value.type == PaymentSourceType.wallet) { - source.selectWalletByRef(value.ref); - final selected = source.selectedWallet; - if (selected != null) { - onChanged?.call(selected); - } - return; - } - - if (value.type == PaymentSourceType.ledger) { - source.selectLedgerByRef(value.ref); - } - }, - ); - } - - Widget _buildSourceSelector({ - required BuildContext context, - required List wallets, - required List ledgerAccounts, - required _SourceOptionKey? selectedValue, - required ValueChanged<_SourceOptionKey> onChanged, - }) { - final theme = Theme.of(context); - final l10n = AppLocalizations.of(context)!; - - if (wallets.isEmpty && ledgerAccounts.isEmpty) { - return Text(l10n.noWalletsAvailable, style: theme.textTheme.bodySmall); - } - - final items = >[ - ...wallets.map((wallet) { - return DropdownMenuItem<_SourceOptionKey>( - value: _walletKey(wallet.id), - child: Text( - '${_walletDisplayName(wallet, l10n)} - ${_walletBalance(wallet)}', - overflow: TextOverflow.ellipsis, - ), - ); - }), - ...ledgerAccounts.map((ledger) { - return DropdownMenuItem<_SourceOptionKey>( - value: _ledgerKey(ledger.ledgerAccountRef), - child: Text( - '${_ledgerDisplayName(ledger, l10n)} - ${_ledgerBalance(ledger)}', - overflow: TextOverflow.ellipsis, - ), - ); - }), - ]; - - final knownValues = items - .map((item) => item.value) - .whereType<_SourceOptionKey>() - .toSet(); - final effectiveValue = knownValues.contains(selectedValue) - ? selectedValue - : null; - - return DropdownButtonFormField<_SourceOptionKey>( - initialValue: effectiveValue, - isExpanded: true, - decoration: InputDecoration( - labelText: l10n.whereGetMoney, - border: const OutlineInputBorder(), - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, - ), - ), - items: items, - onChanged: isBusy - ? null - : (value) { - if (value == null) return; - onChanged(value); - }, - ); - } - - _SourceOptionKey _walletKey(String walletRef) => - (type: PaymentSourceType.wallet, ref: walletRef); - - _SourceOptionKey _ledgerKey(String ledgerAccountRef) => - (type: PaymentSourceType.ledger, ref: ledgerAccountRef); - - String _walletDisplayName(Wallet wallet, AppLocalizations l10n) { - final name = wallet.name.trim(); - if (name.isNotEmpty) return name; - - return l10n.paymentTypeWallet; - } - - String _ledgerDisplayName(LedgerAccount ledger, AppLocalizations l10n) { - final name = ledger.name.trim(); - if (name.isNotEmpty) return name; - - return l10n.paymentTypeLedger; - } - - String _walletBalance(Wallet wallet) { - final symbol = currencyCodeToSymbol(wallet.currency); - return '$symbol ${amountToString(wallet.balance)}'; - } - - String _ledgerBalance(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 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; - } -} 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 new file mode 100644 index 00000000..37e68bc1 --- /dev/null +++ b/frontend/pweb/lib/widgets/payment/source_wallet_selector/balance_formatter.dart @@ -0,0 +1,31 @@ +import 'package:pshared/models/ledger/account.dart'; +import 'package:pshared/models/payment/wallet.dart'; +import 'package:pshared/utils/currency.dart'; +import 'package:pshared/utils/money.dart'; + + +String walletBalance(Wallet wallet) { + final symbol = currencyCodeToSymbol(wallet.currency); + return '$symbol ${amountToString(wallet.balance)}'; +} + +String ledgerBalance(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 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; +} diff --git a/frontend/pweb/lib/widgets/payment/source_wallet_selector/display_name.dart b/frontend/pweb/lib/widgets/payment/source_wallet_selector/display_name.dart new file mode 100644 index 00000000..495954ef --- /dev/null +++ b/frontend/pweb/lib/widgets/payment/source_wallet_selector/display_name.dart @@ -0,0 +1,19 @@ +import 'package:pshared/models/ledger/account.dart'; +import 'package:pshared/models/payment/wallet.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +String walletDisplayName(Wallet wallet, AppLocalizations l10n) { + return sourceDisplayName(name: wallet.name, fallback: l10n.paymentTypeWallet); +} + +String ledgerDisplayName(LedgerAccount ledger, AppLocalizations l10n) { + return sourceDisplayName(name: ledger.name, fallback: l10n.paymentTypeLedger); +} + +String sourceDisplayName({required String name, required String fallback}) { + final normalized = name.trim(); + if (normalized.isNotEmpty) return normalized; + return fallback; +} 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 new file mode 100644 index 00000000..28405370 --- /dev/null +++ b/frontend/pweb/lib/widgets/payment/source_wallet_selector/dropdown_items.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/ledger/account.dart'; +import 'package:pshared/models/payment/wallet.dart'; + +import 'package:pweb/widgets/payment/source_wallet_selector/balance_formatter.dart'; +import 'package:pweb/widgets/payment/source_wallet_selector/display_name.dart'; +import 'package:pweb/widgets/payment/source_wallet_selector/options.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +List> buildSourceSelectorItems({ + required List wallets, + required List ledgerAccounts, + required AppLocalizations l10n, +}) { + return >[ + ...wallets.map((wallet) { + return DropdownMenuItem( + value: walletOptionKey(wallet.id), + child: Text( + '${walletDisplayName(wallet, l10n)} - ${walletBalance(wallet)}', + overflow: TextOverflow.ellipsis, + ), + ); + }), + ...ledgerAccounts.map((ledger) { + return DropdownMenuItem( + value: ledgerOptionKey(ledger.ledgerAccountRef), + child: Text( + '${ledgerDisplayName(ledger, l10n)} - ${ledgerBalance(ledger)}', + overflow: TextOverflow.ellipsis, + ), + ); + }), + ]; +} diff --git a/frontend/pweb/lib/widgets/payment/source_wallet_selector/options.dart b/frontend/pweb/lib/widgets/payment/source_wallet_selector/options.dart new file mode 100644 index 00000000..0efed2b3 --- /dev/null +++ b/frontend/pweb/lib/widgets/payment/source_wallet_selector/options.dart @@ -0,0 +1,25 @@ +import 'package:pshared/controllers/payment/source.dart'; +import 'package:pshared/models/payment/source_type.dart'; + + +typedef SourceOptionKey = ({PaymentSourceType type, String ref}); + +SourceOptionKey walletOptionKey(String walletRef) => + (type: PaymentSourceType.wallet, ref: walletRef); + +SourceOptionKey ledgerOptionKey(String ledgerAccountRef) => + (type: PaymentSourceType.ledger, ref: ledgerAccountRef); + +SourceOptionKey? resolveSelectedSourceOption(PaymentSourceController source) { + final selectedWallet = source.selectedWallet; + final selectedLedger = source.selectedLedgerAccount; + return switch (source.selectedType) { + PaymentSourceType.wallet => + selectedWallet == null ? null : walletOptionKey(selectedWallet.id), + PaymentSourceType.ledger => + selectedLedger == null + ? null + : ledgerOptionKey(selectedLedger.ledgerAccountRef), + null => null, + }; +} 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 new file mode 100644 index 00000000..e897d151 --- /dev/null +++ b/frontend/pweb/lib/widgets/payment/source_wallet_selector/selector_field.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/models/ledger/account.dart'; +import 'package:pshared/models/payment/wallet.dart'; + +import 'package:pweb/widgets/payment/source_wallet_selector/dropdown_items.dart'; +import 'package:pweb/widgets/payment/source_wallet_selector/options.dart'; + +import 'package:pweb/generated/i18n/app_localizations.dart'; + + +Widget buildSourceSelectorField({ + required BuildContext context, + required List wallets, + required List ledgerAccounts, + required SourceOptionKey? selectedValue, + required ValueChanged onChanged, + required bool isBusy, +}) { + final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; + + if (wallets.isEmpty && ledgerAccounts.isEmpty) { + return Text(l10n.noWalletsAvailable, style: theme.textTheme.bodySmall); + } + + final items = buildSourceSelectorItems( + wallets: wallets, + ledgerAccounts: ledgerAccounts, + l10n: l10n, + ); + + final knownValues = items + .map((item) => item.value) + .whereType() + .toSet(); + final effectiveValue = knownValues.contains(selectedValue) + ? selectedValue + : null; + + return DropdownButtonFormField( + initialValue: effectiveValue, + isExpanded: true, + decoration: InputDecoration( + labelText: l10n.whereGetMoney, + border: const OutlineInputBorder(), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + ), + items: items, + onChanged: isBusy + ? null + : (value) { + if (value == null) return; + onChanged(value); + }, + ); +} diff --git a/frontend/pweb/lib/widgets/payment/source_wallet_selector/view.dart b/frontend/pweb/lib/widgets/payment/source_wallet_selector/view.dart new file mode 100644 index 00000000..2eaaf1d1 --- /dev/null +++ b/frontend/pweb/lib/widgets/payment/source_wallet_selector/view.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +import 'package:pshared/controllers/payment/source.dart'; +import 'package:pshared/models/payment/source_type.dart'; +import 'package:pshared/models/payment/wallet.dart'; + +import 'package:pweb/widgets/payment/source_wallet_selector/options.dart'; +import 'package:pweb/widgets/payment/source_wallet_selector/selector_field.dart'; + + +class SourceWalletSelector extends StatelessWidget { + const SourceWalletSelector({ + super.key, + required this.sourceController, + this.isBusy = false, + this.onChanged, + }); + + final PaymentSourceController sourceController; + final bool isBusy; + final ValueChanged? onChanged; + + @override + Widget build(BuildContext context) { + final source = sourceController; + + return buildSourceSelectorField( + context: context, + wallets: source.wallets, + ledgerAccounts: source.ledgerAccounts, + selectedValue: resolveSelectedSourceOption(source), + onChanged: (value) => _onSourceChanged(source, value), + isBusy: isBusy, + ); + } + + void _onSourceChanged(PaymentSourceController source, SourceOptionKey value) { + if (value.type == PaymentSourceType.wallet) { + source.selectWalletByRef(value.ref); + final selected = source.selectedWallet; + if (selected != null) { + onChanged?.call(selected); + } + return; + } + + if (value.type == PaymentSourceType.ledger) { + source.selectLedgerByRef(value.ref); + } + } +}