design: redesign subscription list as distinct premium cards and add client-side filtering to avoid overlap

This commit is contained in:
Suherdy Yacob 2026-06-14 11:02:10 +07:00
parent e73fc63e87
commit eae35b20cd
2 changed files with 132 additions and 102 deletions

View File

@ -37,10 +37,28 @@ class _LoyaltyDashboardState extends State<LoyaltyDashboard> {
OdooService().getCmsContent(), OdooService().getCmsContent(),
]); ]);
final cards = results[0] as List<dynamic>; final rawCards = results[0] as List<dynamic>;
final subs = results[1] as List<dynamic>; final rawSubs = results[1] as List<dynamic>;
final cms = results[2] as Map<String, dynamic>; final cms = results[2] as Map<String, dynamic>;
final List<dynamic> cards = [];
final List<dynamic> subs = [...rawSubs];
for (var card in rawCards) {
final progName = (card['program_id']?[1] as String? ?? '').toLowerCase();
final isSub = progName.contains('subscription') ||
card['subscription_start_date'] != null ||
card['subscription_end_date'] != null;
if (isSub) {
final code = card['code'];
if (!subs.any((s) => s['code'] == code)) {
subs.add(card);
}
} else {
cards.add(card);
}
}
if (mounted) { if (mounted) {
setState(() { setState(() {
_loyaltyCards = cards; _loyaltyCards = cards;

View File

@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../theme/app_theme.dart'; import '../theme/app_theme.dart';
/// Compact "My Subscriptions" list displayed on the home tab below the loyalty card. /// "My Subscriptions" list displayed on the home tab below the loyalty card.
/// Each row shows the subscription name, active/expired badge, validity period, and card code. /// Renders each subscription as a beautiful standalone card visually distinct
/// from the points-based loyalty card.
class SubscriptionListWidget extends StatelessWidget { class SubscriptionListWidget extends StatelessWidget {
final List<dynamic> subscriptions; final List<dynamic> subscriptions;
@ -16,44 +17,32 @@ class SubscriptionListWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 20, 16, 10), padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
child: Text( child: Text(
'MY SUBSCRIPTIONS', 'MY SUBSCRIPTIONS',
style: Theme.of(context).textTheme.labelLarge?.copyWith( style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: AppTheme.onSurfaceVariant, color: AppTheme.onSurfaceVariant,
letterSpacing: 1.2, letterSpacing: 1.2,
fontSize: 11, fontSize: 11,
fontWeight: FontWeight.bold,
), ),
), ),
), ),
Container( ...subscriptions.map((sub) => _SubscriptionCard(sub: sub)),
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: const BoxDecoration(
color: AppTheme.surfaceContainerLow,
),
child: Column(
children: List.generate(subscriptions.length, (index) {
final sub = subscriptions[index];
final isLast = index == subscriptions.length - 1;
return _SubscriptionTile(sub: sub, isLast: isLast);
}),
),
),
], ],
); );
} }
} }
class _SubscriptionTile extends StatelessWidget { class _SubscriptionCard extends StatelessWidget {
final dynamic sub; final dynamic sub;
final bool isLast;
const _SubscriptionTile({required this.sub, required this.isLast}); const _SubscriptionCard({required this.sub});
/// Determine active/expired status from subscription_end_date. /// Determine active/expired status from subscription_end_date.
bool _isActive() { bool _isActive() {
final endRaw = sub['subscription_end_date']; final endRaw = sub['subscription_end_date'];
if (endRaw == null || endRaw == false) return true; // no end date = no expiry if (endRaw == null || endRaw == false) return true; // no end date = active
try { try {
final endDate = DateTime.parse(endRaw.toString()); final endDate = DateTime.parse(endRaw.toString());
return endDate.isAfter(DateTime.now()); return endDate.isAfter(DateTime.now());
@ -94,94 +83,117 @@ class _SubscriptionTile extends StatelessWidget {
final active = _isActive(); final active = _isActive();
return Container( return Container(
margin: const EdgeInsets.fromLTRB(16, 8, 16, 8),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
border: isLast color: AppTheme.surfaceContainerLowest,
? null border: Border.all(
: const Border( color: active ? AppTheme.secondaryContainer : AppTheme.surfaceContainerHighest,
bottom: BorderSide( width: 1.5,
color: AppTheme.surfaceContainer, ),
width: 1, borderRadius: BorderRadius.zero,
),
),
), ),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Column(
child: Row( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
// Icon Row(
Container( mainAxisAlignment: MainAxisAlignment.spaceBetween,
padding: const EdgeInsets.all(10), children: [
decoration: BoxDecoration( Row(
color: active children: [
? AppTheme.secondaryContainer.withValues(alpha: 0.35) Icon(
: AppTheme.surfaceContainer, Icons.card_membership_rounded,
shape: BoxShape.rectangle, size: 18,
), color: active ? AppTheme.secondary : AppTheme.outlineVariant,
child: Icon( ),
Icons.verified_rounded, const SizedBox(width: 8),
size: 22,
color: active ? AppTheme.secondary : AppTheme.outlineVariant,
),
),
const SizedBox(width: 14),
// Name + dates
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
programName,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.onSurface,
),
),
const SizedBox(height: 3),
Text(
'$startDate$endDate',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: AppTheme.onSurfaceVariant,
),
),
if (code.isNotEmpty) ...[
const SizedBox(height: 2),
Text( Text(
code, 'SUBSCRIPTION CARD',
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.labelLarge?.copyWith(
color: AppTheme.outlineVariant, color: active ? AppTheme.secondary : AppTheme.outlineVariant,
fontFamily: 'monospace', fontWeight: FontWeight.bold,
fontSize: 11, fontSize: 10,
letterSpacing: 1.0,
), ),
), ),
], ],
],
),
),
const SizedBox(width: 8),
// Active / Expired badge
Container(
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4),
decoration: BoxDecoration(
color: active
? const Color(0xFF1B5E20).withValues(alpha: 0.12)
: const Color(0xFFB02500).withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(4),
),
child: Text(
active ? 'ACTIVE' : 'EXPIRED',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
letterSpacing: 0.8,
color: active
? const Color(0xFF2E7D32)
: const Color(0xFFB02500),
), ),
), Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: active
? const Color(0xFF1B5E20).withValues(alpha: 0.10)
: const Color(0xFFB02500).withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(4),
),
child: Text(
active ? 'ACTIVE' : 'EXPIRED',
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.bold,
letterSpacing: 0.8,
color: active
? const Color(0xFF2E7D32)
: const Color(0xFFB02500),
),
),
),
],
),
const SizedBox(height: 14),
Text(
programName,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.onSurface,
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Card Number',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: AppTheme.onSurfaceVariant,
fontSize: 10,
),
),
const SizedBox(height: 2),
Text(
code.isNotEmpty ? code : 'N/A',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontFamily: 'monospace',
fontWeight: FontWeight.bold,
color: AppTheme.onSurface,
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'Validity Period',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: AppTheme.onSurfaceVariant,
fontSize: 10,
),
),
const SizedBox(height: 2),
Text(
'$startDate - $endDate',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: AppTheme.onSurface,
fontWeight: FontWeight.w600,
),
),
],
),
],
), ),
], ],
), ),