added download for operation and included fixes for source of payments #639
@@ -1,3 +1,8 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'operation.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
class PaymentOperationDTO {
|
class PaymentOperationDTO {
|
||||||
final String? stepRef;
|
final String? stepRef;
|
||||||
final String? operationRef;
|
final String? operationRef;
|
||||||
@@ -24,37 +29,6 @@ class PaymentOperationDTO {
|
|||||||
});
|
});
|
||||||
|
|
||||||
factory PaymentOperationDTO.fromJson(Map<String, dynamic> json) =>
|
factory PaymentOperationDTO.fromJson(Map<String, dynamic> json) =>
|
||||||
PaymentOperationDTO(
|
_$PaymentOperationDTOFromJson(json);
|
||||||
stepRef: _asString(json['stepRef'] ?? json['step_ref']),
|
Map<String, dynamic> toJson() => _$PaymentOperationDTOToJson(this);
|
||||||
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<String, dynamic> toJson() => <String, dynamic>{
|
|
||||||
'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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ class PaymentDTO {
|
|||||||
final String? state;
|
final String? state;
|
||||||
final String? failureCode;
|
final String? failureCode;
|
||||||
final String? failureReason;
|
final String? failureReason;
|
||||||
@JsonKey(defaultValue: <PaymentOperationDTO>[])
|
|
||||||
final List<PaymentOperationDTO> operations;
|
final List<PaymentOperationDTO> operations;
|
||||||
final PaymentQuoteDTO? lastQuote;
|
final PaymentQuoteDTO? lastQuote;
|
||||||
final Map<String, String>? metadata;
|
final Map<String, String>? metadata;
|
||||||
|
|||||||
@@ -9,22 +9,33 @@ import 'package:pshared/models/ledger/account.dart';
|
|||||||
|
|
||||||
|
|
||||||
extension LedgerAccountDTOMapper on LedgerAccountDTO {
|
extension LedgerAccountDTOMapper on LedgerAccountDTO {
|
||||||
LedgerAccount toDomain() => LedgerAccount(
|
LedgerAccount toDomain() {
|
||||||
ledgerAccountRef: ledgerAccountRef,
|
final mappedDescribable = describable?.toDomain();
|
||||||
organizationRef: organizationRef,
|
final fallbackName = metadata?['name']?.trim() ?? '';
|
||||||
ownerRef: ownerRef,
|
final name = mappedDescribable?.name.trim().isNotEmpty == true
|
||||||
accountCode: accountCode,
|
? mappedDescribable!.name
|
||||||
accountType: accountType.toDomain(),
|
: fallbackName;
|
||||||
currency: currency,
|
|
||||||
status: status.toDomain(),
|
return LedgerAccount(
|
||||||
allowNegative: allowNegative,
|
ledgerAccountRef: ledgerAccountRef,
|
||||||
role: role.toDomain(),
|
organizationRef: organizationRef,
|
||||||
metadata: metadata,
|
ownerRef: ownerRef,
|
||||||
createdAt: createdAt,
|
accountCode: accountCode,
|
||||||
updatedAt: updatedAt,
|
accountType: accountType.toDomain(),
|
||||||
describable: describable?.toDomain() ?? newDescribable(name: '', description: null),
|
currency: currency,
|
||||||
balance: balance?.toDomain(),
|
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 {
|
extension LedgerAccountModelMapper on LedgerAccount {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:pshared/data/dto/payment/operation.dart';
|
import 'package:pshared/data/dto/payment/operation.dart';
|
||||||
import 'package:pshared/models/payment/execution_operation.dart';
|
import 'package:pshared/models/payment/execution_operation.dart';
|
||||||
|
|
||||||
|
|
||||||
extension PaymentOperationDTOMapper on PaymentOperationDTO {
|
extension PaymentOperationDTOMapper on PaymentOperationDTO {
|
||||||
PaymentExecutionOperation toDomain() => PaymentExecutionOperation(
|
PaymentExecutionOperation toDomain() => PaymentExecutionOperation(
|
||||||
stepRef: stepRef,
|
stepRef: stepRef,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:pshared/models/file/downloaded_file.dart';
|
|||||||
import 'package:pshared/service/authorization/service.dart';
|
import 'package:pshared/service/authorization/service.dart';
|
||||||
import 'package:pshared/service/services.dart';
|
import 'package:pshared/service/services.dart';
|
||||||
|
|
||||||
|
|
||||||
class PaymentDocumentsService {
|
class PaymentDocumentsService {
|
||||||
static final _logger = Logger('service.payment_documents');
|
static final _logger = Logger('service.payment_documents');
|
||||||
static const String _objectType = Services.payments;
|
static const String _objectType = Services.payments;
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import 'package:pshared/models/payment/status.dart';
|
|||||||
import 'package:pshared/provider/payment/payments.dart';
|
import 'package:pshared/provider/payment/payments.dart';
|
||||||
import 'package:pweb/models/documents/operation.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';
|
import 'package:pweb/utils/report/payment_mapper.dart';
|
||||||
|
|
||||||
|
|
||||||
class PaymentDetailsController extends ChangeNotifier {
|
class PaymentDetailsController extends ChangeNotifier {
|
||||||
PaymentDetailsController({required String paymentId})
|
PaymentDetailsController({required String paymentId})
|
||||||
: _paymentId = paymentId;
|
: _paymentId = paymentId;
|
||||||
@@ -53,9 +52,7 @@ class PaymentDetailsController extends ChangeNotifier {
|
|||||||
final gatewayService = operation.gateway;
|
final gatewayService = operation.gateway;
|
||||||
if (gatewayService == null || gatewayService.isEmpty) return null;
|
if (gatewayService == null || gatewayService.isEmpty) return null;
|
||||||
|
|
||||||
final pair = parseOperationCodePair(operation.code);
|
if (!isOperationDocumentEligible(operation.code)) return null;
|
||||||
if (pair == null) return null;
|
|
||||||
if (pair.operation != 'card_payout' || pair.action != 'send') return null;
|
|
||||||
|
|
||||||
return OperationDocumentRequestModel(
|
return OperationDocumentRequestModel(
|
||||||
gatewayService: gatewayService,
|
gatewayService: gatewayService,
|
||||||
|
|||||||
@@ -17,10 +17,7 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
|||||||
class LedgerAccountCard extends StatelessWidget {
|
class LedgerAccountCard extends StatelessWidget {
|
||||||
final LedgerAccount account;
|
final LedgerAccount account;
|
||||||
|
|
||||||
const LedgerAccountCard({
|
const LedgerAccountCard({super.key, required this.account});
|
||||||
super.key,
|
|
||||||
required this.account,
|
|
||||||
});
|
|
||||||
|
|
||||||
String _formatBalance() {
|
String _formatBalance() {
|
||||||
final money = account.balance?.balance;
|
final money = account.balance?.balance;
|
||||||
@@ -62,8 +59,13 @@ class LedgerAccountCard extends StatelessWidget {
|
|||||||
final textTheme = Theme.of(context).textTheme;
|
final textTheme = Theme.of(context).textTheme;
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final loc = AppLocalizations.of(context)!;
|
final loc = AppLocalizations.of(context)!;
|
||||||
final subtitle = account.name.isNotEmpty ? account.name : account.accountCode;
|
final accountName = account.name.trim();
|
||||||
final badge = account.currency.trim().isEmpty ? null : account.currency.toUpperCase();
|
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(
|
return Card(
|
||||||
color: colorScheme.onSecondary,
|
color: colorScheme.onSecondary,
|
||||||
@@ -76,16 +78,14 @@ class LedgerAccountCard extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
BalanceHeader(
|
BalanceHeader(title: title, subtitle: subtitle, badge: badge),
|
||||||
title: loc.paymentTypeLedger,
|
|
||||||
subtitle: subtitle.isNotEmpty ? subtitle : null,
|
|
||||||
badge: badge,
|
|
||||||
),
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Consumer<LedgerBalanceMaskController>(
|
Consumer<LedgerBalanceMaskController>(
|
||||||
builder: (context, controller, _) {
|
builder: (context, controller, _) {
|
||||||
final isMasked = controller.isBalanceMasked(account.ledgerAccountRef);
|
final isMasked = controller.isBalanceMasked(
|
||||||
|
account.ledgerAccountRef,
|
||||||
|
);
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
@@ -97,7 +97,9 @@ class LedgerAccountCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => controller.toggleBalanceMask(account.ledgerAccountRef),
|
onTap: () => controller.toggleBalanceMask(
|
||||||
|
account.ledgerAccountRef,
|
||||||
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
isMasked ? Icons.visibility_off : Icons.visibility,
|
isMasked ? Icons.visibility_off : Icons.visibility,
|
||||||
size: 24,
|
size: 24,
|
||||||
|
|||||||
@@ -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/dashboard/payouts/multiple/widgets/quote_status.dart';
|
||||||
import 'package:pweb/pages/payout_page/send/widgets/send_button.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_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/cooldown_hint.dart';
|
||||||
import 'package:pweb/widgets/refresh_balance/wallet.dart';
|
import 'package:pweb/widgets/refresh_balance/wallet.dart';
|
||||||
import 'package:pweb/models/state/control_state.dart';
|
import 'package:pweb/models/state/control_state.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
class SourceQuotePanel extends StatelessWidget {
|
class SourceQuotePanel extends StatelessWidget {
|
||||||
const SourceQuotePanel({super.key, required this.controller});
|
const SourceQuotePanel({super.key, required this.controller});
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import 'package:pshared/controllers/payment/source.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 {
|
class PaymentMethodSelector extends StatelessWidget {
|
||||||
const PaymentMethodSelector({super.key});
|
const PaymentMethodSelector({super.key});
|
||||||
|
|||||||
14
frontend/pweb/lib/utils/report/operations/document_rule.dart
Normal file
14
frontend/pweb/lib/utils/report/operations/document_rule.dart
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -5,8 +5,7 @@ import 'package:pshared/models/payment/status.dart';
|
|||||||
import 'package:pshared/utils/money.dart';
|
import 'package:pshared/utils/money.dart';
|
||||||
|
|
||||||
import 'package:pweb/models/report/operation/document.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) {
|
OperationItem mapPaymentToOperation(Payment payment) {
|
||||||
final debit = payment.lastQuote?.amounts?.sourceDebitTotal;
|
final debit = payment.lastQuote?.amounts?.sourceDebitTotal;
|
||||||
@@ -63,9 +62,7 @@ OperationDocumentInfo? _resolveOperationDocument(Payment payment) {
|
|||||||
if (operationRef == null || operationRef.isEmpty) continue;
|
if (operationRef == null || operationRef.isEmpty) continue;
|
||||||
if (gatewayService == null || gatewayService.isEmpty) continue;
|
if (gatewayService == null || gatewayService.isEmpty) continue;
|
||||||
|
|
||||||
final pair = parseOperationCodePair(operation.code);
|
if (!isOperationDocumentEligible(operation.code)) continue;
|
||||||
if (pair == null) continue;
|
|
||||||
if (pair.operation != 'card_payout' || pair.action != 'send') continue;
|
|
||||||
|
|
||||||
return OperationDocumentInfo(
|
return OperationDocumentInfo(
|
||||||
operationRef: operationRef,
|
operationRef: operationRef,
|
||||||
|
|
|||||||
|
|||||||
@@ -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<Wallet>? 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<Wallet> wallets,
|
|
||||||
required List<LedgerAccount> 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 = <DropdownMenuItem<_SourceOptionKey>>[
|
|
||||||
...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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<DropdownMenuItem<SourceOptionKey>> buildSourceSelectorItems({
|
||||||
|
required List<Wallet> wallets,
|
||||||
|
required List<LedgerAccount> ledgerAccounts,
|
||||||
|
required AppLocalizations l10n,
|
||||||
|
}) {
|
||||||
|
return <DropdownMenuItem<SourceOptionKey>>[
|
||||||
|
...wallets.map((wallet) {
|
||||||
|
return DropdownMenuItem<SourceOptionKey>(
|
||||||
|
value: walletOptionKey(wallet.id),
|
||||||
|
child: Text(
|
||||||
|
'${walletDisplayName(wallet, l10n)} - ${walletBalance(wallet)}',
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
...ledgerAccounts.map((ledger) {
|
||||||
|
return DropdownMenuItem<SourceOptionKey>(
|
||||||
|
value: ledgerOptionKey(ledger.ledgerAccountRef),
|
||||||
|
child: Text(
|
||||||
|
'${ledgerDisplayName(ledger, l10n)} - ${ledgerBalance(ledger)}',
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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<Wallet> wallets,
|
||||||
|
required List<LedgerAccount> ledgerAccounts,
|
||||||
|
required SourceOptionKey? selectedValue,
|
||||||
|
required ValueChanged<SourceOptionKey> 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<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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<Wallet>? 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user
вот, говорил: вот эту штуку хардкодить не надо, лучше вынести куда-то в решающее правило, а не запаковывать внутрь провайдера, где найти ее непросто.