From 843c11b2003950359a1ca3f674d2f82a23f4ddd2 Mon Sep 17 00:00:00 2001 From: Efril Date: Wed, 24 Jun 2026 10:28:13 +0700 Subject: [PATCH] feat: update stock ui --- .../pages/inventory/inventory_page.dart | 521 +++++--------- .../inventory/widgets/inventory_header.dart | 412 +++++++++++ .../widgets/inventory_stock_report.dart | 638 ++++++++++++++++++ 3 files changed, 1201 insertions(+), 370 deletions(-) create mode 100644 lib/presentation/pages/inventory/widgets/inventory_header.dart create mode 100644 lib/presentation/pages/inventory/widgets/inventory_stock_report.dart diff --git a/lib/presentation/pages/inventory/inventory_page.dart b/lib/presentation/pages/inventory/inventory_page.dart index ce2a57c..8c87fad 100644 --- a/lib/presentation/pages/inventory/inventory_page.dart +++ b/lib/presentation/pages/inventory/inventory_page.dart @@ -3,17 +3,11 @@ 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/extension/extension.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 +import '../../components/spacer/spacer.dart'; +import 'widgets/inventory_header.dart'; +import 'widgets/inventory_stock_report.dart'; @RoutePage() class InventoryPage extends StatefulWidget implements AutoRouteWrapper { @@ -32,400 +26,187 @@ class InventoryPage extends StatefulWidget implements AutoRouteWrapper { } class _InventoryPageState extends State - with TickerProviderStateMixin { - late AnimationController _fadeAnimationController; - late AnimationController _slideAnimationController; + with SingleTickerProviderStateMixin { + late AnimationController _fadeController; late Animation _fadeAnimation; - late Animation _slideAnimation; - late TabController _tabController; + + int _selectedTabIndex = 0; @override void initState() { super.initState(); - _tabController = TabController(length: 2, vsync: this); - _fadeAnimationController = AnimationController( + _fadeController = 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, - ), - ); + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation(parent: _fadeController, curve: Curves.easeIn)); - _slideAnimation = - Tween(begin: const Offset(0.0, 0.3), end: Offset.zero).animate( - CurvedAnimation( - parent: _slideAnimationController, - curve: Curves.easeOutBack, - ), - ); - - _fadeAnimationController.forward(); - _slideAnimationController.forward(); + _fadeController.forward(); } @override void dispose() { - _fadeAnimationController.dispose(); - _slideAnimationController.dispose(); - _tabController.dispose(); + _fadeController.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 context.lang.available; - case 'low_stock': - return context.lang.low_stock; - case 'out_of_stock': - return context.lang.out_of_stock; - 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, + return Scaffold( + backgroundColor: AppColor.background, + body: + BlocListener< + InventoryAnalyticLoaderBloc, + InventoryAnalyticLoaderState + >( + listenWhen: (previous, current) => + previous.dateFrom != current.dateFrom || + previous.dateTo != current.dateTo, + listener: (context, state) { + context.read().add( + InventoryAnalyticLoaderEvent.fetched(), + ); + }, + child: + BlocBuilder< + InventoryAnalyticLoaderBloc, + InventoryAnalyticLoaderState + >( + builder: (context, state) { + return CustomScrollView( + slivers: [ + // Header with gradient background, tabs, and summary + SliverToBoxAdapter( + child: FadeTransition( + opacity: _fadeAnimation, + child: InventoryHeader( + state: state, + selectedTabIndex: _selectedTabIndex, + onTabChanged: (index) { + setState(() { + _selectedTabIndex = index; + }); + }, onDateRangeChanged: (startDate, endDate) { - context.read().add( - InventoryAnalyticLoaderEvent.rangeDateChanged( - startDate!, - endDate!, - ), + _onDateRangeChanged( + context, + 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: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Icon( - Icons.inventory_2_rounded, - size: 16, - ), - SizedBox(width: 6), - Text(context.lang.product), - ], - ), - ), - ), - Tab( - height: 40, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Icon( - Icons.restaurant_menu_rounded, - size: 16, - ), - SizedBox(width: 6), - Text(context.lang.ingredients), - ], - ), - ), - ), - ], - ), ), ), - ]; - }, - body: TabBarView( - controller: _tabController, - children: [ - _buildProductTab(state.inventoryAnalytic), - _buildIngredientTab(state.inventoryAnalytic), - ], - ), + ), + + // Stock Report Table + SliverToBoxAdapter( + child: FadeTransition( + opacity: _fadeAnimation, + child: state.isFetching + ? _buildLoadingReport() + : InventoryStockReport( + inventoryAnalytic: state.inventoryAnalytic, + selectedTabIndex: _selectedTabIndex, + ), + ), + ), + + // Bottom spacing + const SliverToBoxAdapter(child: SpaceHeight(100)), + ], + ); + }, + ), + ), + ); + } + + Widget _buildLoadingReport() { + return Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppColor.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: AppColor.textLight.withOpacity(0.08), + spreadRadius: 1, + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: List.generate( + 5, + (index) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Expanded( + flex: 4, + child: Container( + height: 14, + decoration: BoxDecoration( + color: AppColor.borderLight, + borderRadius: BorderRadius.circular(4), ), ), - ); - }, + ), + const SpaceWidth(12), + Expanded( + flex: 2, + child: Container( + height: 14, + decoration: BoxDecoration( + color: AppColor.borderLight, + borderRadius: BorderRadius.circular(4), + ), + ), + ), + const SpaceWidth(12), + Expanded( + flex: 2, + child: Container( + height: 14, + decoration: BoxDecoration( + color: AppColor.borderLight, + borderRadius: BorderRadius.circular(4), + ), + ), + ), + const SpaceWidth(12), + Expanded( + flex: 2, + child: Container( + height: 14, + decoration: BoxDecoration( + color: AppColor.borderLight, + borderRadius: BorderRadius.circular(4), + ), + ), + ), + ], ), + ), + ), ), ); } - Widget _buildSliverAppBar() { - return SliverAppBar( - expandedHeight: 120, - floating: false, - pinned: true, - elevation: 0, - backgroundColor: AppColor.primary, - flexibleSpace: CustomAppBar(title: context.lang.inventory), - ); - } - - 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( - context.lang.total_products, - inventory.totalProducts.toString(), - Icons.inventory_2_rounded, - AppColor.primary, - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildStatCard( - context.lang.total_sold, - inventory.totalSoldProducts.toString(), - Icons.check_circle_rounded, - AppColor.success, - ), - ), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: _buildStatCard( - context.lang.low_stock, - inventory.lowStockProducts.toString(), - Icons.warning_rounded, - AppColor.warning, - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildStatCard( - context.lang.zero_stock, - 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( - context.lang.total_ingredients, - inventory.totalIngredients.toString(), - Icons.restaurant_menu_rounded, - AppColor.primary, - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildStatCard( - context.lang.total_sold, - inventory.totalSoldIngredients.toString(), - Icons.check_circle_rounded, - AppColor.success, - ), - ), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: _buildStatCard( - context.lang.low_stock, - inventory.lowStockIngredients.toString(), - Icons.warning_rounded, - AppColor.warning, - ), - ), - const SizedBox(width: 16), - Expanded( - child: _buildStatCard( - context.lang.zero_stock, - inventory.zeroStockIngredients.toString(), - Icons.error_rounded, - AppColor.error, - ), - ), - ], - ), - ], - ), - ); - } - - Widget _buildStatCard( - String title, - String value, - IconData icon, - Color color, + void _onDateRangeChanged( + BuildContext context, + DateTime startDate, + DateTime endDate, ) { - 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, - ), - ); - }, + context.read().add( + InventoryAnalyticLoaderEvent.rangeDateChanged(startDate, endDate), ); } } diff --git a/lib/presentation/pages/inventory/widgets/inventory_header.dart b/lib/presentation/pages/inventory/widgets/inventory_header.dart new file mode 100644 index 0000000..372730d --- /dev/null +++ b/lib/presentation/pages/inventory/widgets/inventory_header.dart @@ -0,0 +1,412 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; + +import '../../../../application/analytic/inventory_analytic_loader/inventory_analytic_loader_bloc.dart'; +import '../../../../common/extension/extension.dart'; +import '../../../../common/painter/wave_painter.dart'; +import '../../../../common/theme/theme.dart'; +import '../../../components/bottom_sheet/date_range_bottom_sheet.dart'; +import '../../../components/spacer/spacer.dart'; + +class InventoryHeader extends StatelessWidget { + final InventoryAnalyticLoaderState state; + final int selectedTabIndex; + final ValueChanged onTabChanged; + final void Function(DateTime startDate, DateTime endDate)? onDateRangeChanged; + + const InventoryHeader({ + super.key, + required this.state, + required this.selectedTabIndex, + required this.onTabChanged, + this.onDateRangeChanged, + }); + + @override + Widget build(BuildContext context) { + final outletLabel = state.inventoryAnalytic.summary.outletName.isNotEmpty + ? state.inventoryAnalytic.summary.outletName + : 'Semua Outlet'; + final dateLabel = _formatDateRange(state.dateFrom, state.dateTo); + + return Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: AppColor.primaryGradient, + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24), + ), + ), + child: Stack( + children: [ + // Decorative circles + Positioned( + top: -20, + right: -30, + child: Container( + width: 120, + height: 120, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColor.textWhite.withOpacity(0.08), + ), + ), + ), + Positioned( + top: 30, + right: 20, + child: Container( + width: 60, + height: 60, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColor.textWhite.withOpacity(0.05), + ), + ), + ), + Positioned( + top: 10, + left: -20, + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColor.textWhite.withOpacity(0.04), + ), + ), + ), + + // Wave pattern + Positioned.fill( + child: CustomPaint( + painter: WavePainter( + animation: 0.0, + color: AppColor.textWhite.withOpacity(0.1), + ), + ), + ), + + // Content + SafeArea( + bottom: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Back button + Title row + Calendar button + Row( + children: [ + GestureDetector( + onTap: () => context.router.maybePop(), + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColor.textWhite.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.chevron_left_rounded, + color: AppColor.textWhite, + size: 24, + ), + ), + ), + const SpaceWidth(12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.lang.inventory, + style: AppStyle.xl.copyWith( + color: AppColor.textWhite, + fontWeight: FontWeight.w700, + fontSize: 20, + ), + ), + const SizedBox(height: 2), + Text( + '$dateLabel ยท $outletLabel', + style: AppStyle.sm.copyWith( + color: AppColor.textWhite.withOpacity(0.75), + fontWeight: FontWeight.w400, + fontSize: 12, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SpaceWidth(8), + // Date filter button + GestureDetector( + onTap: () => _showDatePicker(context), + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColor.textWhite.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.calendar_month_rounded, + color: AppColor.textWhite, + size: 20, + ), + ), + ), + ], + ), + + const SpaceHeight(20), + + // Tab selector (Product / Ingredient) + _buildTabSelector(context), + + const SpaceHeight(24), + + // Total Value label + Text( + 'Total Nilai Inventori', + style: AppStyle.sm.copyWith( + color: AppColor.textWhite.withOpacity(0.75), + fontWeight: FontWeight.w400, + fontSize: 13, + ), + ), + + const SpaceHeight(4), + + // Big total value + state.isFetching + ? _buildHeaderValueShimmer() + : Text( + state + .inventoryAnalytic + .summary + .totalValue + .currencyFormatRp, + style: AppStyle.h1.copyWith( + color: AppColor.textWhite, + fontWeight: FontWeight.w900, + fontSize: 32, + ), + ), + + const SpaceHeight(16), + + // Chips row + state.isFetching + ? _buildHeaderChipsShimmer() + : _buildHeaderChips(context), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildTabSelector(BuildContext context) { + return Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: AppColor.textWhite.withOpacity(0.15), + borderRadius: BorderRadius.circular(30), + border: Border.all(color: AppColor.textWhite.withOpacity(0.2)), + ), + child: Row( + children: [ + Expanded( + child: _buildTab( + icon: Icons.inventory_2_rounded, + label: context.lang.product, + isSelected: selectedTabIndex == 0, + onTap: () => onTabChanged(0), + ), + ), + Expanded( + child: _buildTab( + icon: Icons.restaurant_menu_rounded, + label: context.lang.ingredients, + isSelected: selectedTabIndex == 1, + onTap: () => onTabChanged(1), + ), + ), + ], + ), + ); + } + + Widget _buildTab({ + required IconData icon, + required String label, + required bool isSelected, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + color: isSelected ? AppColor.white : Colors.transparent, + borderRadius: BorderRadius.circular(26), + ), + child: Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: 16, + color: isSelected ? AppColor.textPrimary : AppColor.textWhite, + ), + const SizedBox(width: 6), + Text( + label, + style: AppStyle.md.copyWith( + color: isSelected ? AppColor.textPrimary : AppColor.textWhite, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ); + } + + void _showDatePicker(BuildContext context) { + DateRangePickerBottomSheet.show( + context: context, + primaryColor: AppColor.primary, + initialStartDate: state.dateFrom, + initialEndDate: state.dateTo, + maxDate: DateTime.now(), + onChanged: (startDate, endDate) { + if (startDate != null && endDate != null) { + onDateRangeChanged?.call(startDate, endDate); + } + }, + ); + } + + Widget _buildHeaderValueShimmer() { + return Shimmer.fromColors( + baseColor: AppColor.textWhite.withOpacity(0.3), + highlightColor: AppColor.textWhite.withOpacity(0.6), + child: Container( + width: 200, + height: 36, + decoration: BoxDecoration( + color: AppColor.textWhite.withOpacity(0.3), + borderRadius: BorderRadius.circular(8), + ), + ), + ); + } + + Widget _buildHeaderChipsShimmer() { + return Row( + children: List.generate( + 3, + (index) => Padding( + padding: const EdgeInsets.only(right: 8), + child: Shimmer.fromColors( + baseColor: AppColor.textWhite.withOpacity(0.15), + highlightColor: AppColor.textWhite.withOpacity(0.3), + child: Container( + width: 90, + height: 32, + decoration: BoxDecoration( + color: AppColor.textWhite.withOpacity(0.15), + borderRadius: BorderRadius.circular(20), + ), + ), + ), + ), + ), + ); + } + + Widget _buildHeaderChips(BuildContext context) { + final summary = state.inventoryAnalytic.summary; + + if (selectedTabIndex == 0) { + return Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _buildChip('${summary.totalProducts} ${context.lang.product}'), + _buildChip('${summary.lowStockProducts} ${context.lang.low_stock}'), + _buildChip('${summary.zeroStockProducts} ${context.lang.zero_stock}'), + ], + ); + } else { + return Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _buildChip('${summary.totalIngredients} ${context.lang.ingredients}'), + _buildChip( + '${summary.lowStockIngredients} ${context.lang.low_stock}', + ), + _buildChip( + '${summary.zeroStockIngredients} ${context.lang.zero_stock}', + ), + ], + ); + } + } + + Widget _buildChip(String label) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + color: AppColor.textWhite.withOpacity(0.15), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColor.textWhite.withOpacity(0.25)), + ), + child: Text( + label, + style: AppStyle.sm.copyWith( + color: AppColor.textWhite, + fontWeight: FontWeight.w600, + fontSize: 12, + ), + ), + ); + } + + String _formatDateRange(DateTime from, DateTime to) { + const months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'Mei', + 'Jun', + 'Jul', + 'Agu', + 'Sep', + 'Okt', + 'Nov', + 'Des', + ]; + + if (from.year == to.year && from.month == to.month && from.day == to.day) { + return '${from.day} ${months[from.month - 1]} ${from.year}'; + } + return '${from.day} ${months[from.month - 1]} - ${to.day} ${months[to.month - 1]} ${to.year}'; + } +} diff --git a/lib/presentation/pages/inventory/widgets/inventory_stock_report.dart b/lib/presentation/pages/inventory/widgets/inventory_stock_report.dart new file mode 100644 index 0000000..ebaa39f --- /dev/null +++ b/lib/presentation/pages/inventory/widgets/inventory_stock_report.dart @@ -0,0 +1,638 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import '../../../../common/extension/extension.dart'; +import '../../../../common/theme/theme.dart'; +import '../../../../domain/analytic/analytic.dart'; +import '../../../components/spacer/spacer.dart'; + +class InventoryStockReport extends StatelessWidget { + final InventoryAnalytic inventoryAnalytic; + final int selectedTabIndex; + + const InventoryStockReport({ + super.key, + required this.inventoryAnalytic, + required this.selectedTabIndex, + }); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppColor.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: AppColor.textLight.withOpacity(0.08), + spreadRadius: 1, + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + selectedTabIndex == 0 + ? context.lang.product + : context.lang.ingredients, + style: AppStyle.lg.copyWith( + fontWeight: FontWeight.w700, + color: AppColor.textPrimary, + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 4, + ), + decoration: BoxDecoration( + color: AppColor.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + selectedTabIndex == 0 + ? '${inventoryAnalytic.products.length} item' + : '${inventoryAnalytic.ingredients.length} item', + style: AppStyle.sm.copyWith( + color: AppColor.primary, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + + const SpaceHeight(16), + + // Summary stats row + _buildSummaryStats(context), + + const SpaceHeight(16), + + const Divider(height: 1, color: AppColor.borderLight), + + const SpaceHeight(12), + + // Table header + _buildTableHeader(context), + + const SpaceHeight(8), + + // Items list + if (selectedTabIndex == 0) + ...inventoryAnalytic.products.map( + (product) => _buildProductRow(context, product), + ) + else + ...inventoryAnalytic.ingredients.map( + (ingredient) => _buildIngredientRow(context, ingredient), + ), + + const SpaceHeight(12), + + // Footer totals + _buildFooterTotals(context), + ], + ), + ); + } + + Widget _buildSummaryStats(BuildContext context) { + final summary = inventoryAnalytic.summary; + + if (selectedTabIndex == 0) { + return Row( + children: [ + Expanded( + child: _buildMiniStat( + context.lang.total_sold, + summary.totalSoldProducts.toString(), + AppColor.success, + Icons.check_circle_rounded, + ), + ), + const SpaceWidth(8), + Expanded( + child: _buildMiniStat( + context.lang.low_stock, + summary.lowStockProducts.toString(), + AppColor.warning, + Icons.warning_rounded, + ), + ), + const SpaceWidth(8), + Expanded( + child: _buildMiniStat( + context.lang.zero_stock, + summary.zeroStockProducts.toString(), + AppColor.error, + Icons.error_rounded, + ), + ), + ], + ); + } else { + return Row( + children: [ + Expanded( + child: _buildMiniStat( + context.lang.total_sold, + summary.totalSoldIngredients.toString(), + AppColor.success, + Icons.check_circle_rounded, + ), + ), + const SpaceWidth(8), + Expanded( + child: _buildMiniStat( + context.lang.low_stock, + summary.lowStockIngredients.toString(), + AppColor.warning, + Icons.warning_rounded, + ), + ), + const SpaceWidth(8), + Expanded( + child: _buildMiniStat( + context.lang.zero_stock, + summary.zeroStockIngredients.toString(), + AppColor.error, + Icons.error_rounded, + ), + ), + ], + ); + } + } + + Widget _buildMiniStat( + String label, + String value, + Color color, + IconData icon, + ) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + decoration: BoxDecoration( + color: color.withOpacity(0.08), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + Icon(icon, size: 18, color: color), + const SpaceHeight(4), + Text( + value, + style: AppStyle.lg.copyWith( + fontWeight: FontWeight.w800, + color: color, + ), + ), + const SpaceHeight(2), + Text( + label, + style: AppStyle.xs.copyWith( + color: color.withOpacity(0.8), + fontWeight: FontWeight.w500, + fontSize: 10, + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ); + } + + Widget _buildTableHeader(BuildContext context) { + if (selectedTabIndex == 0) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + Expanded( + flex: 4, + child: Text( + context.lang.product, + style: AppStyle.sm.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textSecondary, + ), + ), + ), + Expanded( + flex: 2, + child: Text( + context.lang.stock, + textAlign: TextAlign.center, + style: AppStyle.sm.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textSecondary, + ), + ), + ), + Expanded( + flex: 2, + child: Text( + context.lang.in_text, + textAlign: TextAlign.center, + style: AppStyle.sm.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textSecondary, + ), + ), + ), + Expanded( + flex: 2, + child: Text( + context.lang.out_text, + textAlign: TextAlign.center, + style: AppStyle.sm.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textSecondary, + ), + ), + ), + ], + ), + ); + } else { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + Expanded( + flex: 4, + child: Text( + context.lang.ingredients, + style: AppStyle.sm.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textSecondary, + ), + ), + ), + Expanded( + flex: 2, + child: Text( + context.lang.stock, + textAlign: TextAlign.center, + style: AppStyle.sm.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textSecondary, + ), + ), + ), + Expanded( + flex: 2, + child: Text( + context.lang.in_text, + textAlign: TextAlign.center, + style: AppStyle.sm.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textSecondary, + ), + ), + ), + Expanded( + flex: 2, + child: Text( + context.lang.out_text, + textAlign: TextAlign.center, + style: AppStyle.sm.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textSecondary, + ), + ), + ), + ], + ), + ); + } + } + + Widget _buildProductRow(BuildContext context, InventoryProduct product) { + final statusColor = product.isZeroStock + ? AppColor.error + : product.isLowStock + ? AppColor.warning + : AppColor.textPrimary; + + return Container( + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: AppColor.borderLight, width: 0.5), + ), + ), + child: Row( + children: [ + // Product name + category + Expanded( + flex: 4, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.productName, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textPrimary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SpaceHeight(2), + Row( + children: [ + if (product.isZeroStock || product.isLowStock) + Container( + width: 6, + height: 6, + margin: const EdgeInsets.only(right: 4), + decoration: BoxDecoration( + color: statusColor, + shape: BoxShape.circle, + ), + ), + Flexible( + child: Text( + product.categoryName, + style: AppStyle.xs.copyWith( + color: AppColor.textSecondary, + fontWeight: FontWeight.w400, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + + // Stock + Expanded( + flex: 2, + child: Text( + NumberFormat('#,###', 'id_ID').format(product.quantity), + textAlign: TextAlign.center, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w600, + color: statusColor, + ), + ), + ), + + // In + Expanded( + flex: 2, + child: Text( + product.totalIn > 0 + ? '+${NumberFormat('#,###', 'id_ID').format(product.totalIn)}' + : '-', + textAlign: TextAlign.center, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w500, + color: product.totalIn > 0 + ? AppColor.success + : AppColor.textSecondary, + ), + ), + ), + + // Out + Expanded( + flex: 2, + child: Text( + product.totalOut > 0 + ? '-${NumberFormat('#,###', 'id_ID').format(product.totalOut)}' + : '-', + textAlign: TextAlign.center, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w500, + color: product.totalOut > 0 + ? AppColor.error + : AppColor.textSecondary, + ), + ), + ), + ], + ), + ); + } + + Widget _buildIngredientRow( + BuildContext context, + InventoryIngredient ingredient, + ) { + final statusColor = ingredient.isZeroStock + ? AppColor.error + : ingredient.isLowStock + ? AppColor.warning + : AppColor.textPrimary; + + return Container( + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: AppColor.borderLight, width: 0.5), + ), + ), + child: Row( + children: [ + // Ingredient name + unit + Expanded( + flex: 4, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + ingredient.ingredientName, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textPrimary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SpaceHeight(2), + Row( + children: [ + if (ingredient.isZeroStock || ingredient.isLowStock) + Container( + width: 6, + height: 6, + margin: const EdgeInsets.only(right: 4), + decoration: BoxDecoration( + color: statusColor, + shape: BoxShape.circle, + ), + ), + Flexible( + child: Text( + ingredient.unitName, + style: AppStyle.xs.copyWith( + color: AppColor.textSecondary, + fontWeight: FontWeight.w400, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + + // Stock + Expanded( + flex: 2, + child: Text( + NumberFormat('#,###', 'id_ID').format(ingredient.quantity), + textAlign: TextAlign.center, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w600, + color: statusColor, + ), + ), + ), + + // In + Expanded( + flex: 2, + child: Text( + ingredient.totalIn > 0 + ? '+${NumberFormat('#,###', 'id_ID').format(ingredient.totalIn)}' + : '-', + textAlign: TextAlign.center, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w500, + color: ingredient.totalIn > 0 + ? AppColor.success + : AppColor.textSecondary, + ), + ), + ), + + // Out + Expanded( + flex: 2, + child: Text( + ingredient.totalOut > 0 + ? '-${NumberFormat('#,###', 'id_ID').format(ingredient.totalOut)}' + : '-', + textAlign: TextAlign.center, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w500, + color: ingredient.totalOut > 0 + ? AppColor.error + : AppColor.textSecondary, + ), + ), + ), + ], + ), + ); + } + + Widget _buildFooterTotals(BuildContext context) { + int totalStock; + int totalIn; + int totalOut; + + if (selectedTabIndex == 0) { + totalStock = inventoryAnalytic.products.fold( + 0, + (sum, item) => sum + item.quantity, + ); + totalIn = inventoryAnalytic.products.fold( + 0, + (sum, item) => sum + item.totalIn, + ); + totalOut = inventoryAnalytic.products.fold( + 0, + (sum, item) => sum + item.totalOut, + ); + } else { + totalStock = inventoryAnalytic.ingredients.fold( + 0, + (sum, item) => sum + item.quantity, + ); + totalIn = inventoryAnalytic.ingredients.fold( + 0, + (sum, item) => sum + item.totalIn, + ); + totalOut = inventoryAnalytic.ingredients.fold( + 0, + (sum, item) => sum + item.totalOut, + ); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + decoration: BoxDecoration( + color: AppColor.primary.withOpacity(0.08), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Expanded( + flex: 4, + child: Text( + 'TOTAL', + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w700, + color: AppColor.primary, + ), + ), + ), + Expanded( + flex: 2, + child: Text( + NumberFormat('#,###', 'id_ID').format(totalStock), + textAlign: TextAlign.center, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w700, + color: AppColor.primary, + ), + ), + ), + Expanded( + flex: 2, + child: Text( + '+${NumberFormat('#,###', 'id_ID').format(totalIn)}', + textAlign: TextAlign.center, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w700, + color: AppColor.success, + ), + ), + ), + Expanded( + flex: 2, + child: Text( + '-${NumberFormat('#,###', 'id_ID').format(totalOut)}', + textAlign: TextAlign.center, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w700, + color: AppColor.error, + ), + ), + ), + ], + ), + ); + } +}