diff --git a/lib/presentation/pages/sales/sales_page.dart b/lib/presentation/pages/sales/sales_page.dart index 71d4f3a..4b58e05 100644 --- a/lib/presentation/pages/sales/sales_page.dart +++ b/lib/presentation/pages/sales/sales_page.dart @@ -1,6 +1,8 @@ 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/sales/sales_loader/sales_loader_bloc.dart'; import '../../../common/extension/extension.dart'; @@ -32,6 +34,7 @@ class _SalesPageState extends State with TickerProviderStateMixin { late AnimationController fadeAnimationController; late Animation fadeAnimation; + @override void initState() { super.initState(); @@ -95,41 +98,9 @@ class _SalesPageState extends State with TickerProviderStateMixin { position: slideAnimation, child: FadeTransition( opacity: fadeAnimation, - child: Container( - margin: const EdgeInsets.all(16), - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 12, - ), - decoration: BoxDecoration( - color: AppColor.surface, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - children: [ - Icon( - Icons.date_range, - color: AppColor.primary, - size: 20, - ), - SpaceWidth(8), - Text( - 'Aug 1 - Aug 15, 2025', - style: AppStyle.md.copyWith( - color: AppColor.textPrimary, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), + child: state.isFetching + ? _buildDateRangeShimmer() + : _buildDateRangeHeader(), ), ), ), @@ -153,81 +124,9 @@ class _SalesPageState extends State with TickerProviderStateMixin { ), ), const SpaceHeight(16), - 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, - ), - ), - ], - ), - ); - }, - ), + state.isFetching + ? _buildSummaryShimmer() + : _buildSummaryCards(state), ], ), ), @@ -241,108 +140,9 @@ class _SalesPageState extends State with TickerProviderStateMixin { position: slideAnimation, child: FadeTransition( opacity: fadeAnimation, - child: 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, - ), - ); - }, - ), - ], - ), - ), - ], - ), - ), - ); - }, - ), + child: state.isFetching + ? _buildNetSalesShimmer() + : _buildNetSalesCard(state), ), ), ), @@ -369,56 +169,9 @@ class _SalesPageState extends State with TickerProviderStateMixin { ), // Daily Sales List - SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - return SlideTransition( - position: - Tween( - begin: Offset(index.isEven ? -1.0 : 1.0, 0), - end: Offset.zero, - ).animate( - CurvedAnimation( - parent: slideAnimationController, - curve: Interval( - 0.2 + (index * 0.1), - 0.8 + (index * 0.1), - curve: Curves.easeOutBack, - ), - ), - ), - child: FadeTransition( - opacity: Tween(begin: 0.0, end: 1.0).animate( - CurvedAnimation( - parent: fadeAnimationController, - curve: Interval( - 0.3 + (index * 0.1), - 0.9 + (index * 0.1), - 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), - ), + state.isFetching + ? _buildDailySalesShimmer() + : _buildDailySalesList(state), // Bottom Padding const SliverToBoxAdapter(child: SpaceHeight(32)), @@ -429,6 +182,492 @@ class _SalesPageState extends State with TickerProviderStateMixin { ); } + // Shimmer Components + Widget _buildDateRangeShimmer() { + return Container( + margin: const EdgeInsets.all(16), + child: Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + ), + SpaceWidth(8), + Container( + width: 150, + height: 16, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + ), + ], + ), + ), + ), + ); + } + + 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 + ), + ); + } + + // Original Components (preserved) + Widget _buildDateRangeHeader() { + return Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + decoration: BoxDecoration( + color: AppColor.surface, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Icon(Icons.date_range, color: AppColor.primary, size: 20), + SpaceWidth(8), + Text( + 'Aug 1 - Aug 15, 2025', + style: AppStyle.md.copyWith( + color: AppColor.textPrimary, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + + 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,