fix filter
This commit is contained in:
@@ -8,7 +8,7 @@ curl --location 'https://land.dbiz.com//api/method/building_material.building_ma
|
|||||||
"limit_page_length": 0
|
"limit_page_length": 0
|
||||||
}'
|
}'
|
||||||
|
|
||||||
get attribute list
|
get product attribute list
|
||||||
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.item_attribute.get_list' \
|
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.item_attribute.get_list' \
|
||||||
--header 'X-Frappe-Csrf-Token: 13c271e0e58dcad9bcc0053cad0057540eb0675bb7052c2cc1a815b2' \
|
--header 'X-Frappe-Csrf-Token: 13c271e0e58dcad9bcc0053cad0057540eb0675bb7052c2cc1a815b2' \
|
||||||
--header 'Cookie: sid=d9ddd3862832f12901ef4c0d77d6891cd08ef851a254b7d56c857724; full_name=Ha%20Duy%20Lam; sid=d9ddd3862832f12901ef4c0d77d6891cd08ef851a254b7d56c857724; system_user=yes; user_id=lamhd%40gmail.com; user_image=' \
|
--header 'Cookie: sid=d9ddd3862832f12901ef4c0d77d6891cd08ef851a254b7d56c857724; full_name=Ha%20Duy%20Lam; sid=d9ddd3862832f12901ef4c0d77d6891cd08ef851a254b7d56c857724; system_user=yes; user_id=lamhd%40gmail.com; user_image=' \
|
||||||
@@ -18,8 +18,7 @@ curl --location 'https://land.dbiz.com//api/method/building_material.building_ma
|
|||||||
"limit_page_length": 0
|
"limit_page_length": 0
|
||||||
}'
|
}'
|
||||||
|
|
||||||
get brand
|
get product brand
|
||||||
|
|
||||||
curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \
|
curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \
|
||||||
--header 'X-Frappe-Csrf-Token: 52e3deff2accdc4d990312508dff6be0ecae61e01da837f00b2bfae9' \
|
--header 'X-Frappe-Csrf-Token: 52e3deff2accdc4d990312508dff6be0ecae61e01da837f00b2bfae9' \
|
||||||
--header 'Cookie: sid=723d7a4c28209a1c5451d2dce1f7232c04addb2e040a273f3a56ea77; full_name=PublicAPI; sid=723d7a4c28209a1c5451d2dce1f7232c04addb2e040a273f3a56ea77; system_user=no; user_id=public_api%40dbiz.com; user_image=' \
|
--header 'Cookie: sid=723d7a4c28209a1c5451d2dce1f7232c04addb2e040a273f3a56ea77; full_name=PublicAPI; sid=723d7a4c28209a1c5451d2dce1f7232c04addb2e040a273f3a56ea77; system_user=no; user_id=public_api%40dbiz.com; user_image=' \
|
||||||
@@ -30,6 +29,18 @@ curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \
|
|||||||
"limit_page_length": 0
|
"limit_page_length": 0
|
||||||
}'
|
}'
|
||||||
|
|
||||||
|
get product group
|
||||||
|
curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \
|
||||||
|
--header 'X-Frappe-Csrf-Token: 52e3deff2accdc4d990312508dff6be0ecae61e01da837f00b2bfae9' \
|
||||||
|
--header 'Cookie: sid=723d7a4c28209a1c5451d2dce1f7232c04addb2e040a273f3a56ea77; full_name=PublicAPI; sid=723d7a4c28209a1c5451d2dce1f7232c04addb2e040a273f3a56ea77; system_user=no; user_id=public_api%40dbiz.com; user_image=' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"doctype": "Item Group",
|
||||||
|
"fields": ["item_group_name","name"],
|
||||||
|
"filters": {"is_group": 0},
|
||||||
|
"limit_page_length": 0
|
||||||
|
}'
|
||||||
|
|
||||||
get product detail
|
get product detail
|
||||||
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.item.get_detail' \
|
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.item.get_detail' \
|
||||||
--header 'X-Frappe-Csrf-Token: 4989ff095956a891bbae0944a1483097b6eb06f1080961f7164a7e17' \
|
--header 'X-Frappe-Csrf-Token: 4989ff095956a891bbae0944a1483097b6eb06f1080961f7164a7e17' \
|
||||||
|
|||||||
@@ -200,4 +200,165 @@ class ProductsRemoteDataSource {
|
|||||||
.where((product) => product.category == categoryId)
|
.where((product) => product.category == categoryId)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get product groups (Item Groups)
|
||||||
|
///
|
||||||
|
/// Fetches product groups from Frappe ERPNext.
|
||||||
|
/// Returns a list of group objects with name and item_group_name.
|
||||||
|
///
|
||||||
|
/// API endpoint: POST https://land.dbiz.com/api/method/frappe.client.get_list
|
||||||
|
/// Request body:
|
||||||
|
/// ```json
|
||||||
|
/// {
|
||||||
|
/// "doctype": "Item Group",
|
||||||
|
/// "fields": ["item_group_name", "name"],
|
||||||
|
/// "filters": {"is_group": 0},
|
||||||
|
/// "limit_page_length": 0
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
Future<List<Map<String, dynamic>>> getProductGroups() async {
|
||||||
|
try {
|
||||||
|
final headers = await _frappeAuthService.getHeaders();
|
||||||
|
final url =
|
||||||
|
'${ApiConstants.baseUrl}${ApiConstants.frappeApiMethod}${ApiConstants.frappeGetList}';
|
||||||
|
|
||||||
|
final response = await _dioClient.post<Map<String, dynamic>>(
|
||||||
|
url,
|
||||||
|
data: {
|
||||||
|
'doctype': 'Item Group',
|
||||||
|
'fields': ['item_group_name', 'name'],
|
||||||
|
'filters': {'is_group': 0},
|
||||||
|
'limit_page_length': 0,
|
||||||
|
},
|
||||||
|
options: Options(headers: headers),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data == null) {
|
||||||
|
throw Exception('Empty response from server');
|
||||||
|
}
|
||||||
|
|
||||||
|
final message = response.data!['message'];
|
||||||
|
if (message == null) {
|
||||||
|
throw Exception('No message field in response');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (message as List).cast<Map<String, dynamic>>();
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (e.response?.statusCode == 404) {
|
||||||
|
throw Exception('Product groups endpoint not found');
|
||||||
|
} else if (e.response?.statusCode == 500) {
|
||||||
|
throw Exception('Server error while fetching product groups');
|
||||||
|
} else {
|
||||||
|
throw Exception('Failed to fetch product groups: ${e.message}');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Unexpected error fetching product groups: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get product brands
|
||||||
|
///
|
||||||
|
/// Fetches brands from Frappe ERPNext.
|
||||||
|
/// Returns a list of brand names.
|
||||||
|
///
|
||||||
|
/// API endpoint: POST https://land.dbiz.com/api/method/frappe.client.get_list
|
||||||
|
/// Request body:
|
||||||
|
/// ```json
|
||||||
|
/// {
|
||||||
|
/// "doctype": "Brand",
|
||||||
|
/// "fields": ["name"],
|
||||||
|
/// "limit_page_length": 0
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
Future<List<String>> getProductBrands() async {
|
||||||
|
try {
|
||||||
|
final headers = await _frappeAuthService.getHeaders();
|
||||||
|
final url =
|
||||||
|
'${ApiConstants.baseUrl}${ApiConstants.frappeApiMethod}${ApiConstants.frappeGetList}';
|
||||||
|
|
||||||
|
final response = await _dioClient.post<Map<String, dynamic>>(
|
||||||
|
url,
|
||||||
|
data: {
|
||||||
|
'doctype': 'Brand',
|
||||||
|
'fields': ['name'],
|
||||||
|
'limit_page_length': 0,
|
||||||
|
},
|
||||||
|
options: Options(headers: headers),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data == null) {
|
||||||
|
throw Exception('Empty response from server');
|
||||||
|
}
|
||||||
|
|
||||||
|
final message = response.data!['message'];
|
||||||
|
if (message == null) {
|
||||||
|
throw Exception('No message field in response');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (message as List)
|
||||||
|
.map((item) => item['name'] as String)
|
||||||
|
.toList();
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (e.response?.statusCode == 404) {
|
||||||
|
throw Exception('Product brands endpoint not found');
|
||||||
|
} else if (e.response?.statusCode == 500) {
|
||||||
|
throw Exception('Server error while fetching product brands');
|
||||||
|
} else {
|
||||||
|
throw Exception('Failed to fetch product brands: ${e.message}');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Unexpected error fetching product brands: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get product attributes
|
||||||
|
///
|
||||||
|
/// Fetches product attributes from Frappe ERPNext.
|
||||||
|
/// Returns a list of attribute objects.
|
||||||
|
///
|
||||||
|
/// API endpoint: POST https://land.dbiz.com/api/method/building_material.building_material.api.item_attribute.get_list
|
||||||
|
/// Request body:
|
||||||
|
/// ```json
|
||||||
|
/// {
|
||||||
|
/// "filters": {"is_group": 0},
|
||||||
|
/// "limit_page_length": 0
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
Future<List<Map<String, dynamic>>> getProductAttributes() async {
|
||||||
|
try {
|
||||||
|
final headers = await _frappeAuthService.getHeaders();
|
||||||
|
final url =
|
||||||
|
'${ApiConstants.baseUrl}${ApiConstants.frappeApiMethod}${ApiConstants.frappeGetItemAttributes}';
|
||||||
|
|
||||||
|
final response = await _dioClient.post<Map<String, dynamic>>(
|
||||||
|
url,
|
||||||
|
data: {
|
||||||
|
'filters': {'is_group': 0},
|
||||||
|
'limit_page_length': 0,
|
||||||
|
},
|
||||||
|
options: Options(headers: headers),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data == null) {
|
||||||
|
throw Exception('Empty response from server');
|
||||||
|
}
|
||||||
|
|
||||||
|
final message = response.data!['message'];
|
||||||
|
if (message == null) {
|
||||||
|
throw Exception('No message field in response');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (message as List).cast<Map<String, dynamic>>();
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (e.response?.statusCode == 404) {
|
||||||
|
throw Exception('Product attributes endpoint not found');
|
||||||
|
} else if (e.response?.statusCode == 500) {
|
||||||
|
throw Exception('Server error while fetching product attributes');
|
||||||
|
} else {
|
||||||
|
throw Exception('Failed to fetch product attributes: ${e.message}');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Unexpected error fetching product attributes: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/constants/ui_constants.dart';
|
||||||
import 'package:worker/core/theme/colors.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_filters_provider.dart';
|
||||||
|
import 'package:worker/features/products/presentation/providers/product_filter_options_provider.dart';
|
||||||
|
|
||||||
/// Product Filter Drawer Widget
|
/// Product Filter Drawer Widget
|
||||||
///
|
///
|
||||||
@@ -20,236 +21,230 @@ import 'package:worker/features/products/presentation/providers/product_filters_
|
|||||||
class ProductFilterDrawer extends ConsumerWidget {
|
class ProductFilterDrawer extends ConsumerWidget {
|
||||||
const ProductFilterDrawer({super.key});
|
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
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final filtersState = ref.watch(productFiltersProvider);
|
final filtersState = ref.watch(productFiltersProvider);
|
||||||
|
final filterOptionsAsync = ref.watch(productFilterOptionsProvider);
|
||||||
|
|
||||||
return Drawer(
|
return Drawer(
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: filterOptionsAsync.when(
|
||||||
children: [
|
data: (filterOptions) => Column(
|
||||||
// Header
|
children: [
|
||||||
Container(
|
// Header
|
||||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
Container(
|
||||||
decoration: const BoxDecoration(
|
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||||
border: Border(
|
decoration: const BoxDecoration(
|
||||||
bottom: BorderSide(color: AppColors.grey100, width: 1),
|
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: [
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
size: 48,
|
||||||
|
color: AppColors.danger,
|
||||||
|
),
|
||||||
|
const SizedBox(height: AppSpacing.md),
|
||||||
const Text(
|
const Text(
|
||||||
'Bộ lọc sản phẩm',
|
'Không thể tải bộ lọc',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: AppColors.grey900,
|
color: AppColors.grey900,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
const SizedBox(height: AppSpacing.sm),
|
||||||
icon: const Icon(Icons.close),
|
Text(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
error.toString(),
|
||||||
color: AppColors.grey500,
|
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 List<FilterOption> options,
|
||||||
required Set<String> selectedValues,
|
required Set<String> selectedValues,
|
||||||
required Function(String) onToggle,
|
required Function(String) onToggle,
|
||||||
|
bool initiallyExpanded = false,
|
||||||
}) {
|
}) {
|
||||||
return Column(
|
return Theme(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
data: ThemeData(
|
||||||
children: [
|
dividerColor: Colors.transparent,
|
||||||
Text(
|
splashColor: Colors.transparent,
|
||||||
|
highlightColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
child: ExpansionTile(
|
||||||
|
tilePadding: EdgeInsets.zero,
|
||||||
|
childrenPadding: const EdgeInsets.only(left: 8),
|
||||||
|
title: Text(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@@ -272,40 +274,83 @@ class ProductFilterDrawer extends ConsumerWidget {
|
|||||||
color: AppColors.grey900,
|
color: AppColors.grey900,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
initiallyExpanded: initiallyExpanded,
|
||||||
Column(
|
children: options.map((option) {
|
||||||
children: options.map((option) {
|
return CheckboxListTile(
|
||||||
return CheckboxListTile(
|
title: Text(
|
||||||
title: Text(
|
option.label,
|
||||||
option.label,
|
style: const TextStyle(
|
||||||
style: const TextStyle(
|
fontSize: 14,
|
||||||
fontSize: 14,
|
color: AppColors.grey900,
|
||||||
color: AppColors.grey900,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
value: selectedValues.contains(option.value),
|
),
|
||||||
onChanged: (bool? checked) {
|
value: selectedValues.contains(option.value),
|
||||||
onToggle(option.value);
|
onChanged: (bool? checked) {
|
||||||
},
|
onToggle(option.value);
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
},
|
||||||
contentPadding: EdgeInsets.zero,
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
dense: true,
|
contentPadding: EdgeInsets.zero,
|
||||||
activeColor: AppColors.primaryBlue,
|
dense: true,
|
||||||
);
|
activeColor: AppColors.primaryBlue,
|
||||||
}).toList(),
|
);
|
||||||
|
}).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