ledger top up functionality and few small fixes for project architechture and design

This commit is contained in:
Arseni
2026-03-05 21:49:23 +03:00
parent 39c04beb21
commit 97b16542c2
41 changed files with 764 additions and 455 deletions

View File

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

View File

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

View File

@@ -4,18 +4,20 @@ 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';
import 'package:pweb/pages/dashboard/buttons/balance/ledger.dart';
class BalanceCarousel extends StatefulWidget {
final List<BalanceItem> items;
final int currentIndex;
final ValueChanged<int> onIndexChanged;
final ValueChanged<Wallet> onTopUp;
final ValueChanged<LedgerAccount> onLedgerAddFunds;
final ValueChanged<Wallet> onWalletTap;
final ValueChanged<LedgerAccount> onLedgerTap;
@@ -25,6 +27,7 @@ class BalanceCarousel extends StatefulWidget {
required this.currentIndex,
required this.onIndexChanged,
required this.onTopUp,
required this.onLedgerAddFunds,
required this.onWalletTap,
required this.onLedgerTap,
});
@@ -101,17 +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!,
onTap: () => widget.onLedgerTap(item.account!),
LedgerBalanceItem(:final account) => LedgerAccountCard(
account: account,
onTap: () => widget.onLedgerTap(account),
onAddFunds: () => widget.onLedgerAddFunds(account),
),
BalanceItemType.addAction => const AddBalanceCard(),
AddBalanceActionItem() => const AddBalanceCard(),
};
return Padding(

View File

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

View File

@@ -1,133 +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, this.onTap});
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: InkWell(
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
onTap: onTap,
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,
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,15 +12,16 @@ import 'package:pweb/pages/dashboard/buttons/balance/controller.dart';
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,
});
@@ -49,6 +50,7 @@ class BalanceWidget extends StatelessWidget {
currentIndex: carousel.index,
onIndexChanged: carousel.onPageChanged,
onTopUp: onTopUp,
onLedgerAddFunds: onLedgerAddFunds,
onWalletTap: onWalletTap,
onLedgerTap: onLedgerTap,
);