This commit is contained in:
Arseni
2026-03-11 18:26:21 +03:00
parent fdd8dd8845
commit 0172176978
46 changed files with 678 additions and 643 deletions

View File

@@ -7,7 +7,7 @@ import 'package:pshared/models/recipient/payment_method_draft.dart';
import 'package:pweb/pages/address_book/form/widgets/feilds/email.dart';
import 'package:pweb/pages/address_book/form/widgets/header.dart';
import 'package:pweb/pages/address_book/form/widgets/feilds/name.dart';
import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel.dart';
import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel/panel.dart';
import 'package:pweb/pages/address_book/form/widgets/payment_methods/selector_row.dart';
import 'package:pweb/pages/address_book/form/widgets/save_button.dart';

View File

@@ -1,115 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/payment_method_draft.dart';
import 'package:pweb/pages/payment_methods/form.dart';
import 'package:pweb/pages/payment_methods/icon.dart';
import 'package:pweb/widgets/dialogs/confirmation_dialog.dart';
import 'package:pweb/models/state/control_state.dart';
import 'package:pweb/models/state/visibility.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentMethodPanel extends StatelessWidget {
final PaymentType selectedType;
final int selectedIndex;
final List<RecipientMethodDraft> entries;
final ValueChanged<int> onRemove;
final void Function(int, PaymentMethodData) onChanged;
final ControlState editState;
final VisibilityState deleteVisibility;
final double padding;
const PaymentMethodPanel({
super.key,
required this.selectedType,
required this.selectedIndex,
required this.entries,
required this.onRemove,
required this.onChanged,
this.editState = ControlState.enabled,
this.deleteVisibility = VisibilityState.visible,
this.padding = 16,
});
Future<void> _confirmDelete(BuildContext context, VoidCallback onConfirmed) async {
final l10n = AppLocalizations.of(context)!;
final confirmed = await showConfirmationDialog(
context: context,
title: l10n.delete,
message: l10n.deletePaymentConfirmation,
confirmLabel: l10n.delete,
);
if (confirmed) {
onConfirmed();
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final l10n = AppLocalizations.of(context)!;
final label = l10n.paymentMethodDetails;
final entry = selectedIndex >= 0 && selectedIndex < entries.length
? entries[selectedIndex]
: null;
return AnimatedContainer(
duration: const Duration(milliseconds: 3000),
padding: EdgeInsets.all(padding),
decoration: BoxDecoration(
color: theme.colorScheme.onSecondary,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: theme.colorScheme.onSurface.withAlpha(30)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
iconForPaymentType(selectedType),
size: 18,
color: theme.colorScheme.primary,
),
const SizedBox(width: 8),
Expanded(
child: Text(
label,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
),
if (entry != null && deleteVisibility == VisibilityState.visible)
TextButton.icon(
onPressed: () => _confirmDelete(context, () => onRemove(selectedIndex)),
icon: Icon(Icons.delete, color: theme.colorScheme.error),
label: Text(
l10n.delete,
style: TextStyle(color: theme.colorScheme.error),
),
),
],
),
const SizedBox(height: 12),
if (entry != null)
PaymentMethodForm(
key: ValueKey('${selectedType.name}-${entry.existing?.id ?? selectedIndex}-form'),
selectedType: selectedType,
initialData: entry.data,
isEditable: editState == ControlState.enabled,
onChanged: (data) {
if (data == null) return;
onChanged(selectedIndex, data);
},
),
],
),
);
}
}

View File

@@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/payment_method_draft.dart';
import 'package:pweb/models/state/control_state.dart';
import 'package:pweb/models/state/visibility.dart';
import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel/panel_container.dart';
import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel/panel_entry_form.dart';
import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel/panel_header.dart';
import 'package:pweb/utils/payment/method_delete_confirmation.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentMethodPanel extends StatelessWidget {
final PaymentType selectedType;
final int selectedIndex;
final List<RecipientMethodDraft> entries;
final ValueChanged<int>? onRemove;
final void Function(int, PaymentMethodData)? onChanged;
final ControlState editState;
final VisibilityState deleteVisibility;
final double padding;
const PaymentMethodPanel({
super.key,
required this.selectedType,
required this.selectedIndex,
required this.entries,
this.onRemove,
this.onChanged,
this.editState = ControlState.enabled,
this.deleteVisibility = VisibilityState.visible,
this.padding = 16,
}) : assert(editState == ControlState.disabled || onChanged != null),
assert(deleteVisibility == VisibilityState.hidden || onRemove != null);
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final entry = selectedIndex >= 0 && selectedIndex < entries.length
? entries[selectedIndex]
: null;
final showDelete =
entry != null &&
deleteVisibility == VisibilityState.visible &&
onRemove != null;
return PaymentMethodPanelContainer(
padding: padding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PaymentMethodPanelHeader(
selectedType: selectedType,
title: l10n.paymentMethodDetails,
deleteLabel: l10n.delete,
showDelete: showDelete,
onDelete: showDelete
? () => confirmPaymentMethodDelete(
context,
() => onRemove!(selectedIndex),
)
: null,
),
const SizedBox(height: 12),
if (entry != null)
PaymentMethodPanelEntryForm(
selectedType: selectedType,
selectedIndex: selectedIndex,
entry: entry,
isEditable: editState == ControlState.enabled,
onChanged: onChanged == null
? null
: (data) => onChanged!(selectedIndex, data),
),
],
),
);
}
}

View File

@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
class PaymentMethodPanelContainer extends StatelessWidget {
final double padding;
final Widget child;
const PaymentMethodPanelContainer({
super.key,
required this.padding,
required this.child,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AnimatedContainer(
duration: const Duration(milliseconds: 3000),
padding: EdgeInsets.all(padding),
decoration: BoxDecoration(
color: theme.colorScheme.onSecondary,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: theme.colorScheme.onSurface.withAlpha(30)),
),
child: child,
);
}
}

View File

@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/payment_method_draft.dart';
import 'package:pweb/pages/payment_methods/form.dart';
class PaymentMethodPanelEntryForm extends StatelessWidget {
final PaymentType selectedType;
final int selectedIndex;
final RecipientMethodDraft entry;
final bool isEditable;
final ValueChanged<PaymentMethodData>? onChanged;
const PaymentMethodPanelEntryForm({
super.key,
required this.selectedType,
required this.selectedIndex,
required this.entry,
required this.isEditable,
this.onChanged,
});
@override
Widget build(BuildContext context) {
return PaymentMethodForm(
key: ValueKey(
'${selectedType.name}-${entry.existing?.id ?? selectedIndex}-form',
),
selectedType: selectedType,
initialData: entry.data,
isEditable: isEditable,
onChanged: (data) {
if (data == null) return;
onChanged?.call(data);
},
);
}
}

View File

@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pweb/pages/payment_methods/icon.dart';
class PaymentMethodPanelHeader extends StatelessWidget {
final PaymentType selectedType;
final String title;
final String deleteLabel;
final bool showDelete;
final VoidCallback? onDelete;
const PaymentMethodPanelHeader({
super.key,
required this.selectedType,
required this.title,
required this.deleteLabel,
required this.showDelete,
this.onDelete,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Row(
children: [
Icon(
iconForPaymentType(selectedType),
size: 18,
color: theme.colorScheme.primary,
),
const SizedBox(width: 8),
Expanded(
child: Text(
title,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
),
if (showDelete && onDelete != null)
TextButton.icon(
onPressed: onDelete,
icon: Icon(Icons.delete, color: theme.colorScheme.error),
label: Text(
deleteLabel,
style: TextStyle(color: theme.colorScheme.error),
),
),
],
);
}
}

View File

@@ -3,9 +3,12 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/controllers/balance_mask/wallets.dart';
import 'package:pshared/models/money.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pweb/utils/money_display.dart';
class BalanceAmount extends StatelessWidget {
final Wallet wallet;
@@ -25,6 +28,13 @@ class BalanceAmount extends StatelessWidget {
final textTheme = Theme.of(context).textTheme;
final colorScheme = Theme.of(context).colorScheme;
final currencyBalance = currencyCodeToSymbol(wallet.currency);
final formattedBalance = formatMoneyUi(
context,
Money(
amount: amountToString(wallet.balance),
currency: currencyCodeToString(wallet.currency),
),
);
final wallets = context.watch<WalletsController>();
final isMasked = wallets.isBalanceMasked(wallet.id);
@@ -32,9 +42,7 @@ class BalanceAmount extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Text(
isMasked
? '•••• $currencyBalance'
: '${amountToString(wallet.balance)} $currencyBalance',
isMasked ? '•••• $currencyBalance' : formattedBalance,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: textTheme.headlineMedium?.copyWith(

View File

@@ -6,8 +6,8 @@ import 'package:pshared/models/payment/wallet.dart';
import 'package:pweb/models/dashboard/balance_item.dart';
import 'package:pweb/pages/dashboard/buttons/balance/add/card.dart';
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
import 'package:pweb/pages/dashboard/buttons/balance/source/card.dart';
import 'package:pweb/pages/dashboard/buttons/balance/source/cards/ledger.dart';
import 'package:pweb/pages/dashboard/buttons/balance/source/cards/wallet.dart';
class BalanceCarouselCardItem extends StatelessWidget {
@@ -29,9 +29,9 @@ class BalanceCarouselCardItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final card = switch (item) {
WalletBalanceItem(:final wallet) => WalletCard(
WalletBalanceItem(:final wallet) => BalanceSourceCard.wallet(
wallet: wallet,
onTopUp: () => onTopUp(wallet),
onAddFunds: () => onTopUp(wallet),
onTap: () => onWalletTap(wallet),
),
LedgerBalanceItem(:final account) => LedgerAccountCard(

View File

@@ -23,7 +23,7 @@ class LedgerBalanceAmount extends StatelessWidget {
final isMasked = controller.isBalanceMasked(account.ledgerAccountRef);
final balance = isMasked
? LedgerBalanceFormatter.formatMasked(account)
: LedgerBalanceFormatter.format(account);
: LedgerBalanceFormatter.format(context, account);
return Row(
mainAxisSize: MainAxisSize.min,

View File

@@ -61,7 +61,7 @@ class BalanceSourceCard extends StatelessWidget {
? null
: wallet.network!.localizedName(context);
final symbol = wallet.tokenSymbol?.trim();
final copyState = _copyController.wallet(wallet.depositAddress);
final copyState = _copyController.wallet(context, wallet.depositAddress);
return BalanceSourceCardLayout(
title: wallet.name,
@@ -105,7 +105,7 @@ class BalanceSourceCard extends StatelessWidget {
final badge = account.currency.trim().isEmpty
? null
: account.currency.toUpperCase();
final copyState = _copyController.ledger(accountCode);
final copyState = _copyController.ledger(context, accountCode);
return BalanceSourceCardLayout(
title: title,

View File

@@ -1,28 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/wallet.dart';
import 'package:pweb/pages/dashboard/buttons/balance/source/card.dart';
class WalletCard extends StatelessWidget {
final Wallet wallet;
final VoidCallback onTopUp;
final VoidCallback onTap;
const WalletCard({
super.key,
required this.wallet,
required this.onTopUp,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return BalanceSourceCard.wallet(
wallet: wallet,
onTap: onTap,
onAddFunds: onTopUp,
);
}
}

View File

@@ -36,7 +36,9 @@ class PaymentAmountField extends StatelessWidget {
decoration: InputDecoration(
labelText: loc.amount,
border: const OutlineInputBorder(),
prefixText: symbol == null ? null : '$symbol\u00A0',
prefixText: symbol == null
? null
: withTrailingNonBreakingSpace(symbol),
),
onChanged: ui.handleChanged,
),

View File

@@ -1,37 +0,0 @@
import 'package:pshared/models/asset.dart';
import 'package:pshared/models/money.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/money.dart';
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
String moneyLabel(Money? money) {
if (money == null) return 'N/A';
final amount = parseMoneyAmount(money.amount, fallback: double.nan);
if (amount.isNaN) return '${money.amount} ${money.currency}';
try {
return assetToString(
Asset(currency: currencyStringToCode(money.currency), amount: amount),
);
} catch (_) {
return '${money.amount} ${money.currency}';
}
}
String sentAmountLabel(MultiplePayoutsController controller) {
final requested = controller.requestedSentAmount;
final sourceDebit = controller.aggregateDebitAmount;
if (requested == null && sourceDebit == null) return 'N/A';
if (sourceDebit != null) return moneyLabel(sourceDebit);
return moneyLabel(requested);
}
String feeLabel(MultiplePayoutsController controller, AppLocalizations l10n) {
final fee = controller.aggregateFeeAmount;
if (fee == null) return l10n.noFee;
return moneyLabel(fee);
}

View File

@@ -1,11 +1,15 @@
import 'package:flutter/material.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
import 'package:pweb/models/dashboard/summary_values.dart';
import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/helpers.dart';
import 'package:pweb/pages/dashboard/payouts/summary/widget.dart';
import 'package:pweb/utils/money_display.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class SourceQuoteSummary extends StatelessWidget {
const SourceQuoteSummary({
super.key,
@@ -18,12 +22,27 @@ class SourceQuoteSummary extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return PaymentSummary(
spacing: spacing,
values: PaymentSummaryValues(
fee: feeLabel(controller, AppLocalizations.of(context)!),
recipientReceives: moneyLabel(controller.aggregateSettlementAmount),
total: moneyLabel(controller.aggregateDebitAmount),
fee: controller.aggregateFeeAmount == null
? l10n.noFee
: formatMoneyUiWithL10n(
l10n,
controller.aggregateFeeAmount,
separator: nonBreakingSpace,
),
recipientReceives: formatMoneyUiWithL10n(
l10n,
controller.aggregateSettlementAmount,
separator: nonBreakingSpace,
),
total: formatMoneyUiWithL10n(
l10n,
controller.aggregateDebitAmount,
separator: nonBreakingSpace,
),
),
);
}

View File

@@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/asset.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pweb/utils/money_display.dart';
class PaymentSummaryRow extends StatelessWidget {
@@ -11,8 +12,8 @@ class PaymentSummaryRow extends StatelessWidget {
final TextStyle? style;
const PaymentSummaryRow({
super.key,
required this.labelFactory,
super.key,
required this.labelFactory,
required this.asset,
this.value,
this.style,
@@ -20,8 +21,7 @@ class PaymentSummaryRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final formatted = value ??
(asset == null ? 'N/A' : assetToString(asset!));
final formatted = value ?? formatAssetUi(context, asset);
return Text(labelFactory(formatted), style: style);
}
}

View File

@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/recipient/payment_method_draft.dart';
import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel/panel.dart';
import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel.dart';
import 'package:pweb/pages/payout_page/send/widgets/payment_info/header.dart';
import 'package:pweb/models/state/control_state.dart';
import 'package:pweb/models/state/visibility.dart';
@@ -40,8 +40,6 @@ class PaymentInfoManualDetailsSection extends StatelessWidget {
selectedType: data.type,
selectedIndex: 0,
entries: [entry],
onRemove: (_) {},
onChanged: (_, ignored) {},
editState: ControlState.disabled,
deleteVisibility: VisibilityState.hidden,
),

View File

@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/payment_method_draft.dart';
import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel/panel.dart';
import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel.dart';
import 'package:pweb/pages/address_book/form/widgets/payment_methods/selector_row.dart';
import 'package:pweb/pages/payout_page/send/widgets/payment_info/details_builder.dart';
import 'package:pweb/pages/payout_page/send/widgets/payment_info/header.dart';
@@ -78,8 +78,6 @@ class PaymentInfoMethodsSection extends StatelessWidget {
selectedType: state.selectedType,
selectedIndex: state.selectedIndex!,
entries: state.selectedEntries,
onRemove: (_) {},
onChanged: (_, _) {},
editState: ControlState.disabled,
deleteVisibility: VisibilityState.hidden,
),

View File

@@ -1,30 +1,16 @@
import 'package:flutter/widgets.dart';
import 'package:pshared/models/ledger/account.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/money.dart';
import 'package:pweb/utils/money_display.dart';
class LedgerBalanceFormatter {
const LedgerBalanceFormatter._();
static String format(LedgerAccount account) {
final money = account.balance?.balance;
if (money == null) return '--';
final amount = parseMoneyAmount(money.amount, fallback: double.nan);
if (amount.isNaN) {
return '${money.amount} ${money.currency}';
}
try {
final currency = currencyStringToCode(money.currency);
final symbol = currencyCodeToSymbol(currency);
if (symbol.trim().isEmpty) {
return '${amountToString(amount)} ${money.currency}';
}
return '${amountToString(amount)} $symbol';
} catch (_) {
return '${amountToString(amount)} ${money.currency}';
}
static String format(BuildContext context, LedgerAccount account) {
return formatMoneyUi(context, account.balance?.balance);
}
static String formatMasked(LedgerAccount account) {

View File

@@ -26,7 +26,7 @@ class LedgerSection extends StatelessWidget {
final hasAccountCode = accountCode.isNotEmpty;
final balance = isMasked
? LedgerBalanceFormatter.formatMasked(ledger)
: LedgerBalanceFormatter.format(ledger);
: LedgerBalanceFormatter.format(context, ledger);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,

View File

@@ -1,91 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pweb/models/wallet/wallet_transaction.dart';
import 'package:pweb/pages/payout_page/wallet/history/chip.dart';
import 'package:pweb/pages/report/table/badge.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class WalletTransactionsTable extends StatelessWidget {
final List<WalletTransaction> transactions;
const WalletTransactionsTable({super.key, required this.transactions});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final loc = AppLocalizations.of(context)!;
if (transactions.isEmpty) {
return Card(
color: theme.colorScheme.onSecondary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(loc.walletHistoryEmpty),
),
);
}
return Card(
color: theme.colorScheme.onSecondary,
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(12),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columnSpacing: 18,
headingTextStyle: const TextStyle(fontWeight: FontWeight.w600),
columns: [
DataColumn(label: Text(loc.colStatus)),
DataColumn(label: Text(loc.colType)),
DataColumn(label: Text(loc.colAmount)),
DataColumn(label: Text(loc.colBalance)),
DataColumn(label: Text(loc.colCounterparty)),
DataColumn(label: Text(loc.colDate)),
DataColumn(label: Text(loc.colComment)),
],
rows: List.generate(
transactions.length,
(index) {
final tx = transactions[index];
final color = WidgetStateProperty.resolveWith<Color?>(
(states) => index.isEven
? theme.colorScheme.surfaceContainerHighest
: null,
);
return DataRow.byIndex(
index: index,
color: color,
cells: [
DataCell(OperationStatusBadge(status: tx.status)),
DataCell(TypeChip(type: tx.type)),
DataCell(Text(
'${tx.type.sign}${amountToString(tx.amount)} ${currencyCodeToSymbol(tx.currency)}')),
DataCell(Text(
tx.balanceAfter == null
? '-'
: '${amountToString(tx.balanceAfter!)} ${currencyCodeToSymbol(tx.currency)}',
)),
DataCell(Text(tx.counterparty ?? '-')),
DataCell(Text(
'${TimeOfDay.fromDateTime(tx.date).format(context)}\n'
'${tx.date.toLocal().toIso8601String().split("T").first}',
)),
DataCell(Text(tx.description)),
],
);
},
),
),
),
),
);
}
}

View File

@@ -15,19 +15,23 @@ class OperationCard extends StatelessWidget {
final OperationItem operation;
final ValueChanged<OperationItem>? onTap;
const OperationCard({
super.key,
required this.operation,
this.onTap,
});
const OperationCard({super.key, required this.operation, this.onTap});
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
final theme = Theme.of(context);
final canOpen = onTap != null && paymentIdFromOperation(operation) != null;
final amountLabel = formatAmount(operation.amount, operation.currency);
final toAmountLabel = formatAmount(operation.toAmount, operation.toCurrency);
final amountLabel = formatAmount(
context,
operation.amount,
operation.currency,
);
final toAmountLabel = formatAmount(
context,
operation.toAmount,
operation.toCurrency,
);
final showToAmount = shouldShowToAmount(operation);
final timeLabel = formatOperationTime(context, operation.date);

View File

@@ -42,6 +42,7 @@ class PayoutTotalsList extends StatelessWidget {
const SizedBox(width: 8),
Text(
formatAmount(
context,
totals[index].amount,
totals[index].currency,
),

View File

@@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/money.dart';
import 'package:pshared/models/payment/payment.dart';
import 'package:pshared/models/payment/fx/quote.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pshared/utils/money.dart';
import 'package:pweb/pages/report/details/section.dart';
import 'package:pweb/pages/report/details/sections/rows.dart';
@@ -13,26 +15,17 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentFxSection extends StatelessWidget {
final Payment payment;
const PaymentFxSection({
super.key,
required this.payment,
});
const PaymentFxSection({super.key, required this.payment});
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
final fx = payment.lastQuote?.fxQuote;
final rows = buildDetailRows([
DetailValue(
label: loc.fxRateLabel,
value: _formatRate(fx),
),
DetailValue(label: loc.fxRateLabel, value: _formatRate(fx)),
]);
return DetailsSection(
title: loc.paymentDetailsFx,
children: rows,
);
return DetailsSection(title: loc.paymentDetailsFx, children: rows);
}
String? _formatRate(FxQuote? fx) {
@@ -40,24 +33,33 @@ class PaymentFxSection extends StatelessWidget {
final price = fx.price?.trim();
if (price == null || price.isEmpty) return null;
final base = _firstNonEmpty([
currencySymbolFromCode(fx.baseCurrency),
currencySymbolFromCode(fx.baseAmount?.currency),
final baseCurrency = _firstNonEmpty([
fx.baseCurrency,
fx.baseAmount?.currency,
currencySymbolFromCode(fx.baseCurrency),
currencySymbolFromCode(fx.baseAmount?.currency),
]);
final quote = _firstNonEmpty([
currencySymbolFromCode(fx.quoteCurrency),
currencySymbolFromCode(fx.quoteAmount?.currency),
final quoteCurrency = _firstNonEmpty([
fx.quoteCurrency,
fx.quoteAmount?.currency,
currencySymbolFromCode(fx.quoteCurrency),
currencySymbolFromCode(fx.quoteAmount?.currency),
]);
if (base == null || quote == null) {
return price;
}
if (baseCurrency == null || quoteCurrency == null) return price;
return '1 $base = $price $quote';
final baseDisplay = formatMoneyDisplay(
Money(amount: '1', currency: baseCurrency),
fallback: '1 $baseCurrency',
invalidAmountFallback: '1',
);
final quoteDisplay = formatMoneyDisplay(
Money(amount: _normalizeAmount(price), currency: quoteCurrency),
fallback: '$price $quoteCurrency',
invalidAmountFallback: price,
);
return '$baseDisplay = $quoteDisplay';
}
String? _firstNonEmpty(List<String?> values) {
@@ -67,4 +69,8 @@ class PaymentFxSection extends StatelessWidget {
}
return null;
}
String _normalizeAmount(String raw) {
return raw.replaceAll(RegExp(r'\s+'), '').replaceAll(',', '.');
}
}

View File

@@ -1,67 +0,0 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/payment/payment.dart';
import 'package:pweb/pages/report/details/row.dart';
import 'package:pweb/pages/report/details/section.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentMetadataSection extends StatelessWidget {
final Payment payment;
const PaymentMetadataSection({
super.key,
required this.payment,
});
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
final metadata = payment.metadata ?? const {};
const allowedKeys = {'upload_filename', 'upload_rows'};
final filtered = Map<String, String>.fromEntries(
metadata.entries.where((entry) => allowedKeys.contains(entry.key)),
);
if (filtered.isEmpty) {
return DetailsSection(
title: loc.paymentDetailsMetadata,
children: [
Text(
loc.metadataEmpty,
style: Theme.of(context).textTheme.bodyMedium,
),
],
);
}
final entries = filtered.entries.toList()
..sort((a, b) => a.key.compareTo(b.key));
return DetailsSection(
title: loc.paymentDetailsMetadata,
children: entries
.map(
(entry) => DetailRow(
label: _metadataLabel(loc, entry.key),
value: entry.value,
monospaced: true,
),
)
.toList(),
);
}
}
String _metadataLabel(AppLocalizations loc, String key) {
switch (key) {
case 'upload_filename':
return loc.metadataUploadFileName;
case 'upload_rows':
return loc.metadataTotalRecipients;
default:
return key;
}
}

View File

@@ -27,6 +27,7 @@ class OperationHistoryTile extends StatelessWidget {
final loc = AppLocalizations.of(context)!;
final theme = Theme.of(context);
final title = resolveOperationTitle(loc, operation.code);
final operationLabel = operation.label?.trim();
final stateView = resolveStepStateView(context, operation.state);
final completedAt = formatCompletedAt(context, operation.completedAt);
final canDownload = canDownloadDocument && onDownloadDocument != null;
@@ -49,13 +50,24 @@ class OperationHistoryTile extends StatelessWidget {
StepStateChip(view: stateView),
],
),
if (operationLabel != null &&
operationLabel.isNotEmpty &&
operationLabel != title) ...[
const SizedBox(height: 4),
Text(
operationLabel,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
const SizedBox(height: 6),
Text(
'${loc.completedAtLabel}: $completedAt',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
if (canDownload) ...[
const SizedBox(height: 8),
TextButton.icon(

View File

@@ -9,6 +9,7 @@ import 'package:pweb/pages/report/details/summary_card/info_line.dart';
import 'package:pweb/pages/report/table/badge.dart';
import 'package:pweb/utils/report/amount_parts.dart';
import 'package:pweb/utils/report/format.dart';
import 'package:pweb/utils/money_display.dart';
import 'package:pweb/utils/report/payment_mapper.dart';
import 'package:pweb/utils/clipboard.dart';
@@ -24,6 +25,7 @@ class PaymentSummaryCard extends StatelessWidget {
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
final theme = Theme.of(context);
final unavailableValue = unavailableMoneyValue(context);
final status = statusFromPayment(payment);
final dateLabel = formatDateLabel(context, resolvePaymentDate(payment));
@@ -33,14 +35,16 @@ class PaymentSummaryCard extends StatelessWidget {
final toAmount = payment.lastQuote?.amounts?.destinationSettlement;
final fee = quoteFeeTotal(payment.lastQuote);
final amountLabel = formatMoney(primaryAmount);
final toAmountLabel = formatMoney(toAmount);
final feeLabel = formatMoney(fee);
final amountLabel = formatMoney(context, primaryAmount);
final toAmountLabel = formatMoney(context, toAmount);
final feeLabel = formatMoney(context, fee);
final paymentRef = (payment.paymentRef ?? '').trim();
final showToAmount = toAmountLabel != '-';
final showToAmount = toAmountLabel != unavailableValue;
final showFee = payment.lastQuote != null;
final feeText = feeLabel != '-' ? loc.fee(feeLabel) : loc.fee(loc.noFee);
final feeText = feeLabel != unavailableValue
? loc.fee(feeLabel)
: loc.fee(loc.noFee);
final showPaymentId = paymentRef.isNotEmpty;
final amountParts = splitAmount(amountLabel);
@@ -73,12 +77,12 @@ class PaymentSummaryCard extends StatelessWidget {
currency: amountParts.currency,
),
const SizedBox(height: 6),
if (amountLabel != '-')
if (amountLabel != unavailableValue)
InfoLine(
icon: Icons.send_outlined,
text: loc.sentAmount(amountLabel),
),
if (showToAmount && toAmountLabel != '-')
if (showToAmount && toAmountLabel != unavailableValue)
InfoLine(
icon: Icons.south_east,
text: loc.recipientWillReceive(toAmountLabel),

View File

@@ -1,14 +1,17 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/money.dart';
import 'package:pshared/models/payment/operation.dart';
import 'package:pshared/models/payment/status.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pweb/pages/report/table/badge.dart';
import 'package:pweb/utils/money_display.dart';
import 'package:pweb/utils/report/download_act.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class OperationRow {
static DataRow build(OperationItem op, BuildContext context) {
final isUnknownDate = op.date.millisecondsSinceEpoch == 0;
@@ -35,13 +38,21 @@ class OperationRow {
label: Text(loc.downloadAct),
)
: Text(op.fileName ?? '');
final amountLabel = formatMoneyUiWithL10n(
loc,
Money(amount: amountToString(op.amount), currency: op.currency),
);
final toAmountLabel = formatMoneyUiWithL10n(
loc,
Money(amount: amountToString(op.toAmount), currency: op.toCurrency),
);
return DataRow(
cells: [
DataCell(OperationStatusBadge(status: op.status)),
DataCell(documentCell),
DataCell(Text('${amountToString(op.amount)} ${op.currency}')),
DataCell(Text('${amountToString(op.toAmount)} ${op.toCurrency}')),
DataCell(Text(amountLabel)),
DataCell(Text(toAmountLabel)),
DataCell(Text(op.payId)),
DataCell(Text(op.cardNumber ?? '-')),
DataCell(Text(op.name)),