import 'package:flutter/material.dart'; import '../services/odoo_service.dart'; import '../widgets/carousel_widget.dart'; import '../widgets/promo_card_widget.dart'; import '../widgets/subscription_list_widget.dart'; /// Home tab — shows loyalty card, subscriptions, carousel, and promo highlights. /// Notification polling and AppBar are handled by MainShell. class LoyaltyDashboard extends StatefulWidget { final int partnerId; const LoyaltyDashboard({super.key, required this.partnerId}); @override State createState() => _LoyaltyDashboardState(); } class _LoyaltyDashboardState extends State { List _loyaltyCards = []; List _subscriptions = []; List _carouselSlides = []; List _promos = []; bool _isLoading = true; @override void initState() { super.initState(); _fetchAll(); } Future _fetchAll() async { setState(() => _isLoading = true); try { final results = await Future.wait([ OdooService().getLoyaltyCards(widget.partnerId), OdooService().getSubscriptionCards(widget.partnerId), OdooService().getCmsContent(), ]); final rawCards = results[0] as List; final rawSubs = results[1] as List; final cms = results[2] as Map; if (mounted) { setState(() { _loyaltyCards = rawCards; _subscriptions = rawSubs; _carouselSlides = (cms['carousel'] as List?) ?? []; _promos = (cms['promos'] as List?) ?? []; _isLoading = false; }); } } catch (e) { if (mounted) { setState(() => _isLoading = false); final errStr = e.toString().toLowerCase(); final isSessionExpired = e.runtimeType.toString().contains('SessionExpired') || errStr.contains('session expired') || errStr.contains('session_expired'); if (!isSessionExpired) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Failed to load dashboard. Please try again.')), ); } } } } @override Widget build(BuildContext context) { if (_isLoading) { return const Center(child: CircularProgressIndicator()); } return RefreshIndicator( onRefresh: _fetchAll, child: ListView( children: [ // ── Loyalty Card ───────────────────────────────────────────── if (_loyaltyCards.isEmpty) Padding( padding: const EdgeInsets.fromLTRB(24, 32, 24, 0), child: Text( 'No active loyalty card yet.', style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center, ), ) else ..._loyaltyCards.map((card) => _LoyaltyCardTile(card: card)), // ── Subscriptions ───────────────────────────────────────────── if (_subscriptions.isNotEmpty) SubscriptionListWidget(subscriptions: _subscriptions), const SizedBox(height: 20), // ── Carousel ────────────────────────────────────────────────── if (_carouselSlides.isNotEmpty) ...[ CarouselWidget(slides: _carouselSlides), const SizedBox(height: 24), ], // ── Promo Highlights ───────────────────────────────────────── if (_promos.isNotEmpty) ...[ PromoCardRow(promos: _promos), const SizedBox(height: 24), ], ], ), ); } } class _LoyaltyCardTile extends StatelessWidget { final dynamic card; const _LoyaltyCardTile({required this.card}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final programName = (card['program_id']?[1] as String? ?? '').toLowerCase(); String tier = 'MEMBER'; if (programName.contains('silver')) tier = 'SILVER MEMBER'; if (programName.contains('gold')) tier = 'GOLD MEMBER'; if (programName.contains('platinum')) tier = 'PLATINUM MEMBER'; final onPrimary = colorScheme.onPrimary; final accentColor = colorScheme.secondary; return Container( margin: const EdgeInsets.fromLTRB(16, 16, 16, 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ colorScheme.primary, Color.lerp(colorScheme.primary, Colors.black, 0.22) ?? colorScheme.primary, ], ), border: Border.all( color: accentColor.withValues(alpha: 0.45), width: 1.5, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.16), blurRadius: 18, offset: const Offset(0, 8), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(20), child: Stack( children: [ // Decorative background patterns Positioned( right: -40, top: -40, child: Container( width: 180, height: 180, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white.withValues(alpha: 0.04), ), ), ), Positioned( right: 20, bottom: -80, child: Container( width: 200, height: 200, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white.withValues(alpha: 0.03), ), ), ), Positioned( left: -30, bottom: -40, child: Container( width: 130, height: 130, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.black.withValues(alpha: 0.06), ), ), ), // Card Content Padding( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Top Row: Logo / Star + Tier Badge Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Icon( Icons.stars_rounded, color: accentColor, size: 22, ), const SizedBox(width: 8), Text( 'MAPAN CLUB', style: theme.textTheme.labelMedium?.copyWith( color: onPrimary.withValues(alpha: 0.85), fontWeight: FontWeight.w900, letterSpacing: 2.0, ), ), ], ), Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( color: accentColor, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Text( tier, style: theme.textTheme.labelLarge?.copyWith( color: colorScheme.primary.computeLuminance() > 0.5 ? Colors.black : Colors.white, fontWeight: FontWeight.bold, fontSize: 9, letterSpacing: 0.8, ), ), ), ], ), const SizedBox(height: 36), // Middle Section: Card Label Text( 'MEMBER ID', style: theme.textTheme.labelSmall?.copyWith( color: onPrimary.withValues(alpha: 0.6), fontWeight: FontWeight.bold, letterSpacing: 1.5, ), ), const SizedBox(height: 8), // Card Number (Membership Code) Text( '${card['code'] ?? 'N/A'}', style: theme.textTheme.titleMedium?.copyWith( color: onPrimary, fontFamily: 'monospace', fontSize: 16, fontWeight: FontWeight.bold, shadows: [ Shadow( color: Colors.black.withValues(alpha: 0.25), offset: const Offset(1, 1), blurRadius: 2, ), ], ), ), const SizedBox(height: 24), // Divider line Container( height: 1, color: onPrimary.withValues(alpha: 0.12), ), const SizedBox(height: 16), // Bottom Section: Points Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'AVAILABLE POINTS', style: theme.textTheme.bodySmall?.copyWith( color: onPrimary.withValues(alpha: 0.6), fontWeight: FontWeight.bold, fontSize: 9, letterSpacing: 1.2, ), ), const SizedBox(height: 6), Row( children: [ Icon( Icons.restaurant_rounded, color: accentColor, size: 14, ), const SizedBox(width: 6), Text( 'Dine & Save', style: theme.textTheme.bodyMedium?.copyWith( color: onPrimary.withValues(alpha: 0.8), fontStyle: FontStyle.italic, fontSize: 12, ), ), ], ), ], ), Expanded( child: Align( alignment: Alignment.bottomRight, child: FittedBox( fit: BoxFit.scaleDown, child: Text( '${card['points'] ?? 0} pts', style: theme.textTheme.headlineLarge?.copyWith( color: accentColor, fontWeight: FontWeight.w900, fontSize: 32, ), ), ), ), ), ], ), ], ), ), ], ), ), ); } }