update filter products

This commit is contained in:
Phuoc Nguyen
2025-12-03 14:33:08 +07:00
parent cae04b3ae7
commit e1c9f818d2
11 changed files with 380 additions and 67 deletions

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

@@ -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

@@ -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(
@@ -62,27 +63,22 @@ class BrandFilterChips extends ConsumerWidget {
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
notifier.toggleBrand(brand.value);
} }
// Apply directly to trigger API call
ref.read(appliedProductFiltersProvider.notifier).applyFilters(newFilters);
// Also sync pending filters with applied
ref.read(productFiltersProvider.notifier).syncWithApplied();
} }
}, },
backgroundColor: colorScheme.surface, backgroundColor: colorScheme.surface,