💄 Optimized wallet

This commit is contained in:
2025-10-04 15:48:16 +08:00
parent 3f640b7898
commit 6c91093198
6 changed files with 551 additions and 142 deletions

View File

@@ -37,6 +37,13 @@ Future<SnWallet?> walletCurrent(Ref ref) async {
}
}
@riverpod
Future<SnWalletStats> walletStats(Ref ref) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/id/wallets/stats');
return SnWalletStats.fromJson(resp.data);
}
class CreateFundSheet extends StatefulWidget {
const CreateFundSheet({super.key});
@@ -652,21 +659,13 @@ Future<SnWalletFund> walletFund(Ref ref, String fundId) async {
return SnWalletFund.fromJson(resp.data);
}
@riverpod
Future<Map<String, dynamic>> walletFundStats(Ref ref) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/id/wallets/stats');
return resp.data as Map<String, dynamic>;
}
class WalletScreen extends HookConsumerWidget {
const WalletScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final wallet = ref.watch(walletCurrentProvider);
final tabController = useTabController(initialLength: 3);
final fundStats = ref.watch(walletFundStatsProvider);
final tabController = useTabController(initialLength: 2);
Future<void> createWallet() async {
final client = ref.read(apiClientProvider);
@@ -726,120 +725,73 @@ class WalletScreen extends HookConsumerWidget {
).center();
}
return Column(
children: [
// Wallet Overview
Column(
spacing: 8,
children: [
// Pockets
...data.pockets.map(
(pocket) => Card(
margin: EdgeInsets.zero,
child: ListTile(
leading: Icon(
kCurrencyIconData[pocket.currency] ??
Symbols.universal_currency_alt,
return NestedScrollView(
headerSliverBuilder:
(context, innerBoxIsScrolled) => [
// Wallet Overview
SliverToBoxAdapter(
child: Column(
spacing: 8,
children: [
// Wallet Stats
_buildCompactStatsWidget(context, ref),
// Pockets
Card(
margin: EdgeInsets.zero,
child: Column(
children: [
...data.pockets.map(
(pocket) => ListTile(
leading: Icon(
kCurrencyIconData[pocket.currency] ??
Symbols.universal_currency_alt,
),
title:
Text(
getCurrencyTranslationKey(
pocket.currency,
),
).tr(),
subtitle: Text(
'${pocket.amount.toStringAsFixed(2)} ${getCurrencyTranslationKey(pocket.currency, isShort: true).tr()}',
),
),
),
],
),
),
title:
Text(
getCurrencyTranslationKey(pocket.currency),
).tr(),
subtitle: Text(
'${pocket.amount.toStringAsFixed(2)} ${getCurrencyTranslationKey(pocket.currency, isShort: true).tr()}',
),
),
],
).padding(horizontal: 12, top: 12),
),
// Tab Bar
SliverToBoxAdapter(
child: TabBar(
controller: tabController,
tabs: [
Tab(text: 'transactions'.tr()),
Tab(text: 'myFunds'.tr()),
],
),
),
// Fund Stats
fundStats.when(
data:
(stats) => Card(
margin: EdgeInsets.zero,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Symbols.celebration,
color:
Theme.of(context).colorScheme.primary,
),
const Gap(8),
Text(
'fundOverview'.tr(),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
const Gap(8),
Row(
children: [
Expanded(
child: _buildStatItem(
context,
'totalFundsSent'.tr(),
'${stats['total_sent'] ?? 0}',
Icons.send,
),
),
Expanded(
child: _buildStatItem(
context,
'totalFundsReceived'.tr(),
'${stats['total_received'] ?? 0}',
Icons.call_received,
),
),
],
),
],
),
),
),
loading:
() => const Card(
margin: EdgeInsets.zero,
child: Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
),
),
error: (error, stack) => const SizedBox.shrink(),
),
],
).padding(horizontal: 16, vertical: 16),
// Tab Bar
TabBar(
controller: tabController,
tabs: [
Tab(text: 'transactions'.tr()),
Tab(text: 'myFunds'.tr()),
],
),
// Tab Content
Expanded(
child: TabBarView(
controller: tabController,
children: [
// Transactions Tab
PagingHelperView(
body: TabBarView(
controller: tabController,
children: [
// Transactions Tab
CustomScrollView(
slivers: [
PagingHelperSliverView(
provider: transactionListNotifierProvider,
futureRefreshable: transactionListNotifierProvider.future,
notifierRefreshable:
transactionListNotifierProvider.notifier,
contentBuilder:
(data, widgetCount, endItemView) => ListView.builder(
padding: EdgeInsets.zero,
(
data,
widgetCount,
endItemView,
) => SliverList.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
@@ -854,10 +806,14 @@ class WalletScreen extends HookConsumerWidget {
key: ValueKey(transaction.id),
leading: Icon(
isIncome
? Symbols.arrow_upward
: Symbols.arrow_downward,
? Symbols.payment_arrow_down
: Symbols.paid,
),
title: Text(
transaction.remarks ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
title: Text(transaction.remarks ?? ''),
subtitle: Text(
DateFormat.yMd().add_Hm().format(
transaction.createdAt,
@@ -873,13 +829,13 @@ class WalletScreen extends HookConsumerWidget {
},
),
),
// My Funds Tab
_buildFundsList(context, ref),
],
),
),
],
// My Funds Tab
_buildFundsList(context, ref),
],
),
);
},
error:
@@ -1040,6 +996,112 @@ class WalletScreen extends HookConsumerWidget {
);
}
Widget _buildCompactStatsWidget(BuildContext context, WidgetRef ref) {
final stats = ref.watch(walletStatsProvider);
return stats.when(
data: (statsData) {
return Card(
margin: EdgeInsets.zero,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'walletStats'.tr(),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const Spacer(),
Text(
'${DateFormat.yMd().format(statsData.periodBegin)} - ${DateFormat.yMd().format(statsData.periodEnd)}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
const Gap(12),
Row(
children: [
Expanded(
child: _buildStatItem(
context,
'totalTransactions'.tr(),
statsData.totalTransactions.toString(),
Symbols.swap_horiz,
),
),
const Gap(16),
Expanded(
child: _buildStatItem(
context,
'totalOrders'.tr(),
statsData.totalOrders.toString(),
Symbols.receipt_long,
),
),
],
),
const Gap(12),
Row(
children: [
Expanded(
child: _buildStatItem(
context,
'totalIncome'.tr(),
statsData.totalIncome.toStringAsFixed(2),
Symbols.arrow_upward,
),
),
const Gap(16),
Expanded(
child: _buildStatItem(
context,
'totalOutgoing'.tr(),
statsData.totalOutgoing.toStringAsFixed(2),
Symbols.arrow_downward,
),
),
const Gap(16),
Expanded(
child: _buildStatItem(
context,
'netBalance'.tr(),
statsData.sum.toStringAsFixed(2),
Symbols.account_balance,
),
),
],
),
],
),
),
);
},
loading:
() => Card(
margin: EdgeInsets.zero,
child: const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
),
),
error:
(error, stack) => Card(
margin: EdgeInsets.zero,
child: Padding(
padding: const EdgeInsets.all(16),
child: Center(child: Text('Error loading stats')),
),
),
);
}
Future<void> _handleFundCreation(
BuildContext context,
WidgetRef ref,
@@ -1075,7 +1137,6 @@ class WalletScreen extends HookConsumerWidget {
// Wait for server to handle order
await Future.delayed(const Duration(seconds: 1));
ref.invalidate(walletFundsProvider);
ref.invalidate(walletFundStatsProvider);
ref.invalidate(walletCurrentProvider);
if (context.mounted) {
showSnackBar('fundCreatedSuccessfully'.tr());