SEND063
This commit is contained in:
@@ -2,12 +2,9 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/operation.dart';
|
||||
|
||||
|
||||
bool shouldShowToAmount(OperationItem operation) {
|
||||
if (operation.toCurrency.trim().isEmpty) return false;
|
||||
if (operation.currency.trim().isEmpty) return true;
|
||||
if (operation.currency != operation.toCurrency) return true;
|
||||
return (operation.toAmount - operation.amount).abs() > 0.0001;
|
||||
return true;
|
||||
}
|
||||
|
||||
String formatOperationTime(BuildContext context, DateTime date) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/execution_operation.dart';
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
|
||||
import 'package:pweb/pages/report/details/header.dart';
|
||||
@@ -13,12 +14,17 @@ class PaymentDetailsContent extends StatelessWidget {
|
||||
final Payment payment;
|
||||
final VoidCallback onBack;
|
||||
final VoidCallback? onDownloadAct;
|
||||
final bool Function(PaymentExecutionOperation operation)?
|
||||
canDownloadOperationDocument;
|
||||
final ValueChanged<PaymentExecutionOperation>? onDownloadOperationDocument;
|
||||
|
||||
const PaymentDetailsContent({
|
||||
super.key,
|
||||
required this.payment,
|
||||
required this.onBack,
|
||||
this.onDownloadAct,
|
||||
this.canDownloadOperationDocument,
|
||||
this.onDownloadOperationDocument,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -29,17 +35,15 @@ class PaymentDetailsContent extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
PaymentDetailsHeader(
|
||||
title: loc.paymentInfo,
|
||||
onBack: onBack,
|
||||
),
|
||||
PaymentDetailsHeader(title: loc.paymentInfo, onBack: onBack),
|
||||
const SizedBox(height: 16),
|
||||
PaymentSummaryCard(
|
||||
PaymentSummaryCard(payment: payment, onDownloadAct: onDownloadAct),
|
||||
const SizedBox(height: 16),
|
||||
PaymentDetailsSections(
|
||||
payment: payment,
|
||||
onDownloadAct: onDownloadAct,
|
||||
canDownloadOperationDocument: canDownloadOperationDocument,
|
||||
onDownloadOperationDocument: onDownloadOperationDocument,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
PaymentDetailsSections(payment: payment),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -19,17 +19,17 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
class PaymentDetailsPage extends StatelessWidget {
|
||||
final String paymentId;
|
||||
|
||||
const PaymentDetailsPage({
|
||||
super.key,
|
||||
required this.paymentId,
|
||||
});
|
||||
const PaymentDetailsPage({super.key, required this.paymentId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProxyProvider<PaymentsProvider, PaymentDetailsController>(
|
||||
return ChangeNotifierProxyProvider<
|
||||
PaymentsProvider,
|
||||
PaymentDetailsController
|
||||
>(
|
||||
create: (_) => PaymentDetailsController(paymentId: paymentId),
|
||||
update: (_, payments, controller) => controller!
|
||||
..update(payments, paymentId),
|
||||
update: (_, payments, controller) =>
|
||||
controller!..update(payments, paymentId),
|
||||
child: const _PaymentDetailsView(),
|
||||
);
|
||||
}
|
||||
@@ -67,6 +67,17 @@ class _PaymentDetailsView extends StatelessWidget {
|
||||
onDownloadAct: controller.canDownload
|
||||
? () => downloadPaymentAct(context, payment.paymentRef ?? '')
|
||||
: null,
|
||||
canDownloadOperationDocument:
|
||||
controller.canDownloadOperationDocument,
|
||||
onDownloadOperationDocument: (operation) {
|
||||
final request = controller.operationDocumentRequest(operation);
|
||||
if (request == null) return;
|
||||
downloadPaymentAct(
|
||||
context,
|
||||
request.paymentRef,
|
||||
operationRef: request.operationRef,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,36 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/execution_operation.dart';
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
|
||||
import 'package:pweb/pages/report/details/sections/fx.dart';
|
||||
import 'package:pweb/pages/report/details/sections/metadata.dart';
|
||||
import 'package:pweb/pages/report/details/sections/operations/section.dart';
|
||||
|
||||
|
||||
class PaymentDetailsSections extends StatelessWidget {
|
||||
final Payment payment;
|
||||
final bool Function(PaymentExecutionOperation operation)?
|
||||
canDownloadOperationDocument;
|
||||
final ValueChanged<PaymentExecutionOperation>? onDownloadOperationDocument;
|
||||
|
||||
const PaymentDetailsSections({
|
||||
super.key,
|
||||
required this.payment,
|
||||
this.canDownloadOperationDocument,
|
||||
this.onDownloadOperationDocument,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasFx = _hasFxQuote(payment);
|
||||
if (!hasFx) {
|
||||
return PaymentMetadataSection(payment: payment);
|
||||
}
|
||||
final hasOperations = payment.operations.isNotEmpty;
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(child: PaymentFxSection(payment: payment)),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(child: PaymentMetadataSection(payment: payment)),
|
||||
if (hasFx) ...[
|
||||
PaymentFxSection(payment: payment),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
if (hasOperations) ...[
|
||||
PaymentOperationsSection(
|
||||
payment: payment,
|
||||
canDownloadDocument: canDownloadOperationDocument,
|
||||
onDownloadDocument: onDownloadOperationDocument,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
bool _hasFxQuote(Payment payment) => payment.lastQuote?.fxQuote != null;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/execution_operation.dart';
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
|
||||
import 'package:pweb/pages/report/details/section.dart';
|
||||
import 'package:pweb/pages/report/details/sections/operations/tile.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class PaymentOperationsSection extends StatelessWidget {
|
||||
final Payment payment;
|
||||
final bool Function(PaymentExecutionOperation operation)? canDownloadDocument;
|
||||
final ValueChanged<PaymentExecutionOperation>? onDownloadDocument;
|
||||
|
||||
const PaymentOperationsSection({
|
||||
super.key,
|
||||
required this.payment,
|
||||
this.canDownloadDocument,
|
||||
this.onDownloadDocument,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final operations = payment.operations;
|
||||
if (operations.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final children = <Widget>[];
|
||||
for (var i = 0; i < operations.length; i++) {
|
||||
final operation = operations[i];
|
||||
final canDownload = canDownloadDocument?.call(operation) ?? false;
|
||||
children.add(
|
||||
OperationHistoryTile(
|
||||
operation: operation,
|
||||
canDownloadDocument: canDownload,
|
||||
onDownloadDocument: canDownload && onDownloadDocument != null
|
||||
? () => onDownloadDocument!(operation)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
if (i < operations.length - 1) {
|
||||
children.addAll([
|
||||
const SizedBox(height: 8),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: Theme.of(context).dividerColor.withAlpha(20),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return DetailsSection(title: loc.operationfryTitle, children: children);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/utils/payment/status_view.dart';
|
||||
|
||||
|
||||
class StepStateChip extends StatelessWidget {
|
||||
final StatusView view;
|
||||
|
||||
const StepStateChip({super.key, required this.view});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: view.backgroundColor,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
view.label.toUpperCase(),
|
||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||
color: view.foregroundColor,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/execution_operation.dart';
|
||||
|
||||
import 'package:pweb/utils/report/operations/state_mapper.dart';
|
||||
import 'package:pweb/pages/report/details/sections/operations/state_chip.dart';
|
||||
import 'package:pweb/utils/report/operations/time_format.dart';
|
||||
import 'package:pweb/utils/report/operations/title_mapper.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class OperationHistoryTile extends StatelessWidget {
|
||||
final PaymentExecutionOperation operation;
|
||||
final bool canDownloadDocument;
|
||||
final VoidCallback? onDownloadDocument;
|
||||
|
||||
const OperationHistoryTile({
|
||||
super.key,
|
||||
required this.operation,
|
||||
required this.canDownloadDocument,
|
||||
this.onDownloadDocument,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final theme = Theme.of(context);
|
||||
final title = resolveOperationTitle(loc, operation.code);
|
||||
final stateView = resolveStepStateView(context, operation.state);
|
||||
final completedAt = formatCompletedAt(context, operation.completedAt);
|
||||
final canDownload = canDownloadDocument && onDownloadDocument != null;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
StepStateChip(view: stateView),
|
||||
],
|
||||
),
|
||||
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(
|
||||
onPressed: onDownloadDocument,
|
||||
icon: const Icon(Icons.download),
|
||||
label: Text(loc.downloadAct),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,9 @@ class PaymentSummaryCard extends StatelessWidget {
|
||||
final feeLabel = formatMoney(fee);
|
||||
final paymentRef = (payment.paymentRef ?? '').trim();
|
||||
|
||||
final showToAmount = toAmountLabel != '-' && toAmountLabel != amountLabel;
|
||||
final showToAmount = toAmountLabel != '-';
|
||||
final showFee = payment.lastQuote != null;
|
||||
final feeText = feeLabel != '-' ? loc.fee(feeLabel) : loc.fee(loc.noFee);
|
||||
final showPaymentId = paymentRef.isNotEmpty;
|
||||
final amountParts = splitAmount(amountLabel);
|
||||
|
||||
@@ -85,10 +87,10 @@ class PaymentSummaryCard extends StatelessWidget {
|
||||
icon: Icons.south_east,
|
||||
text: loc.recipientWillReceive(toAmountLabel),
|
||||
),
|
||||
if (feeLabel != '-')
|
||||
if (showFee)
|
||||
InfoLine(
|
||||
icon: Icons.receipt_long_outlined,
|
||||
text: loc.fee(feeLabel),
|
||||
text: feeText,
|
||||
muted: true,
|
||||
),
|
||||
if (onDownloadAct != null) ...[
|
||||
|
||||
@@ -14,37 +14,34 @@ class OperationStatusBadge extends StatelessWidget {
|
||||
|
||||
const OperationStatusBadge({super.key, required this.status});
|
||||
|
||||
Color _badgeColor(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return operationStatusView(l10n, status).color;
|
||||
}
|
||||
|
||||
Color _textColor(Color background) {
|
||||
// computeLuminance returns 0 for black, 1 for white
|
||||
return background.computeLuminance() > 0.5 ? Colors.black : Colors.white;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final label = status.localized(context);
|
||||
final bg = _badgeColor(context);
|
||||
final fg = _textColor(bg);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final view = operationStatusView(
|
||||
l10n,
|
||||
Theme.of(context).colorScheme,
|
||||
status,
|
||||
);
|
||||
final label = view.label;
|
||||
final bg = view.backgroundColor;
|
||||
final fg = view.foregroundColor;
|
||||
|
||||
return badges.Badge(
|
||||
badgeStyle: badges.BadgeStyle(
|
||||
shape: badges.BadgeShape.square,
|
||||
badgeColor: bg,
|
||||
borderRadius: BorderRadius.circular(12), // fully rounded
|
||||
borderRadius: BorderRadius.circular(12), // fully rounded
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6, vertical: 2 // tighter padding
|
||||
horizontal: 6,
|
||||
vertical: 2, // tighter padding
|
||||
),
|
||||
),
|
||||
badgeContent: Text(
|
||||
label.toUpperCase(), // or keep sentence case
|
||||
label.toUpperCase(), // or keep sentence case
|
||||
style: TextStyle(
|
||||
color: fg,
|
||||
fontSize: 11, // smaller text
|
||||
fontWeight: FontWeight.w500, // medium weight
|
||||
fontSize: 11, // smaller text
|
||||
fontWeight: FontWeight.w500, // medium weight
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -31,9 +31,7 @@ class OperationFilters extends StatelessWidget {
|
||||
: '${dateToLocalFormat(context, selectedRange!.start)} – ${dateToLocalFormat(context, selectedRange!.end)}';
|
||||
|
||||
return Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -61,12 +59,12 @@ class OperationFilters extends StatelessWidget {
|
||||
OutlinedButton.icon(
|
||||
onPressed: onPickRange,
|
||||
icon: const Icon(Icons.date_range_outlined, size: 18),
|
||||
label: Text(
|
||||
periodLabel,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
label: Text(periodLabel, overflow: TextOverflow.ellipsis),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 10,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
@@ -76,11 +74,7 @@ class OperationFilters extends StatelessWidget {
|
||||
Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 8,
|
||||
children: const [
|
||||
OperationStatus.success,
|
||||
OperationStatus.processing,
|
||||
OperationStatus.error,
|
||||
].map((status) {
|
||||
children: OperationStatus.values.map((status) {
|
||||
final label = status.localized(context);
|
||||
final isSelected = selectedStatuses.contains(status);
|
||||
return FilterChip(
|
||||
|
||||
Reference in New Issue
Block a user