fix product search/filter

This commit is contained in:
Phuoc Nguyen
2025-11-19 15:48:51 +07:00
parent 03a7b7940a
commit 841d77d886
19 changed files with 638 additions and 142 deletions

View File

@@ -64,10 +64,7 @@ class ProductsRemoteDataSource {
final response = await _dioClient.post<Map<String, dynamic>>(
url,
data: {
'limit_start': limitStart,
'limit_page_length': limitPageLength,
},
data: {'limit_start': limitStart, 'limit_page_length': limitPageLength},
options: Options(headers: headers),
);
@@ -83,7 +80,9 @@ class ProductsRemoteDataSource {
final productsList = message as List;
return productsList
.map((item) => ProductModel.fromFrappeJson(item as Map<String, dynamic>))
.map(
(item) => ProductModel.fromFrappeJson(item as Map<String, dynamic>),
)
.toList();
} on DioException catch (e) {
if (e.response?.statusCode == 404) {
@@ -125,9 +124,7 @@ class ProductsRemoteDataSource {
final response = await _dioClient.post<Map<String, dynamic>>(
url,
data: {
'name': itemCode,
},
data: {'name': itemCode},
options: Options(headers: headers),
);
@@ -161,25 +158,72 @@ class ProductsRemoteDataSource {
/// Search products
///
/// Searches products by name or description.
/// For now, we fetch all products and filter locally.
/// In the future, the API might support server-side search.
Future<List<ProductModel>> searchProducts(String query) async {
// For now, fetch all products and filter locally
// TODO: Implement server-side search if API supports it
final allProducts = await getAllProducts();
/// Searches products by keyword using Frappe API with pagination support.
/// Uses the search_keyword parameter from the API.
///
/// API endpoint: POST https://land.dbiz.com/api/method/building_material.building_material.api.item.get_list
/// Request body:
/// ```json
/// {
/// "limit_start": 0,
/// "limit_page_length": 12,
/// "search_keyword": "gạch men"
/// }
/// ```
Future<List<ProductModel>> searchProducts(
String query, {
int limitStart = 0,
int limitPageLength = 12,
}) async {
try {
// Get Frappe session headers
final headers = await _frappeAuthService.getHeaders();
final lowercaseQuery = query.toLowerCase();
// Build full API URL
const url =
'${ApiConstants.baseUrl}${ApiConstants.frappeApiMethod}${ApiConstants.frappeGetItems}';
return allProducts.where((product) {
final name = product.name.toLowerCase();
final description = (product.description ?? '').toLowerCase();
final productId = product.productId.toLowerCase();
final response = await _dioClient.post<Map<String, dynamic>>(
url,
data: {
'limit_start': limitStart,
'limit_page_length': limitPageLength,
'search_keyword': query,
},
options: Options(headers: headers),
);
return name.contains(lowercaseQuery) ||
description.contains(lowercaseQuery) ||
productId.contains(lowercaseQuery);
}).toList();
if (response.data == null) {
throw Exception('Empty response from server');
}
// Parse the response
final message = response.data!['message'];
if (message == null) {
throw Exception('No message field in response');
}
final productsList = message as List;
return productsList
.map(
(item) => ProductModel.fromFrappeJson(item as Map<String, dynamic>),
)
.toList();
} on DioException catch (e) {
if (e.response?.statusCode == 404) {
throw Exception('Search endpoint not found');
} else if (e.response?.statusCode == 500) {
throw Exception('Server error while searching products');
} else if (e.type == DioExceptionType.connectionTimeout) {
throw Exception('Connection timeout while searching products');
} else if (e.type == DioExceptionType.receiveTimeout) {
throw Exception('Response timeout while searching products');
} else {
throw Exception('Failed to search products: ${e.message}');
}
} catch (e) {
throw Exception('Unexpected error searching products: $e');
}
}
/// Get products by category
@@ -302,9 +346,7 @@ class ProductsRemoteDataSource {
throw Exception('No message field in response');
}
return (message as List)
.map((item) => item['name'] as String)
.toList();
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');
@@ -368,4 +410,108 @@ class ProductsRemoteDataSource {
throw Exception('Unexpected error fetching product attributes: $e');
}
}
/// Get products with filters (Complete API - Final Version)
///
/// Fetches products with support for all filter combinations:
/// - item_group: List of product groups/categories
/// - brand: List of brands
/// - item_attribute: List of attribute filters (color, size, surface, etc.)
/// - search_keyword: Search query
/// - Pagination support
///
/// API endpoint: POST https://land.dbiz.com/api/method/building_material.building_material.api.item.get_list
/// Request body:
/// ```json
/// {
/// "limit_start": 0,
/// "limit_page_length": 12,
/// "item_group": ["CẨM THẠCH [ Marble ]"],
/// "brand": ["TEST 1"],
/// "item_attribute": [
/// {
/// "attribute": "Màu sắc",
/// "attribute_value": "Nhạt"
/// }
/// ],
/// "search_keyword": "gạch"
/// }
/// ```
Future<List<ProductModel>> getProductsWithFilters({
int limitStart = 0,
int limitPageLength = 12,
List<String>? itemGroups,
List<String>? brands,
List<Map<String, String>>? itemAttributes,
String? searchKeyword,
}) async {
try {
// Get Frappe session headers
final headers = await _frappeAuthService.getHeaders();
// Build full API URL
const url =
'${ApiConstants.baseUrl}${ApiConstants.frappeApiMethod}${ApiConstants.frappeGetItems}';
// Build request data
final Map<String, dynamic> requestData = {
'limit_start': limitStart,
'limit_page_length': limitPageLength,
};
// Add filters only if they have values
if (itemGroups != null && itemGroups.isNotEmpty) {
requestData['item_group'] = itemGroups;
}
if (brands != null && brands.isNotEmpty) {
requestData['brand'] = brands;
}
if (itemAttributes != null && itemAttributes.isNotEmpty) {
requestData['item_attribute'] = itemAttributes;
}
if (searchKeyword != null && searchKeyword.isNotEmpty) {
requestData['search_keyword'] = searchKeyword;
}
final response = await _dioClient.post<Map<String, dynamic>>(
url,
data: requestData,
options: Options(headers: headers),
);
if (response.data == null) {
throw Exception('Empty response from server');
}
// Parse the response
final message = response.data!['message'];
if (message == null) {
throw Exception('No message field in response');
}
final productsList = message as List;
return productsList
.map(
(item) => ProductModel.fromFrappeJson(item as Map<String, dynamic>),
)
.toList();
} on DioException catch (e) {
if (e.response?.statusCode == 404) {
throw Exception('Products endpoint not found');
} else if (e.response?.statusCode == 500) {
throw Exception('Server error while fetching filtered products');
} else if (e.type == DioExceptionType.connectionTimeout) {
throw Exception('Connection timeout while fetching filtered products');
} else if (e.type == DioExceptionType.receiveTimeout) {
throw Exception('Response timeout while fetching filtered products');
} else {
throw Exception('Failed to fetch filtered products: ${e.message}');
}
} catch (e) {
throw Exception('Unexpected error fetching filtered products: $e');
}
}
}

View File

@@ -43,10 +43,18 @@ class ProductsRepositoryImpl implements ProductsRepository {
}
@override
Future<List<Product>> searchProducts(String query) async {
Future<List<Product>> searchProducts(
String query, {
int limitStart = 0,
int limitPageLength = 12,
}) async {
try {
// Search via remote API
final productModels = await remoteDataSource.searchProducts(query);
// Search via remote API with pagination
final productModels = await remoteDataSource.searchProducts(
query,
limitStart: limitStart,
limitPageLength: limitPageLength,
);
return productModels.map((model) => model.toEntity()).toList();
} catch (e) {
print('[ProductsRepository] Error searching products: $e');
@@ -98,4 +106,30 @@ class ProductsRepositoryImpl implements ProductsRepository {
rethrow;
}
}
@override
Future<List<Product>> getProductsWithFilters({
int limitStart = 0,
int limitPageLength = 12,
List<String>? itemGroups,
List<String>? brands,
List<Map<String, String>>? itemAttributes,
String? searchKeyword,
}) async {
try {
// Fetch from Frappe API with all filters and pagination
final productModels = await remoteDataSource.getProductsWithFilters(
limitStart: limitStart,
limitPageLength: limitPageLength,
itemGroups: itemGroups,
brands: brands,
itemAttributes: itemAttributes,
searchKeyword: searchKeyword,
);
return productModels.map((model) => model.toEntity()).toList();
} catch (e) {
print('[ProductsRepository] Error getting filtered products: $e');
rethrow;
}
}
}