import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:shimmer/shimmer.dart'; import 'dart:math' as math; import '../../../application/analytic/sales_loader/sales_loader_bloc.dart'; import '../../../common/extension/extension.dart'; import '../../../common/theme/theme.dart'; import '../../../domain/analytic/analytic.dart'; import '../../../injection.dart'; import '../../components/appbar/appbar.dart'; import '../../components/field/date_range_picker_field.dart'; import '../../components/spacer/spacer.dart'; import 'widgets/summary_card.dart'; @RoutePage() class SalesPage extends StatefulWidget implements AutoRouteWrapper { const SalesPage({super.key}); @override State createState() => _SalesPageState(); @override Widget wrappedRoute(BuildContext context) => BlocProvider( create: (context) => getIt()..add(SalesLoaderEvent.fectched()), child: this, ); } class _SalesPageState extends State with TickerProviderStateMixin { late AnimationController slideAnimationController; late Animation slideAnimation; late AnimationController fadeAnimationController; late Animation fadeAnimation; @override void initState() { super.initState(); // Slide Animation slideAnimationController = AnimationController( duration: const Duration(milliseconds: 800), vsync: this, ); slideAnimation = Tween(begin: const Offset(0, 0.3), end: Offset.zero).animate( CurvedAnimation( parent: slideAnimationController, curve: Curves.easeOutCubic, ), ); // Fade Animation fadeAnimationController = AnimationController( duration: const Duration(milliseconds: 600), vsync: this, ); fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: fadeAnimationController, curve: Curves.easeOut), ); // Start animations Future.delayed(const Duration(milliseconds: 300), () { slideAnimationController.forward(); fadeAnimationController.forward(); }); } @override void dispose() { slideAnimationController.dispose(); fadeAnimationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColor.background, body: BlocListener( listenWhen: (previous, current) => previous.dateFrom != current.dateFrom && previous.dateTo != current.dateTo, listener: (context, state) { context.read().add(SalesLoaderEvent.fectched()); }, child: BlocBuilder( builder: (context, state) { return CustomScrollView( slivers: [ // App Bar SliverAppBar( expandedHeight: 120, floating: false, pinned: true, backgroundColor: AppColor.primary, flexibleSpace: CustomAppBar(title: 'Penjualan'), ), // Date Range Header SliverToBoxAdapter( child: SlideTransition( position: slideAnimation, child: FadeTransition( opacity: fadeAnimation, child: Padding( padding: const EdgeInsets.all(16.0), child: DateRangePickerField( maxDate: DateTime.now(), startDate: state.dateFrom, endDate: state.dateTo, onChanged: (startDate, endDate) { context.read().add( SalesLoaderEvent.rangeDateChanged( startDate!, endDate!, ), ); }, ), ), ), ), ), // Summary Cards SliverToBoxAdapter( child: SlideTransition( position: slideAnimation, child: FadeTransition( opacity: fadeAnimation, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Summary', style: AppStyle.xxl.copyWith( fontWeight: FontWeight.bold, color: AppColor.textPrimary, ), ), const SpaceHeight(16), state.isFetching ? _buildSummaryShimmer() : _buildSummaryCards(state), ], ), ), ), ), ), // Net Sales Card SliverToBoxAdapter( child: SlideTransition( position: slideAnimation, child: FadeTransition( opacity: fadeAnimation, child: state.isFetching ? _buildNetSalesShimmer() : _buildNetSalesCard(state), ), ), ), // Daily Sales Section Header SliverToBoxAdapter( child: SlideTransition( position: slideAnimation, child: FadeTransition( opacity: fadeAnimation, child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: Text( 'Daily Breakdown', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: AppColor.textPrimary, ), ), ), ), ), ), // Daily Sales List state.isFetching ? _buildDailySalesShimmer() : _buildDailySalesList(state), // Bottom Padding const SliverToBoxAdapter(child: SpaceHeight(32)), ], ); }, ), ), ); } Widget _buildSummaryShimmer() { return Column( children: [ Row( children: [ Expanded(child: _buildSummaryCardShimmer()), SpaceWidth(12), Expanded(child: _buildSummaryCardShimmer()), ], ), const SpaceHeight(12), Row( children: [ Expanded(child: _buildSummaryCardShimmer()), SpaceWidth(12), Expanded(child: _buildSummaryCardShimmer()), ], ), ], ); } Widget _buildSummaryCardShimmer() { return Shimmer.fromColors( baseColor: Colors.grey[300]!, highlightColor: Colors.grey[100]!, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 24, height: 24, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(6), ), ), SpaceWidth(8), Container( width: 60, height: 14, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), ), ), ], ), const SpaceHeight(8), Container( width: double.infinity, height: 20, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), ), ), ], ), ), ); } Widget _buildNetSalesShimmer() { return Container( margin: const EdgeInsets.all(16), child: Shimmer.fromColors( baseColor: Colors.grey[300]!, highlightColor: Colors.grey[100]!, child: Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), ), child: Row( children: [ Container( width: 52, height: 52, decoration: BoxDecoration( color: Colors.grey[400], borderRadius: BorderRadius.circular(12), ), ), SpaceWidth(16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 80, height: 14, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), ), ), const SpaceHeight(8), Container( width: 150, height: 24, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), ), ), ], ), ), ], ), ), ), ); } Widget _buildDailySalesShimmer() { return SliverList( delegate: SliverChildBuilderDelegate( (context, index) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: Shimmer.fromColors( baseColor: Colors.grey[300]!, highlightColor: Colors.grey[100]!, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: Colors.grey[400], borderRadius: BorderRadius.circular(10), ), ), SpaceWidth(12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 100, height: 16, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), ), ), const SpaceHeight(4), Container( width: 80, height: 14, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), ), ), ], ), ), Container( width: 60, height: 24, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), ), ], ), ), ), ); }, childCount: 8, // Show 8 shimmer items while loading ), ); } Widget _buildSummaryCards(SalesLoaderState state) { return Column( children: [ TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: const Duration(milliseconds: 800), curve: Curves.elasticOut, builder: (context, value, child) { return Transform.scale( scale: value, child: Row( children: [ Expanded( child: _buildSummaryCard( 'Total Sales', state.sales.summary.totalSales.currencyFormatRp, Icons.trending_up, AppColor.success, 0, ), ), SpaceWidth(12), Expanded( child: _buildSummaryCard( 'Total Orders', state.sales.summary.totalOrders.toString(), Icons.shopping_cart, AppColor.info, 100, ), ), ], ), ); }, ), const SpaceHeight(12), TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: const Duration(milliseconds: 1000), curve: Curves.elasticOut, builder: (context, value, child) { return Transform.scale( scale: value, child: Row( children: [ Expanded( child: _buildSummaryCard( 'Avg Order Value', state.sales.summary.averageOrderValue .round() .currencyFormatRp, Icons.attach_money, AppColor.warning, 200, ), ), SpaceWidth(12), Expanded( child: _buildSummaryCard( 'Total Items', state.sales.summary.totalItems.toString(), Icons.inventory, AppColor.primary, 300, ), ), ], ), ); }, ), ], ); } Widget _buildNetSalesCard(SalesLoaderState state) { return TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: const Duration(milliseconds: 1200), curve: Curves.bounceOut, builder: (context, value, child) { return Transform.scale( scale: value, child: Container( margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: const LinearGradient( colors: AppColor.successGradient, begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: AppColor.success.withOpacity(0.3), blurRadius: 15, offset: const Offset(0, 5), ), ], ), child: Row( children: [ TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: const Duration(milliseconds: 1500), curve: Curves.elasticOut, builder: (context, iconValue, child) { return Transform.rotate( angle: iconValue * 0.1, child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), child: const Icon( Icons.account_balance_wallet, color: AppColor.textWhite, size: 28, ), ), ); }, ), SpaceWidth(16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Net Sales', style: TextStyle( color: AppColor.textWhite.withOpacity(0.9), fontSize: 14, fontWeight: FontWeight.w500, ), ), const SpaceHeight(4), TweenAnimationBuilder( tween: Tween( begin: 0.0, end: state.sales.summary.netSales.toDouble(), ), duration: const Duration(milliseconds: 2000), curve: Curves.easeOutCubic, builder: (context, countValue, child) { return Text( state.sales.summary.netSales.currencyFormatRp, style: const TextStyle( color: AppColor.textWhite, fontSize: 24, fontWeight: FontWeight.bold, ), ); }, ), ], ), ), ], ), ), ); }, ); } Widget _buildDailySalesList(SalesLoaderState state) { return SliverList( delegate: SliverChildBuilderDelegate((context, index) { // Calculate intervals ensuring they don't exceed 1.0 final slideStart = math.min(0.2 + (index * 0.05), 0.7); final slideEnd = math.min(slideStart + 0.3, 1.0); final fadeStart = math.min(0.3 + (index * 0.05), 0.8); final fadeEnd = math.min(fadeStart + 0.2, 1.0); return SlideTransition( position: Tween( begin: Offset(index.isEven ? -1.0 : 1.0, 0), end: Offset.zero, ).animate( CurvedAnimation( parent: slideAnimationController, curve: Interval( slideStart, slideEnd, curve: Curves.easeOutBack, ), ), ), child: FadeTransition( opacity: Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: fadeAnimationController, curve: Interval(fadeStart, fadeEnd, curve: Curves.easeOut), ), ), child: Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), decoration: BoxDecoration( color: AppColor.surface, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: _buildDailySalesItem(state.sales.data[index]), ), ), ); }, childCount: state.sales.data.length), ); } Widget _buildSummaryCard( String title, String value, IconData icon, Color color, int delay, ) { return SalesSummaryCard( fadeAnimation: fadeAnimation, title: title, value: value, icon: icon, color: color, delay: delay, ); } Widget _buildDailySalesItem(SalesAnalyticData dailySale) { return ExpansionTile( leading: Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: AppColor.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: Icon(Icons.calendar_today, color: AppColor.primary, size: 20), ), title: Text( '${dailySale.date.day}/${dailySale.date.month}/${dailySale.date.year}', style: const TextStyle( fontWeight: FontWeight.bold, color: AppColor.textPrimary, ), ), subtitle: Text( dailySale.sales.currencyFormatRp, style: TextStyle( color: AppColor.success, fontWeight: FontWeight.w600, fontSize: 16, ), ), trailing: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: AppColor.info.withOpacity(0.1), borderRadius: BorderRadius.circular(20), ), child: Text( '${dailySale.orders} orders', style: TextStyle( color: AppColor.info, fontWeight: FontWeight.w500, fontSize: 12, ), ), ), children: [ Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Expanded( child: _buildDetailItem( 'Items', '${dailySale.items}', Icons.inventory_2, ), ), Expanded( child: _buildDetailItem( 'Tax', dailySale.tax.currencyFormatRp, Icons.receipt, ), ), Expanded( child: _buildDetailItem( 'Discount', dailySale.discount.currencyFormatRp, Icons.local_offer, ), ), ], ), ), ], ); } Widget _buildDetailItem(String label, String value, IconData icon) { return TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: const Duration(milliseconds: 600), curve: Curves.bounceOut, builder: (context, animValue, child) { return Transform.scale( scale: animValue, child: Column( children: [ TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: const Duration(milliseconds: 800), curve: Curves.elasticOut, builder: (context, iconValue, child) { return Transform.rotate( angle: iconValue * 0.1, child: Icon(icon, color: AppColor.textSecondary, size: 20), ); }, ), const SpaceHeight(4), Text( label, style: TextStyle(color: AppColor.textSecondary, fontSize: 12), ), const SpaceHeight(2), AnimatedBuilder( animation: fadeAnimation, builder: (context, child) { return Text( value, style: TextStyle( color: AppColor.textPrimary, fontWeight: FontWeight.w600, fontSize: 14, ), ); }, ), ], ), ); }, ); } }