Compare commits
3 Commits
15393765b9
...
SEND066
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97b16542c2 | ||
|
|
39c04beb21 | ||
|
|
d6a3a0cc5b |
@@ -1,87 +0,0 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/gateway/tgsettle/storage"
|
||||
"github.com/tech/sendico/gateway/tgsettle/storage/model"
|
||||
"github.com/tech/sendico/pkg/db/repository"
|
||||
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
treasuryTelegramUsersCollection = "treasury_telegram_users"
|
||||
fieldTreasuryTelegramUserID = "telegramUserId"
|
||||
)
|
||||
|
||||
type TreasuryTelegramUsers struct {
|
||||
logger mlogger.Logger
|
||||
repo repository.Repository
|
||||
}
|
||||
|
||||
func NewTreasuryTelegramUsers(logger mlogger.Logger, db *mongo.Database) (*TreasuryTelegramUsers, error) {
|
||||
if db == nil {
|
||||
return nil, merrors.InvalidArgument("mongo database is nil")
|
||||
}
|
||||
if logger == nil {
|
||||
logger = zap.NewNop()
|
||||
}
|
||||
logger = logger.Named("treasury_telegram_users").With(zap.String("collection", treasuryTelegramUsersCollection))
|
||||
|
||||
repo := repository.CreateMongoRepository(db, treasuryTelegramUsersCollection)
|
||||
if err := repo.CreateIndex(&ri.Definition{
|
||||
Keys: []ri.Key{{Field: fieldTreasuryTelegramUserID, Sort: ri.Asc}},
|
||||
Unique: true,
|
||||
}); err != nil {
|
||||
logger.Error("Failed to create treasury telegram users user_id index", zap.Error(err), zap.String("index_field", fieldTreasuryTelegramUserID))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TreasuryTelegramUsers{
|
||||
logger: logger,
|
||||
repo: repo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *TreasuryTelegramUsers) FindByTelegramUserID(ctx context.Context, telegramUserID string) (*model.TreasuryTelegramUser, error) {
|
||||
telegramUserID = strings.TrimSpace(telegramUserID)
|
||||
if telegramUserID == "" {
|
||||
return nil, merrors.InvalidArgument("telegram_user_id is required", "telegram_user_id")
|
||||
}
|
||||
var result model.TreasuryTelegramUser
|
||||
err := t.repo.FindOneByFilter(ctx, repository.Filter(fieldTreasuryTelegramUserID, telegramUserID), &result)
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.logger.Warn("Failed to load treasury telegram user", zap.Error(err), zap.String("telegram_user_id", telegramUserID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
result.TelegramUserID = strings.TrimSpace(result.TelegramUserID)
|
||||
result.LedgerAccountID = strings.TrimSpace(result.LedgerAccountID)
|
||||
if len(result.AllowedChatIDs) > 0 {
|
||||
normalized := make([]string, 0, len(result.AllowedChatIDs))
|
||||
for _, next := range result.AllowedChatIDs {
|
||||
next = strings.TrimSpace(next)
|
||||
if next == "" {
|
||||
continue
|
||||
}
|
||||
normalized = append(normalized, next)
|
||||
}
|
||||
result.AllowedChatIDs = normalized
|
||||
}
|
||||
if result.TelegramUserID == "" || result.LedgerAccountID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
var _ storage.TreasuryTelegramUsersStore = (*TreasuryTelegramUsers)(nil)
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:pshared/data/dto/payment/operation.dart';
|
||||
import 'package:pshared/data/dto/payment/intent/payment.dart';
|
||||
import 'package:pshared/data/dto/payment/payment_quote.dart';
|
||||
|
||||
part 'payment.g.dart';
|
||||
|
||||
|
||||
@JsonSerializable()
|
||||
class PaymentDTO {
|
||||
final String? paymentRef;
|
||||
@@ -13,6 +13,7 @@ class PaymentDTO {
|
||||
final String? state;
|
||||
final String? failureCode;
|
||||
final String? failureReason;
|
||||
final PaymentIntentDTO? intent;
|
||||
final List<PaymentOperationDTO> operations;
|
||||
final PaymentQuoteDTO? lastQuote;
|
||||
final Map<String, String>? metadata;
|
||||
@@ -24,6 +25,7 @@ class PaymentDTO {
|
||||
this.state,
|
||||
this.failureCode,
|
||||
this.failureReason,
|
||||
this.intent,
|
||||
this.operations = const <PaymentOperationDTO>[],
|
||||
this.lastQuote,
|
||||
this.metadata,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:pshared/data/dto/payment/payment.dart';
|
||||
import 'package:pshared/data/mapper/payment/intent/payment.dart';
|
||||
import 'package:pshared/data/mapper/payment/operation.dart';
|
||||
import 'package:pshared/data/mapper/payment/quote.dart';
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
import 'package:pshared/models/payment/state.dart';
|
||||
|
||||
|
||||
extension PaymentDTOMapper on PaymentDTO {
|
||||
Payment toDomain() => Payment(
|
||||
paymentRef: paymentRef,
|
||||
@@ -13,6 +13,7 @@ extension PaymentDTOMapper on PaymentDTO {
|
||||
orchestrationState: paymentOrchestrationStateFromValue(state),
|
||||
failureCode: failureCode,
|
||||
failureReason: failureReason,
|
||||
intent: intent?.toDomain(),
|
||||
operations: operations.map((item) => item.toDomain()).toList(),
|
||||
lastQuote: lastQuote?.toDomain(),
|
||||
metadata: metadata,
|
||||
@@ -27,6 +28,7 @@ extension PaymentMapper on Payment {
|
||||
state: state ?? paymentOrchestrationStateToValue(orchestrationState),
|
||||
failureCode: failureCode,
|
||||
failureReason: failureReason,
|
||||
intent: intent?.toDTO(),
|
||||
operations: operations.map((item) => item.toDTO()).toList(),
|
||||
lastQuote: lastQuote?.toDTO(),
|
||||
metadata: metadata,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
class OperationDocumentInfo {
|
||||
final String operationRef;
|
||||
class OperationDocumentRef {
|
||||
final String gatewayService;
|
||||
final String operationRef;
|
||||
|
||||
const OperationDocumentInfo({
|
||||
required this.operationRef,
|
||||
const OperationDocumentRef({
|
||||
required this.gatewayService,
|
||||
required this.operationRef,
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:pshared/models/payment/execution_operation.dart';
|
||||
import 'package:pshared/models/payment/intent.dart';
|
||||
import 'package:pshared/models/payment/quote/quote.dart';
|
||||
import 'package:pshared/models/payment/state.dart';
|
||||
|
||||
@@ -9,6 +10,7 @@ class Payment {
|
||||
final PaymentOrchestrationState orchestrationState;
|
||||
final String? failureCode;
|
||||
final String? failureReason;
|
||||
final PaymentIntent? intent;
|
||||
final List<PaymentExecutionOperation> operations;
|
||||
final PaymentQuote? lastQuote;
|
||||
final Map<String, String>? metadata;
|
||||
@@ -21,6 +23,7 @@ class Payment {
|
||||
required this.orchestrationState,
|
||||
required this.failureCode,
|
||||
required this.failureReason,
|
||||
this.intent,
|
||||
required this.operations,
|
||||
required this.lastQuote,
|
||||
required this.metadata,
|
||||
|
||||
@@ -25,6 +25,7 @@ class PayoutRoutes {
|
||||
static const walletTopUp = 'payout-wallet-top-up';
|
||||
|
||||
static const paymentTypeQuery = 'paymentType';
|
||||
static const destinationLedgerAccountRefQuery = 'destinationLedgerAccountRef';
|
||||
static const reportPaymentIdQuery = 'paymentId';
|
||||
|
||||
static const dashboardPath = '/dashboard';
|
||||
@@ -40,7 +41,6 @@ class PayoutRoutes {
|
||||
static const editWalletPath = '/methods/edit';
|
||||
static const walletTopUpPath = '/wallet/top-up';
|
||||
|
||||
|
||||
static String nameFor(PayoutDestination destination) {
|
||||
switch (destination) {
|
||||
case PayoutDestination.dashboard:
|
||||
@@ -126,9 +126,13 @@ class PayoutRoutes {
|
||||
|
||||
static Map<String, String> buildQueryParameters({
|
||||
PaymentType? paymentType,
|
||||
String? destinationLedgerAccountRef,
|
||||
}) {
|
||||
final params = <String, String>{
|
||||
if (paymentType != null) paymentTypeQuery: paymentType.name,
|
||||
if (destinationLedgerAccountRef != null &&
|
||||
destinationLedgerAccountRef.trim().isNotEmpty)
|
||||
destinationLedgerAccountRefQuery: destinationLedgerAccountRef.trim(),
|
||||
};
|
||||
return params;
|
||||
}
|
||||
@@ -140,35 +144,44 @@ class PayoutRoutes {
|
||||
? null
|
||||
: PaymentType.values.firstWhereOrNull((type) => type.name == raw);
|
||||
|
||||
static String? destinationLedgerAccountRefFromState(GoRouterState state) =>
|
||||
destinationLedgerAccountRefFromRaw(
|
||||
state.uri.queryParameters[destinationLedgerAccountRefQuery],
|
||||
);
|
||||
|
||||
static String? destinationLedgerAccountRefFromRaw(String? raw) {
|
||||
final value = raw?.trim();
|
||||
if (value == null || value.isEmpty) return null;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
extension PayoutNavigation on BuildContext {
|
||||
void goToPayout(PayoutDestination destination) => goNamed(PayoutRoutes.nameFor(destination));
|
||||
void goToPayout(PayoutDestination destination) =>
|
||||
goNamed(PayoutRoutes.nameFor(destination));
|
||||
|
||||
void pushToPayout(PayoutDestination destination) => pushNamed(PayoutRoutes.nameFor(destination));
|
||||
void pushToPayout(PayoutDestination destination) =>
|
||||
pushNamed(PayoutRoutes.nameFor(destination));
|
||||
|
||||
void goToPayment({
|
||||
PaymentType? paymentType,
|
||||
}) =>
|
||||
goNamed(
|
||||
String? destinationLedgerAccountRef,
|
||||
}) => goNamed(
|
||||
PayoutRoutes.payment,
|
||||
queryParameters: PayoutRoutes.buildQueryParameters(
|
||||
paymentType: paymentType,
|
||||
destinationLedgerAccountRef: destinationLedgerAccountRef,
|
||||
),
|
||||
);
|
||||
|
||||
void goToReportPayment(String paymentId) => goNamed(
|
||||
PayoutRoutes.reportPayment,
|
||||
queryParameters: {
|
||||
PayoutRoutes.reportPaymentIdQuery: paymentId,
|
||||
},
|
||||
queryParameters: {PayoutRoutes.reportPaymentIdQuery: paymentId},
|
||||
);
|
||||
|
||||
void pushToReportPayment(String paymentId) => pushNamed(
|
||||
PayoutRoutes.reportPayment,
|
||||
queryParameters: {
|
||||
PayoutRoutes.reportPaymentIdQuery: paymentId,
|
||||
},
|
||||
queryParameters: {PayoutRoutes.reportPaymentIdQuery: paymentId},
|
||||
);
|
||||
|
||||
void pushToWalletTopUp() => pushNamed(PayoutRoutes.walletTopUp);
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
||||
import 'package:pshared/controllers/payment/source.dart';
|
||||
import 'package:pshared/models/ledger/account.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
import 'package:pshared/provider/ledger.dart';
|
||||
@@ -227,7 +228,9 @@ RouteBase payoutShellRoute() => ShellRoute(
|
||||
onGoToPaymentWithoutRecipient: (type) =>
|
||||
_startPayment(context, recipient: null, paymentType: type),
|
||||
onTopUp: (wallet) => _openWalletTopUp(context, wallet),
|
||||
onLedgerAddFunds: (account) => _openLedgerAddFunds(context, account),
|
||||
onWalletTap: (wallet) => _openWalletEdit(context, wallet),
|
||||
onLedgerTap: (account) => _openLedgerEdit(context, account),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -304,6 +307,8 @@ RouteBase payoutShellRoute() => ShellRoute(
|
||||
child: PaymentPage(
|
||||
onBack: (_) => _popOrGo(context),
|
||||
initialPaymentType: PayoutRoutes.paymentTypeFromState(state),
|
||||
initialDestinationLedgerAccountRef:
|
||||
PayoutRoutes.destinationLedgerAccountRefFromState(state),
|
||||
fallbackDestination: fallbackDestination,
|
||||
),
|
||||
);
|
||||
@@ -340,17 +345,9 @@ RouteBase payoutShellRoute() => ShellRoute(
|
||||
GoRoute(
|
||||
name: PayoutRoutes.editWallet,
|
||||
path: PayoutRoutes.editWalletPath,
|
||||
pageBuilder: (context, state) {
|
||||
final walletsProvider = context.read<WalletsController>();
|
||||
final wallet = walletsProvider.selectedWallet;
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
|
||||
return NoTransitionPage(
|
||||
child: wallet != null
|
||||
? WalletEditPage(onBack: () => _popOrGo(context))
|
||||
: Center(child: Text(loc.noWalletSelected)),
|
||||
);
|
||||
},
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: WalletEditPage(onBack: () => _popOrGo(context)),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: PayoutRoutes.walletTopUp,
|
||||
@@ -389,10 +386,32 @@ void _openEditRecipient(BuildContext context, {required Recipient recipient}) {
|
||||
}
|
||||
|
||||
void _openWalletEdit(BuildContext context, Wallet wallet) {
|
||||
context.read<PaymentSourceController>().selectWallet(wallet);
|
||||
context.read<WalletsController>().selectWallet(wallet);
|
||||
context.pushToEditWallet();
|
||||
}
|
||||
|
||||
void _openLedgerEdit(BuildContext context, LedgerAccount account) {
|
||||
context.read<PaymentSourceController>().selectLedgerByRef(
|
||||
account.ledgerAccountRef,
|
||||
);
|
||||
context.pushToEditWallet();
|
||||
}
|
||||
|
||||
void _openLedgerAddFunds(BuildContext context, LedgerAccount account) {
|
||||
context.read<PaymentSourceController>().selectLedgerByRef(
|
||||
account.ledgerAccountRef,
|
||||
);
|
||||
context.read<RecipientsProvider>().setCurrentObject(null);
|
||||
context.pushNamed(
|
||||
PayoutRoutes.payment,
|
||||
queryParameters: PayoutRoutes.buildQueryParameters(
|
||||
paymentType: PaymentType.ledger,
|
||||
destinationLedgerAccountRef: account.ledgerAccountRef,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openWalletTopUp(BuildContext context, Wallet wallet) {
|
||||
context.read<WalletsController>().selectWallet(wallet);
|
||||
context.pushToWalletTopUp();
|
||||
|
||||
@@ -3,18 +3,24 @@ import 'dart:collection';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/operation.dart';
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
import 'package:pshared/models/payment/source_type.dart';
|
||||
import 'package:pshared/models/payment/status.dart';
|
||||
import 'package:pshared/provider/payment/payments.dart';
|
||||
|
||||
import 'package:pweb/models/state/load_more_state.dart';
|
||||
import 'package:pweb/utils/report/operations/operations.dart';
|
||||
import 'package:pweb/utils/report/payment_mapper.dart';
|
||||
import 'package:pweb/utils/report/source_filter.dart';
|
||||
|
||||
|
||||
class ReportOperationsController extends ChangeNotifier {
|
||||
PaymentsProvider? _payments;
|
||||
PaymentSourceType? _sourceType;
|
||||
Set<String> _sourceRefs = const <String>{};
|
||||
DateTimeRange? _selectedRange;
|
||||
final Set<OperationStatus> _selectedStatuses = {};
|
||||
List<Payment> _paymentItems = const [];
|
||||
List<OperationItem> _operations = const [];
|
||||
List<OperationItem> _filtered = const [];
|
||||
|
||||
@@ -36,10 +42,20 @@ class ReportOperationsController extends ChangeNotifier {
|
||||
return LoadMoreState.hidden;
|
||||
}
|
||||
|
||||
void update(PaymentsProvider provider) {
|
||||
void update(
|
||||
PaymentsProvider provider, {
|
||||
PaymentSourceType? sourceType,
|
||||
String? sourceRef,
|
||||
List<String>? sourceRefs,
|
||||
}) {
|
||||
if (!identical(_payments, provider)) {
|
||||
_payments = provider;
|
||||
}
|
||||
_sourceType = sourceType;
|
||||
final effectiveSourceRefs =
|
||||
sourceRefs ??
|
||||
(sourceRef == null ? const <String>[] : <String>[sourceRef]);
|
||||
_sourceRefs = _normalizeRefs(effectiveSourceRefs);
|
||||
_rebuildOperations();
|
||||
}
|
||||
|
||||
@@ -74,13 +90,16 @@ class ReportOperationsController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _rebuildOperations() {
|
||||
final items = _payments?.payments ?? const [];
|
||||
_operations = items.map(mapPaymentToOperation).toList();
|
||||
_paymentItems = _payments?.payments ?? const [];
|
||||
_operations = _paymentItems
|
||||
.where(_matchesCurrentSource)
|
||||
.map(mapPaymentToOperation)
|
||||
.toList();
|
||||
_rebuildFiltered(notify: true);
|
||||
}
|
||||
|
||||
void _rebuildFiltered({bool notify = true}) {
|
||||
_filtered = _applyFilters(_operations);
|
||||
_filtered = _applyFilters(sortOperations(_operations));
|
||||
if (notify) {
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -88,13 +107,14 @@ class ReportOperationsController extends ChangeNotifier {
|
||||
|
||||
List<OperationItem> _applyFilters(List<OperationItem> operations) {
|
||||
if (_selectedRange == null && _selectedStatuses.isEmpty) {
|
||||
return sortOperations(operations);
|
||||
return operations;
|
||||
}
|
||||
|
||||
final filtered = operations.where((op) {
|
||||
final statusMatch =
|
||||
_selectedStatuses.isEmpty || _selectedStatuses.contains(op.status);
|
||||
final dateMatch = _selectedRange == null ||
|
||||
final dateMatch =
|
||||
_selectedRange == null ||
|
||||
isUnknownDate(op.date) ||
|
||||
(op.date.isAfter(
|
||||
_selectedRange!.start.subtract(const Duration(seconds: 1)),
|
||||
@@ -105,7 +125,30 @@ class ReportOperationsController extends ChangeNotifier {
|
||||
return statusMatch && dateMatch;
|
||||
}).toList();
|
||||
|
||||
return sortOperations(filtered);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
bool _matchesCurrentSource(Payment payment) {
|
||||
final sourceType = _sourceType;
|
||||
if (sourceType == null || _sourceRefs.isEmpty) return true;
|
||||
for (final sourceRef in _sourceRefs) {
|
||||
if (paymentMatchesSource(
|
||||
payment,
|
||||
sourceType: sourceType,
|
||||
sourceRef: sourceRef,
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Set<String> _normalizeRefs(List<String> refs) {
|
||||
final normalized = refs
|
||||
.map((value) => value.trim())
|
||||
.where((value) => value.isNotEmpty)
|
||||
.toSet();
|
||||
return normalized;
|
||||
}
|
||||
|
||||
bool _isSameRange(DateTimeRange? left, DateTimeRange? right) {
|
||||
|
||||
@@ -71,16 +71,24 @@ class WalletTransactionsController extends ChangeNotifier {
|
||||
|
||||
void _rebuildFiltered({bool notify = true}) {
|
||||
final source = _provider?.transactions ?? const <WalletTransaction>[];
|
||||
final activeWalletId = _provider?.walletId;
|
||||
_filteredTransactions = source.where((tx) {
|
||||
final walletMatch =
|
||||
activeWalletId == null || tx.walletId == activeWalletId;
|
||||
final statusMatch =
|
||||
_selectedStatuses.isEmpty || _selectedStatuses.contains(tx.status);
|
||||
final typeMatch =
|
||||
_selectedTypes.isEmpty || _selectedTypes.contains(tx.type);
|
||||
final dateMatch = _dateRange == null ||
|
||||
(tx.date.isAfter(_dateRange!.start.subtract(const Duration(seconds: 1))) &&
|
||||
tx.date.isBefore(_dateRange!.end.add(const Duration(seconds: 1))));
|
||||
final dateMatch =
|
||||
_dateRange == null ||
|
||||
(tx.date.isAfter(
|
||||
_dateRange!.start.subtract(const Duration(seconds: 1)),
|
||||
) &&
|
||||
tx.date.isBefore(
|
||||
_dateRange!.end.add(const Duration(seconds: 1)),
|
||||
));
|
||||
|
||||
return statusMatch && typeMatch && dateMatch;
|
||||
return walletMatch && statusMatch && typeMatch && dateMatch;
|
||||
}).toList();
|
||||
|
||||
if (notify) notifyListeners();
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:pshared/models/payment/operation_document.dart';
|
||||
import 'package:pshared/models/payment/execution_operation.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/models/documents/operation.dart';
|
||||
|
||||
import 'package:pweb/utils/report/operations/document_rule.dart';
|
||||
import 'package:pweb/utils/report/payment_mapper.dart';
|
||||
|
||||
|
||||
class PaymentDetailsController extends ChangeNotifier {
|
||||
PaymentDetailsController({required String paymentId})
|
||||
@@ -22,26 +21,7 @@ class PaymentDetailsController extends ChangeNotifier {
|
||||
bool get isLoading => _payments?.isLoading ?? false;
|
||||
Exception? get error => _payments?.error;
|
||||
|
||||
bool get canDownload {
|
||||
final current = _payment;
|
||||
if (current == null) return false;
|
||||
if (statusFromPayment(current) != OperationStatus.success) return false;
|
||||
return primaryOperationDocumentRequest != null;
|
||||
}
|
||||
|
||||
OperationDocumentRequestModel? get primaryOperationDocumentRequest {
|
||||
final current = _payment;
|
||||
if (current == null) return null;
|
||||
for (final operation in current.operations) {
|
||||
final request = operationDocumentRequest(operation);
|
||||
if (request != null) {
|
||||
return request;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
OperationDocumentRequestModel? operationDocumentRequest(
|
||||
OperationDocumentRef? operationDocumentRequest(
|
||||
PaymentExecutionOperation operation,
|
||||
) {
|
||||
final current = _payment;
|
||||
@@ -54,7 +34,7 @@ class PaymentDetailsController extends ChangeNotifier {
|
||||
|
||||
if (!isOperationDocumentEligible(operation.code)) return null;
|
||||
|
||||
return OperationDocumentRequestModel(
|
||||
return OperationDocumentRef(
|
||||
gatewayService: gatewayService,
|
||||
operationRef: operationRef,
|
||||
);
|
||||
|
||||
@@ -638,7 +638,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"noFee": "No fee",
|
||||
"noFee": "None",
|
||||
|
||||
"recipientWillReceive": "Recipient will receive: {amount}",
|
||||
"@recipientWillReceive": {
|
||||
|
||||
@@ -638,7 +638,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"noFee": "Нет комиссии",
|
||||
"noFee": "Без оплаты",
|
||||
|
||||
"recipientWillReceive": "Получатель получит: {amount}",
|
||||
"@recipientWillReceive": {
|
||||
|
||||
26
frontend/pweb/lib/models/dashboard/balance_item.dart
Normal file
26
frontend/pweb/lib/models/dashboard/balance_item.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:pshared/models/ledger/account.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
|
||||
sealed class BalanceItem {
|
||||
const BalanceItem();
|
||||
|
||||
const factory BalanceItem.wallet(Wallet wallet) = WalletBalanceItem;
|
||||
const factory BalanceItem.ledger(LedgerAccount account) = LedgerBalanceItem;
|
||||
const factory BalanceItem.addAction() = AddBalanceActionItem;
|
||||
}
|
||||
|
||||
final class WalletBalanceItem extends BalanceItem {
|
||||
final Wallet wallet;
|
||||
|
||||
const WalletBalanceItem(this.wallet);
|
||||
}
|
||||
|
||||
final class LedgerBalanceItem extends BalanceItem {
|
||||
final LedgerAccount account;
|
||||
|
||||
const LedgerBalanceItem(this.account);
|
||||
}
|
||||
|
||||
final class AddBalanceActionItem extends BalanceItem {
|
||||
const AddBalanceActionItem();
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
class OperationDocumentRequestModel {
|
||||
final String gatewayService;
|
||||
final String operationRef;
|
||||
|
||||
const OperationDocumentRequestModel({
|
||||
required this.gatewayService,
|
||||
required this.operationRef,
|
||||
});
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
enum PaymentState {
|
||||
success,
|
||||
failed,
|
||||
cancelled,
|
||||
processing,
|
||||
unknown,
|
||||
}
|
||||
|
||||
PaymentState paymentStateFromRaw(String? raw) {
|
||||
final trimmed = (raw ?? '').trim().toUpperCase();
|
||||
final normalized = trimmed.startsWith('PAYMENT_STATE_')
|
||||
? trimmed.substring('PAYMENT_STATE_'.length)
|
||||
: trimmed;
|
||||
|
||||
switch (normalized) {
|
||||
case 'SUCCESS':
|
||||
return PaymentState.success;
|
||||
case 'FAILED':
|
||||
return PaymentState.failed;
|
||||
case 'CANCELLED':
|
||||
return PaymentState.cancelled;
|
||||
case 'PROCESSING':
|
||||
return PaymentState.processing;
|
||||
default:
|
||||
return PaymentState.unknown;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import 'package:pshared/models/ledger/account.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
|
||||
|
||||
enum BalanceItemType { wallet, ledger, addAction }
|
||||
|
||||
class BalanceItem {
|
||||
final BalanceItemType type;
|
||||
final Wallet? wallet;
|
||||
final LedgerAccount? account;
|
||||
|
||||
const BalanceItem.wallet(this.wallet) : type = BalanceItemType.wallet, account = null;
|
||||
|
||||
const BalanceItem.ledger(this.account) : type = BalanceItemType.ledger, wallet = null;
|
||||
|
||||
const BalanceItem.addAction() : type = BalanceItemType.addAction, wallet = null, account = null;
|
||||
|
||||
bool get isWallet => type == BalanceItemType.wallet;
|
||||
bool get isLedger => type == BalanceItemType.ledger;
|
||||
bool get isAdd => type == BalanceItemType.addAction;
|
||||
}
|
||||
@@ -1,17 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
import 'package:pshared/models/payment/chain_network.dart';
|
||||
import 'package:pshared/utils/l10n/chain.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/add_funds.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/amount.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/header.dart';
|
||||
import 'package:pweb/widgets/refresh_balance/wallet.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/source/card.dart';
|
||||
|
||||
|
||||
class WalletCard extends StatelessWidget {
|
||||
@@ -28,56 +19,10 @@ class WalletCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final networkLabel = (wallet.network == null || wallet.network == ChainNetwork.unspecified)
|
||||
? null
|
||||
: wallet.network!.localizedName(context);
|
||||
final symbol = wallet.tokenSymbol?.trim();
|
||||
|
||||
return Card(
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
elevation: WalletCardConfig.elevation,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
|
||||
onTap: onTap,
|
||||
child: SizedBox.expand(
|
||||
child: Padding(
|
||||
padding: WalletCardConfig.contentPadding,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
BalanceHeader(
|
||||
title: wallet.name,
|
||||
subtitle: networkLabel,
|
||||
badge: (symbol == null || symbol.isEmpty) ? null : symbol,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
BalanceAmount(
|
||||
wallet: wallet,
|
||||
onToggleMask: () {
|
||||
context.read<WalletsController>().toggleBalanceMask(wallet.id);
|
||||
},
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
WalletBalanceRefreshButton(
|
||||
walletRef: wallet.id,
|
||||
),
|
||||
BalanceAddFunds(onTopUp: onTopUp),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
return BalanceSourceCard.wallet(
|
||||
wallet: wallet,
|
||||
onTap: onTap,
|
||||
onAddFunds: onTopUp,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
||||
import 'package:pshared/models/ledger/account.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
|
||||
import 'package:pweb/models/dashboard/balance_item.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/add/card.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/balance_item.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/card.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/indicator.dart';
|
||||
@@ -16,7 +17,9 @@ class BalanceCarousel extends StatefulWidget {
|
||||
final int currentIndex;
|
||||
final ValueChanged<int> onIndexChanged;
|
||||
final ValueChanged<Wallet> onTopUp;
|
||||
final ValueChanged<LedgerAccount> onLedgerAddFunds;
|
||||
final ValueChanged<Wallet> onWalletTap;
|
||||
final ValueChanged<LedgerAccount> onLedgerTap;
|
||||
|
||||
const BalanceCarousel({
|
||||
super.key,
|
||||
@@ -24,7 +27,9 @@ class BalanceCarousel extends StatefulWidget {
|
||||
required this.currentIndex,
|
||||
required this.onIndexChanged,
|
||||
required this.onTopUp,
|
||||
required this.onLedgerAddFunds,
|
||||
required this.onWalletTap,
|
||||
required this.onLedgerTap,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -99,14 +104,18 @@ class _BalanceCarouselState extends State<BalanceCarousel> {
|
||||
itemCount: widget.items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = widget.items[index];
|
||||
final Widget card = switch (item.type) {
|
||||
BalanceItemType.wallet => WalletCard(
|
||||
wallet: item.wallet!,
|
||||
onTopUp: () => widget.onTopUp(item.wallet!),
|
||||
onTap: () => widget.onWalletTap(item.wallet!),
|
||||
final Widget card = switch (item) {
|
||||
WalletBalanceItem(:final wallet) => WalletCard(
|
||||
wallet: wallet,
|
||||
onTopUp: () => widget.onTopUp(wallet),
|
||||
onTap: () => widget.onWalletTap(wallet),
|
||||
),
|
||||
BalanceItemType.ledger => LedgerAccountCard(account: item.account!),
|
||||
BalanceItemType.addAction => const AddBalanceCard(),
|
||||
LedgerBalanceItem(:final account) => LedgerAccountCard(
|
||||
account: account,
|
||||
onTap: () => widget.onLedgerTap(account),
|
||||
onAddFunds: () => widget.onLedgerAddFunds(account),
|
||||
),
|
||||
AddBalanceActionItem() => const AddBalanceCard(),
|
||||
};
|
||||
|
||||
return Padding(
|
||||
@@ -123,19 +132,16 @@ class _BalanceCarouselState extends State<BalanceCarousel> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: safeIndex > 0
|
||||
? () => _goToPage(safeIndex - 1)
|
||||
: null,
|
||||
onPressed: safeIndex > 0 ? () => _goToPage(safeIndex - 1) : null,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
CarouselIndicator(
|
||||
itemCount: widget.items.length,
|
||||
index: safeIndex,
|
||||
),
|
||||
CarouselIndicator(itemCount: widget.items.length, index: safeIndex),
|
||||
const SizedBox(width: 16),
|
||||
IconButton(
|
||||
onPressed: safeIndex < widget.items.length - 1 ? () => _goToPage(safeIndex + 1) : null,
|
||||
onPressed: safeIndex < widget.items.length - 1
|
||||
? () => _goToPage(safeIndex + 1)
|
||||
: null,
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -3,7 +3,8 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
||||
import 'package:pshared/provider/ledger.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/balance_item.dart';
|
||||
import 'package:pweb/models/dashboard/balance_item.dart';
|
||||
|
||||
|
||||
class BalanceCarouselController with ChangeNotifier {
|
||||
WalletsController? _walletsController;
|
||||
@@ -73,14 +74,19 @@ class BalanceCarouselController with ChangeNotifier {
|
||||
String? _currentWalletRef(List<BalanceItem> items, int index) {
|
||||
if (items.isEmpty || index < 0 || index >= items.length) return null;
|
||||
final current = items[index];
|
||||
if (!current.isWallet) return null;
|
||||
return current.wallet?.id;
|
||||
return switch (current) {
|
||||
WalletBalanceItem(:final wallet) => wallet.id,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
int? _walletIndexByRef(List<BalanceItem> items, String? walletRef) {
|
||||
if (walletRef == null || walletRef.isEmpty) return null;
|
||||
final idx = items.indexWhere(
|
||||
(item) => item.isWallet && item.wallet?.id == walletRef,
|
||||
(item) => switch (item) {
|
||||
WalletBalanceItem(:final wallet) => wallet.id == walletRef,
|
||||
_ => false,
|
||||
},
|
||||
);
|
||||
if (idx < 0) return null;
|
||||
return idx;
|
||||
@@ -97,17 +103,17 @@ class BalanceCarouselController with ChangeNotifier {
|
||||
for (var i = 0; i < left.length; i++) {
|
||||
final a = left[i];
|
||||
final b = right[i];
|
||||
if (a.type != b.type) return false;
|
||||
if (a.runtimeType != b.runtimeType) return false;
|
||||
if (_itemIdentity(a) != _itemIdentity(b)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
String _itemIdentity(BalanceItem item) => switch (item.type) {
|
||||
BalanceItemType.wallet => item.wallet?.id ?? '',
|
||||
BalanceItemType.ledger => item.account?.ledgerAccountRef ?? '',
|
||||
BalanceItemType.addAction => 'add',
|
||||
String _itemIdentity(BalanceItem item) => switch (item) {
|
||||
WalletBalanceItem(:final wallet) => wallet.id,
|
||||
LedgerBalanceItem(:final account) => account.ledgerAccountRef,
|
||||
AddBalanceActionItem() => 'add',
|
||||
};
|
||||
|
||||
void _syncSelectedWallet() {
|
||||
@@ -115,9 +121,8 @@ class BalanceCarouselController with ChangeNotifier {
|
||||
if (walletsController == null || _items.isEmpty) return;
|
||||
|
||||
final current = _items[_index];
|
||||
if (!current.isWallet || current.wallet == null) return;
|
||||
|
||||
final wallet = current.wallet!;
|
||||
if (current is! WalletBalanceItem) return;
|
||||
final wallet = current.wallet;
|
||||
if (walletsController.selectedWallet?.id == wallet.id) return;
|
||||
walletsController.selectWallet(wallet);
|
||||
}
|
||||
|
||||
@@ -1,124 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/controllers/balance_mask/ledger_accounts.dart';
|
||||
import 'package:pshared/models/ledger/account.dart';
|
||||
import 'package:pshared/utils/currency.dart';
|
||||
import 'package:pshared/utils/money.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/header.dart';
|
||||
import 'package:pweb/widgets/refresh_balance/ledger.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/source/card.dart';
|
||||
|
||||
|
||||
class LedgerAccountCard extends StatelessWidget {
|
||||
final LedgerAccount account;
|
||||
final VoidCallback onAddFunds;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const LedgerAccountCard({super.key, required this.account});
|
||||
|
||||
String _formatBalance() {
|
||||
final money = account.balance?.balance;
|
||||
if (money == null) return '--';
|
||||
|
||||
final amount = parseMoneyAmount(money.amount, fallback: double.nan);
|
||||
if (amount.isNaN) {
|
||||
return '${money.amount} ${money.currency}';
|
||||
}
|
||||
|
||||
try {
|
||||
final currency = currencyStringToCode(money.currency);
|
||||
final symbol = currencyCodeToSymbol(currency);
|
||||
if (symbol.trim().isEmpty) {
|
||||
return '${amountToString(amount)} ${money.currency}';
|
||||
}
|
||||
return '${amountToString(amount)} $symbol';
|
||||
} catch (_) {
|
||||
return '${amountToString(amount)} ${money.currency}';
|
||||
}
|
||||
}
|
||||
|
||||
String _formatMaskedBalance() {
|
||||
final currency = account.currency.trim();
|
||||
if (currency.isEmpty) return '••••';
|
||||
try {
|
||||
final symbol = currencyCodeToSymbol(currencyStringToCode(currency));
|
||||
if (symbol.trim().isEmpty) {
|
||||
return '•••• $currency';
|
||||
}
|
||||
return '•••• $symbol';
|
||||
} catch (_) {
|
||||
return '•••• $currency';
|
||||
}
|
||||
}
|
||||
const LedgerAccountCard({
|
||||
super.key,
|
||||
required this.account,
|
||||
required this.onAddFunds,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final accountName = account.name.trim();
|
||||
final accountCode = account.accountCode.trim();
|
||||
final title = accountName.isNotEmpty ? accountName : loc.paymentTypeLedger;
|
||||
final subtitle = accountCode.isNotEmpty ? accountCode : null;
|
||||
final badge = account.currency.trim().isEmpty
|
||||
? null
|
||||
: account.currency.toUpperCase();
|
||||
|
||||
return Card(
|
||||
color: colorScheme.onSecondary,
|
||||
elevation: WalletCardConfig.elevation,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
|
||||
),
|
||||
child: Padding(
|
||||
padding: WalletCardConfig.contentPadding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
BalanceHeader(title: title, subtitle: subtitle, badge: badge),
|
||||
Row(
|
||||
children: [
|
||||
Consumer<LedgerBalanceMaskController>(
|
||||
builder: (context, controller, _) {
|
||||
final isMasked = controller.isBalanceMasked(
|
||||
account.ledgerAccountRef,
|
||||
);
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
isMasked ? _formatMaskedBalance() : _formatBalance(),
|
||||
style: textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
GestureDetector(
|
||||
onTap: () => controller.toggleBalanceMask(
|
||||
account.ledgerAccountRef,
|
||||
),
|
||||
child: Icon(
|
||||
isMasked ? Icons.visibility_off : Icons.visibility,
|
||||
size: 24,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
LedgerBalanceRefreshButton(
|
||||
ledgerAccountRef: account.ledgerAccountRef,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
return BalanceSourceCard.ledger(
|
||||
account: account,
|
||||
onTap: onTap ?? () {},
|
||||
onAddFunds: onAddFunds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/controllers/balance_mask/ledger_accounts.dart';
|
||||
import 'package:pshared/models/ledger/account.dart';
|
||||
|
||||
import 'package:pweb/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart';
|
||||
|
||||
|
||||
class LedgerBalanceAmount extends StatelessWidget {
|
||||
final LedgerAccount account;
|
||||
|
||||
const LedgerBalanceAmount({super.key, required this.account});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Consumer<LedgerBalanceMaskController>(
|
||||
builder: (context, controller, _) {
|
||||
final isMasked = controller.isBalanceMasked(account.ledgerAccountRef);
|
||||
final balance = isMasked
|
||||
? LedgerBalanceFormatter.formatMasked(account)
|
||||
: LedgerBalanceFormatter.format(account);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
balance,
|
||||
style: textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
controller.toggleBalanceMask(account.ledgerAccountRef);
|
||||
},
|
||||
child: Icon(
|
||||
isMasked ? Icons.visibility_off : Icons.visibility,
|
||||
size: 24,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/provider/ledger.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/actions/bar.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class LedgerSourceActions extends StatelessWidget {
|
||||
final String ledgerAccountRef;
|
||||
final VoidCallback onAddFunds;
|
||||
|
||||
const LedgerSourceActions({
|
||||
super.key,
|
||||
required this.ledgerAccountRef,
|
||||
required this.onAddFunds,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ledgerProvider = context.watch<LedgerAccountsProvider>();
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final isBusy =
|
||||
ledgerProvider.isWalletRefreshing(ledgerAccountRef) ||
|
||||
ledgerProvider.isLoading;
|
||||
final hasTarget = ledgerProvider.accounts.any(
|
||||
(a) => a.ledgerAccountRef == ledgerAccountRef,
|
||||
);
|
||||
|
||||
return BalanceActionsBar(
|
||||
isRefreshBusy: isBusy,
|
||||
canRefresh: hasTarget,
|
||||
onRefresh: () {
|
||||
context.read<LedgerAccountsProvider>().refreshBalance(ledgerAccountRef);
|
||||
},
|
||||
onAddFunds: onAddFunds,
|
||||
refreshLabel: loc.refreshBalance,
|
||||
addFundsLabel: loc.addFunds,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/provider/payment/wallets.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/actions/bar.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class WalletSourceActions extends StatelessWidget {
|
||||
final String walletRef;
|
||||
final VoidCallback onAddFunds;
|
||||
|
||||
const WalletSourceActions({
|
||||
super.key,
|
||||
required this.walletRef,
|
||||
required this.onAddFunds,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final walletsProvider = context.watch<WalletsProvider>();
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final isBusy =
|
||||
walletsProvider.isWalletRefreshing(walletRef) ||
|
||||
walletsProvider.isLoading;
|
||||
final hasTarget = walletsProvider.wallets.any((w) => w.id == walletRef);
|
||||
|
||||
return BalanceActionsBar(
|
||||
isRefreshBusy: isBusy,
|
||||
canRefresh: hasTarget,
|
||||
onRefresh: () {
|
||||
context.read<WalletsProvider>().refreshBalance(walletRef);
|
||||
},
|
||||
onAddFunds: onAddFunds,
|
||||
refreshLabel: loc.refreshBalance,
|
||||
addFundsLabel: loc.addFunds,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
||||
import 'package:pshared/models/ledger/account.dart';
|
||||
import 'package:pshared/models/payment/chain_network.dart';
|
||||
import 'package:pshared/models/payment/source_type.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
import 'package:pshared/utils/l10n/chain.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/amount.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/ledger_amount.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/source/actions/ledger.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/source/actions/wallet.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/source/card_layout.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class BalanceSourceCard extends StatelessWidget {
|
||||
final PaymentSourceType _type;
|
||||
final Wallet? _wallet;
|
||||
final LedgerAccount? _ledgerAccount;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback onAddFunds;
|
||||
|
||||
const BalanceSourceCard.wallet({
|
||||
super.key,
|
||||
required Wallet wallet,
|
||||
required this.onTap,
|
||||
required this.onAddFunds,
|
||||
}) : _type = PaymentSourceType.wallet,
|
||||
_wallet = wallet,
|
||||
_ledgerAccount = null;
|
||||
|
||||
const BalanceSourceCard.ledger({
|
||||
super.key,
|
||||
required LedgerAccount account,
|
||||
required this.onTap,
|
||||
required this.onAddFunds,
|
||||
}) : _type = PaymentSourceType.ledger,
|
||||
_wallet = null,
|
||||
_ledgerAccount = account;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => switch (_type) {
|
||||
PaymentSourceType.wallet => _buildWalletCard(context, _wallet!),
|
||||
PaymentSourceType.ledger => _buildLedgerCard(context, _ledgerAccount!),
|
||||
};
|
||||
|
||||
Widget _buildWalletCard(BuildContext context, Wallet wallet) {
|
||||
final networkLabel =
|
||||
(wallet.network == null || wallet.network == ChainNetwork.unspecified)
|
||||
? null
|
||||
: wallet.network!.localizedName(context);
|
||||
final symbol = wallet.tokenSymbol?.trim();
|
||||
|
||||
return BalanceSourceCardLayout(
|
||||
title: wallet.name,
|
||||
subtitle: networkLabel,
|
||||
badge: (symbol == null || symbol.isEmpty) ? null : symbol,
|
||||
onTap: onTap,
|
||||
actions: WalletSourceActions(
|
||||
walletRef: wallet.id,
|
||||
onAddFunds: onAddFunds,
|
||||
),
|
||||
amount: BalanceAmount(
|
||||
wallet: wallet,
|
||||
onToggleMask: () {
|
||||
context.read<WalletsController>().toggleBalanceMask(wallet.id);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLedgerCard(BuildContext context, LedgerAccount account) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final accountName = account.name.trim();
|
||||
final accountCode = account.accountCode.trim();
|
||||
final title = accountName.isNotEmpty ? accountName : loc.paymentTypeLedger;
|
||||
final subtitle = accountCode.isNotEmpty ? accountCode : null;
|
||||
final badge = account.currency.trim().isEmpty
|
||||
? null
|
||||
: account.currency.toUpperCase();
|
||||
|
||||
return BalanceSourceCardLayout(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
badge: badge,
|
||||
onTap: onTap,
|
||||
actions: LedgerSourceActions(
|
||||
ledgerAccountRef: account.ledgerAccountRef,
|
||||
onAddFunds: onAddFunds,
|
||||
),
|
||||
amount: LedgerBalanceAmount(account: account),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/header.dart';
|
||||
|
||||
|
||||
class BalanceSourceCardLayout extends StatelessWidget {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final String? badge;
|
||||
final Widget amount;
|
||||
final Widget actions;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const BalanceSourceCardLayout({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.badge,
|
||||
required this.amount,
|
||||
required this.actions,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Card(
|
||||
color: colorScheme.onSecondary,
|
||||
elevation: WalletCardConfig.elevation,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
|
||||
onTap: onTap,
|
||||
child: SizedBox.expand(
|
||||
child: Padding(
|
||||
padding: WalletCardConfig.contentPadding,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
BalanceHeader(title: title, subtitle: subtitle, badge: badge),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(child: amount),
|
||||
const SizedBox(width: 12),
|
||||
actions,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
||||
import 'package:pshared/models/ledger/account.dart';
|
||||
import 'package:pshared/provider/ledger.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
|
||||
@@ -13,12 +14,16 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
class BalanceWidget extends StatelessWidget {
|
||||
final ValueChanged<Wallet> onTopUp;
|
||||
final ValueChanged<LedgerAccount> onLedgerAddFunds;
|
||||
final ValueChanged<Wallet> onWalletTap;
|
||||
final ValueChanged<LedgerAccount> onLedgerTap;
|
||||
|
||||
const BalanceWidget({
|
||||
super.key,
|
||||
required this.onTopUp,
|
||||
required this.onLedgerAddFunds,
|
||||
required this.onWalletTap,
|
||||
required this.onLedgerTap,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -45,7 +50,9 @@ class BalanceWidget extends StatelessWidget {
|
||||
currentIndex: carousel.index,
|
||||
onIndexChanged: carousel.onPageChanged,
|
||||
onTopUp: onTopUp,
|
||||
onLedgerAddFunds: onLedgerAddFunds,
|
||||
onWalletTap: onWalletTap,
|
||||
onLedgerTap: onLedgerTap,
|
||||
);
|
||||
|
||||
if (wallets.isEmpty && accounts.isEmpty) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/ledger/account.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
@@ -15,6 +16,7 @@ import 'package:pweb/pages/loader.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class AppSpacing {
|
||||
static const double small = 10;
|
||||
static const double medium = 16;
|
||||
@@ -25,14 +27,18 @@ class DashboardPage extends StatefulWidget {
|
||||
final ValueChanged<Recipient> onRecipientSelected;
|
||||
final void Function(PaymentType type) onGoToPaymentWithoutRecipient;
|
||||
final ValueChanged<Wallet> onTopUp;
|
||||
final ValueChanged<LedgerAccount> onLedgerAddFunds;
|
||||
final ValueChanged<Wallet> onWalletTap;
|
||||
final ValueChanged<LedgerAccount> onLedgerTap;
|
||||
|
||||
const DashboardPage({
|
||||
super.key,
|
||||
required this.onRecipientSelected,
|
||||
required this.onGoToPaymentWithoutRecipient,
|
||||
required this.onTopUp,
|
||||
required this.onLedgerAddFunds,
|
||||
required this.onWalletTap,
|
||||
required this.onLedgerTap,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -86,7 +92,9 @@ class _DashboardPageState extends State<DashboardPage> {
|
||||
BalanceWidgetProviders(
|
||||
child: BalanceWidget(
|
||||
onTopUp: widget.onTopUp,
|
||||
onLedgerAddFunds: widget.onLedgerAddFunds,
|
||||
onWalletTap: widget.onWalletTap,
|
||||
onLedgerTap: widget.onLedgerTap,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.small),
|
||||
|
||||
@@ -5,11 +5,11 @@ import 'package:provider/provider.dart';
|
||||
import 'package:pshared/utils/currency.dart';
|
||||
|
||||
import 'package:pweb/controllers/payments/amount_field.dart';
|
||||
import 'package:pweb/models/payment/amount/mode.dart';
|
||||
import 'package:pweb/pages/dashboard/payouts/amount/mode/selector.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class PaymentAmountField extends StatelessWidget {
|
||||
const PaymentAmountField();
|
||||
|
||||
@@ -37,10 +37,6 @@ class PaymentAmountField extends StatelessWidget {
|
||||
labelText: loc.amount,
|
||||
border: const OutlineInputBorder(),
|
||||
prefixText: symbol == null ? null : '$symbol\u00A0',
|
||||
helperText: switch (ui.mode) {
|
||||
PaymentAmountMode.debit => loc.debitAmountLabel,
|
||||
PaymentAmountMode.settlement => loc.expectedSettlementAmountLabel,
|
||||
},
|
||||
),
|
||||
onChanged: ui.handleChanged,
|
||||
),
|
||||
|
||||
@@ -102,7 +102,7 @@ class PaymentFormWidget extends StatelessWidget {
|
||||
children: [
|
||||
detailsHeader,
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
@@ -114,7 +114,7 @@ class PaymentFormWidget extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: _smallSpacing),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:pshared/provider/payment/multiple/quotation.dart';
|
||||
|
||||
import 'package:pweb/controllers/payouts/multiple_payouts.dart';
|
||||
import 'package:pweb/controllers/payouts/payout_verification.dart';
|
||||
import 'package:pweb/utils/payment/payout_verification_flow.dart';
|
||||
import 'package:pweb/utils/payment/verification_flow.dart';
|
||||
import 'package:pweb/widgets/dialogs/payment_status_dialog.dart';
|
||||
|
||||
|
||||
|
||||
@@ -26,15 +26,18 @@ class QuoteStatusCard extends StatelessWidget {
|
||||
});
|
||||
|
||||
static const double _cardRadius = 12;
|
||||
static const double _cardSpacing = 12;
|
||||
static const double _cardSpacing = 8;
|
||||
static const double _iconSize = 18;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final foregroundColor = _resolveForegroundColor(theme, statusType);
|
||||
final elementColor = _resolveElementColor(theme, statusType);
|
||||
final statusStyle = theme.textTheme.bodyMedium?.copyWith(color: elementColor);
|
||||
final statusStyle = theme.textTheme.bodyMedium?.copyWith(
|
||||
color: elementColor,
|
||||
);
|
||||
final helperStyle = theme.textTheme.bodySmall?.copyWith(
|
||||
color: foregroundColor.withValues(alpha: 0.8),
|
||||
);
|
||||
@@ -44,12 +47,10 @@ class QuoteStatusCard extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: _resolveCardColor(theme, statusType),
|
||||
borderRadius: BorderRadius.circular(_cardRadius),
|
||||
border: Border.all(
|
||||
color: elementColor.withValues(alpha: 0.5),
|
||||
),
|
||||
border: Border.all(color: elementColor.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
@@ -59,7 +60,9 @@ class QuoteStatusCard extends StatelessWidget {
|
||||
height: _iconSize,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(foregroundColor),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
foregroundColor,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
@@ -81,19 +84,15 @@ class QuoteStatusCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (canRefresh)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: _cardSpacing),
|
||||
child: showPrimaryRefresh
|
||||
? ElevatedButton(
|
||||
onPressed: canRefresh ? onRefresh : null,
|
||||
child: Text(AppLocalizations.of(context)!.quoteRefresh),
|
||||
)
|
||||
: TextButton(
|
||||
onPressed: canRefresh ? onRefresh : null,
|
||||
child: Text(AppLocalizations.of(context)!.quoteRefresh),
|
||||
),
|
||||
if (canRefresh) ...[
|
||||
const SizedBox(width: _cardSpacing),
|
||||
IconButton(
|
||||
onPressed: onRefresh,
|
||||
tooltip: loc.quoteRefresh,
|
||||
icon: const Icon(Icons.refresh),
|
||||
color: showPrimaryRefresh ? foregroundColor : elementColor,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ class RecipientAvatar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textColor = Theme.of(context).colorScheme.onPrimary;
|
||||
final textColor = Theme.of(context).colorScheme.onSecondary;
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -31,7 +31,7 @@ class RecipientAvatar extends StatelessWidget {
|
||||
CircleAvatar(
|
||||
radius: avatarRadius,
|
||||
backgroundImage: avatarUrl != null ? NetworkImage(avatarUrl!) : null,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryFixed,
|
||||
child: avatarUrl == null
|
||||
? Text(
|
||||
getInitials(name),
|
||||
|
||||
@@ -7,54 +7,56 @@ import 'package:pweb/pages/dashboard/payouts/single/address_book/avatar.dart';
|
||||
class ShortListAddressBookPayout extends StatelessWidget {
|
||||
final List<Recipient> recipients;
|
||||
final ValueChanged<Recipient> onSelected;
|
||||
final Widget? trailing;
|
||||
final Widget? leading;
|
||||
|
||||
const ShortListAddressBookPayout({
|
||||
super.key,
|
||||
required this.recipients,
|
||||
required this.onSelected,
|
||||
this.trailing,
|
||||
this.leading,
|
||||
});
|
||||
|
||||
static const double _avatarRadius = 20;
|
||||
static const double _avatarSize = 80;
|
||||
static const EdgeInsets _padding = EdgeInsets.symmetric(horizontal: 10, vertical: 8);
|
||||
static const EdgeInsets _padding = EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 8,
|
||||
);
|
||||
static const TextStyle _nameStyle = TextStyle(fontSize: 12);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final trailingWidget = trailing;
|
||||
final leadingWidget = leading;
|
||||
final recipientItems = recipients.map((recipient) {
|
||||
return Padding(
|
||||
padding: _padding,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
hoverColor: Theme.of(context).colorScheme.onTertiary,
|
||||
onTap: () => onSelected(recipient),
|
||||
child: SizedBox(
|
||||
height: _avatarSize,
|
||||
width: _avatarSize,
|
||||
child: RecipientAvatar(
|
||||
isVisible: true,
|
||||
name: recipient.name,
|
||||
avatarUrl: recipient.avatarUrl,
|
||||
avatarRadius: _avatarRadius,
|
||||
nameStyle: _nameStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children:
|
||||
recipients.map((recipient) {
|
||||
return Padding(
|
||||
padding: _padding,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
hoverColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
onTap: () => onSelected(recipient),
|
||||
child: SizedBox(
|
||||
height: _avatarSize,
|
||||
width: _avatarSize,
|
||||
child: RecipientAvatar(
|
||||
isVisible: true,
|
||||
name: recipient.name,
|
||||
avatarUrl: recipient.avatarUrl,
|
||||
avatarRadius: _avatarRadius,
|
||||
nameStyle: _nameStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList()
|
||||
..addAll(
|
||||
trailingWidget == null
|
||||
? const []
|
||||
: [Padding(padding: _padding, child: trailingWidget)],
|
||||
),
|
||||
children: [
|
||||
if (leadingWidget != null)
|
||||
Padding(padding: _padding, child: leadingWidget),
|
||||
...recipientItems,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,10 +21,7 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
class AddressBookPayout extends StatefulWidget {
|
||||
final ValueChanged<Recipient> onSelected;
|
||||
|
||||
const AddressBookPayout({
|
||||
super.key,
|
||||
required this.onSelected,
|
||||
});
|
||||
const AddressBookPayout({super.key, required this.onSelected});
|
||||
|
||||
@override
|
||||
State<AddressBookPayout> createState() => _AddressBookPayoutState();
|
||||
@@ -71,6 +68,7 @@ class _AddressBookPayoutState extends State<AddressBookPayout> {
|
||||
provider.setCurrentObject(null);
|
||||
context.pushNamed(PayoutRoutes.addRecipient);
|
||||
}
|
||||
|
||||
final filteredRecipients = filterRecipients(
|
||||
recipients: recipients,
|
||||
query: _query,
|
||||
@@ -81,16 +79,18 @@ class _AddressBookPayoutState extends State<AddressBookPayout> {
|
||||
}
|
||||
|
||||
if (provider.error != null) {
|
||||
return Center(child: Text(loc.notificationError(provider.error ?? loc.noErrorInformation)));
|
||||
return Center(
|
||||
child: Text(
|
||||
loc.notificationError(provider.error ?? loc.noErrorInformation),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: _isExpanded ? _expandedHeight : _collapsedHeight,
|
||||
child: Card(
|
||||
margin: const EdgeInsets.all(_cardMargin),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 4,
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
child: Padding(
|
||||
@@ -105,27 +105,27 @@ class _AddressBookPayoutState extends State<AddressBookPayout> {
|
||||
const SizedBox(height: _spacingBetween),
|
||||
Expanded(
|
||||
child: recipients.isEmpty
|
||||
? Center(
|
||||
child: AddRecipientTile(
|
||||
label: loc.addRecipient,
|
||||
onTap: onAddRecipient,
|
||||
),
|
||||
)
|
||||
: _isExpanded && filteredRecipients.isEmpty
|
||||
? Center(
|
||||
child: AddRecipientTile(
|
||||
label: loc.addRecipient,
|
||||
onTap: onAddRecipient,
|
||||
),
|
||||
)
|
||||
: _isExpanded && filteredRecipients.isEmpty
|
||||
? AddressBookPlaceholder(text: loc.noRecipientsFound)
|
||||
: _isExpanded
|
||||
? LongListAddressBookPayout(
|
||||
filteredRecipients: filteredRecipients,
|
||||
onSelected: widget.onSelected,
|
||||
)
|
||||
: ShortListAddressBookPayout(
|
||||
recipients: recipients,
|
||||
onSelected: widget.onSelected,
|
||||
trailing: AddRecipientTile(
|
||||
label: loc.addRecipient,
|
||||
onTap: onAddRecipient,
|
||||
),
|
||||
? LongListAddressBookPayout(
|
||||
filteredRecipients: filteredRecipients,
|
||||
onSelected: widget.onSelected,
|
||||
)
|
||||
: ShortListAddressBookPayout(
|
||||
recipients: recipients,
|
||||
onSelected: widget.onSelected,
|
||||
leading: AddRecipientTile(
|
||||
label: loc.addRecipient,
|
||||
onTap: onAddRecipient,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -5,19 +5,21 @@ import 'package:pshared/models/recipient/recipient.dart';
|
||||
|
||||
import 'package:pweb/widgets/sidebar/destinations.dart';
|
||||
import 'package:pweb/controllers/payments/page_ui.dart';
|
||||
import 'package:pweb/pages/payout_page/send/page_handlers.dart';
|
||||
import 'package:pweb/utils/payment/page_handlers.dart';
|
||||
import 'package:pweb/pages/payout_page/send/page_view.dart';
|
||||
|
||||
|
||||
class PaymentPage extends StatefulWidget {
|
||||
final ValueChanged<Recipient?>? onBack;
|
||||
final PaymentType? initialPaymentType;
|
||||
final String? initialDestinationLedgerAccountRef;
|
||||
final PayoutDestination fallbackDestination;
|
||||
|
||||
const PaymentPage({
|
||||
super.key,
|
||||
this.onBack,
|
||||
this.initialPaymentType,
|
||||
this.initialDestinationLedgerAccountRef,
|
||||
this.fallbackDestination = PayoutDestination.dashboard,
|
||||
});
|
||||
|
||||
@@ -34,7 +36,11 @@ class _PaymentPageState extends State<PaymentPage> {
|
||||
_uiController = PaymentPageUiController();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => initializePaymentPage(context, widget.initialPaymentType),
|
||||
(_) => initializePaymentPage(
|
||||
context,
|
||||
widget.initialPaymentType,
|
||||
destinationLedgerAccountRef: widget.initialDestinationLedgerAccountRef,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
import 'package:pshared/provider/payment/flow.dart';
|
||||
import 'package:pshared/provider/payment/quotation/quotation.dart';
|
||||
import 'package:pshared/provider/recipient/pmethods.dart';
|
||||
import 'package:pshared/provider/recipient/provider.dart';
|
||||
@@ -14,6 +15,7 @@ import 'package:pweb/controllers/payments/page_ui.dart';
|
||||
import 'package:pweb/controllers/payouts/payout_verification.dart';
|
||||
import 'package:pweb/models/state/control_state.dart';
|
||||
|
||||
|
||||
class PaymentPageView extends StatelessWidget {
|
||||
final PaymentPageUiController uiController;
|
||||
final ValueChanged<Recipient?>? onBack;
|
||||
@@ -47,6 +49,7 @@ class PaymentPageView extends StatelessWidget {
|
||||
final uiController = context.watch<PaymentPageUiController>();
|
||||
final methodsProvider = context.watch<PaymentMethodsProvider>();
|
||||
final recipientProvider = context.watch<RecipientsProvider>();
|
||||
final flowProvider = context.watch<PaymentFlowProvider>();
|
||||
final quotationProvider = context.watch<QuotationProvider>();
|
||||
final verificationController = context
|
||||
.watch<PayoutVerificationController>();
|
||||
@@ -58,10 +61,12 @@ class PaymentPageView extends StatelessWidget {
|
||||
recipients: recipientProvider.recipients,
|
||||
query: uiController.query,
|
||||
);
|
||||
final hasDestinationSelection =
|
||||
flowProvider.selectedPaymentData != null;
|
||||
final sendState =
|
||||
verificationController.isCooldownActiveFor(verificationContextKey)
|
||||
? ControlState.disabled
|
||||
: (recipient == null
|
||||
: (!hasDestinationSelection
|
||||
? ControlState.disabled
|
||||
: ControlState.enabled);
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/recipient/payment_method_draft.dart';
|
||||
|
||||
import 'package:pweb/pages/address_book/form/widgets/payment_methods/panel.dart';
|
||||
import 'package:pweb/pages/payout_page/send/widgets/payment_info/header.dart';
|
||||
import 'package:pweb/models/state/control_state.dart';
|
||||
import 'package:pweb/models/state/visibility.dart';
|
||||
import 'package:pweb/utils/dimensions.dart';
|
||||
|
||||
|
||||
class PaymentInfoManualDetailsSection extends StatelessWidget {
|
||||
final AppDimensions dimensions;
|
||||
final String title;
|
||||
final VisibilityState titleVisibility;
|
||||
final PaymentMethodData data;
|
||||
|
||||
const PaymentInfoManualDetailsSection({
|
||||
super.key,
|
||||
required this.dimensions,
|
||||
required this.title,
|
||||
required this.titleVisibility,
|
||||
required this.data,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entry = RecipientMethodDraft(type: data.type, data: data);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
PaymentInfoHeader(
|
||||
dimensions: dimensions,
|
||||
title: title,
|
||||
visibility: titleVisibility,
|
||||
),
|
||||
PaymentMethodPanel(
|
||||
selectedType: data.type,
|
||||
selectedIndex: 0,
|
||||
entries: [entry],
|
||||
onRemove: (_) {},
|
||||
onChanged: (_, ignored) {},
|
||||
editState: ControlState.disabled,
|
||||
deleteVisibility: VisibilityState.hidden,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:pshared/provider/payment/flow.dart';
|
||||
|
||||
import 'package:pweb/pages/payout_page/send/widgets/payment_info/methods_section.dart';
|
||||
import 'package:pweb/pages/payout_page/send/widgets/payment_info/methods_state.dart';
|
||||
import 'package:pweb/pages/payout_page/send/widgets/payment_info/manual_details.dart';
|
||||
import 'package:pweb/pages/payout_page/send/widgets/payment_info/no_methods.dart';
|
||||
import 'package:pweb/pages/payout_page/send/widgets/payment_info/no_recipient.dart';
|
||||
import 'package:pweb/models/state/visibility.dart';
|
||||
@@ -35,8 +36,9 @@ class PaymentInfoSection extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final flowProvider = context.watch<PaymentFlowProvider>();
|
||||
final manualData = flowProvider.manualPaymentData;
|
||||
|
||||
if (!flowProvider.hasRecipient) {
|
||||
if (!flowProvider.hasRecipient && manualData == null) {
|
||||
return PaymentInfoNoRecipientSection(
|
||||
dimensions: dimensions,
|
||||
title: loc.paymentInfo,
|
||||
@@ -44,6 +46,15 @@ class PaymentInfoSection extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
if (!flowProvider.hasRecipient && manualData != null) {
|
||||
return PaymentInfoManualDetailsSection(
|
||||
dimensions: dimensions,
|
||||
title: loc.paymentInfo,
|
||||
titleVisibility: titleVisibility,
|
||||
data: manualData,
|
||||
);
|
||||
}
|
||||
|
||||
final methods = flowProvider.methodsForRecipient;
|
||||
final types = visiblePaymentTypes;
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ class RecipientSection extends StatelessWidget {
|
||||
ShortListAddressBookPayout(
|
||||
recipients: recipientProvider.recipients,
|
||||
onSelected: onRecipientSelected,
|
||||
trailing: AddRecipientTile(
|
||||
leading: AddRecipientTile(
|
||||
label: loc.addRecipient,
|
||||
onTap: onAddRecipient,
|
||||
),
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
import 'package:pshared/provider/payment/flow.dart';
|
||||
import 'package:pshared/provider/recipient/provider.dart';
|
||||
|
||||
import 'package:pweb/pages/payout_page/send/widgets/payment_info/section.dart';
|
||||
@@ -46,24 +49,30 @@ class PaymentRecipientDetailsCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final flowProvider = context.watch<PaymentFlowProvider>();
|
||||
final isRecipientSelectionLocked =
|
||||
!flowProvider.hasRecipient && flowProvider.manualPaymentData != null;
|
||||
|
||||
return PaymentSectionCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RecipientSection(
|
||||
recipient: recipient,
|
||||
dimensions: dimensions,
|
||||
recipientProvider: recipientProvider,
|
||||
searchQuery: searchQuery,
|
||||
filteredRecipients: filteredRecipients,
|
||||
searchController: searchController,
|
||||
searchFocusNode: searchFocusNode,
|
||||
onSearchChanged: onSearchChanged,
|
||||
onRecipientSelected: onRecipientSelected,
|
||||
onRecipientCleared: onRecipientCleared,
|
||||
onAddRecipient: onAddRecipient,
|
||||
),
|
||||
SizedBox(height: dimensions.paddingMedium),
|
||||
if (!isRecipientSelectionLocked) ...[
|
||||
RecipientSection(
|
||||
recipient: recipient,
|
||||
dimensions: dimensions,
|
||||
recipientProvider: recipientProvider,
|
||||
searchQuery: searchQuery,
|
||||
filteredRecipients: filteredRecipients,
|
||||
searchController: searchController,
|
||||
searchFocusNode: searchFocusNode,
|
||||
onSearchChanged: onSearchChanged,
|
||||
onRecipientSelected: onRecipientSelected,
|
||||
onRecipientCleared: onRecipientCleared,
|
||||
onAddRecipient: onAddRecipient,
|
||||
),
|
||||
SizedBox(height: dimensions.paddingMedium),
|
||||
],
|
||||
PaymentInfoSection(
|
||||
dimensions: dimensions,
|
||||
titleVisibility: VisibilityState.hidden,
|
||||
|
||||
@@ -2,9 +2,10 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/controllers/payment/source.dart';
|
||||
|
||||
import 'package:pweb/pages/payout_page/wallet/edit/buttons/send.dart';
|
||||
import 'package:pweb/pages/payout_page/wallet/edit/buttons/top_up.dart';
|
||||
import 'package:pshared/provider/payment/wallets.dart';
|
||||
|
||||
|
||||
class ButtonsWalletWidget extends StatelessWidget {
|
||||
@@ -12,25 +13,20 @@ class ButtonsWalletWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<WalletsProvider>();
|
||||
|
||||
if (provider.wallets.isEmpty) return const SizedBox.shrink();
|
||||
final source = context.watch<PaymentSourceController>();
|
||||
if (!source.hasSources) return const SizedBox.shrink();
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SendPayoutButton(),
|
||||
),
|
||||
VerticalDivider(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
thickness: 1,
|
||||
width: 10,
|
||||
),
|
||||
Expanded(
|
||||
child: TopUpButton(),
|
||||
),
|
||||
],
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Expanded(child: SendPayoutButton()),
|
||||
VerticalDivider(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
thickness: 1,
|
||||
width: 10,
|
||||
),
|
||||
Expanded(child: TopUpButton()),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@ import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
||||
import 'package:pshared/controllers/payment/source.dart';
|
||||
import 'package:pshared/models/payment/source_type.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
|
||||
import 'package:pweb/app/router/payout_routes.dart';
|
||||
@@ -18,24 +19,27 @@ class SendPayoutButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
return ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
shadowColor: null,
|
||||
elevation: 0,
|
||||
),
|
||||
onPressed: () {
|
||||
final wallets = context.read<WalletsController>();
|
||||
final wallet = wallets.selectedWallet;
|
||||
final source = context.watch<PaymentSourceController>();
|
||||
|
||||
if (wallet != null) {
|
||||
context.pushNamed(
|
||||
PayoutRoutes.payment,
|
||||
queryParameters: PayoutRoutes.buildQueryParameters(
|
||||
paymentType: PaymentType.wallet,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
final sourceType = source.selectedType;
|
||||
final paymentType = switch (sourceType) {
|
||||
PaymentSourceType.wallet => PaymentType.wallet,
|
||||
PaymentSourceType.ledger => PaymentType.ledger,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
return ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(shadowColor: null, elevation: 0),
|
||||
onPressed: paymentType == null
|
||||
? null
|
||||
: () {
|
||||
context.pushNamed(
|
||||
PayoutRoutes.payment,
|
||||
queryParameters: PayoutRoutes.buildQueryParameters(
|
||||
paymentType: paymentType,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(loc.payoutNavSendPayout),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,35 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
||||
import 'package:pshared/controllers/payment/source.dart';
|
||||
import 'package:pshared/models/payment/source_type.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/provider/recipient/provider.dart';
|
||||
|
||||
import 'package:pweb/app/router/payout_routes.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class TopUpButton extends StatelessWidget{
|
||||
class TopUpButton extends StatelessWidget {
|
||||
const TopUpButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
final source = context.watch<PaymentSourceController>();
|
||||
final selectedType = source.selectedType;
|
||||
final selectedLedger = source.selectedLedgerAccount;
|
||||
final canTopUp =
|
||||
selectedType == PaymentSourceType.wallet ||
|
||||
(selectedType == PaymentSourceType.ledger && selectedLedger != null);
|
||||
|
||||
return ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
shadowColor: null,
|
||||
elevation: 0,
|
||||
),
|
||||
onPressed: () {
|
||||
final wallet = context.read<WalletsController>().selectedWallet;
|
||||
if (wallet == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(loc.noWalletSelected)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
context.pushToWalletTopUp();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(shadowColor: null, elevation: 0),
|
||||
onPressed: !canTopUp
|
||||
? null
|
||||
: () {
|
||||
if (selectedType == PaymentSourceType.wallet) {
|
||||
context.pushToWalletTopUp();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedType == PaymentSourceType.ledger &&
|
||||
selectedLedger != null) {
|
||||
context.read<RecipientsProvider>().setCurrentObject(null);
|
||||
context.pushNamed(
|
||||
PayoutRoutes.payment,
|
||||
queryParameters: PayoutRoutes.buildQueryParameters(
|
||||
paymentType: PaymentType.ledger,
|
||||
destinationLedgerAccountRef:
|
||||
selectedLedger.ledgerAccountRef,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(loc.topUpBalance),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,55 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/amount.dart';
|
||||
import 'package:pweb/widgets/refresh_balance/wallet.dart';
|
||||
import 'package:pshared/controllers/payment/source.dart';
|
||||
|
||||
import 'package:pweb/pages/payout_page/wallet/edit/fields/ledger/section.dart';
|
||||
import 'package:pweb/pages/payout_page/wallet/edit/fields/wallet/wallet_section.dart';
|
||||
|
||||
class WalletEditFields extends StatelessWidget {
|
||||
const WalletEditFields({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<WalletsController>(
|
||||
builder: (context, controller, _) {
|
||||
final wallet = controller.selectedWallet;
|
||||
|
||||
if (wallet == null) {
|
||||
return SizedBox.shrink();
|
||||
return Consumer<PaymentSourceController>(
|
||||
builder: (context, sourceController, _) {
|
||||
final wallet = sourceController.selectedWallet;
|
||||
if (wallet != null) {
|
||||
return WalletSection(wallet: wallet);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: BalanceAmount(
|
||||
wallet: wallet,
|
||||
onToggleMask: () => controller.toggleBalanceMask(wallet.id),
|
||||
),
|
||||
),
|
||||
WalletBalanceRefreshButton(walletRef: wallet.id),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(wallet.walletUserID, style: Theme.of(context).textTheme.bodyLarge),
|
||||
IconButton(
|
||||
icon: Icon(Icons.copy),
|
||||
iconSize: 18,
|
||||
onPressed: () => Clipboard.setData(ClipboardData(text: wallet.walletUserID)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
final ledger = sourceController.selectedLedgerAccount;
|
||||
if (ledger != null) {
|
||||
return LedgerSection(ledger: ledger);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:pshared/models/ledger/account.dart';
|
||||
import 'package:pshared/utils/currency.dart';
|
||||
import 'package:pshared/utils/money.dart';
|
||||
|
||||
|
||||
class LedgerBalanceFormatter {
|
||||
const LedgerBalanceFormatter._();
|
||||
|
||||
static String format(LedgerAccount account) {
|
||||
final money = account.balance?.balance;
|
||||
if (money == null) return '--';
|
||||
|
||||
final amount = parseMoneyAmount(money.amount, fallback: double.nan);
|
||||
if (amount.isNaN) {
|
||||
return '${money.amount} ${money.currency}';
|
||||
}
|
||||
|
||||
try {
|
||||
final currency = currencyStringToCode(money.currency);
|
||||
final symbol = currencyCodeToSymbol(currency);
|
||||
if (symbol.trim().isEmpty) {
|
||||
return '${amountToString(amount)} ${money.currency}';
|
||||
}
|
||||
return '${amountToString(amount)} $symbol';
|
||||
} catch (_) {
|
||||
return '${amountToString(amount)} ${money.currency}';
|
||||
}
|
||||
}
|
||||
|
||||
static String formatMasked(LedgerAccount account) {
|
||||
final currency = account.currency.trim();
|
||||
if (currency.isEmpty) return '••••';
|
||||
|
||||
try {
|
||||
final symbol = currencyCodeToSymbol(currencyStringToCode(currency));
|
||||
if (symbol.trim().isEmpty) {
|
||||
return '•••• $currency';
|
||||
}
|
||||
return '•••• $symbol';
|
||||
} catch (_) {
|
||||
return '•••• $currency';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class LedgerBalanceRow extends StatelessWidget {
|
||||
final String balance;
|
||||
final bool isMasked;
|
||||
final VoidCallback onToggleMask;
|
||||
|
||||
const LedgerBalanceRow({
|
||||
super.key,
|
||||
required this.balance,
|
||||
required this.isMasked,
|
||||
required this.onToggleMask,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
balance,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
onTap: onToggleMask,
|
||||
child: Icon(
|
||||
isMasked ? Icons.visibility_off : Icons.visibility,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/controllers/balance_mask/ledger_accounts.dart';
|
||||
import 'package:pshared/models/ledger/account.dart';
|
||||
|
||||
import 'package:pweb/pages/payout_page/wallet/edit/fields/ledger/balance_formatter.dart';
|
||||
import 'package:pweb/pages/payout_page/wallet/edit/fields/ledger/balance_row.dart';
|
||||
import 'package:pweb/pages/payout_page/wallet/edit/fields/shared/copyable_value_row.dart';
|
||||
import 'package:pweb/widgets/refresh_balance/ledger.dart';
|
||||
|
||||
|
||||
class LedgerSection extends StatelessWidget {
|
||||
final LedgerAccount ledger;
|
||||
|
||||
const LedgerSection({super.key, required this.ledger});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<LedgerBalanceMaskController>(
|
||||
builder: (context, balanceMask, _) {
|
||||
final isMasked = balanceMask.isBalanceMasked(ledger.ledgerAccountRef);
|
||||
final accountCode = ledger.accountCode.trim();
|
||||
final hasAccountCode = accountCode.isNotEmpty;
|
||||
final balance = isMasked
|
||||
? LedgerBalanceFormatter.formatMasked(ledger)
|
||||
: LedgerBalanceFormatter.format(ledger);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: LedgerBalanceRow(
|
||||
balance: balance,
|
||||
isMasked: isMasked,
|
||||
onToggleMask: () {
|
||||
balanceMask.toggleBalanceMask(ledger.ledgerAccountRef);
|
||||
},
|
||||
),
|
||||
),
|
||||
LedgerBalanceRefreshButton(
|
||||
ledgerAccountRef: ledger.ledgerAccountRef,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CopyableValueRow(
|
||||
value: hasAccountCode ? accountCode : '-',
|
||||
canCopy: hasAccountCode,
|
||||
onCopy: hasAccountCode
|
||||
? () {
|
||||
Clipboard.setData(ClipboardData(text: accountCode));
|
||||
}
|
||||
: null,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
wrapValueWithFlexible: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class CopyableValueRow extends StatelessWidget {
|
||||
final String value;
|
||||
final bool canCopy;
|
||||
final VoidCallback? onCopy;
|
||||
final TextOverflow overflow;
|
||||
final bool wrapValueWithFlexible;
|
||||
|
||||
const CopyableValueRow({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.canCopy,
|
||||
required this.onCopy,
|
||||
this.overflow = TextOverflow.visible,
|
||||
this.wrapValueWithFlexible = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final valueText = Text(
|
||||
value,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
overflow: overflow,
|
||||
);
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (wrapValueWithFlexible) Flexible(child: valueText) else valueText,
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy),
|
||||
iconSize: 18,
|
||||
onPressed: canCopy ? onCopy : null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
|
||||
import 'package:pweb/pages/dashboard/buttons/balance/amount.dart';
|
||||
import 'package:pweb/pages/payout_page/wallet/edit/fields/shared/copyable_value_row.dart';
|
||||
import 'package:pweb/widgets/refresh_balance/wallet.dart';
|
||||
|
||||
|
||||
class WalletSection extends StatelessWidget {
|
||||
final Wallet wallet;
|
||||
|
||||
const WalletSection({super.key, required this.wallet});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final depositAddress = wallet.depositAddress?.trim();
|
||||
final hasDepositAddress =
|
||||
depositAddress != null && depositAddress.isNotEmpty;
|
||||
final copyAddress = hasDepositAddress ? depositAddress : '';
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: BalanceAmount(
|
||||
wallet: wallet,
|
||||
onToggleMask: () {
|
||||
context.read<WalletsController>().toggleBalanceMask(
|
||||
wallet.id,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
WalletBalanceRefreshButton(walletRef: wallet.id),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CopyableValueRow(
|
||||
value: hasDepositAddress ? depositAddress : '-',
|
||||
canCopy: hasDepositAddress,
|
||||
onCopy: hasDepositAddress
|
||||
? () {
|
||||
Clipboard.setData(ClipboardData(text: copyAddress));
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
||||
import 'package:pshared/controllers/payment/source.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
@@ -12,15 +12,20 @@ class WalletEditHeader extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = context.watch<WalletsController>();
|
||||
final controller = context.watch<PaymentSourceController>();
|
||||
final wallet = controller.selectedWallet;
|
||||
final ledger = controller.selectedLedgerAccount;
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
|
||||
if (wallet == null) {
|
||||
if (wallet == null && ledger == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final theme = Theme.of(context);
|
||||
final title = wallet != null
|
||||
? loc.paymentTypeCryptoWallet
|
||||
: loc.paymentTypeLedger;
|
||||
final subtitle = wallet?.tokenSymbol;
|
||||
|
||||
return Row(
|
||||
spacing: 8,
|
||||
@@ -32,14 +37,14 @@ class WalletEditHeader extends StatelessWidget {
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
loc.paymentTypeCryptoWallet,
|
||||
style: theme.textTheme.headlineMedium!.copyWith(
|
||||
title,
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (wallet.tokenSymbol != null)
|
||||
if (subtitle != null && subtitle.trim().isNotEmpty)
|
||||
Text(
|
||||
wallet.tokenSymbol!,
|
||||
subtitle,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
||||
@@ -2,17 +2,16 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/controllers/balance_mask/wallets.dart';
|
||||
import 'package:pshared/controllers/payment/source.dart';
|
||||
|
||||
import 'package:pweb/pages/payout_page/wallet/edit/buttons/buttons.dart';
|
||||
import 'package:pweb/pages/payout_page/wallet/edit/fields.dart';
|
||||
import 'package:pweb/pages/payout_page/wallet/edit/header.dart';
|
||||
import 'package:pweb/pages/payout_page/wallet/history/history.dart';
|
||||
import 'package:pweb/utils/dimensions.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
//TODO make this page more generic and reusable
|
||||
class WalletEditPage extends StatelessWidget {
|
||||
final VoidCallback onBack;
|
||||
|
||||
@@ -23,11 +22,11 @@ class WalletEditPage extends StatelessWidget {
|
||||
final dimensions = AppDimensions();
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
|
||||
return Consumer<WalletsController>(
|
||||
return Consumer<PaymentSourceController>(
|
||||
builder: (context, controller, child) {
|
||||
final wallet = controller.selectedWallet;
|
||||
final sourceType = controller.selectedType;
|
||||
|
||||
if (wallet == null) {
|
||||
if (sourceType == null) {
|
||||
return Center(child: Text(loc.noWalletSelected));
|
||||
}
|
||||
|
||||
@@ -36,11 +35,15 @@ class WalletEditPage extends StatelessWidget {
|
||||
child: Column(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: dimensions.maxContentWidth),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: dimensions.maxContentWidth,
|
||||
),
|
||||
child: Material(
|
||||
elevation: dimensions.elevationSmall,
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
borderRadius: BorderRadius.circular(dimensions.borderRadiusMedium),
|
||||
borderRadius: BorderRadius.circular(
|
||||
dimensions.borderRadiusMedium,
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(dimensions.paddingLarge),
|
||||
child: SingleChildScrollView(
|
||||
@@ -55,19 +58,12 @@ class WalletEditPage extends StatelessWidget {
|
||||
WalletEditFields(),
|
||||
const SizedBox(height: 24),
|
||||
ButtonsWalletWidget(),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: WalletHistory(wallet: wallet),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -2,118 +2,87 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/models/payment/wallet.dart';
|
||||
import 'package:pshared/models/payment/source_type.dart';
|
||||
import 'package:pshared/provider/payment/payments.dart';
|
||||
|
||||
import 'package:pweb/pages/payout_page/wallet/history/filters.dart';
|
||||
import 'package:pweb/pages/payout_page/wallet/history/table.dart';
|
||||
import 'package:pweb/controllers/operations/wallet_transactions.dart';
|
||||
import 'package:pweb/providers/wallet_transactions.dart';
|
||||
import 'package:pweb/controllers/operations/report_operations.dart';
|
||||
import 'package:pweb/models/state/load_more_state.dart';
|
||||
import 'package:pweb/pages/report/cards/list.dart';
|
||||
import 'package:pweb/pages/report/operations/actions.dart';
|
||||
import 'package:pweb/pages/report/operations/states/error.dart';
|
||||
import 'package:pweb/pages/report/operations/states/loading.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class WalletHistory extends StatefulWidget {
|
||||
final Wallet wallet;
|
||||
class WalletHistory extends StatelessWidget {
|
||||
final String sourceRef;
|
||||
final PaymentSourceType sourceType;
|
||||
final List<String> sourceRefs;
|
||||
|
||||
const WalletHistory({super.key, required this.wallet});
|
||||
|
||||
@override
|
||||
State<WalletHistory> createState() => _WalletHistoryState();
|
||||
}
|
||||
|
||||
class _WalletHistoryState extends State<WalletHistory> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant WalletHistory oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.wallet.id != widget.wallet.id) {
|
||||
_load();
|
||||
}
|
||||
}
|
||||
|
||||
void _load() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context
|
||||
.read<WalletTransactionsProvider>()
|
||||
.load(walletId: widget.wallet.id);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pickRange() async {
|
||||
final provider = context.read<WalletTransactionsController>();
|
||||
final now = DateTime.now();
|
||||
final initial = provider.dateRange ??
|
||||
DateTimeRange(
|
||||
start: now.subtract(const Duration(days: 30)),
|
||||
end: now,
|
||||
);
|
||||
|
||||
final picked = await showDateRangePicker(
|
||||
context: context,
|
||||
firstDate: now.subtract(const Duration(days: 365)),
|
||||
lastDate: now.add(const Duration(days: 1)),
|
||||
initialDateRange: initial,
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
provider.setDateRange(picked);
|
||||
}
|
||||
}
|
||||
const WalletHistory({
|
||||
super.key,
|
||||
required this.sourceRef,
|
||||
required this.sourceType,
|
||||
required this.sourceRefs,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProxyProvider<
|
||||
PaymentsProvider,
|
||||
ReportOperationsController
|
||||
>(
|
||||
create: (_) => ReportOperationsController(),
|
||||
update: (_, payments, controller) => controller!
|
||||
..update(
|
||||
payments,
|
||||
sourceType: sourceType,
|
||||
sourceRef: sourceRef,
|
||||
sourceRefs: sourceRefs,
|
||||
),
|
||||
child: const _WalletHistoryContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WalletHistoryContent extends StatelessWidget {
|
||||
const _WalletHistoryContent();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final loc = AppLocalizations.of(context)!;
|
||||
|
||||
return Consumer<WalletTransactionsController>(
|
||||
builder: (context, provider, child) {
|
||||
if (provider.isLoading) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
return Consumer<ReportOperationsController>(
|
||||
builder: (context, controller, child) {
|
||||
if (controller.isLoading) {
|
||||
return const OperationHistoryLoading();
|
||||
}
|
||||
|
||||
if (controller.error != null) {
|
||||
final message =
|
||||
controller.error?.toString() ?? loc.noErrorInformation;
|
||||
return OperationHistoryError(
|
||||
message: loc.notificationError(message),
|
||||
retryLabel: loc.retry,
|
||||
onRetry: controller.refresh,
|
||||
);
|
||||
}
|
||||
|
||||
if (provider.error != null) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
loc.failedToLoadHistory,
|
||||
style: theme.textTheme.titleMedium!
|
||||
.copyWith(color: theme.colorScheme.error),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(loc.notificationError(provider.error ?? loc.noErrorInformation)),
|
||||
const SizedBox(height: 8),
|
||||
OutlinedButton(
|
||||
onPressed: _load,
|
||||
child: Text(loc.retry),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final transactions = provider.filteredTransactions;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
WalletHistoryFilters(
|
||||
provider: provider,
|
||||
onPickRange: _pickRange,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
WalletTransactionsTable(transactions: transactions),
|
||||
],
|
||||
final hasLoadMore = controller.loadMoreState != LoadMoreState.hidden;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
OperationsCardsList(
|
||||
operations: controller.filteredOperations,
|
||||
onTap: (operation) => openPaymentDetails(context, operation),
|
||||
loadMoreState: controller.loadMoreState,
|
||||
onLoadMore: hasLoadMore ? controller.loadMore : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -13,7 +13,6 @@ import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
class PaymentDetailsContent extends StatelessWidget {
|
||||
final Payment payment;
|
||||
final VoidCallback onBack;
|
||||
final VoidCallback? onDownloadAct;
|
||||
final bool Function(PaymentExecutionOperation operation)?
|
||||
canDownloadOperationDocument;
|
||||
final ValueChanged<PaymentExecutionOperation>? onDownloadOperationDocument;
|
||||
@@ -22,7 +21,6 @@ class PaymentDetailsContent extends StatelessWidget {
|
||||
super.key,
|
||||
required this.payment,
|
||||
required this.onBack,
|
||||
this.onDownloadAct,
|
||||
this.canDownloadOperationDocument,
|
||||
this.onDownloadOperationDocument,
|
||||
});
|
||||
@@ -37,7 +35,7 @@ class PaymentDetailsContent extends StatelessWidget {
|
||||
children: [
|
||||
PaymentDetailsHeader(title: loc.paymentInfo, onBack: onBack),
|
||||
const SizedBox(height: 16),
|
||||
PaymentSummaryCard(payment: payment, onDownloadAct: onDownloadAct),
|
||||
PaymentSummaryCard(payment: payment),
|
||||
const SizedBox(height: 16),
|
||||
PaymentDetailsSections(
|
||||
payment: payment,
|
||||
|
||||
@@ -64,17 +64,6 @@ class _PaymentDetailsView extends StatelessWidget {
|
||||
return PaymentDetailsContent(
|
||||
payment: payment,
|
||||
onBack: () => _handleBack(context),
|
||||
onDownloadAct: controller.canDownload
|
||||
? () {
|
||||
final request = controller.primaryOperationDocumentRequest;
|
||||
if (request == null) return;
|
||||
downloadPaymentAct(
|
||||
context,
|
||||
gatewayService: request.gatewayService,
|
||||
operationRef: request.operationRef,
|
||||
);
|
||||
}
|
||||
: null,
|
||||
canDownloadOperationDocument:
|
||||
controller.canDownloadOperationDocument,
|
||||
onDownloadOperationDocument: (operation) {
|
||||
|
||||
@@ -44,12 +44,12 @@ class PaymentOperationsSection extends StatelessWidget {
|
||||
);
|
||||
if (i < operations.length - 1) {
|
||||
children.addAll([
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 10),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: Theme.of(context).dividerColor.withAlpha(20),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 10),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,11 @@ import 'package:pweb/utils/clipboard.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class PaymentSummaryCard extends StatelessWidget {
|
||||
final Payment payment;
|
||||
final VoidCallback? onDownloadAct;
|
||||
|
||||
const PaymentSummaryCard({
|
||||
super.key,
|
||||
required this.payment,
|
||||
this.onDownloadAct,
|
||||
});
|
||||
const PaymentSummaryCard({super.key, required this.payment});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -93,14 +89,6 @@ class PaymentSummaryCard extends StatelessWidget {
|
||||
text: feeText,
|
||||
muted: true,
|
||||
),
|
||||
if (onDownloadAct != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
OutlinedButton.icon(
|
||||
onPressed: onDownloadAct,
|
||||
icon: const Icon(Icons.download),
|
||||
label: Text(loc.downloadAct),
|
||||
),
|
||||
],
|
||||
if (showPaymentId) ...[
|
||||
const SizedBox(height: 16),
|
||||
Divider(color: theme.dividerColor.withAlpha(35), height: 1),
|
||||
|
||||
@@ -13,8 +13,9 @@ import 'package:pshared/utils/payment/quote_helpers.dart';
|
||||
|
||||
import 'package:pweb/models/payment/multiple_payouts/csv_row.dart';
|
||||
import 'package:pweb/models/payment/multiple_payouts/state.dart';
|
||||
import 'package:pweb/utils/payment/multiple_csv_parser.dart';
|
||||
import 'package:pweb/utils/payment/multiple_intent_builder.dart';
|
||||
import 'package:pweb/utils/payment/multiple/csv_parser.dart';
|
||||
import 'package:pweb/utils/payment/multiple/intent_builder.dart';
|
||||
|
||||
|
||||
class MultiplePayoutsProvider extends ChangeNotifier {
|
||||
final MultipleCsvParser _csvParser;
|
||||
|
||||
@@ -13,6 +13,7 @@ class WalletTransactionsProvider extends ChangeNotifier {
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
String? _walletId;
|
||||
int _loadSeq = 0;
|
||||
|
||||
List<WalletTransaction> get transactions => List.unmodifiable(_transactions);
|
||||
bool get isLoading => _isLoading;
|
||||
@@ -20,18 +21,28 @@ class WalletTransactionsProvider extends ChangeNotifier {
|
||||
String? get walletId => _walletId;
|
||||
|
||||
Future<void> load({String? walletId}) async {
|
||||
final targetWalletId = walletId ?? _walletId;
|
||||
final requestSeq = ++_loadSeq;
|
||||
_walletId = targetWalletId;
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_walletId = walletId ?? _walletId;
|
||||
_transactions = await _service.fetchHistory(walletId: _walletId);
|
||||
final fetched = await _service.fetchHistory(walletId: targetWalletId);
|
||||
if (requestSeq != _loadSeq) return;
|
||||
|
||||
_transactions = targetWalletId == null
|
||||
? fetched
|
||||
: fetched.where((tx) => tx.walletId == targetWalletId).toList();
|
||||
} catch (e) {
|
||||
if (requestSeq != _loadSeq) return;
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
if (requestSeq == _loadSeq) {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:pshared/models/payment/methods/ledger.dart';
|
||||
import 'package:pshared/models/payment/type.dart';
|
||||
import 'package:pshared/models/recipient/recipient.dart';
|
||||
import 'package:pshared/provider/payment/flow.dart';
|
||||
@@ -17,14 +18,30 @@ import 'package:pweb/widgets/dialogs/payment_status_dialog.dart';
|
||||
import 'package:pweb/controllers/payments/page.dart';
|
||||
import 'package:pweb/controllers/payments/page_ui.dart';
|
||||
import 'package:pweb/controllers/payouts/payout_verification.dart';
|
||||
import 'package:pweb/utils/payment/payout_verification_flow.dart';
|
||||
import 'package:pweb/utils/payment/verification_flow.dart';
|
||||
|
||||
|
||||
void initializePaymentPage(
|
||||
BuildContext context,
|
||||
PaymentType? initialPaymentType,
|
||||
) {
|
||||
PaymentType? initialPaymentType, {
|
||||
String? destinationLedgerAccountRef,
|
||||
}) {
|
||||
final flowProvider = context.read<PaymentFlowProvider>();
|
||||
final recipientsProvider = context.read<RecipientsProvider>();
|
||||
|
||||
flowProvider.setPreferredType(initialPaymentType);
|
||||
|
||||
final destinationRef = destinationLedgerAccountRef?.trim();
|
||||
if (destinationRef != null && destinationRef.isNotEmpty) {
|
||||
recipientsProvider.setCurrentObject(null);
|
||||
flowProvider.setPreferredType(PaymentType.ledger);
|
||||
flowProvider.setManualPaymentData(
|
||||
LedgerPaymentMethod(ledgerAccountRef: destinationRef),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
flowProvider.setManualPaymentData(null);
|
||||
}
|
||||
|
||||
void handleSearchChanged(PaymentPageUiController uiController, String query) {
|
||||
@@ -1,12 +1,13 @@
|
||||
import 'package:pshared/models/payment/operation_document.dart';
|
||||
import 'package:pshared/models/payment/operation.dart';
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
import 'package:pshared/models/payment/state.dart';
|
||||
import 'package:pshared/models/payment/status.dart';
|
||||
import 'package:pshared/utils/money.dart';
|
||||
|
||||
import 'package:pweb/models/report/operation/document.dart';
|
||||
import 'package:pweb/utils/report/operations/document_rule.dart';
|
||||
|
||||
|
||||
OperationItem mapPaymentToOperation(Payment payment) {
|
||||
final debit = payment.lastQuote?.amounts?.sourceDebitTotal;
|
||||
final settlement = payment.lastQuote?.amounts?.destinationSettlement;
|
||||
@@ -55,7 +56,7 @@ OperationItem mapPaymentToOperation(Payment payment) {
|
||||
);
|
||||
}
|
||||
|
||||
OperationDocumentInfo? _resolveOperationDocument(Payment payment) {
|
||||
OperationDocumentRef? _resolveOperationDocument(Payment payment) {
|
||||
for (final operation in payment.operations) {
|
||||
final operationRef = operation.operationRef;
|
||||
final gatewayService = operation.gateway;
|
||||
@@ -64,7 +65,7 @@ OperationDocumentInfo? _resolveOperationDocument(Payment payment) {
|
||||
|
||||
if (!isOperationDocumentEligible(operation.code)) continue;
|
||||
|
||||
return OperationDocumentInfo(
|
||||
return OperationDocumentRef(
|
||||
operationRef: operationRef,
|
||||
gatewayService: gatewayService,
|
||||
);
|
||||
|
||||
112
frontend/pweb/lib/utils/report/source_filter.dart
Normal file
112
frontend/pweb/lib/utils/report/source_filter.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
import 'package:pshared/models/payment/intent.dart';
|
||||
import 'package:pshared/models/payment/methods/data.dart';
|
||||
import 'package:pshared/models/payment/methods/ledger.dart';
|
||||
import 'package:pshared/models/payment/methods/managed_wallet.dart';
|
||||
import 'package:pshared/models/payment/methods/wallet.dart';
|
||||
import 'package:pshared/models/payment/payment.dart';
|
||||
import 'package:pshared/models/payment/source_type.dart';
|
||||
|
||||
|
||||
bool paymentMatchesSource(
|
||||
Payment payment, {
|
||||
required PaymentSourceType sourceType,
|
||||
required String sourceRef,
|
||||
}) {
|
||||
final normalizedSourceRef = _normalize(sourceRef);
|
||||
if (normalizedSourceRef == null) return false;
|
||||
|
||||
final paymentSourceRef = _paymentSourceRef(payment, sourceType);
|
||||
return paymentSourceRef != null && paymentSourceRef == normalizedSourceRef;
|
||||
}
|
||||
|
||||
String? _paymentSourceRef(Payment payment, PaymentSourceType sourceType) {
|
||||
final fromIntent = _sourceRefFromIntent(payment.intent, sourceType);
|
||||
if (fromIntent != null) return fromIntent;
|
||||
return _sourceRefFromMetadata(payment.metadata, sourceType);
|
||||
}
|
||||
|
||||
String? _sourceRefFromIntent(
|
||||
PaymentIntent? intent,
|
||||
PaymentSourceType sourceType,
|
||||
) {
|
||||
final source = intent?.source;
|
||||
if (source == null) return null;
|
||||
|
||||
final fromIntentAttributes = _sourceRefFromMetadata(
|
||||
intent?.attributes,
|
||||
sourceType,
|
||||
);
|
||||
if (fromIntentAttributes != null) return fromIntentAttributes;
|
||||
|
||||
switch (sourceType) {
|
||||
case PaymentSourceType.wallet:
|
||||
return _walletSourceRef(source);
|
||||
case PaymentSourceType.ledger:
|
||||
return _ledgerSourceRef(source);
|
||||
}
|
||||
}
|
||||
|
||||
String? _walletSourceRef(PaymentMethodData source) {
|
||||
if (source is ManagedWalletPaymentMethod) {
|
||||
return _normalize(source.managedWalletRef) ??
|
||||
_sourceRefFromMetadata(source.metadata, PaymentSourceType.wallet);
|
||||
}
|
||||
if (source is WalletPaymentMethod) {
|
||||
return _normalize(source.walletId) ??
|
||||
_sourceRefFromMetadata(source.metadata, PaymentSourceType.wallet);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _ledgerSourceRef(PaymentMethodData source) {
|
||||
if (source is LedgerPaymentMethod) {
|
||||
return _normalize(source.ledgerAccountRef) ??
|
||||
_sourceRefFromMetadata(source.metadata, PaymentSourceType.ledger);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _sourceRefFromMetadata(
|
||||
Map<String, String>? metadata,
|
||||
PaymentSourceType sourceType,
|
||||
) {
|
||||
if (metadata == null || metadata.isEmpty) return null;
|
||||
|
||||
final keys = switch (sourceType) {
|
||||
PaymentSourceType.wallet => const <String>[
|
||||
'source_wallet_ref',
|
||||
'managed_wallet_ref',
|
||||
'wallet_ref',
|
||||
'wallet_id',
|
||||
'source_wallet_id',
|
||||
'source_wallet_user_id',
|
||||
'wallet_user_id',
|
||||
'wallet_user_ref',
|
||||
'wallet_number',
|
||||
'source_wallet_number',
|
||||
'source_managed_wallet_ref',
|
||||
'source_ref',
|
||||
],
|
||||
PaymentSourceType.ledger => const <String>[
|
||||
'source_ledger_account_ref',
|
||||
'ledger_account_ref',
|
||||
'source_account_code',
|
||||
'ledger_account_code',
|
||||
'account_code',
|
||||
'source_ref',
|
||||
],
|
||||
};
|
||||
|
||||
for (final key in keys) {
|
||||
final value = _normalize(metadata[key]);
|
||||
if (value != null) return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _normalize(String? value) {
|
||||
final normalized = value?.trim();
|
||||
if (normalized == null || normalized.isEmpty) return null;
|
||||
return normalized;
|
||||
}
|
||||
Reference in New Issue
Block a user