import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import '../../../application/wallet/wallet_loader/wallet_loader_bloc.dart'; import '../../../common/extension/extension.dart'; import '../../../common/theme/theme.dart'; import '../../../domain/wallet/wallet.dart'; import '../../components/button/button.dart'; import '../../router/app_router.gr.dart'; import 'wallet_ui.dart'; import 'widgets/wallet_expiring_banner.dart'; import 'widgets/wallet_transaction_tile.dart'; /// Beranda wallet (ยง4.1). @RoutePage() class WalletPage extends StatefulWidget { const WalletPage({super.key}); @override State createState() => _WalletPageState(); } class _WalletPageState extends State { @override void initState() { super.initState(); _refresh(); } void _refresh() { context.read().add(const WalletLoaderEvent.fetched()); } /// Saldo dimuat ulang setiap kembali dari layar transaksi. Future _open(PageRouteInfo route) async { await context.router.push(route); if (mounted) _refresh(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColor.background, appBar: AppBar(title: const Text('EnakPoint & EnakCoin')), body: BlocBuilder( builder: (context, state) { if (!state.hasLoaded) { return state.failureOption.fold( () => const Center( child: SpinKitFadingCircle(color: AppColor.primary, size: 36), ), (f) => _buildError(walletFailureMessage(f)), ); } final summary = state.summary; return RefreshIndicator( color: AppColor.primary, onRefresh: () async { _refresh(); await context.read().stream.firstWhere( (s) => !s.isFetching, ); }, child: ListView( padding: const EdgeInsets.only(bottom: 32), children: [ _buildBalanceCard(summary), ..._buildExpiringBanners(summary), _buildActions(summary), const SizedBox(height: 8), _buildRecent(summary), ], ), ); }, ), ); } Widget _buildError(String message) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(message, style: AppStyle.md, textAlign: TextAlign.center), const SizedBox(height: 16), AppOutlineButton( title: 'Coba lagi', width: 160, onPressed: _refresh, ), ], ), ), ); } Widget _buildBalanceCard(WalletSummary summary) { return Container( margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: const LinearGradient( colors: AppColor.primaryGradient, begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: AppColor.primary.withOpacity(0.3), blurRadius: 12, offset: const Offset(0, 6), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const _CurrencyBadge(currency: WalletCurrency.point, size: 44), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'EnakPoint', style: AppStyle.sm.copyWith( color: AppColor.textWhite.withOpacity(0.9), ), ), Text( summary.pointBalance.thousandFormat, style: AppStyle.h2.copyWith( color: AppColor.textWhite, fontWeight: FontWeight.w700, ), ), Text( pointDiscountLabel(summary.pointDiscountValue), style: AppStyle.sm.copyWith( color: AppColor.textWhite.withOpacity(0.9), fontWeight: FontWeight.w500, ), ), ], ), ), ], ), const SizedBox(height: 16), Divider(color: AppColor.white.withOpacity(0.3), height: 1), const SizedBox(height: 12), Row( children: [ const _CurrencyBadge(currency: WalletCurrency.coin, size: 28), const SizedBox(width: 8), Text( 'EnakCoin', style: AppStyle.md.copyWith( color: AppColor.textWhite.withOpacity(0.9), ), ), const Spacer(), Text( summary.coinBalance.thousandFormat, style: AppStyle.xl.copyWith( color: AppColor.textWhite, fontWeight: FontWeight.w700, ), ), ], ), ], ), ); } List _buildExpiringBanners(WalletSummary summary) { final banners = [ if (summary.nearestExpiringPoint != null) WalletExpiringBanner( currency: WalletCurrency.point, expiring: summary.nearestExpiringPoint!, onTap: () => _open(const WalletExpiringRoute()), ), if (summary.nearestExpiringCoin != null) WalletExpiringBanner( currency: WalletCurrency.coin, expiring: summary.nearestExpiringCoin!, onTap: () => _open(const WalletExpiringRoute()), ), ]; return banners .map( (banner) => Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), child: banner, ), ) .toList(); } Widget _buildActions(WalletSummary summary) { final actions = <(IconData, String, PageRouteInfo)>[ (Icons.swap_horiz, 'Tukar EnakCoin', const ExchangeRoute()), (Icons.send_outlined, 'Transfer', const TransferRoute()), (Icons.casino_outlined, 'Main game', const EnakGameRoute()), (Icons.confirmation_number_outlined, 'Voucher', const RewardRoute()), ]; return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(vertical: 16), decoration: BoxDecoration( color: AppColor.white, borderRadius: BorderRadius.circular(AppValue.borderRadius), ), child: Row( children: actions.map((action) { final (icon, label, route) = action; return Expanded( child: InkWell( onTap: () => _open(route), child: Column( children: [ Container( width: 48, height: 48, decoration: BoxDecoration( color: AppColor.primaryWithOpacity(0.1), shape: BoxShape.circle, ), child: Icon(icon, color: AppColor.primary), ), const SizedBox(height: 8), Text( label, textAlign: TextAlign.center, style: AppStyle.xs.copyWith(fontWeight: FontWeight.w600), ), ], ), ), ); }).toList(), ), ); } Widget _buildRecent(WalletSummary summary) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: AppColor.white, borderRadius: BorderRadius.circular(AppValue.borderRadius), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 8, 4, 0), child: Row( children: [ Expanded( child: Text( 'Riwayat terbaru', style: AppStyle.lg.copyWith(fontWeight: FontWeight.w700), ), ), TextButton( onPressed: () => _open(WalletHistoryRoute()), child: Text( 'Lihat semua', style: AppStyle.md.copyWith( color: AppColor.primary, fontWeight: FontWeight.w600, ), ), ), ], ), ), if (summary.recentTransactions.isEmpty) Padding( padding: const EdgeInsets.all(24), child: Text( 'Belum ada transaksi', textAlign: TextAlign.center, style: AppStyle.md.copyWith(color: AppColor.textSecondary), ), ) else ...summary.recentTransactions .take(5) .map( (tx) => WalletTransactionTile( transaction: tx, showCurrency: true, ), ), ], ), ); } } /// Ikon mata uang di atas lingkaran putih, agar kontras di kartu merah. class _CurrencyBadge extends StatelessWidget { const _CurrencyBadge({required this.currency, required this.size}); final WalletCurrency currency; final double size; @override Widget build(BuildContext context) { return Container( width: size, height: size, padding: EdgeInsets.all(size * 0.14), decoration: const BoxDecoration( color: AppColor.white, shape: BoxShape.circle, ), child: WalletCurrencyIcon(currency: currency, size: size * 0.72), ); } }