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

@@ -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)),