feat(mobile/coupons): complete coupon holdings feature

- 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 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-07 20:09:31 -08:00
parent c00c48c8bd
commit 4065d586a9
7 changed files with 341 additions and 175 deletions

View File

@ -21,6 +21,7 @@ const Map<String, String> en = {
'common.today': 'Today', 'common.today': 'Today',
'common.thisWeek': 'This Week', 'common.thisWeek': 'This Week',
'common.thisMonth': 'This Month', 'common.thisMonth': 'This Month',
'common.notFound': 'Not found',
// ============ Navigation ============ // ============ Navigation ============
'nav.home': 'Home', 'nav.home': 'Home',
@ -272,6 +273,7 @@ const Map<String, String> en = {
'myCoupon.active': 'Active', 'myCoupon.active': 'Active',
'myCoupon.showQrHint': 'Show this QR code to the merchant to redeem', 'myCoupon.showQrHint': 'Show this QR code to the merchant to redeem',
'myCoupon.switchBarcode': 'Switch to Barcode', 'myCoupon.switchBarcode': 'Switch to Barcode',
'myCoupon.switchQr': 'Switch to QR Code',
'myCoupon.faceValue': 'Face Value', 'myCoupon.faceValue': 'Face Value',
'myCoupon.purchasePrice': 'Purchase Price', 'myCoupon.purchasePrice': 'Purchase Price',
'myCoupon.validUntil': 'Valid Until', 'myCoupon.validUntil': 'Valid Until',

View File

@ -21,6 +21,7 @@ const Map<String, String> ja = {
'common.today': '今日', 'common.today': '今日',
'common.thisWeek': '今週', 'common.thisWeek': '今週',
'common.thisMonth': '今月', 'common.thisMonth': '今月',
'common.notFound': 'データが見つかりません',
// ============ Navigation ============ // ============ Navigation ============
'nav.home': 'ホーム', 'nav.home': 'ホーム',
@ -273,6 +274,7 @@ const Map<String, String> ja = {
'myCoupon.active': '利用可能', 'myCoupon.active': '利用可能',
'myCoupon.showQrHint': 'このQRコードを店舗スタッフに提示してスキャンしてもらってください', 'myCoupon.showQrHint': 'このQRコードを店舗スタッフに提示してスキャンしてもらってください',
'myCoupon.switchBarcode': 'バーコードに切替', 'myCoupon.switchBarcode': 'バーコードに切替',
'myCoupon.switchQr': 'QRコードに切替',
'myCoupon.faceValue': '額面', 'myCoupon.faceValue': '額面',
'myCoupon.purchasePrice': '購入価格', 'myCoupon.purchasePrice': '購入価格',
'myCoupon.validUntil': '有効期限', 'myCoupon.validUntil': '有効期限',

View File

@ -21,6 +21,7 @@ const Map<String, String> zhCN = {
'common.today': '今日', 'common.today': '今日',
'common.thisWeek': '本周', 'common.thisWeek': '本周',
'common.thisMonth': '本月', 'common.thisMonth': '本月',
'common.notFound': '数据不存在',
// ============ Navigation ============ // ============ Navigation ============
'nav.home': '首页', 'nav.home': '首页',
@ -273,6 +274,7 @@ const Map<String, String> zhCN = {
'myCoupon.active': '可使用', 'myCoupon.active': '可使用',
'myCoupon.showQrHint': '出示此二维码给商户扫描核销', 'myCoupon.showQrHint': '出示此二维码给商户扫描核销',
'myCoupon.switchBarcode': '切换条形码', 'myCoupon.switchBarcode': '切换条形码',
'myCoupon.switchQr': '切换二维码',
'myCoupon.faceValue': '面值', 'myCoupon.faceValue': '面值',
'myCoupon.purchasePrice': '购买价格', 'myCoupon.purchasePrice': '购买价格',
'myCoupon.validUntil': '有效期', 'myCoupon.validUntil': '有效期',

View File

@ -21,6 +21,7 @@ const Map<String, String> zhTW = {
'common.today': '今日', 'common.today': '今日',
'common.thisWeek': '本週', 'common.thisWeek': '本週',
'common.thisMonth': '本月', 'common.thisMonth': '本月',
'common.notFound': '資料不存在',
// ============ Navigation ============ // ============ Navigation ============
'nav.home': '首頁', 'nav.home': '首頁',
@ -273,6 +274,7 @@ const Map<String, String> zhTW = {
'myCoupon.active': '可使用', 'myCoupon.active': '可使用',
'myCoupon.showQrHint': '出示此二維碼給商戶掃描核銷', 'myCoupon.showQrHint': '出示此二維碼給商戶掃描核銷',
'myCoupon.switchBarcode': '切換條碼', 'myCoupon.switchBarcode': '切換條碼',
'myCoupon.switchQr': '切換二維碼',
'myCoupon.faceValue': '面值', 'myCoupon.faceValue': '面值',
'myCoupon.purchasePrice': '購買價格', 'myCoupon.purchasePrice': '購買價格',
'myCoupon.validUntil': '有效期', 'myCoupon.validUntil': '有效期',

View File

@ -1,18 +1,50 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';
import '../../../../app/theme/app_colors.dart'; import '../../../../app/theme/app_colors.dart';
import '../../../../app/theme/app_typography.dart'; import '../../../../app/theme/app_typography.dart';
import '../../../../app/theme/app_spacing.dart'; import '../../../../app/theme/app_spacing.dart';
import '../../../../shared/widgets/genex_button.dart'; import '../../../../shared/widgets/genex_button.dart';
import '../../../../shared/widgets/status_tag.dart';
import '../../../../app/i18n/app_localizations.dart'; import '../../../../app/i18n/app_localizations.dart';
import '../../../../core/services/coupon_service.dart';
import '../../data/models/coupon_model.dart';
/// A4. - QR码/ + // /// A4. - QR码/ + /
/// ///
/// /使 /// CouponModel wallet_coupons_page String ID
/// 使 class MyCouponDetailPage extends StatefulWidget {
class MyCouponDetailPage extends StatelessWidget {
const MyCouponDetailPage({super.key}); const MyCouponDetailPage({super.key});
@override
State<MyCouponDetailPage> createState() => _MyCouponDetailPageState();
}
class _MyCouponDetailPageState extends State<MyCouponDetailPage> {
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<void> _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -23,186 +55,275 @@ class MyCouponDetailPage extends StatelessWidget {
), ),
title: Text(context.t('myCoupon.title')), title: Text(context.t('myCoupon.title')),
actions: [ actions: [
IconButton( if (_coupon != null)
icon: const Icon(Icons.more_horiz_rounded), IconButton(
onPressed: () => _showMoreOptions(context), icon: const Icon(Icons.more_horiz_rounded),
), onPressed: () => _showMoreOptions(context),
),
], ],
), ),
body: SingleChildScrollView( body: _isLoading
padding: AppSpacing.pagePadding, ? const Center(child: CircularProgressIndicator())
child: Column( : _coupon == null
children: [ ? Center(child: Text(context.t('common.notFound')))
const SizedBox(height: 16), : _buildBody(context, _coupon!),
);
}
// QR Code Card Widget _buildBody(BuildContext context, CouponModel coupon) {
Container( final qrData = 'GNX:${coupon.id}';
width: double.infinity, final isActive = coupon.status == 'in_circulation';
padding: const EdgeInsets.all(24), final expiryStr = '${coupon.expiryDate.year}/${coupon.expiryDate.month.toString().padLeft(2, '0')}/${coupon.expiryDate.day.toString().padLeft(2, '0')}';
decoration: BoxDecoration( final orderNo = 'GNX-${coupon.id.replaceAll('-', '').substring(0, 12).toUpperCase()}';
gradient: AppColors.cardGradient,
borderRadius: AppSpacing.borderRadiusLg, return SingleChildScrollView(
boxShadow: AppSpacing.shadowPrimary, padding: AppSpacing.pagePadding,
), child: Column(
child: Column( children: [
children: [ const SizedBox(height: 16),
// Brand + Status
Row( // QR / Barcode Card
mainAxisAlignment: MainAxisAlignment.spaceBetween, Container(
children: [ width: double.infinity,
Column( 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, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('Starbucks', style: AppTypography.bodySmall.copyWith( Text(coupon.brandName ?? '', style: AppTypography.bodySmall.copyWith(
color: Colors.white70, color: Colors.white70,
)), )),
Text('星巴克 \$25 礼品卡', style: AppTypography.h2.copyWith( Text(coupon.name, style: AppTypography.h2.copyWith(
color: Colors.white, color: Colors.white,
)), ), overflow: TextOverflow.ellipsis),
], ],
), ),
Container( ),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), const SizedBox(width: 8),
decoration: BoxDecoration( Container(
color: Colors.white24, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
borderRadius: AppSpacing.borderRadiusFull, decoration: BoxDecoration(
), color: Colors.white24,
child: Text(context.t('myCoupon.active'), style: AppTypography.caption.copyWith( borderRadius: AppSpacing.borderRadiusFull,
),
child: Text(
_statusLabel(context, coupon.status),
style: AppTypography.caption.copyWith(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.w600, 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(
Text( coupon.id.replaceAll('-', '').substring(0, 16).toUpperCase(),
context.t('myCoupon.showQrHint'), style: AppTypography.caption.copyWith(
style: AppTypography.bodySmall.copyWith(color: Colors.white70), color: Colors.white70,
), letterSpacing: 1.5,
fontWeight: FontWeight.w600,
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');
},
), ),
), ),
const SizedBox(width: 12), const SizedBox(height: 8),
Expanded(
child: GenexButton( Text(
label: context.t('myCoupon.sell'), context.t('myCoupon.showQrHint'),
icon: Icons.sell_rounded, style: AppTypography.bodySmall.copyWith(color: Colors.white70),
variant: GenexButtonVariant.outline, ),
onPressed: () { const SizedBox(height: 8),
Navigator.pushNamed(context, '/sell');
}, 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 // Info Card
Container( Container(
width: double.infinity, padding: AppSpacing.cardPadding,
padding: AppSpacing.cardPadding, decoration: BoxDecoration(
decoration: BoxDecoration( color: AppColors.surface,
color: AppColors.surface, borderRadius: AppSpacing.borderRadiusMd,
borderRadius: AppSpacing.borderRadiusMd, border: Border.all(color: AppColors.borderLight),
border: Border.all(color: AppColors.borderLight), ),
), child: Column(
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, _infoRow(context.t('myCoupon.faceValue'),
children: [ '\$${coupon.faceValue.toStringAsFixed(2)}'),
Text(context.t('myCoupon.usageNote'), style: AppTypography.labelMedium), const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: Divider()),
const SizedBox(height: 12), _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.useInStore')),
_ruleItem(context.t('myCoupon.useInTime')), _ruleItem(context.t('myCoupon.useInTime')),
_ruleItem(context.t('myCoupon.onePerVisit')), _ruleItem(context.t('myCoupon.onePerVisit')),
_ruleItem(context.t('myCoupon.noCash')), _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) { Widget _infoRow(String label, String value) {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -219,16 +340,20 @@ class MyCouponDetailPage extends StatelessWidget {
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.only(bottom: 8),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Padding(
width: 4, height: 4, padding: const EdgeInsets.only(top: 6),
decoration: const BoxDecoration( child: Container(
color: AppColors.textTertiary, width: 4, height: 4,
shape: BoxShape.circle, decoration: const BoxDecoration(
color: AppColors.textTertiary,
shape: BoxShape.circle,
),
), ),
), ),
const SizedBox(width: 8), 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), 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(), const Divider(),
_optionTile(Icons.receipt_long_rounded, context.t('myCoupon.viewTrades'), '', () {}), _optionTile(Icons.receipt_long_rounded, context.t('myCoupon.viewTrades'), '', () {}),
const Divider(), const Divider(),

View File

@ -35,10 +35,10 @@ class _WalletCouponsPageState extends State<WalletCouponsPage>
// Tab index backend status filter // Tab index backend status filter
static const _tabStatusFilters = <int, String?>{ static const _tabStatusFilters = <int, String?>{
0: null, // 0: null, //
1: 'listed', // 使 (listed / in_circulation) 1: 'in_circulation', // 使
2: 'sold', // 2: 'listed', //
3: 'expired', // 3: 'expired', //
}; };
@override @override
@ -259,7 +259,7 @@ class _WalletCouponsPageState extends State<WalletCouponsPage>
final displayStatus = _mapStatus(coupon.status); final displayStatus = _mapStatus(coupon.status);
return GestureDetector( return GestureDetector(
onTap: () => Navigator.pushNamed(context, '/coupon/mine/detail', arguments: coupon.id), onTap: () => Navigator.pushNamed(context, '/coupon/mine/detail', arguments: coupon),
child: Container( child: Container(
padding: AppSpacing.cardPadding, padding: AppSpacing.cardPadding,
decoration: BoxDecoration( decoration: BoxDecoration(

View File

@ -2,16 +2,39 @@ import 'package:flutter/material.dart';
import '../../../../app/theme/app_colors.dart'; import '../../../../app/theme/app_colors.dart';
import '../../../../app/theme/app_typography.dart'; import '../../../../app/theme/app_typography.dart';
import '../../../../app/theme/app_spacing.dart'; import '../../../../app/theme/app_spacing.dart';
import '../../../../shared/widgets/kyc_badge.dart';
import '../../../../app/i18n/app_localizations.dart'; import '../../../../app/i18n/app_localizations.dart';
import '../../../../core/services/coupon_service.dart';
import '../../data/models/holdings_summary_model.dart';
/// A7. /// A7.
/// ///
/// KYC等级标识 /// KYC等级标识
/// KYC认证Pro模式 /// KYC认证Pro模式
class ProfilePage extends StatelessWidget { class ProfilePage extends StatefulWidget {
const ProfilePage({super.key}); const ProfilePage({super.key});
@override
State<ProfilePage> createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
HoldingsSummaryModel? _holdingsSummary;
@override
void initState() {
super.initState();
_loadSummary();
}
Future<void> _loadSummary() async {
try {
final summary = await CouponApiService().getHoldingsSummary();
if (mounted) setState(() => _holdingsSummary = summary);
} catch (e) {
debugPrint('[ProfilePage] loadSummary error: $e');
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -37,6 +60,9 @@ class ProfilePage extends StatelessWidget {
])), ])),
SliverToBoxAdapter(child: _buildMenuSection(context.t('profile.trade'), [ 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, _MenuItem(Icons.receipt_long_rounded, context.t('wallet.records'), '', true,
onTap: () => Navigator.pushNamed(context, '/trading')), onTap: () => Navigator.pushNamed(context, '/trading')),
_MenuItem(Icons.storefront_rounded, context.t('tradingPage.pendingOrders'), context.t('status.onSale'), true, _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) { Widget _buildQuickStats(BuildContext context) {
final holdCount = _holdingsSummary?.count ?? 0;
final saved = _holdingsSummary?.totalSaved ?? 0;
final stats = [ final stats = [
(context.t('profile.holdCoupons'), '12'), (context.t('profile.holdCoupons'), '$holdCount', () => Navigator.pushNamed(context, '/wallet/coupons')),
(context.t('profile.trade'), '28'), (context.t('profile.trade'), '28', null),
(context.t('profile.saved'), '\$156'), (context.t('profile.saved'), '\$${saved.toStringAsFixed(0)}', null),
(context.t('profile.credit'), '750'), (context.t('profile.credit'), '750', null),
]; ];
return Container( return Container(
@ -162,12 +191,15 @@ class ProfilePage extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: stats.map((stat) { children: stats.map((stat) {
return Column( return GestureDetector(
children: [ onTap: stat.$3,
Text(stat.$2, style: AppTypography.h2.copyWith(color: AppColors.primary)), child: Column(
const SizedBox(height: 4), children: [
Text(stat.$1, style: AppTypography.caption), Text(stat.$2, style: AppTypography.h2.copyWith(color: AppColors.primary)),
], const SizedBox(height: 4),
Text(stat.$1, style: AppTypography.caption),
],
),
); );
}).toList(), }).toList(),
), ),