import 'package:flutter/material.dart'; import '../utils/safe_cast.dart'; /// "My Subscriptions" list displayed on the home tab below the loyalty card. /// Renders each subscription as a beautiful standalone ticket card visually distinct /// from the points-based loyalty card. class SubscriptionListWidget extends StatelessWidget { final List subscriptions; const SubscriptionListWidget({super.key, required this.subscriptions}); @override Widget build(BuildContext context) { if (subscriptions.isEmpty) return const SizedBox.shrink(); final theme = Theme.of(context); final colorScheme = theme.colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 24, 16, 8), child: Text( 'MY SUBSCRIPTIONS', style: theme.textTheme.labelLarge?.copyWith( color: colorScheme.onSurfaceVariant, letterSpacing: 1.2, fontSize: 11, fontWeight: FontWeight.bold, ), ), ), ...subscriptions.map((sub) => _SubscriptionCard(sub: sub)), ], ); } } class _SubscriptionCard extends StatelessWidget { final dynamic sub; const _SubscriptionCard({required this.sub}); /// Determine active/expired status from subscription_end_date. bool _isActive() { final endRaw = sub['subscription_end_date']; if (endRaw == null || endRaw == false) return true; // no end date = active try { final endDate = DateTime.parse(endRaw.toString()); return endDate.isAfter(DateTime.now()); } catch (_) { return true; } } /// Format a date string (YYYY-MM-DD) to a readable label. String _formatDate(dynamic raw) { if (raw == null || raw == false) return '—'; try { final dt = DateTime.parse(raw.toString()); return '${dt.day.toString().padLeft(2, '0')} ' '${_monthName(dt.month)} ' '${dt.year}'; } catch (_) { return raw.toString(); } } String _monthName(int m) { const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ]; return months[m - 1]; } @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final programName = sub['program_id'] is List ? (safeString(sub['program_id'][1]) ?? 'Subscription') : 'Subscription'; final code = safeString(sub['code']) ?? ''; final startDate = _formatDate(sub['subscription_start_date']); final endDate = _formatDate(sub['subscription_end_date']); final active = _isActive(); return Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), child: PhysicalShape( clipper: TicketClipper(), color: colorScheme.surfaceContainerLowest, elevation: 3, shadowColor: Colors.black.withValues(alpha: 0.15), child: Container( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Top Section: Title & Status Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Icon( Icons .local_activity_rounded, // Distinct ticket pass icon size: 18, color: active ? colorScheme.primary : colorScheme.outline, ), const SizedBox(width: 8), Text( 'SUBSCRIPTION PASS', // Distinct title style: theme.textTheme.labelLarge?.copyWith( color: active ? colorScheme.primary : colorScheme.outline, fontWeight: FontWeight.bold, fontSize: 10, letterSpacing: 1.2, ), ), ], ), 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(20), ), 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: 18), // Dashed Separator CustomPaint( size: const Size(double.infinity, 1), painter: DashedLinePainter( color: colorScheme.outline.withValues(alpha: 0.25), ), ), const SizedBox(height: 18), // Bottom Section: Details Text( programName, style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: colorScheme.onSurface, fontFamily: 'serif', ), ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Pass Code', // Distinct field label style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, fontSize: 10, ), ), const SizedBox(height: 2), Text( code.isNotEmpty ? code : 'N/A', style: theme.textTheme.bodyMedium?.copyWith( fontFamily: 'monospace', fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), ], ), if (sub['points'] != null) Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( 'Claim Balance', style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, fontSize: 10, ), ), const SizedBox(height: 2), Text( '${(sub['points'] as num).toDouble() % 1 == 0 ? (sub['points'] as num).toInt() : sub['points']} Claims', style: theme.textTheme.bodyMedium?.copyWith( color: colorScheme.primary, fontWeight: FontWeight.bold, ), ), ], ), ], ), if (sub['subscription_start_date'] != null && sub['subscription_start_date'] != false) ...[ const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Valid From', style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, fontSize: 10, ), ), const SizedBox(height: 2), Text( startDate, style: theme.textTheme.bodyMedium?.copyWith( color: colorScheme.onSurface, fontWeight: FontWeight.w600, ), ), ], ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( 'Expires On', style: theme.textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, fontSize: 10, ), ), const SizedBox(height: 2), Text( endDate, style: theme.textTheme.bodyMedium?.copyWith( color: colorScheme.onSurface, fontWeight: FontWeight.w600, ), ), ], ), ], ), ], ], ), ), ), ); } } /// Custom Clipper for a ticket-like appearance with side notches. class TicketClipper extends CustomClipper { @override Path getClip(Size size) { final path = Path(); path.lineTo(0, 0); path.lineTo(size.width, 0); // Right side notch at height 49 (approx. center of vertical separator space) const double notchY = 49.0; const double notchRadius = 8.0; path.lineTo(size.width, notchY - notchRadius); path.arcToPoint( Offset(size.width, notchY + notchRadius), radius: const Radius.circular(notchRadius), clockwise: false, ); path.lineTo(size.width, size.height); path.lineTo(0, size.height); // Left side notch path.lineTo(0, notchY + notchRadius); path.arcToPoint( const Offset(0, notchY - notchRadius), radius: const Radius.circular(notchRadius), clockwise: false, ); path.close(); return path; } @override bool shouldReclip(CustomClipper oldClipper) => false; } /// Painter to draw a clean dashed divider line. class DashedLinePainter extends CustomPainter { final Color color; DashedLinePainter({required this.color}); @override void paint(Canvas canvas, Size size) { double dashWidth = 5.0; double dashSpace = 4.0; double startX = 0.0; final paint = Paint() ..color = color ..strokeWidth = 1.0; while (startX < size.width) { canvas.drawLine(Offset(startX, 0), Offset(startX + dashWidth, 0), paint); startX += dashWidth + dashSpace; } } @override bool shouldRepaint(CustomPainter oldDelegate) => false; }