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);
/// connectivityState.when(
/// data: (status) => Text('Status: $status'),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, _) => Text('Error: $error'),
/// );
/// ```
@@ -81,7 +81,7 @@ const connectivityStreamProvider = ConnectivityStreamProvider._();
/// final connectivityState = ref.watch(connectivityStreamProvider);
/// connectivityState.when(
/// data: (status) => Text('Status: $status'),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, _) => Text('Error: $error'),
/// );
/// ```
@@ -104,7 +104,7 @@ final class ConnectivityStreamProvider
/// final connectivityState = ref.watch(connectivityStreamProvider);
/// connectivityState.when(
/// data: (status) => Text('Status: $status'),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, _) => Text('Error: $error'),
/// );
/// ```
@@ -219,7 +219,7 @@ String _$currentConnectivityHash() =>
/// final isOnlineAsync = ref.watch(isOnlineProvider);
/// isOnlineAsync.when(
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, _) => Text('Error: $error'),
/// );
/// ```
@@ -235,7 +235,7 @@ const isOnlineProvider = IsOnlineProvider._();
/// final isOnlineAsync = ref.watch(isOnlineProvider);
/// isOnlineAsync.when(
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, _) => Text('Error: $error'),
/// );
/// ```
@@ -251,7 +251,7 @@ final class IsOnlineProvider
/// final isOnlineAsync = ref.watch(isOnlineProvider);
/// isOnlineAsync.when(
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// 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
///
/// [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({
required String productId,
required String productName,
@@ -85,4 +111,252 @@ class AnalyticsService {
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(
/// data: (userInfo) => Text(userInfo.fullName),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
///
@@ -184,7 +184,7 @@ const userInfoProvider = UserInfoProvider._();
///
/// userInfoAsync.when(
/// data: (userInfo) => Text(userInfo.fullName),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
///
@@ -206,7 +206,7 @@ final class UserInfoProvider
///
/// userInfoAsync.when(
/// data: (userInfo) => Text(userInfo.fullName),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
///
@@ -247,7 +247,7 @@ String _$userInfoHash() => r'ed28fdf0213dfd616592b9735cd291f147867047';
///
/// userInfoAsync.when(
/// data: (userInfo) => Text(userInfo.fullName),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
///

View File

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

View File

@@ -21,7 +21,7 @@ part of 'promotions_provider.dart';
///
/// promotionsAsync.when(
/// data: (promotions) => PromotionSlider(promotions: promotions),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
/// ```
@@ -42,7 +42,7 @@ const promotionsProvider = PromotionsProvider._();
///
/// promotionsAsync.when(
/// data: (promotions) => PromotionSlider(promotions: promotions),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
/// ```
@@ -68,7 +68,7 @@ final class PromotionsProvider
///
/// promotionsAsync.when(
/// data: (promotions) => PromotionSlider(promotions: promotions),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// 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/theme/colors.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/widgets/brand_filter_chips.dart';
import 'package:worker/features/products/presentation/widgets/product_filter_drawer.dart';
@@ -100,6 +101,8 @@ class ProductsPage extends ConsumerWidget {
),
child: IconButton(
onPressed: () {
// Sync pending filters with applied filters before opening drawer
ref.read(productFiltersProvider.notifier).syncWithApplied();
// Open filter drawer from right
Scaffold.of(scaffoldContext).openEndDrawer();
},

View File

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

View File

@@ -1,6 +1,8 @@
/// 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;
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> {
@override
ProductFiltersState build() => const ProductFiltersState();
@@ -114,19 +119,52 @@ class ProductFiltersNotifier extends Notifier<ProductFiltersState> {
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 (placeholder for future implementation)
/// Apply filters - copies pending state to applied state
/// This is the ONLY action that triggers API calls
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 =
NotifierProvider<ProductFiltersNotifier, ProductFiltersState>(
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)
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
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)
final searchQuery = ref.read(searchQueryProvider);
final filters = ref.read(productFiltersProvider);
// Read APPLIED filters, not pending filters
final filters = ref.read(appliedProductFiltersProvider);
// Get repository
final repository = await ref.read(productsRepositoryProvider.future);

View File

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

View File

@@ -13,15 +13,16 @@ import 'package:worker/features/products/presentation/providers/product_filters_
/// Brand Filter Chips Widget
///
/// Displays brands as horizontally scrolling chips.
/// Synced with filter drawer - both use productFiltersProvider.brands.
/// Chips are single-select (tapping a brand clears others and sets just that one).
/// Watches appliedProductFiltersProvider to show currently active filters.
/// Tapping a chip directly applies the filter (triggers API call immediately).
class BrandFilterChips extends ConsumerWidget {
const BrandFilterChips({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
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);
return filterOptionsAsync.when(
@@ -46,9 +47,9 @@ class BrandFilterChips extends ConsumerWidget {
// "Tất cả" is selected if no brands are selected
// A brand chip is selected if it's the ONLY brand selected
final isSelected = brand.value == 'all'
? filtersState.brands.isEmpty
: (filtersState.brands.length == 1 &&
filtersState.brands.contains(brand.value));
? appliedFilters.brands.isEmpty
: (appliedFilters.brands.length == 1 &&
appliedFilters.brands.contains(brand.value));
return FilterChip(
label: Text(
@@ -62,27 +63,22 @@ class BrandFilterChips extends ConsumerWidget {
selected: isSelected,
onSelected: (selected) {
if (selected) {
final notifier = ref.read(productFiltersProvider.notifier);
// Create new filter state with only the selected brand
ProductFiltersState newFilters;
if (brand.value == 'all') {
// Clear all brand filters
// Reset all brands by setting to empty set
final currentBrands = List<String>.from(filtersState.brands);
for (final b in currentBrands) {
notifier.toggleBrand(b); // Toggle off each brand
}
// Clear brand filter, keep other filters
newFilters = appliedFilters.copyWith(brands: {});
} else {
// Single-select: clear all other brands and set only this one
final currentBrands = List<String>.from(filtersState.brands);
// First, clear all existing brands
for (final b in currentBrands) {
notifier.toggleBrand(b);
// Set only this brand, keep other filters
newFilters = appliedFilters.copyWith(brands: {brand.value});
}
// 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,