This commit is contained in:
Phuoc Nguyen
2025-10-10 16:38:07 +07:00
parent e5b247d622
commit b94c158004
177 changed files with 25080 additions and 152 deletions

View File

@@ -0,0 +1,215 @@
import 'package:dio/dio.dart';
import '../../../../core/constants/api_constants.dart';
import '../../../../core/errors/exceptions.dart';
import '../../../../core/network/dio_client.dart';
import '../models/category_model.dart';
/// Remote data source for categories API operations
abstract class CategoryRemoteDataSource {
/// Fetch all categories from the API
Future<List<CategoryModel>> fetchCategories();
/// Fetch a single category by ID
Future<CategoryModel> fetchCategoryById(String id);
/// Sync categories (bulk update/create)
Future<void> syncCategories(List<CategoryModel> categories);
}
/// Implementation of CategoryRemoteDataSource using Dio
class CategoryRemoteDataSourceImpl implements CategoryRemoteDataSource {
final DioClient _dioClient;
CategoryRemoteDataSourceImpl(this._dioClient);
@override
Future<List<CategoryModel>> fetchCategories() async {
try {
final response = await _dioClient.get(ApiConstants.categories);
if (response.statusCode == ApiConstants.statusOk) {
final data = response.data;
// Handle different response structures
List<dynamic> categoriesJson;
if (data is Map<String, dynamic>) {
// Response wrapped in object: { "categories": [...] }
categoriesJson = data['categories'] as List<dynamic>? ??
data['data'] as List<dynamic>? ??
[];
} else if (data is List) {
// Direct array response
categoriesJson = data;
} else {
throw DataParsingException('Unexpected response format');
}
return categoriesJson
.map((json) => CategoryModel.fromJson(json as Map<String, dynamic>))
.toList();
} else {
throw ServerException(
'Failed to fetch categories',
response.statusCode ?? 500,
);
}
} on AppException {
rethrow;
} catch (e, stackTrace) {
throw NetworkException(
'Failed to fetch categories: ${e.toString()}',
null,
e,
stackTrace,
);
}
}
@override
Future<CategoryModel> fetchCategoryById(String id) async {
try {
final response = await _dioClient.get(ApiConstants.categoryById(id));
if (response.statusCode == ApiConstants.statusOk) {
final data = response.data;
// Handle different response structures
Map<String, dynamic> categoryJson;
if (data is Map<String, dynamic>) {
// Check if category is wrapped in a key
categoryJson = data['category'] as Map<String, dynamic>? ??
data['data'] as Map<String, dynamic>? ??
data;
} else {
throw DataParsingException('Unexpected response format');
}
return CategoryModel.fromJson(categoryJson);
} else if (response.statusCode == ApiConstants.statusNotFound) {
throw NotFoundException('Category with ID $id not found');
} else {
throw ServerException(
'Failed to fetch category',
response.statusCode ?? 500,
);
}
} on AppException {
rethrow;
} catch (e, stackTrace) {
throw NetworkException(
'Failed to fetch category: ${e.toString()}',
null,
e,
stackTrace,
);
}
}
@override
Future<void> syncCategories(List<CategoryModel> categories) async {
try {
final categoriesJson = categories.map((c) => c.toJson()).toList();
final response = await _dioClient.post(
ApiConstants.syncCategories,
data: {
'categories': categoriesJson,
},
);
if (response.statusCode != ApiConstants.statusOk &&
response.statusCode != ApiConstants.statusCreated) {
throw ServerException(
'Failed to sync categories',
response.statusCode ?? 500,
);
}
} on AppException {
rethrow;
} catch (e, stackTrace) {
throw NetworkException(
'Failed to sync categories: ${e.toString()}',
null,
e,
stackTrace,
);
}
}
}
/// Mock implementation for testing and development
class CategoryRemoteDataSourceMock implements CategoryRemoteDataSource {
/// Simulate network delay
Future<void> _delay() async {
await Future.delayed(
Duration(milliseconds: ApiConstants.mockApiDelay),
);
}
@override
Future<List<CategoryModel>> fetchCategories() async {
await _delay();
// Return mock categories
return [
CategoryModel(
id: '1',
name: 'Electronics',
description: 'Electronic devices and accessories',
iconPath: 'assets/icons/electronics.png',
color: '#2196F3',
productCount: 25,
createdAt: DateTime.now().subtract(const Duration(days: 60)),
),
CategoryModel(
id: '2',
name: 'Clothing',
description: 'Fashion and apparel',
iconPath: 'assets/icons/clothing.png',
color: '#E91E63',
productCount: 50,
createdAt: DateTime.now().subtract(const Duration(days: 50)),
),
CategoryModel(
id: '3',
name: 'Food & Beverages',
description: 'Food, drinks, and snacks',
iconPath: 'assets/icons/food.png',
color: '#FF9800',
productCount: 100,
createdAt: DateTime.now().subtract(const Duration(days: 40)),
),
CategoryModel(
id: '4',
name: 'Home & Garden',
description: 'Home improvement and garden supplies',
iconPath: 'assets/icons/home.png',
color: '#4CAF50',
productCount: 30,
createdAt: DateTime.now().subtract(const Duration(days: 30)),
),
];
}
@override
Future<CategoryModel> fetchCategoryById(String id) async {
await _delay();
// Return mock category
return CategoryModel(
id: id,
name: 'Sample Category $id',
description: 'This is sample category $id',
iconPath: 'assets/icons/category.png',
color: '#2196F3',
productCount: 10,
createdAt: DateTime.now().subtract(const Duration(days: 30)),
);
}
@override
Future<void> syncCategories(List<CategoryModel> categories) async {
await _delay();
// Mock sync - do nothing
}
}