Compare commits

..

3 Commits

Author SHA1 Message Date
Phuoc Nguyen
2dadcc5ce1 update 2025-12-03 16:10:39 +07:00
Phuoc Nguyen
27798cc234 update cart/favorite 2025-12-03 15:53:46 +07:00
Phuoc Nguyen
e1c9f818d2 update filter products 2025-12-03 14:33:08 +07:00
31 changed files with 755 additions and 186 deletions

View File

@@ -1,4 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA"/>
<application <application
android:label="worker" android:label="worker"
android:name="${applicationName}" android:name="${applicationName}"

View File

@@ -8,12 +8,14 @@ curl --location 'https://land.dbiz.com//api/method/building_material.building_ma
{ {
"item_id": "Bình giữ nhiệt Euroutile", "item_id": "Bình giữ nhiệt Euroutile",
"amount": 3000000, "amount": 3000000,
"quantity" : 5.78 "quantity" : 5.78,
"conversion_of_sm: 1.5
}, },
{ {
"item_id": "Gạch ốp Signature SIG.P-8806", "item_id": "Gạch ốp Signature SIG.P-8806",
"amount": 4000000, "amount": 4000000,
"quantity" : 33 "quantity" : 33,
"conversion_of_sm: 1.5
} }
] ]
}' }'

View File

@@ -56,7 +56,7 @@ curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \
--data '{ --data '{
"doctype": "Item Group", "doctype": "Item Group",
"fields": ["item_group_name","name"], "fields": ["item_group_name","name"],
"filters": {"is_group": 0}, "filters": {"is_group": 0, "custom_published" : 1},
"limit_page_length": 0 "limit_page_length": 0
}' }'

View File

@@ -34,6 +34,8 @@
<string>Ứng dụng cần quyền truy cập microphone để ghi âm video nếu cần</string> <string>Ứng dụng cần quyền truy cập microphone để ghi âm video nếu cần</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>Ứng dụng cần quyền truy cập thư viện ảnh để chọn ảnh giấy tờ xác thực và mã QR từ thiết bị của bạn</string> <string>Ứng dụng cần quyền truy cập thư viện ảnh để chọn ảnh giấy tờ xác thực và mã QR từ thiết bị của bạn</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Ứng dụng sử dụng vị trí để cải thiện trải nghiệm và đề xuất showroom gần bạn</string>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>

View File

@@ -249,13 +249,13 @@ class CustomCurlLoggerInterceptor extends Interceptor {
@override @override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) { void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final curl = _cURLRepresentation(options); final curl = _cURLRepresentation(options);
debugPrint( // debugPrint(
'╔╣ CURL Request ╠══════════════════════════════════════════════════', // '╔╣ CURL Request ╠══════════════════════════════════════════════════',
); // );
debugPrint(curl); // debugPrint(curl);
debugPrint( // debugPrint(
'╚═════════════════════════════════════════════════════════════════', // '╚═════════════════════════════════════════════════════════════════',
); // );
// Also log to dart:developer for better filtering in DevTools // Also log to dart:developer for better filtering in DevTools
developer.log(curl, name: 'DIO_CURL', time: DateTime.now()); developer.log(curl, name: 'DIO_CURL', time: DateTime.now());
handler.next(options); handler.next(options);
@@ -468,7 +468,7 @@ Future<Dio> dio(Ref ref) async {
// Add interceptors in order // Add interceptors in order
// 1. Custom Curl interceptor (first to log cURL commands) // 1. Custom Curl interceptor (first to log cURL commands)
// Uses debugPrint and developer.log for better visibility // Uses debugPrint and developer.log for better visibility
// ..interceptors.add(CustomCurlLoggerInterceptor()) ..interceptors.add(CustomCurlLoggerInterceptor())
// 2. Logging interceptor // 2. Logging interceptor
..interceptors.add(ref.watch(loggingInterceptorProvider)) ..interceptors.add(ref.watch(loggingInterceptorProvider))
// 3. Auth interceptor (add tokens to requests) // 3. Auth interceptor (add tokens to requests)

View File

@@ -131,7 +131,7 @@ final class DioProvider
} }
} }
String _$dioHash() => r'f15495e99d11744c245e2be892657748aeeb8ae7'; String _$dioHash() => r'd15bfe824d6501e5cbd56ff152de978030d97be4';
/// Provider for DioClient /// Provider for DioClient

View File

@@ -65,7 +65,7 @@ String _$connectivityHash() => r'6d67af0ea4110f6ee0246dd332f90f8901380eda';
/// final connectivityState = ref.watch(connectivityStreamProvider); /// final connectivityState = ref.watch(connectivityStreamProvider);
/// connectivityState.when( /// connectivityState.when(
/// data: (status) => Text('Status: $status'), /// data: (status) => Text('Status: $status'),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, _) => Text('Error: $error'), /// error: (error, _) => Text('Error: $error'),
/// ); /// );
/// ``` /// ```
@@ -81,7 +81,7 @@ const connectivityStreamProvider = ConnectivityStreamProvider._();
/// final connectivityState = ref.watch(connectivityStreamProvider); /// final connectivityState = ref.watch(connectivityStreamProvider);
/// connectivityState.when( /// connectivityState.when(
/// data: (status) => Text('Status: $status'), /// data: (status) => Text('Status: $status'),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, _) => Text('Error: $error'), /// error: (error, _) => Text('Error: $error'),
/// ); /// );
/// ``` /// ```
@@ -104,7 +104,7 @@ final class ConnectivityStreamProvider
/// final connectivityState = ref.watch(connectivityStreamProvider); /// final connectivityState = ref.watch(connectivityStreamProvider);
/// connectivityState.when( /// connectivityState.when(
/// data: (status) => Text('Status: $status'), /// data: (status) => Text('Status: $status'),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, _) => Text('Error: $error'), /// error: (error, _) => Text('Error: $error'),
/// ); /// );
/// ``` /// ```
@@ -219,7 +219,7 @@ String _$currentConnectivityHash() =>
/// final isOnlineAsync = ref.watch(isOnlineProvider); /// final isOnlineAsync = ref.watch(isOnlineProvider);
/// isOnlineAsync.when( /// isOnlineAsync.when(
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'), /// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, _) => Text('Error: $error'), /// error: (error, _) => Text('Error: $error'),
/// ); /// );
/// ``` /// ```
@@ -235,7 +235,7 @@ const isOnlineProvider = IsOnlineProvider._();
/// final isOnlineAsync = ref.watch(isOnlineProvider); /// final isOnlineAsync = ref.watch(isOnlineProvider);
/// isOnlineAsync.when( /// isOnlineAsync.when(
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'), /// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, _) => Text('Error: $error'), /// error: (error, _) => Text('Error: $error'),
/// ); /// );
/// ``` /// ```
@@ -251,7 +251,7 @@ final class IsOnlineProvider
/// final isOnlineAsync = ref.watch(isOnlineProvider); /// final isOnlineAsync = ref.watch(isOnlineProvider);
/// isOnlineAsync.when( /// isOnlineAsync.when(
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'), /// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, _) => Text('Error: $error'), /// error: (error, _) => Text('Error: $error'),
/// ); /// );
/// ``` /// ```

View File

@@ -52,13 +52,39 @@ class AnalyticsService {
} }
} }
// ============================================================================
// E-commerce Events
// ============================================================================
/// Log view item event - when user views product detail
static Future<void> logViewItem({
required String productId,
required String productName,
required double price,
String? brand,
String? category,
}) async {
try {
await _analytics.logViewItem(
currency: 'VND',
value: price,
items: [
AnalyticsEventItem(
itemId: productId,
itemName: productName,
price: price,
itemBrand: brand,
itemCategory: category,
),
],
);
debugPrint('📊 Analytics: view_item - $productName');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
/// Log add to cart event /// Log add to cart event
///
/// [productId] - Product SKU or ID
/// [productName] - Product display name
/// [price] - Unit price in VND
/// [quantity] - Quantity added
/// [category] - Optional product category
static Future<void> logAddToCart({ static Future<void> logAddToCart({
required String productId, required String productId,
required String productName, required String productName,
@@ -85,4 +111,252 @@ class AnalyticsService {
debugPrint('📊 Analytics error: $e'); debugPrint('📊 Analytics error: $e');
} }
} }
/// Log remove from cart event
static Future<void> logRemoveFromCart({
required String productId,
required String productName,
required double price,
required int quantity,
}) async {
try {
await _analytics.logRemoveFromCart(
currency: 'VND',
value: price * quantity,
items: [
AnalyticsEventItem(
itemId: productId,
itemName: productName,
price: price,
quantity: quantity,
),
],
);
debugPrint('📊 Analytics: remove_from_cart - $productName');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
/// Log view cart event
static Future<void> logViewCart({
required double cartValue,
required List<AnalyticsEventItem> items,
}) async {
try {
await _analytics.logViewCart(
currency: 'VND',
value: cartValue,
items: items,
);
debugPrint('📊 Analytics: view_cart - ${items.length} items');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
/// Log begin checkout event
static Future<void> logBeginCheckout({
required double value,
required List<AnalyticsEventItem> items,
String? coupon,
}) async {
try {
await _analytics.logBeginCheckout(
currency: 'VND',
value: value,
items: items,
coupon: coupon,
);
debugPrint('📊 Analytics: begin_checkout - $value VND');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
/// Log purchase event - when order is completed
static Future<void> logPurchase({
required String orderId,
required double value,
required List<AnalyticsEventItem> items,
double? shipping,
double? tax,
String? coupon,
}) async {
try {
await _analytics.logPurchase(
currency: 'VND',
transactionId: orderId,
value: value,
items: items,
shipping: shipping,
tax: tax,
coupon: coupon,
);
debugPrint('📊 Analytics: purchase - Order $orderId');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
// ============================================================================
// Search & Discovery Events
// ============================================================================
/// Log search event
static Future<void> logSearch({
required String searchTerm,
}) async {
try {
await _analytics.logSearch(searchTerm: searchTerm);
debugPrint('📊 Analytics: search - $searchTerm');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
/// Log select item event - when user taps on a product in list
static Future<void> logSelectItem({
required String productId,
required String productName,
String? listName,
}) async {
try {
await _analytics.logSelectItem(
itemListName: listName,
items: [
AnalyticsEventItem(
itemId: productId,
itemName: productName,
),
],
);
debugPrint('📊 Analytics: select_item - $productName');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
// ============================================================================
// Loyalty & Rewards Events
// ============================================================================
/// Log earn points event
static Future<void> logEarnPoints({
required int points,
required String source,
}) async {
try {
await _analytics.logEarnVirtualCurrency(
virtualCurrencyName: 'loyalty_points',
value: points.toDouble(),
);
debugPrint('📊 Analytics: earn_points - $points from $source');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
/// Log spend points event - when user redeems points
static Future<void> logSpendPoints({
required int points,
required String itemName,
}) async {
try {
await _analytics.logSpendVirtualCurrency(
virtualCurrencyName: 'loyalty_points',
value: points.toDouble(),
itemName: itemName,
);
debugPrint('📊 Analytics: spend_points - $points for $itemName');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
// ============================================================================
// User Events
// ============================================================================
/// Log login event
static Future<void> logLogin({
String? method,
}) async {
try {
await _analytics.logLogin(loginMethod: method ?? 'phone');
debugPrint('📊 Analytics: login - $method');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
/// Log sign up event
static Future<void> logSignUp({
String? method,
}) async {
try {
await _analytics.logSignUp(signUpMethod: method ?? 'phone');
debugPrint('📊 Analytics: sign_up - $method');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
/// Log share event
static Future<void> logShare({
required String contentType,
required String itemId,
String? method,
}) async {
try {
await _analytics.logShare(
contentType: contentType,
itemId: itemId,
method: method ?? 'unknown',
);
debugPrint('📊 Analytics: share - $contentType $itemId');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
// ============================================================================
// Custom Events
// ============================================================================
/// Log custom event
static Future<void> logEvent({
required String name,
Map<String, Object>? parameters,
}) async {
try {
await _analytics.logEvent(name: name, parameters: parameters);
debugPrint('📊 Analytics: $name');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
/// Set user ID for analytics
static Future<void> setUserId(String? userId) async {
try {
await _analytics.setUserId(id: userId);
debugPrint('📊 Analytics: setUserId - $userId');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
/// Set user property
static Future<void> setUserProperty({
required String name,
required String? value,
}) async {
try {
await _analytics.setUserProperty(name: name, value: value);
debugPrint('📊 Analytics: setUserProperty - $name: $value');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
} }

View File

@@ -160,7 +160,7 @@ String _$getUserInfoUseCaseHash() =>
/// ///
/// userInfoAsync.when( /// userInfoAsync.when(
/// data: (userInfo) => Text(userInfo.fullName), /// data: (userInfo) => Text(userInfo.fullName),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ///
@@ -184,7 +184,7 @@ const userInfoProvider = UserInfoProvider._();
/// ///
/// userInfoAsync.when( /// userInfoAsync.when(
/// data: (userInfo) => Text(userInfo.fullName), /// data: (userInfo) => Text(userInfo.fullName),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ///
@@ -206,7 +206,7 @@ final class UserInfoProvider
/// ///
/// userInfoAsync.when( /// userInfoAsync.when(
/// data: (userInfo) => Text(userInfo.fullName), /// data: (userInfo) => Text(userInfo.fullName),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ///
@@ -247,7 +247,7 @@ String _$userInfoHash() => r'ed28fdf0213dfd616592b9735cd291f147867047';
/// ///
/// userInfoAsync.when( /// userInfoAsync.when(
/// data: (userInfo) => Text(userInfo.fullName), /// data: (userInfo) => Text(userInfo.fullName),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ///

View File

@@ -190,15 +190,21 @@ class CartRemoteDataSourceImpl implements CartRemoteDataSource {
try { try {
// Map API response to CartItemModel // Map API response to CartItemModel
// API fields: name, item, quantity, amount, item_code, item_name, image, conversion_of_sm // 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( final cartItem = CartItemModel(
cartItemId: item['name'] as String? ?? '', cartItemId: item['name'] as String? ?? '',
cartId: 'user_cart', // Fixed cart ID for user's cart cartId: 'user_cart', // Fixed cart ID for user's cart
productId: item['item_code'] as String? ?? item['item'] as String? ?? '', productId: item['item_code'] as String? ?? item['item'] as String? ?? '',
quantity: (item['quantity'] as num?)?.toDouble() ?? 0.0, quantity: quantity,
unitPrice: (item['amount'] as num?)?.toDouble() ?? 0.0, unitPrice: unitPrice,
subtotal: ((item['quantity'] as num?)?.toDouble() ?? 0.0) * subtotal: quantity * unitPrice,
((item['amount'] as num?)?.toDouble() ?? 0.0),
addedAt: DateTime.now(), // API doesn't provide timestamp 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); cartItems.add(cartItem);

View File

@@ -1,9 +1,12 @@
import 'package:hive_ce/hive.dart'; import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.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'; part 'cart_item_model.g.dart';
/// Cart Item Model - Type ID: 5 /// Cart Item Model - Type ID: 5
///
/// Includes product details from cart API to avoid fetching each product.
@HiveType(typeId: HiveTypeIds.cartItemModel) @HiveType(typeId: HiveTypeIds.cartItemModel)
class CartItemModel extends HiveObject { class CartItemModel extends HiveObject {
CartItemModel({ CartItemModel({
@@ -14,6 +17,9 @@ class CartItemModel extends HiveObject {
required this.unitPrice, required this.unitPrice,
required this.subtotal, required this.subtotal,
required this.addedAt, required this.addedAt,
this.itemName,
this.image,
this.conversionOfSm,
}); });
@HiveField(0) @HiveField(0)
@@ -37,6 +43,18 @@ class CartItemModel extends HiveObject {
@HiveField(6) @HiveField(6)
final DateTime addedAt; 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) { factory CartItemModel.fromJson(Map<String, dynamic> json) {
return CartItemModel( return CartItemModel(
cartItemId: json['cart_item_id'] as String, cartItemId: json['cart_item_id'] as String,
@@ -67,6 +85,9 @@ class CartItemModel extends HiveObject {
double? unitPrice, double? unitPrice,
double? subtotal, double? subtotal,
DateTime? addedAt, DateTime? addedAt,
String? itemName,
String? image,
double? conversionOfSm,
}) => CartItemModel( }) => CartItemModel(
cartItemId: cartItemId ?? this.cartItemId, cartItemId: cartItemId ?? this.cartItemId,
cartId: cartId ?? this.cartId, cartId: cartId ?? this.cartId,
@@ -75,5 +96,22 @@ class CartItemModel extends HiveObject {
unitPrice: unitPrice ?? this.unitPrice, unitPrice: unitPrice ?? this.unitPrice,
subtotal: subtotal ?? this.subtotal, subtotal: subtotal ?? this.subtotal,
addedAt: addedAt ?? this.addedAt, 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,
); );
} }

View File

@@ -24,13 +24,16 @@ class CartItemModelAdapter extends TypeAdapter<CartItemModel> {
unitPrice: (fields[4] as num).toDouble(), unitPrice: (fields[4] as num).toDouble(),
subtotal: (fields[5] as num).toDouble(), subtotal: (fields[5] as num).toDouble(),
addedAt: fields[6] as DateTime, addedAt: fields[6] as DateTime,
itemName: fields[7] as String?,
image: fields[8] as String?,
conversionOfSm: (fields[9] as num?)?.toDouble(),
); );
} }
@override @override
void write(BinaryWriter writer, CartItemModel obj) { void write(BinaryWriter writer, CartItemModel obj) {
writer writer
..writeByte(7) ..writeByte(10)
..writeByte(0) ..writeByte(0)
..write(obj.cartItemId) ..write(obj.cartItemId)
..writeByte(1) ..writeByte(1)
@@ -44,7 +47,13 @@ class CartItemModelAdapter extends TypeAdapter<CartItemModel> {
..writeByte(5) ..writeByte(5)
..write(obj.subtotal) ..write(obj.subtotal)
..writeByte(6) ..writeByte(6)
..write(obj.addedAt); ..write(obj.addedAt)
..writeByte(7)
..write(obj.itemName)
..writeByte(8)
..write(obj.image)
..writeByte(9)
..write(obj.conversionOfSm);
} }
@override @override

View File

@@ -36,6 +36,7 @@ class CartRepositoryImpl implements CartRepository {
required List<String> itemIds, required List<String> itemIds,
required List<double> quantities, required List<double> quantities,
required List<double> prices, required List<double> prices,
List<double?>? conversionFactors,
}) async { }) async {
try { try {
// Validate input // Validate input
@@ -48,11 +49,16 @@ class CartRepositoryImpl implements CartRepository {
// Build API request items // Build API request items
final items = <Map<String, dynamic>>[]; final items = <Map<String, dynamic>>[];
for (int i = 0; i < itemIds.length; i++) { for (int i = 0; i < itemIds.length; i++) {
items.add({ final item = <String, dynamic>{
'item_id': itemIds[i], 'item_id': itemIds[i],
'quantity': quantities[i], 'quantity': quantities[i],
'amount': prices[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 // Try API first
@@ -66,6 +72,7 @@ class CartRepositoryImpl implements CartRepository {
productId: itemIds[i], productId: itemIds[i],
quantity: quantities[i], quantity: quantities[i],
unitPrice: prices[i], unitPrice: prices[i],
conversionOfSm: conversionFactors?[i],
); );
await _localDataSource.addCartItem(cartItemModel); await _localDataSource.addCartItem(cartItemModel);
} }
@@ -80,6 +87,7 @@ class CartRepositoryImpl implements CartRepository {
productId: itemIds[i], productId: itemIds[i],
quantity: quantities[i], quantity: quantities[i],
unitPrice: prices[i], unitPrice: prices[i],
conversionOfSm: conversionFactors?[i],
); );
await _localDataSource.addCartItem(cartItemModel); await _localDataSource.addCartItem(cartItemModel);
} }
@@ -176,6 +184,7 @@ class CartRepositoryImpl implements CartRepository {
required String itemId, required String itemId,
required double quantity, required double quantity,
required double price, required double price,
double? conversionFactor,
}) async { }) async {
try { try {
// API doesn't have update endpoint, use add with new quantity // API doesn't have update endpoint, use add with new quantity
@@ -184,6 +193,7 @@ class CartRepositoryImpl implements CartRepository {
itemIds: [itemId], itemIds: [itemId],
quantities: [quantity], quantities: [quantity],
prices: [price], prices: [price],
conversionFactors: conversionFactor != null ? [conversionFactor] : null,
); );
} catch (e) { } catch (e) {
throw UnknownException('Failed to update cart item quantity', e); throw UnknownException('Failed to update cart item quantity', e);
@@ -268,6 +278,9 @@ class CartRepositoryImpl implements CartRepository {
unitPrice: model.unitPrice, unitPrice: model.unitPrice,
subtotal: model.subtotal, subtotal: model.subtotal,
addedAt: model.addedAt, addedAt: model.addedAt,
itemName: model.itemName,
image: model.image,
conversionOfSm: model.conversionOfSm,
); );
} }
@@ -276,6 +289,7 @@ class CartRepositoryImpl implements CartRepository {
required String productId, required String productId,
required double quantity, required double quantity,
required double unitPrice, required double unitPrice,
double? conversionOfSm,
}) { }) {
return CartItemModel( return CartItemModel(
cartItemId: DateTime.now().millisecondsSinceEpoch.toString(), cartItemId: DateTime.now().millisecondsSinceEpoch.toString(),
@@ -285,6 +299,7 @@ class CartRepositoryImpl implements CartRepository {
unitPrice: unitPrice, unitPrice: unitPrice,
subtotal: quantity * unitPrice, subtotal: quantity * unitPrice,
addedAt: DateTime.now(), addedAt: DateTime.now(),
conversionOfSm: conversionOfSm,
); );
} }
} }

View File

@@ -6,7 +6,7 @@ library;
/// Cart Item Entity /// Cart Item Entity
/// ///
/// Contains item-level information: /// Contains item-level information:
/// - Product reference /// - Product reference and basic info
/// - Quantity /// - Quantity
/// - Pricing /// - Pricing
class CartItem { class CartItem {
@@ -31,6 +31,15 @@ class CartItem {
/// Timestamp when item was added /// Timestamp when item was added
final DateTime addedAt; 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({ const CartItem({
required this.cartItemId, required this.cartItemId,
required this.cartId, required this.cartId,
@@ -39,6 +48,9 @@ class CartItem {
required this.unitPrice, required this.unitPrice,
required this.subtotal, required this.subtotal,
required this.addedAt, required this.addedAt,
this.itemName,
this.image,
this.conversionOfSm,
}); });
/// Calculate subtotal (for verification) /// Calculate subtotal (for verification)
@@ -53,6 +65,9 @@ class CartItem {
double? unitPrice, double? unitPrice,
double? subtotal, double? subtotal,
DateTime? addedAt, DateTime? addedAt,
String? itemName,
String? image,
double? conversionOfSm,
}) { }) {
return CartItem( return CartItem(
cartItemId: cartItemId ?? this.cartItemId, cartItemId: cartItemId ?? this.cartItemId,
@@ -62,6 +77,9 @@ class CartItem {
unitPrice: unitPrice ?? this.unitPrice, unitPrice: unitPrice ?? this.unitPrice,
subtotal: subtotal ?? this.subtotal, subtotal: subtotal ?? this.subtotal,
addedAt: addedAt ?? this.addedAt, addedAt: addedAt ?? this.addedAt,
itemName: itemName ?? this.itemName,
image: image ?? this.image,
conversionOfSm: conversionOfSm ?? this.conversionOfSm,
); );
} }

View File

@@ -25,6 +25,7 @@ abstract class CartRepository {
/// [itemIds] - Product ERPNext item codes /// [itemIds] - Product ERPNext item codes
/// [quantities] - Quantities for each item /// [quantities] - Quantities for each item
/// [prices] - Unit prices for each item /// [prices] - Unit prices for each item
/// [conversionFactors] - Conversion factors (m² to tiles) for each item
/// ///
/// Returns true if successful. /// Returns true if successful.
/// Throws exceptions on failure. /// Throws exceptions on failure.
@@ -32,6 +33,7 @@ abstract class CartRepository {
required List<String> itemIds, required List<String> itemIds,
required List<double> quantities, required List<double> quantities,
required List<double> prices, required List<double> prices,
List<double?>? conversionFactors,
}); });
/// Remove items from cart /// Remove items from cart
@@ -55,6 +57,7 @@ abstract class CartRepository {
/// [itemId] - Product ERPNext item code /// [itemId] - Product ERPNext item code
/// [quantity] - New quantity /// [quantity] - New quantity
/// [price] - Unit price /// [price] - Unit price
/// [conversionFactor] - Conversion factor (m² to tiles)
/// ///
/// Returns true if successful. /// Returns true if successful.
/// Throws exceptions on failure. /// Throws exceptions on failure.
@@ -62,6 +65,7 @@ abstract class CartRepository {
required String itemId, required String itemId,
required double quantity, required double quantity,
required double price, required double price,
double? conversionFactor,
}); });
/// Clear all items from cart /// Clear all items from cart

View File

@@ -3,7 +3,7 @@
/// Shopping cart screen with selection and checkout. /// Shopping cart screen with selection and checkout.
/// Features expanded item list with total price at bottom. /// Features expanded item list with total price at bottom.
library; library;
import 'package:worker/core/utils/extensions.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
@@ -35,14 +35,8 @@ class CartPage extends ConsumerStatefulWidget {
class _CartPageState extends ConsumerState<CartPage> { class _CartPageState extends ConsumerState<CartPage> {
bool _isSyncing = false; bool _isSyncing = false;
@override // Cart is initialized once in home_page.dart at app startup
void initState() { // Provider has keepAlive: true, so no need to reload here
super.initState();
// Initialize cart from API on mount
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(cartProvider.notifier).initialize();
});
}
// Note: Sync is handled in PopScope.onPopInvokedWithResult for back navigation // Note: Sync is handled in PopScope.onPopInvokedWithResult for back navigation
// and in checkout button handler for checkout flow. // and in checkout button handler for checkout flow.
@@ -53,11 +47,7 @@ class _CartPageState extends ConsumerState<CartPage> {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final cartState = ref.watch(cartProvider); final cartState = ref.watch(cartProvider);
final currencyFormatter = NumberFormat.currency(
locale: 'vi_VN',
symbol: 'đ',
decimalDigits: 0,
);
final itemCount = cartState.itemCount; final itemCount = cartState.itemCount;
final hasSelection = cartState.selectedCount > 0; final hasSelection = cartState.selectedCount > 0;
@@ -144,7 +134,11 @@ class _CartPageState extends ConsumerState<CartPage> {
context, context,
cartState, cartState,
ref, ref,
currencyFormatter, NumberFormat.currency(
locale: 'vi_VN',
symbol: 'đ',
decimalDigits: 0,
),
hasSelection, hasSelection,
), ),
], ],

View File

@@ -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_data_providers.dart';
import 'package:worker/features/cart/presentation/providers/cart_state.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/domain/entities/product.dart';
import 'package:worker/features/products/presentation/providers/products_provider.dart';
part 'cart_provider.g.dart'; part 'cart_provider.g.dart';
@@ -46,8 +45,12 @@ class Cart extends _$Cart {
/// Initialize cart by loading from API /// 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 { Future<void> initialize() async {
// Skip if already loaded
if (state.items.isNotEmpty) return;
final repository = await ref.read(cartRepositoryProvider.future); final repository = await ref.read(cartRepositoryProvider.future);
// Set loading state // Set loading state
@@ -55,6 +58,7 @@ class Cart extends _$Cart {
try { try {
// Load cart items from API (with Hive fallback) // 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(); final cartItems = await repository.getCartItems();
// Get member tier from user profile // Get member tier from user profile
@@ -63,16 +67,28 @@ class Cart extends _$Cart {
const memberDiscountPercent = 15.0; const memberDiscountPercent = 15.0;
// Convert CartItem entities to CartItemData for UI // Convert CartItem entities to CartItemData for UI
// Use product data from cart API directly - no need to fetch each product
final items = <CartItemData>[]; final items = <CartItemData>[];
final selectedItems = <String, bool>{}; final selectedItems = <String, bool>{};
// Fetch product details for each cart item
final productsRepository = await ref.read(productsRepositoryProvider.future);
for (final cartItem in cartItems) { for (final cartItem in cartItems) {
try { // Create minimal Product from cart item data (no need to fetch from API)
// Fetch full product entity from products repository final now = DateTime.now();
final product = await productsRepository.getProductById(cartItem.productId); 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 // Calculate conversion for this item
final converted = _calculateConversion( final converted = _calculateConversion(
@@ -80,7 +96,7 @@ class Cart extends _$Cart {
product.conversionOfSm, product.conversionOfSm,
); );
// Create CartItemData with full product info // Create CartItemData with product info from cart API
items.add( items.add(
CartItemData( CartItemData(
product: product, product: product,
@@ -92,12 +108,6 @@ class Cart extends _$Cart {
// Initialize as not selected by default // Initialize as not selected by default
selectedItems[product.productId] = false; 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');
}
} }
final newState = CartState( final newState = CartState(
@@ -150,6 +160,7 @@ class Cart extends _$Cart {
itemIds: [product.erpnextItemCode ?? product.productId], itemIds: [product.erpnextItemCode ?? product.productId],
quantities: [quantity], quantities: [quantity],
prices: [product.basePrice], prices: [product.basePrice],
conversionFactors: [product.conversionOfSm],
); );
// Calculate conversion // Calculate conversion
@@ -332,6 +343,7 @@ class Cart extends _$Cart {
itemId: item.product.erpnextItemCode ?? productId, itemId: item.product.erpnextItemCode ?? productId,
quantity: quantity, quantity: quantity,
price: item.product.basePrice, price: item.product.basePrice,
conversionFactor: item.product.conversionOfSm,
); );
} catch (e) { } catch (e) {
// Silent fail - keep local state, user can retry later // Silent fail - keep local state, user can retry later
@@ -370,6 +382,7 @@ class Cart extends _$Cart {
itemId: item.product.erpnextItemCode ?? productId, itemId: item.product.erpnextItemCode ?? productId,
quantity: newQuantity, quantity: newQuantity,
price: item.product.basePrice, price: item.product.basePrice,
conversionFactor: item.product.conversionOfSm,
); );
// Update local state // Update local state

View File

@@ -64,7 +64,7 @@ final class CartProvider extends $NotifierProvider<Cart, CartState> {
} }
} }
String _$cartHash() => r'706de28734e7059b2e9484f3b1d94226a0e90bb9'; String _$cartHash() => r'a12712db833127ef66b75f52cba8376409806afa';
/// Cart Notifier /// Cart Notifier
/// ///

View File

@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.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/widgets/loading_indicator.dart';
import 'package:worker/core/theme/typography.dart'; import 'package:worker/core/theme/typography.dart';
import 'package:worker/features/cart/presentation/providers/cart_provider.dart'; import 'package:worker/features/cart/presentation/providers/cart_provider.dart';
@@ -79,11 +80,6 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
final isSelected = final isSelected =
cartState.selectedItems[widget.item.product.productId] ?? false; cartState.selectedItems[widget.item.product.productId] ?? false;
final currencyFormatter = NumberFormat.currency(
locale: 'vi_VN',
symbol: 'đ',
decimalDigits: 0,
);
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
@@ -121,7 +117,11 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage( 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, width: 100,
height: 100, height: 100,
fit: BoxFit.cover, fit: BoxFit.cover,
@@ -168,7 +168,7 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
// Price // Price
Text( Text(
'${currencyFormatter.format(widget.item.product.basePrice)}/m²', '${widget.item.product.basePrice.toVNCurrency}/m²',
style: AppTypography.titleMedium.copyWith( style: AppTypography.titleMedium.copyWith(
color: colorScheme.primary, color: colorScheme.primary,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,

View File

@@ -60,6 +60,48 @@ class FavoriteProductsLocalDataSource {
bool isBoxOpen() { bool isBoxOpen() {
return Hive.isBoxOpen(HiveBoxNames.favoriteProductsBox); 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 /// Debug print helper

View File

@@ -71,7 +71,12 @@ class FavoriteProducts extends _$FavoriteProducts {
@override @override
Future<List<Product>> build() async { Future<List<Product>> build() async {
_repository = await ref.read(favoritesRepositoryProvider.future); _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 /// 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. /// No userId needed - the API uses the authenticated session.
Future<void> addFavorite(String productId) async { Future<void> addFavorite(String productId) async {
try { try {
_debugPrint('Adding product to favorites: $productId'); _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) // Call repository to add to favorites (uses auth token from session)
await _repository.addFavorite(productId); await _repository.addFavorite(productId);
// Refresh the products list after successful addition
await refresh();
_debugPrint('Successfully added favorite: $productId'); _debugPrint('Successfully added favorite: $productId');
} catch (e) { } catch (e) {
// Rollback optimistic update on error
ref.read(favoriteIdsLocalProvider.notifier).removeId(productId);
_debugPrint('Error adding favorite: $e'); _debugPrint('Error adding favorite: $e');
rethrow; rethrow;
} }
@@ -120,20 +127,22 @@ class FavoriteProducts extends _$FavoriteProducts {
/// Remove a product from favorites /// 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. /// No userId needed - the API uses the authenticated session.
Future<void> removeFavorite(String productId) async { Future<void> removeFavorite(String productId) async {
try { try {
_debugPrint('Removing product from favorites: $productId'); _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) // Call repository to remove from favorites (uses auth token from session)
await _repository.removeFavorite(productId); await _repository.removeFavorite(productId);
// Refresh the products list after successful removal
await refresh();
_debugPrint('Successfully removed favorite: $productId'); _debugPrint('Successfully removed favorite: $productId');
} catch (e) { } catch (e) {
// Rollback optimistic update on error
ref.read(favoriteIdsLocalProvider.notifier).addId(productId);
_debugPrint('Error removing favorite: $e'); _debugPrint('Error removing favorite: $e');
rethrow; rethrow;
} }
@@ -143,9 +152,11 @@ class FavoriteProducts extends _$FavoriteProducts {
/// ///
/// If the product is favorited, it will be removed. /// If the product is favorited, it will be removed.
/// If the product is not favorited, it will be added. /// If the product is not favorited, it will be added.
/// Checks from local state for instant response.
Future<void> toggleFavorite(String productId) async { Future<void> toggleFavorite(String productId) async {
final currentProducts = state.value ?? []; // Check from local IDs (instant, no API call)
final isFavorited = currentProducts.any((p) => p.productId == productId); final localIds = ref.read(favoriteIdsLocalProvider);
final isFavorited = localIds.contains(productId);
if (isFavorited) { if (isFavorited) {
await removeFavorite(productId); await removeFavorite(productId);
@@ -170,20 +181,48 @@ class FavoriteProducts extends _$FavoriteProducts {
// HELPER PROVIDERS // 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. /// Reads directly from Hive local cache for instant response.
/// Returns true if the product is in the user's favorites, false otherwise. /// This is used in product detail page to avoid unnecessary API calls.
/// Safe to use in build methods - will return false during loading/error states. /// The cache is synced when favorites are loaded or modified.
@riverpod @riverpod
bool isFavorite(Ref ref, String productId) { 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( final localDataSource = ref.read(favoriteProductsLocalDataSourceProvider);
data: (products) => products.any((p) => p.productId == productId), return localDataSource.isFavorite(productId);
loading: () => false, }
error: (_, __) => false,
); /// 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 /// Get the total count of favorites

View File

@@ -231,7 +231,7 @@ final class FavoriteProductsProvider
FavoriteProducts create() => FavoriteProducts(); FavoriteProducts create() => FavoriteProducts();
} }
String _$favoriteProductsHash() => r'd43c41db210259021df104f9fecdd00cf474d196'; String _$favoriteProductsHash() => r'6d042f469a1f71bb06f8b5b76014bf24e30e6758';
/// Manages favorite products with full Product data from wishlist API /// 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. /// Reads directly from Hive local cache for instant response.
/// Returns true if the product is in the user's favorites, false otherwise. /// This is used in product detail page to avoid unnecessary API calls.
/// Safe to use in build methods - will return false during loading/error states. /// The cache is synced when favorites are loaded or modified.
@ProviderFor(isFavorite) @ProviderFor(isFavorite)
const isFavoriteProvider = IsFavoriteFamily._(); 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. /// Reads directly from Hive local cache for instant response.
/// Returns true if the product is in the user's favorites, false otherwise. /// This is used in product detail page to avoid unnecessary API calls.
/// Safe to use in build methods - will return false during loading/error states. /// The cache is synced when favorites are loaded or modified.
final class IsFavoriteProvider extends $FunctionalProvider<bool, bool, bool> final class IsFavoriteProvider extends $FunctionalProvider<bool, bool, bool>
with $Provider<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. /// Reads directly from Hive local cache for instant response.
/// Returns true if the product is in the user's favorites, false otherwise. /// This is used in product detail page to avoid unnecessary API calls.
/// Safe to use in build methods - will return false during loading/error states. /// The cache is synced when favorites are loaded or modified.
const IsFavoriteProvider._({ const IsFavoriteProvider._({
required IsFavoriteFamily super.from, required IsFavoriteFamily super.from,
required String super.argument, 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. /// Reads directly from Hive local cache for instant response.
/// Returns true if the product is in the user's favorites, false otherwise. /// This is used in product detail page to avoid unnecessary API calls.
/// Safe to use in build methods - will return false during loading/error states. /// The cache is synced when favorites are loaded or modified.
final class IsFavoriteFamily extends $Family final class IsFavoriteFamily extends $Family
with $FunctionalFamilyOverride<bool, String> { with $FunctionalFamilyOverride<bool, String> {
@@ -361,11 +361,11 @@ final class IsFavoriteFamily extends $Family
isAutoDispose: true, 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. /// Reads directly from Hive local cache for instant response.
/// Returns true if the product is in the user's favorites, false otherwise. /// This is used in product detail page to avoid unnecessary API calls.
/// Safe to use in build methods - will return false during loading/error states. /// The cache is synced when favorites are loaded or modified.
IsFavoriteProvider call(String productId) => IsFavoriteProvider call(String productId) =>
IsFavoriteProvider._(argument: productId, from: this); IsFavoriteProvider._(argument: productId, from: this);
@@ -374,6 +374,77 @@ final class IsFavoriteFamily extends $Family
String toString() => r'isFavoriteProvider'; 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 /// Get the total count of favorites
/// ///
/// Derived from the favorite products list. /// Derived from the favorite products list.

View File

@@ -20,7 +20,7 @@ part of 'member_card_provider.dart';
/// ///
/// memberCardAsync.when( /// memberCardAsync.when(
/// data: (memberCard) => MemberCardWidget(memberCard: memberCard), /// data: (memberCard) => MemberCardWidget(memberCard: memberCard),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
@@ -40,7 +40,7 @@ const memberCardProvider = MemberCardNotifierProvider._();
/// ///
/// memberCardAsync.when( /// memberCardAsync.when(
/// data: (memberCard) => MemberCardWidget(memberCard: memberCard), /// data: (memberCard) => MemberCardWidget(memberCard: memberCard),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
@@ -58,7 +58,7 @@ final class MemberCardNotifierProvider
/// ///
/// memberCardAsync.when( /// memberCardAsync.when(
/// data: (memberCard) => MemberCardWidget(memberCard: memberCard), /// data: (memberCard) => MemberCardWidget(memberCard: memberCard),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
@@ -96,7 +96,7 @@ String _$memberCardNotifierHash() =>
/// ///
/// memberCardAsync.when( /// memberCardAsync.when(
/// data: (memberCard) => MemberCardWidget(memberCard: memberCard), /// data: (memberCard) => MemberCardWidget(memberCard: memberCard),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```

View File

@@ -21,7 +21,7 @@ part of 'promotions_provider.dart';
/// ///
/// promotionsAsync.when( /// promotionsAsync.when(
/// data: (promotions) => PromotionSlider(promotions: promotions), /// data: (promotions) => PromotionSlider(promotions: promotions),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
@@ -42,7 +42,7 @@ const promotionsProvider = PromotionsProvider._();
/// ///
/// promotionsAsync.when( /// promotionsAsync.when(
/// data: (promotions) => PromotionSlider(promotions: promotions), /// data: (promotions) => PromotionSlider(promotions: promotions),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
@@ -68,7 +68,7 @@ final class PromotionsProvider
/// ///
/// promotionsAsync.when( /// promotionsAsync.when(
/// data: (promotions) => PromotionSlider(promotions: promotions), /// data: (promotions) => PromotionSlider(promotions: promotions),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```

View File

@@ -278,7 +278,7 @@ class ProductsRemoteDataSource {
data: { data: {
'doctype': 'Item Group', 'doctype': 'Item Group',
'fields': ['item_group_name', 'name'], 'fields': ['item_group_name', 'name'],
'filters': {'is_group': 0}, 'filters': {'is_group': 0, 'custom_published': 1},
'limit_page_length': 0, 'limit_page_length': 0,
}, },
options: Options(headers: headers), options: Options(headers: headers),

View File

@@ -13,6 +13,7 @@ import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/router/app_router.dart'; import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/theme/colors.dart'; import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/cart/presentation/providers/cart_provider.dart'; import 'package:worker/features/cart/presentation/providers/cart_provider.dart';
import 'package:worker/features/products/presentation/providers/product_filters_provider.dart';
import 'package:worker/features/products/presentation/providers/products_provider.dart'; import 'package:worker/features/products/presentation/providers/products_provider.dart';
import 'package:worker/features/products/presentation/widgets/brand_filter_chips.dart'; import 'package:worker/features/products/presentation/widgets/brand_filter_chips.dart';
import 'package:worker/features/products/presentation/widgets/product_filter_drawer.dart'; import 'package:worker/features/products/presentation/widgets/product_filter_drawer.dart';
@@ -100,6 +101,8 @@ class ProductsPage extends ConsumerWidget {
), ),
child: IconButton( child: IconButton(
onPressed: () { onPressed: () {
// Sync pending filters with applied filters before opening drawer
ref.read(productFiltersProvider.notifier).syncWithApplied();
// Open filter drawer from right // Open filter drawer from right
Scaffold.of(scaffoldContext).openEndDrawer(); Scaffold.of(scaffoldContext).openEndDrawer();
}, },

View File

@@ -24,7 +24,7 @@ part of 'product_filter_options_provider.dart';
/// ///
/// filterOptionsAsync.when( /// filterOptionsAsync.when(
/// data: (options) => ProductFilterDrawer(options: options), /// data: (options) => ProductFilterDrawer(options: options),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
@@ -48,7 +48,7 @@ const productFilterOptionsProvider = ProductFilterOptionsProvider._();
/// ///
/// filterOptionsAsync.when( /// filterOptionsAsync.when(
/// data: (options) => ProductFilterDrawer(options: options), /// data: (options) => ProductFilterDrawer(options: options),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
@@ -79,7 +79,7 @@ final class ProductFilterOptionsProvider
/// ///
/// filterOptionsAsync.when( /// filterOptionsAsync.when(
/// data: (options) => ProductFilterDrawer(options: options), /// data: (options) => ProductFilterDrawer(options: options),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```

View File

@@ -1,6 +1,8 @@
/// Provider: Product Filters State /// Provider: Product Filters State
/// ///
/// Manages product filter selections. /// Manages product filter selections with separate pending and applied states.
/// Pending filters: Updated on every checkbox toggle (no API call)
/// Applied filters: Only updated when Apply button is pressed (triggers API)
library; library;
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -54,7 +56,10 @@ class ProductFiltersState {
} }
} }
/// Product Filters Notifier /// Product Filters Notifier (Pending Filters - for UI selection)
///
/// This provider stores the PENDING filter selections in the drawer.
/// Changes here do NOT trigger API calls.
class ProductFiltersNotifier extends Notifier<ProductFiltersState> { class ProductFiltersNotifier extends Notifier<ProductFiltersState> {
@override @override
ProductFiltersState build() => const ProductFiltersState(); ProductFiltersState build() => const ProductFiltersState();
@@ -114,19 +119,52 @@ class ProductFiltersNotifier extends Notifier<ProductFiltersState> {
state = state.copyWith(brands: newSet); state = state.copyWith(brands: newSet);
} }
/// Reset all filters /// Reset all filters (both pending and applied)
void reset() { void reset() {
state = const ProductFiltersState(); state = const ProductFiltersState();
// Also reset applied filters
ref.read(appliedProductFiltersProvider.notifier).reset();
} }
/// Apply filters (placeholder for future implementation) /// Apply filters - copies pending state to applied state
/// This is the ONLY action that triggers API calls
void apply() { void apply() {
// TODO: Trigger products provider refresh with filters ref.read(appliedProductFiltersProvider.notifier).applyFilters(state);
}
/// Sync pending state with applied state (when opening drawer)
void syncWithApplied() {
state = ref.read(appliedProductFiltersProvider);
} }
} }
/// Product Filters Provider /// Applied Product Filters Notifier (Triggers API calls)
///
/// This provider stores the APPLIED filter state.
/// The products provider watches THIS provider, not the pending one.
class AppliedProductFiltersNotifier extends Notifier<ProductFiltersState> {
@override
ProductFiltersState build() => const ProductFiltersState();
/// Apply filters from pending state
void applyFilters(ProductFiltersState filters) {
state = filters;
}
/// Reset applied filters
void reset() {
state = const ProductFiltersState();
}
}
/// Product Filters Provider (Pending - for drawer UI)
final productFiltersProvider = final productFiltersProvider =
NotifierProvider<ProductFiltersNotifier, ProductFiltersState>( NotifierProvider<ProductFiltersNotifier, ProductFiltersState>(
ProductFiltersNotifier.new, ProductFiltersNotifier.new,
); );
/// Applied Product Filters Provider (Triggers API)
final appliedProductFiltersProvider =
NotifierProvider<AppliedProductFiltersNotifier, ProductFiltersState>(
AppliedProductFiltersNotifier.new,
);

View File

@@ -81,7 +81,8 @@ class Products extends _$Products {
// Watch dependencies (triggers rebuild when they change) // Watch dependencies (triggers rebuild when they change)
final searchQuery = ref.watch(searchQueryProvider); final searchQuery = ref.watch(searchQueryProvider);
final filters = ref.watch(productFiltersProvider); // Watch APPLIED filters, not pending filters (API only called on Apply)
final filters = ref.watch(appliedProductFiltersProvider);
// Get repository with injected data sources // Get repository with injected data sources
final repository = await ref.watch(productsRepositoryProvider.future); final repository = await ref.watch(productsRepositoryProvider.future);
@@ -148,7 +149,8 @@ class Products extends _$Products {
// Read dependencies to get current filters (use read, not watch) // Read dependencies to get current filters (use read, not watch)
final searchQuery = ref.read(searchQueryProvider); final searchQuery = ref.read(searchQueryProvider);
final filters = ref.read(productFiltersProvider); // Read APPLIED filters, not pending filters
final filters = ref.read(appliedProductFiltersProvider);
// Get repository // Get repository
final repository = await ref.read(productsRepositoryProvider.future); final repository = await ref.read(productsRepositoryProvider.future);

View File

@@ -167,7 +167,7 @@ String _$productsRepositoryHash() =>
/// ///
/// productsAsync.when( /// productsAsync.when(
/// data: (products) => ProductGrid(products: products), /// data: (products) => ProductGrid(products: products),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
@@ -187,7 +187,7 @@ const productsProvider = ProductsProvider._();
/// ///
/// productsAsync.when( /// productsAsync.when(
/// data: (products) => ProductGrid(products: products), /// data: (products) => ProductGrid(products: products),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
@@ -205,7 +205,7 @@ final class ProductsProvider
/// ///
/// productsAsync.when( /// productsAsync.when(
/// data: (products) => ProductGrid(products: products), /// data: (products) => ProductGrid(products: products),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
@@ -228,7 +228,7 @@ final class ProductsProvider
Products create() => Products(); Products create() => Products();
} }
String _$productsHash() => r'a4f416712cdbf2e633622c65b1fdc95686e31fa4'; String _$productsHash() => r'502af6c2e9012a619c15fd04bfe778045739e247';
/// Products Provider /// Products Provider
/// ///
@@ -242,7 +242,7 @@ String _$productsHash() => r'a4f416712cdbf2e633622c65b1fdc95686e31fa4';
/// ///
/// productsAsync.when( /// productsAsync.when(
/// data: (products) => ProductGrid(products: products), /// data: (products) => ProductGrid(products: products),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
@@ -333,7 +333,7 @@ String _$allProductsHash() => r'402d7c6e8d119c7c7eab5e696fb8163831259def';
/// ///
/// productAsync.when( /// productAsync.when(
/// data: (product) => ProductDetailView(product: product), /// data: (product) => ProductDetailView(product: product),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
@@ -352,7 +352,7 @@ const productDetailProvider = ProductDetailFamily._();
/// ///
/// productAsync.when( /// productAsync.when(
/// data: (product) => ProductDetailView(product: product), /// data: (product) => ProductDetailView(product: product),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
@@ -371,7 +371,7 @@ final class ProductDetailProvider
/// ///
/// productAsync.when( /// productAsync.when(
/// data: (product) => ProductDetailView(product: product), /// data: (product) => ProductDetailView(product: product),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
@@ -431,7 +431,7 @@ String _$productDetailHash() => r'ca219f1451f518c84ca1832aacb3c83920f4bfd2';
/// ///
/// productAsync.when( /// productAsync.when(
/// data: (product) => ProductDetailView(product: product), /// data: (product) => ProductDetailView(product: product),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
@@ -458,7 +458,7 @@ final class ProductDetailFamily extends $Family
/// ///
/// productAsync.when( /// productAsync.when(
/// data: (product) => ProductDetailView(product: product), /// data: (product) => ProductDetailView(product: product),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```

View File

@@ -13,15 +13,16 @@ import 'package:worker/features/products/presentation/providers/product_filters_
/// Brand Filter Chips Widget /// Brand Filter Chips Widget
/// ///
/// Displays brands as horizontally scrolling chips. /// Displays brands as horizontally scrolling chips.
/// Synced with filter drawer - both use productFiltersProvider.brands. /// Watches appliedProductFiltersProvider to show currently active filters.
/// Chips are single-select (tapping a brand clears others and sets just that one). /// Tapping a chip directly applies the filter (triggers API call immediately).
class BrandFilterChips extends ConsumerWidget { class BrandFilterChips extends ConsumerWidget {
const BrandFilterChips({super.key}); const BrandFilterChips({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final filtersState = ref.watch(productFiltersProvider); // Watch APPLIED filters to show current active state
final appliedFilters = ref.watch(appliedProductFiltersProvider);
final filterOptionsAsync = ref.watch(productFilterOptionsProvider); final filterOptionsAsync = ref.watch(productFilterOptionsProvider);
return filterOptionsAsync.when( return filterOptionsAsync.when(
@@ -46,9 +47,9 @@ class BrandFilterChips extends ConsumerWidget {
// "Tất cả" is selected if no brands are selected // "Tất cả" is selected if no brands are selected
// A brand chip is selected if it's the ONLY brand selected // A brand chip is selected if it's the ONLY brand selected
final isSelected = brand.value == 'all' final isSelected = brand.value == 'all'
? filtersState.brands.isEmpty ? appliedFilters.brands.isEmpty
: (filtersState.brands.length == 1 && : (appliedFilters.brands.length == 1 &&
filtersState.brands.contains(brand.value)); appliedFilters.brands.contains(brand.value));
return FilterChip( return FilterChip(
label: Text( label: Text(
@@ -56,33 +57,28 @@ class BrandFilterChips extends ConsumerWidget {
style: TextStyle( style: TextStyle(
fontSize: 14.0, fontSize: 14.0,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
color: colorScheme.onSurface, color: isSelected ? Colors.white : colorScheme.onSurface,
), ),
), ),
selected: isSelected, selected: isSelected,
onSelected: (selected) { onSelected: (selected) {
if (selected) { if (selected) {
final notifier = ref.read(productFiltersProvider.notifier); // Create new filter state with only the selected brand
ProductFiltersState newFilters;
if (brand.value == 'all') { if (brand.value == 'all') {
// Clear all brand filters // Clear brand filter, keep other filters
// Reset all brands by setting to empty set newFilters = appliedFilters.copyWith(brands: {});
final currentBrands = List<String>.from(filtersState.brands);
for (final b in currentBrands) {
notifier.toggleBrand(b); // Toggle off each brand
}
} else { } else {
// Single-select: clear all other brands and set only this one // Set only this brand, keep other filters
final currentBrands = List<String>.from(filtersState.brands); newFilters = appliedFilters.copyWith(brands: {brand.value});
// First, clear all existing brands
for (final b in currentBrands) {
notifier.toggleBrand(b);
} }
// Then add the selected brand // Apply directly to trigger API call
notifier.toggleBrand(brand.value); ref.read(appliedProductFiltersProvider.notifier).applyFilters(newFilters);
}
// Also sync pending filters with applied
ref.read(productFiltersProvider.notifier).syncWithApplied();
} }
}, },
backgroundColor: colorScheme.surface, backgroundColor: colorScheme.surface,