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 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 - Indonesian content final PointCard pointCard = PointCard( totalPoints: 15000, usedPoints: 3500, membershipLevel: "Member Emas", ); final List categories = [ Category( id: "c1", name: "Minuman", icon: "🥤", products: [ Product( id: "p1", name: "Es Teh Manis", image: "🧊", pointsRequired: 1500, description: "Teh manis dingin segar", isPopular: true, ), Product( id: "p2", name: "Kopi Susu", image: "☕", pointsRequired: 2000, description: "Kopi dengan susu creamy", ), Product( id: "p3", name: "Jus Jeruk", image: "🍊", pointsRequired: 2500, description: "Jus jeruk segar alami", ), ], ), Category( id: "c2", name: "Makanan", icon: "🍽️", products: [ Product( id: "p4", name: "Nasi Gudeg", image: "🍛", pointsRequired: 4000, description: "Gudeg Jogja autentik", isPopular: true, ), Product( id: "p5", name: "Gado-gado", image: "🥗", pointsRequired: 3500, description: "Sayuran dengan bumbu kacang", ), Product( id: "p6", name: "Bakso", image: "🍲", pointsRequired: 3000, description: "Bakso sapi dengan mie", ), ], ), Category( id: "c3", name: "Cemilan", icon: "🍪", products: [ Product( id: "p7", name: "Keripik Singkong", image: "🥔", pointsRequired: 1000, description: "Keripik singkong renyah", ), Product( id: "p8", name: "Onde-onde", image: "🍡", pointsRequired: 1500, description: "Onde-onde isi kacang hijau", ), Product( id: "p9", name: "Pisang Goreng", image: "🍌", pointsRequired: 1200, description: "Pisang goreng krispy", ), ], ), Category( id: "c4", name: "Voucher", icon: "🎟️", products: [ Product( id: "p10", name: "Diskon 50%", image: "🏷️", pointsRequired: 5000, description: "Potongan harga 50% untuk semua menu", isPopular: true, ), Product( id: "p11", name: "Gratis Ongkir", image: "🚚", pointsRequired: 2000, description: "Bebas ongkos kirim untuk pesanan apapun", ), Product( id: "p12", name: "Buy 1 Get 1", image: "🎁", pointsRequired: 25000, // High points untuk demonstrasi insufficient description: "Beli 1 gratis 1 untuk minuman", ), ], ), ]; Map categoryKeys = {}; String? activeCategoryId; // Track active category @override void initState() { super.initState(); activeCategoryId = categories.first.id; // Set first category as active _initializeCategoryKeys(); } void _initializeCategoryKeys() { categoryKeys.clear(); for (var category in 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, ); } }); } }); } @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, actions: [ IconButton( icon: Icon(Icons.history), onPressed: () => context.router.push(PoinHistoryRoute()), ), ], ), // Point Card Section SliverToBoxAdapter(child: _buildPointCard()), // Sticky Category Tabs SliverPersistentHeader( pinned: true, key: ValueKey(activeCategoryId), // Simplified key delegate: _StickyHeaderDelegate( child: _buildCategoryTabs(), height: 66, activeCategoryId: activeCategoryId, // Pass active category ID ), ), ]; }, body: ListView.builder( padding: EdgeInsets.only(top: 16), itemCount: categories.length, itemBuilder: (context, index) { final category = 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( "Poin Tersedia", 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( "Terpakai: ${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 _buildCategoryTabs() { 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: categories.length, itemBuilder: (context, index) { final category = 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, 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, 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), ), ), if (product.isPopular) Positioned( top: 6, right: 6, child: Container( padding: EdgeInsets.symmetric( horizontal: 6, vertical: 3, ), decoration: BoxDecoration( color: AppColor.warning, borderRadius: BorderRadius.circular(10), ), child: Text( "Populer", style: AppStyle.xs.copyWith( color: AppColor.white, fontWeight: FontWeight.w600, fontSize: 10, ), ), ), ), ], ), ), // Product Info 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, 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, 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 ? "Tukar" : "Poin Kurang", style: AppStyle.xs.copyWith( 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), decoration: BoxDecoration( color: AppColor.white, borderRadius: BorderRadius.circular(12), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.lock_outline, color: AppColor.textSecondary, size: 20, ), SizedBox(height: 4), Text( "Butuh ${pointsShortage}", style: AppStyle.xs.copyWith( fontWeight: FontWeight.w600, color: AppColor.textSecondary, fontSize: 10, ), textAlign: TextAlign.center, ), Text( "poin lagi", style: AppStyle.xs.copyWith( color: AppColor.textSecondary, fontSize: 10, ), textAlign: TextAlign.center, ), ], ), ), ), ), ], ), ); } void _redeemProduct(Product product) { context.router.push( ProductRedeemRoute(product: product, 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? activeCategoryId; // Track active category _StickyHeaderDelegate({ required this.child, required this.height, 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) { // Rebuild when active category changes if (oldDelegate is _StickyHeaderDelegate) { bool categoryChanged = oldDelegate.activeCategoryId != activeCategoryId; print("shouldRebuild - Category changed: $categoryChanged"); print( "Old category: ${oldDelegate.activeCategoryId}, New category: $activeCategoryId", ); return categoryChanged; } return true; } }