// ======================================== // OFFLINE-ONLY HOMEPAGE - NO API CALLS // lib/presentation/home/pages/home_page.dart // ======================================== import 'dart:developer'; import 'package:enaklo_pos/core/components/flushbar.dart'; import 'package:enaklo_pos/data/datasources/product/product_local_datasource.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 = ''; // Local database only Map _databaseStats = {}; final ProductLocalDatasource _localDatasource = ProductLocalDatasource.instance; bool _isLoadingStats = true; @override void initState() { super.initState(); _initializeLocalData(); _loadProducts(); } @override void dispose() { searchController.dispose(); scrollController.dispose(); super.dispose(); } // Initialize local data only void _initializeLocalData() { _loadDatabaseStats(); } // Load database statistics void _loadDatabaseStats() async { try { final stats = await _localDatasource.getDatabaseStats(); if (mounted) { setState(() { _databaseStats = stats; _isLoadingStats = false; }); } log('📊 Local database stats: $stats'); } catch (e) { log('❌ Error loading local stats: $e'); setState(() { _isLoadingStats = false; }); } } void _loadProducts() { log('📱 Loading products from local database only...'); // Load products from local database only context .read() .add(const ProductLoaderEvent.getProduct()); // Initialize other components context.read().add(CheckoutEvent.started(widget.items)); context.read().add(CategoryLoaderEvent.get()); context.read().add(CurrentOutletEvent.currentOutlet()); } void _refreshLocalData() { log('🔄 Refreshing local data...'); context.read().add(const ProductLoaderEvent.refresh()); _loadDatabaseStats(); } 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 local products for category: $categoryId'); context.read().add( ProductLoaderEvent.loadMore( categoryId: categoryId, ), ); return true; } return false; } @override Widget build(BuildContext context) { return BlocListener( listener: (context, state) { state.maybeWhen( orElse: () {}, loading: () {}, 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: [ // Local database indicator _buildLocalModeIndicator(), // Main content Expanded( child: Row( children: [ // Left panel - Products Expanded( flex: 3, child: Align( alignment: AlignmentDirectional.topStart, child: BlocBuilder( builder: (context, state) { return state.maybeWhen( orElse: () => Center(child: CircularProgressIndicator()), loaded: (categories, categoryId) => Column( mainAxisAlignment: MainAxisAlignment.start, children: [ // Enhanced home title with local stats _buildLocalHomeTitle(categoryId), // Products section Expanded( child: BlocBuilder( builder: (context, productState) { return CategoryTabBar( 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, categoryId), ), ); }).toList(), ); }, ), ), ], ), ); }, ), ), ), // Right panel - Cart (unchanged) Expanded( flex: 2, child: _buildCartSection(), ), ], ), ), ], ), ), ), ); } // Local mode indicator Widget _buildLocalModeIndicator() { return Container( width: double.infinity, padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16), color: Colors.blue.shade600, child: Row( children: [ Icon(Icons.storage, color: Colors.white, size: 16), SizedBox(width: 8), Expanded( child: Text( _isLoadingStats ? 'Mode Lokal - Memuat data...' : 'Mode Lokal - ${_databaseStats['total_products'] ?? 0} produk tersimpan', style: TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.w500, ), ), ), if (_databaseStats.isNotEmpty) ...[ Text( '${(_databaseStats['database_size_mb'] ?? 0.0).toStringAsFixed(1)} MB', style: TextStyle( color: Colors.white.withOpacity(0.8), fontSize: 11, ), ), SizedBox(width: 8), ], InkWell( onTap: _refreshLocalData, child: Container( padding: EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(4), ), child: Icon(Icons.refresh, color: Colors.white, size: 14), ), ), ], ), ); } // Enhanced home title with local stats only Widget _buildLocalHomeTitle(String? categoryId) { return Container( padding: EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, border: Border(bottom: BorderSide(color: Colors.grey.shade200)), ), child: Column( children: [ // Original HomeTitle with faster search HomeTitle( controller: searchController, onChanged: (value) { setState(() { searchQuery = value; }); // Fast local search - no debounce needed for local data Future.delayed(Duration(milliseconds: 200), () { if (value == searchController.text) { log('🔍 Local search: "$value"'); context.read().add( ProductLoaderEvent.searchProduct( categoryId: categoryId, query: value, ), ); } }); }, ), // Local database stats if (_databaseStats.isNotEmpty) ...[ SizedBox(height: 8), Row( children: [ // Local storage indicator Container( padding: EdgeInsets.symmetric(horizontal: 6, vertical: 3), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(10), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.storage, size: 12, color: Colors.blue.shade600), SizedBox(width: 3), Text( 'Lokal', style: TextStyle( fontSize: 10, color: Colors.blue.shade600, fontWeight: FontWeight.w500, ), ), ], ), ), SizedBox(width: 8), // Database stats chips _buildStatChip( '${_databaseStats['total_products'] ?? 0}', 'produk', Icons.inventory_2, Colors.green, ), SizedBox(width: 6), _buildStatChip( '${_databaseStats['total_variants'] ?? 0}', 'varian', Icons.tune, Colors.orange, ), SizedBox(width: 6), _buildStatChip( '${_databaseStats['cache_entries'] ?? 0}', 'cache', Icons.memory, Colors.purple, ), Spacer(), // Clear cache button InkWell( onTap: () { _localDatasource.clearExpiredCache(); _loadDatabaseStats(); AppFlushbar.showSuccess(context, 'Cache dibersihkan'); }, child: Container( padding: EdgeInsets.all(5), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(5), ), child: Icon( Icons.clear_all, size: 14, color: Colors.grey.shade600, ), ), ), SizedBox(width: 4), // Refresh button InkWell( onTap: _refreshLocalData, child: Container( padding: EdgeInsets.all(5), decoration: BoxDecoration( color: AppColors.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(5), ), child: Icon( Icons.refresh, size: 14, color: AppColors.primary, ), ), ), ], ), ], ], ), ); } Widget _buildStatChip( String value, String label, IconData icon, Color color) { return Container( padding: EdgeInsets.symmetric(horizontal: 5, vertical: 2), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 10, color: color), SizedBox(width: 2), Text( value, style: TextStyle( fontSize: 9, fontWeight: FontWeight.w600, color: color, ), ), SizedBox(width: 1), Text( label, style: TextStyle( fontSize: 8, color: color.withOpacity(0.8), ), ), ], ), ); } Widget _buildLoadingState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(color: AppColors.primary), SizedBox(height: 16), Text( 'Memuat data lokal...', 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 lokal', textAlign: TextAlign.center, ), SizedBox(height: 8), Text( 'Tambahkan produk ke database lokal terlebih dahulu', 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 Lokal', 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: [ // Product count with local indicator if (products.isNotEmpty) Container( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: Row( children: [ Container( padding: EdgeInsets.symmetric(horizontal: 4, vertical: 1), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(4), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.storage, size: 10, color: Colors.blue.shade600), SizedBox(width: 2), Text( '${products.length}', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Colors.blue.shade600, ), ), ], ), ), SizedBox(width: 6), Text( 'produk dari database lokal', style: TextStyle( color: Colors.grey.shade600, fontSize: 11, ), ), if (currentPage > 1) ...[ SizedBox(width: 6), Container( padding: EdgeInsets.symmetric(horizontal: 4, vertical: 1), decoration: BoxDecoration( color: AppColors.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(4), ), child: Text( 'Hal $currentPage', style: TextStyle( color: AppColors.primary, fontSize: 9, fontWeight: FontWeight.w500, ), ), ), ], Spacer(), if (isLoadingMore) SizedBox( width: 12, height: 12, child: CircularProgressIndicator( strokeWidth: 2, color: AppColors.primary, ), ), ], ), ), // Products grid - faster loading from local DB Expanded( child: NotificationListener( onNotification: (notification) => _handleScrollNotification(notification, categoryId), child: GridView.builder( itemCount: products.length, controller: scrollController, padding: const EdgeInsets.all(16), cacheExtent: 200.0, // Bigger cache for smooth scrolling gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 180, mainAxisSpacing: 30, crossAxisSpacing: 30, childAspectRatio: 180 / 240, ), itemBuilder: (context, index) => ProductCard( data: products[index], onCartButton: () { // Cart functionality }, ), ), ), ), // End of data indicator if (hasReachedMax && products.isNotEmpty) Container( padding: EdgeInsets.all(8), child: Text( 'Semua produk lokal 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 (unchanged) 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', ), ), ); }, ), ], ), ), ], ), ), ); } }