# 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

@@ -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');
}
}
}

View File

@@ -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,
);
}

View 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';

View File

@@ -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));
}
}
}