feat: Tab Category Home
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user