import 'package:flutter/material.dart'; import '../services/odoo_service.dart'; import '../utils/safe_cast.dart'; class RewardsScreen extends StatefulWidget { final int partnerId; const RewardsScreen({super.key, required this.partnerId}); @override State createState() => _RewardsScreenState(); } class _RewardsScreenState extends State { double _userPoints = 0.0; // Main loyalty points balance shown in the top banner Map _programPoints = {}; // Maps programId to card points List _rewards = []; bool _isLoading = true; @override void initState() { super.initState(); _fetchData(); } Future _fetchData() async { if (!mounted) return; setState(() => _isLoading = true); try { // Fetch both loyalty cards and subscription cards in parallel final results = await Future.wait([ OdooService().getLoyaltyCards(widget.partnerId), OdooService().getSubscriptionCards(widget.partnerId), ]); final loyaltyCards = results[0]; final subscriptionCards = results[1]; double loyaltyPoints = 0.0; final Map pointsMap = {}; final List programIds = []; // 1. Process main loyalty card if (loyaltyCards.isNotEmpty) { loyaltyPoints = safeDouble(loyaltyCards.first['points']); final prog = loyaltyCards.first['program_id']; int? progId; if (prog is List && prog.isNotEmpty) { progId = prog[0] as int?; } else if (prog is int) { progId = prog; } if (progId != null) { pointsMap[progId] = loyaltyPoints; programIds.add(progId); } } // 2. Process active subscription cards for (final card in subscriptionCards) { final pts = safeDouble(card['points']); final prog = card['program_id']; int? progId; if (prog is List && prog.isNotEmpty) { progId = prog[0] as int?; } else if (prog is int) { progId = prog; } if (progId != null) { pointsMap[progId] = pts; if (!programIds.contains(progId)) { programIds.add(progId); } } } // 3. Fetch rewards for all resolved program IDs List fetchedRewards = []; if (programIds.isNotEmpty) { fetchedRewards = await OdooService().getLoyaltyRewards(programIds); } if (mounted) { setState(() { _userPoints = loyaltyPoints; _programPoints = pointsMap; _rewards = fetchedRewards; _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 rewards. Please try again.')), ); } } } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; if (_isLoading) { return const Center(child: CircularProgressIndicator()); } return Scaffold( backgroundColor: Colors.transparent, // transparency allows MainShell gradient to show body: RefreshIndicator( onRefresh: _fetchData, child: Column( children: [ // Top Balance Banner Container( margin: const EdgeInsets.fromLTRB(16, 16, 16, 8), padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 24), decoration: BoxDecoration( gradient: LinearGradient( colors: [ colorScheme.primary, colorScheme.primary.withValues(alpha: 0.85), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), border: Border.all( color: colorScheme.secondary.withValues(alpha: 0.4), width: 1.5, ), boxShadow: [ BoxShadow( color: colorScheme.primary.withValues(alpha: 0.15), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Points Balance', style: theme.textTheme.labelMedium?.copyWith( color: colorScheme.onPrimary.withValues(alpha: 0.8), letterSpacing: 1.0, ), ), const SizedBox(height: 4), Text( '${_userPoints.toStringAsFixed(0)} Points', style: theme.textTheme.headlineMedium?.copyWith( color: colorScheme.onPrimary, fontWeight: FontWeight.bold, fontFamily: 'Lora', ), ), ], ), Icon( Icons.stars_rounded, color: colorScheme.secondary, size: 44, ), ], ), ), // Instruction Banner Padding( padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8), child: Row( children: [ Icon( Icons.info_outline_rounded, size: 16, color: colorScheme.primary.withValues(alpha: 0.75), ), const SizedBox(width: 8), Expanded( child: Text( 'Present your Member ID barcode or Phone Number to the cashier to claim these rewards.', style: theme.textTheme.bodySmall?.copyWith( color: theme.textTheme.bodyMedium?.color?.withValues(alpha: 0.75), height: 1.3, ), ), ), ], ), ), // Rewards List Expanded( child: _rewards.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.redeem_rounded, size: 64, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.3), ), const SizedBox(height: 16), Text( 'No rewards currently available', style: theme.textTheme.titleMedium?.copyWith( color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6), ), ), ], ), ) : ListView.builder( padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), itemCount: _rewards.length, itemBuilder: (context, index) { final reward = _rewards[index]; final reqPoints = safeDouble(reward['required_points']); final String desc = safeString(reward['description']) ?? 'Loyalty Reward'; final String type = safeString(reward['reward_type']) ?? 'product'; // Resolve reward's program ID and name final progVal = reward['program_id']; int? rewardProgramId; String programName = 'Reward'; if (progVal is List && progVal.isNotEmpty) { rewardProgramId = progVal[0] as int?; if (progVal.length > 1) { programName = progVal[1] as String; } } else if (progVal is int) { rewardProgramId = progVal; } // Determine point balance and availability based on the specific program final currentCardPoints = rewardProgramId != null ? (_programPoints[rewardProgramId] ?? 0.0) : 0.0; final isAvailable = currentCardPoints >= reqPoints; // Decide icon based on reward type IconData iconData = Icons.local_offer_rounded; if (type == 'product') { iconData = Icons.local_dining_rounded; } else if (type == 'shipping') { iconData = Icons.local_shipping_rounded; } return Container( margin: const EdgeInsets.only(bottom: 14), decoration: BoxDecoration( color: colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(16), border: Border.all( color: colorScheme.outline.withValues(alpha: 0.15), width: 1, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.04), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Left Points Ticket Column Container( width: 90, decoration: BoxDecoration( color: colorScheme.primary.withValues(alpha: 0.06), borderRadius: const BorderRadius.only( topLeft: Radius.circular(15), bottomLeft: Radius.circular(15), ), ), padding: const EdgeInsets.symmetric(horizontal: 10), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( reqPoints.toStringAsFixed(0), 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, ), ), ], ), ), // Custom Dashed Divider (Simulating Tear-off Voucher) CustomPaint( size: const Size(1, double.infinity), painter: _TicketDividerPainter( color: colorScheme.outline.withValues(alpha: 0.25), ), ), // Right Content Column Expanded( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ // Program Source Name Text( programName.toUpperCase(), style: theme.textTheme.labelSmall?.copyWith( color: colorScheme.primary.withValues(alpha: 0.65), fontWeight: FontWeight.bold, letterSpacing: 1.0, ), ), const SizedBox(height: 4), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( iconData, size: 18, color: colorScheme.secondary, ), const SizedBox(width: 8), Expanded( child: Text( desc, style: theme.textTheme.titleMedium?.copyWith( fontFamily: 'Lora', fontWeight: FontWeight.bold, height: 1.2, ), ), ), ], ), const SizedBox(height: 12), // Status indicator isAvailable ? Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4, ), decoration: BoxDecoration( color: Colors.green.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), border: Border.all( color: Colors.green.withValues(alpha: 0.25), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.check_circle_rounded, size: 12, color: Colors.green, ), const SizedBox(width: 4), Text( 'Available to Redeem', style: theme.textTheme.bodySmall?.copyWith( color: Colors.green[800], fontWeight: FontWeight.bold, fontSize: 10, ), ), ], ), ) : Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4, ), decoration: BoxDecoration( color: colorScheme.error.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(12), border: Border.all( color: colorScheme.error.withValues(alpha: 0.2), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.lock_rounded, size: 12, color: colorScheme.error.withValues(alpha: 0.8), ), const SizedBox(width: 4), Text( 'Need ${(reqPoints - currentCardPoints).toStringAsFixed(0)} more pts', style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.error, fontWeight: FontWeight.bold, fontSize: 10, ), ), ], ), ), ], ), ), ), ], ), ), ); }, ), ), ], ), ), ); } } class _TicketDividerPainter extends CustomPainter { final Color color; _TicketDividerPainter({required this.color}); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = color ..strokeWidth = 1.0 ..style = PaintingStyle.stroke; double maxH = size.height; double dashHeight = 4.0; double dashSpace = 4.0; double currentY = 0.0; while (currentY < maxH) { canvas.drawLine( Offset(0, currentY), Offset(0, currentY + dashHeight), paint, ); currentY += dashHeight + dashSpace; } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; }