2025-08-15 18:02:09 +07:00

557 lines
15 KiB
Dart

import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import '../../../common/theme/theme.dart';
import 'widgets/appbar.dart';
import 'widgets/ingredient_tile.dart';
import 'widgets/product_tile.dart';
import 'widgets/stat_card.dart';
import 'widgets/tabbar_delegate.dart';
// Sample inventory data for products
class ProductItem {
final String id;
final String name;
final String category;
final int quantity;
final double price;
final String status;
final String image;
ProductItem({
required this.id,
required this.name,
required this.category,
required this.quantity,
required this.price,
required this.status,
required this.image,
});
}
// Sample inventory data for ingredients
class IngredientItem {
final String id;
final String name;
final String unit;
final double quantity;
final double minQuantity;
final String status;
final String image;
IngredientItem({
required this.id,
required this.name,
required this.unit,
required this.quantity,
required this.minQuantity,
required this.status,
required this.image,
});
}
// Custom SliverPersistentHeaderDelegate untuk TabBar
@RoutePage()
class InventoryPage extends StatefulWidget {
const InventoryPage({super.key});
@override
State<InventoryPage> createState() => _InventoryPageState();
}
class _InventoryPageState extends State<InventoryPage>
with TickerProviderStateMixin {
late AnimationController _fadeAnimationController;
late AnimationController _slideAnimationController;
late AnimationController _rotationAnimationController;
late Animation<double> _fadeAnimation;
late Animation<Offset> _slideAnimation;
late Animation<double> rotationAnimation;
late TabController _tabController;
final List<ProductItem> productItems = [
ProductItem(
id: '1',
name: 'Laptop Gaming ASUS ROG',
category: 'Elektronik',
quantity: 5,
price: 15000000,
status: 'available',
image: '💻',
),
ProductItem(
id: '2',
name: 'Kemeja Formal Pria',
category: 'Fashion',
quantity: 25,
price: 250000,
status: 'available',
image: '👔',
),
ProductItem(
id: '3',
name: 'Smartphone Samsung Galaxy',
category: 'Elektronik',
quantity: 12,
price: 8500000,
status: 'available',
image: '📱',
),
ProductItem(
id: '4',
name: 'Tas Ransel Travel',
category: 'Fashion',
quantity: 8,
price: 350000,
status: 'low_stock',
image: '🎒',
),
ProductItem(
id: '4',
name: 'Tas Ransel Travel',
category: 'Fashion',
quantity: 8,
price: 350000,
status: 'low_stock',
image: '🎒',
),
ProductItem(
id: '4',
name: 'Tas Ransel Travel',
category: 'Fashion',
quantity: 8,
price: 350000,
status: 'low_stock',
image: '🎒',
),
];
final List<IngredientItem> ingredientItems = [
IngredientItem(
id: '1',
name: 'Tepung Terigu',
unit: 'kg',
quantity: 50.5,
minQuantity: 10.0,
status: 'available',
image: '🌾',
),
IngredientItem(
id: '2',
name: 'Gula Pasir',
unit: 'kg',
quantity: 2.5,
minQuantity: 5.0,
status: 'low_stock',
image: '🍬',
),
IngredientItem(
id: '3',
name: 'Telur Ayam',
unit: 'butir',
quantity: 120,
minQuantity: 50,
status: 'available',
image: '🥚',
),
IngredientItem(
id: '4',
name: 'Susu Segar',
unit: 'liter',
quantity: 0,
minQuantity: 10.0,
status: 'out_of_stock',
image: '🥛',
),
IngredientItem(
id: '5',
name: 'Mentega',
unit: 'kg',
quantity: 15.2,
minQuantity: 5.0,
status: 'available',
image: '🧈',
),
];
@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,
);
_rotationAnimationController = AnimationController(
duration: const Duration(seconds: 20),
vsync: this,
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _fadeAnimationController,
curve: Curves.easeInOut,
),
);
_slideAnimation =
Tween<Offset>(begin: const Offset(0.0, 0.3), end: Offset.zero).animate(
CurvedAnimation(
parent: _slideAnimationController,
curve: Curves.easeOutBack,
),
);
rotationAnimation = Tween<double>(
begin: 0.0,
end: 2 * 3.14159,
).animate(_rotationAnimationController);
_fadeAnimationController.forward();
_slideAnimationController.forward();
_rotationAnimationController.repeat();
}
@override
void dispose() {
_fadeAnimationController.dispose();
_slideAnimationController.dispose();
_rotationAnimationController.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 Scaffold(
backgroundColor: AppColor.background,
body: FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(
position: _slideAnimation,
child: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) {
return [
_buildSliverAppBar(),
SliverPersistentHeader(
pinned: true,
delegate: InventorySliverTabBarDelegate(
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(), _buildIngredientTab()],
),
),
),
),
);
}
Widget _buildSliverAppBar() {
return SliverAppBar(
expandedHeight: 120,
floating: false,
pinned: true,
elevation: 0,
backgroundColor: AppColor.primary,
flexibleSpace: InventoryAppBar(rotationAnimation: rotationAnimation),
);
}
Widget _buildProductTab() {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(child: _buildProductStats()),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.75,
),
delegate: SliverChildBuilderDelegate(
(context, index) =>
InventoryProductTile(item: productItems[index]),
childCount: productItems.length,
),
),
),
],
);
}
Widget _buildIngredientTab() {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(child: _buildIngredientStats()),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) =>
InventoryIngredientTile(item: ingredientItems[index]),
childCount: ingredientItems.length,
),
),
),
],
);
}
Widget _buildProductStats() {
final totalProducts = productItems.length;
final availableProducts = productItems
.where((item) => item.status == 'available')
.length;
final lowStockProducts = productItems
.where((item) => item.status == 'low_stock')
.length;
return Container(
margin: const EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
Expanded(
child: _buildStatCard(
'Total Produk',
totalProducts.toString(),
Icons.inventory_2_rounded,
AppColor.primary,
'+12%',
),
),
const SizedBox(width: 16),
Expanded(
child: _buildStatCard(
'Tersedia',
availableProducts.toString(),
Icons.check_circle_rounded,
AppColor.success,
'+5%',
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildStatCard(
'Stok Rendah',
lowStockProducts.toString(),
Icons.warning_rounded,
AppColor.warning,
'-8%',
),
),
const SizedBox(width: 16),
Expanded(
child: Container(), // Empty space for balance
),
],
),
],
),
);
}
Widget _buildIngredientStats() {
final totalIngredients = ingredientItems.length;
final availableIngredients = ingredientItems
.where((item) => item.status == 'available')
.length;
final lowStockIngredients = ingredientItems
.where((item) => item.status == 'low_stock')
.length;
final outOfStockIngredients = ingredientItems
.where((item) => item.status == 'out_of_stock')
.length;
return Container(
margin: const EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
Expanded(
child: _buildStatCard(
'Total Bahan',
totalIngredients.toString(),
Icons.restaurant_menu_rounded,
AppColor.primary,
'+8%',
),
),
const SizedBox(width: 16),
Expanded(
child: _buildStatCard(
'Tersedia',
availableIngredients.toString(),
Icons.check_circle_rounded,
AppColor.success,
'+15%',
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildStatCard(
'Stok Kurang',
lowStockIngredients.toString(),
Icons.warning_rounded,
AppColor.warning,
'-3%',
),
),
const SizedBox(width: 16),
Expanded(
child: _buildStatCard(
'Habis',
outOfStockIngredients.toString(),
Icons.error_rounded,
AppColor.error,
'+1',
),
),
],
),
],
),
);
}
Widget _buildStatCard(
String title,
String value,
IconData icon,
Color color,
String change,
) {
return TweenAnimationBuilder<double>(
tween: Tween<double>(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,
change: change,
),
);
},
);
}
}