2025-08-17 12:50:10 +07:00

504 lines
14 KiB
Dart

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 '../../../application/category/category_loader/category_loader_bloc.dart';
import '../../../common/theme/theme.dart';
import '../../../domain/category/category.dart';
import '../../../injection.dart';
import '../../components/appbar/appbar.dart';
import '../../components/button/button.dart';
import 'widgets/category_delegate.dart';
import 'widgets/product_tile.dart';
@RoutePage()
class ProductPage extends StatefulWidget implements AutoRouteWrapper {
const ProductPage({super.key});
@override
State<ProductPage> createState() => _ProductPageState();
@override
Widget wrappedRoute(BuildContext context) => BlocProvider(
create: (context) =>
getIt<CategoryLoaderBloc>()..add(CategoryLoaderEvent.fetched()),
child: this,
);
}
enum ViewType { grid, list }
class _ProductPageState extends State<ProductPage>
with TickerProviderStateMixin {
Category selectedCategory = Category.addAllData();
ViewType currentViewType = ViewType.grid;
// Sample product data
List<Product> products = [
Product(
id: '1',
name: 'Nasi Goreng Special',
price: 25000,
category: 'Makanan',
stock: 50,
imageUrl: 'assets/images/nasi_goreng.jpg',
isActive: true,
),
Product(
id: '8',
name: 'Nasi Goreng',
price: 15000,
category: 'Makanan',
stock: 50,
imageUrl: 'assets/images/nasi_goreng.jpg',
isActive: true,
),
Product(
id: '9',
name: 'Nasi Goreng Telor',
price: 18000,
category: 'Makanan',
stock: 50,
imageUrl: 'assets/images/nasi_goreng.jpg',
isActive: true,
),
Product(
id: '10',
name: 'Mie Goreng ',
price: 18000,
category: 'Makanan',
stock: 50,
imageUrl: 'assets/images/nasi_goreng.jpg',
isActive: true,
),
Product(
id: '2',
name: 'Es Teh Manis',
price: 8000,
category: 'Minuman',
stock: 100,
imageUrl: 'assets/images/es_teh.jpg',
isActive: true,
),
Product(
id: '6',
name: 'Es Jeruk',
price: 10000,
category: 'Minuman',
stock: 100,
imageUrl: 'assets/images/es_teh.jpg',
isActive: true,
),
Product(
id: '7',
name: 'Es Kelapa',
price: 12000,
category: 'Minuman',
stock: 100,
imageUrl: 'assets/images/es_teh.jpg',
isActive: true,
),
Product(
id: '3',
name: 'Keripik Singkong',
price: 15000,
category: 'Snack',
stock: 25,
imageUrl: 'assets/images/keripik.jpg',
isActive: true,
),
Product(
id: '4',
name: 'Es Krim Vanilla',
price: 12000,
category: 'Dessert',
stock: 30,
imageUrl: 'assets/images/ice_cream.jpg',
isActive: false,
),
Product(
id: '5',
name: 'Ayam Bakar',
price: 35000,
category: 'Makanan',
stock: 20,
imageUrl: 'assets/images/ayam_bakar.jpg',
isActive: true,
),
];
List<Product> get filteredProducts {
return products.where((product) {
bool matchesCategory =
selectedCategory.name == 'Semua' ||
product.category == selectedCategory.id;
return matchesCategory;
}).toList();
}
@override
initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColor.background,
body: CustomScrollView(
slivers: [
_buildSliverAppBar(),
_buildCategoryFilter(),
_buildProductContent(),
_buildEmptyState(),
],
),
);
}
Widget _buildSliverAppBar() {
return SliverAppBar(
expandedHeight: 120.0,
floating: false,
pinned: true,
elevation: 0,
flexibleSpace: CustomAppBar(title: 'Produk'),
actions: [
ActionIconButton(onTap: () {}, icon: LineIcons.search),
ActionIconButton(onTap: _showAddProductDialog, icon: LineIcons.plus),
ActionIconButton(
onTap: _toggleViewType,
icon: currentViewType == ViewType.grid
? LineIcons.list
: LineIcons.thLarge,
),
ActionIconButton(onTap: _showOptionsMenu, icon: LineIcons.filter),
],
);
}
Widget _buildCategoryFilter() {
return BlocBuilder<CategoryLoaderBloc, CategoryLoaderState>(
builder: (context, state) {
return SliverPersistentHeader(
pinned: true,
delegate: ProductCategoryHeaderDelegate(
categories: state.categories,
selectedCategory: selectedCategory,
onCategoryChanged: (category) {
setState(() {
selectedCategory = category;
});
},
),
);
},
);
}
Widget _buildProductContent() {
if (filteredProducts.isEmpty) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
return currentViewType == ViewType.grid
? _buildProductGrid()
: _buildProductList();
}
Widget _buildProductGrid() {
return SliverPadding(
padding: const EdgeInsets.all(16.0),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.85,
crossAxisSpacing: 16.0,
mainAxisSpacing: 16.0,
),
delegate: SliverChildBuilderDelegate((context, index) {
final product = filteredProducts[index];
return ProductTile(product: product, onTap: () {});
}, childCount: filteredProducts.length),
),
);
}
Widget _buildProductList() {
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
sliver: SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
final product = filteredProducts[index];
return _buildProductListItem(product);
}, childCount: filteredProducts.length),
),
);
}
Widget _buildProductListItem(Product product) {
return Container(
margin: const EdgeInsets.only(bottom: 12.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: InkWell(
onTap: () {},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
// Product Image
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: AppColor.background,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(
product.imageUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: AppColor.background,
child: Icon(
Icons.image_outlined,
color: AppColor.textLight,
size: 32,
),
);
},
),
),
),
const SizedBox(width: 12),
// Product Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
product.category,
style: TextStyle(fontSize: 12, color: AppColor.textLight),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Rp ${_formatPrice(product.price)}',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColor.primary,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: product.isActive
? Colors.green.withOpacity(0.1)
: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Stock: ${product.stock}',
style: TextStyle(
fontSize: 12,
color: product.isActive
? Colors.green
: Colors.red,
fontWeight: FontWeight.w500,
),
),
),
],
),
],
),
),
// Action Button
IconButton(
onPressed: () {},
icon: const Icon(Icons.more_vert),
color: AppColor.textLight,
),
],
),
),
),
);
}
String _formatPrice(int price) {
return price.toString().replaceAllMapped(
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]}.',
);
}
void _toggleViewType() {
setState(() {
currentViewType = currentViewType == ViewType.grid
? ViewType.list
: ViewType.grid;
});
}
Widget _buildEmptyState() {
if (filteredProducts.isNotEmpty) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
return SliverToBoxAdapter(
child: Container(
height: 300,
margin: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inventory_2_outlined,
size: 64,
color: AppColor.textLight,
),
const SizedBox(height: 16),
Text(
'Tidak ada produk ditemukan',
style: TextStyle(
color: AppColor.textSecondary,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
'Coba ubah filter atau tambah produk baru',
style: TextStyle(color: AppColor.textLight, fontSize: 14),
textAlign: TextAlign.center,
),
],
),
),
);
}
void _showAddProductDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Tambah Produk'),
content: const Text(
'Dialog tambah produk akan diimplementasikan di sini',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('Simpan'),
),
],
),
);
}
void _showOptionsMenu() {
showModalBottomSheet(
context: context,
builder: (context) => Container(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: Icon(
currentViewType == ViewType.grid ? Icons.list : Icons.grid_view,
),
title: Text(
currentViewType == ViewType.grid
? 'Tampilan List'
: 'Tampilan Grid',
),
onTap: () {
Navigator.pop(context);
_toggleViewType();
},
),
ListTile(
leading: const Icon(Icons.sort),
title: const Text('Urutkan'),
onTap: () => Navigator.pop(context),
),
ListTile(
leading: const Icon(Icons.filter_list),
title: const Text('Filter Lanjutan'),
onTap: () => Navigator.pop(context),
),
ListTile(
leading: const Icon(Icons.download),
title: const Text('Export Data'),
onTap: () => Navigator.pop(context),
),
],
),
),
);
}
}
// Product Model
class Product {
final String id;
final String name;
final int price;
final String category;
final int stock;
final String imageUrl;
bool isActive;
Product({
required this.id,
required this.name,
required this.price,
required this.category,
required this.stock,
required this.imageUrl,
required this.isActive,
});
}