import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../application/analytic/inventory_analytic_loader/inventory_analytic_loader_bloc.dart'; import '../../../common/theme/theme.dart'; import '../../../domain/analytic/analytic.dart'; import '../../../injection.dart'; import '../../components/appbar/appbar.dart'; import 'widgets/ingredient_tile.dart'; import 'widgets/product_tile.dart'; import 'widgets/stat_card.dart'; import 'widgets/tabbar_delegate.dart'; // Custom SliverPersistentHeaderDelegate untuk TabBar @RoutePage() class InventoryPage extends StatefulWidget implements AutoRouteWrapper { const InventoryPage({super.key}); @override State createState() => _InventoryPageState(); @override Widget wrappedRoute(BuildContext context) => BlocProvider( create: (_) => getIt() ..add(InventoryAnalyticLoaderEvent.fetched()), child: this, ); } class _InventoryPageState extends State with TickerProviderStateMixin { late AnimationController _fadeAnimationController; late AnimationController _slideAnimationController; late Animation _fadeAnimation; late Animation _slideAnimation; late TabController _tabController; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); _fadeAnimationController = AnimationController( duration: const Duration(milliseconds: 1000), vsync: this, ); _slideAnimationController = AnimationController( duration: const Duration(milliseconds: 800), vsync: this, ); _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _fadeAnimationController, curve: Curves.easeInOut, ), ); _slideAnimation = Tween(begin: const Offset(0.0, 0.3), end: Offset.zero).animate( CurvedAnimation( parent: _slideAnimationController, curve: Curves.easeOutBack, ), ); _fadeAnimationController.forward(); _slideAnimationController.forward(); } @override void dispose() { _fadeAnimationController.dispose(); _slideAnimationController.dispose(); _tabController.dispose(); super.dispose(); } Color getStatusColor(String status) { switch (status) { case 'available': return AppColor.success; case 'low_stock': return AppColor.warning; case 'out_of_stock': return AppColor.error; default: return AppColor.textSecondary; } } String getStatusText(String status) { switch (status) { case 'available': return 'Tersedia'; case 'low_stock': return 'Stok Rendah'; case 'out_of_stock': return 'Habis'; default: return 'Unknown'; } } @override Widget build(BuildContext context) { return BlocListener< InventoryAnalyticLoaderBloc, InventoryAnalyticLoaderState >( listenWhen: (previous, current) => previous.dateFrom != current.dateFrom || previous.dateTo != current.dateTo, listener: (context, state) { context.read().add( InventoryAnalyticLoaderEvent.fetched(), ); }, child: Scaffold( backgroundColor: AppColor.background, body: BlocBuilder< InventoryAnalyticLoaderBloc, InventoryAnalyticLoaderState >( builder: (context, state) { return FadeTransition( opacity: _fadeAnimation, child: SlideTransition( position: _slideAnimation, child: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) { return [ _buildSliverAppBar(), SliverPersistentHeader( pinned: true, delegate: InventorySliverTabBarDelegate( startDate: state.dateFrom, endDate: state.dateTo, onDateRangeChanged: (startDate, endDate) { context.read().add( InventoryAnalyticLoaderEvent.rangeDateChanged( startDate!, endDate!, ), ); }, tabBar: TabBar( controller: _tabController, indicator: BoxDecoration( gradient: LinearGradient( colors: AppColor.primaryGradient, begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(25), boxShadow: [ BoxShadow( color: AppColor.primary.withOpacity(0.3), blurRadius: 12, offset: const Offset(0, 4), ), ], ), indicatorSize: TabBarIndicatorSize.tab, indicatorPadding: const EdgeInsets.all(6), labelColor: AppColor.textWhite, unselectedLabelColor: AppColor.textSecondary, labelStyle: const TextStyle( fontWeight: FontWeight.w700, fontSize: 13, ), unselectedLabelStyle: const TextStyle( fontWeight: FontWeight.w500, fontSize: 13, ), dividerColor: Colors.transparent, splashFactory: NoSplash.splashFactory, overlayColor: MaterialStateProperty.all( Colors.transparent, ), tabs: [ Tab( height: 40, child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, ), child: const Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.inventory_2_rounded, size: 16, ), SizedBox(width: 6), Text('Produk'), ], ), ), ), Tab( height: 40, child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, ), child: const Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.restaurant_menu_rounded, size: 16, ), SizedBox(width: 6), Text('Bahan'), ], ), ), ), ], ), ), ), ]; }, body: TabBarView( controller: _tabController, children: [ _buildProductTab(state.inventoryAnalytic), _buildIngredientTab(state.inventoryAnalytic), ], ), ), ), ); }, ), ), ); } Widget _buildSliverAppBar() { return SliverAppBar( expandedHeight: 120, floating: false, pinned: true, elevation: 0, backgroundColor: AppColor.primary, flexibleSpace: CustomAppBar(title: 'Inventaris'), ); } Widget _buildProductTab(InventoryAnalytic inventoryAnalytic) { return CustomScrollView( slivers: [ SliverToBoxAdapter( child: _buildProductStats(inventoryAnalytic.summary), ), SliverPadding( padding: const EdgeInsets.all(16), sliver: SliverList( delegate: SliverChildBuilderDelegate( (context, index) => InventoryProductTile(item: inventoryAnalytic.products[index]), childCount: inventoryAnalytic.products.length, ), ), ), ], ); } Widget _buildIngredientTab(InventoryAnalytic inventoryAnalytic) { return CustomScrollView( slivers: [ SliverToBoxAdapter( child: _buildIngredientStats(inventoryAnalytic.summary), ), SliverPadding( padding: const EdgeInsets.all(16), sliver: SliverList( delegate: SliverChildBuilderDelegate( (context, index) => InventoryIngredientTile( item: inventoryAnalytic.ingredients[index], ), childCount: inventoryAnalytic.ingredients.length, ), ), ), ], ); } Widget _buildProductStats(InventorySummary inventory) { return Container( margin: const EdgeInsets.all(16), child: Column( children: [ Row( children: [ Expanded( child: _buildStatCard( 'Total Produk', inventory.totalProducts.toString(), Icons.inventory_2_rounded, AppColor.primary, ), ), const SizedBox(width: 16), Expanded( child: _buildStatCard( 'Produk Terjual', inventory.totalSoldProducts.toString(), Icons.check_circle_rounded, AppColor.success, ), ), ], ), const SizedBox(height: 16), Row( children: [ Expanded( child: _buildStatCard( 'Stok Rendah', inventory.lowStockProducts.toString(), Icons.warning_rounded, AppColor.warning, ), ), const SizedBox(width: 16), Expanded( child: _buildStatCard( 'Stok Kosong', inventory.zeroStockProducts.toString(), Icons.error_rounded, AppColor.error, ), ), ], ), ], ), ); } Widget _buildIngredientStats(InventorySummary inventory) { return Container( margin: const EdgeInsets.all(16), child: Column( children: [ Row( children: [ Expanded( child: _buildStatCard( 'Total Bahan', inventory.totalIngredients.toString(), Icons.restaurant_menu_rounded, AppColor.primary, ), ), const SizedBox(width: 16), Expanded( child: _buildStatCard( 'Bahan Terjual', inventory.totalSoldIngredients.toString(), Icons.check_circle_rounded, AppColor.success, ), ), ], ), const SizedBox(height: 16), Row( children: [ Expanded( child: _buildStatCard( 'Stok Kurang', inventory.lowStockIngredients.toString(), Icons.warning_rounded, AppColor.warning, ), ), const SizedBox(width: 16), Expanded( child: _buildStatCard( 'Habis', inventory.zeroStockIngredients.toString(), Icons.error_rounded, AppColor.error, ), ), ], ), ], ), ); } Widget _buildStatCard( String title, String value, IconData icon, Color color, ) { return TweenAnimationBuilder( tween: Tween(begin: 0, end: 1), duration: const Duration(milliseconds: 800), builder: (context, animationValue, child) { return Transform.scale( scale: animationValue, child: InventoryStatCard( title: title, value: value, icon: icon, color: color, ), ); }, ); } }