update cart/favorite
This commit is contained in:
@@ -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> 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)
|
||||
|
||||
@@ -131,7 +131,7 @@ final class DioProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$dioHash() => r'f15495e99d11744c245e2be892657748aeeb8ae7';
|
||||
String _$dioHash() => r'd15bfe824d6501e5cbd56ff152de978030d97be4';
|
||||
|
||||
/// Provider for DioClient
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<String, dynamic> 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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,13 +24,16 @@ class CartItemModelAdapter extends TypeAdapter<CartItemModel> {
|
||||
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<CartItemModel> {
|
||||
..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
|
||||
|
||||
@@ -36,6 +36,7 @@ class CartRepositoryImpl implements CartRepository {
|
||||
required List<String> itemIds,
|
||||
required List<double> quantities,
|
||||
required List<double> prices,
|
||||
List<double?>? conversionFactors,
|
||||
}) async {
|
||||
try {
|
||||
// Validate input
|
||||
@@ -48,11 +49,16 @@ class CartRepositoryImpl implements CartRepository {
|
||||
// Build API request items
|
||||
final items = <Map<String, dynamic>>[];
|
||||
for (int i = 0; i < itemIds.length; i++) {
|
||||
items.add({
|
||||
final item = <String, dynamic>{
|
||||
'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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String> itemIds,
|
||||
required List<double> quantities,
|
||||
required List<double> prices,
|
||||
List<double?>? 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
|
||||
|
||||
@@ -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<CartPage> {
|
||||
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<CartPage> {
|
||||
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<CartPage> {
|
||||
context,
|
||||
cartState,
|
||||
ref,
|
||||
currencyFormatter,
|
||||
NumberFormat.currency(
|
||||
locale: 'vi_VN',
|
||||
symbol: 'đ',
|
||||
decimalDigits: 0,
|
||||
),
|
||||
hasSelection,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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<void> 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 = <CartItemData>[];
|
||||
final selectedItems = <String, bool>{};
|
||||
|
||||
// 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
|
||||
|
||||
@@ -64,7 +64,7 @@ final class CartProvider extends $NotifierProvider<Cart, CartState> {
|
||||
}
|
||||
}
|
||||
|
||||
String _$cartHash() => r'706de28734e7059b2e9484f3b1d94226a0e90bb9';
|
||||
String _$cartHash() => r'a12712db833127ef66b75f52cba8376409806afa';
|
||||
|
||||
/// Cart Notifier
|
||||
///
|
||||
|
||||
@@ -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<CartItemWidget> {
|
||||
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<CartItemWidget> {
|
||||
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<CartItemWidget> {
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -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<String> getFavoriteIds() {
|
||||
try {
|
||||
return _box.keys.cast<String>().toSet();
|
||||
} catch (e) {
|
||||
_debugPrint('Error getting favorite IDs: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a product to local favorites cache
|
||||
Future<void> 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<void> 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
|
||||
|
||||
@@ -71,7 +71,12 @@ class FavoriteProducts extends _$FavoriteProducts {
|
||||
@override
|
||||
Future<List<Product>> 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<void> 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<void> 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<void> 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<String> 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
|
||||
|
||||
@@ -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<List<Product>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<bool, bool, bool>
|
||||
with $Provider<bool> {
|
||||
/// 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<bool, bool, bool>
|
||||
}
|
||||
}
|
||||
|
||||
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<bool, String> {
|
||||
@@ -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<FavoriteIdsLocal, Set<String>> {
|
||||
/// 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<String> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<Set<String>>(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<String>> {
|
||||
Set<String> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<Set<String>, Set<String>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<Set<String>, Set<String>>,
|
||||
Set<String>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the total count of favorites
|
||||
///
|
||||
/// Derived from the favorite products list.
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user