SEND063
This commit is contained in:
56
frontend/pshared/lib/data/dto/payment/operation.dart
Normal file
56
frontend/pshared/lib/data/dto/payment/operation.dart
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
class PaymentOperationDTO {
|
||||||
|
final String? stepRef;
|
||||||
|
final String? operationRef;
|
||||||
|
final String? code;
|
||||||
|
final String? state;
|
||||||
|
final String? label;
|
||||||
|
final String? failureCode;
|
||||||
|
final String? failureReason;
|
||||||
|
final String? startedAt;
|
||||||
|
final String? completedAt;
|
||||||
|
|
||||||
|
const PaymentOperationDTO({
|
||||||
|
this.stepRef,
|
||||||
|
this.operationRef,
|
||||||
|
this.code,
|
||||||
|
this.state,
|
||||||
|
this.label,
|
||||||
|
this.failureCode,
|
||||||
|
this.failureReason,
|
||||||
|
this.startedAt,
|
||||||
|
this.completedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory PaymentOperationDTO.fromJson(Map<String, dynamic> json) =>
|
||||||
|
PaymentOperationDTO(
|
||||||
|
stepRef: _asString(json['stepRef'] ?? json['step_ref']),
|
||||||
|
operationRef: _asString(json['operationRef'] ?? json['operation_ref']),
|
||||||
|
code: _asString(json['code']),
|
||||||
|
state: _asString(json['state']),
|
||||||
|
label: _asString(json['label']),
|
||||||
|
failureCode: _asString(json['failureCode'] ?? json['failure_code']),
|
||||||
|
failureReason: _asString(
|
||||||
|
json['failureReason'] ?? json['failure_reason'],
|
||||||
|
),
|
||||||
|
startedAt: _asString(json['startedAt'] ?? json['started_at']),
|
||||||
|
completedAt: _asString(json['completedAt'] ?? json['completed_at']),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => <String, dynamic>{
|
||||||
|
'stepRef': stepRef,
|
||||||
|
'operationRef': operationRef,
|
||||||
|
'code': code,
|
||||||
|
'state': state,
|
||||||
|
'label': label,
|
||||||
|
'failureCode': failureCode,
|
||||||
|
'failureReason': failureReason,
|
||||||
|
'startedAt': startedAt,
|
||||||
|
'completedAt': completedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _asString(Object? value) {
|
||||||
|
final text = value?.toString().trim();
|
||||||
|
if (text == null || text.isEmpty) return null;
|
||||||
|
return text;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/data/dto/payment/operation.dart';
|
||||||
import 'package:pshared/data/dto/payment/payment_quote.dart';
|
import 'package:pshared/data/dto/payment/payment_quote.dart';
|
||||||
|
|
||||||
part 'payment.g.dart';
|
part 'payment.g.dart';
|
||||||
@@ -12,6 +13,8 @@ class PaymentDTO {
|
|||||||
final String? state;
|
final String? state;
|
||||||
final String? failureCode;
|
final String? failureCode;
|
||||||
final String? failureReason;
|
final String? failureReason;
|
||||||
|
@JsonKey(defaultValue: <PaymentOperationDTO>[])
|
||||||
|
final List<PaymentOperationDTO> operations;
|
||||||
final PaymentQuoteDTO? lastQuote;
|
final PaymentQuoteDTO? lastQuote;
|
||||||
final Map<String, String>? metadata;
|
final Map<String, String>? metadata;
|
||||||
final String? createdAt;
|
final String? createdAt;
|
||||||
@@ -22,6 +25,7 @@ class PaymentDTO {
|
|||||||
this.state,
|
this.state,
|
||||||
this.failureCode,
|
this.failureCode,
|
||||||
this.failureReason,
|
this.failureReason,
|
||||||
|
this.operations = const <PaymentOperationDTO>[],
|
||||||
this.lastQuote,
|
this.lastQuote,
|
||||||
this.metadata,
|
this.metadata,
|
||||||
this.createdAt,
|
this.createdAt,
|
||||||
|
|||||||
37
frontend/pshared/lib/data/mapper/payment/operation.dart
Normal file
37
frontend/pshared/lib/data/mapper/payment/operation.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:pshared/data/dto/payment/operation.dart';
|
||||||
|
import 'package:pshared/models/payment/execution_operation.dart';
|
||||||
|
|
||||||
|
|
||||||
|
extension PaymentOperationDTOMapper on PaymentOperationDTO {
|
||||||
|
PaymentExecutionOperation toDomain() => PaymentExecutionOperation(
|
||||||
|
stepRef: stepRef,
|
||||||
|
operationRef: operationRef,
|
||||||
|
code: code,
|
||||||
|
state: state,
|
||||||
|
label: label,
|
||||||
|
failureCode: failureCode,
|
||||||
|
failureReason: failureReason,
|
||||||
|
startedAt: _parseDateTime(startedAt),
|
||||||
|
completedAt: _parseDateTime(completedAt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PaymentExecutionOperationMapper on PaymentExecutionOperation {
|
||||||
|
PaymentOperationDTO toDTO() => PaymentOperationDTO(
|
||||||
|
stepRef: stepRef,
|
||||||
|
operationRef: operationRef,
|
||||||
|
code: code,
|
||||||
|
state: state,
|
||||||
|
label: label,
|
||||||
|
failureCode: failureCode,
|
||||||
|
failureReason: failureReason,
|
||||||
|
startedAt: startedAt?.toUtc().toIso8601String(),
|
||||||
|
completedAt: completedAt?.toUtc().toIso8601String(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime? _parseDateTime(String? value) {
|
||||||
|
final normalized = value?.trim();
|
||||||
|
if (normalized == null || normalized.isEmpty) return null;
|
||||||
|
return DateTime.tryParse(normalized);
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import 'package:pshared/data/dto/payment/payment.dart';
|
import 'package:pshared/data/dto/payment/payment.dart';
|
||||||
|
import 'package:pshared/data/mapper/payment/operation.dart';
|
||||||
import 'package:pshared/data/mapper/payment/quote.dart';
|
import 'package:pshared/data/mapper/payment/quote.dart';
|
||||||
import 'package:pshared/models/payment/payment.dart';
|
import 'package:pshared/models/payment/payment.dart';
|
||||||
import 'package:pshared/models/payment/state.dart';
|
import 'package:pshared/models/payment/state.dart';
|
||||||
|
|
||||||
|
|
||||||
extension PaymentDTOMapper on PaymentDTO {
|
extension PaymentDTOMapper on PaymentDTO {
|
||||||
Payment toDomain() => Payment(
|
Payment toDomain() => Payment(
|
||||||
paymentRef: paymentRef,
|
paymentRef: paymentRef,
|
||||||
@@ -11,6 +13,7 @@ extension PaymentDTOMapper on PaymentDTO {
|
|||||||
orchestrationState: paymentOrchestrationStateFromValue(state),
|
orchestrationState: paymentOrchestrationStateFromValue(state),
|
||||||
failureCode: failureCode,
|
failureCode: failureCode,
|
||||||
failureReason: failureReason,
|
failureReason: failureReason,
|
||||||
|
operations: operations.map((item) => item.toDomain()).toList(),
|
||||||
lastQuote: lastQuote?.toDomain(),
|
lastQuote: lastQuote?.toDomain(),
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
createdAt: createdAt == null ? null : DateTime.tryParse(createdAt!),
|
createdAt: createdAt == null ? null : DateTime.tryParse(createdAt!),
|
||||||
@@ -24,6 +27,7 @@ extension PaymentMapper on Payment {
|
|||||||
state: state ?? paymentOrchestrationStateToValue(orchestrationState),
|
state: state ?? paymentOrchestrationStateToValue(orchestrationState),
|
||||||
failureCode: failureCode,
|
failureCode: failureCode,
|
||||||
failureReason: failureReason,
|
failureReason: failureReason,
|
||||||
|
operations: operations.map((item) => item.toDTO()).toList(),
|
||||||
lastQuote: lastQuote?.toDTO(),
|
lastQuote: lastQuote?.toDTO(),
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
createdAt: createdAt?.toUtc().toIso8601String(),
|
createdAt: createdAt?.toUtc().toIso8601String(),
|
||||||
|
|||||||
@@ -10,11 +10,31 @@
|
|||||||
"@operationStatusProcessing": {
|
"@operationStatusProcessing": {
|
||||||
"description": "Label for the “processing” operation status"
|
"description": "Label for the “processing” operation status"
|
||||||
},
|
},
|
||||||
|
"operationStatusPending": "Pending",
|
||||||
|
"@operationStatusPending": {
|
||||||
|
"description": "Label for the “pending” operation status"
|
||||||
|
},
|
||||||
|
"operationStatusRetrying": "Retrying",
|
||||||
|
"@operationStatusRetrying": {
|
||||||
|
"description": "Label for the “retrying” operation status"
|
||||||
|
},
|
||||||
|
|
||||||
"operationStatusSuccess": "Success",
|
"operationStatusSuccess": "Success",
|
||||||
"@operationStatusSuccess": {
|
"@operationStatusSuccess": {
|
||||||
"description": "Label for the “success” operation status"
|
"description": "Label for the “success” operation status"
|
||||||
},
|
},
|
||||||
|
"operationStatusSkipped": "Skipped",
|
||||||
|
"@operationStatusSkipped": {
|
||||||
|
"description": "Label for the “skipped” operation status"
|
||||||
|
},
|
||||||
|
"operationStatusCancelled": "Cancelled",
|
||||||
|
"@operationStatusCancelled": {
|
||||||
|
"description": "Label for the “cancelled” operation status"
|
||||||
|
},
|
||||||
|
"operationStatusNeedsAttention": "Needs attention",
|
||||||
|
"@operationStatusNeedsAttention": {
|
||||||
|
"description": "Label for the “needs attention” operation status"
|
||||||
|
},
|
||||||
|
|
||||||
"operationStatusError": "Error",
|
"operationStatusError": "Error",
|
||||||
"@operationStatusError": {
|
"@operationStatusError": {
|
||||||
|
|||||||
@@ -10,11 +10,31 @@
|
|||||||
"@operationStatusProcessing": {
|
"@operationStatusProcessing": {
|
||||||
"description": "Label for the “processing” operation status"
|
"description": "Label for the “processing” operation status"
|
||||||
},
|
},
|
||||||
|
"operationStatusPending": "В ожидании",
|
||||||
|
"@operationStatusPending": {
|
||||||
|
"description": "Label for the “pending” operation status"
|
||||||
|
},
|
||||||
|
"operationStatusRetrying": "Повтор",
|
||||||
|
"@operationStatusRetrying": {
|
||||||
|
"description": "Label for the “retrying” operation status"
|
||||||
|
},
|
||||||
|
|
||||||
"operationStatusSuccess": "Успех",
|
"operationStatusSuccess": "Успех",
|
||||||
"@operationStatusSuccess": {
|
"@operationStatusSuccess": {
|
||||||
"description": "Label for the “success” operation status"
|
"description": "Label for the “success” operation status"
|
||||||
},
|
},
|
||||||
|
"operationStatusSkipped": "Пропущен",
|
||||||
|
"@operationStatusSkipped": {
|
||||||
|
"description": "Label for the “skipped” operation status"
|
||||||
|
},
|
||||||
|
"operationStatusCancelled": "Отменен",
|
||||||
|
"@operationStatusCancelled": {
|
||||||
|
"description": "Label for the “cancelled” operation status"
|
||||||
|
},
|
||||||
|
"operationStatusNeedsAttention": "Требует внимания",
|
||||||
|
"@operationStatusNeedsAttention": {
|
||||||
|
"description": "Label for the “needs attention” operation status"
|
||||||
|
},
|
||||||
|
|
||||||
"operationStatusError": "Ошибка",
|
"operationStatusError": "Ошибка",
|
||||||
"@operationStatusError": {
|
"@operationStatusError": {
|
||||||
|
|||||||
23
frontend/pshared/lib/models/payment/execution_operation.dart
Normal file
23
frontend/pshared/lib/models/payment/execution_operation.dart
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
class PaymentExecutionOperation {
|
||||||
|
final String? stepRef;
|
||||||
|
final String? operationRef;
|
||||||
|
final String? code;
|
||||||
|
final String? state;
|
||||||
|
final String? label;
|
||||||
|
final String? failureCode;
|
||||||
|
final String? failureReason;
|
||||||
|
final DateTime? startedAt;
|
||||||
|
final DateTime? completedAt;
|
||||||
|
|
||||||
|
const PaymentExecutionOperation({
|
||||||
|
required this.stepRef,
|
||||||
|
required this.operationRef,
|
||||||
|
required this.code,
|
||||||
|
required this.state,
|
||||||
|
required this.label,
|
||||||
|
required this.failureCode,
|
||||||
|
required this.failureReason,
|
||||||
|
required this.startedAt,
|
||||||
|
required this.completedAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:pshared/models/payment/execution_operation.dart';
|
||||||
import 'package:pshared/models/payment/quote/quote.dart';
|
import 'package:pshared/models/payment/quote/quote.dart';
|
||||||
import 'package:pshared/models/payment/state.dart';
|
import 'package:pshared/models/payment/state.dart';
|
||||||
|
|
||||||
@@ -8,6 +9,7 @@ class Payment {
|
|||||||
final PaymentOrchestrationState orchestrationState;
|
final PaymentOrchestrationState orchestrationState;
|
||||||
final String? failureCode;
|
final String? failureCode;
|
||||||
final String? failureReason;
|
final String? failureReason;
|
||||||
|
final List<PaymentExecutionOperation> operations;
|
||||||
final PaymentQuote? lastQuote;
|
final PaymentQuote? lastQuote;
|
||||||
final Map<String, String>? metadata;
|
final Map<String, String>? metadata;
|
||||||
final DateTime? createdAt;
|
final DateTime? createdAt;
|
||||||
@@ -19,6 +21,7 @@ class Payment {
|
|||||||
required this.orchestrationState,
|
required this.orchestrationState,
|
||||||
required this.failureCode,
|
required this.failureCode,
|
||||||
required this.failureReason,
|
required this.failureReason,
|
||||||
|
required this.operations,
|
||||||
required this.lastQuote,
|
required this.lastQuote,
|
||||||
required this.metadata,
|
required this.metadata,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
|
|||||||
@@ -2,24 +2,35 @@ import 'package:flutter/widgets.dart';
|
|||||||
|
|
||||||
import 'package:pshared/generated/i18n/ps_localizations.dart';
|
import 'package:pshared/generated/i18n/ps_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
enum OperationStatus {
|
enum OperationStatus {
|
||||||
|
pending,
|
||||||
processing,
|
processing,
|
||||||
|
retrying,
|
||||||
success,
|
success,
|
||||||
|
skipped,
|
||||||
|
cancelled,
|
||||||
|
needsAttention,
|
||||||
error,
|
error,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
extension OperationStatusX on OperationStatus {
|
extension OperationStatusX on OperationStatus {
|
||||||
/// Returns the localized string for this status,
|
|
||||||
/// e.g. “Processing”, “Success”, “Error”.
|
|
||||||
String localized(BuildContext context) {
|
String localized(BuildContext context) {
|
||||||
final loc = PSLocalizations.of(context)!;
|
final loc = PSLocalizations.of(context)!;
|
||||||
switch (this) {
|
switch (this) {
|
||||||
|
case OperationStatus.pending:
|
||||||
|
return loc.operationStatusPending;
|
||||||
case OperationStatus.processing:
|
case OperationStatus.processing:
|
||||||
return loc.operationStatusProcessing;
|
return loc.operationStatusProcessing;
|
||||||
|
case OperationStatus.retrying:
|
||||||
|
return loc.operationStatusRetrying;
|
||||||
case OperationStatus.success:
|
case OperationStatus.success:
|
||||||
return loc.operationStatusSuccess;
|
return loc.operationStatusSuccess;
|
||||||
|
case OperationStatus.skipped:
|
||||||
|
return loc.operationStatusSkipped;
|
||||||
|
case OperationStatus.cancelled:
|
||||||
|
return loc.operationStatusCancelled;
|
||||||
|
case OperationStatus.needsAttention:
|
||||||
|
return loc.operationStatusNeedsAttention;
|
||||||
case OperationStatus.error:
|
case OperationStatus.error:
|
||||||
return loc.operationStatusError;
|
return loc.operationStatusError;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,30 @@ import 'package:pshared/models/file/downloaded_file.dart';
|
|||||||
import 'package:pshared/service/authorization/service.dart';
|
import 'package:pshared/service/authorization/service.dart';
|
||||||
import 'package:pshared/service/services.dart';
|
import 'package:pshared/service/services.dart';
|
||||||
|
|
||||||
|
|
||||||
class PaymentDocumentsService {
|
class PaymentDocumentsService {
|
||||||
static final _logger = Logger('service.payment_documents');
|
static final _logger = Logger('service.payment_documents');
|
||||||
static const String _objectType = Services.payments;
|
static const String _objectType = Services.payments;
|
||||||
|
|
||||||
static Future<DownloadedFile> getAct(String organizationRef, String paymentRef) async {
|
static Future<DownloadedFile> getAct(
|
||||||
final encodedRef = Uri.encodeQueryComponent(paymentRef);
|
String organizationRef,
|
||||||
final url = '/documents/act/$organizationRef?payment_ref=$encodedRef';
|
String paymentRef, {
|
||||||
|
String? operationRef,
|
||||||
|
}) async {
|
||||||
|
final query = <String, String>{'payment_ref': paymentRef};
|
||||||
|
final operationRefValue = operationRef;
|
||||||
|
if (operationRefValue != null && operationRefValue.isNotEmpty) {
|
||||||
|
query['operation_ref'] = operationRefValue;
|
||||||
|
query['operationRef'] = operationRefValue;
|
||||||
|
}
|
||||||
|
final queryString = Uri(queryParameters: query).query;
|
||||||
|
final url = '/documents/act/$organizationRef?$queryString';
|
||||||
_logger.fine('Downloading act document for payment $paymentRef');
|
_logger.fine('Downloading act document for payment $paymentRef');
|
||||||
final response = await AuthorizationService.getGETBinaryResponse(_objectType, url);
|
final response = await AuthorizationService.getGETBinaryResponse(
|
||||||
final filename = _filenameFromDisposition(response.header('content-disposition')) ??
|
_objectType,
|
||||||
|
url,
|
||||||
|
);
|
||||||
|
final filename =
|
||||||
|
_filenameFromDisposition(response.header('content-disposition')) ??
|
||||||
'act_$paymentRef.pdf';
|
'act_$paymentRef.pdf';
|
||||||
final mimeType = response.header('content-type') ?? 'application/pdf';
|
final mimeType = response.header('content-type') ?? 'application/pdf';
|
||||||
return DownloadedFile(
|
return DownloadedFile(
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ void main() {
|
|||||||
orchestrationState: PaymentOrchestrationState.created,
|
orchestrationState: PaymentOrchestrationState.created,
|
||||||
failureCode: null,
|
failureCode: null,
|
||||||
failureReason: null,
|
failureReason: null,
|
||||||
|
operations: [],
|
||||||
lastQuote: null,
|
lastQuote: null,
|
||||||
metadata: null,
|
metadata: null,
|
||||||
createdAt: null,
|
createdAt: null,
|
||||||
@@ -80,6 +81,7 @@ void main() {
|
|||||||
orchestrationState: PaymentOrchestrationState.settled,
|
orchestrationState: PaymentOrchestrationState.settled,
|
||||||
failureCode: null,
|
failureCode: null,
|
||||||
failureReason: null,
|
failureReason: null,
|
||||||
|
operations: [],
|
||||||
lastQuote: null,
|
lastQuote: null,
|
||||||
metadata: null,
|
metadata: null,
|
||||||
createdAt: null,
|
createdAt: null,
|
||||||
@@ -99,6 +101,7 @@ void main() {
|
|||||||
orchestrationState: PaymentOrchestrationState.executing,
|
orchestrationState: PaymentOrchestrationState.executing,
|
||||||
failureCode: 'failure_ledger',
|
failureCode: 'failure_ledger',
|
||||||
failureReason: 'ledger failed',
|
failureReason: 'ledger failed',
|
||||||
|
operations: [],
|
||||||
lastQuote: null,
|
lastQuote: null,
|
||||||
metadata: null,
|
metadata: null,
|
||||||
createdAt: null,
|
createdAt: null,
|
||||||
@@ -110,6 +113,7 @@ void main() {
|
|||||||
orchestrationState: PaymentOrchestrationState.failed,
|
orchestrationState: PaymentOrchestrationState.failed,
|
||||||
failureCode: null,
|
failureCode: null,
|
||||||
failureReason: null,
|
failureReason: null,
|
||||||
|
operations: [],
|
||||||
lastQuote: null,
|
lastQuote: null,
|
||||||
metadata: null,
|
metadata: null,
|
||||||
createdAt: null,
|
createdAt: null,
|
||||||
|
|||||||
@@ -205,13 +205,13 @@ RouteBase payoutShellRoute() => ShellRoute(
|
|||||||
),
|
),
|
||||||
ChangeNotifierProxyProvider2<
|
ChangeNotifierProxyProvider2<
|
||||||
MultiplePayoutsProvider,
|
MultiplePayoutsProvider,
|
||||||
WalletsController,
|
PaymentSourceController,
|
||||||
MultiplePayoutsController
|
MultiplePayoutsController
|
||||||
>(
|
>(
|
||||||
create: (_) =>
|
create: (_) =>
|
||||||
MultiplePayoutsController(csvInput: WebCsvInputService()),
|
MultiplePayoutsController(csvInput: WebCsvInputService()),
|
||||||
update: (context, provider, wallets, controller) =>
|
update: (context, provider, sourceController, controller) =>
|
||||||
controller!..update(provider, wallets),
|
controller!..update(provider, sourceController),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: PageSelector(child: child, routerState: state),
|
child: PageSelector(child: child, routerState: state),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import 'package:pshared/models/payment/status.dart';
|
|||||||
import 'package:pshared/provider/payment/payments.dart';
|
import 'package:pshared/provider/payment/payments.dart';
|
||||||
|
|
||||||
import 'package:pweb/models/state/load_more_state.dart';
|
import 'package:pweb/models/state/load_more_state.dart';
|
||||||
import 'package:pweb/utils/report/operations.dart';
|
import 'package:pweb/utils/report/operations/operations.dart';
|
||||||
import 'package:pweb/utils/report/payment_mapper.dart';
|
import 'package:pweb/utils/report/payment_mapper.dart';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/models/payment/execution_operation.dart';
|
||||||
import 'package:pshared/models/payment/payment.dart';
|
import 'package:pshared/models/payment/payment.dart';
|
||||||
import 'package:pshared/models/payment/status.dart';
|
import 'package:pshared/models/payment/status.dart';
|
||||||
import 'package:pshared/provider/payment/payments.dart';
|
import 'package:pshared/provider/payment/payments.dart';
|
||||||
|
import 'package:pweb/models/documents/operation.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/utils/payment/operation_code.dart';
|
||||||
import 'package:pweb/utils/report/payment_mapper.dart';
|
import 'package:pweb/utils/report/payment_mapper.dart';
|
||||||
|
|
||||||
|
|
||||||
class PaymentDetailsController extends ChangeNotifier {
|
class PaymentDetailsController extends ChangeNotifier {
|
||||||
PaymentDetailsController({required String paymentId})
|
PaymentDetailsController({required String paymentId})
|
||||||
: _paymentId = paymentId;
|
: _paymentId = paymentId;
|
||||||
|
|
||||||
PaymentsProvider? _payments;
|
PaymentsProvider? _payments;
|
||||||
String _paymentId;
|
String _paymentId;
|
||||||
@@ -25,10 +28,34 @@ class PaymentDetailsController extends ChangeNotifier {
|
|||||||
if (current == null) return false;
|
if (current == null) return false;
|
||||||
final status = statusFromPayment(current);
|
final status = statusFromPayment(current);
|
||||||
final paymentRef = current.paymentRef ?? '';
|
final paymentRef = current.paymentRef ?? '';
|
||||||
return status == OperationStatus.success &&
|
return status == OperationStatus.success && paymentRef.trim().isNotEmpty;
|
||||||
paymentRef.trim().isNotEmpty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OperationDocumentRequestModel? operationDocumentRequest(
|
||||||
|
PaymentExecutionOperation operation,
|
||||||
|
) {
|
||||||
|
final current = _payment;
|
||||||
|
if (current == null) return null;
|
||||||
|
|
||||||
|
final paymentRef = current.paymentRef?.trim() ?? '';
|
||||||
|
if (paymentRef.isEmpty) return null;
|
||||||
|
|
||||||
|
final operationRef = operation.operationRef;
|
||||||
|
if (operationRef == null || operationRef.isEmpty) return null;
|
||||||
|
|
||||||
|
final pair = parseOperationCodePair(operation.code);
|
||||||
|
if (pair == null) return null;
|
||||||
|
if (pair.operation != 'card_payout' || pair.action != 'send') return null;
|
||||||
|
|
||||||
|
return OperationDocumentRequestModel(
|
||||||
|
paymentRef: paymentRef,
|
||||||
|
operationRef: operationRef,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool canDownloadOperationDocument(PaymentExecutionOperation operation) =>
|
||||||
|
operationDocumentRequest(operation) != null;
|
||||||
|
|
||||||
void update(PaymentsProvider provider, String paymentId) {
|
void update(PaymentsProvider provider, String paymentId) {
|
||||||
if (_paymentId != paymentId) {
|
if (_paymentId != paymentId) {
|
||||||
_paymentId = paymentId;
|
_paymentId = paymentId;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:pshared/models/payment/operation.dart';
|
import 'package:pshared/models/payment/operation.dart';
|
||||||
import 'package:pshared/provider/payment/payments.dart';
|
import 'package:pshared/provider/payment/payments.dart';
|
||||||
|
|
||||||
import 'package:pweb/utils/report/operations.dart';
|
import 'package:pweb/utils/report/operations/operations.dart';
|
||||||
import 'package:pweb/utils/report/payment_mapper.dart';
|
import 'package:pweb/utils/report/payment_mapper.dart';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
import 'package:pshared/controllers/payment/source.dart';
|
||||||
import 'package:pshared/models/money.dart';
|
import 'package:pshared/models/money.dart';
|
||||||
import 'package:pshared/models/payment/payment.dart';
|
import 'package:pshared/models/payment/payment.dart';
|
||||||
import 'package:pshared/models/payment/quote/status_type.dart';
|
import 'package:pshared/models/payment/quote/status_type.dart';
|
||||||
@@ -15,15 +15,17 @@ import 'package:pweb/services/payments/csv_input.dart';
|
|||||||
class MultiplePayoutsController extends ChangeNotifier {
|
class MultiplePayoutsController extends ChangeNotifier {
|
||||||
final CsvInputService _csvInput;
|
final CsvInputService _csvInput;
|
||||||
MultiplePayoutsProvider? _provider;
|
MultiplePayoutsProvider? _provider;
|
||||||
WalletsController? _wallets;
|
PaymentSourceController? _sourceController;
|
||||||
_PickState _pickState = _PickState.idle;
|
_PickState _pickState = _PickState.idle;
|
||||||
Exception? _uiError;
|
Exception? _uiError;
|
||||||
|
|
||||||
MultiplePayoutsController({
|
MultiplePayoutsController({required CsvInputService csvInput})
|
||||||
required CsvInputService csvInput,
|
: _csvInput = csvInput;
|
||||||
}) : _csvInput = csvInput;
|
|
||||||
|
|
||||||
void update(MultiplePayoutsProvider provider, WalletsController wallets) {
|
void update(
|
||||||
|
MultiplePayoutsProvider provider,
|
||||||
|
PaymentSourceController sourceController,
|
||||||
|
) {
|
||||||
var shouldNotify = false;
|
var shouldNotify = false;
|
||||||
if (!identical(_provider, provider)) {
|
if (!identical(_provider, provider)) {
|
||||||
_provider?.removeListener(_onProviderChanged);
|
_provider?.removeListener(_onProviderChanged);
|
||||||
@@ -31,10 +33,10 @@ class MultiplePayoutsController extends ChangeNotifier {
|
|||||||
_provider?.addListener(_onProviderChanged);
|
_provider?.addListener(_onProviderChanged);
|
||||||
shouldNotify = true;
|
shouldNotify = true;
|
||||||
}
|
}
|
||||||
if (!identical(_wallets, wallets)) {
|
if (!identical(_sourceController, sourceController)) {
|
||||||
_wallets?.removeListener(_onWalletsChanged);
|
_sourceController?.removeListener(_onSourceChanged);
|
||||||
_wallets = wallets;
|
_sourceController = sourceController;
|
||||||
_wallets?.addListener(_onWalletsChanged);
|
_sourceController?.addListener(_onSourceChanged);
|
||||||
shouldNotify = true;
|
shouldNotify = true;
|
||||||
}
|
}
|
||||||
if (shouldNotify) {
|
if (shouldNotify) {
|
||||||
@@ -58,7 +60,7 @@ class MultiplePayoutsController extends ChangeNotifier {
|
|||||||
_provider?.quoteStatusType ?? QuoteStatusType.missing;
|
_provider?.quoteStatusType ?? QuoteStatusType.missing;
|
||||||
Duration? get quoteTimeLeft => _provider?.quoteTimeLeft;
|
Duration? get quoteTimeLeft => _provider?.quoteTimeLeft;
|
||||||
|
|
||||||
bool get canSend => _provider?.canSend ?? false;
|
bool get canSend => (_provider?.canSend ?? false) && _selectedWallet != null;
|
||||||
Money? get aggregateDebitAmount =>
|
Money? get aggregateDebitAmount =>
|
||||||
_provider?.aggregateDebitAmountFor(_selectedWallet);
|
_provider?.aggregateDebitAmountFor(_selectedWallet);
|
||||||
Money? get requestedSentAmount => _provider?.requestedSentAmount;
|
Money? get requestedSentAmount => _provider?.requestedSentAmount;
|
||||||
@@ -128,11 +130,11 @@ class MultiplePayoutsController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onWalletsChanged() {
|
void _onSourceChanged() {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Wallet? get _selectedWallet => _wallets?.selectedWallet;
|
Wallet? get _selectedWallet => _sourceController?.selectedWallet;
|
||||||
|
|
||||||
void _setUiError(Object error) {
|
void _setUiError(Object error) {
|
||||||
_uiError = error is Exception ? error : Exception(error.toString());
|
_uiError = error is Exception ? error : Exception(error.toString());
|
||||||
@@ -150,7 +152,7 @@ class MultiplePayoutsController extends ChangeNotifier {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_provider?.removeListener(_onProviderChanged);
|
_provider?.removeListener(_onProviderChanged);
|
||||||
_wallets?.removeListener(_onWalletsChanged);
|
_sourceController?.removeListener(_onSourceChanged);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -403,6 +403,34 @@
|
|||||||
"idempotencyKeyLabel": "Idempotency key",
|
"idempotencyKeyLabel": "Idempotency key",
|
||||||
"quoteIdLabel": "Quote ID",
|
"quoteIdLabel": "Quote ID",
|
||||||
"createdAtLabel": "Created at",
|
"createdAtLabel": "Created at",
|
||||||
|
"completedAtLabel": "Completed at",
|
||||||
|
"operationStepStateSkipped": "Skipped",
|
||||||
|
"operationStepStateNeedsAttention": "Needs attention",
|
||||||
|
"operationStepStateRetrying": "Retrying",
|
||||||
|
"paymentOperationPair": "{operation} {action}",
|
||||||
|
"@paymentOperationPair": {
|
||||||
|
"description": "Title pattern for one payment execution operation line in payment details",
|
||||||
|
"placeholders": {
|
||||||
|
"operation": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"paymentOperationCardPayout": "Card payout",
|
||||||
|
"paymentOperationCrypto": "Crypto",
|
||||||
|
"paymentOperationSettlement": "Settlement",
|
||||||
|
"paymentOperationLedger": "Ledger",
|
||||||
|
"paymentOperationActionSend": "Send",
|
||||||
|
"paymentOperationActionObserve": "Observe",
|
||||||
|
"paymentOperationActionFxConvert": "FX convert",
|
||||||
|
"paymentOperationActionCredit": "Credit",
|
||||||
|
"paymentOperationActionBlock": "Block",
|
||||||
|
"paymentOperationActionDebit": "Debit",
|
||||||
|
"paymentOperationActionRelease": "Release",
|
||||||
|
"paymentOperationActionMove": "Move",
|
||||||
"debitAmountLabel": "You pay",
|
"debitAmountLabel": "You pay",
|
||||||
"debitSettlementAmountLabel": "Debit settlement amount",
|
"debitSettlementAmountLabel": "Debit settlement amount",
|
||||||
"expectedSettlementAmountLabel": "Recipient gets",
|
"expectedSettlementAmountLabel": "Recipient gets",
|
||||||
|
|||||||
@@ -403,6 +403,34 @@
|
|||||||
"idempotencyKeyLabel": "Ключ идемпотентности",
|
"idempotencyKeyLabel": "Ключ идемпотентности",
|
||||||
"quoteIdLabel": "ID котировки",
|
"quoteIdLabel": "ID котировки",
|
||||||
"createdAtLabel": "Создан",
|
"createdAtLabel": "Создан",
|
||||||
|
"completedAtLabel": "Завершено",
|
||||||
|
"operationStepStateSkipped": "Пропущен",
|
||||||
|
"operationStepStateNeedsAttention": "Требует внимания",
|
||||||
|
"operationStepStateRetrying": "Повтор",
|
||||||
|
"paymentOperationPair": "{operation} {action}",
|
||||||
|
"@paymentOperationPair": {
|
||||||
|
"description": "Шаблон заголовка строки шага выполнения платежа в деталях платежа",
|
||||||
|
"placeholders": {
|
||||||
|
"operation": {
|
||||||
|
"type": "String"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"paymentOperationCardPayout": "Выплата на карту",
|
||||||
|
"paymentOperationCrypto": "Крипто",
|
||||||
|
"paymentOperationSettlement": "Расчётный контур",
|
||||||
|
"paymentOperationLedger": "Леджер",
|
||||||
|
"paymentOperationActionSend": "Отправка",
|
||||||
|
"paymentOperationActionObserve": "Проверка",
|
||||||
|
"paymentOperationActionFxConvert": "FX-конверсия",
|
||||||
|
"paymentOperationActionCredit": "Зачисление",
|
||||||
|
"paymentOperationActionBlock": "Блокировка",
|
||||||
|
"paymentOperationActionDebit": "Списание",
|
||||||
|
"paymentOperationActionRelease": "Разблокировка",
|
||||||
|
"paymentOperationActionMove": "Перемещение",
|
||||||
"debitAmountLabel": "Вы платите",
|
"debitAmountLabel": "Вы платите",
|
||||||
"debitSettlementAmountLabel": "Списано к зачислению",
|
"debitSettlementAmountLabel": "Списано к зачислению",
|
||||||
"expectedSettlementAmountLabel": "Получателю поступит",
|
"expectedSettlementAmountLabel": "Получателю поступит",
|
||||||
|
|||||||
9
frontend/pweb/lib/models/documents/operation.dart
Normal file
9
frontend/pweb/lib/models/documents/operation.dart
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
class OperationDocumentRequestModel {
|
||||||
|
final String paymentRef;
|
||||||
|
final String operationRef;
|
||||||
|
|
||||||
|
const OperationDocumentRequestModel({
|
||||||
|
required this.paymentRef,
|
||||||
|
required this.operationRef,
|
||||||
|
});
|
||||||
|
}
|
||||||
6
frontend/pweb/lib/models/payment/source_funds.dart
Normal file
6
frontend/pweb/lib/models/payment/source_funds.dart
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
enum SourceOfFundsVisibleState {
|
||||||
|
headerAction,
|
||||||
|
summary,
|
||||||
|
quoteStatus,
|
||||||
|
sendAction,
|
||||||
|
}
|
||||||
@@ -13,8 +13,6 @@ import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
|
|||||||
import 'package:pweb/pages/dashboard/buttons/balance/header.dart';
|
import 'package:pweb/pages/dashboard/buttons/balance/header.dart';
|
||||||
import 'package:pweb/widgets/refresh_balance/wallet.dart';
|
import 'package:pweb/widgets/refresh_balance/wallet.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class WalletCard extends StatelessWidget {
|
class WalletCard extends StatelessWidget {
|
||||||
final Wallet wallet;
|
final Wallet wallet;
|
||||||
@@ -30,7 +28,6 @@ class WalletCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final loc = AppLocalizations.of(context)!;
|
|
||||||
final networkLabel = (wallet.network == null || wallet.network == ChainNetwork.unspecified)
|
final networkLabel = (wallet.network == null || wallet.network == ChainNetwork.unspecified)
|
||||||
? null
|
? null
|
||||||
: wallet.network!.localizedName(context);
|
: wallet.network!.localizedName(context);
|
||||||
@@ -53,11 +50,12 @@ class WalletCard extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
BalanceHeader(
|
BalanceHeader(
|
||||||
title: loc.paymentTypeCryptoWallet,
|
title: wallet.name,
|
||||||
subtitle: networkLabel,
|
subtitle: networkLabel,
|
||||||
badge: (symbol == null || symbol.isEmpty) ? null : symbol,
|
badge: (symbol == null || symbol.isEmpty) ? null : symbol,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
BalanceAmount(
|
BalanceAmount(
|
||||||
wallet: wallet,
|
wallet: wallet,
|
||||||
@@ -65,12 +63,16 @@ class WalletCard extends StatelessWidget {
|
|||||||
context.read<WalletsController>().toggleBalanceMask(wallet.id);
|
context.read<WalletsController>().toggleBalanceMask(wallet.id);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
WalletBalanceRefreshButton(
|
Column(
|
||||||
walletRef: wallet.id,
|
children: [
|
||||||
|
WalletBalanceRefreshButton(
|
||||||
|
walletRef: wallet.id,
|
||||||
|
),
|
||||||
|
BalanceAddFunds(onTopUp: onTopUp),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
BalanceAddFunds(onTopUp: onTopUp),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class SourceQuotePanelHeader extends StatelessWidget {
|
|
||||||
const SourceQuotePanelHeader({
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final l10n = AppLocalizations.of(context)!;
|
|
||||||
return Text(
|
|
||||||
l10n.sourceOfFunds,
|
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,93 +2,134 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
import 'package:pshared/controllers/payment/source.dart';
|
||||||
import 'package:pshared/provider/payment/multiple/quotation.dart';
|
import 'package:pshared/provider/payment/multiple/quotation.dart';
|
||||||
|
|
||||||
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
|
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
|
||||||
import 'package:pweb/controllers/payouts/payout_verification.dart';
|
import 'package:pweb/controllers/payouts/payout_verification.dart';
|
||||||
|
import 'package:pweb/models/payment/source_funds.dart';
|
||||||
import 'package:pweb/pages/dashboard/payouts/multiple/actions.dart';
|
import 'package:pweb/pages/dashboard/payouts/multiple/actions.dart';
|
||||||
import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/header.dart';
|
|
||||||
import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/summary.dart';
|
import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/summary.dart';
|
||||||
import 'package:pweb/pages/dashboard/payouts/multiple/widgets/quote_status.dart';
|
import 'package:pweb/pages/dashboard/payouts/multiple/widgets/quote_status.dart';
|
||||||
import 'package:pweb/pages/payout_page/send/widgets/send_button.dart';
|
import 'package:pweb/pages/payout_page/send/widgets/send_button.dart';
|
||||||
|
import 'package:pweb/widgets/payment/source_of_funds_panel.dart';
|
||||||
import 'package:pweb/widgets/payment/source_wallet_selector.dart';
|
import 'package:pweb/widgets/payment/source_wallet_selector.dart';
|
||||||
import 'package:pweb/widgets/cooldown_hint.dart';
|
import 'package:pweb/widgets/cooldown_hint.dart';
|
||||||
|
import 'package:pweb/widgets/refresh_balance/wallet.dart';
|
||||||
import 'package:pweb/models/state/control_state.dart';
|
import 'package:pweb/models/state/control_state.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
class SourceQuotePanel extends StatelessWidget {
|
class SourceQuotePanel extends StatelessWidget {
|
||||||
const SourceQuotePanel({
|
const SourceQuotePanel({super.key, required this.controller});
|
||||||
super.key,
|
|
||||||
required this.controller,
|
|
||||||
required this.walletsController,
|
|
||||||
});
|
|
||||||
|
|
||||||
final MultiplePayoutsController controller;
|
final MultiplePayoutsController controller;
|
||||||
final WalletsController walletsController;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final verificationController =
|
final sourceController = context.watch<PaymentSourceController>();
|
||||||
context.watch<PayoutVerificationController>();
|
final verificationController = context
|
||||||
|
.watch<PayoutVerificationController>();
|
||||||
final quotationProvider = context.watch<MultiQuotationProvider>();
|
final quotationProvider = context.watch<MultiQuotationProvider>();
|
||||||
final verificationContextKey = quotationProvider.quotation?.quoteRef ??
|
final verificationContextKey =
|
||||||
|
quotationProvider.quotation?.quoteRef ??
|
||||||
quotationProvider.quotation?.idempotencyKey;
|
quotationProvider.quotation?.idempotencyKey;
|
||||||
final isCooldownActive = verificationController.isCooldownActiveFor(
|
final isCooldownActive = verificationController.isCooldownActiveFor(
|
||||||
verificationContextKey,
|
verificationContextKey,
|
||||||
);
|
);
|
||||||
final canSend = controller.canSend && !isCooldownActive;
|
final canSend = controller.canSend && !isCooldownActive;
|
||||||
return Container(
|
return SourceOfFundsPanel(
|
||||||
width: double.infinity,
|
title: l10n.sourceOfFunds,
|
||||||
padding: const EdgeInsets.all(12),
|
sourceSelector: SourceWalletSelector(
|
||||||
decoration: BoxDecoration(
|
sourceController: sourceController,
|
||||||
color: theme.colorScheme.surface,
|
isBusy: controller.isBusy,
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(color: theme.colorScheme.outlineVariant),
|
|
||||||
),
|
),
|
||||||
|
visibleStates: const <SourceOfFundsVisibleState>{
|
||||||
|
SourceOfFundsVisibleState.headerAction,
|
||||||
|
SourceOfFundsVisibleState.summary,
|
||||||
|
SourceOfFundsVisibleState.quoteStatus,
|
||||||
|
SourceOfFundsVisibleState.sendAction,
|
||||||
|
},
|
||||||
|
stateWidgets: <SourceOfFundsVisibleState, Widget>{
|
||||||
|
SourceOfFundsVisibleState.headerAction: _MultipleRefreshAction(
|
||||||
|
sourceController: sourceController,
|
||||||
|
),
|
||||||
|
SourceOfFundsVisibleState.summary: SourceQuoteSummary(
|
||||||
|
controller: controller,
|
||||||
|
spacing: 12,
|
||||||
|
),
|
||||||
|
SourceOfFundsVisibleState.quoteStatus: MultipleQuoteStatusCard(
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
SourceOfFundsVisibleState.sendAction: _MultipleSendAction(
|
||||||
|
controller: controller,
|
||||||
|
canSend: canSend,
|
||||||
|
isCooldownActive: isCooldownActive,
|
||||||
|
verificationController: verificationController,
|
||||||
|
verificationContextKey: verificationContextKey,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MultipleRefreshAction extends StatelessWidget {
|
||||||
|
const _MultipleRefreshAction({required this.sourceController});
|
||||||
|
|
||||||
|
final PaymentSourceController sourceController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final selectedWallet = sourceController.selectedWallet;
|
||||||
|
if (selectedWallet == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return WalletBalanceRefreshButton(walletRef: selectedWallet.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MultipleSendAction extends StatelessWidget {
|
||||||
|
const _MultipleSendAction({
|
||||||
|
required this.controller,
|
||||||
|
required this.canSend,
|
||||||
|
required this.isCooldownActive,
|
||||||
|
required this.verificationController,
|
||||||
|
required this.verificationContextKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
final MultiplePayoutsController controller;
|
||||||
|
final bool canSend;
|
||||||
|
final bool isCooldownActive;
|
||||||
|
final PayoutVerificationController verificationController;
|
||||||
|
final String? verificationContextKey;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
SourceQuotePanelHeader(),
|
SendButton(
|
||||||
const SizedBox(height: 8),
|
onPressed: () => handleMultiplePayoutSend(context, controller),
|
||||||
SourceWalletSelector(
|
state: controller.isSending
|
||||||
walletsController: walletsController,
|
? ControlState.loading
|
||||||
isBusy: controller.isBusy,
|
: canSend
|
||||||
|
? ControlState.enabled
|
||||||
|
: ControlState.disabled,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
if (isCooldownActive) ...[
|
||||||
const Divider(height: 1),
|
const SizedBox(height: 8),
|
||||||
const SizedBox(height: 12),
|
CooldownHint(
|
||||||
SourceQuoteSummary(controller: controller, spacing: 12),
|
seconds: verificationController.cooldownRemainingSecondsFor(
|
||||||
const SizedBox(height: 12),
|
verificationContextKey,
|
||||||
MultipleQuoteStatusCard(controller: controller),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
SendButton(
|
|
||||||
onPressed: () => handleMultiplePayoutSend(context, controller),
|
|
||||||
state: controller.isSending
|
|
||||||
? ControlState.loading
|
|
||||||
: canSend
|
|
||||||
? ControlState.enabled
|
|
||||||
: ControlState.disabled,
|
|
||||||
),
|
|
||||||
if (isCooldownActive) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
CooldownHint(
|
|
||||||
seconds: verificationController.cooldownRemainingSecondsFor(
|
|
||||||
verificationContextKey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
|
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
|
||||||
import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/widget.dart';
|
import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/widget.dart';
|
||||||
import 'package:pweb/pages/dashboard/payouts/multiple/panels/upload_panel/widget.dart';
|
import 'package:pweb/pages/dashboard/payouts/multiple/panels/upload_panel/widget.dart';
|
||||||
@@ -9,14 +7,9 @@ import 'package:pweb/pages/dashboard/payouts/multiple/sections/upload_csv/panel_
|
|||||||
|
|
||||||
|
|
||||||
class UploadCsvLayout extends StatelessWidget {
|
class UploadCsvLayout extends StatelessWidget {
|
||||||
const UploadCsvLayout({
|
const UploadCsvLayout({super.key, required this.controller});
|
||||||
super.key,
|
|
||||||
required this.controller,
|
|
||||||
required this.walletsController,
|
|
||||||
});
|
|
||||||
|
|
||||||
final MultiplePayoutsController controller;
|
final MultiplePayoutsController controller;
|
||||||
final WalletsController walletsController;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -27,28 +20,17 @@ class UploadCsvLayout extends StatelessWidget {
|
|||||||
if (!useHorizontal) {
|
if (!useHorizontal) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
PanelCard(
|
PanelCard(child: UploadPanel(controller: controller)),
|
||||||
child: UploadPanel(
|
|
||||||
controller: controller,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (hasFile) ...[
|
if (hasFile) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
SourceQuotePanel(
|
SourceQuotePanel(controller: controller),
|
||||||
controller: controller,
|
|
||||||
walletsController: walletsController,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasFile) {
|
if (!hasFile) {
|
||||||
return PanelCard(
|
return PanelCard(child: UploadPanel(controller: controller));
|
||||||
child: UploadPanel(
|
|
||||||
controller: controller,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return IntrinsicHeight(
|
return IntrinsicHeight(
|
||||||
@@ -57,19 +39,12 @@ class UploadCsvLayout extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 3,
|
flex: 3,
|
||||||
child: PanelCard(
|
child: PanelCard(child: UploadPanel(controller: controller)),
|
||||||
child: UploadPanel(
|
|
||||||
controller: controller,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 5,
|
flex: 5,
|
||||||
child: SourceQuotePanel(
|
child: SourceQuotePanel(controller: controller),
|
||||||
controller: controller,
|
|
||||||
walletsController: walletsController,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import 'package:pweb/controllers/payouts/multiple_payouts.dart';
|
|||||||
import 'package:pweb/pages/dashboard/payouts/multiple/sections/upload_csv/header.dart';
|
import 'package:pweb/pages/dashboard/payouts/multiple/sections/upload_csv/header.dart';
|
||||||
import 'package:pweb/pages/dashboard/payouts/multiple/sections/upload_csv/layout.dart';
|
import 'package:pweb/pages/dashboard/payouts/multiple/sections/upload_csv/layout.dart';
|
||||||
|
|
||||||
|
|
||||||
class UploadCSVSection extends StatelessWidget {
|
class UploadCSVSection extends StatelessWidget {
|
||||||
const UploadCSVSection({super.key});
|
const UploadCSVSection({super.key});
|
||||||
|
|
||||||
@@ -22,10 +21,7 @@ class UploadCSVSection extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
UploadCsvHeader(theme: theme),
|
UploadCsvHeader(theme: theme),
|
||||||
const SizedBox(height: _verticalSpacing),
|
const SizedBox(height: _verticalSpacing),
|
||||||
UploadCsvLayout(
|
UploadCsvLayout(controller: controller),
|
||||||
controller: controller,
|
|
||||||
walletsController: context.watch(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import 'package:pshared/controllers/payment/source.dart';
|
import 'package:pshared/controllers/payment/source.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/models/payment/source_funds.dart';
|
||||||
import 'package:pweb/pages/payout_page/send/widgets/method_selector.dart';
|
import 'package:pweb/pages/payout_page/send/widgets/method_selector.dart';
|
||||||
import 'package:pweb/pages/payout_page/send/widgets/section/title.dart';
|
|
||||||
import 'package:pweb/pages/payout_page/send/widgets/section/card.dart';
|
|
||||||
import 'package:pweb/utils/dimensions.dart';
|
import 'package:pweb/utils/dimensions.dart';
|
||||||
|
import 'package:pweb/widgets/payment/source_of_funds_panel.dart';
|
||||||
import 'package:pweb/widgets/refresh_balance/ledger.dart';
|
import 'package:pweb/widgets/refresh_balance/ledger.dart';
|
||||||
import 'package:pweb/widgets/refresh_balance/wallet.dart';
|
import 'package:pweb/widgets/refresh_balance/wallet.dart';
|
||||||
|
|
||||||
|
|
||||||
class PaymentSourceOfFundsCard extends StatelessWidget {
|
class PaymentSourceOfFundsCard extends StatelessWidget {
|
||||||
final AppDimensions dimensions;
|
final AppDimensions dimensions;
|
||||||
final String title;
|
final String title;
|
||||||
@@ -23,38 +24,33 @@ class PaymentSourceOfFundsCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PaymentSectionCard(
|
return SourceOfFundsPanel(
|
||||||
child: Column(
|
title: title,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
selectorSpacing: dimensions.paddingSmall,
|
||||||
children: [
|
sourceSelector: const PaymentMethodSelector(),
|
||||||
Row(
|
visibleStates: const <SourceOfFundsVisibleState>{
|
||||||
children: [
|
SourceOfFundsVisibleState.headerAction,
|
||||||
Expanded(child: SectionTitle(title)),
|
},
|
||||||
Consumer<PaymentSourceController>(
|
stateWidgets: <SourceOfFundsVisibleState, Widget>{
|
||||||
builder: (context, provider, _) {
|
SourceOfFundsVisibleState
|
||||||
final selectedWallet = provider.selectedWallet;
|
.headerAction: Consumer<PaymentSourceController>(
|
||||||
if (selectedWallet != null) {
|
builder: (context, provider, _) {
|
||||||
return WalletBalanceRefreshButton(
|
final selectedWallet = provider.selectedWallet;
|
||||||
walletRef: selectedWallet.id,
|
if (selectedWallet != null) {
|
||||||
);
|
return WalletBalanceRefreshButton(walletRef: selectedWallet.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
final selectedLedger = provider.selectedLedgerAccount;
|
final selectedLedger = provider.selectedLedgerAccount;
|
||||||
if (selectedLedger != null) {
|
if (selectedLedger != null) {
|
||||||
return LedgerBalanceRefreshButton(
|
return LedgerBalanceRefreshButton(
|
||||||
ledgerAccountRef: selectedLedger.ledgerAccountRef,
|
ledgerAccountRef: selectedLedger.ledgerAccountRef,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
},
|
||||||
),
|
|
||||||
SizedBox(height: dimensions.paddingSmall),
|
|
||||||
const PaymentMethodSelector(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,9 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:pshared/models/payment/operation.dart';
|
import 'package:pshared/models/payment/operation.dart';
|
||||||
|
|
||||||
|
|
||||||
bool shouldShowToAmount(OperationItem operation) {
|
bool shouldShowToAmount(OperationItem operation) {
|
||||||
if (operation.toCurrency.trim().isEmpty) return false;
|
if (operation.toCurrency.trim().isEmpty) return false;
|
||||||
if (operation.currency.trim().isEmpty) return true;
|
return true;
|
||||||
if (operation.currency != operation.toCurrency) return true;
|
|
||||||
return (operation.toAmount - operation.amount).abs() > 0.0001;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String formatOperationTime(BuildContext context, DateTime date) {
|
String formatOperationTime(BuildContext context, DateTime date) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/models/payment/execution_operation.dart';
|
||||||
import 'package:pshared/models/payment/payment.dart';
|
import 'package:pshared/models/payment/payment.dart';
|
||||||
|
|
||||||
import 'package:pweb/pages/report/details/header.dart';
|
import 'package:pweb/pages/report/details/header.dart';
|
||||||
@@ -13,12 +14,17 @@ class PaymentDetailsContent extends StatelessWidget {
|
|||||||
final Payment payment;
|
final Payment payment;
|
||||||
final VoidCallback onBack;
|
final VoidCallback onBack;
|
||||||
final VoidCallback? onDownloadAct;
|
final VoidCallback? onDownloadAct;
|
||||||
|
final bool Function(PaymentExecutionOperation operation)?
|
||||||
|
canDownloadOperationDocument;
|
||||||
|
final ValueChanged<PaymentExecutionOperation>? onDownloadOperationDocument;
|
||||||
|
|
||||||
const PaymentDetailsContent({
|
const PaymentDetailsContent({
|
||||||
super.key,
|
super.key,
|
||||||
required this.payment,
|
required this.payment,
|
||||||
required this.onBack,
|
required this.onBack,
|
||||||
this.onDownloadAct,
|
this.onDownloadAct,
|
||||||
|
this.canDownloadOperationDocument,
|
||||||
|
this.onDownloadOperationDocument,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -29,17 +35,15 @@ class PaymentDetailsContent extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
PaymentDetailsHeader(
|
PaymentDetailsHeader(title: loc.paymentInfo, onBack: onBack),
|
||||||
title: loc.paymentInfo,
|
|
||||||
onBack: onBack,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
PaymentSummaryCard(
|
PaymentSummaryCard(payment: payment, onDownloadAct: onDownloadAct),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
PaymentDetailsSections(
|
||||||
payment: payment,
|
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 {
|
class PaymentDetailsPage extends StatelessWidget {
|
||||||
final String paymentId;
|
final String paymentId;
|
||||||
|
|
||||||
const PaymentDetailsPage({
|
const PaymentDetailsPage({super.key, required this.paymentId});
|
||||||
super.key,
|
|
||||||
required this.paymentId,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProxyProvider<PaymentsProvider, PaymentDetailsController>(
|
return ChangeNotifierProxyProvider<
|
||||||
|
PaymentsProvider,
|
||||||
|
PaymentDetailsController
|
||||||
|
>(
|
||||||
create: (_) => PaymentDetailsController(paymentId: paymentId),
|
create: (_) => PaymentDetailsController(paymentId: paymentId),
|
||||||
update: (_, payments, controller) => controller!
|
update: (_, payments, controller) =>
|
||||||
..update(payments, paymentId),
|
controller!..update(payments, paymentId),
|
||||||
child: const _PaymentDetailsView(),
|
child: const _PaymentDetailsView(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -67,6 +67,17 @@ class _PaymentDetailsView extends StatelessWidget {
|
|||||||
onDownloadAct: controller.canDownload
|
onDownloadAct: controller.canDownload
|
||||||
? () => downloadPaymentAct(context, payment.paymentRef ?? '')
|
? () => downloadPaymentAct(context, payment.paymentRef ?? '')
|
||||||
: null,
|
: 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:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/models/payment/execution_operation.dart';
|
||||||
import 'package:pshared/models/payment/payment.dart';
|
import 'package:pshared/models/payment/payment.dart';
|
||||||
|
|
||||||
import 'package:pweb/pages/report/details/sections/fx.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 {
|
class PaymentDetailsSections extends StatelessWidget {
|
||||||
final Payment payment;
|
final Payment payment;
|
||||||
|
final bool Function(PaymentExecutionOperation operation)?
|
||||||
|
canDownloadOperationDocument;
|
||||||
|
final ValueChanged<PaymentExecutionOperation>? onDownloadOperationDocument;
|
||||||
|
|
||||||
const PaymentDetailsSections({
|
const PaymentDetailsSections({
|
||||||
super.key,
|
super.key,
|
||||||
required this.payment,
|
required this.payment,
|
||||||
|
this.canDownloadOperationDocument,
|
||||||
|
this.onDownloadOperationDocument,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final hasFx = _hasFxQuote(payment);
|
final hasFx = _hasFxQuote(payment);
|
||||||
if (!hasFx) {
|
final hasOperations = payment.operations.isNotEmpty;
|
||||||
return PaymentMetadataSection(payment: payment);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Row(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: PaymentFxSection(payment: payment)),
|
if (hasFx) ...[
|
||||||
const SizedBox(width: 16),
|
PaymentFxSection(payment: payment),
|
||||||
Expanded(child: PaymentMetadataSection(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;
|
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 feeLabel = formatMoney(fee);
|
||||||
final paymentRef = (payment.paymentRef ?? '').trim();
|
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 showPaymentId = paymentRef.isNotEmpty;
|
||||||
final amountParts = splitAmount(amountLabel);
|
final amountParts = splitAmount(amountLabel);
|
||||||
|
|
||||||
@@ -85,10 +87,10 @@ class PaymentSummaryCard extends StatelessWidget {
|
|||||||
icon: Icons.south_east,
|
icon: Icons.south_east,
|
||||||
text: loc.recipientWillReceive(toAmountLabel),
|
text: loc.recipientWillReceive(toAmountLabel),
|
||||||
),
|
),
|
||||||
if (feeLabel != '-')
|
if (showFee)
|
||||||
InfoLine(
|
InfoLine(
|
||||||
icon: Icons.receipt_long_outlined,
|
icon: Icons.receipt_long_outlined,
|
||||||
text: loc.fee(feeLabel),
|
text: feeText,
|
||||||
muted: true,
|
muted: true,
|
||||||
),
|
),
|
||||||
if (onDownloadAct != null) ...[
|
if (onDownloadAct != null) ...[
|
||||||
|
|||||||
@@ -14,37 +14,34 @@ class OperationStatusBadge extends StatelessWidget {
|
|||||||
|
|
||||||
const OperationStatusBadge({super.key, required this.status});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final label = status.localized(context);
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final bg = _badgeColor(context);
|
final view = operationStatusView(
|
||||||
final fg = _textColor(bg);
|
l10n,
|
||||||
|
Theme.of(context).colorScheme,
|
||||||
|
status,
|
||||||
|
);
|
||||||
|
final label = view.label;
|
||||||
|
final bg = view.backgroundColor;
|
||||||
|
final fg = view.foregroundColor;
|
||||||
|
|
||||||
return badges.Badge(
|
return badges.Badge(
|
||||||
badgeStyle: badges.BadgeStyle(
|
badgeStyle: badges.BadgeStyle(
|
||||||
shape: badges.BadgeShape.square,
|
shape: badges.BadgeShape.square,
|
||||||
badgeColor: bg,
|
badgeColor: bg,
|
||||||
borderRadius: BorderRadius.circular(12), // fully rounded
|
borderRadius: BorderRadius.circular(12), // fully rounded
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 6, vertical: 2 // tighter padding
|
horizontal: 6,
|
||||||
|
vertical: 2, // tighter padding
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
badgeContent: Text(
|
badgeContent: Text(
|
||||||
label.toUpperCase(), // or keep sentence case
|
label.toUpperCase(), // or keep sentence case
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: fg,
|
color: fg,
|
||||||
fontSize: 11, // smaller text
|
fontSize: 11, // smaller text
|
||||||
fontWeight: FontWeight.w500, // medium weight
|
fontWeight: FontWeight.w500, // medium weight
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,9 +31,7 @@ class OperationFilters extends StatelessWidget {
|
|||||||
: '${dateToLocalFormat(context, selectedRange!.start)} – ${dateToLocalFormat(context, selectedRange!.end)}';
|
: '${dateToLocalFormat(context, selectedRange!.start)} – ${dateToLocalFormat(context, selectedRange!.end)}';
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
@@ -61,12 +59,12 @@ class OperationFilters extends StatelessWidget {
|
|||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: onPickRange,
|
onPressed: onPickRange,
|
||||||
icon: const Icon(Icons.date_range_outlined, size: 18),
|
icon: const Icon(Icons.date_range_outlined, size: 18),
|
||||||
label: Text(
|
label: Text(periodLabel, overflow: TextOverflow.ellipsis),
|
||||||
periodLabel,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 10,
|
||||||
|
),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
@@ -76,11 +74,7 @@ class OperationFilters extends StatelessWidget {
|
|||||||
Wrap(
|
Wrap(
|
||||||
spacing: 10,
|
spacing: 10,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
children: const [
|
children: OperationStatus.values.map((status) {
|
||||||
OperationStatus.success,
|
|
||||||
OperationStatus.processing,
|
|
||||||
OperationStatus.error,
|
|
||||||
].map((status) {
|
|
||||||
final label = status.localized(context);
|
final label = status.localized(context);
|
||||||
final isSelected = selectedStatuses.contains(status);
|
final isSelected = selectedStatuses.contains(status);
|
||||||
return FilterChip(
|
return FilterChip(
|
||||||
|
|||||||
23
frontend/pweb/lib/utils/payment/operation_code.dart
Normal file
23
frontend/pweb/lib/utils/payment/operation_code.dart
Normal 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;
|
||||||
|
}
|
||||||
@@ -7,50 +7,150 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
|||||||
|
|
||||||
class StatusView {
|
class StatusView {
|
||||||
final String label;
|
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) {
|
Color get color => backgroundColor;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StatusView operationStatusView(
|
StatusView operationStatusView(
|
||||||
AppLocalizations l10n,
|
AppLocalizations l10n,
|
||||||
|
ColorScheme scheme,
|
||||||
OperationStatus status,
|
OperationStatus status,
|
||||||
) {
|
) {
|
||||||
switch (status) {
|
return operationStatusViewFromToken(
|
||||||
case OperationStatus.success:
|
l10n,
|
||||||
return statusView(l10n, 'SUCCESS');
|
scheme,
|
||||||
case OperationStatus.error:
|
operationStatusTokenFromEnum(status),
|
||||||
return statusView(l10n, 'FAILED');
|
);
|
||||||
case OperationStatus.processing:
|
}
|
||||||
return statusView(l10n, 'ACCEPTED');
|
|
||||||
|
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(' ');
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ import 'package:pweb/utils/error/snackbar.dart';
|
|||||||
import 'package:pweb/generated/i18n/app_localizations.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>();
|
final organizations = context.read<OrganizationsProvider>();
|
||||||
if (!organizations.isOrganizationSet) {
|
if (!organizations.isOrganizationSet) {
|
||||||
return;
|
return;
|
||||||
@@ -28,6 +32,7 @@ Future<void> downloadPaymentAct(BuildContext context, String paymentRef) async {
|
|||||||
final file = await PaymentDocumentsService.getAct(
|
final file = await PaymentDocumentsService.getAct(
|
||||||
organizations.current.id,
|
organizations.current.id,
|
||||||
trimmed,
|
trimmed,
|
||||||
|
operationRef: operationRef,
|
||||||
);
|
);
|
||||||
await downloadFile(file);
|
await downloadFile(file);
|
||||||
},
|
},
|
||||||
|
|||||||
12
frontend/pweb/lib/utils/report/operations/state_mapper.dart
Normal file
12
frontend/pweb/lib/utils/report/operations/state_mapper.dart
Normal 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);
|
||||||
|
}
|
||||||
15
frontend/pweb/lib/utils/report/operations/time_format.dart
Normal file
15
frontend/pweb/lib/utils/report/operations/time_format.dart
Normal 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;
|
||||||
|
}
|
||||||
59
frontend/pweb/lib/utils/report/operations/title_mapper.dart
Normal file
59
frontend/pweb/lib/utils/report/operations/title_mapper.dart
Normal 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(' ');
|
||||||
|
}
|
||||||
@@ -56,11 +56,14 @@ OperationStatus statusFromPayment(Payment payment) {
|
|||||||
return OperationStatus.error;
|
return OperationStatus.error;
|
||||||
case PaymentOrchestrationState.settled:
|
case PaymentOrchestrationState.settled:
|
||||||
return OperationStatus.success;
|
return OperationStatus.success;
|
||||||
case PaymentOrchestrationState.created:
|
|
||||||
case PaymentOrchestrationState.executing:
|
|
||||||
case PaymentOrchestrationState.needsAttention:
|
case PaymentOrchestrationState.needsAttention:
|
||||||
case PaymentOrchestrationState.unspecified:
|
return OperationStatus.needsAttention;
|
||||||
|
case PaymentOrchestrationState.created:
|
||||||
|
return OperationStatus.pending;
|
||||||
|
case PaymentOrchestrationState.executing:
|
||||||
return OperationStatus.processing;
|
return OperationStatus.processing;
|
||||||
|
case PaymentOrchestrationState.unspecified:
|
||||||
|
return OperationStatus.pending;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
83
frontend/pweb/lib/widgets/payment/source_of_funds_panel.dart
Normal file
83
frontend/pweb/lib/widgets/payment/source_of_funds_panel.dart
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/models/payment/source_funds.dart';
|
||||||
|
import 'package:pweb/pages/payout_page/send/widgets/section/card.dart';
|
||||||
|
import 'package:pweb/pages/payout_page/send/widgets/section/title.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class SourceOfFundsPanel extends StatelessWidget {
|
||||||
|
const SourceOfFundsPanel({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.sourceSelector,
|
||||||
|
this.visibleStates = const <SourceOfFundsVisibleState>{},
|
||||||
|
this.stateWidgets = const <SourceOfFundsVisibleState, Widget>{},
|
||||||
|
this.selectorSpacing = 8,
|
||||||
|
this.sectionSpacing = 12,
|
||||||
|
this.padding,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final Widget sourceSelector;
|
||||||
|
final Set<SourceOfFundsVisibleState> visibleStates;
|
||||||
|
final Map<SourceOfFundsVisibleState, Widget> stateWidgets;
|
||||||
|
final double selectorSpacing;
|
||||||
|
final double sectionSpacing;
|
||||||
|
final EdgeInsetsGeometry? padding;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final headerAction = _stateWidget(SourceOfFundsVisibleState.headerAction);
|
||||||
|
final headerActions = headerAction == null
|
||||||
|
? const <Widget>[]
|
||||||
|
: <Widget>[headerAction];
|
||||||
|
final bodySections = _buildBodySections();
|
||||||
|
|
||||||
|
return PaymentSectionCard(
|
||||||
|
padding: padding,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: SectionTitle(title)),
|
||||||
|
...headerActions,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: selectorSpacing),
|
||||||
|
sourceSelector,
|
||||||
|
if (bodySections.isNotEmpty) ...[
|
||||||
|
SizedBox(height: sectionSpacing),
|
||||||
|
const Divider(height: 1),
|
||||||
|
SizedBox(height: sectionSpacing),
|
||||||
|
...bodySections,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildBodySections() {
|
||||||
|
const orderedStates = <SourceOfFundsVisibleState>[
|
||||||
|
SourceOfFundsVisibleState.summary,
|
||||||
|
SourceOfFundsVisibleState.quoteStatus,
|
||||||
|
SourceOfFundsVisibleState.sendAction,
|
||||||
|
];
|
||||||
|
|
||||||
|
final sections = <Widget>[];
|
||||||
|
for (final state in orderedStates) {
|
||||||
|
final section = _stateWidget(state);
|
||||||
|
if (section == null) continue;
|
||||||
|
if (sections.isNotEmpty) {
|
||||||
|
sections.add(SizedBox(height: sectionSpacing));
|
||||||
|
}
|
||||||
|
sections.add(section);
|
||||||
|
}
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget? _stateWidget(SourceOfFundsVisibleState state) {
|
||||||
|
if (!visibleStates.contains(state)) return null;
|
||||||
|
return stateWidgets[state];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
|
||||||
import 'package:pshared/controllers/payment/source.dart';
|
import 'package:pshared/controllers/payment/source.dart';
|
||||||
import 'package:pshared/models/ledger/account.dart';
|
import 'package:pshared/models/ledger/account.dart';
|
||||||
import 'package:pshared/models/payment/source_type.dart';
|
import 'package:pshared/models/payment/source_type.dart';
|
||||||
@@ -10,78 +9,52 @@ import 'package:pshared/utils/money.dart';
|
|||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
typedef _SourceOptionKey = ({PaymentSourceType type, String ref});
|
typedef _SourceOptionKey = ({PaymentSourceType type, String ref});
|
||||||
|
|
||||||
class SourceWalletSelector extends StatelessWidget {
|
class SourceWalletSelector extends StatelessWidget {
|
||||||
const SourceWalletSelector({
|
const SourceWalletSelector({
|
||||||
super.key,
|
super.key,
|
||||||
this.walletsController,
|
required this.sourceController,
|
||||||
this.sourceController,
|
|
||||||
this.isBusy = false,
|
this.isBusy = false,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
}) : assert(
|
});
|
||||||
(walletsController != null) != (sourceController != null),
|
|
||||||
'Provide either walletsController or sourceController',
|
|
||||||
);
|
|
||||||
|
|
||||||
final WalletsController? walletsController;
|
final PaymentSourceController sourceController;
|
||||||
final PaymentSourceController? sourceController;
|
|
||||||
final bool isBusy;
|
final bool isBusy;
|
||||||
final ValueChanged<Wallet>? onChanged;
|
final ValueChanged<Wallet>? onChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final source = sourceController;
|
final source = sourceController;
|
||||||
if (source != null) {
|
final selectedWallet = source.selectedWallet;
|
||||||
final selectedWallet = source.selectedWallet;
|
final selectedLedger = source.selectedLedgerAccount;
|
||||||
final selectedLedger = source.selectedLedgerAccount;
|
final selectedValue = switch (source.selectedType) {
|
||||||
final selectedValue = switch (source.selectedType) {
|
PaymentSourceType.wallet =>
|
||||||
PaymentSourceType.wallet =>
|
selectedWallet == null ? null : _walletKey(selectedWallet.id),
|
||||||
selectedWallet == null ? null : _walletKey(selectedWallet.id),
|
PaymentSourceType.ledger =>
|
||||||
PaymentSourceType.ledger =>
|
selectedLedger == null
|
||||||
selectedLedger == null
|
? null
|
||||||
? null
|
: _ledgerKey(selectedLedger.ledgerAccountRef),
|
||||||
: _ledgerKey(selectedLedger.ledgerAccountRef),
|
null => null,
|
||||||
null => null,
|
};
|
||||||
};
|
|
||||||
|
|
||||||
return _buildSourceSelector(
|
|
||||||
context: context,
|
|
||||||
wallets: source.wallets,
|
|
||||||
ledgerAccounts: source.ledgerAccounts,
|
|
||||||
selectedValue: selectedValue,
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value.type == PaymentSourceType.wallet) {
|
|
||||||
source.selectWalletByRef(value.ref);
|
|
||||||
final selected = source.selectedWallet;
|
|
||||||
if (selected != null) {
|
|
||||||
onChanged?.call(selected);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.type == PaymentSourceType.ledger) {
|
|
||||||
source.selectLedgerByRef(value.ref);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final wallets = walletsController!;
|
|
||||||
return _buildSourceSelector(
|
return _buildSourceSelector(
|
||||||
context: context,
|
context: context,
|
||||||
wallets: wallets.wallets,
|
wallets: source.wallets,
|
||||||
ledgerAccounts: const <LedgerAccount>[],
|
ledgerAccounts: source.ledgerAccounts,
|
||||||
selectedValue: wallets.selectedWalletRef == null
|
selectedValue: selectedValue,
|
||||||
? null
|
|
||||||
: _walletKey(wallets.selectedWalletRef!),
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value.type != PaymentSourceType.wallet) return;
|
if (value.type == PaymentSourceType.wallet) {
|
||||||
wallets.selectWalletByRef(value.ref);
|
source.selectWalletByRef(value.ref);
|
||||||
final selected = wallets.selectedWallet;
|
final selected = source.selectedWallet;
|
||||||
if (selected != null) {
|
if (selected != null) {
|
||||||
onChanged?.call(selected);
|
onChanged?.call(selected);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.type == PaymentSourceType.ledger) {
|
||||||
|
source.selectLedgerByRef(value.ref);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -106,7 +79,7 @@ class SourceWalletSelector extends StatelessWidget {
|
|||||||
return DropdownMenuItem<_SourceOptionKey>(
|
return DropdownMenuItem<_SourceOptionKey>(
|
||||||
value: _walletKey(wallet.id),
|
value: _walletKey(wallet.id),
|
||||||
child: Text(
|
child: Text(
|
||||||
'${wallet.name} - ${_walletBalance(wallet)}',
|
'${_walletDisplayName(wallet, l10n)} - ${_walletBalance(wallet)}',
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -115,7 +88,7 @@ class SourceWalletSelector extends StatelessWidget {
|
|||||||
return DropdownMenuItem<_SourceOptionKey>(
|
return DropdownMenuItem<_SourceOptionKey>(
|
||||||
value: _ledgerKey(ledger.ledgerAccountRef),
|
value: _ledgerKey(ledger.ledgerAccountRef),
|
||||||
child: Text(
|
child: Text(
|
||||||
'${ledger.name} - ${_ledgerBalance(ledger)}',
|
'${_ledgerDisplayName(ledger, l10n)} - ${_ledgerBalance(ledger)}',
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -157,6 +130,20 @@ class SourceWalletSelector extends StatelessWidget {
|
|||||||
_SourceOptionKey _ledgerKey(String ledgerAccountRef) =>
|
_SourceOptionKey _ledgerKey(String ledgerAccountRef) =>
|
||||||
(type: PaymentSourceType.ledger, ref: ledgerAccountRef);
|
(type: PaymentSourceType.ledger, ref: ledgerAccountRef);
|
||||||
|
|
||||||
|
String _walletDisplayName(Wallet wallet, AppLocalizations l10n) {
|
||||||
|
final name = wallet.name.trim();
|
||||||
|
if (name.isNotEmpty) return name;
|
||||||
|
|
||||||
|
return l10n.paymentTypeWallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _ledgerDisplayName(LedgerAccount ledger, AppLocalizations l10n) {
|
||||||
|
final name = ledger.name.trim();
|
||||||
|
if (name.isNotEmpty) return name;
|
||||||
|
|
||||||
|
return l10n.paymentTypeLedger;
|
||||||
|
}
|
||||||
|
|
||||||
String _walletBalance(Wallet wallet) {
|
String _walletBalance(Wallet wallet) {
|
||||||
final symbol = currencyCodeToSymbol(wallet.currency);
|
final symbol = currencyCodeToSymbol(wallet.currency);
|
||||||
return '$symbol ${amountToString(wallet.balance)}';
|
return '$symbol ${amountToString(wallet.balance)}';
|
||||||
|
|||||||
Reference in New Issue
Block a user