feat: Tab Category Home

This commit is contained in:
efrilm
2025-08-06 17:41:04 +07:00
parent 7ee7db225c
commit 561aec371d
10 changed files with 1138 additions and 224 deletions
+77 -182
View File
@@ -1,8 +1,10 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:enaklo_pos/core/components/flushbar.dart';
import 'package:enaklo_pos/presentation/home/bloc/category_loader/category_loader_bloc.dart';
import 'package:enaklo_pos/presentation/home/bloc/current_outlet/current_outlet_bloc.dart';
import 'package:enaklo_pos/presentation/home/bloc/product_loader/product_loader_bloc.dart';
import 'package:enaklo_pos/presentation/home/bloc/user_update_outlet/user_update_outlet_bloc.dart';
import 'package:enaklo_pos/presentation/home/widgets/category_tab_bar.dart';
import 'package:enaklo_pos/presentation/home/widgets/home_right_title.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -18,7 +20,6 @@ import '../../../core/components/buttons.dart';
import '../../../core/components/spaces.dart';
import '../../../core/constants/colors.dart';
import '../bloc/checkout/checkout_bloc.dart';
import '../widgets/custom_tab_bar.dart';
import '../widgets/home_title.dart';
import '../widgets/order_menu.dart';
import '../widgets/product_card.dart';
@@ -53,6 +54,14 @@ class _HomePageState extends State<HomePage> {
super.initState();
}
@override
void dispose() {
// Properly dispose controllers
searchController.dispose();
scrollController.dispose();
super.dispose();
}
void _syncAndLoadProducts() {
// Trigger sync from API first
// context.read<SyncProductBloc>().add(const SyncProductEvent.syncProduct());
@@ -68,6 +77,9 @@ class _HomePageState extends State<HomePage> {
// Initialize checkout with tax and service charge settings
context.read<CheckoutBloc>().add(const CheckoutEvent.started());
// Get Category
context.read<CategoryLoaderBloc>().add(CategoryLoaderEvent.get());
// Get Outlets
context.read<CurrentOutletBloc>().add(CurrentOutletEvent.currentOutlet());
}
@@ -91,12 +103,20 @@ class _HomePageState extends State<HomePage> {
}).toList();
}
List<Product> _filterProductsByCategory(
List<Product> products, int categoryId) {
final filteredBySearch = _filterProducts(products);
return filteredBySearch
.where((element) => element.price == categoryId)
.toList();
bool _handleScrollNotification(ScrollNotification notification) {
// Check if the ScrollController is attached before accessing position
if (!scrollController.hasClients) {
return false;
}
if (notification is ScrollEndNotification &&
scrollController.position.extentAfter == 0) {
context
.read<ProductLoaderBloc>()
.add(const ProductLoaderEvent.loadMore());
return true;
}
return false;
}
@override
@@ -147,99 +167,58 @@ class _HomePageState extends State<HomePage> {
orElse: () => false,
loaded: (products, hasReachedMax, currentPage,
isLoadingMore) {
if (notification is ScrollEndNotification &&
scrollController.position.extentAfter ==
0) {
context.read<ProductLoaderBloc>().add(
const ProductLoaderEvent.loadMore());
return true;
}
return true;
return _handleScrollNotification(
notification);
},
);
},
child: Expanded(
child: CustomTabBarV2(
tabTitles: const [
'Semua',
'Makanan',
'Minuman',
'Paket'
],
tabViews: [
// All Products Tab
SizedBox(
child: state.maybeWhen(orElse: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loading: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loaded: (products, hashasReachedMax,
currentPage, isLoadingMore) {
final filteredProducts =
_filterProducts(products);
if (filteredProducts.isEmpty) {
return const Center(
child: Text('No Items Found'),
);
}
return GridView.builder(
itemCount: filteredProducts.length,
controller: scrollController,
padding: const EdgeInsets.all(16),
gridDelegate:
SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 180,
mainAxisSpacing: 30,
crossAxisSpacing: 30,
childAspectRatio: 180 / 240,
),
itemBuilder: (context, index) =>
ProductCard(
data: filteredProducts[index],
onCartButton: () {},
),
);
}),
),
// Makanan Tab
SizedBox(
child: state.maybeWhen(orElse: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loading: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loaded: (products, hashasReachedMax,
currentPage, isLoadingMore) {
if (products.isEmpty) {
return const Center(
child: Text('No Items'),
);
}
final filteredProducts =
_filterProductsByCategory(
products, 1);
return filteredProducts.isEmpty
? const _IsEmpty()
: GridView.builder(
child: Expanded(child: BlocBuilder<
CategoryLoaderBloc, CategoryLoaderState>(
builder: (contextCategory, stateCateogry) {
return stateCateogry.maybeWhen(
orElse: () => SizedBox.shrink(),
loading: () {
return const Center(
child: CircularProgressIndicator(),
);
},
loaded: (categories) {
return CategoryTabBar(
categories: categories,
tabViews: categories.map((category) {
return SizedBox(
child: state.maybeWhen(orElse: () {
return const Center(
child:
CircularProgressIndicator(),
);
}, loading: () {
return const Center(
child:
CircularProgressIndicator(),
);
}, loaded: (products,
hashasReachedMax,
currentPage,
isLoadingMore) {
final filteredProducts =
_filterProducts(products);
if (filteredProducts.isEmpty) {
return const Center(
child: Text('No Items Found'),
);
}
return GridView.builder(
itemCount:
filteredProducts.length,
padding: const EdgeInsets.all(16),
controller: scrollController,
padding: const EdgeInsets.all(16),
gridDelegate:
SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent:
200, // Lebar maksimal tiap item (bisa kamu ubah)
maxCrossAxisExtent: 180,
mainAxisSpacing: 30,
crossAxisSpacing: 30,
childAspectRatio: 0.85,
childAspectRatio: 180 / 240,
),
itemBuilder: (context, index) =>
ProductCard(
@@ -247,99 +226,14 @@ class _HomePageState extends State<HomePage> {
onCartButton: () {},
),
);
}),
),
// Minuman Tab
SizedBox(
child: state.maybeWhen(orElse: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loading: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loaded: (products, hashasReachedMax,
currentPage, isLoadingMore) {
if (products.isEmpty) {
return const Center(
child: Text('No Items'),
}),
);
}
final filteredProducts =
_filterProductsByCategory(
products, 2);
return filteredProducts.isEmpty
? const _IsEmpty()
: GridView.builder(
itemCount:
filteredProducts.length,
padding: const EdgeInsets.all(16),
controller: scrollController,
gridDelegate:
SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent:
200, // Lebar maksimal tiap item (bisa kamu ubah)
mainAxisSpacing: 30,
crossAxisSpacing: 30,
childAspectRatio: 0.85,
),
itemBuilder: (context, index) {
return ProductCard(
data: filteredProducts[index],
onCartButton: () {},
);
},
);
}),
),
// Snack Tab
SizedBox(
child: state.maybeWhen(orElse: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loading: () {
return const Center(
child: CircularProgressIndicator(),
);
}, loaded: (products, hashasReachedMax,
currentPage, isLoadingMore) {
if (products.isEmpty) {
return const Center(
child: Text('No Items'),
);
}
final filteredProducts =
_filterProductsByCategory(
products, 3);
return filteredProducts.isEmpty
? const _IsEmpty()
: GridView.builder(
itemCount:
filteredProducts.length,
padding: const EdgeInsets.all(16),
controller: scrollController,
gridDelegate:
SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent:
200, // Lebar maksimal tiap item (bisa kamu ubah)
mainAxisSpacing: 30,
crossAxisSpacing: 30,
childAspectRatio: 0.85,
),
itemBuilder: (context, index) {
return ProductCard(
data: filteredProducts[index],
onCartButton: () {},
);
},
);
}),
),
],
),
),
}).toList(),
);
},
);
},
)),
);
},
),
@@ -611,6 +505,7 @@ class _HomePageState extends State<HomePage> {
}
}
// ignore: unused_element
class _IsEmpty extends StatelessWidget {
const _IsEmpty();