# Conflicts:
#	docs/API_RESPONSE_FIX.md
#	docs/AUTH_UI_SUMMARY.md
#	docs/AUTO_LOGIN_DEBUG.md
#	docs/AUTO_LOGIN_FIXED.md
#	docs/BUILD_STATUS.md
#	docs/CLEANUP_COMPLETE.md
#	docs/EXPORT_FILES_SUMMARY.md
#	docs/RIVERPOD_DI_MIGRATION.md
#	docs/TEST_AUTO_LOGIN.md
#	lib/features/categories/data/datasources/category_remote_datasource.dart
#	lib/features/categories/presentation/providers/categories_provider.dart
#	lib/features/categories/presentation/providers/categories_provider.g.dart
#	lib/features/products/data/datasources/product_remote_datasource.dart
#	lib/features/products/data/models/product_model.dart
#	lib/features/products/presentation/pages/products_page.dart
#	lib/features/products/presentation/providers/products_provider.dart
#	lib/features/products/presentation/providers/products_provider.g.dart
This commit is contained in:
2025-10-15 20:55:40 +07:00
39 changed files with 6344 additions and 1714 deletions

View File

@@ -6,6 +6,7 @@ abstract class ProductLocalDataSource {
Future<List<ProductModel>> getAllProducts();
Future<ProductModel?> getProductById(String id);
Future<void> cacheProducts(List<ProductModel> products);
Future<void> updateProduct(ProductModel product);
Future<void> clearProducts();
}
@@ -30,6 +31,11 @@ class ProductLocalDataSourceImpl implements ProductLocalDataSource {
await box.putAll(productMap);
}
@override
Future<void> updateProduct(ProductModel product) async {
await box.put(product.id, product);
}
@override
Future<void> clearProducts() async {
await box.clear();

View File

@@ -1,42 +1,19 @@
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 {
/// Get all products with pagination and filters
/// Returns Map with 'data' (List of ProductModel) and 'meta' (pagination info)
Future<Map<String, dynamic>> getAllProducts({
Future<List<ProductModel>> 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);
/// 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,
);
Future<List<ProductModel>> searchProducts(String query, {int page = 1, int limit = 20});
Future<List<ProductModel>> getProductsByCategory(String categoryId, {int page = 1, int limit = 20});
}
class ProductRemoteDataSourceImpl implements ProductRemoteDataSource {
@@ -45,14 +22,11 @@ class ProductRemoteDataSourceImpl implements ProductRemoteDataSource {
ProductRemoteDataSourceImpl(this.client);
@override
Future<Map<String, dynamic>> getAllProducts({
Future<List<ProductModel>> getAllProducts({
int page = 1,
int limit = 20,
String? categoryId,
String? search,
double? minPrice,
double? maxPrice,
bool? isAvailable,
}) async {
try {
final queryParams = <String, dynamic>{
@@ -60,39 +34,28 @@ class ProductRemoteDataSourceImpl implements ProductRemoteDataSource {
'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;
if (categoryId != null) {
queryParams['categoryId'] = categoryId;
}
if (search != null && search.isNotEmpty) {
queryParams['search'] = search;
}
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',
);
// API returns: { success: true, data: [...products...], meta: {...} }
if (response.data['success'] == true) {
final List<dynamic> data = response.data['data'] ?? [];
return data.map((json) => ProductModel.fromJson(json)).toList();
} else {
throw ServerException(response.data['message'] ?? 'Failed to fetch products');
}
return {
'data': apiResponse.data,
'meta': apiResponse.meta?.toJson() ?? {},
};
} on DioException catch (e) {
throw _handleDioError(e);
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException('Failed to fetch products: $e');
}
}
@@ -102,32 +65,20 @@ class ProductRemoteDataSourceImpl implements ProductRemoteDataSource {
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',
);
// API returns: { success: true, data: {...product...} }
if (response.data['success'] == true) {
return ProductModel.fromJson(response.data['data']);
} else {
throw ServerException(response.data['message'] ?? 'Product not found');
}
return apiResponse.data;
} on DioException catch (e) {
throw _handleDioError(e);
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException('Failed to fetch product: $e');
}
}
@override
Future<Map<String, dynamic>> searchProducts(
String query,
int page,
int limit,
) async {
Future<List<ProductModel>> searchProducts(String query, {int page = 1, int limit = 20}) async {
try {
final response = await client.get(
ApiConstants.searchProducts,
@@ -138,37 +89,21 @@ class ProductRemoteDataSourceImpl implements ProductRemoteDataSource {
},
);
// 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',
);
// API returns: { success: true, data: [...products...], meta: {...} }
if (response.data['success'] == true) {
final List<dynamic> data = response.data['data'] ?? [];
return data.map((json) => ProductModel.fromJson(json)).toList();
} else {
throw ServerException(response.data['message'] ?? 'Failed to search products');
}
return {
'data': apiResponse.data,
'meta': apiResponse.meta?.toJson() ?? {},
};
} on DioException catch (e) {
throw _handleDioError(e);
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException('Failed to search products: $e');
}
}
@override
Future<Map<String, dynamic>> getProductsByCategory(
String categoryId,
int page,
int limit,
) async {
Future<List<ProductModel>> getProductsByCategory(String categoryId, {int page = 1, int limit = 20}) async {
try {
final response = await client.get(
ApiConstants.productsByCategory(categoryId),
@@ -178,65 +113,16 @@ class ProductRemoteDataSourceImpl implements ProductRemoteDataSource {
},
);
// 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',
);
// API returns: { success: true, data: [...products...], meta: {...} }
if (response.data['success'] == true) {
final List<dynamic> data = response.data['data'] ?? [];
return data.map((json) => ProductModel.fromJson(json)).toList();
} else {
throw ServerException(response.data['message'] ?? 'Failed to fetch products by category');
}
return {
'data': apiResponse.data,
'meta': apiResponse.meta?.toJson() ?? {},
};
} on DioException catch (e) {
throw _handleDioError(e);
} catch (e) {
if (e is ServerException) rethrow;
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,
this.description,
required this.description,
required this.price,
this.imageUrl,
required this.categoryId,
@@ -83,17 +83,11 @@ 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: price,
description: json['description'] as String? ?? '',
price: (json['price'] as num).toDouble(),
imageUrl: json['imageUrl'] as String?,
categoryId: json['categoryId'] as String,
stockQuantity: json['stockQuantity'] as int? ?? 0,
@@ -101,7 +95,6 @@ class ProductModel extends HiveObject {
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

@@ -0,0 +1,43 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:hive_ce/hive.dart';
import '../datasources/product_local_datasource.dart';
import '../datasources/product_remote_datasource.dart';
import '../repositories/product_repository_impl.dart';
import '../models/product_model.dart';
import '../../domain/repositories/product_repository.dart';
import '../../../../core/providers/providers.dart';
import '../../../../core/constants/storage_constants.dart';
part 'product_providers.g.dart';
/// Provider for product Hive box
@riverpod
Box<ProductModel> productBox(Ref ref) {
return Hive.box<ProductModel>(StorageConstants.productsBox);
}
/// Provider for product local data source
@riverpod
ProductLocalDataSource productLocalDataSource(Ref ref) {
final box = ref.watch(productBoxProvider);
return ProductLocalDataSourceImpl(box);
}
/// Provider for product remote data source
@riverpod
ProductRemoteDataSource productRemoteDataSource(Ref ref) {
final dioClient = ref.watch(dioClientProvider);
return ProductRemoteDataSourceImpl(dioClient);
}
/// Provider for product repository
@riverpod
ProductRepository productRepository(Ref ref) {
final localDataSource = ref.watch(productLocalDataSourceProvider);
final remoteDataSource = ref.watch(productRemoteDataSourceProvider);
return ProductRepositoryImpl(
localDataSource: localDataSource,
remoteDataSource: remoteDataSource,
);
}

View File

@@ -0,0 +1,219 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'product_providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Provider for product Hive box
@ProviderFor(productBox)
const productBoxProvider = ProductBoxProvider._();
/// Provider for product Hive box
final class ProductBoxProvider
extends
$FunctionalProvider<
Box<ProductModel>,
Box<ProductModel>,
Box<ProductModel>
>
with $Provider<Box<ProductModel>> {
/// Provider for product Hive box
const ProductBoxProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'productBoxProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$productBoxHash();
@$internal
@override
$ProviderElement<Box<ProductModel>> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
Box<ProductModel> create(Ref ref) {
return productBox(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Box<ProductModel> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Box<ProductModel>>(value),
);
}
}
String _$productBoxHash() => r'68cd21ea28cfc716f34daef17849a0393cdb2b80';
/// Provider for product local data source
@ProviderFor(productLocalDataSource)
const productLocalDataSourceProvider = ProductLocalDataSourceProvider._();
/// Provider for product local data source
final class ProductLocalDataSourceProvider
extends
$FunctionalProvider<
ProductLocalDataSource,
ProductLocalDataSource,
ProductLocalDataSource
>
with $Provider<ProductLocalDataSource> {
/// Provider for product local data source
const ProductLocalDataSourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'productLocalDataSourceProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$productLocalDataSourceHash();
@$internal
@override
$ProviderElement<ProductLocalDataSource> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
ProductLocalDataSource create(Ref ref) {
return productLocalDataSource(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ProductLocalDataSource value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ProductLocalDataSource>(value),
);
}
}
String _$productLocalDataSourceHash() =>
r'ef4673055777e8dc8a8419a80548b319789d99f9';
/// Provider for product remote data source
@ProviderFor(productRemoteDataSource)
const productRemoteDataSourceProvider = ProductRemoteDataSourceProvider._();
/// Provider for product remote data source
final class ProductRemoteDataSourceProvider
extends
$FunctionalProvider<
ProductRemoteDataSource,
ProductRemoteDataSource,
ProductRemoteDataSource
>
with $Provider<ProductRemoteDataSource> {
/// Provider for product remote data source
const ProductRemoteDataSourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'productRemoteDataSourceProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$productRemoteDataSourceHash();
@$internal
@override
$ProviderElement<ProductRemoteDataSource> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
ProductRemoteDataSource create(Ref ref) {
return productRemoteDataSource(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ProductRemoteDataSource value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ProductRemoteDataSource>(value),
);
}
}
String _$productRemoteDataSourceHash() =>
r'954798907bb0c9baade27b84eaba612a5dec8f68';
/// Provider for product repository
@ProviderFor(productRepository)
const productRepositoryProvider = ProductRepositoryProvider._();
/// Provider for product repository
final class ProductRepositoryProvider
extends
$FunctionalProvider<
ProductRepository,
ProductRepository,
ProductRepository
>
with $Provider<ProductRepository> {
/// Provider for product repository
const ProductRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'productRepositoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$productRepositoryHash();
@$internal
@override
$ProviderElement<ProductRepository> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
ProductRepository create(Ref ref) {
return productRepository(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ProductRepository value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ProductRepository>(value),
);
}
}
String _$productRepositoryHash() => r'7c5c5b274ce459add6449c29be822ea04503d3dc';