added comment for payment, changed intent and added amount ui in operations
This commit is contained in:
@@ -145,16 +145,27 @@ RouteBase payoutShellRoute() => ShellRoute(
|
||||
update: (context, verification, controller) =>
|
||||
controller!..update(verification),
|
||||
),
|
||||
ChangeNotifierProxyProvider4<
|
||||
ChangeNotifierProxyProvider5<
|
||||
PaymentProvider,
|
||||
QuotationProvider,
|
||||
PaymentFlowProvider,
|
||||
PaymentAmountProvider,
|
||||
RecipientsProvider,
|
||||
PaymentPageController
|
||||
>(
|
||||
create: (_) => PaymentPageController(),
|
||||
update: (context, payment, quotation, flow, recipients, controller) =>
|
||||
controller!..update(payment, quotation, flow, recipients),
|
||||
update:
|
||||
(
|
||||
context,
|
||||
payment,
|
||||
quotation,
|
||||
flow,
|
||||
amount,
|
||||
recipients,
|
||||
controller,
|
||||
) =>
|
||||
controller!
|
||||
..update(payment, quotation, flow, amount, recipients),
|
||||
),
|
||||
ChangeNotifierProxyProvider<
|
||||
OrganizationsProvider,
|
||||
@@ -301,6 +312,12 @@ RouteBase payoutShellRoute() => ShellRoute(
|
||||
name: PayoutRoutes.payment,
|
||||
path: PayoutRoutes.paymentPath,
|
||||
pageBuilder: (context, state) {
|
||||
final amountProvider = context.read<PaymentAmountProvider>();
|
||||
if (amountProvider.comment.isNotEmpty) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
amountProvider.setComment('');
|
||||
});
|
||||
}
|
||||
final fallbackDestination = PayoutDestination.dashboard;
|
||||
|
||||
return NoTransitionPage(
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:pshared/utils/money.dart';
|
||||
|
||||
import 'package:pweb/models/payment/amount/mode.dart';
|
||||
|
||||
|
||||
class PaymentAmountFieldController extends ChangeNotifier {
|
||||
static const String _settlementCurrencyCode = 'RUB';
|
||||
|
||||
@@ -19,9 +20,9 @@ class PaymentAmountFieldController extends ChangeNotifier {
|
||||
bool _isSyncingText = false;
|
||||
PaymentAmountMode _mode = PaymentAmountMode.debit;
|
||||
|
||||
PaymentAmountFieldController({required double initialAmount})
|
||||
PaymentAmountFieldController({required double? initialAmount})
|
||||
: textController = TextEditingController(
|
||||
text: amountToString(initialAmount),
|
||||
text: initialAmount == null ? '' : amountToString(initialAmount),
|
||||
);
|
||||
|
||||
PaymentAmountMode get mode => _mode;
|
||||
@@ -57,9 +58,7 @@ class PaymentAmountFieldController extends ChangeNotifier {
|
||||
void handleChanged(String value) {
|
||||
if (_isSyncingText) return;
|
||||
final parsed = _parseAmount(value);
|
||||
if (parsed != null) {
|
||||
_provider?.setAmount(parsed);
|
||||
}
|
||||
_provider?.setAmount(parsed);
|
||||
}
|
||||
|
||||
void handleModeChanged(PaymentAmountMode value) {
|
||||
@@ -130,11 +129,11 @@ class PaymentAmountFieldController extends ChangeNotifier {
|
||||
return parsed.isNaN ? null : parsed;
|
||||
}
|
||||
|
||||
void _syncTextWithAmount(double amount) {
|
||||
void _syncTextWithAmount(double? amount) {
|
||||
final parsedText = _parseAmount(textController.text);
|
||||
if (parsedText != null && parsedText == amount) return;
|
||||
if (parsedText == amount) return;
|
||||
|
||||
final nextText = amountToString(amount);
|
||||
final nextText = amount == null ? '' : amountToString(amount);
|
||||
_isSyncingText = true;
|
||||
textController.value = TextEditingValue(
|
||||
text: nextText,
|
||||
|
||||
53
frontend/pweb/lib/controllers/payments/comment_field.dart
Normal file
53
frontend/pweb/lib/controllers/payments/comment_field.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/provider/payment/amount.dart';
|
||||
|
||||
|
||||
class PaymentCommentFieldController extends ChangeNotifier {
|
||||
final TextEditingController textController;
|
||||
|
||||
PaymentAmountProvider? _provider;
|
||||
bool _isSyncingText = false;
|
||||
|
||||
PaymentCommentFieldController({required String initialComment})
|
||||
: textController = TextEditingController(text: initialComment);
|
||||
|
||||
void update(PaymentAmountProvider provider) {
|
||||
if (identical(_provider, provider)) {
|
||||
_syncTextWithComment(provider.comment);
|
||||
return;
|
||||
}
|
||||
_provider?.removeListener(_handleProviderChanged);
|
||||
_provider = provider;
|
||||
_provider?.addListener(_handleProviderChanged);
|
||||
_syncTextWithComment(provider.comment);
|
||||
}
|
||||
|
||||
void handleChanged(String value) {
|
||||
if (_isSyncingText) return;
|
||||
_provider?.setComment(value);
|
||||
}
|
||||
|
||||
void _handleProviderChanged() {
|
||||
final provider = _provider;
|
||||
if (provider == null) return;
|
||||
_syncTextWithComment(provider.comment);
|
||||
}
|
||||
|
||||
void _syncTextWithComment(String comment) {
|
||||
if (textController.text == comment) return;
|
||||
_isSyncingText = true;
|
||||
textController.value = TextEditingValue(
|
||||
text: comment,
|
||||
selection: TextSelection.collapsed(offset: comment.length),
|
||||
);
|
||||
_isSyncingText = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_provider?.removeListener(_handleProviderChanged);
|
||||
textController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:pshared/provider/payment/flow.dart';
|
||||
import 'package:pshared/provider/payment/amount.dart';
|
||||
import 'package:pshared/provider/payment/provider.dart';
|
||||
import 'package:pshared/provider/payment/quotation/quotation.dart';
|
||||
import 'package:pshared/provider/recipient/provider.dart';
|
||||
@@ -14,6 +15,7 @@ class PaymentPageController extends ChangeNotifier {
|
||||
PaymentProvider? _payment;
|
||||
QuotationProvider? _quotation;
|
||||
PaymentFlowProvider? _flow;
|
||||
PaymentAmountProvider? _amount;
|
||||
RecipientsProvider? _recipients;
|
||||
|
||||
bool _isSending = false;
|
||||
@@ -26,11 +28,13 @@ class PaymentPageController extends ChangeNotifier {
|
||||
PaymentProvider payment,
|
||||
QuotationProvider quotation,
|
||||
PaymentFlowProvider flow,
|
||||
PaymentAmountProvider amount,
|
||||
RecipientsProvider recipients,
|
||||
) {
|
||||
_payment = payment;
|
||||
_quotation = quotation;
|
||||
_flow = flow;
|
||||
_amount = amount;
|
||||
_recipients = recipients;
|
||||
}
|
||||
|
||||
@@ -59,6 +63,7 @@ class PaymentPageController extends ChangeNotifier {
|
||||
_quotation?.reset();
|
||||
_payment?.reset();
|
||||
_flow?.setManualPaymentData(null);
|
||||
_amount?.setComment('');
|
||||
_recipients?.setCurrentObject(null);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ class CsvPayoutRow {
|
||||
final int expMonth;
|
||||
final int expYear;
|
||||
final String amount;
|
||||
final String? comment;
|
||||
|
||||
const CsvPayoutRow({
|
||||
required this.pan,
|
||||
@@ -13,5 +14,6 @@ class CsvPayoutRow {
|
||||
required this.expMonth,
|
||||
required this.expYear,
|
||||
required this.amount,
|
||||
this.comment,
|
||||
});
|
||||
}
|
||||
|
||||
27
frontend/pweb/lib/pages/dashboard/payouts/comment/field.dart
Normal file
27
frontend/pweb/lib/pages/dashboard/payouts/comment/field.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pweb/controllers/payments/comment_field.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class PaymentCommentField extends StatelessWidget {
|
||||
const PaymentCommentField({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final controller = context.watch<PaymentCommentFieldController>();
|
||||
|
||||
return TextField(
|
||||
controller: controller.textController,
|
||||
decoration: InputDecoration(
|
||||
labelText: '${loc.comment} (${loc.optional})',
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
onChanged: controller.handleChanged,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/provider/payment/amount.dart';
|
||||
|
||||
import 'package:pweb/controllers/payments/comment_field.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/comment/field.dart';
|
||||
|
||||
|
||||
class PaymentCommentWidget extends StatelessWidget {
|
||||
const PaymentCommentWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProxyProvider<
|
||||
PaymentAmountProvider,
|
||||
PaymentCommentFieldController
|
||||
>(
|
||||
create: (ctx) {
|
||||
final initialComment = ctx.read<PaymentAmountProvider>().comment;
|
||||
return PaymentCommentFieldController(initialComment: initialComment);
|
||||
},
|
||||
update: (ctx, amountProvider, controller) {
|
||||
controller!.update(amountProvider);
|
||||
return controller;
|
||||
},
|
||||
child: const PaymentCommentField(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:pweb/controllers/payouts/quotation.dart';
|
||||
import 'package:pweb/models/dashboard/quote_status_data.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/amount/widget.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/comment/widget.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/fee_payer.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/quote_status/widgets/card.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/quote_status/widgets/refresh_section.dart';
|
||||
@@ -23,7 +24,6 @@ class PaymentFormWidget extends StatelessWidget {
|
||||
static const double _columnSpacing = 24;
|
||||
static const double _narrowWidth = 560;
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
@@ -66,6 +66,8 @@ class PaymentFormWidget extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const PaymentAmountWidget(),
|
||||
const SizedBox(height: _mediumSpacing),
|
||||
const PaymentCommentWidget(),
|
||||
const SizedBox(height: _smallSpacing),
|
||||
FeePayerSwitch(
|
||||
spacing: _smallSpacing,
|
||||
@@ -104,12 +106,9 @@ class PaymentFormWidget extends StatelessWidget {
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: const PaymentAmountWidget(),
|
||||
),
|
||||
Expanded(flex: 3, child: const PaymentAmountWidget()),
|
||||
const SizedBox(width: _columnSpacing),
|
||||
Expanded(flex: 2, child: quoteCard),
|
||||
Expanded(flex: 2, child: PaymentCommentWidget()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: _smallSpacing),
|
||||
@@ -136,8 +135,9 @@ class PaymentFormWidget extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
autoRefreshSection,
|
||||
],
|
||||
quoteCard,
|
||||
const SizedBox(height: _mediumSpacing),
|
||||
autoRefreshSection],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:pweb/models/payment/multiple_payouts/csv_row.dart';
|
||||
|
||||
|
||||
const String sampleFileName = 'sample.csv';
|
||||
|
||||
final List<CsvPayoutRow> sampleRows = [
|
||||
@@ -11,6 +10,7 @@ final List<CsvPayoutRow> sampleRows = [
|
||||
expMonth: 12,
|
||||
expYear: 27,
|
||||
amount: "500",
|
||||
comment: "Salary payout",
|
||||
),
|
||||
CsvPayoutRow(
|
||||
pan: "9022****12",
|
||||
@@ -27,16 +27,26 @@ final List<CsvPayoutRow> sampleRows = [
|
||||
expMonth: 3,
|
||||
expYear: 28,
|
||||
amount: "120",
|
||||
comment: "Refund",
|
||||
),
|
||||
];
|
||||
|
||||
String buildSampleCsvContent() {
|
||||
final rows = <String>[
|
||||
'pan,first_name,last_name,exp_month,exp_year,amount',
|
||||
'pan,first_name,last_name,exp_month,exp_year,amount,comment',
|
||||
...sampleRows.map(
|
||||
(row) =>
|
||||
'${row.pan},${row.firstName},${row.lastName},${row.expMonth},${row.expYear},${row.amount}',
|
||||
'${row.pan},${row.firstName},${row.lastName},${row.expMonth},${row.expYear},${row.amount},${_escapeCsvCell(row.comment)}',
|
||||
),
|
||||
];
|
||||
return rows.join('\n');
|
||||
}
|
||||
|
||||
String _escapeCsvCell(String? value) {
|
||||
if (value == null || value.isEmpty) return '';
|
||||
if (!value.contains(',') && !value.contains('"') && !value.contains('\n')) {
|
||||
return value;
|
||||
}
|
||||
final escaped = value.replaceAll('"', '""');
|
||||
return '"$escaped"';
|
||||
}
|
||||
|
||||
@@ -6,10 +6,7 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class FileFormatSampleTable extends StatelessWidget {
|
||||
const FileFormatSampleTable({
|
||||
super.key,
|
||||
required this.rows,
|
||||
});
|
||||
const FileFormatSampleTable({super.key, required this.rows});
|
||||
|
||||
final List<CsvPayoutRow> rows;
|
||||
|
||||
@@ -24,6 +21,7 @@ class FileFormatSampleTable extends StatelessWidget {
|
||||
DataColumn(label: Text(l10n.lastName)),
|
||||
DataColumn(label: Text(l10n.expiryDate)),
|
||||
DataColumn(label: Text(l10n.amountColumn)),
|
||||
DataColumn(label: Text(l10n.commentColumn)),
|
||||
],
|
||||
rows: rows.map((row) {
|
||||
return DataRow(
|
||||
@@ -35,6 +33,7 @@ class FileFormatSampleTable extends StatelessWidget {
|
||||
Text('${row.expMonth.toString().padLeft(2, '0')}/${row.expYear}'),
|
||||
),
|
||||
DataCell(Text(row.amount)),
|
||||
DataCell(Text(row.comment ?? '')),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
import 'package:pshared/provider/payment/amount.dart';
|
||||
|
||||
import 'package:pweb/widgets/sidebar/destinations.dart';
|
||||
import 'package:pweb/controllers/payments/page_ui.dart';
|
||||
@@ -46,6 +49,7 @@ class _PaymentPageState extends State<PaymentPage> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
context.read<PaymentAmountProvider>().setComment('');
|
||||
_uiController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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/money_display.dart';
|
||||
import 'package:pweb/utils/report/operations/time_format.dart';
|
||||
import 'package:pweb/utils/report/operations/title_mapper.dart';
|
||||
|
||||
@@ -30,6 +31,7 @@ class OperationHistoryTile extends StatelessWidget {
|
||||
final operationLabel = operation.label?.trim();
|
||||
final stateView = resolveStepStateView(context, operation.state);
|
||||
final completedAt = formatCompletedAt(context, operation.completedAt);
|
||||
final amount = formatMoneyUi(context, operation.amount);
|
||||
final canDownload = canDownloadDocument && onDownloadDocument != null;
|
||||
|
||||
return Column(
|
||||
@@ -67,7 +69,14 @@ class OperationHistoryTile extends StatelessWidget {
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${loc.amountColumn}: $amount',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (canDownload) ...[
|
||||
const SizedBox(height: 8),
|
||||
TextButton.icon(
|
||||
|
||||
@@ -45,12 +45,12 @@ class MultipleCsvParser {
|
||||
'expiry_year',
|
||||
]);
|
||||
final amountIndex = _resolveHeaderIndex(header, const ['amount', 'sum']);
|
||||
final commentIndex = _resolveHeaderIndex(header, const ['comment']);
|
||||
|
||||
if (panIndex < 0 ||
|
||||
firstNameIndex < 0 ||
|
||||
lastNameIndex < 0 ||
|
||||
(expDateIndex < 0 &&
|
||||
(expMonthIndex < 0 || expYearIndex < 0)) ||
|
||||
(expDateIndex < 0 && (expMonthIndex < 0 || expYearIndex < 0)) ||
|
||||
amountIndex < 0) {
|
||||
throw FormatException(
|
||||
'CSV header must contain pan, first_name, last_name, amount columns and either exp_date/expiry or exp_month and exp_year',
|
||||
@@ -67,6 +67,7 @@ class MultipleCsvParser {
|
||||
final expMonthRaw = _cell(raw, expMonthIndex);
|
||||
final expYearRaw = _cell(raw, expYearIndex);
|
||||
final amount = _normalizeAmount(_cell(raw, amountIndex));
|
||||
final comment = commentIndex >= 0 ? _cell(raw, commentIndex) : '';
|
||||
|
||||
if (pan.isEmpty) {
|
||||
throw FormatException('CSV row ${i + 1}: pan is required');
|
||||
@@ -117,6 +118,7 @@ class MultipleCsvParser {
|
||||
expMonth: expMonth,
|
||||
expYear: expYear,
|
||||
amount: amount,
|
||||
comment: _normalizeComment(comment),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -206,6 +208,11 @@ class MultipleCsvParser {
|
||||
return value.trim().replaceAll(' ', '').replaceAll(',', '.');
|
||||
}
|
||||
|
||||
String? _normalizeComment(String value) {
|
||||
final normalized = value.trim();
|
||||
return normalized.isEmpty ? null : normalized;
|
||||
}
|
||||
|
||||
_ExpiryDate _parseExpiryDate(String value, int rowNumber) {
|
||||
final match = RegExp(r'^\s*(\d{1,2})\s*/\s*(\d{2})\s*$').firstMatch(value);
|
||||
if (match == null) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:pshared/models/money.dart';
|
||||
import 'package:pshared/models/payment/customer.dart';
|
||||
import 'package:pshared/models/payment/fees/treatment.dart';
|
||||
import 'package:pshared/models/payment/intent.dart';
|
||||
import 'package:pshared/models/payment/kind.dart';
|
||||
@@ -9,6 +10,7 @@ import 'package:pshared/utils/payment/fx_helpers.dart';
|
||||
|
||||
import 'package:pweb/models/payment/multiple_payouts/csv_row.dart';
|
||||
|
||||
|
||||
class MultipleIntentBuilder {
|
||||
static const String _currency = 'RUB';
|
||||
|
||||
@@ -23,22 +25,33 @@ class MultipleIntentBuilder {
|
||||
);
|
||||
|
||||
return rows
|
||||
.map((row) {
|
||||
.asMap()
|
||||
.entries
|
||||
.map((entry) {
|
||||
final rowIndex = entry.key;
|
||||
final row = entry.value;
|
||||
final amount = Money(amount: row.amount, currency: _currency);
|
||||
final destination = CardPaymentMethod(
|
||||
pan: row.pan,
|
||||
firstName: row.firstName,
|
||||
lastName: row.lastName,
|
||||
expMonth: row.expMonth,
|
||||
expYear: row.expYear,
|
||||
);
|
||||
return PaymentIntent(
|
||||
kind: PaymentKind.payout,
|
||||
source: sourceMethod,
|
||||
destination: CardPaymentMethod(
|
||||
pan: row.pan,
|
||||
firstName: row.firstName,
|
||||
lastName: row.lastName,
|
||||
expMonth: row.expMonth,
|
||||
expYear: row.expYear,
|
||||
),
|
||||
destination: destination,
|
||||
amount: amount,
|
||||
feeTreatment: FeeTreatment.addToSource,
|
||||
settlementMode: SettlementMode.fixReceived,
|
||||
fx: fxIntent,
|
||||
comment: row.comment,
|
||||
customer: Customer(
|
||||
id: 'csv_row_${rowIndex + 1}',
|
||||
firstName: destination.firstName,
|
||||
lastName: destination.lastName,
|
||||
),
|
||||
);
|
||||
})
|
||||
.toList(growable: false);
|
||||
|
||||
@@ -25,6 +25,7 @@ OperationItem mapPaymentToOperation(Payment payment) {
|
||||
_firstNonEmpty([payment.lastQuote?.quoteRef, payment.paymentRef]) ?? '-';
|
||||
final comment =
|
||||
_firstNonEmpty([
|
||||
payment.comment,
|
||||
payment.failureReason,
|
||||
payment.failureCode,
|
||||
payment.state,
|
||||
|
||||
Reference in New Issue
Block a user