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

@@ -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);
}
// Then add the selected brand
notifier.toggleBrand(brand.value);
// Set only this brand, keep other filters
newFilters = appliedFilters.copyWith(brands: {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,