feat: track subscription program IDs and implement dynamic formatting for free rewards and point values

This commit is contained in:
Suherdy Yacob 2026-06-16 07:25:18 +07:00
parent b25f9e151a
commit 484ed297f1

View File

@ -13,6 +13,7 @@ class RewardsScreen extends StatefulWidget {
class _RewardsScreenState extends State<RewardsScreen> { class _RewardsScreenState extends State<RewardsScreen> {
double _userPoints = 0.0; // Main loyalty points balance shown in the top banner double _userPoints = 0.0; // Main loyalty points balance shown in the top banner
Map<int, double> _programPoints = {}; // Maps programId to card points Map<int, double> _programPoints = {}; // Maps programId to card points
Set<int> _subscriptionProgramIds = {}; // Record program IDs that are subscription cards
List<dynamic> _rewards = []; List<dynamic> _rewards = [];
bool _isLoading = true; bool _isLoading = true;
@ -56,6 +57,8 @@ class _RewardsScreenState extends State<RewardsScreen> {
} }
} }
final Set<int> subProgIds = {};
// 2. Process active subscription cards // 2. Process active subscription cards
for (final card in subscriptionCards) { for (final card in subscriptionCards) {
final pts = safeDouble(card['points']); final pts = safeDouble(card['points']);
@ -68,6 +71,7 @@ class _RewardsScreenState extends State<RewardsScreen> {
} }
if (progId != null) { if (progId != null) {
pointsMap[progId] = pts; pointsMap[progId] = pts;
subProgIds.add(progId);
if (!programIds.contains(progId)) { if (!programIds.contains(progId)) {
programIds.add(progId); programIds.add(progId);
} }
@ -84,6 +88,7 @@ class _RewardsScreenState extends State<RewardsScreen> {
setState(() { setState(() {
_userPoints = loyaltyPoints; _userPoints = loyaltyPoints;
_programPoints = pointsMap; _programPoints = pointsMap;
_subscriptionProgramIds = subProgIds;
_rewards = fetchedRewards; _rewards = fetchedRewards;
_isLoading = false; _isLoading = false;
}); });
@ -104,6 +109,25 @@ class _RewardsScreenState extends State<RewardsScreen> {
} }
} }
String _formatPoints(double points) {
final int val = points.round();
if (val >= 1000) {
final double k = val / 1000;
if (k % 1 == 0) {
return '${k.toInt()}K';
} else {
return '${k.toStringAsFixed(1)}K';
}
}
return val.toString();
}
String _formatWithCommas(double value) {
final int val = value.round();
final RegExp reg = RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))');
return val.toString().replaceAllMapped(reg, (Match match) => '${match[1]},');
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
@ -160,7 +184,7 @@ class _RewardsScreenState extends State<RewardsScreen> {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'${_userPoints.toStringAsFixed(0)} Points', '${_formatWithCommas(_userPoints)} Points',
style: theme.textTheme.headlineMedium?.copyWith( style: theme.textTheme.headlineMedium?.copyWith(
color: colorScheme.onPrimary, color: colorScheme.onPrimary,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -246,11 +270,19 @@ class _RewardsScreenState extends State<RewardsScreen> {
rewardProgramId = progVal; rewardProgramId = progVal;
} }
// Determine if it is a subscription reward
final isSubscription = rewardProgramId != null &&
_subscriptionProgramIds.contains(rewardProgramId);
// If it's a subscription reward and required points <= 1, treat it as FREE.
// For regular loyalty program, only treat <= 0 as FREE.
final bool isFree = reqPoints <= 0 || (isSubscription && reqPoints <= 1);
// Determine point balance and availability based on the specific program // Determine point balance and availability based on the specific program
final currentCardPoints = rewardProgramId != null final currentCardPoints = rewardProgramId != null
? (_programPoints[rewardProgramId] ?? 0.0) ? (_programPoints[rewardProgramId] ?? 0.0)
: 0.0; : 0.0;
final isAvailable = currentCardPoints >= reqPoints; final isAvailable = isFree ? true : (currentCardPoints >= reqPoints);
// Decide icon based on reward type // Decide icon based on reward type
IconData iconData = Icons.local_offer_rounded; IconData iconData = Icons.local_offer_rounded;
@ -292,28 +324,51 @@ class _RewardsScreenState extends State<RewardsScreen> {
), ),
), ),
padding: const EdgeInsets.symmetric(horizontal: 10), padding: const EdgeInsets.symmetric(horizontal: 10),
child: Column( child: isFree
mainAxisAlignment: MainAxisAlignment.center, ? Column(
children: [ mainAxisAlignment: MainAxisAlignment.center,
Text( children: [
reqPoints.toStringAsFixed(0), Icon(
style: theme.textTheme.titleLarge?.copyWith( Icons.card_giftcard_rounded,
color: colorScheme.primary, color: colorScheme.primary,
fontWeight: FontWeight.bold, size: 28,
fontFamily: 'monospace', ),
const SizedBox(height: 4),
Text(
'FREE',
style: theme.textTheme.labelSmall?.copyWith(
color: colorScheme.primary,
fontWeight: FontWeight.bold,
letterSpacing: 1.0,
),
),
],
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
_formatPoints(reqPoints),
style: theme.textTheme.titleLarge?.copyWith(
color: colorScheme.primary,
fontWeight: FontWeight.bold,
fontFamily: 'monospace',
),
),
),
const SizedBox(height: 2),
Text(
'PTS',
style: theme.textTheme.labelSmall?.copyWith(
color: colorScheme.primary.withValues(alpha: 0.8),
fontWeight: FontWeight.w900,
letterSpacing: 1.0,
),
),
],
), ),
),
const SizedBox(height: 2),
Text(
'PTS',
style: theme.textTheme.labelSmall?.copyWith(
color: colorScheme.primary.withValues(alpha: 0.8),
fontWeight: FontWeight.w900,
letterSpacing: 1.0,
),
),
],
),
), ),
// Custom Dashed Divider (Simulating Tear-off Voucher) // Custom Dashed Divider (Simulating Tear-off Voucher)
@ -420,7 +475,7 @@ class _RewardsScreenState extends State<RewardsScreen> {
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'Need ${(reqPoints - currentCardPoints).toStringAsFixed(0)} more pts', 'Need ${_formatWithCommas(reqPoints - currentCardPoints)} more pts',
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.error, color: colorScheme.error,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,