feat: sales loading

This commit is contained in:
efrilm 2025-08-17 14:53:52 +07:00
parent d3543149f2
commit 6b1e56a46b

View File

@ -1,6 +1,8 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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 '../../../application/sales/sales_loader/sales_loader_bloc.dart';
import '../../../common/extension/extension.dart'; import '../../../common/extension/extension.dart';
@ -32,6 +34,7 @@ class _SalesPageState extends State<SalesPage> with TickerProviderStateMixin {
late AnimationController fadeAnimationController; late AnimationController fadeAnimationController;
late Animation<double> fadeAnimation; late Animation<double> fadeAnimation;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -95,41 +98,9 @@ class _SalesPageState extends State<SalesPage> with TickerProviderStateMixin {
position: slideAnimation, position: slideAnimation,
child: FadeTransition( child: FadeTransition(
opacity: fadeAnimation, opacity: fadeAnimation,
child: Container( child: state.isFetching
margin: const EdgeInsets.all(16), ? _buildDateRangeShimmer()
padding: const EdgeInsets.symmetric( : _buildDateRangeHeader(),
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,
),
),
],
),
),
), ),
), ),
), ),
@ -153,6 +124,332 @@ class _SalesPageState extends State<SalesPage> with TickerProviderStateMixin {
), ),
), ),
const SpaceHeight(16), 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)),
],
);
},
),
);
}
// 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<double>( TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0), tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 800), duration: const Duration(milliseconds: 800),
@ -165,11 +462,7 @@ class _SalesPageState extends State<SalesPage> with TickerProviderStateMixin {
Expanded( Expanded(
child: _buildSummaryCard( child: _buildSummaryCard(
'Total Sales', 'Total Sales',
state state.sales.summary.totalSales.currencyFormatRp,
.sales
.summary
.totalSales
.currencyFormatRp,
Icons.trending_up, Icons.trending_up,
AppColor.success, AppColor.success,
0, 0,
@ -179,8 +472,7 @@ class _SalesPageState extends State<SalesPage> with TickerProviderStateMixin {
Expanded( Expanded(
child: _buildSummaryCard( child: _buildSummaryCard(
'Total Orders', 'Total Orders',
state.sales.summary.totalOrders state.sales.summary.totalOrders.toString(),
.toString(),
Icons.shopping_cart, Icons.shopping_cart,
AppColor.info, AppColor.info,
100, 100,
@ -216,8 +508,7 @@ class _SalesPageState extends State<SalesPage> with TickerProviderStateMixin {
Expanded( Expanded(
child: _buildSummaryCard( child: _buildSummaryCard(
'Total Items', 'Total Items',
state.sales.summary.totalItems state.sales.summary.totalItems.toString(),
.toString(),
Icons.inventory, Icons.inventory,
AppColor.primary, AppColor.primary,
300, 300,
@ -229,19 +520,11 @@ class _SalesPageState extends State<SalesPage> with TickerProviderStateMixin {
}, },
), ),
], ],
), );
), }
),
),
),
// Net Sales Card Widget _buildNetSalesCard(SalesLoaderState state) {
SliverToBoxAdapter( return TweenAnimationBuilder<double>(
child: SlideTransition(
position: slideAnimation,
child: FadeTransition(
opacity: fadeAnimation,
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0), tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 1200), duration: const Duration(milliseconds: 1200),
curve: Curves.bounceOut, curve: Curves.bounceOut,
@ -279,9 +562,7 @@ class _SalesPageState extends State<SalesPage> with TickerProviderStateMixin {
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2), color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(12),
12,
),
), ),
child: const Icon( child: const Icon(
Icons.account_balance_wallet, Icons.account_balance_wallet,
@ -295,15 +576,12 @@ class _SalesPageState extends State<SalesPage> with TickerProviderStateMixin {
SpaceWidth(16), SpaceWidth(16),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: crossAxisAlignment: CrossAxisAlignment.start,
CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Net Sales', 'Net Sales',
style: TextStyle( style: TextStyle(
color: AppColor.textWhite.withOpacity( color: AppColor.textWhite.withOpacity(0.9),
0.9,
),
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
@ -312,20 +590,13 @@ class _SalesPageState extends State<SalesPage> with TickerProviderStateMixin {
TweenAnimationBuilder<double>( TweenAnimationBuilder<double>(
tween: Tween( tween: Tween(
begin: 0.0, begin: 0.0,
end: state.sales.summary.netSales end: state.sales.summary.netSales.toDouble(),
.toDouble(),
),
duration: const Duration(
milliseconds: 2000,
), ),
duration: const Duration(milliseconds: 2000),
curve: Curves.easeOutCubic, curve: Curves.easeOutCubic,
builder: (context, countValue, child) { builder: (context, countValue, child) {
return Text( return Text(
state state.sales.summary.netSales.currencyFormatRp,
.sales
.summary
.netSales
.currencyFormatRp,
style: const TextStyle( style: const TextStyle(
color: AppColor.textWhite, color: AppColor.textWhite,
fontSize: 24, fontSize: 24,
@ -342,35 +613,18 @@ class _SalesPageState extends State<SalesPage> with TickerProviderStateMixin {
), ),
); );
}, },
), );
), }
),
),
// Daily Sales Section Header Widget _buildDailySalesList(SalesLoaderState state) {
SliverToBoxAdapter( return SliverList(
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
SliverList(
delegate: SliverChildBuilderDelegate((context, index) { 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( return SlideTransition(
position: position:
Tween<Offset>( Tween<Offset>(
@ -380,8 +634,8 @@ class _SalesPageState extends State<SalesPage> with TickerProviderStateMixin {
CurvedAnimation( CurvedAnimation(
parent: slideAnimationController, parent: slideAnimationController,
curve: Interval( curve: Interval(
0.2 + (index * 0.1), slideStart,
0.8 + (index * 0.1), slideEnd,
curve: Curves.easeOutBack, curve: Curves.easeOutBack,
), ),
), ),
@ -390,18 +644,11 @@ class _SalesPageState extends State<SalesPage> with TickerProviderStateMixin {
opacity: Tween<double>(begin: 0.0, end: 1.0).animate( opacity: Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation( CurvedAnimation(
parent: fadeAnimationController, parent: fadeAnimationController,
curve: Interval( curve: Interval(fadeStart, fadeEnd, curve: Curves.easeOut),
0.3 + (index * 0.1),
0.9 + (index * 0.1),
curve: Curves.easeOut,
),
), ),
), ),
child: Container( child: Container(
margin: const EdgeInsets.symmetric( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
horizontal: 16,
vertical: 6,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColor.surface, color: AppColor.surface,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@ -418,14 +665,6 @@ class _SalesPageState extends State<SalesPage> with TickerProviderStateMixin {
), ),
); );
}, childCount: state.sales.data.length), }, childCount: state.sales.data.length),
),
// Bottom Padding
const SliverToBoxAdapter(child: SpaceHeight(32)),
],
);
},
),
); );
} }