This commit is contained in:
2025-10-10 22:49:05 +07:00
parent 02941e2234
commit 38c16bf0b9
49 changed files with 2702 additions and 740 deletions

View File

@@ -1,12 +1,42 @@
import 'package:dio/dio.dart';
import '../models/product_model.dart';
import '../../../../core/network/dio_client.dart';
import '../../../../core/network/api_response.dart';
import '../../../../core/constants/api_constants.dart';
import '../../../../core/errors/exceptions.dart';
/// Product remote data source using API
abstract class ProductRemoteDataSource {
Future<List<ProductModel>> getAllProducts();
/// Get all products with pagination and filters
/// Returns Map with 'data' (List of ProductModel) and 'meta' (pagination info)
Future<Map<String, dynamic>> getAllProducts({
int page = 1,
int limit = 20,
String? categoryId,
String? search,
double? minPrice,
double? maxPrice,
bool? isAvailable,
});
/// Get single product by ID
Future<ProductModel> getProductById(String id);
Future<List<ProductModel>> searchProducts(String query);
/// Search products by query with pagination
/// Returns Map with 'data' (List of ProductModel) and 'meta' (pagination info)
Future<Map<String, dynamic>> searchProducts(
String query,
int page,
int limit,
);
/// Get products by category with pagination
/// Returns Map with 'data' (List of ProductModel) and 'meta' (pagination info)
Future<Map<String, dynamic>> getProductsByCategory(
String categoryId,
int page,
int limit,
);
}
class ProductRemoteDataSourceImpl implements ProductRemoteDataSource {
@@ -15,25 +45,198 @@ class ProductRemoteDataSourceImpl implements ProductRemoteDataSource {
ProductRemoteDataSourceImpl(this.client);
@override
Future<List<ProductModel>> getAllProducts() async {
final response = await client.get(ApiConstants.products);
final List<dynamic> data = response.data['products'] ?? [];
return data.map((json) => ProductModel.fromJson(json)).toList();
Future<Map<String, dynamic>> getAllProducts({
int page = 1,
int limit = 20,
String? categoryId,
String? search,
double? minPrice,
double? maxPrice,
bool? isAvailable,
}) async {
try {
final queryParams = <String, dynamic>{
'page': page,
'limit': limit,
};
// Add optional filters
if (categoryId != null) queryParams['categoryId'] = categoryId;
if (search != null) queryParams['search'] = search;
if (minPrice != null) queryParams['minPrice'] = minPrice;
if (maxPrice != null) queryParams['maxPrice'] = maxPrice;
if (isAvailable != null) queryParams['isAvailable'] = isAvailable;
final response = await client.get(
ApiConstants.products,
queryParameters: queryParams,
);
// Parse API response using ApiResponse model
final apiResponse = ApiResponse<List<ProductModel>>.fromJson(
response.data as Map<String, dynamic>,
(data) => (data as List<dynamic>)
.map((json) => ProductModel.fromJson(json as Map<String, dynamic>))
.toList(),
);
if (!apiResponse.success) {
throw ServerException(
apiResponse.message ?? 'Failed to fetch products',
);
}
return {
'data': apiResponse.data,
'meta': apiResponse.meta?.toJson() ?? {},
};
} on DioException catch (e) {
throw _handleDioError(e);
} catch (e) {
throw ServerException('Failed to fetch products: $e');
}
}
@override
Future<ProductModel> getProductById(String id) async {
final response = await client.get(ApiConstants.productById(id));
return ProductModel.fromJson(response.data);
try {
final response = await client.get(ApiConstants.productById(id));
// Parse API response using ApiResponse model
final apiResponse = ApiResponse<ProductModel>.fromJson(
response.data as Map<String, dynamic>,
(data) => ProductModel.fromJson(data as Map<String, dynamic>),
);
if (!apiResponse.success) {
throw ServerException(
apiResponse.message ?? 'Failed to fetch product',
);
}
return apiResponse.data;
} on DioException catch (e) {
throw _handleDioError(e);
} catch (e) {
throw ServerException('Failed to fetch product: $e');
}
}
@override
Future<List<ProductModel>> searchProducts(String query) async {
final response = await client.get(
ApiConstants.searchProducts,
queryParameters: {'q': query},
);
final List<dynamic> data = response.data['products'] ?? [];
return data.map((json) => ProductModel.fromJson(json)).toList();
Future<Map<String, dynamic>> searchProducts(
String query,
int page,
int limit,
) async {
try {
final response = await client.get(
ApiConstants.searchProducts,
queryParameters: {
'q': query,
'page': page,
'limit': limit,
},
);
// Parse API response using ApiResponse model
final apiResponse = ApiResponse<List<ProductModel>>.fromJson(
response.data as Map<String, dynamic>,
(data) => (data as List<dynamic>)
.map((json) => ProductModel.fromJson(json as Map<String, dynamic>))
.toList(),
);
if (!apiResponse.success) {
throw ServerException(
apiResponse.message ?? 'Failed to search products',
);
}
return {
'data': apiResponse.data,
'meta': apiResponse.meta?.toJson() ?? {},
};
} on DioException catch (e) {
throw _handleDioError(e);
} catch (e) {
throw ServerException('Failed to search products: $e');
}
}
@override
Future<Map<String, dynamic>> getProductsByCategory(
String categoryId,
int page,
int limit,
) async {
try {
final response = await client.get(
ApiConstants.productsByCategory(categoryId),
queryParameters: {
'page': page,
'limit': limit,
},
);
// Parse API response using ApiResponse model
final apiResponse = ApiResponse<List<ProductModel>>.fromJson(
response.data as Map<String, dynamic>,
(data) => (data as List<dynamic>)
.map((json) => ProductModel.fromJson(json as Map<String, dynamic>))
.toList(),
);
if (!apiResponse.success) {
throw ServerException(
apiResponse.message ?? 'Failed to fetch products by category',
);
}
return {
'data': apiResponse.data,
'meta': apiResponse.meta?.toJson() ?? {},
};
} on DioException catch (e) {
throw _handleDioError(e);
} catch (e) {
throw ServerException('Failed to fetch products by category: $e');
}
}
/// Handle Dio errors and convert to custom exceptions
Exception _handleDioError(DioException error) {
switch (error.response?.statusCode) {
case ApiConstants.statusBadRequest:
return ValidationException(
error.response?.data['message'] ?? 'Invalid request',
);
case ApiConstants.statusUnauthorized:
return UnauthorizedException(
error.response?.data['message'] ?? 'Unauthorized access',
);
case ApiConstants.statusForbidden:
return UnauthorizedException(
error.response?.data['message'] ?? 'Access forbidden',
);
case ApiConstants.statusNotFound:
return NotFoundException(
error.response?.data['message'] ?? 'Product not found',
);
case ApiConstants.statusInternalServerError:
case ApiConstants.statusBadGateway:
case ApiConstants.statusServiceUnavailable:
return ServerException(
error.response?.data['message'] ?? 'Server error',
);
default:
if (error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.receiveTimeout ||
error.type == DioExceptionType.sendTimeout) {
return NetworkException('Connection timeout');
} else if (error.type == DioExceptionType.connectionError) {
return NetworkException('No internet connection');
}
return ServerException('Unexpected error occurred');
}
}
}

View File

@@ -13,7 +13,7 @@ class ProductModel extends HiveObject {
final String name;
@HiveField(2)
final String description;
final String? description;
@HiveField(3)
final double price;
@@ -39,7 +39,7 @@ class ProductModel extends HiveObject {
ProductModel({
required this.id,
required this.name,
required this.description,
this.description,
required this.price,
this.imageUrl,
required this.categoryId,
@@ -83,18 +83,25 @@ class ProductModel extends HiveObject {
/// Create from JSON
factory ProductModel.fromJson(Map<String, dynamic> json) {
// Handle price as string or number from API
final priceValue = json['price'];
final price = priceValue is String
? double.parse(priceValue)
: (priceValue as num).toDouble();
return ProductModel(
id: json['id'] as String,
name: json['name'] as String,
description: json['description'] as String,
price: (json['price'] as num).toDouble(),
description: json['description'] as String?,
price: price,
imageUrl: json['imageUrl'] as String?,
categoryId: json['categoryId'] as String,
stockQuantity: json['stockQuantity'] as int,
isAvailable: json['isAvailable'] as bool,
stockQuantity: json['stockQuantity'] as int? ?? 0,
isAvailable: json['isAvailable'] as bool? ?? true,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
);
// Note: Nested 'category' object is ignored as we only need categoryId
}
/// Convert to JSON

View File

@@ -19,7 +19,7 @@ class ProductModelAdapter extends TypeAdapter<ProductModel> {
return ProductModel(
id: fields[0] as String,
name: fields[1] as String,
description: fields[2] as String,
description: fields[2] as String?,
price: (fields[3] as num).toDouble(),
imageUrl: fields[4] as String?,
categoryId: fields[5] as String,

View File

@@ -3,6 +3,7 @@ import '../../domain/entities/product.dart';
import '../../domain/repositories/product_repository.dart';
import '../datasources/product_local_datasource.dart';
import '../datasources/product_remote_datasource.dart';
import '../models/product_model.dart';
import '../../../../core/errors/failures.dart';
import '../../../../core/errors/exceptions.dart';
@@ -40,10 +41,11 @@ class ProductRepositoryImpl implements ProductRepository {
Future<Either<Failure, List<Product>>> searchProducts(String query) async {
try {
final allProducts = await localDataSource.getAllProducts();
final filtered = allProducts.where((p) =>
p.name.toLowerCase().contains(query.toLowerCase()) ||
p.description.toLowerCase().contains(query.toLowerCase())
).toList();
final filtered = allProducts.where((p) {
final nameMatch = p.name.toLowerCase().contains(query.toLowerCase());
final descMatch = p.description?.toLowerCase().contains(query.toLowerCase()) ?? false;
return nameMatch || descMatch;
}).toList();
return Right(filtered.map((model) => model.toEntity()).toList());
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
@@ -66,9 +68,14 @@ class ProductRepositoryImpl implements ProductRepository {
@override
Future<Either<Failure, List<Product>>> syncProducts() async {
try {
final products = await remoteDataSource.getAllProducts();
final response = await remoteDataSource.getAllProducts();
final productsData = response['data'] as List<dynamic>;
final products = productsData
.map((json) => ProductModel.fromJson(json as Map<String, dynamic>))
.toList();
await localDataSource.cacheProducts(products);
return Right(products.map((model) => model.toEntity()).toList());
final entities = products.map((model) => model.toEntity()).toList();
return Right(entities);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on NetworkException catch (e) {