Merge branch 'main' of https://git.renolation.com/renolation/retail
# 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:
@@ -1,25 +1,12 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../models/category_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';
|
||||
|
||||
/// Category remote data source using API
|
||||
abstract class CategoryRemoteDataSource {
|
||||
/// Get all categories (public endpoint - no auth required)
|
||||
Future<List<CategoryModel>> getAllCategories();
|
||||
|
||||
/// Get single category by ID (public endpoint - no auth required)
|
||||
Future<CategoryModel> getCategoryById(String id);
|
||||
|
||||
/// Get category with its products with pagination (public endpoint)
|
||||
/// Returns Map with 'category' and 'products' with pagination info
|
||||
Future<Map<String, dynamic>> getCategoryWithProducts(
|
||||
String id,
|
||||
int page,
|
||||
int limit,
|
||||
);
|
||||
}
|
||||
|
||||
class CategoryRemoteDataSourceImpl implements CategoryRemoteDataSource {
|
||||
@@ -32,24 +19,15 @@ class CategoryRemoteDataSourceImpl implements CategoryRemoteDataSource {
|
||||
try {
|
||||
final response = await client.get(ApiConstants.categories);
|
||||
|
||||
// Parse API response using ApiResponse model
|
||||
final apiResponse = ApiResponse<List<CategoryModel>>.fromJson(
|
||||
response.data as Map<String, dynamic>,
|
||||
(data) => (data as List<dynamic>)
|
||||
.map((json) => CategoryModel.fromJson(json as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
if (!apiResponse.success) {
|
||||
throw ServerException(
|
||||
apiResponse.message ?? 'Failed to fetch categories',
|
||||
);
|
||||
// API returns: { success: true, data: [...categories...] }
|
||||
if (response.data['success'] == true) {
|
||||
final List<dynamic> data = response.data['data'] ?? [];
|
||||
return data.map((json) => CategoryModel.fromJson(json)).toList();
|
||||
} else {
|
||||
throw ServerException(response.data['message'] ?? 'Failed to fetch categories');
|
||||
}
|
||||
|
||||
return apiResponse.data;
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioError(e);
|
||||
} catch (e) {
|
||||
if (e is ServerException) rethrow;
|
||||
throw ServerException('Failed to fetch categories: $e');
|
||||
}
|
||||
}
|
||||
@@ -59,108 +37,15 @@ class CategoryRemoteDataSourceImpl implements CategoryRemoteDataSource {
|
||||
try {
|
||||
final response = await client.get(ApiConstants.categoryById(id));
|
||||
|
||||
// Parse API response using ApiResponse model
|
||||
final apiResponse = ApiResponse<CategoryModel>.fromJson(
|
||||
response.data as Map<String, dynamic>,
|
||||
(data) => CategoryModel.fromJson(data as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
if (!apiResponse.success) {
|
||||
throw ServerException(
|
||||
apiResponse.message ?? 'Failed to fetch category',
|
||||
);
|
||||
// API returns: { success: true, data: {...category...} }
|
||||
if (response.data['success'] == true) {
|
||||
return CategoryModel.fromJson(response.data['data']);
|
||||
} else {
|
||||
throw ServerException(response.data['message'] ?? 'Category not found');
|
||||
}
|
||||
|
||||
return apiResponse.data;
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioError(e);
|
||||
} catch (e) {
|
||||
if (e is ServerException) rethrow;
|
||||
throw ServerException('Failed to fetch category: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> getCategoryWithProducts(
|
||||
String id,
|
||||
int page,
|
||||
int limit,
|
||||
) async {
|
||||
try {
|
||||
final response = await client.get(
|
||||
'${ApiConstants.categories}/$id/products',
|
||||
queryParameters: {
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
},
|
||||
);
|
||||
|
||||
// Parse API response - data contains category with nested products
|
||||
final apiResponse = ApiResponse<Map<String, dynamic>>.fromJson(
|
||||
response.data as Map<String, dynamic>,
|
||||
(data) => data as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
if (!apiResponse.success) {
|
||||
throw ServerException(
|
||||
apiResponse.message ?? 'Failed to fetch category with products',
|
||||
);
|
||||
}
|
||||
|
||||
final responseData = apiResponse.data;
|
||||
|
||||
// Extract category info (excluding products array)
|
||||
final categoryData = Map<String, dynamic>.from(responseData);
|
||||
final products = categoryData.remove('products') as List<dynamic>? ?? [];
|
||||
|
||||
// Create category model from remaining data
|
||||
final category = CategoryModel.fromJson(categoryData);
|
||||
|
||||
return {
|
||||
'category': category,
|
||||
'products': products,
|
||||
'meta': apiResponse.meta?.toJson() ?? {},
|
||||
};
|
||||
} on DioException catch (e) {
|
||||
throw _handleDioError(e);
|
||||
} catch (e) {
|
||||
throw ServerException('Failed to fetch category with products: $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'] ?? 'Category 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '../datasources/category_local_datasource.dart';
|
||||
import '../datasources/category_remote_datasource.dart';
|
||||
import '../repositories/category_repository_impl.dart';
|
||||
import '../models/category_model.dart';
|
||||
import '../../domain/repositories/category_repository.dart';
|
||||
import '../../../../core/providers/providers.dart';
|
||||
import '../../../../core/constants/storage_constants.dart';
|
||||
|
||||
part 'category_providers.g.dart';
|
||||
|
||||
/// Provider for category Hive box
|
||||
@riverpod
|
||||
Box<CategoryModel> categoryBox(Ref ref) {
|
||||
return Hive.box<CategoryModel>(StorageConstants.categoriesBox);
|
||||
}
|
||||
|
||||
/// Provider for category local data source
|
||||
@riverpod
|
||||
CategoryLocalDataSource categoryLocalDataSource(Ref ref) {
|
||||
final box = ref.watch(categoryBoxProvider);
|
||||
return CategoryLocalDataSourceImpl(box);
|
||||
}
|
||||
|
||||
/// Provider for category remote data source
|
||||
@riverpod
|
||||
CategoryRemoteDataSource categoryRemoteDataSource(Ref ref) {
|
||||
final dioClient = ref.watch(dioClientProvider);
|
||||
return CategoryRemoteDataSourceImpl(dioClient);
|
||||
}
|
||||
|
||||
/// Provider for category repository
|
||||
@riverpod
|
||||
CategoryRepository categoryRepository(Ref ref) {
|
||||
final localDataSource = ref.watch(categoryLocalDataSourceProvider);
|
||||
final remoteDataSource = ref.watch(categoryRemoteDataSourceProvider);
|
||||
|
||||
return CategoryRepositoryImpl(
|
||||
localDataSource: localDataSource,
|
||||
remoteDataSource: remoteDataSource,
|
||||
);
|
||||
}
|
||||
220
lib/features/categories/data/providers/category_providers.g.dart
Normal file
220
lib/features/categories/data/providers/category_providers.g.dart
Normal file
@@ -0,0 +1,220 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'category_providers.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provider for category Hive box
|
||||
|
||||
@ProviderFor(categoryBox)
|
||||
const categoryBoxProvider = CategoryBoxProvider._();
|
||||
|
||||
/// Provider for category Hive box
|
||||
|
||||
final class CategoryBoxProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
Box<CategoryModel>,
|
||||
Box<CategoryModel>,
|
||||
Box<CategoryModel>
|
||||
>
|
||||
with $Provider<Box<CategoryModel>> {
|
||||
/// Provider for category Hive box
|
||||
const CategoryBoxProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'categoryBoxProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$categoryBoxHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<Box<CategoryModel>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Box<CategoryModel> create(Ref ref) {
|
||||
return categoryBox(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(Box<CategoryModel> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<Box<CategoryModel>>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$categoryBoxHash() => r'cbcd3cf6f0673b13a5e0af6dba10ca10f32be70c';
|
||||
|
||||
/// Provider for category local data source
|
||||
|
||||
@ProviderFor(categoryLocalDataSource)
|
||||
const categoryLocalDataSourceProvider = CategoryLocalDataSourceProvider._();
|
||||
|
||||
/// Provider for category local data source
|
||||
|
||||
final class CategoryLocalDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
CategoryLocalDataSource,
|
||||
CategoryLocalDataSource,
|
||||
CategoryLocalDataSource
|
||||
>
|
||||
with $Provider<CategoryLocalDataSource> {
|
||||
/// Provider for category local data source
|
||||
const CategoryLocalDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'categoryLocalDataSourceProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$categoryLocalDataSourceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<CategoryLocalDataSource> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
CategoryLocalDataSource create(Ref ref) {
|
||||
return categoryLocalDataSource(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(CategoryLocalDataSource value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<CategoryLocalDataSource>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$categoryLocalDataSourceHash() =>
|
||||
r'8d42c0dcfb986dfa0413e4267c4b08f24963ef50';
|
||||
|
||||
/// Provider for category remote data source
|
||||
|
||||
@ProviderFor(categoryRemoteDataSource)
|
||||
const categoryRemoteDataSourceProvider = CategoryRemoteDataSourceProvider._();
|
||||
|
||||
/// Provider for category remote data source
|
||||
|
||||
final class CategoryRemoteDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
CategoryRemoteDataSource,
|
||||
CategoryRemoteDataSource,
|
||||
CategoryRemoteDataSource
|
||||
>
|
||||
with $Provider<CategoryRemoteDataSource> {
|
||||
/// Provider for category remote data source
|
||||
const CategoryRemoteDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'categoryRemoteDataSourceProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$categoryRemoteDataSourceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<CategoryRemoteDataSource> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
CategoryRemoteDataSource create(Ref ref) {
|
||||
return categoryRemoteDataSource(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(CategoryRemoteDataSource value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<CategoryRemoteDataSource>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$categoryRemoteDataSourceHash() =>
|
||||
r'60294160d6655f1455064fb01016d341570e9a5d';
|
||||
|
||||
/// Provider for category repository
|
||||
|
||||
@ProviderFor(categoryRepository)
|
||||
const categoryRepositoryProvider = CategoryRepositoryProvider._();
|
||||
|
||||
/// Provider for category repository
|
||||
|
||||
final class CategoryRepositoryProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
CategoryRepository,
|
||||
CategoryRepository,
|
||||
CategoryRepository
|
||||
>
|
||||
with $Provider<CategoryRepository> {
|
||||
/// Provider for category repository
|
||||
const CategoryRepositoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'categoryRepositoryProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$categoryRepositoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<CategoryRepository> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
CategoryRepository create(Ref ref) {
|
||||
return categoryRepository(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(CategoryRepository value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<CategoryRepository>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$categoryRepositoryHash() =>
|
||||
r'256a9f2aa52a1858bbb50a87f2f838c33552ef22';
|
||||
@@ -2,14 +2,17 @@ import 'package:dartz/dartz.dart';
|
||||
import '../../domain/entities/category.dart';
|
||||
import '../../domain/repositories/category_repository.dart';
|
||||
import '../datasources/category_local_datasource.dart';
|
||||
import '../datasources/category_remote_datasource.dart';
|
||||
import '../../../../core/errors/failures.dart';
|
||||
import '../../../../core/errors/exceptions.dart';
|
||||
|
||||
class CategoryRepositoryImpl implements CategoryRepository {
|
||||
final CategoryLocalDataSource localDataSource;
|
||||
final CategoryRemoteDataSource remoteDataSource;
|
||||
|
||||
CategoryRepositoryImpl({
|
||||
required this.localDataSource,
|
||||
required this.remoteDataSource,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -38,12 +41,13 @@ class CategoryRepositoryImpl implements CategoryRepository {
|
||||
@override
|
||||
Future<Either<Failure, List<Category>>> syncCategories() async {
|
||||
try {
|
||||
// For now, return cached categories
|
||||
// In the future, implement remote sync
|
||||
final categories = await localDataSource.getAllCategories();
|
||||
final categories = await remoteDataSource.getAllCategories();
|
||||
await localDataSource.cacheCategories(categories);
|
||||
return Right(categories.map((model) => model.toEntity()).toList());
|
||||
} on CacheException catch (e) {
|
||||
return Left(CacheFailure(e.message));
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException catch (e) {
|
||||
return Left(NetworkFailure(e.message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user