fix filter
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
/// Provider: Product Filter Options Provider
|
||||
///
|
||||
/// Merges data from 3 APIs (groups, brands, attributes) for product filtering.
|
||||
library;
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker/features/products/presentation/providers/products_provider.dart';
|
||||
|
||||
part 'product_filter_options_provider.g.dart';
|
||||
|
||||
/// Filter Options Data Model
|
||||
///
|
||||
/// Combines all filter options from the 3 APIs
|
||||
class ProductFilterOptions {
|
||||
const ProductFilterOptions({
|
||||
required this.groups,
|
||||
required this.brands,
|
||||
required this.attributeGroups,
|
||||
});
|
||||
|
||||
/// Product groups (Item Groups)
|
||||
final List<FilterOption> groups;
|
||||
|
||||
/// Product brands
|
||||
final List<FilterOption> brands;
|
||||
|
||||
/// Product attribute groups (Colour, Size, Surface, etc.)
|
||||
/// Each group contains a list of attribute values
|
||||
final List<AttributeGroup> attributeGroups;
|
||||
}
|
||||
|
||||
/// Attribute Group Model
|
||||
///
|
||||
/// Represents a group of attributes (e.g., Colour, Size, Surface)
|
||||
/// with its associated values
|
||||
class AttributeGroup {
|
||||
const AttributeGroup({
|
||||
required this.name,
|
||||
required this.attributeName,
|
||||
required this.values,
|
||||
});
|
||||
|
||||
/// Internal name
|
||||
final String name;
|
||||
|
||||
/// Display name
|
||||
final String attributeName;
|
||||
|
||||
/// List of attribute values
|
||||
final List<AttributeValue> values;
|
||||
}
|
||||
|
||||
/// Attribute Value Model
|
||||
///
|
||||
/// Represents a single attribute value within a group
|
||||
class AttributeValue {
|
||||
const AttributeValue({
|
||||
required this.name,
|
||||
required this.attributeValue,
|
||||
this.abbr,
|
||||
});
|
||||
|
||||
/// Internal name/ID
|
||||
final String name;
|
||||
|
||||
/// Display value
|
||||
final String attributeValue;
|
||||
|
||||
/// Abbreviation (optional)
|
||||
final String? abbr;
|
||||
}
|
||||
|
||||
/// Filter Option Model
|
||||
///
|
||||
/// Represents a single filter option with value and label
|
||||
class FilterOption {
|
||||
const FilterOption({
|
||||
required this.value,
|
||||
required this.label,
|
||||
});
|
||||
|
||||
final String value;
|
||||
final String label;
|
||||
}
|
||||
|
||||
/// Product Filter Options Provider
|
||||
///
|
||||
/// Fetches and combines data from 3 Frappe APIs:
|
||||
/// 1. Product Groups (Item Groups)
|
||||
/// 2. Product Brands
|
||||
/// 3. Product Attributes
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final filterOptionsAsync = ref.watch(productFilterOptionsProvider);
|
||||
///
|
||||
/// filterOptionsAsync.when(
|
||||
/// data: (options) => ProductFilterDrawer(options: options),
|
||||
/// loading: () => CircularProgressIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
@riverpod
|
||||
Future<ProductFilterOptions> productFilterOptions(Ref ref) async {
|
||||
try {
|
||||
// Get remote datasource
|
||||
final remoteDataSource = await ref.watch(productsRemoteDataSourceProvider.future);
|
||||
|
||||
// Fetch all 3 APIs in parallel
|
||||
final results = await Future.wait([
|
||||
remoteDataSource.getProductGroups(),
|
||||
remoteDataSource.getProductBrands(),
|
||||
remoteDataSource.getProductAttributes(),
|
||||
]);
|
||||
|
||||
final groupsData = results[0] as List<Map<String, dynamic>>;
|
||||
final brandsData = results[1] as List<String>;
|
||||
final attributesData = results[2] as List<Map<String, dynamic>>;
|
||||
|
||||
// Convert groups to FilterOption
|
||||
final groups = groupsData
|
||||
.map((group) => FilterOption(
|
||||
value: group['name'] as String,
|
||||
label: group['item_group_name'] as String? ?? group['name'] as String,
|
||||
))
|
||||
.toList();
|
||||
|
||||
// Convert brands to FilterOption
|
||||
final brands = brandsData
|
||||
.map((brand) => FilterOption(
|
||||
value: brand,
|
||||
label: brand,
|
||||
))
|
||||
.toList();
|
||||
|
||||
// Convert attributes to AttributeGroup
|
||||
// Each attribute has: name, attribute_name, and values list
|
||||
final attributeGroups = attributesData.map((attr) {
|
||||
final valuesList = attr['values'] as List? ?? [];
|
||||
|
||||
final values = valuesList.map((val) {
|
||||
return AttributeValue(
|
||||
name: val['name'] as String,
|
||||
attributeValue: val['attribute_value'] as String,
|
||||
abbr: val['abbr'] as String?,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return AttributeGroup(
|
||||
name: attr['name'] as String,
|
||||
attributeName: attr['attribute_name'] as String? ?? attr['name'] as String,
|
||||
values: values,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return ProductFilterOptions(
|
||||
groups: groups,
|
||||
brands: brands,
|
||||
attributeGroups: attributeGroups,
|
||||
);
|
||||
} catch (e) {
|
||||
print('[ProductFilterOptionsProvider] Error fetching filter options: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'product_filter_options_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Product Filter Options Provider
|
||||
///
|
||||
/// Fetches and combines data from 3 Frappe APIs:
|
||||
/// 1. Product Groups (Item Groups)
|
||||
/// 2. Product Brands
|
||||
/// 3. Product Attributes
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final filterOptionsAsync = ref.watch(productFilterOptionsProvider);
|
||||
///
|
||||
/// filterOptionsAsync.when(
|
||||
/// data: (options) => ProductFilterDrawer(options: options),
|
||||
/// loading: () => CircularProgressIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
|
||||
@ProviderFor(productFilterOptions)
|
||||
const productFilterOptionsProvider = ProductFilterOptionsProvider._();
|
||||
|
||||
/// Product Filter Options Provider
|
||||
///
|
||||
/// Fetches and combines data from 3 Frappe APIs:
|
||||
/// 1. Product Groups (Item Groups)
|
||||
/// 2. Product Brands
|
||||
/// 3. Product Attributes
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final filterOptionsAsync = ref.watch(productFilterOptionsProvider);
|
||||
///
|
||||
/// filterOptionsAsync.when(
|
||||
/// data: (options) => ProductFilterDrawer(options: options),
|
||||
/// loading: () => CircularProgressIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
|
||||
final class ProductFilterOptionsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<ProductFilterOptions>,
|
||||
ProductFilterOptions,
|
||||
FutureOr<ProductFilterOptions>
|
||||
>
|
||||
with
|
||||
$FutureModifier<ProductFilterOptions>,
|
||||
$FutureProvider<ProductFilterOptions> {
|
||||
/// Product Filter Options Provider
|
||||
///
|
||||
/// Fetches and combines data from 3 Frappe APIs:
|
||||
/// 1. Product Groups (Item Groups)
|
||||
/// 2. Product Brands
|
||||
/// 3. Product Attributes
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final filterOptionsAsync = ref.watch(productFilterOptionsProvider);
|
||||
///
|
||||
/// filterOptionsAsync.when(
|
||||
/// data: (options) => ProductFilterDrawer(options: options),
|
||||
/// loading: () => CircularProgressIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
/// ```
|
||||
const ProductFilterOptionsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'productFilterOptionsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$productFilterOptionsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<ProductFilterOptions> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<ProductFilterOptions> create(Ref ref) {
|
||||
return productFilterOptions(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$productFilterOptionsHash() =>
|
||||
r'394f47113bc2afeea8a0a4548df826900884644b';
|
||||
@@ -8,6 +8,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/features/products/presentation/providers/product_filters_provider.dart';
|
||||
import 'package:worker/features/products/presentation/providers/product_filter_options_provider.dart';
|
||||
|
||||
/// Product Filter Drawer Widget
|
||||
///
|
||||
@@ -20,236 +21,230 @@ import 'package:worker/features/products/presentation/providers/product_filters_
|
||||
class ProductFilterDrawer extends ConsumerWidget {
|
||||
const ProductFilterDrawer({super.key});
|
||||
|
||||
// Filter options (from HTML)
|
||||
static const List<FilterOption> _productLines = [
|
||||
FilterOption(value: 'tam-lon', label: 'Tấm lớn'),
|
||||
FilterOption(value: 'third-firing', label: 'Third-Firing'),
|
||||
FilterOption(value: 'outdoor', label: 'Outdoor'),
|
||||
FilterOption(value: 'van-da', label: 'Vân đá'),
|
||||
FilterOption(value: 'xi-mang', label: 'Xi măng'),
|
||||
FilterOption(value: 'van-go', label: 'Vân gỗ'),
|
||||
FilterOption(value: 'xuong-trang', label: 'Xương trắng'),
|
||||
FilterOption(value: 'cam-thach', label: 'Cẩm thạch'),
|
||||
];
|
||||
|
||||
static const List<FilterOption> _spaces = [
|
||||
FilterOption(value: 'phong-khach', label: 'Phòng khách'),
|
||||
FilterOption(value: 'phong-ngu', label: 'Phòng ngủ'),
|
||||
FilterOption(value: 'phong-tam', label: 'Phòng tắm'),
|
||||
FilterOption(value: 'nha-bep', label: 'Nhà bếp'),
|
||||
FilterOption(value: 'khong-gian-khac', label: 'Không gian khác'),
|
||||
];
|
||||
|
||||
static const List<FilterOption> _sizes = [
|
||||
FilterOption(value: '200x1600', label: '200x1600'),
|
||||
FilterOption(value: '1200x2400', label: '1200x2400'),
|
||||
FilterOption(value: '7500x1500', label: '7500x1500'),
|
||||
FilterOption(value: '1200x1200', label: '1200x1200'),
|
||||
FilterOption(value: '600x1200', label: '600x1200'),
|
||||
FilterOption(value: '450x900', label: '450x900'),
|
||||
];
|
||||
|
||||
static const List<FilterOption> _surfaces = [
|
||||
FilterOption(value: 'satin', label: 'SATIN'),
|
||||
FilterOption(value: 'honed', label: 'HONED'),
|
||||
FilterOption(value: 'matt', label: 'MATT'),
|
||||
FilterOption(value: 'polish', label: 'POLISH'),
|
||||
FilterOption(value: 'babyskin', label: 'BABYSKIN'),
|
||||
];
|
||||
|
||||
static const List<FilterOption> _brands = [
|
||||
FilterOption(value: 'eurotile', label: 'Eurotile'),
|
||||
FilterOption(value: 'vasta-stone', label: 'Vasta Stone'),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final filtersState = ref.watch(productFiltersProvider);
|
||||
final filterOptionsAsync = ref.watch(productFilterOptionsProvider);
|
||||
|
||||
return Drawer(
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Header
|
||||
Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: AppColors.grey100, width: 1),
|
||||
child: filterOptionsAsync.when(
|
||||
data: (filterOptions) => Column(
|
||||
children: [
|
||||
// Header
|
||||
Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: AppColors.grey100, width: 1),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Bộ lọc sản phẩm',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
|
||||
// Filter Options (Scrollable)
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Nhóm sản phẩm (Item Groups) - from API
|
||||
_buildFilterGroup(
|
||||
title: 'Nhóm sản phẩm',
|
||||
options: filterOptions.groups,
|
||||
initiallyExpanded: true,
|
||||
selectedValues: filtersState.productLines,
|
||||
onToggle: (value) => ref
|
||||
.read(productFiltersProvider.notifier)
|
||||
.toggleProductLine(value),
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// Thương hiệu (Brands) - from API
|
||||
_buildFilterGroup(
|
||||
title: 'Thương hiệu',
|
||||
options: filterOptions.brands,
|
||||
selectedValues: filtersState.brands,
|
||||
onToggle: (value) => ref
|
||||
.read(productFiltersProvider.notifier)
|
||||
.toggleBrand(value),
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// Attribute Groups (Colour, Size, Surface) - from API
|
||||
...filterOptions.attributeGroups.map((attrGroup) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildAttributeGroup(
|
||||
title: attrGroup.attributeName,
|
||||
attributeGroup: attrGroup,
|
||||
selectedValues: filtersState.sizes, // TODO: Map to correct filter state
|
||||
onToggle: (value) => ref
|
||||
.read(productFiltersProvider.notifier)
|
||||
.toggleSize(value),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
],
|
||||
);
|
||||
}),
|
||||
|
||||
const SizedBox(height: 100), // Space for footer buttons
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Footer Buttons
|
||||
Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(color: AppColors.grey100, width: 1),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Reset Button
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
ref.read(productFiltersProvider.notifier).reset();
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.grey900,
|
||||
side: const BorderSide(
|
||||
color: AppColors.grey100,
|
||||
width: 1,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: AppSpacing.md,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Xóa bộ lọc',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
// Apply Button
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
ref.read(productFiltersProvider.notifier).apply();
|
||||
Navigator.of(context).pop();
|
||||
|
||||
if (filtersState.hasActiveFilters) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Đã áp dụng ${filtersState.totalCount} bộ lọc',
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: AppColors.white,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: AppSpacing.md,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Áp dụng',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primaryBlue),
|
||||
),
|
||||
),
|
||||
error: (error, stack) => Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: AppColors.danger,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
const Text(
|
||||
'Bộ lọc sản phẩm',
|
||||
'Không thể tải bộ lọc',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
color: AppColors.grey500,
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
error.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
ref.invalidate(productFilterOptionsProvider);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: AppColors.white,
|
||||
),
|
||||
child: const Text('Thử lại'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Filter Options (Scrollable)
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Dòng sản phẩm
|
||||
_buildFilterGroup(
|
||||
title: 'Dòng sản phẩm',
|
||||
options: _productLines,
|
||||
selectedValues: filtersState.productLines,
|
||||
onToggle: (value) => ref
|
||||
.read(productFiltersProvider.notifier)
|
||||
.toggleProductLine(value),
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// Không gian
|
||||
_buildFilterGroup(
|
||||
title: 'Không gian',
|
||||
options: _spaces,
|
||||
selectedValues: filtersState.spaces,
|
||||
onToggle: (value) => ref
|
||||
.read(productFiltersProvider.notifier)
|
||||
.toggleSpace(value),
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// Kích thước
|
||||
_buildFilterGroup(
|
||||
title: 'Kích thước',
|
||||
options: _sizes,
|
||||
selectedValues: filtersState.sizes,
|
||||
onToggle: (value) => ref
|
||||
.read(productFiltersProvider.notifier)
|
||||
.toggleSize(value),
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// Bề mặt
|
||||
_buildFilterGroup(
|
||||
title: 'Bề mặt',
|
||||
options: _surfaces,
|
||||
selectedValues: filtersState.surfaces,
|
||||
onToggle: (value) => ref
|
||||
.read(productFiltersProvider.notifier)
|
||||
.toggleSurface(value),
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// Thương hiệu
|
||||
_buildFilterGroup(
|
||||
title: 'Thương hiệu',
|
||||
options: _brands,
|
||||
selectedValues: filtersState.brands,
|
||||
onToggle: (value) => ref
|
||||
.read(productFiltersProvider.notifier)
|
||||
.toggleBrand(value),
|
||||
),
|
||||
|
||||
const SizedBox(height: 100), // Space for footer buttons
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Footer Buttons
|
||||
Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(color: AppColors.grey100, width: 1),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Reset Button
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
ref.read(productFiltersProvider.notifier).reset();
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.grey900,
|
||||
side: const BorderSide(
|
||||
color: AppColors.grey100,
|
||||
width: 1,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: AppSpacing.md,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Xóa bộ lọc',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
// Apply Button
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
ref.read(productFiltersProvider.notifier).apply();
|
||||
Navigator.of(context).pop();
|
||||
|
||||
if (filtersState.hasActiveFilters) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Đã áp dụng ${filtersState.totalCount} bộ lọc',
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: AppColors.white,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: AppSpacing.md,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Áp dụng',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -260,11 +255,18 @@ class ProductFilterDrawer extends ConsumerWidget {
|
||||
required List<FilterOption> options,
|
||||
required Set<String> selectedValues,
|
||||
required Function(String) onToggle,
|
||||
bool initiallyExpanded = false,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
return Theme(
|
||||
data: ThemeData(
|
||||
dividerColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
),
|
||||
child: ExpansionTile(
|
||||
tilePadding: EdgeInsets.zero,
|
||||
childrenPadding: const EdgeInsets.only(left: 8),
|
||||
title: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
@@ -272,40 +274,83 @@ class ProductFilterDrawer extends ConsumerWidget {
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Column(
|
||||
children: options.map((option) {
|
||||
return CheckboxListTile(
|
||||
title: Text(
|
||||
option.label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
initiallyExpanded: initiallyExpanded,
|
||||
children: options.map((option) {
|
||||
return CheckboxListTile(
|
||||
title: Text(
|
||||
option.label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
value: selectedValues.contains(option.value),
|
||||
onChanged: (bool? checked) {
|
||||
onToggle(option.value);
|
||||
},
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
activeColor: AppColors.primaryBlue,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
value: selectedValues.contains(option.value),
|
||||
onChanged: (bool? checked) {
|
||||
onToggle(option.value);
|
||||
},
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
activeColor: AppColors.primaryBlue,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAttributeGroup({
|
||||
required String title,
|
||||
required AttributeGroup attributeGroup,
|
||||
required Set<String> selectedValues,
|
||||
required Function(String) onToggle,
|
||||
}) {
|
||||
return Theme(
|
||||
data: ThemeData(
|
||||
dividerColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
),
|
||||
child: ExpansionTile(
|
||||
tilePadding: EdgeInsets.zero,
|
||||
childrenPadding: const EdgeInsets.only(left: 8),
|
||||
title: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
],
|
||||
initiallyExpanded: false,
|
||||
children: attributeGroup.values.map((value) {
|
||||
return CheckboxListTile(
|
||||
title: Text(
|
||||
value.attributeValue,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
// subtitle: value.abbr != null
|
||||
// ? Text(
|
||||
// 'Mã: ${value.abbr}',
|
||||
// style: const TextStyle(
|
||||
// fontSize: 12,
|
||||
// color: AppColors.grey500,
|
||||
// ),
|
||||
// )
|
||||
// : null,
|
||||
value: selectedValues.contains(value.name),
|
||||
onChanged: (bool? checked) {
|
||||
onToggle(value.name);
|
||||
},
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
activeColor: AppColors.primaryBlue,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter Option Model
|
||||
class FilterOption {
|
||||
final String value;
|
||||
final String label;
|
||||
|
||||
const FilterOption({
|
||||
required this.value,
|
||||
required this.label,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user