redesigned payment page + a lot of fixes
This commit is contained in:
67
frontend/pweb/lib/controllers/payments/amount_field.dart
Normal file
67
frontend/pweb/lib/controllers/payments/amount_field.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/provider/payment/amount.dart';
|
||||
import 'package:pshared/utils/currency.dart';
|
||||
import 'package:pshared/utils/money.dart';
|
||||
|
||||
|
||||
class PaymentAmountFieldController extends ChangeNotifier {
|
||||
final TextEditingController textController;
|
||||
PaymentAmountProvider? _provider;
|
||||
bool _isSyncingText = false;
|
||||
|
||||
PaymentAmountFieldController({required double initialAmount})
|
||||
: textController = TextEditingController(
|
||||
text: amountToString(initialAmount),
|
||||
);
|
||||
|
||||
void update(PaymentAmountProvider provider) {
|
||||
if (identical(_provider, provider)) return;
|
||||
_provider?.removeListener(_handleProviderChanged);
|
||||
_provider = provider;
|
||||
_provider?.addListener(_handleProviderChanged);
|
||||
_syncTextWithAmount(provider.amount);
|
||||
}
|
||||
|
||||
void handleChanged(String value) {
|
||||
if (_isSyncingText) return;
|
||||
final parsed = _parseAmount(value);
|
||||
if (parsed != null) {
|
||||
_provider?.setAmount(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleProviderChanged() {
|
||||
final provider = _provider;
|
||||
if (provider == null) return;
|
||||
_syncTextWithAmount(provider.amount);
|
||||
}
|
||||
|
||||
double? _parseAmount(String value) {
|
||||
final parsed = parseMoneyAmount(
|
||||
value.replaceAll(',', '.'),
|
||||
fallback: double.nan,
|
||||
);
|
||||
return parsed.isNaN ? null : parsed;
|
||||
}
|
||||
|
||||
void _syncTextWithAmount(double amount) {
|
||||
final parsedText = _parseAmount(textController.text);
|
||||
if (parsedText != null && parsedText == amount) return;
|
||||
|
||||
final nextText = amountToString(amount);
|
||||
_isSyncingText = true;
|
||||
textController.value = TextEditingValue(
|
||||
text: nextText,
|
||||
selection: TextSelection.collapsed(offset: nextText.length),
|
||||
);
|
||||
_isSyncingText = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_provider?.removeListener(_handleProviderChanged);
|
||||
textController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
62
frontend/pweb/lib/controllers/payments/details.dart
Normal file
62
frontend/pweb/lib/controllers/payments/details.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
import 'package:pshared/models/payment/status.dart';
|
||||
import 'package:pshared/provider/payment/payments.dart';
|
||||
|
||||
import 'package:pweb/utils/report/payment_mapper.dart';
|
||||
|
||||
|
||||
class PaymentDetailsController extends ChangeNotifier {
|
||||
PaymentDetailsController({required String paymentId})
|
||||
: _paymentId = paymentId;
|
||||
|
||||
PaymentsProvider? _payments;
|
||||
String _paymentId;
|
||||
Payment? _payment;
|
||||
|
||||
String get paymentId => _paymentId;
|
||||
Payment? get payment => _payment;
|
||||
bool get isLoading => _payments?.isLoading ?? false;
|
||||
Exception? get error => _payments?.error;
|
||||
|
||||
bool get canDownload {
|
||||
final current = _payment;
|
||||
if (current == null) return false;
|
||||
final status = statusFromPayment(current);
|
||||
final paymentRef = current.paymentRef ?? '';
|
||||
return status == OperationStatus.success &&
|
||||
paymentRef.trim().isNotEmpty;
|
||||
}
|
||||
|
||||
void update(PaymentsProvider provider, String paymentId) {
|
||||
if (_paymentId != paymentId) {
|
||||
_paymentId = paymentId;
|
||||
}
|
||||
|
||||
if (!identical(_payments, provider)) {
|
||||
_payments = provider;
|
||||
}
|
||||
|
||||
_rebuild();
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
await _payments?.refresh();
|
||||
}
|
||||
|
||||
void _rebuild() {
|
||||
_payment = _findPayment(_payments?.payments ?? const [], _paymentId);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Payment? _findPayment(List<Payment> payments, String paymentId) {
|
||||
final trimmed = paymentId.trim();
|
||||
if (trimmed.isEmpty) return null;
|
||||
for (final payment in payments) {
|
||||
if (payment.paymentRef == trimmed) return payment;
|
||||
if (payment.idempotencyKey == trimmed) return payment;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
80
frontend/pweb/lib/controllers/payments/page.dart
Normal file
80
frontend/pweb/lib/controllers/payments/page.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:pshared/provider/payment/flow.dart';
|
||||
import 'package:pshared/provider/payment/provider.dart';
|
||||
import 'package:pshared/provider/payment/quotation/quotation.dart';
|
||||
import 'package:pshared/provider/recipient/provider.dart';
|
||||
|
||||
import 'package:pweb/services/posthog.dart';
|
||||
|
||||
|
||||
class PaymentPageController extends ChangeNotifier {
|
||||
PaymentProvider? _payment;
|
||||
QuotationProvider? _quotation;
|
||||
PaymentFlowProvider? _flow;
|
||||
RecipientsProvider? _recipients;
|
||||
|
||||
bool _isSending = false;
|
||||
Exception? _error;
|
||||
|
||||
bool get isSending => _isSending;
|
||||
Exception? get error => _error;
|
||||
|
||||
void update(
|
||||
PaymentProvider payment,
|
||||
QuotationProvider quotation,
|
||||
PaymentFlowProvider flow,
|
||||
RecipientsProvider recipients,
|
||||
) {
|
||||
_payment = payment;
|
||||
_quotation = quotation;
|
||||
_flow = flow;
|
||||
_recipients = recipients;
|
||||
}
|
||||
|
||||
Future<bool> sendPayment() async {
|
||||
if (_isSending) return false;
|
||||
final payment = _payment;
|
||||
if (payment == null) {
|
||||
_setError(StateError('Payment provider is not ready'));
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
_setSending(true);
|
||||
_error = null;
|
||||
final result = await payment.pay();
|
||||
return result != null && payment.error == null;
|
||||
} catch (e) {
|
||||
_setError(e);
|
||||
return false;
|
||||
} finally {
|
||||
_setSending(false);
|
||||
}
|
||||
}
|
||||
|
||||
void resetAfterSuccess() {
|
||||
_quotation?.reset();
|
||||
_payment?.reset();
|
||||
_flow?.setManualPaymentData(null);
|
||||
_recipients?.setCurrentObject(null);
|
||||
}
|
||||
|
||||
void handleSuccess() {
|
||||
unawaited(PosthogService.paymentInitiated(method: _flow?.selectedType));
|
||||
resetAfterSuccess();
|
||||
}
|
||||
|
||||
void _setSending(bool value) {
|
||||
if (_isSending == value) return;
|
||||
_isSending = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setError(Object error) {
|
||||
_error = error is Exception ? error : Exception(error.toString());
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
43
frontend/pweb/lib/controllers/payments/page_ui.dart
Normal file
43
frontend/pweb/lib/controllers/payments/page_ui.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:pweb/models/state/visibility.dart';
|
||||
|
||||
|
||||
class PaymentPageUiController extends ChangeNotifier {
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
final FocusNode searchFocusNode = FocusNode();
|
||||
|
||||
String _query = '';
|
||||
VisibilityState _paymentDetailsVisibility = VisibilityState.hidden;
|
||||
|
||||
String get query => _query;
|
||||
VisibilityState get paymentDetailsVisibility => _paymentDetailsVisibility;
|
||||
|
||||
void setQuery(String query) {
|
||||
if (_query == query) return;
|
||||
_query = query;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearSearch() {
|
||||
if (searchController.text.isNotEmpty) {
|
||||
searchController.clear();
|
||||
}
|
||||
searchFocusNode.unfocus();
|
||||
setQuery('');
|
||||
}
|
||||
|
||||
void togglePaymentDetails() {
|
||||
_paymentDetailsVisibility = _paymentDetailsVisibility == VisibilityState.visible
|
||||
? VisibilityState.hidden
|
||||
: VisibilityState.visible;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
searchController.dispose();
|
||||
searchFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
55
frontend/pweb/lib/controllers/payments/payment_config.dart
Normal file
55
frontend/pweb/lib/controllers/payments/payment_config.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/methods/type.dart';
|
||||
import 'package:pshared/provider/recipient/pmethods.dart';
|
||||
|
||||
import 'package:pweb/pages/payment_methods/add/widget.dart';
|
||||
import 'package:pweb/widgets/dialogs/confirmation_dialog.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class PaymentConfigController {
|
||||
final BuildContext context;
|
||||
|
||||
PaymentConfigController(this.context);
|
||||
|
||||
Future<void> addMethod() async => showDialog<PaymentMethodData>(
|
||||
context: context,
|
||||
builder: (_) => const AddPaymentMethodDialog(),
|
||||
);
|
||||
|
||||
Future<void> editMethod(PaymentMethod method) async {
|
||||
// TODO: implement edit functionality
|
||||
}
|
||||
|
||||
Future<void> deleteMethod(PaymentMethod method) async {
|
||||
final methodsProvider = context.read<PaymentMethodsProvider>();
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final confirmed = await showConfirmationDialog(
|
||||
context: context,
|
||||
title: l10n.delete,
|
||||
message: l10n.deletePaymentConfirmation,
|
||||
confirmLabel: l10n.delete,
|
||||
);
|
||||
if (confirmed) {
|
||||
methodsProvider.delete(method.id);
|
||||
}
|
||||
}
|
||||
|
||||
void toggleEnabled(PaymentMethod method, bool value) {
|
||||
context.read<PaymentMethodsProvider>().setArchivedMethod(method: method, newIsArchived: value);
|
||||
}
|
||||
|
||||
void makeMain(PaymentMethod method) {
|
||||
context.read<PaymentMethodsProvider>().makeMain(method);
|
||||
}
|
||||
|
||||
void reorder(int oldIndex, int newIndex) {
|
||||
// TODO: rimplement on top of Indexable
|
||||
// context.read<PaymentMethodsProvider>().reorderMethods(oldIndex, newIndex);
|
||||
}
|
||||
}
|
||||
33
frontend/pweb/lib/controllers/payments/recent_payments.dart
Normal file
33
frontend/pweb/lib/controllers/payments/recent_payments.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:pshared/models/payment/operation.dart';
|
||||
import 'package:pshared/provider/payment/payments.dart';
|
||||
|
||||
import 'package:pweb/utils/report/operations.dart';
|
||||
import 'package:pweb/utils/report/payment_mapper.dart';
|
||||
|
||||
|
||||
class RecentPaymentsController extends ChangeNotifier {
|
||||
PaymentsProvider? _payments;
|
||||
List<OperationItem> _recent = const [];
|
||||
|
||||
List<OperationItem> get recentOperations => _recent;
|
||||
bool get isLoading => _payments?.isLoading ?? false;
|
||||
Exception? get error => _payments?.error;
|
||||
|
||||
void update(PaymentsProvider provider) {
|
||||
if (!identical(_payments, provider)) {
|
||||
_payments = provider;
|
||||
}
|
||||
_rebuild();
|
||||
}
|
||||
|
||||
void _rebuild() {
|
||||
final operations = (_payments?.payments ?? const [])
|
||||
.map(mapPaymentToOperation)
|
||||
.toList();
|
||||
_recent = sortOperations(operations).take(5).toList();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user