import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:line_icons/line_icons.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/appbar/appbar.dart'; import '../../components/field/date_range_picker_field.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/stat_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: [ // Re-fetch when date range changes BlocListener( listenWhen: (prev, curr) => prev.dateFrom != curr.dateFrom || prev.dateTo != curr.dateTo, listener: (context, _) => context .read() .add(const PurchasingAnalyticLoaderEvent.fetched()), ), // Re-fetch when outlet changes BlocListener( listenWhen: (prev, curr) => prev.outletId != curr.outletId, listener: (context, _) => context .read() .add(const PurchasingAnalyticLoaderEvent.fetched()), ), ], child: BlocBuilder( builder: (context, outletListState) { return BlocBuilder( builder: (context, state) { return CustomScrollView( slivers: [ // App Bar SliverAppBar( expandedHeight: 120.0, floating: false, pinned: true, elevation: 0, backgroundColor: AppColor.primary, flexibleSpace: CustomAppBar(title: context.lang.purchase), ), // Date Range + Outlet Picker SliverToBoxAdapter( child: FadeTransition( opacity: _fadeAnimation, child: Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), child: Column( children: [ DateRangePickerField( maxDate: DateTime.now(), startDate: state.dateFrom, endDate: state.dateTo, onChanged: (startDate, endDate) { context .read() .add( PurchasingAnalyticLoaderEvent .rangeDateChanged( startDate!, endDate!, ), ); }, ), const SpaceHeight(8), PurchaseOutletSelectorField( selectedOutletId: state.outletId, outlets: outletListState.outlets, isLoading: outletListState.isFetching, onOutletChanged: (outletId) { context .read() .add( PurchasingAnalyticLoaderEvent .outletChanged(outletId), ); }, ), ], ), ), ), ), const SliverToBoxAdapter(child: SpaceHeight(16)), // Summary Section SliverToBoxAdapter( child: FadeTransition( opacity: _fadeAnimation, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( context.lang.summary, style: AppStyle.xxl.copyWith( fontWeight: FontWeight.bold, color: AppColor.textPrimary, ), ), const SpaceHeight(16), state.isFetching ? _buildSummaryShimmer() : _buildSummaryCards(state), ], ), ), ), ), const SliverToBoxAdapter(child: SpaceHeight(24)), // Total Purchases Highlight Card SliverToBoxAdapter( child: FadeTransition( opacity: _fadeAnimation, child: state.isFetching ? _buildHighlightShimmer() : _buildTotalPurchasesCard(state), ), ), const SliverToBoxAdapter(child: SpaceHeight(24)), // 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)), ], ); }, ); }, ), ), ); } // ─── Summary Cards ──────────────────────────────────────────────────────── Widget _buildSummaryCards(PurchasingAnalyticLoaderState state) { final s = state.purchasing.summary; return Column( children: [ Row( children: [ Expanded( child: PurchaseStatCard( title: 'Total Pembelian', value: s.totalPurchases.currencyFormatRp, icon: LineIcons.shoppingCart, iconColor: AppColor.primary, cardAnimation: _fadeAnimation, ), ), const SpaceWidth(12), Expanded( child: PurchaseStatCard( title: 'Total PO', value: '${s.totalPurchaseOrders} PO', icon: LineIcons.fileAlt, iconColor: AppColor.info, cardAnimation: _fadeAnimation, ), ), ], ), const SpaceHeight(12), Row( children: [ Expanded( child: PurchaseStatCard( title: 'Bahan Baku', value: s.rawMaterialPurchases.currencyFormatRp, icon: LineIcons.leaf, iconColor: AppColor.secondary, cardAnimation: _fadeAnimation, ), ), const SpaceWidth(12), Expanded( child: PurchaseStatCard( title: 'Pengeluaran', value: s.expensePurchases.currencyFormatRp, icon: LineIcons.receipt, iconColor: AppColor.warning, cardAnimation: _fadeAnimation, ), ), ], ), const SpaceHeight(12), Row( children: [ Expanded( child: PurchaseStatCard( title: 'Total Qty', value: '${s.totalQuantity} pcs', icon: LineIcons.boxes, iconColor: AppColor.warning, cardAnimation: _fadeAnimation, ), ), const SpaceWidth(12), Expanded( child: PurchaseStatCard( title: 'Rata-rata PO', value: s.averagePurchaseOrderValue.round().currencyFormatRp, icon: LineIcons.dollarSign, iconColor: AppColor.secondaryDark, cardAnimation: _fadeAnimation, ), ), ], ), const SpaceHeight(12), Row( children: [ Expanded( child: PurchaseStatCard( title: 'Item Bahan Baku', value: '${s.totalIngredients} item', icon: LineIcons.leaf, iconColor: AppColor.secondaryDark, cardAnimation: _fadeAnimation, ), ), const SpaceWidth(12), Expanded( child: PurchaseStatCard( title: 'Vendor', value: '${s.totalVendors} vendor', icon: LineIcons.truck, iconColor: AppColor.primaryDark, cardAnimation: _fadeAnimation, ), ), ], ), ], ); } Widget _buildTotalPurchasesCard(PurchasingAnalyticLoaderState state) { return TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: const Duration(milliseconds: 900), curve: Curves.bounceOut, builder: (context, value, _) { return Transform.scale( scale: value, child: Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: const LinearGradient( colors: AppColor.primaryGradient, begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: AppColor.primary.withOpacity(0.35), blurRadius: 16, offset: const Offset(0, 6), ), ], ), child: Row( children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), child: const Icon( LineIcons.shoppingBag, color: AppColor.textWhite, size: 28, ), ), const SpaceWidth(16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( context.lang.total_purchase, style: TextStyle( color: AppColor.textWhite.withOpacity(0.9), fontSize: 14, fontWeight: FontWeight.w500, ), ), const SpaceHeight(4), Text( state.purchasing.summary.totalPurchases .currencyFormatRp, style: const TextStyle( color: AppColor.textWhite, fontSize: 24, fontWeight: FontWeight.bold, ), ), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '${state.purchasing.summary.totalPurchaseOrders} PO', style: const TextStyle( color: AppColor.textWhite, fontSize: 16, fontWeight: FontWeight.bold, ), ), Text( 'purchase order', style: TextStyle( color: AppColor.textWhite.withOpacity(0.8), fontSize: 12, ), ), ], ), ], ), ), ); }, ); } // ─── 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 _buildSummaryShimmer() { return Column( children: [ Row( children: [ Expanded(child: _shimmerCard(height: 100)), const SpaceWidth(12), Expanded(child: _shimmerCard(height: 100)), ], ), const SpaceHeight(12), Row( children: [ Expanded(child: _shimmerCard(height: 100)), const SpaceWidth(12), Expanded(child: _shimmerCard(height: 100)), ], ), const SpaceHeight(12), Row( children: [ Expanded(child: _shimmerCard(height: 100)), const SpaceWidth(12), Expanded(child: _shimmerCard(height: 100)), ], ), const SpaceHeight(12), Row( children: [ Expanded(child: _shimmerCard(height: 100)), const SpaceWidth(12), Expanded(child: _shimmerCard(height: 100)), ], ), ], ); } Widget _buildHighlightShimmer() { return Container( margin: const EdgeInsets.symmetric(horizontal: 16), child: _shimmerCard(height: 88), ); } 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), ), ), ); } }