// ======================================== // HOMEPAGE - LOCAL DATA ONLY, NO SYNC // lib/presentation/home/pages/home_page.dart // ======================================== import 'dart:developer'; import 'package:enaklo_pos/core/components/flushbar.dart'; import 'package:enaklo_pos/data/models/response/category_response_model.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/models/product_quantity.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'; import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; import 'package:enaklo_pos/core/extensions/int_ext.dart'; import 'package:enaklo_pos/data/models/response/table_model.dart'; import 'package:enaklo_pos/presentation/home/pages/confirm_payment_page.dart'; import '../../../core/assets/assets.gen.dart'; import '../../../core/components/buttons.dart'; import '../../../core/components/spaces.dart'; import '../../../core/constants/colors.dart'; import '../bloc/checkout/checkout_bloc.dart'; import '../widgets/home_title.dart'; import '../widgets/order_menu.dart'; import '../widgets/product_card.dart'; class HomePage extends StatefulWidget { final bool isTable; final TableModel? table; final List items; const HomePage({ super.key, required this.isTable, this.table, required this.items, }); @override State createState() => _HomePageState(); } class _HomePageState extends State { final searchController = TextEditingController(); final ScrollController scrollController = ScrollController(); String searchQuery = ''; @override void initState() { super.initState(); _loadData(); } @override void dispose() { searchController.dispose(); scrollController.dispose(); super.dispose(); } void _loadData() { log('📱 Loading data from local database...'); // Load categories from local database context .read() .add(const CategoryLoaderEvent.getCategories()); // Load products from local database context .read() .add(const ProductLoaderEvent.getProduct()); // Initialize other components context.read().add(CheckoutEvent.started(widget.items)); context.read().add(CurrentOutletEvent.currentOutlet()); } // void _refreshData() { // log('🔄 Refreshing l ocal data...'); // context.read().add(const ProductLoaderEvent.refresh()); // context.read().add(const CategoryLoaderEvent.refresh()); // } void onCategoryTap(int index) { searchController.clear(); setState(() { searchQuery = ''; }); } bool _handleScrollNotification( ScrollNotification notification, String? categoryId) { if (notification is ScrollEndNotification && scrollController.position.extentAfter == 0) { log('📄 Loading more products...'); context.read().add( ProductLoaderEvent.loadMore(), ); return true; } return false; } @override Widget build(BuildContext context) { return BlocListener( listener: (context, state) { state.maybeWhen( orElse: () {}, success: () { Future.delayed(Duration(milliseconds: 300), () { AppFlushbar.showSuccess(context, 'Outlet berhasil diubah'); context .read() .add(CurrentOutletEvent.currentOutlet()); }); }, error: (message) => AppFlushbar.showError(context, message), ); }, child: Hero( tag: 'confirmation_screen', child: Scaffold( backgroundColor: AppColors.white, body: Column( children: [ // Main content Expanded( child: Row( children: [ // Left panel - Products with Categories Expanded( flex: 3, child: BlocBuilder( builder: (context, categoryState) { return categoryState.maybeWhen( orElse: () => _buildCategoryLoadingState(), loading: () => _buildCategoryLoadingState(), error: (message) => _buildCategoryErrorState(message), loaded: (categories, hasReachedMax, currentPage, isLoadingMore, isActive, searchQuery) => _buildCategoryContent(categories), ); }, ), ), // Right panel - Cart Expanded( flex: 2, child: _buildCartSection(), ), ], ), ), ], ), ), ), ); } Widget _buildCategoryLoadingState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(color: AppColors.primary), SizedBox(height: 16), Text( 'Memuat kategori...', style: TextStyle(color: Colors.grey.shade600), ), ], ), ); } Widget _buildCategoryErrorState(String message) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 48, color: Colors.red.shade400), SizedBox(height: 16), Text('Error Kategori', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), SizedBox(height: 8), Padding( padding: EdgeInsets.symmetric(horizontal: 32), child: Text( message, textAlign: TextAlign.center, style: TextStyle(color: Colors.grey.shade600), ), ), SizedBox(height: 16), Button.filled( width: 120, onPressed: () { context .read() .add(const CategoryLoaderEvent.getCategories()); }, label: 'Coba Lagi', ), ], ), ); } Widget _buildCategoryContent(List categories) { return Column( children: [ // Simple home title _buildSimpleHomeTitle(), // Products section with categories Expanded( child: BlocBuilder( builder: (context, productState) { return CategoryTabBar( key: ValueKey(categories.length), categories: categories, tabViews: categories.map((category) { return SizedBox( child: productState.maybeWhen( orElse: () => _buildLoadingState(), loading: () => _buildLoadingState(), loaded: (products, hasReachedMax, currentPage, isLoadingMore, categoryId, searchQuery) { if (products.isEmpty) { return _buildEmptyState(categoryId); } return _buildProductGrid( products, hasReachedMax, isLoadingMore, categoryId, currentPage, ); }, error: (message) => _buildErrorState(message, category.id), ), ); }).toList(), ); }, ), ), ], ); } // Simple home title Widget _buildSimpleHomeTitle() { return HomeTitle( controller: searchController, onChanged: (value) { setState(() { searchQuery = value; }); // Fast local search Future.delayed(Duration(milliseconds: 200), () { if (value == searchController.text) { log('🔍 Local search: "$value"'); context.read().add( ProductLoaderEvent.searchProduct( query: value, ), ); } }); }, ); } Widget _buildLoadingState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(color: AppColors.primary), SizedBox(height: 16), Text( 'Memuat data...', style: TextStyle(color: Colors.grey.shade600), ), ], ), ); } Widget _buildEmptyState(String? categoryId) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Assets.icons.noProduct.svg(), SizedBox(height: 20), Text( searchQuery.isNotEmpty ? 'Produk "$searchQuery" tidak ditemukan' : 'Belum ada data produk', textAlign: TextAlign.center, ), SizedBox(height: 8), Text( 'Data akan dimuat dari database lokal', style: TextStyle( color: Colors.grey.shade600, fontSize: 12, ), textAlign: TextAlign.center, ), SpaceHeight(20), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ if (searchQuery.isNotEmpty) ...[ Button.filled( width: 100, onPressed: () { searchController.clear(); setState(() => searchQuery = ''); context.read().add( ProductLoaderEvent.getProduct(categoryId: categoryId), ); }, label: 'Reset', ), SizedBox(width: 12), ], Button.filled( width: 120, onPressed: () { context.read().add( ProductLoaderEvent.getProduct(categoryId: categoryId), ); }, label: 'Muat Ulang', ), ], ), ], ), ); } Widget _buildErrorState(String message, String? categoryId) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 48, color: Colors.red.shade400), SizedBox(height: 16), Text( 'Error Database', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), SizedBox(height: 8), Padding( padding: EdgeInsets.symmetric(horizontal: 32), child: Text( message, textAlign: TextAlign.center, style: TextStyle(color: Colors.grey.shade600), ), ), SizedBox(height: 16), Button.filled( width: 120, onPressed: () { context.read().add( ProductLoaderEvent.getProduct(categoryId: categoryId), ); }, label: 'Coba Lagi', ), ], ), ); } Widget _buildProductGrid( List products, bool hasReachedMax, bool isLoadingMore, String? categoryId, int currentPage, ) { return Column( children: [ // Simple product count if (products.isNotEmpty) Container( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: Row( children: [ Text( '${products.length} produk ditemukan', style: TextStyle( color: Colors.grey.shade600, fontSize: 12, ), ), Spacer(), if (isLoadingMore) SizedBox( width: 12, height: 12, child: CircularProgressIndicator( strokeWidth: 2, color: AppColors.primary, ), ), ], ), ), // Products grid Expanded( child: NotificationListener( onNotification: (notification) => _handleScrollNotification(notification, categoryId), child: GridView.builder( itemCount: products.length, controller: scrollController, padding: const EdgeInsets.all(16), cacheExtent: 200.0, gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 180, mainAxisSpacing: 30, crossAxisSpacing: 30, childAspectRatio: 180 / 240, ), itemBuilder: (context, index) => ProductCard( data: products[index], onCartButton: () { // Cart functionality }, ), ), ), ), // End indicator if (hasReachedMax && products.isNotEmpty) Container( padding: EdgeInsets.all(8), child: Text( 'Semua produk telah dimuat', style: TextStyle( color: Colors.grey.shade500, fontSize: 11, ), ), ), ], ); } // Cart section (unchanged from original) Widget _buildCartSection() { return Align( alignment: Alignment.topCenter, child: Material( color: Colors.white, child: Column( children: [ HomeRightTitle(table: widget.table), Padding( padding: const EdgeInsets.all(16.0).copyWith(bottom: 0, top: 27), child: Column( children: [ const Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Item', style: TextStyle( color: AppColors.primary, fontSize: 16, fontWeight: FontWeight.w600, ), ), SizedBox(width: 130), SizedBox( width: 50.0, child: Text( 'Qty', style: TextStyle( color: AppColors.primary, fontSize: 16, fontWeight: FontWeight.w600, ), ), ), SizedBox( child: Text( 'Price', style: TextStyle( color: AppColors.primary, fontSize: 16, fontWeight: FontWeight.w600, ), ), ), ], ), const SpaceHeight(8), const Divider(), ], ), ), Expanded( child: BlocBuilder( builder: (context, state) { return state.maybeWhen( orElse: () => const Center(child: Text('No Items')), loaded: (products, discountModel, discount, discountAmount, tax, serviceCharge, totalQuantity, totalPrice, draftName, orderType, deliveryType) { if (products.isEmpty) { return const Center(child: Text('No Items')); } return ListView.separated( shrinkWrap: true, padding: const EdgeInsets.symmetric(horizontal: 16), itemBuilder: (context, index) => OrderMenu(data: products[index]), separatorBuilder: (context, index) => const SpaceHeight(1.0), itemCount: products.length, ); }, ); }, ), ), // Payment section Padding( padding: const EdgeInsets.all(16.0).copyWith(top: 0), child: Column( children: [ const Divider(), const SpaceHeight(16.0), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Pajak', style: TextStyle( color: AppColors.black, fontWeight: FontWeight.bold, ), ), BlocBuilder( builder: (context, state) { final tax = state.maybeWhen( orElse: () => 0, loaded: (products, discountModel, discount, discountAmount, tax, serviceCharge, totalQuantity, totalPrice, draftName, orderType, deliveryType) { if (products.isEmpty) return 0; return tax; }, ); return Text( '$tax %', style: const TextStyle( color: AppColors.primary, fontWeight: FontWeight.w600, ), ); }, ), ], ), const SpaceHeight(16.0), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Sub total', style: TextStyle( color: AppColors.black, fontWeight: FontWeight.bold, ), ), BlocBuilder( builder: (context, state) { final price = state.maybeWhen( orElse: () => 0, loaded: (products, discountModel, discount, discountAmount, tax, serviceCharge, totalQuantity, totalPrice, draftName, orderType, deliveryType) { if (products.isEmpty) return 0; return products .map((e) => (e.product.price! * e.quantity) + (e.variant?.priceModifier ?? 0)) .reduce((value, element) => value + element); }, ); return Text( price.currencyFormatRp, style: const TextStyle( color: AppColors.primary, fontWeight: FontWeight.w900, ), ); }, ), ], ), SpaceHeight(16.0), BlocBuilder( builder: (context, state) { return state.maybeWhen( orElse: () => Align( alignment: Alignment.bottomCenter, child: Button.filled( borderRadius: 12, elevation: 1, disabled: true, onPressed: () { context.push(ConfirmPaymentPage( isTable: widget.table == null ? false : true, table: widget.table, )); }, label: 'Lanjutkan Pembayaran', ), ), loaded: (items, discountModel, discount, discountAmount, tax, serviceCharge, totalQuantity, totalPrice, draftName, orderType, deliveryType) => Align( alignment: Alignment.bottomCenter, child: Button.filled( borderRadius: 12, elevation: 1, disabled: items.isEmpty, onPressed: () { if (orderType.name == 'dineIn' && widget.table == null) { AppFlushbar.showError(context, 'Mohon pilih meja terlebih dahulu'); return; } if (orderType.name == 'delivery' && deliveryType == null) { AppFlushbar.showError(context, 'Mohon pilih pengiriman terlebih dahulu'); return; } context.push(ConfirmPaymentPage( isTable: widget.table == null ? false : true, table: widget.table, )); }, label: 'Lanjutkan Pembayaran', ), ), ); }, ), ], ), ), ], ), ), ); } }