This commit is contained in:
Arseni
2026-03-04 17:43:18 +03:00
parent 80b25a8608
commit aff804ec58
46 changed files with 1090 additions and 345 deletions

View File

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

View File

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

View File

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

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

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

View File

@@ -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),
),
],
],
);
}
}

View File

@@ -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) ...[

View File

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

View File

@@ -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(