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

@@ -0,0 +1,23 @@
class OperationCodePair {
final String operation;
final String action;
const OperationCodePair({required this.operation, required this.action});
}
OperationCodePair? parseOperationCodePair(String? code) {
final normalized = code?.trim().toLowerCase();
if (normalized == null || normalized.isEmpty) return null;
final parts = normalized.split('.').where((part) => part.isNotEmpty).toList();
if (parts.length >= 4 && (parts.first == 'hop' || parts.first == 'edge')) {
return OperationCodePair(operation: parts[2], action: parts[3]);
}
if (parts.length >= 2) {
return OperationCodePair(
operation: parts[parts.length - 2],
action: parts.last,
);
}
return null;
}

View File

@@ -7,50 +7,150 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
class StatusView {
final String label;
final Color color;
final Color backgroundColor;
final Color foregroundColor;
const StatusView(this.label, this.color);
}
StatusView({
required this.label,
required this.backgroundColor,
Color? foregroundColor,
}) : foregroundColor =
foregroundColor ??
(backgroundColor.computeLuminance() > 0.5
? Colors.black
: Colors.white);
StatusView statusView(AppLocalizations l10n, String? raw) {
final trimmed = (raw ?? '').trim();
final upper = trimmed.toUpperCase();
final normalized = upper.startsWith('PAYMENT_STATE_')
? upper.substring('PAYMENT_STATE_'.length)
: upper;
switch (normalized) {
case 'SETTLED':
return StatusView(l10n.paymentStatusPending, Colors.orange);
case 'SUCCESS':
return StatusView(l10n.paymentStatusSuccessful, Colors.green);
case 'FUNDS_RESERVED':
return StatusView(l10n.paymentStatusReserved, Colors.blue);
case 'ACCEPTED':
return StatusView(l10n.paymentStatusProcessing, Colors.orange);
case 'SUBMITTED':
return StatusView(l10n.paymentStatusProcessing, Colors.blue);
case 'FAILED':
return StatusView(l10n.paymentStatusFailed, Colors.red);
case 'CANCELLED':
return StatusView(l10n.paymentStatusCancelled, Colors.grey);
case 'UNSPECIFIED':
case '':
default:
return StatusView(l10n.paymentStatusPending, Colors.grey);
}
Color get color => backgroundColor;
}
StatusView operationStatusView(
AppLocalizations l10n,
ColorScheme scheme,
OperationStatus status,
) {
switch (status) {
case OperationStatus.success:
return statusView(l10n, 'SUCCESS');
case OperationStatus.error:
return statusView(l10n, 'FAILED');
case OperationStatus.processing:
return statusView(l10n, 'ACCEPTED');
return operationStatusViewFromToken(
l10n,
scheme,
operationStatusTokenFromEnum(status),
);
}
StatusView operationStatusViewFromToken(
AppLocalizations l10n,
ColorScheme scheme,
String? rawState, {
String? fallbackLabel,
}) {
final token = normalizeOperationStatusToken(rawState);
switch (token) {
case 'success':
case 'succeeded':
case 'completed':
case 'confirmed':
case 'settled':
return StatusView(
label: l10n.operationStatusSuccessful,
backgroundColor: scheme.tertiaryContainer,
foregroundColor: scheme.onTertiaryContainer,
);
case 'skipped':
return StatusView(
label: l10n.operationStepStateSkipped,
backgroundColor: scheme.secondaryContainer,
foregroundColor: scheme.onSecondaryContainer,
);
case 'error':
case 'failed':
case 'rejected':
case 'aborted':
return StatusView(
label: l10n.operationStatusUnsuccessful,
backgroundColor: scheme.errorContainer,
foregroundColor: scheme.onErrorContainer,
);
case 'cancelled':
case 'canceled':
return StatusView(
label: l10n.paymentStatusCancelled,
backgroundColor: scheme.surfaceContainerHighest,
foregroundColor: scheme.onSurfaceVariant,
);
case 'processing':
case 'running':
case 'executing':
case 'in_progress':
case 'started':
return StatusView(
label: l10n.paymentStatusProcessing,
backgroundColor: scheme.primaryContainer,
foregroundColor: scheme.onPrimaryContainer,
);
case 'pending':
case 'queued':
case 'waiting':
case 'created':
case 'scheduled':
return StatusView(
label: l10n.operationStatusPending,
backgroundColor: scheme.secondary,
foregroundColor: scheme.onSecondary,
);
case 'needs_attention':
return StatusView(
label: l10n.operationStepStateNeedsAttention,
backgroundColor: scheme.tertiary,
foregroundColor: scheme.onTertiary,
);
case 'retrying':
return StatusView(
label: l10n.operationStepStateRetrying,
backgroundColor: scheme.primary,
foregroundColor: scheme.onPrimary,
);
default:
return StatusView(
label: fallbackLabel ?? humanizeOperationStatusToken(token),
backgroundColor: scheme.surfaceContainerHighest,
foregroundColor: scheme.onSurfaceVariant,
);
}
}
String operationStatusTokenFromEnum(OperationStatus status) {
switch (status) {
case OperationStatus.pending:
return 'pending';
case OperationStatus.processing:
return 'processing';
case OperationStatus.retrying:
return 'retrying';
case OperationStatus.success:
return 'success';
case OperationStatus.skipped:
return 'skipped';
case OperationStatus.cancelled:
return 'cancelled';
case OperationStatus.needsAttention:
return 'needs_attention';
case OperationStatus.error:
return 'error';
}
}
String normalizeOperationStatusToken(String? state) {
final normalized = (state ?? '').trim().toLowerCase();
if (normalized.isEmpty) return 'pending';
return normalized
.replaceFirst(RegExp(r'^step_execution_state_'), '')
.replaceFirst(RegExp(r'^orchestration_state_'), '');
}
String humanizeOperationStatusToken(String token) {
final parts = token.split('_').where((part) => part.isNotEmpty).toList();
if (parts.isEmpty) return token;
return parts
.map(
(part) => '${part[0].toUpperCase()}${part.substring(1).toLowerCase()}',
)
.join(' ');
}

View File

@@ -11,7 +11,11 @@ import 'package:pweb/utils/error/snackbar.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
Future<void> downloadPaymentAct(BuildContext context, String paymentRef) async {
Future<void> downloadPaymentAct(
BuildContext context,
String paymentRef, {
String? operationRef,
}) async {
final organizations = context.read<OrganizationsProvider>();
if (!organizations.isOrganizationSet) {
return;
@@ -28,6 +32,7 @@ Future<void> downloadPaymentAct(BuildContext context, String paymentRef) async {
final file = await PaymentDocumentsService.getAct(
organizations.current.id,
trimmed,
operationRef: operationRef,
);
await downloadFile(file);
},

View File

@@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
import 'package:pweb/utils/payment/status_view.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
StatusView resolveStepStateView(BuildContext context, String? rawState) {
final loc = AppLocalizations.of(context)!;
final scheme = Theme.of(context).colorScheme;
return operationStatusViewFromToken(loc, scheme, rawState);
}

View File

@@ -0,0 +1,15 @@
import 'package:flutter/widgets.dart';
import 'package:pweb/utils/report/format.dart';
String formatCompletedAt(BuildContext context, DateTime? completedAt) {
final value = meaningfulDate(completedAt);
return formatDateLabel(context, value);
}
DateTime? meaningfulDate(DateTime? value) {
if (value == null) return null;
if (value.year <= 1) return null;
return value;
}

View File

@@ -0,0 +1,59 @@
import 'package:pweb/utils/payment/operation_code.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
String resolveOperationTitle(AppLocalizations loc, String? code) {
final pair = parseOperationCodePair(code);
if (pair == null) return '-';
final operation = _localizedOperation(loc, pair.operation);
final action = _localizedAction(loc, pair.action);
return loc.paymentOperationPair(operation, action);
}
String _localizedOperation(AppLocalizations loc, String operation) {
switch (operation) {
case 'card_payout':
return loc.paymentOperationCardPayout;
case 'crypto':
return loc.paymentOperationCrypto;
case 'settlement':
return loc.paymentOperationSettlement;
case 'ledger':
return loc.paymentOperationLedger;
default:
return _humanizeToken(operation);
}
}
String _localizedAction(AppLocalizations loc, String action) {
switch (action) {
case 'send':
return loc.paymentOperationActionSend;
case 'observe':
return loc.paymentOperationActionObserve;
case 'fx_convert':
return loc.paymentOperationActionFxConvert;
case 'credit':
return loc.paymentOperationActionCredit;
case 'block':
return loc.paymentOperationActionBlock;
case 'debit':
return loc.paymentOperationActionDebit;
case 'release':
return loc.paymentOperationActionRelease;
case 'move':
return loc.paymentOperationActionMove;
default:
return _humanizeToken(action);
}
}
String _humanizeToken(String token) {
final parts = token.split('_').where((part) => part.isNotEmpty).toList();
if (parts.isEmpty) return token;
return parts
.map((part) => '${part[0].toUpperCase()}${part.substring(1)}')
.join(' ');
}

View File

@@ -56,11 +56,14 @@ OperationStatus statusFromPayment(Payment payment) {
return OperationStatus.error;
case PaymentOrchestrationState.settled:
return OperationStatus.success;
case PaymentOrchestrationState.created:
case PaymentOrchestrationState.executing:
case PaymentOrchestrationState.needsAttention:
case PaymentOrchestrationState.unspecified:
return OperationStatus.needsAttention;
case PaymentOrchestrationState.created:
return OperationStatus.pending;
case PaymentOrchestrationState.executing:
return OperationStatus.processing;
case PaymentOrchestrationState.unspecified:
return OperationStatus.pending;
}
}