From 27798cc23439576a07f8afb310e29be04b1aa4cf Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Wed, 3 Dec 2025 15:53:46 +0700 Subject: [PATCH] update cart/favorite --- docs/cart.sh | 6 +- docs/products.sh | 2 +- lib/core/network/dio_client.dart | 16 +-- lib/core/network/dio_client.g.dart | 2 +- .../datasources/cart_remote_datasource.dart | 14 ++- .../cart/data/models/cart_item_model.dart | 38 ++++++ .../cart/data/models/cart_item_model.g.dart | 13 +- .../repositories/cart_repository_impl.dart | 19 ++- .../cart/domain/entities/cart_item.dart | 20 ++- .../domain/repositories/cart_repository.dart | 4 + .../cart/presentation/pages/cart_page.dart | 24 ++-- .../presentation/providers/cart_provider.dart | 73 ++++++----- .../providers/cart_provider.g.dart | 2 +- .../widgets/cart_item_widget.dart | 14 +-- .../favorite_products_local_datasource.dart | 42 +++++++ .../providers/favorites_provider.dart | 81 ++++++++---- .../providers/favorites_provider.g.dart | 115 ++++++++++++++---- .../products_remote_datasource.dart | 2 +- .../widgets/brand_filter_chips.dart | 2 +- 19 files changed, 370 insertions(+), 119 deletions(-) diff --git a/docs/cart.sh b/docs/cart.sh index 5b9a04b..8761d7c 100644 --- a/docs/cart.sh +++ b/docs/cart.sh @@ -8,12 +8,14 @@ curl --location 'https://land.dbiz.com//api/method/building_material.building_ma { "item_id": "Bình giữ nhiệt Euroutile", "amount": 3000000, - "quantity" : 5.78 + "quantity" : 5.78, + "conversion_of_sm: 1.5 }, { "item_id": "Gạch ốp Signature SIG.P-8806", "amount": 4000000, - "quantity" : 33 + "quantity" : 33, + "conversion_of_sm: 1.5 } ] }' diff --git a/docs/products.sh b/docs/products.sh index c47bd9d..f6089f1 100644 --- a/docs/products.sh +++ b/docs/products.sh @@ -56,7 +56,7 @@ curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \ --data '{ "doctype": "Item Group", "fields": ["item_group_name","name"], - "filters": {"is_group": 0}, + "filters": {"is_group": 0, "custom_published" : 1}, "limit_page_length": 0 }' diff --git a/lib/core/network/dio_client.dart b/lib/core/network/dio_client.dart index cb528ff..fd50c2f 100644 --- a/lib/core/network/dio_client.dart +++ b/lib/core/network/dio_client.dart @@ -249,13 +249,13 @@ class CustomCurlLoggerInterceptor extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { final curl = _cURLRepresentation(options); - debugPrint( - '╔╣ CURL Request ╠══════════════════════════════════════════════════', - ); - debugPrint(curl); - debugPrint( - '╚═════════════════════════════════════════════════════════════════', - ); + // debugPrint( + // '╔╣ CURL Request ╠══════════════════════════════════════════════════', + // ); + // debugPrint(curl); + // debugPrint( + // '╚═════════════════════════════════════════════════════════════════', + // ); // Also log to dart:developer for better filtering in DevTools developer.log(curl, name: 'DIO_CURL', time: DateTime.now()); handler.next(options); @@ -468,7 +468,7 @@ Future dio(Ref ref) async { // Add interceptors in order // 1. Custom Curl interceptor (first to log cURL commands) // Uses debugPrint and developer.log for better visibility - // ..interceptors.add(CustomCurlLoggerInterceptor()) + ..interceptors.add(CustomCurlLoggerInterceptor()) // 2. Logging interceptor ..interceptors.add(ref.watch(loggingInterceptorProvider)) // 3. Auth interceptor (add tokens to requests) diff --git a/lib/core/network/dio_client.g.dart b/lib/core/network/dio_client.g.dart index 271c30e..31d3f64 100644 --- a/lib/core/network/dio_client.g.dart +++ b/lib/core/network/dio_client.g.dart @@ -131,7 +131,7 @@ final class DioProvider } } -String _$dioHash() => r'f15495e99d11744c245e2be892657748aeeb8ae7'; +String _$dioHash() => r'd15bfe824d6501e5cbd56ff152de978030d97be4'; /// Provider for DioClient diff --git a/lib/features/cart/data/datasources/cart_remote_datasource.dart b/lib/features/cart/data/datasources/cart_remote_datasource.dart index cc9e820..1e56323 100644 --- a/lib/features/cart/data/datasources/cart_remote_datasource.dart +++ b/lib/features/cart/data/datasources/cart_remote_datasource.dart @@ -190,15 +190,21 @@ class CartRemoteDataSourceImpl implements CartRemoteDataSource { try { // Map API response to CartItemModel // API fields: name, item, quantity, amount, item_code, item_name, image, conversion_of_sm + final quantity = (item['quantity'] as num?)?.toDouble() ?? 0.0; + final unitPrice = (item['amount'] as num?)?.toDouble() ?? 0.0; + final cartItem = CartItemModel( cartItemId: item['name'] as String? ?? '', cartId: 'user_cart', // Fixed cart ID for user's cart productId: item['item_code'] as String? ?? item['item'] as String? ?? '', - quantity: (item['quantity'] as num?)?.toDouble() ?? 0.0, - unitPrice: (item['amount'] as num?)?.toDouble() ?? 0.0, - subtotal: ((item['quantity'] as num?)?.toDouble() ?? 0.0) * - ((item['amount'] as num?)?.toDouble() ?? 0.0), + quantity: quantity, + unitPrice: unitPrice, + subtotal: quantity * unitPrice, addedAt: DateTime.now(), // API doesn't provide timestamp + // Product details from cart API - no need to fetch separately + itemName: item['item_name'] as String?, + image: item['image'] as String?, + conversionOfSm: (item['conversion_of_sm'] as num?)?.toDouble(), ); cartItems.add(cartItem); diff --git a/lib/features/cart/data/models/cart_item_model.dart b/lib/features/cart/data/models/cart_item_model.dart index 8bf6dc2..a08413e 100644 --- a/lib/features/cart/data/models/cart_item_model.dart +++ b/lib/features/cart/data/models/cart_item_model.dart @@ -1,9 +1,12 @@ import 'package:hive_ce/hive.dart'; import 'package:worker/core/constants/storage_constants.dart'; +import 'package:worker/features/cart/domain/entities/cart_item.dart'; part 'cart_item_model.g.dart'; /// Cart Item Model - Type ID: 5 +/// +/// Includes product details from cart API to avoid fetching each product. @HiveType(typeId: HiveTypeIds.cartItemModel) class CartItemModel extends HiveObject { CartItemModel({ @@ -14,6 +17,9 @@ class CartItemModel extends HiveObject { required this.unitPrice, required this.subtotal, required this.addedAt, + this.itemName, + this.image, + this.conversionOfSm, }); @HiveField(0) @@ -37,6 +43,18 @@ class CartItemModel extends HiveObject { @HiveField(6) final DateTime addedAt; + /// Product name from cart API + @HiveField(7) + final String? itemName; + + /// Product image URL from cart API + @HiveField(8) + final String? image; + + /// Conversion factor (m² to tiles) from cart API + @HiveField(9) + final double? conversionOfSm; + factory CartItemModel.fromJson(Map json) { return CartItemModel( cartItemId: json['cart_item_id'] as String, @@ -67,6 +85,9 @@ class CartItemModel extends HiveObject { double? unitPrice, double? subtotal, DateTime? addedAt, + String? itemName, + String? image, + double? conversionOfSm, }) => CartItemModel( cartItemId: cartItemId ?? this.cartItemId, cartId: cartId ?? this.cartId, @@ -75,5 +96,22 @@ class CartItemModel extends HiveObject { unitPrice: unitPrice ?? this.unitPrice, subtotal: subtotal ?? this.subtotal, addedAt: addedAt ?? this.addedAt, + itemName: itemName ?? this.itemName, + image: image ?? this.image, + conversionOfSm: conversionOfSm ?? this.conversionOfSm, + ); + + /// Convert to domain entity + CartItem toEntity() => CartItem( + cartItemId: cartItemId, + cartId: cartId, + productId: productId, + quantity: quantity, + unitPrice: unitPrice, + subtotal: subtotal, + addedAt: addedAt, + itemName: itemName, + image: image, + conversionOfSm: conversionOfSm, ); } diff --git a/lib/features/cart/data/models/cart_item_model.g.dart b/lib/features/cart/data/models/cart_item_model.g.dart index 3f67d17..979caf0 100644 --- a/lib/features/cart/data/models/cart_item_model.g.dart +++ b/lib/features/cart/data/models/cart_item_model.g.dart @@ -24,13 +24,16 @@ class CartItemModelAdapter extends TypeAdapter { unitPrice: (fields[4] as num).toDouble(), subtotal: (fields[5] as num).toDouble(), addedAt: fields[6] as DateTime, + itemName: fields[7] as String?, + image: fields[8] as String?, + conversionOfSm: (fields[9] as num?)?.toDouble(), ); } @override void write(BinaryWriter writer, CartItemModel obj) { writer - ..writeByte(7) + ..writeByte(10) ..writeByte(0) ..write(obj.cartItemId) ..writeByte(1) @@ -44,7 +47,13 @@ class CartItemModelAdapter extends TypeAdapter { ..writeByte(5) ..write(obj.subtotal) ..writeByte(6) - ..write(obj.addedAt); + ..write(obj.addedAt) + ..writeByte(7) + ..write(obj.itemName) + ..writeByte(8) + ..write(obj.image) + ..writeByte(9) + ..write(obj.conversionOfSm); } @override diff --git a/lib/features/cart/data/repositories/cart_repository_impl.dart b/lib/features/cart/data/repositories/cart_repository_impl.dart index 56b5d01..5c3c7cf 100644 --- a/lib/features/cart/data/repositories/cart_repository_impl.dart +++ b/lib/features/cart/data/repositories/cart_repository_impl.dart @@ -36,6 +36,7 @@ class CartRepositoryImpl implements CartRepository { required List itemIds, required List quantities, required List prices, + List? conversionFactors, }) async { try { // Validate input @@ -48,11 +49,16 @@ class CartRepositoryImpl implements CartRepository { // Build API request items final items = >[]; for (int i = 0; i < itemIds.length; i++) { - items.add({ + final item = { 'item_id': itemIds[i], 'quantity': quantities[i], 'amount': prices[i], - }); + }; + // Add conversion_of_sm if provided + if (conversionFactors != null && i < conversionFactors.length) { + item['conversion_of_sm'] = conversionFactors[i] ?? 0.0; + } + items.add(item); } // Try API first @@ -66,6 +72,7 @@ class CartRepositoryImpl implements CartRepository { productId: itemIds[i], quantity: quantities[i], unitPrice: prices[i], + conversionOfSm: conversionFactors?[i], ); await _localDataSource.addCartItem(cartItemModel); } @@ -80,6 +87,7 @@ class CartRepositoryImpl implements CartRepository { productId: itemIds[i], quantity: quantities[i], unitPrice: prices[i], + conversionOfSm: conversionFactors?[i], ); await _localDataSource.addCartItem(cartItemModel); } @@ -176,6 +184,7 @@ class CartRepositoryImpl implements CartRepository { required String itemId, required double quantity, required double price, + double? conversionFactor, }) async { try { // API doesn't have update endpoint, use add with new quantity @@ -184,6 +193,7 @@ class CartRepositoryImpl implements CartRepository { itemIds: [itemId], quantities: [quantity], prices: [price], + conversionFactors: conversionFactor != null ? [conversionFactor] : null, ); } catch (e) { throw UnknownException('Failed to update cart item quantity', e); @@ -268,6 +278,9 @@ class CartRepositoryImpl implements CartRepository { unitPrice: model.unitPrice, subtotal: model.subtotal, addedAt: model.addedAt, + itemName: model.itemName, + image: model.image, + conversionOfSm: model.conversionOfSm, ); } @@ -276,6 +289,7 @@ class CartRepositoryImpl implements CartRepository { required String productId, required double quantity, required double unitPrice, + double? conversionOfSm, }) { return CartItemModel( cartItemId: DateTime.now().millisecondsSinceEpoch.toString(), @@ -285,6 +299,7 @@ class CartRepositoryImpl implements CartRepository { unitPrice: unitPrice, subtotal: quantity * unitPrice, addedAt: DateTime.now(), + conversionOfSm: conversionOfSm, ); } } diff --git a/lib/features/cart/domain/entities/cart_item.dart b/lib/features/cart/domain/entities/cart_item.dart index 317483d..0eabf72 100644 --- a/lib/features/cart/domain/entities/cart_item.dart +++ b/lib/features/cart/domain/entities/cart_item.dart @@ -6,7 +6,7 @@ library; /// Cart Item Entity /// /// Contains item-level information: -/// - Product reference +/// - Product reference and basic info /// - Quantity /// - Pricing class CartItem { @@ -31,6 +31,15 @@ class CartItem { /// Timestamp when item was added final DateTime addedAt; + /// Product name from cart API + final String? itemName; + + /// Product image URL from cart API + final String? image; + + /// Conversion factor (m² to tiles) from cart API + final double? conversionOfSm; + const CartItem({ required this.cartItemId, required this.cartId, @@ -39,6 +48,9 @@ class CartItem { required this.unitPrice, required this.subtotal, required this.addedAt, + this.itemName, + this.image, + this.conversionOfSm, }); /// Calculate subtotal (for verification) @@ -53,6 +65,9 @@ class CartItem { double? unitPrice, double? subtotal, DateTime? addedAt, + String? itemName, + String? image, + double? conversionOfSm, }) { return CartItem( cartItemId: cartItemId ?? this.cartItemId, @@ -62,6 +77,9 @@ class CartItem { unitPrice: unitPrice ?? this.unitPrice, subtotal: subtotal ?? this.subtotal, addedAt: addedAt ?? this.addedAt, + itemName: itemName ?? this.itemName, + image: image ?? this.image, + conversionOfSm: conversionOfSm ?? this.conversionOfSm, ); } diff --git a/lib/features/cart/domain/repositories/cart_repository.dart b/lib/features/cart/domain/repositories/cart_repository.dart index 5b7d36c..d35d680 100644 --- a/lib/features/cart/domain/repositories/cart_repository.dart +++ b/lib/features/cart/domain/repositories/cart_repository.dart @@ -25,6 +25,7 @@ abstract class CartRepository { /// [itemIds] - Product ERPNext item codes /// [quantities] - Quantities for each item /// [prices] - Unit prices for each item + /// [conversionFactors] - Conversion factors (m² to tiles) for each item /// /// Returns true if successful. /// Throws exceptions on failure. @@ -32,6 +33,7 @@ abstract class CartRepository { required List itemIds, required List quantities, required List prices, + List? conversionFactors, }); /// Remove items from cart @@ -55,6 +57,7 @@ abstract class CartRepository { /// [itemId] - Product ERPNext item code /// [quantity] - New quantity /// [price] - Unit price + /// [conversionFactor] - Conversion factor (m² to tiles) /// /// Returns true if successful. /// Throws exceptions on failure. @@ -62,6 +65,7 @@ abstract class CartRepository { required String itemId, required double quantity, required double price, + double? conversionFactor, }); /// Clear all items from cart diff --git a/lib/features/cart/presentation/pages/cart_page.dart b/lib/features/cart/presentation/pages/cart_page.dart index 76cc05d..a6c1eb7 100644 --- a/lib/features/cart/presentation/pages/cart_page.dart +++ b/lib/features/cart/presentation/pages/cart_page.dart @@ -3,7 +3,7 @@ /// Shopping cart screen with selection and checkout. /// Features expanded item list with total price at bottom. library; - +import 'package:worker/core/utils/extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -35,14 +35,8 @@ class CartPage extends ConsumerStatefulWidget { class _CartPageState extends ConsumerState { bool _isSyncing = false; - @override - void initState() { - super.initState(); - // Initialize cart from API on mount - WidgetsBinding.instance.addPostFrameCallback((_) { - ref.read(cartProvider.notifier).initialize(); - }); - } + // Cart is initialized once in home_page.dart at app startup + // Provider has keepAlive: true, so no need to reload here // Note: Sync is handled in PopScope.onPopInvokedWithResult for back navigation // and in checkout button handler for checkout flow. @@ -53,11 +47,7 @@ class _CartPageState extends ConsumerState { final colorScheme = Theme.of(context).colorScheme; final cartState = ref.watch(cartProvider); - final currencyFormatter = NumberFormat.currency( - locale: 'vi_VN', - symbol: 'đ', - decimalDigits: 0, - ); + final itemCount = cartState.itemCount; final hasSelection = cartState.selectedCount > 0; @@ -144,7 +134,11 @@ class _CartPageState extends ConsumerState { context, cartState, ref, - currencyFormatter, + NumberFormat.currency( + locale: 'vi_VN', + symbol: 'đ', + decimalDigits: 0, + ), hasSelection, ), ], diff --git a/lib/features/cart/presentation/providers/cart_provider.dart b/lib/features/cart/presentation/providers/cart_provider.dart index 1c6159b..55b0770 100644 --- a/lib/features/cart/presentation/providers/cart_provider.dart +++ b/lib/features/cart/presentation/providers/cart_provider.dart @@ -9,7 +9,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:worker/features/cart/presentation/providers/cart_data_providers.dart'; import 'package:worker/features/cart/presentation/providers/cart_state.dart'; import 'package:worker/features/products/domain/entities/product.dart'; -import 'package:worker/features/products/presentation/providers/products_provider.dart'; part 'cart_provider.g.dart'; @@ -46,8 +45,12 @@ class Cart extends _$Cart { /// Initialize cart by loading from API /// - /// Call this from UI on mount to load cart items from backend. + /// Call this ONCE from HomePage on app startup. + /// Cart API returns product details, no need to fetch each product separately. Future initialize() async { + // Skip if already loaded + if (state.items.isNotEmpty) return; + final repository = await ref.read(cartRepositoryProvider.future); // Set loading state @@ -55,6 +58,7 @@ class Cart extends _$Cart { try { // Load cart items from API (with Hive fallback) + // Cart API returns: item_code, item_name, image, conversion_of_sm, quantity, amount final cartItems = await repository.getCartItems(); // Get member tier from user profile @@ -63,41 +67,47 @@ class Cart extends _$Cart { const memberDiscountPercent = 15.0; // Convert CartItem entities to CartItemData for UI + // Use product data from cart API directly - no need to fetch each product final items = []; final selectedItems = {}; - // Fetch product details for each cart item - final productsRepository = await ref.read(productsRepositoryProvider.future); - for (final cartItem in cartItems) { - try { - // Fetch full product entity from products repository - final product = await productsRepository.getProductById(cartItem.productId); + // Create minimal Product from cart item data (no need to fetch from API) + final now = DateTime.now(); + final product = Product( + productId: cartItem.productId, + name: cartItem.itemName ?? cartItem.productId, + basePrice: cartItem.unitPrice, + images: cartItem.image != null ? [cartItem.image!] : [], + thumbnail: cartItem.image ?? '', + imageCaptions: const {}, + specifications: const {}, + conversionOfSm: cartItem.conversionOfSm, + erpnextItemCode: cartItem.productId, + isActive: true, + isFeatured: false, + createdAt: now, + updatedAt: now, + ); - // Calculate conversion for this item - final converted = _calculateConversion( - cartItem.quantity, - product.conversionOfSm, - ); + // Calculate conversion for this item + final converted = _calculateConversion( + cartItem.quantity, + product.conversionOfSm, + ); - // Create CartItemData with full product info - items.add( - CartItemData( - product: product, - quantity: cartItem.quantity, - quantityConverted: converted.convertedQuantity, - boxes: converted.boxes, - ), - ); + // Create CartItemData with product info from cart API + items.add( + CartItemData( + product: product, + quantity: cartItem.quantity, + quantityConverted: converted.convertedQuantity, + boxes: converted.boxes, + ), + ); - // Initialize as not selected by default - selectedItems[product.productId] = false; - } catch (productError) { - // Skip this item if product can't be fetched - // In production, use a proper logging framework - // ignore: avoid_print - print('[CartProvider] Failed to load product ${cartItem.productId}: $productError'); - } + // Initialize as not selected by default + selectedItems[product.productId] = false; } final newState = CartState( @@ -150,6 +160,7 @@ class Cart extends _$Cart { itemIds: [product.erpnextItemCode ?? product.productId], quantities: [quantity], prices: [product.basePrice], + conversionFactors: [product.conversionOfSm], ); // Calculate conversion @@ -332,6 +343,7 @@ class Cart extends _$Cart { itemId: item.product.erpnextItemCode ?? productId, quantity: quantity, price: item.product.basePrice, + conversionFactor: item.product.conversionOfSm, ); } catch (e) { // Silent fail - keep local state, user can retry later @@ -370,6 +382,7 @@ class Cart extends _$Cart { itemId: item.product.erpnextItemCode ?? productId, quantity: newQuantity, price: item.product.basePrice, + conversionFactor: item.product.conversionOfSm, ); // Update local state diff --git a/lib/features/cart/presentation/providers/cart_provider.g.dart b/lib/features/cart/presentation/providers/cart_provider.g.dart index 8fc4a39..2cc9144 100644 --- a/lib/features/cart/presentation/providers/cart_provider.g.dart +++ b/lib/features/cart/presentation/providers/cart_provider.g.dart @@ -64,7 +64,7 @@ final class CartProvider extends $NotifierProvider { } } -String _$cartHash() => r'706de28734e7059b2e9484f3b1d94226a0e90bb9'; +String _$cartHash() => r'a12712db833127ef66b75f52cba8376409806afa'; /// Cart Notifier /// diff --git a/lib/features/cart/presentation/widgets/cart_item_widget.dart b/lib/features/cart/presentation/widgets/cart_item_widget.dart index a4c96f7..e82ee19 100644 --- a/lib/features/cart/presentation/widgets/cart_item_widget.dart +++ b/lib/features/cart/presentation/widgets/cart_item_widget.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart'; +import 'package:worker/core/utils/extensions.dart'; import 'package:worker/core/widgets/loading_indicator.dart'; import 'package:worker/core/theme/typography.dart'; import 'package:worker/features/cart/presentation/providers/cart_provider.dart'; @@ -79,11 +80,6 @@ class _CartItemWidgetState extends ConsumerState { final isSelected = cartState.selectedItems[widget.item.product.productId] ?? false; - final currencyFormatter = NumberFormat.currency( - locale: 'vi_VN', - symbol: 'đ', - decimalDigits: 0, - ); return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), @@ -121,7 +117,11 @@ class _CartItemWidgetState extends ConsumerState { ClipRRect( borderRadius: BorderRadius.circular(8), child: CachedNetworkImage( - imageUrl: widget.item.product.thumbnail, + imageUrl: widget.item.product.thumbnail.isNotEmpty + ? widget.item.product.thumbnail + : (widget.item.product.images.isNotEmpty + ? widget.item.product.images.first + : ''), width: 100, height: 100, fit: BoxFit.cover, @@ -168,7 +168,7 @@ class _CartItemWidgetState extends ConsumerState { // Price Text( - '${currencyFormatter.format(widget.item.product.basePrice)}/m²', + '${widget.item.product.basePrice.toVNCurrency}/m²', style: AppTypography.titleMedium.copyWith( color: colorScheme.primary, fontWeight: FontWeight.bold, diff --git a/lib/features/favorites/data/datasources/favorite_products_local_datasource.dart b/lib/features/favorites/data/datasources/favorite_products_local_datasource.dart index f3f1a88..5c3e749 100644 --- a/lib/features/favorites/data/datasources/favorite_products_local_datasource.dart +++ b/lib/features/favorites/data/datasources/favorite_products_local_datasource.dart @@ -60,6 +60,48 @@ class FavoriteProductsLocalDataSource { bool isBoxOpen() { return Hive.isBoxOpen(HiveBoxNames.favoriteProductsBox); } + + /// Check if a product is in favorites (local only - no API call) + bool isFavorite(String productId) { + try { + return _box.containsKey(productId); + } catch (e) { + _debugPrint('Error checking favorite: $e'); + return false; + } + } + + /// Get all favorite product IDs (local only - no API call) + Set getFavoriteIds() { + try { + return _box.keys.cast().toSet(); + } catch (e) { + _debugPrint('Error getting favorite IDs: $e'); + return {}; + } + } + + /// Add a product to local favorites cache + Future addFavorite(ProductModel product) async { + try { + await _box.put(product.productId, product); + _debugPrint('Added to local favorites: ${product.productId}'); + } catch (e) { + _debugPrint('Error adding to local favorites: $e'); + rethrow; + } + } + + /// Remove a product from local favorites cache + Future removeFavorite(String productId) async { + try { + await _box.delete(productId); + _debugPrint('Removed from local favorites: $productId'); + } catch (e) { + _debugPrint('Error removing from local favorites: $e'); + rethrow; + } + } } /// Debug print helper diff --git a/lib/features/favorites/presentation/providers/favorites_provider.dart b/lib/features/favorites/presentation/providers/favorites_provider.dart index 31181d1..1fd33cd 100644 --- a/lib/features/favorites/presentation/providers/favorites_provider.dart +++ b/lib/features/favorites/presentation/providers/favorites_provider.dart @@ -71,7 +71,12 @@ class FavoriteProducts extends _$FavoriteProducts { @override Future> build() async { _repository = await ref.read(favoritesRepositoryProvider.future); - return await _loadProducts(); + final products = await _loadProducts(); + + // Sync local IDs after loading + ref.read(favoriteIdsLocalProvider.notifier).refresh(); + + return products; } // ========================================================================== @@ -99,20 +104,22 @@ class FavoriteProducts extends _$FavoriteProducts { /// Add a product to favorites /// - /// Calls API to add to wishlist, then refreshes the products list. + /// Calls API to add to wishlist, updates local state only (no refetch). /// No userId needed - the API uses the authenticated session. Future addFavorite(String productId) async { try { _debugPrint('Adding product to favorites: $productId'); + // Optimistically update local state first for instant UI feedback + ref.read(favoriteIdsLocalProvider.notifier).addId(productId); + // Call repository to add to favorites (uses auth token from session) await _repository.addFavorite(productId); - // Refresh the products list after successful addition - await refresh(); - _debugPrint('Successfully added favorite: $productId'); } catch (e) { + // Rollback optimistic update on error + ref.read(favoriteIdsLocalProvider.notifier).removeId(productId); _debugPrint('Error adding favorite: $e'); rethrow; } @@ -120,20 +127,22 @@ class FavoriteProducts extends _$FavoriteProducts { /// Remove a product from favorites /// - /// Calls API to remove from wishlist, then refreshes the products list. + /// Calls API to remove from wishlist, updates local state only (no refetch). /// No userId needed - the API uses the authenticated session. Future removeFavorite(String productId) async { try { _debugPrint('Removing product from favorites: $productId'); + // Optimistically update local state first for instant UI feedback + ref.read(favoriteIdsLocalProvider.notifier).removeId(productId); + // Call repository to remove from favorites (uses auth token from session) await _repository.removeFavorite(productId); - // Refresh the products list after successful removal - await refresh(); - _debugPrint('Successfully removed favorite: $productId'); } catch (e) { + // Rollback optimistic update on error + ref.read(favoriteIdsLocalProvider.notifier).addId(productId); _debugPrint('Error removing favorite: $e'); rethrow; } @@ -143,9 +152,11 @@ class FavoriteProducts extends _$FavoriteProducts { /// /// If the product is favorited, it will be removed. /// If the product is not favorited, it will be added. + /// Checks from local state for instant response. Future toggleFavorite(String productId) async { - final currentProducts = state.value ?? []; - final isFavorited = currentProducts.any((p) => p.productId == productId); + // Check from local IDs (instant, no API call) + final localIds = ref.read(favoriteIdsLocalProvider); + final isFavorited = localIds.contains(productId); if (isFavorited) { await removeFavorite(productId); @@ -170,20 +181,48 @@ class FavoriteProducts extends _$FavoriteProducts { // HELPER PROVIDERS // ============================================================================ -/// Check if a specific product is favorited +/// Check if a specific product is favorited (LOCAL ONLY - no API call) /// -/// Derived from the favorite products list. -/// Returns true if the product is in the user's favorites, false otherwise. -/// Safe to use in build methods - will return false during loading/error states. +/// Reads directly from Hive local cache for instant response. +/// This is used in product detail page to avoid unnecessary API calls. +/// The cache is synced when favorites are loaded or modified. @riverpod bool isFavorite(Ref ref, String productId) { - final favoriteProductsAsync = ref.watch(favoriteProductsProvider); + // Watch the notifier state to trigger rebuild when favorites change + // But check from local Hive directly for instant response + ref.watch(favoriteIdsLocalProvider); - return favoriteProductsAsync.when( - data: (products) => products.any((p) => p.productId == productId), - loading: () => false, - error: (_, __) => false, - ); + final localDataSource = ref.read(favoriteProductsLocalDataSourceProvider); + return localDataSource.isFavorite(productId); +} + +/// Local favorite IDs provider (synced with Hive) +/// +/// This provider watches Hive changes and provides a Set of favorite product IDs. +/// Used to trigger rebuilds when favorites are added/removed. +@Riverpod(keepAlive: true) +class FavoriteIdsLocal extends _$FavoriteIdsLocal { + @override + Set build() { + final localDataSource = ref.read(favoriteProductsLocalDataSourceProvider); + return localDataSource.getFavoriteIds(); + } + + /// Refresh from local storage + void refresh() { + final localDataSource = ref.read(favoriteProductsLocalDataSourceProvider); + state = localDataSource.getFavoriteIds(); + } + + /// Add a product ID to local state + void addId(String productId) { + state = {...state, productId}; + } + + /// Remove a product ID from local state + void removeId(String productId) { + state = {...state}..remove(productId); + } } /// Get the total count of favorites diff --git a/lib/features/favorites/presentation/providers/favorites_provider.g.dart b/lib/features/favorites/presentation/providers/favorites_provider.g.dart index fff7376..113f1bd 100644 --- a/lib/features/favorites/presentation/providers/favorites_provider.g.dart +++ b/lib/features/favorites/presentation/providers/favorites_provider.g.dart @@ -231,7 +231,7 @@ final class FavoriteProductsProvider FavoriteProducts create() => FavoriteProducts(); } -String _$favoriteProductsHash() => r'd43c41db210259021df104f9fecdd00cf474d196'; +String _$favoriteProductsHash() => r'6d042f469a1f71bb06f8b5b76014bf24e30e6758'; /// Manages favorite products with full Product data from wishlist API /// @@ -269,28 +269,28 @@ abstract class _$FavoriteProducts extends $AsyncNotifier> { } } -/// Check if a specific product is favorited +/// Check if a specific product is favorited (LOCAL ONLY - no API call) /// -/// Derived from the favorite products list. -/// Returns true if the product is in the user's favorites, false otherwise. -/// Safe to use in build methods - will return false during loading/error states. +/// Reads directly from Hive local cache for instant response. +/// This is used in product detail page to avoid unnecessary API calls. +/// The cache is synced when favorites are loaded or modified. @ProviderFor(isFavorite) const isFavoriteProvider = IsFavoriteFamily._(); -/// Check if a specific product is favorited +/// Check if a specific product is favorited (LOCAL ONLY - no API call) /// -/// Derived from the favorite products list. -/// Returns true if the product is in the user's favorites, false otherwise. -/// Safe to use in build methods - will return false during loading/error states. +/// Reads directly from Hive local cache for instant response. +/// This is used in product detail page to avoid unnecessary API calls. +/// The cache is synced when favorites are loaded or modified. final class IsFavoriteProvider extends $FunctionalProvider with $Provider { - /// Check if a specific product is favorited + /// Check if a specific product is favorited (LOCAL ONLY - no API call) /// - /// Derived from the favorite products list. - /// Returns true if the product is in the user's favorites, false otherwise. - /// Safe to use in build methods - will return false during loading/error states. + /// Reads directly from Hive local cache for instant response. + /// This is used in product detail page to avoid unnecessary API calls. + /// The cache is synced when favorites are loaded or modified. const IsFavoriteProvider._({ required IsFavoriteFamily super.from, required String super.argument, @@ -342,13 +342,13 @@ final class IsFavoriteProvider extends $FunctionalProvider } } -String _$isFavoriteHash() => r'6e2f5a50d2350975e17d91f395595cd284b69c20'; +String _$isFavoriteHash() => r'7aa2377f37ceb2c450c9e29b5c134ba160e4ecc2'; -/// Check if a specific product is favorited +/// Check if a specific product is favorited (LOCAL ONLY - no API call) /// -/// Derived from the favorite products list. -/// Returns true if the product is in the user's favorites, false otherwise. -/// Safe to use in build methods - will return false during loading/error states. +/// Reads directly from Hive local cache for instant response. +/// This is used in product detail page to avoid unnecessary API calls. +/// The cache is synced when favorites are loaded or modified. final class IsFavoriteFamily extends $Family with $FunctionalFamilyOverride { @@ -361,11 +361,11 @@ final class IsFavoriteFamily extends $Family isAutoDispose: true, ); - /// Check if a specific product is favorited + /// Check if a specific product is favorited (LOCAL ONLY - no API call) /// - /// Derived from the favorite products list. - /// Returns true if the product is in the user's favorites, false otherwise. - /// Safe to use in build methods - will return false during loading/error states. + /// Reads directly from Hive local cache for instant response. + /// This is used in product detail page to avoid unnecessary API calls. + /// The cache is synced when favorites are loaded or modified. IsFavoriteProvider call(String productId) => IsFavoriteProvider._(argument: productId, from: this); @@ -374,6 +374,77 @@ final class IsFavoriteFamily extends $Family String toString() => r'isFavoriteProvider'; } +/// Local favorite IDs provider (synced with Hive) +/// +/// This provider watches Hive changes and provides a Set of favorite product IDs. +/// Used to trigger rebuilds when favorites are added/removed. + +@ProviderFor(FavoriteIdsLocal) +const favoriteIdsLocalProvider = FavoriteIdsLocalProvider._(); + +/// Local favorite IDs provider (synced with Hive) +/// +/// This provider watches Hive changes and provides a Set of favorite product IDs. +/// Used to trigger rebuilds when favorites are added/removed. +final class FavoriteIdsLocalProvider + extends $NotifierProvider> { + /// Local favorite IDs provider (synced with Hive) + /// + /// This provider watches Hive changes and provides a Set of favorite product IDs. + /// Used to trigger rebuilds when favorites are added/removed. + const FavoriteIdsLocalProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'favoriteIdsLocalProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$favoriteIdsLocalHash(); + + @$internal + @override + FavoriteIdsLocal create() => FavoriteIdsLocal(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(Set value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider>(value), + ); + } +} + +String _$favoriteIdsLocalHash() => r'db248bc6dcd8ba39d8c3e410188cac67ebf96140'; + +/// Local favorite IDs provider (synced with Hive) +/// +/// This provider watches Hive changes and provides a Set of favorite product IDs. +/// Used to trigger rebuilds when favorites are added/removed. + +abstract class _$FavoriteIdsLocal extends $Notifier> { + Set build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref, Set>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, Set>, + Set, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + /// Get the total count of favorites /// /// Derived from the favorite products list. diff --git a/lib/features/products/data/datasources/products_remote_datasource.dart b/lib/features/products/data/datasources/products_remote_datasource.dart index 8515bfd..4a06121 100644 --- a/lib/features/products/data/datasources/products_remote_datasource.dart +++ b/lib/features/products/data/datasources/products_remote_datasource.dart @@ -278,7 +278,7 @@ class ProductsRemoteDataSource { data: { 'doctype': 'Item Group', 'fields': ['item_group_name', 'name'], - 'filters': {'is_group': 0}, + 'filters': {'is_group': 0, 'custom_published': 1}, 'limit_page_length': 0, }, options: Options(headers: headers), diff --git a/lib/features/products/presentation/widgets/brand_filter_chips.dart b/lib/features/products/presentation/widgets/brand_filter_chips.dart index 929e2cb..02c3925 100644 --- a/lib/features/products/presentation/widgets/brand_filter_chips.dart +++ b/lib/features/products/presentation/widgets/brand_filter_chips.dart @@ -57,7 +57,7 @@ class BrandFilterChips extends ConsumerWidget { style: TextStyle( fontSize: 14.0, fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, - color: colorScheme.onSurface, + color: isSelected ? Colors.white : colorScheme.onSurface, ), ), selected: isSelected,