import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import '../../../common/theme/theme.dart'; import '../../../sample/reward_sample_data.dart'; import '../../components/assets/assets.gen.dart'; import '../../router/app_router.gr.dart'; import 'reward_ui.dart'; import 'widgets/reward_card.dart'; import 'widgets/reward_detail_sheet.dart'; enum _RewardFilter { all('Semua'), active('Aktif'), used('Terpakai'), expired('Kedaluwarsa'); const _RewardFilter(this.label); final String label; bool matches(Reward reward) => switch (this) { _RewardFilter.all => true, _RewardFilter.active => reward.status == RewardStatus.active, _RewardFilter.used => reward.status == RewardStatus.used, _RewardFilter.expired => reward.status == RewardStatus.expired, }; } @RoutePage() class RewardPage extends StatefulWidget { const RewardPage({super.key}); @override State createState() => _RewardPageState(); } class _RewardPageState extends State { _RewardFilter _filter = _RewardFilter.all; /// Aktif lebih dulu, urut yang paling cepat berakhir; sisanya terbaru dulu. final List _rewards = [...rewardSamples] ..sort((a, b) { final aActive = a.status == RewardStatus.active; final bActive = b.status == RewardStatus.active; if (aActive != bActive) return aActive ? -1 : 1; if (aActive) return a.validUntil.compareTo(b.validUntil); return b.redeemedAt.compareTo(a.redeemedAt); }); List get _activeRewards => _rewards.where(_RewardFilter.active.matches).toList(); int _countOf(_RewardFilter filter) => _rewards.where(filter.matches).length; @override Widget build(BuildContext context) { final visible = _rewards.where(_filter.matches).toList(); return Scaffold( backgroundColor: AppColor.background, appBar: AppBar(title: const Text('Reward Saya')), body: CustomScrollView( slivers: [ SliverToBoxAdapter(child: _buildSummary()), SliverPersistentHeader( pinned: true, delegate: _FilterHeaderDelegate( selected: _filter, countOf: _countOf, onSelected: (filter) => setState(() => _filter = filter), ), ), if (visible.isEmpty) SliverFillRemaining(hasScrollBody: false, child: _buildEmpty()) else SliverPadding( padding: const EdgeInsets.fromLTRB(16, 4, 16, 32), sliver: SliverList.separated( itemCount: visible.length, separatorBuilder: (_, _) => const SizedBox(height: 12), itemBuilder: (context, index) { final reward = visible[index]; return RewardCard( reward: reward, onTap: () => RewardDetailSheet.show(context, reward), ); }, ), ), ], ), ); } Widget _buildSummary() { final active = _activeRewards; // Sudah terurut: yang pertama adalah yang paling cepat berakhir. final expiringSoon = active.where(isRewardExpiringSoon).firstOrNull; return Container( margin: const EdgeInsets.fromLTRB(16, 16, 16, 4), padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: const LinearGradient( colors: AppColor.primaryGradient, begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: AppColor.primary.withOpacity(0.3), blurRadius: 12, offset: const Offset(0, 6), ), ], ), child: Column( children: [ Row( children: [ Container( width: 48, height: 48, padding: const EdgeInsets.all(8), decoration: const BoxDecoration( color: AppColor.white, shape: BoxShape.circle, ), child: Assets.icons.enakrewardIcon.image( fit: BoxFit.contain, // Aset aslinya besar; decode seukuran tampilan saja. cacheWidth: (32 * MediaQuery.devicePixelRatioOf(context)) .round(), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Siap dipakai', style: AppStyle.sm.copyWith( color: AppColor.textWhite.withOpacity(0.9), ), ), FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text( '${active.length} reward', style: AppStyle.h5.copyWith( color: AppColor.textWhite, fontWeight: FontWeight.w700, ), ), ), ], ), ), Material( color: AppColor.white, borderRadius: BorderRadius.circular(20), child: InkWell( borderRadius: BorderRadius.circular(20), onTap: () => context.router.push(const CoinRoute()), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 8, ), child: Text( 'Tukar Poin', style: AppStyle.sm.copyWith( color: AppColor.primary, fontWeight: FontWeight.w700, ), ), ), ), ), ], ), if (expiringSoon != null) ...[ const SizedBox(height: 14), Material( color: AppColor.white.withOpacity(0.15), borderRadius: BorderRadius.circular(10), child: InkWell( borderRadius: BorderRadius.circular(10), onTap: () => RewardDetailSheet.show(context, expiringSoon), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 10, ), child: Row( children: [ const Icon( Icons.schedule, size: 16, color: AppColor.textWhite, ), const SizedBox(width: 8), Expanded( child: Text( '${expiringSoon.name} ยท ' '${rewardValidityLabel(expiringSoon).toLowerCase()}', maxLines: 1, overflow: TextOverflow.ellipsis, style: AppStyle.sm.copyWith( color: AppColor.textWhite, fontWeight: FontWeight.w600, ), ), ), const Icon( Icons.chevron_right, size: 18, color: AppColor.textWhite, ), ], ), ), ), ), ], ], ), ); } Widget _buildEmpty() { final (String emoji, String message) = switch (_filter) { _RewardFilter.all => ('๐ŸŽ', 'Belum ada reward'), _RewardFilter.active => ('๐ŸŽฏ', 'Belum ada reward aktif'), _RewardFilter.used => ('โœ…', 'Belum ada reward yang dipakai'), _RewardFilter.expired => ('โฐ', 'Tidak ada reward yang kedaluwarsa'), }; return Padding( padding: const EdgeInsets.fromLTRB(32, 32, 32, 64), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 96, height: 96, alignment: Alignment.center, decoration: BoxDecoration( color: AppColor.primary.withOpacity(0.06), shape: BoxShape.circle, ), child: Text(emoji, style: const TextStyle(fontSize: 44)), ), const SizedBox(height: 16), Text( message, textAlign: TextAlign.center, style: AppStyle.lg.copyWith(fontWeight: FontWeight.w700), ), const SizedBox(height: 6), Text( 'Tukar poin kamu untuk mendapatkan reward menarik', textAlign: TextAlign.center, style: AppStyle.sm.copyWith(color: AppColor.textSecondary), ), if (_filter == _RewardFilter.all || _filter == _RewardFilter.active) ...[ const SizedBox(height: 20), ElevatedButton( onPressed: () => context.router.push(const CoinRoute()), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 12, ), ), child: Text( 'Tukar Poin Sekarang', style: AppStyle.sm.copyWith( color: AppColor.white, fontWeight: FontWeight.w700, ), ), ), ], ], ), ); } } class _FilterHeaderDelegate extends SliverPersistentHeaderDelegate { _FilterHeaderDelegate({ required this.selected, required this.countOf, required this.onSelected, }); final _RewardFilter selected; final int Function(_RewardFilter) countOf; final ValueChanged<_RewardFilter> onSelected; static const double _height = 56; @override double get minExtent => _height; @override double get maxExtent => _height; @override Widget build( BuildContext context, double shrinkOffset, bool overlapsContent, ) { return Container( color: AppColor.background, child: ListView.separated( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), itemCount: _RewardFilter.values.length, separatorBuilder: (_, _) => const SizedBox(width: 8), itemBuilder: (context, index) { final filter = _RewardFilter.values[index]; return _FilterChip( label: filter.label, count: countOf(filter), isSelected: filter == selected, onTap: () => onSelected(filter), ); }, ), ); } @override bool shouldRebuild(covariant _FilterHeaderDelegate oldDelegate) => oldDelegate.selected != selected; } class _FilterChip extends StatelessWidget { const _FilterChip({ required this.label, required this.count, required this.isSelected, required this.onTap, }); final String label; final int count; final bool isSelected; final VoidCallback onTap; @override Widget build(BuildContext context) { final foreground = isSelected ? AppColor.textWhite : AppColor.textPrimary; return Material( color: isSelected ? AppColor.primary : AppColor.white, shape: StadiumBorder( side: BorderSide( color: isSelected ? AppColor.primary : AppColor.border, ), ), child: InkWell( customBorder: const StadiumBorder(), onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 14), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( label, style: AppStyle.sm.copyWith( color: foreground, fontWeight: FontWeight.w600, ), ), const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), decoration: BoxDecoration( color: isSelected ? AppColor.white.withOpacity(0.2) : AppColor.primary.withOpacity(0.08), borderRadius: BorderRadius.circular(10), ), child: Text( '$count', style: AppStyle.xs.copyWith( color: isSelected ? AppColor.textWhite : AppColor.primary, fontWeight: FontWeight.w700, ), ), ), ], ), ), ), ); } }