import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import '../../../common/theme/theme.dart'; import '../../router/app_router.gr.dart'; // Models class PointCard { final int totalPoints; final int usedPoints; final String membershipLevel; PointCard({ required this.totalPoints, required this.usedPoints, required this.membershipLevel, }); int get availablePoints => totalPoints - usedPoints; } class Merchant { final String id; final String name; final String logo; final List categories; Merchant({ required this.id, required this.name, required this.logo, required this.categories, }); } class Category { final String id; final String name; final String icon; final List products; Category({ required this.id, required this.name, required this.icon, required this.products, }); } class Product { final String id; final String name; final String image; final int pointsRequired; final String description; final bool isPopular; final String? fullDescription; final String? validUntil; final String? termsAndConditions; Product({ required this.id, required this.name, required this.image, required this.pointsRequired, required this.description, this.isPopular = false, this.fullDescription, this.validUntil, this.termsAndConditions, }); } @RoutePage() class PoinPage extends StatefulWidget { const PoinPage({super.key}); @override State createState() => _PoinPageState(); } class _PoinPageState extends State { final ScrollController _scrollController = ScrollController(); // Sample data final PointCard pointCard = PointCard( totalPoints: 15000, usedPoints: 3500, membershipLevel: "Gold Member", ); final List merchants = [ Merchant( id: "1", name: "Starbucks", logo: "☕", categories: [ Category( id: "c1", name: "Beverages", icon: "🥤", products: [ Product( id: "p1", name: "Americano", image: "☕", pointsRequired: 2500, description: "Classic black coffee", isPopular: true, ), Product( id: "p2", name: "Cappuccino", image: "☕", pointsRequired: 3000, description: "Espresso with steamed milk", ), Product( id: "p3", name: "Frappuccino", image: "🥤", pointsRequired: 4000, description: "Iced blended coffee", ), ], ), Category( id: "c2", name: "Food", icon: "🍰", products: [ Product( id: "p4", name: "Croissant", image: "🥐", pointsRequired: 1500, description: "Buttery pastry", ), Product( id: "p5", name: "Sandwich", image: "🥪", pointsRequired: 3500, description: "Fresh deli sandwich", ), ], ), ], ), Merchant( id: "2", name: "McDonald's", logo: "🍔", categories: [ Category( id: "c3", name: "Burgers", icon: "🍔", products: [ Product( id: "p6", name: "Big Mac", image: "🍔", pointsRequired: 5000, description: "Iconic double burger", isPopular: true, ), Product( id: "p7", name: "Quarter Pounder", image: "🍔", pointsRequired: 4500, description: "Fresh beef quarter pound", ), ], ), Category( id: "c4", name: "Drinks", icon: "🥤", products: [ Product( id: "p8", name: "Coca Cola", image: "🥤", pointsRequired: 1000, description: "Ice cold soda", ), Product( id: "p9", name: "McCafe Coffee", image: "☕", pointsRequired: 20000, // High points untuk demonstrasi insufficient description: "Premium coffee blend", ), ], ), ], ), ]; Merchant? selectedMerchant; Map categoryKeys = {}; String? selectedMerchantId; // Track merchant ID untuk detect changes String? activeCategoryId; // Track active category @override void initState() { super.initState(); selectedMerchant = merchants.first; selectedMerchantId = selectedMerchant?.id; activeCategoryId = selectedMerchant?.categories.first.id; // Set first category as active _initializeCategoryKeys(); } void _initializeCategoryKeys() { categoryKeys.clear(); for (var category in selectedMerchant?.categories ?? []) { categoryKeys[category.id] = GlobalKey(); } } void _scrollToCategory(String categoryId) { // Update active category state FIRST setState(() { activeCategoryId = categoryId; }); // Tunggu sampai widget selesai rebuild dan keys ter-attach Future.delayed(Duration(milliseconds: 50), () { final key = categoryKeys[categoryId]; if (key?.currentContext != null) { print("Scrolling to category: $categoryId"); // Debug log try { Scrollable.ensureVisible( key!.currentContext!, duration: Duration(milliseconds: 500), curve: Curves.easeInOut, alignment: 0.1, // Position kategori sedikit dari atas ); } catch (e) { print("Error scrolling to category: $e"); } } else { print("Key not found for category: $categoryId"); // Debug log print("Available keys: ${categoryKeys.keys.toList()}"); // Debug log // Retry dengan delay lebih lama jika belum ready Future.delayed(Duration(milliseconds: 200), () { final retryKey = categoryKeys[categoryId]; if (retryKey?.currentContext != null) { Scrollable.ensureVisible( retryKey!.currentContext!, duration: Duration(milliseconds: 500), curve: Curves.easeInOut, alignment: 0.1, ); } }); } }); } void _onMerchantChanged(Merchant merchant) { if (selectedMerchantId == merchant.id) return; // Prevent unnecessary rebuilds setState(() { selectedMerchant = merchant; selectedMerchantId = merchant.id; // Reset active category to first category of new merchant activeCategoryId = merchant.categories.first.id; }); // Reinitialize category keys for new merchant - immediate call _initializeCategoryKeys(); // Auto scroll to top when merchant changes if (_scrollController.hasClients) { _scrollController.animateTo( 0, duration: Duration(milliseconds: 300), curve: Curves.easeOut, ); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColor.background, body: NestedScrollView( controller: _scrollController, headerSliverBuilder: (context, innerBoxIsScrolled) { return [ // Sticky AppBar SliverAppBar( elevation: 0, title: Text("Poin"), centerTitle: true, floating: false, pinned: true, // Made sticky snap: false, ), // Point Card Section SliverToBoxAdapter(child: _buildPointCard()), // Merchant Selection SliverToBoxAdapter(child: _buildMerchantSelection()), // Sticky Category Tabs SliverPersistentHeader( pinned: true, key: ValueKey( '${selectedMerchant?.id}_$activeCategoryId', ), // Force rebuild with key delegate: _StickyHeaderDelegate( child: _buildCategoryTabs(), height: 66, merchantId: selectedMerchant?.id ?? '', // Pass merchant ID activeCategoryId: activeCategoryId, // Pass active category ID ), ), ]; }, body: selectedMerchant == null ? SizedBox.shrink() : ListView.builder( padding: EdgeInsets.only(top: 16), itemCount: selectedMerchant!.categories.length, itemBuilder: (context, index) { final category = selectedMerchant!.categories[index]; return _buildCategorySection(category); }, ), ), ); } Widget _buildPointCard() { return Container( margin: EdgeInsets.all(16), padding: EdgeInsets.all(20), decoration: BoxDecoration( gradient: LinearGradient( colors: [AppColor.primary, AppColor.primaryDark], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: AppColor.primary.withOpacity(0.3), blurRadius: 12, offset: Offset(0, 6), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( pointCard.membershipLevel, style: AppStyle.sm.copyWith( color: AppColor.textWhite.withOpacity(0.9), fontWeight: FontWeight.w500, ), ), SizedBox(height: 4), Text( "${pointCard.availablePoints}", style: AppStyle.h2.copyWith( color: AppColor.textWhite, fontWeight: FontWeight.w700, ), ), Text( "Available Points", style: AppStyle.sm.copyWith( color: AppColor.textWhite.withOpacity(0.9), ), ), ], ), Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( color: AppColor.white.withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), child: Icon( Icons.stars_rounded, color: AppColor.textWhite, size: 32, ), ), ], ), SizedBox(height: 16), Container( height: 8, decoration: BoxDecoration( color: AppColor.white.withOpacity(0.3), borderRadius: BorderRadius.circular(4), ), child: FractionallySizedBox( widthFactor: (pointCard.totalPoints - pointCard.usedPoints) / pointCard.totalPoints, alignment: Alignment.centerLeft, child: Container( decoration: BoxDecoration( color: AppColor.textWhite, borderRadius: BorderRadius.circular(4), ), ), ), ), SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Used: ${pointCard.usedPoints}", style: AppStyle.xs.copyWith( color: AppColor.textWhite.withOpacity(0.8), ), ), Text( "Total: ${pointCard.totalPoints}", style: AppStyle.xs.copyWith( color: AppColor.textWhite.withOpacity(0.8), ), ), ], ), ], ), ); } Widget _buildMerchantSelection() { return Container( margin: EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Select Merchant", style: AppStyle.lg.copyWith( fontWeight: FontWeight.w600, color: AppColor.textPrimary, ), ), SizedBox(height: 12), Container( height: 80, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: merchants.length, itemBuilder: (context, index) { final merchant = merchants[index]; final isSelected = selectedMerchant?.id == merchant.id; return GestureDetector( onTap: () => _onMerchantChanged(merchant), child: Container( width: 80, margin: EdgeInsets.only(right: 12), decoration: BoxDecoration( color: isSelected ? AppColor.primary : AppColor.white, borderRadius: BorderRadius.circular(12), border: Border.all( color: isSelected ? AppColor.primary : AppColor.border, width: 2, ), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(merchant.logo, style: TextStyle(fontSize: 24)), SizedBox(height: 4), Text( merchant.name, style: AppStyle.xs.copyWith( color: isSelected ? AppColor.textWhite : AppColor.textPrimary, fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ), ); }, ), ), ], ), ); } Widget _buildCategoryTabs() { if (selectedMerchant == null) return SizedBox.shrink(); return Container( color: AppColor.background, // Background untuk sticky header padding: EdgeInsets.symmetric(vertical: 8), child: Container( height: 50, child: ListView.builder( scrollDirection: Axis.horizontal, padding: EdgeInsets.symmetric(horizontal: 16), itemCount: selectedMerchant!.categories.length, itemBuilder: (context, index) { final category = selectedMerchant!.categories[index]; final isActive = activeCategoryId == category.id; // Check if this category is active return GestureDetector( onTap: () => _scrollToCategory(category.id), child: Container( margin: EdgeInsets.only(right: 12), padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12), decoration: BoxDecoration( color: isActive ? AppColor.primary : AppColor.white, // Change background when active borderRadius: BorderRadius.circular(25), border: Border.all( color: isActive ? AppColor.primary : AppColor.border, // Change border when active width: 2, ), boxShadow: [ BoxShadow( color: AppColor.textLight.withOpacity(0.1), blurRadius: 4, offset: Offset(0, 2), ), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(category.icon, style: TextStyle(fontSize: 16)), SizedBox(width: 6), Text( category.name, style: AppStyle.sm.copyWith( fontWeight: FontWeight.w500, color: isActive ? AppColor.textWhite : AppColor .textPrimary, // Change text color when active ), ), ], ), ), ); }, ), ), ); } Widget _buildCategorySection(Category category) { return Container( key: categoryKeys[category.id], margin: EdgeInsets.only(bottom: 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ Text(category.icon, style: TextStyle(fontSize: 20)), SizedBox(width: 8), Text( category.name, style: AppStyle.xl.copyWith( fontWeight: FontWeight.w600, color: AppColor.textPrimary, ), ), ], ), ), SizedBox(height: 12), // Fixed height yang lebih besar untuk menghindari overflow Container( height: 240, // Increased from 200 to 240 child: ListView.builder( scrollDirection: Axis.horizontal, padding: EdgeInsets.symmetric(horizontal: 16), itemCount: category.products.length, itemBuilder: (context, index) { final product = category.products[index]; return _buildProductCard(product); }, ), ), ], ), ); } Widget _buildProductCard(Product product) { final canRedeem = pointCard.availablePoints >= product.pointsRequired; final pointsShortage = canRedeem ? 0 : product.pointsRequired - pointCard.availablePoints; return Container( width: 160, margin: EdgeInsets.only(right: 12), child: Stack( children: [ Container( decoration: BoxDecoration( color: AppColor.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: AppColor.textLight.withOpacity(0.15), blurRadius: 8, offset: Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Product Image Container( height: 90, // Reduced from 100 to give more space below width: double.infinity, decoration: BoxDecoration( color: AppColor.backgroundLight, borderRadius: BorderRadius.vertical( top: Radius.circular(16), ), ), child: Stack( children: [ Center( child: Text( product.image, style: TextStyle(fontSize: 36), // Reduced from 40 ), ), if (product.isPopular) Positioned( top: 6, // Adjusted position right: 6, child: Container( padding: EdgeInsets.symmetric( horizontal: 6, // Reduced padding vertical: 3, ), decoration: BoxDecoration( color: AppColor.warning, borderRadius: BorderRadius.circular(10), ), child: Text( "Popular", style: AppStyle.xs.copyWith( color: AppColor.white, fontWeight: FontWeight.w600, fontSize: 10, // Smaller font ), ), ), ), ], ), ), // Product Info - Made more flexible Expanded( child: Padding( padding: EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( product.name, style: AppStyle.md.copyWith( fontWeight: FontWeight.w600, color: canRedeem ? AppColor.textPrimary : AppColor.textLight, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), SizedBox(height: 4), Expanded( child: Text( product.description, style: AppStyle.xs.copyWith( color: canRedeem ? AppColor.textSecondary : AppColor.textLight, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), SizedBox(height: 8), Row( children: [ Icon( Icons.stars, size: 14, // Reduced size color: canRedeem ? AppColor.warning : AppColor.textLight, ), SizedBox(width: 4), Expanded( child: Text( "${product.pointsRequired}", style: AppStyle.sm.copyWith( fontWeight: FontWeight.w600, color: canRedeem ? AppColor.primary : AppColor.textLight, ), overflow: TextOverflow.ellipsis, ), ), ], ), SizedBox(height: 8), SizedBox( width: double.infinity, height: 32, // Reduced from 36 child: ElevatedButton( onPressed: canRedeem ? () => _redeemProduct(product) : null, style: ElevatedButton.styleFrom( backgroundColor: canRedeem ? AppColor.primary : AppColor.textLight, foregroundColor: AppColor.white, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: FittedBox( child: Text( canRedeem ? "Redeem" : "Insufficient", style: AppStyle.xs.copyWith( // Changed to xs fontWeight: FontWeight.w600, color: AppColor.white, ), ), ), ), ), ], ), ), ), ], ), ), // Overlay untuk insufficient points if (!canRedeem) Container( decoration: BoxDecoration( color: AppColor.textLight.withOpacity(0.7), borderRadius: BorderRadius.circular(16), ), child: Center( child: Container( padding: EdgeInsets.symmetric( horizontal: 12, vertical: 10, ), // Reduced padding decoration: BoxDecoration( color: AppColor.white, borderRadius: BorderRadius.circular(12), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.lock_outline, color: AppColor.textSecondary, size: 20, // Reduced size ), SizedBox(height: 4), Text( "Need ${pointsShortage}", style: AppStyle.xs.copyWith( fontWeight: FontWeight.w600, color: AppColor.textSecondary, fontSize: 10, // Smaller font ), textAlign: TextAlign.center, ), Text( "more points", style: AppStyle.xs.copyWith( color: AppColor.textSecondary, fontSize: 10, // Smaller font ), textAlign: TextAlign.center, ), ], ), ), ), ), ], ), ); } void _redeemProduct(Product product) { context.router.push( ProductRedeemRoute( product: product, merchant: selectedMerchant!, pointCard: pointCard, ), ); } @override void dispose() { _scrollController.dispose(); super.dispose(); } } // Custom SliverPersistentHeaderDelegate untuk sticky category tabs class _StickyHeaderDelegate extends SliverPersistentHeaderDelegate { final Widget child; final double height; final String merchantId; // Add merchant ID to track changes final String? activeCategoryId; // Add active category to track changes _StickyHeaderDelegate({ required this.child, required this.height, required this.merchantId, // Track merchant changes required this.activeCategoryId, // Track category changes }); @override Widget build( BuildContext context, double shrinkOffset, bool overlapsContent, ) { return child; } @override double get maxExtent => height; @override double get minExtent => height; @override bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { // Always rebuild when merchant OR active category changes if (oldDelegate is _StickyHeaderDelegate) { bool merchantChanged = oldDelegate.merchantId != merchantId; bool categoryChanged = oldDelegate.activeCategoryId != activeCategoryId; print( "shouldRebuild - Merchant changed: $merchantChanged, Category changed: $categoryChanged", ); print( "Old merchant: ${oldDelegate.merchantId}, New merchant: $merchantId", ); print( "Old category: ${oldDelegate.activeCategoryId}, New category: $activeCategoryId", ); return merchantChanged || categoryChanged; } return true; } }