update filter products
This commit is contained in:
@@ -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'),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
/// );
|
/// );
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -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),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
|||||||
@@ -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),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
|||||||
@@ -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();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
state = const ProductFiltersState();
|
||||||
|
// Also reset applied filters
|
||||||
|
ref.read(appliedProductFiltersProvider.notifier).reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply filters - copies pending state to applied state
|
||||||
|
/// This is the ONLY action that triggers API calls
|
||||||
|
void apply() {
|
||||||
|
ref.read(appliedProductFiltersProvider.notifier).applyFilters(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sync pending state with applied state (when opening drawer)
|
||||||
|
void syncWithApplied() {
|
||||||
|
state = ref.read(appliedProductFiltersProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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() {
|
void reset() {
|
||||||
state = const ProductFiltersState();
|
state = const ProductFiltersState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply filters (placeholder for future implementation)
|
|
||||||
void apply() {
|
|
||||||
// TODO: Trigger products provider refresh with filters
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Product Filters Provider
|
/// 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,
|
||||||
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
|||||||
@@ -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
|
// 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,
|
||||||
|
|||||||
Reference in New Issue
Block a user