From 4065d586a9cfb47ae7f2d1e06b76cb73e8b98d5b Mon Sep 17 00:00:00 2001 From: hailin Date: Sat, 7 Mar 2026 20:09:31 -0800 Subject: [PATCH] feat(mobile/coupons): complete coupon holdings feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - wallet_coupons_page: pass full CouponModel to detail route (was coupon.id) - wallet_coupons_page: fix status filters (in_circulation=可使用, listed=挂售中) - my_coupon_detail_page: full rewrite — StatefulWidget, accept CouponModel or String ID, real QR code via qr_flutter, barcode toggle, real data (faceValue/price/expiry/orderNo/resale), conditional action buttons per isTransferable + resaleCount, grey gradient for expired coupons - profile_page: convert to StatefulWidget, load holdingsSummary on init, show real hold count & totalSaved in quick stats (tappable → /wallet/coupons), add "我的持仓" menu entry - i18n: add myCoupon.switchQr + common.notFound to all 4 locales Co-Authored-By: Claude Sonnet 4.6 --- .../genex-mobile/lib/app/i18n/strings/en.dart | 2 + .../genex-mobile/lib/app/i18n/strings/ja.dart | 2 + .../lib/app/i18n/strings/zh_cn.dart | 2 + .../lib/app/i18n/strings/zh_tw.dart | 2 + .../pages/my_coupon_detail_page.dart | 442 +++++++++++------- .../pages/wallet_coupons_page.dart | 10 +- .../presentation/pages/profile_page.dart | 56 ++- 7 files changed, 341 insertions(+), 175 deletions(-) diff --git a/frontend/genex-mobile/lib/app/i18n/strings/en.dart b/frontend/genex-mobile/lib/app/i18n/strings/en.dart index c95347c..e6e7db9 100644 --- a/frontend/genex-mobile/lib/app/i18n/strings/en.dart +++ b/frontend/genex-mobile/lib/app/i18n/strings/en.dart @@ -21,6 +21,7 @@ const Map en = { 'common.today': 'Today', 'common.thisWeek': 'This Week', 'common.thisMonth': 'This Month', + 'common.notFound': 'Not found', // ============ Navigation ============ 'nav.home': 'Home', @@ -272,6 +273,7 @@ const Map en = { 'myCoupon.active': 'Active', 'myCoupon.showQrHint': 'Show this QR code to the merchant to redeem', 'myCoupon.switchBarcode': 'Switch to Barcode', + 'myCoupon.switchQr': 'Switch to QR Code', 'myCoupon.faceValue': 'Face Value', 'myCoupon.purchasePrice': 'Purchase Price', 'myCoupon.validUntil': 'Valid Until', diff --git a/frontend/genex-mobile/lib/app/i18n/strings/ja.dart b/frontend/genex-mobile/lib/app/i18n/strings/ja.dart index b3b0ec8..e13f3d6 100644 --- a/frontend/genex-mobile/lib/app/i18n/strings/ja.dart +++ b/frontend/genex-mobile/lib/app/i18n/strings/ja.dart @@ -21,6 +21,7 @@ const Map ja = { 'common.today': '今日', 'common.thisWeek': '今週', 'common.thisMonth': '今月', + 'common.notFound': 'データが見つかりません', // ============ Navigation ============ 'nav.home': 'ホーム', @@ -273,6 +274,7 @@ const Map ja = { 'myCoupon.active': '利用可能', 'myCoupon.showQrHint': 'このQRコードを店舗スタッフに提示してスキャンしてもらってください', 'myCoupon.switchBarcode': 'バーコードに切替', + 'myCoupon.switchQr': 'QRコードに切替', 'myCoupon.faceValue': '額面', 'myCoupon.purchasePrice': '購入価格', 'myCoupon.validUntil': '有効期限', diff --git a/frontend/genex-mobile/lib/app/i18n/strings/zh_cn.dart b/frontend/genex-mobile/lib/app/i18n/strings/zh_cn.dart index 71ac600..29be710 100644 --- a/frontend/genex-mobile/lib/app/i18n/strings/zh_cn.dart +++ b/frontend/genex-mobile/lib/app/i18n/strings/zh_cn.dart @@ -21,6 +21,7 @@ const Map zhCN = { 'common.today': '今日', 'common.thisWeek': '本周', 'common.thisMonth': '本月', + 'common.notFound': '数据不存在', // ============ Navigation ============ 'nav.home': '首页', @@ -273,6 +274,7 @@ const Map zhCN = { 'myCoupon.active': '可使用', 'myCoupon.showQrHint': '出示此二维码给商户扫描核销', 'myCoupon.switchBarcode': '切换条形码', + 'myCoupon.switchQr': '切换二维码', 'myCoupon.faceValue': '面值', 'myCoupon.purchasePrice': '购买价格', 'myCoupon.validUntil': '有效期', diff --git a/frontend/genex-mobile/lib/app/i18n/strings/zh_tw.dart b/frontend/genex-mobile/lib/app/i18n/strings/zh_tw.dart index 2c48d4b..1507066 100644 --- a/frontend/genex-mobile/lib/app/i18n/strings/zh_tw.dart +++ b/frontend/genex-mobile/lib/app/i18n/strings/zh_tw.dart @@ -21,6 +21,7 @@ const Map zhTW = { 'common.today': '今日', 'common.thisWeek': '本週', 'common.thisMonth': '本月', + 'common.notFound': '資料不存在', // ============ Navigation ============ 'nav.home': '首頁', @@ -273,6 +274,7 @@ const Map zhTW = { 'myCoupon.active': '可使用', 'myCoupon.showQrHint': '出示此二維碼給商戶掃描核銷', 'myCoupon.switchBarcode': '切換條碼', + 'myCoupon.switchQr': '切換二維碼', 'myCoupon.faceValue': '面值', 'myCoupon.purchasePrice': '購買價格', 'myCoupon.validUntil': '有效期', diff --git a/frontend/genex-mobile/lib/features/coupons/presentation/pages/my_coupon_detail_page.dart b/frontend/genex-mobile/lib/features/coupons/presentation/pages/my_coupon_detail_page.dart index 40b34ad..50d58e4 100644 --- a/frontend/genex-mobile/lib/features/coupons/presentation/pages/my_coupon_detail_page.dart +++ b/frontend/genex-mobile/lib/features/coupons/presentation/pages/my_coupon_detail_page.dart @@ -1,18 +1,50 @@ import 'package:flutter/material.dart'; +import 'package:qr_flutter/qr_flutter.dart'; import '../../../../app/theme/app_colors.dart'; import '../../../../app/theme/app_typography.dart'; import '../../../../app/theme/app_spacing.dart'; import '../../../../shared/widgets/genex_button.dart'; -import '../../../../shared/widgets/status_tag.dart'; import '../../../../app/i18n/app_localizations.dart'; +import '../../../../core/services/coupon_service.dart'; +import '../../data/models/coupon_model.dart'; -/// A4. 券详情(持有券)- QR码/条形码 + 转赠/出售/提取 +/// A4. 券详情(持有券)- QR码/条形码 + 转赠/出售 /// -/// 券二维码/条形码(核销用)、券信息、使用说明、 -/// 「转赠」「出售」「使用说明」按钮 -class MyCouponDetailPage extends StatelessWidget { +/// 接收路由参数:CouponModel(来自 wallet_coupons_page)或 String ID(兼容旧路由) +class MyCouponDetailPage extends StatefulWidget { const MyCouponDetailPage({super.key}); + @override + State createState() => _MyCouponDetailPageState(); +} + +class _MyCouponDetailPageState extends State { + CouponModel? _coupon; + bool _isLoading = false; + bool _showBarcode = false; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_coupon != null) return; + final args = ModalRoute.of(context)?.settings.arguments; + if (args is CouponModel) { + _coupon = args; + } else if (args is String) { + _loadById(args); + } + } + + Future _loadById(String id) async { + setState(() => _isLoading = true); + try { + final coupon = await CouponApiService().getCouponDetail(id); + if (mounted) setState(() { _coupon = coupon; _isLoading = false; }); + } catch (e) { + if (mounted) setState(() => _isLoading = false); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -23,186 +55,275 @@ class MyCouponDetailPage extends StatelessWidget { ), title: Text(context.t('myCoupon.title')), actions: [ - IconButton( - icon: const Icon(Icons.more_horiz_rounded), - onPressed: () => _showMoreOptions(context), - ), + if (_coupon != null) + IconButton( + icon: const Icon(Icons.more_horiz_rounded), + onPressed: () => _showMoreOptions(context), + ), ], ), - body: SingleChildScrollView( - padding: AppSpacing.pagePadding, - child: Column( - children: [ - const SizedBox(height: 16), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _coupon == null + ? Center(child: Text(context.t('common.notFound'))) + : _buildBody(context, _coupon!), + ); + } - // QR Code Card - Container( - width: double.infinity, - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - gradient: AppColors.cardGradient, - borderRadius: AppSpacing.borderRadiusLg, - boxShadow: AppSpacing.shadowPrimary, - ), - child: Column( - children: [ - // Brand + Status - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( + Widget _buildBody(BuildContext context, CouponModel coupon) { + final qrData = 'GNX:${coupon.id}'; + final isActive = coupon.status == 'in_circulation'; + final expiryStr = '${coupon.expiryDate.year}/${coupon.expiryDate.month.toString().padLeft(2, '0')}/${coupon.expiryDate.day.toString().padLeft(2, '0')}'; + final orderNo = 'GNX-${coupon.id.replaceAll('-', '').substring(0, 12).toUpperCase()}'; + + return SingleChildScrollView( + padding: AppSpacing.pagePadding, + child: Column( + children: [ + const SizedBox(height: 16), + + // QR / Barcode Card + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: coupon.isExpired + ? const LinearGradient(colors: [Color(0xFF9E9E9E), Color(0xFF757575)]) + : AppColors.cardGradient, + borderRadius: AppSpacing.borderRadiusLg, + boxShadow: AppSpacing.shadowPrimary, + ), + child: Column( + children: [ + // Brand + Status + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Starbucks', style: AppTypography.bodySmall.copyWith( + Text(coupon.brandName ?? '', style: AppTypography.bodySmall.copyWith( color: Colors.white70, )), - Text('星巴克 \$25 礼品卡', style: AppTypography.h2.copyWith( + Text(coupon.name, style: AppTypography.h2.copyWith( color: Colors.white, - )), + ), overflow: TextOverflow.ellipsis), ], ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), - decoration: BoxDecoration( - color: Colors.white24, - borderRadius: AppSpacing.borderRadiusFull, - ), - child: Text(context.t('myCoupon.active'), style: AppTypography.caption.copyWith( + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: Colors.white24, + borderRadius: AppSpacing.borderRadiusFull, + ), + child: Text( + _statusLabel(context, coupon.status), + style: AppTypography.caption.copyWith( color: Colors.white, fontWeight: FontWeight.w600, - )), + ), ), - ], - ), - const SizedBox(height: 24), - - // QR Code area - Container( - width: 200, - height: 200, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: AppSpacing.borderRadiusMd, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.qr_code_rounded, size: 140, - color: AppColors.textPrimary), - const SizedBox(height: 8), - Text('GNX-STB-A1B2C3D4', - style: AppTypography.caption.copyWith( - letterSpacing: 1.5, - fontWeight: FontWeight.w600, - )), - ], ), + ], + ), + const SizedBox(height: 24), + + // QR Code / Barcode area + Container( + width: 200, + height: 200, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: AppSpacing.borderRadiusMd, ), - const SizedBox(height: 16), + child: _showBarcode + ? _buildBarcode(coupon.id) + : Padding( + padding: const EdgeInsets.all(12), + child: QrImageView( + data: qrData, + version: QrVersions.auto, + size: 176, + ), + ), + ), + const SizedBox(height: 12), - // Instructions - Text( - context.t('myCoupon.showQrHint'), - style: AppTypography.bodySmall.copyWith(color: Colors.white70), - ), - - const SizedBox(height: 8), - - // Barcode toggle - TextButton.icon( - onPressed: () {}, - icon: const Icon(Icons.view_headline_rounded, size: 18, - color: Colors.white70), - label: Text(context.t('myCoupon.switchBarcode'), style: AppTypography.labelSmall.copyWith( - color: Colors.white70, - )), - ), - ], - ), - ), - const SizedBox(height: 20), - - // Face Value + Expiry - Container( - padding: AppSpacing.cardPadding, - decoration: BoxDecoration( - color: AppColors.surface, - borderRadius: AppSpacing.borderRadiusMd, - border: Border.all(color: AppColors.borderLight), - ), - child: Column( - children: [ - _infoRow(context.t('myCoupon.faceValue'), '\$25.00'), - const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()), - _infoRow(context.t('myCoupon.purchasePrice'), '\$21.25'), - const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()), - _infoRow(context.t('myCoupon.validUntil'), '2026/12/31'), - const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()), - _infoRow(context.t('myCoupon.orderNo'), 'GNX-20260209-001234'), - const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()), - _infoRow(context.t('myCoupon.resellCount'), '3次'), - ], - ), - ), - const SizedBox(height: 16), - - // Action Buttons - Row( - children: [ - Expanded( - child: GenexButton( - label: context.t('myCoupon.transfer'), - icon: Icons.card_giftcard_rounded, - variant: GenexButtonVariant.secondary, - onPressed: () { - Navigator.pushNamed(context, '/transfer'); - }, + Text( + coupon.id.replaceAll('-', '').substring(0, 16).toUpperCase(), + style: AppTypography.caption.copyWith( + color: Colors.white70, + letterSpacing: 1.5, + fontWeight: FontWeight.w600, ), ), - const SizedBox(width: 12), - Expanded( - child: GenexButton( - label: context.t('myCoupon.sell'), - icon: Icons.sell_rounded, - variant: GenexButtonVariant.outline, - onPressed: () { - Navigator.pushNamed(context, '/sell'); - }, + const SizedBox(height: 8), + + Text( + context.t('myCoupon.showQrHint'), + style: AppTypography.bodySmall.copyWith(color: Colors.white70), + ), + const SizedBox(height: 8), + + TextButton.icon( + onPressed: () => setState(() => _showBarcode = !_showBarcode), + icon: Icon( + _showBarcode ? Icons.qr_code_rounded : Icons.view_headline_rounded, + size: 18, + color: Colors.white70, + ), + label: Text( + _showBarcode + ? context.t('myCoupon.switchQr') + : context.t('myCoupon.switchBarcode'), + style: AppTypography.labelSmall.copyWith(color: Colors.white70), ), ), ], ), - const SizedBox(height: 16), + ), + const SizedBox(height: 20), - // Usage Rules - Container( - width: double.infinity, - padding: AppSpacing.cardPadding, - decoration: BoxDecoration( - color: AppColors.surface, - borderRadius: AppSpacing.borderRadiusMd, - border: Border.all(color: AppColors.borderLight), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(context.t('myCoupon.usageNote'), style: AppTypography.labelMedium), - const SizedBox(height: 12), + // Info Card + Container( + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + children: [ + _infoRow(context.t('myCoupon.faceValue'), + '\$${coupon.faceValue.toStringAsFixed(2)}'), + const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()), + _infoRow(context.t('myCoupon.purchasePrice'), + '\$${coupon.currentPrice.toStringAsFixed(2)}'), + const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()), + _infoRow(context.t('myCoupon.validUntil'), expiryStr), + const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()), + _infoRow(context.t('myCoupon.orderNo'), orderNo), + const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()), + _infoRow(context.t('myCoupon.resellCount'), + '${coupon.resaleCount}/${coupon.maxResaleCount}'), + ], + ), + ), + const SizedBox(height: 16), + + // Action Buttons (only for active coupons) + if (isActive) + Row( + children: [ + if (coupon.isTransferable) + Expanded( + child: GenexButton( + label: context.t('myCoupon.transfer'), + icon: Icons.card_giftcard_rounded, + variant: GenexButtonVariant.secondary, + onPressed: () { + Navigator.pushNamed(context, '/transfer', arguments: coupon.id); + }, + ), + ), + if (coupon.isTransferable) const SizedBox(width: 12), + if (coupon.resaleCount < coupon.maxResaleCount) + Expanded( + child: GenexButton( + label: context.t('myCoupon.sell'), + icon: Icons.sell_rounded, + variant: GenexButtonVariant.outline, + onPressed: () { + Navigator.pushNamed(context, '/sell', arguments: coupon.id); + }, + ), + ), + ], + ), + + if (isActive) const SizedBox(height: 16), + + // Usage Rules + Container( + width: double.infinity, + padding: AppSpacing.cardPadding, + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: AppSpacing.borderRadiusMd, + border: Border.all(color: AppColors.borderLight), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(context.t('myCoupon.usageNote'), style: AppTypography.labelMedium), + const SizedBox(height: 12), + if (coupon.description != null && coupon.description!.isNotEmpty) + _ruleItem(coupon.description!) + else ...[ _ruleItem(context.t('myCoupon.useInStore')), _ruleItem(context.t('myCoupon.useInTime')), _ruleItem(context.t('myCoupon.onePerVisit')), _ruleItem(context.t('myCoupon.noCash')), ], - ), + ], ), + ), - const SizedBox(height: 80), - ], - ), + const SizedBox(height: 80), + ], ), ); } + /// Simple barcode visual using thin/thick bars + Widget _buildBarcode(String id) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: List.generate(40, (i) { + final val = id.codeUnitAt(i % id.length); + final width = (val % 3 == 0) ? 3.0 : (val % 2 == 0) ? 2.0 : 1.5; + final isWhite = i % 7 == 0; + return Container( + width: width, + margin: const EdgeInsets.symmetric(horizontal: 0.5), + color: isWhite ? Colors.white : Colors.black87, + ); + }), + ), + ), + const SizedBox(height: 8), + Text( + id.replaceAll('-', '').substring(0, 12).toUpperCase(), + style: AppTypography.caption.copyWith( + letterSpacing: 1.5, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } + + String _statusLabel(BuildContext context, String status) { + switch (status) { + case 'in_circulation': return context.t('myCoupon.active'); + case 'listed': return context.t('walletCoupons.pendingRedeem'); + case 'expired': return context.t('walletCoupons.expired'); + case 'redeemed': return context.t('status.used'); + default: return status; + } + } + Widget _infoRow(String label, String value) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -219,16 +340,20 @@ class MyCouponDetailPage extends StatelessWidget { return Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - width: 4, height: 4, - decoration: const BoxDecoration( - color: AppColors.textTertiary, - shape: BoxShape.circle, + Padding( + padding: const EdgeInsets.only(top: 6), + child: Container( + width: 4, height: 4, + decoration: const BoxDecoration( + color: AppColors.textTertiary, + shape: BoxShape.circle, + ), ), ), const SizedBox(width: 8), - Text(text, style: AppTypography.bodySmall), + Expanded(child: Text(text, style: AppTypography.bodySmall)), ], ), ); @@ -250,7 +375,8 @@ class MyCouponDetailPage extends StatelessWidget { ), ), const SizedBox(height: 16), - _optionTile(Icons.wallet_rounded, context.t('myCoupon.extractToWallet'), context.t('myCoupon.requireKycL2'), () {}), + _optionTile(Icons.wallet_rounded, context.t('myCoupon.extractToWallet'), + context.t('myCoupon.requireKycL2'), () {}), const Divider(), _optionTile(Icons.receipt_long_rounded, context.t('myCoupon.viewTrades'), '', () {}), const Divider(), diff --git a/frontend/genex-mobile/lib/features/coupons/presentation/pages/wallet_coupons_page.dart b/frontend/genex-mobile/lib/features/coupons/presentation/pages/wallet_coupons_page.dart index 54262c4..f7badf9 100644 --- a/frontend/genex-mobile/lib/features/coupons/presentation/pages/wallet_coupons_page.dart +++ b/frontend/genex-mobile/lib/features/coupons/presentation/pages/wallet_coupons_page.dart @@ -35,10 +35,10 @@ class _WalletCouponsPageState extends State // Tab index → backend status filter static const _tabStatusFilters = { - 0: null, // 全部 - 1: 'listed', // 可使用 (listed / in_circulation) - 2: 'sold', // 待核销 - 3: 'expired', // 已过期 + 0: null, // 全部 + 1: 'in_circulation', // 可使用(在用户钱包中) + 2: 'listed', // 挂售中(已挂出待成交) + 3: 'expired', // 已过期 }; @override @@ -259,7 +259,7 @@ class _WalletCouponsPageState extends State final displayStatus = _mapStatus(coupon.status); return GestureDetector( - onTap: () => Navigator.pushNamed(context, '/coupon/mine/detail', arguments: coupon.id), + onTap: () => Navigator.pushNamed(context, '/coupon/mine/detail', arguments: coupon), child: Container( padding: AppSpacing.cardPadding, decoration: BoxDecoration( diff --git a/frontend/genex-mobile/lib/features/profile/presentation/pages/profile_page.dart b/frontend/genex-mobile/lib/features/profile/presentation/pages/profile_page.dart index fae9064..c7ff7d9 100644 --- a/frontend/genex-mobile/lib/features/profile/presentation/pages/profile_page.dart +++ b/frontend/genex-mobile/lib/features/profile/presentation/pages/profile_page.dart @@ -2,16 +2,39 @@ import 'package:flutter/material.dart'; import '../../../../app/theme/app_colors.dart'; import '../../../../app/theme/app_typography.dart'; import '../../../../app/theme/app_spacing.dart'; -import '../../../../shared/widgets/kyc_badge.dart'; import '../../../../app/i18n/app_localizations.dart'; +import '../../../../core/services/coupon_service.dart'; +import '../../data/models/holdings_summary_model.dart'; /// A7. 个人中心 /// /// 头像、昵称、KYC等级标识、信用积分 /// KYC认证、支付管理、设置、Pro模式 -class ProfilePage extends StatelessWidget { +class ProfilePage extends StatefulWidget { const ProfilePage({super.key}); + @override + State createState() => _ProfilePageState(); +} + +class _ProfilePageState extends State { + HoldingsSummaryModel? _holdingsSummary; + + @override + void initState() { + super.initState(); + _loadSummary(); + } + + Future _loadSummary() async { + try { + final summary = await CouponApiService().getHoldingsSummary(); + if (mounted) setState(() => _holdingsSummary = summary); + } catch (e) { + debugPrint('[ProfilePage] loadSummary error: $e'); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -37,6 +60,9 @@ class ProfilePage extends StatelessWidget { ])), SliverToBoxAdapter(child: _buildMenuSection(context.t('profile.trade'), [ + _MenuItem(Icons.confirmation_number_outlined, context.t('profile.holdCoupons'), + '${_holdingsSummary?.count ?? '--'}', true, + onTap: () => Navigator.pushNamed(context, '/wallet/coupons')), _MenuItem(Icons.receipt_long_rounded, context.t('wallet.records'), '', true, onTap: () => Navigator.pushNamed(context, '/trading')), _MenuItem(Icons.storefront_rounded, context.t('tradingPage.pendingOrders'), context.t('status.onSale'), true, @@ -144,11 +170,14 @@ class ProfilePage extends StatelessWidget { } Widget _buildQuickStats(BuildContext context) { + final holdCount = _holdingsSummary?.count ?? 0; + final saved = _holdingsSummary?.totalSaved ?? 0; + final stats = [ - (context.t('profile.holdCoupons'), '12'), - (context.t('profile.trade'), '28'), - (context.t('profile.saved'), '\$156'), - (context.t('profile.credit'), '750'), + (context.t('profile.holdCoupons'), '$holdCount', () => Navigator.pushNamed(context, '/wallet/coupons')), + (context.t('profile.trade'), '28', null), + (context.t('profile.saved'), '\$${saved.toStringAsFixed(0)}', null), + (context.t('profile.credit'), '750', null), ]; return Container( @@ -162,12 +191,15 @@ class ProfilePage extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: stats.map((stat) { - return Column( - children: [ - Text(stat.$2, style: AppTypography.h2.copyWith(color: AppColors.primary)), - const SizedBox(height: 4), - Text(stat.$1, style: AppTypography.caption), - ], + return GestureDetector( + onTap: stat.$3, + child: Column( + children: [ + Text(stat.$2, style: AppTypography.h2.copyWith(color: AppColors.primary)), + const SizedBox(height: 4), + Text(stat.$1, style: AppTypography.caption), + ], + ), ); }).toList(), ),