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 '../../../application/analytic/purchasing_analytic_loader/purchasing_analytic_loader_bloc.dart'; import '../../../application/outlet/outlet_list_loader/outlet_list_loader_bloc.dart'; import '../../../common/extension/extension.dart'; import '../../../common/theme/theme.dart'; import '../../../injection.dart'; import '../../components/spacer/spacer.dart'; import 'widgets/ingredient_card.dart'; import 'widgets/outlet_selector_field.dart'; import 'widgets/purchase_daily_tile.dart'; import 'widgets/purchase_header.dart'; import 'widgets/purchase_rincian_card.dart'; import 'widgets/vendor_card.dart'; @RoutePage() class PurchasePage extends StatefulWidget implements AutoRouteWrapper { const PurchasePage({super.key}); @override State createState() => _PurchasePageState(); @override Widget wrappedRoute(BuildContext context) => MultiBlocProvider( providers: [ BlocProvider( create: (context) => getIt() ..add(const PurchasingAnalyticLoaderEvent.fetched()), ), BlocProvider( create: (context) => getIt() ..add(const OutletListLoaderEvent.fetched()), ), ], child: this, ); } class _PurchasePageState extends State with TickerProviderStateMixin { late AnimationController _fadeController; late Animation _fadeAnimation; @override void initState() { super.initState(); _fadeController = AnimationController( duration: const Duration(milliseconds: 600), vsync: this, ); _fadeAnimation = CurvedAnimation( parent: _fadeController, curve: Curves.easeOut, ); Future.delayed(const Duration(milliseconds: 200), () { if (mounted) _fadeController.forward(); }); } @override void dispose() { _fadeController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColor.background, body: MultiBlocListener( listeners: [ BlocListener< PurchasingAnalyticLoaderBloc, PurchasingAnalyticLoaderState >( listenWhen: (prev, curr) => prev.dateFrom != curr.dateFrom || prev.dateTo != curr.dateTo, listener: (context, _) => context .read() .add(const PurchasingAnalyticLoaderEvent.fetched()), ), BlocListener< PurchasingAnalyticLoaderBloc, PurchasingAnalyticLoaderState >( listenWhen: (prev, curr) => prev.outletId != curr.outletId, listener: (context, _) => context .read() .add(const PurchasingAnalyticLoaderEvent.fetched()), ), ], child: BlocBuilder( builder: (context, outletListState) { return BlocBuilder< PurchasingAnalyticLoaderBloc, PurchasingAnalyticLoaderState >( builder: (context, state) { return CustomScrollView( slivers: [ // Header (same style as Sales) SliverToBoxAdapter( child: PurchaseHeader( state: state, onDateRangeChanged: (startDate, endDate) { context.read().add( PurchasingAnalyticLoaderEvent.rangeDateChanged( startDate, endDate, ), ); }, ), ), // Outlet Selector SliverToBoxAdapter( child: FadeTransition( opacity: _fadeAnimation, child: Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), child: PurchaseOutletSelectorField( selectedOutletId: state.outletId, outlets: outletListState.outlets, isLoading: outletListState.isFetching, onOutletChanged: (outletId) { context.read().add( PurchasingAnalyticLoaderEvent.outletChanged( outletId, ), ); }, ), ), ), ), // Rincian Pembelian (same style as Sales) SliverToBoxAdapter( child: FadeTransition( opacity: _fadeAnimation, child: Padding( padding: const EdgeInsets.all(16), child: PurchaseRincianCard(state: state), ), ), ), // Daily Breakdown Header SliverToBoxAdapter( child: FadeTransition( opacity: _fadeAnimation, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( context.lang.daily_breakdown, style: AppStyle.xxl.copyWith( fontWeight: FontWeight.bold, color: AppColor.textPrimary, ), ), ), ), ), const SliverToBoxAdapter(child: SpaceHeight(12)), state.isFetching ? _buildListShimmer() : _buildDailyList(state), const SliverToBoxAdapter(child: SpaceHeight(24)), // Ingredient Header SliverToBoxAdapter( child: FadeTransition( opacity: _fadeAnimation, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( 'Bahan Baku', style: AppStyle.xxl.copyWith( fontWeight: FontWeight.bold, color: AppColor.textPrimary, ), ), ), ), ), const SliverToBoxAdapter(child: SpaceHeight(12)), state.isFetching ? _buildListShimmer() : _buildIngredientList(state), const SliverToBoxAdapter(child: SpaceHeight(24)), // Vendor Header SliverToBoxAdapter( child: FadeTransition( opacity: _fadeAnimation, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( 'Vendor', style: AppStyle.xxl.copyWith( fontWeight: FontWeight.bold, color: AppColor.textPrimary, ), ), ), ), ), const SliverToBoxAdapter(child: SpaceHeight(12)), state.isFetching ? _buildListShimmer() : _buildVendorList(state), const SliverToBoxAdapter(child: SpaceHeight(80)), ], ); }, ); }, ), ), ); } // ─── Lists ──────────────────────────────────────────────────────────────── Widget _buildDailyList(PurchasingAnalyticLoaderState state) { if (state.purchasing.data.isEmpty) { return SliverToBoxAdapter( child: _buildEmptyState('Tidak ada data harian'), ); } return SliverList( delegate: SliverChildBuilderDelegate( (context, index) => PurchaseDailyTile( data: state.purchasing.data[index], index: index, animation: _fadeAnimation, ), childCount: state.purchasing.data.length, ), ); } Widget _buildIngredientList(PurchasingAnalyticLoaderState state) { if (state.purchasing.ingredientData.isEmpty) { return SliverToBoxAdapter( child: _buildEmptyState('Tidak ada data bahan baku'), ); } return SliverList( delegate: SliverChildBuilderDelegate( (context, index) => PurchaseIngredientCard( data: state.purchasing.ingredientData[index], index: index, animation: _fadeAnimation, ), childCount: state.purchasing.ingredientData.length, ), ); } Widget _buildVendorList(PurchasingAnalyticLoaderState state) { if (state.purchasing.vendorData.isEmpty) { return SliverToBoxAdapter( child: _buildEmptyState('Tidak ada data vendor'), ); } return SliverList( delegate: SliverChildBuilderDelegate( (context, index) => PurchaseVendorCard( data: state.purchasing.vendorData[index], index: index, animation: _fadeAnimation, ), childCount: state.purchasing.vendorData.length, ), ); } // ─── Empty State ────────────────────────────────────────────────────────── Widget _buildEmptyState(String message) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: AppColor.surface, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColor.border.withOpacity(0.3)), ), child: Center( child: Text( message, style: AppStyle.md.copyWith(color: AppColor.textSecondary), ), ), ); } // ─── Shimmer Loaders ────────────────────────────────────────────────────── Widget _buildListShimmer() { return SliverList( delegate: SliverChildBuilderDelegate( (_, __) => Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: _shimmerCard(height: 72), ), childCount: 4, ), ); } Widget _shimmerCard({required double height}) { return Shimmer.fromColors( baseColor: Colors.grey[300]!, highlightColor: Colors.grey[100]!, child: Container( height: height, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), ), ); } }