solyanka iz fix for payout page design, ledger wallet now clickable

This commit is contained in:
Arseni
2026-03-05 15:48:52 +03:00
parent a9b00b6871
commit d6a3a0cc5b
31 changed files with 596 additions and 370 deletions

View File

@@ -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()),
],
);
}
}
}

View File

@@ -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)!;
final source = context.watch<PaymentSourceController>();
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: () {
final wallets = context.read<WalletsController>();
final wallet = wallets.selectedWallet;
if (wallet != null) {
context.pushNamed(
PayoutRoutes.payment,
queryParameters: PayoutRoutes.buildQueryParameters(
paymentType: PaymentType.wallet,
),
);
}
},
style: ElevatedButton.styleFrom(shadowColor: null, elevation: 0),
onPressed: paymentType == null
? null
: () {
context.pushNamed(
PayoutRoutes.payment,
queryParameters: PayoutRoutes.buildQueryParameters(
paymentType: paymentType,
),
);
},
child: Text(loc.payoutNavSendPayout),
);
}

View File

@@ -2,34 +2,26 @@ 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:pshared/models/payment/source_type.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 canTopUp = source.selectedType == PaymentSourceType.wallet;
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 ? () => context.pushToWalletTopUp() : null,
child: Text(loc.topUpBalance),
);
}

View File

@@ -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();
},
);
}

View File

@@ -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';
}
}
}

View File

@@ -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,
),
),
],
);
}
}

View File

@@ -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,
),
],
);
},
);
}
}

View File

@@ -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,
),
],
);
}
}

View File

@@ -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,
),
],
);
}
}

View File

@@ -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,
),

View File

@@ -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;
if (wallet == null) {
final sourceType = controller.selectedType;
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),
),
),
],
),
);

View File

@@ -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,
),
],
),
);
},
);