product repo

This commit is contained in:
efrilm
2025-10-24 22:03:35 +07:00
parent dea5de8828
commit 4fdd1e44f8
18 changed files with 6256 additions and 0 deletions
@@ -0,0 +1,331 @@
import 'dart:developer';
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../domain/product/product.dart';
import '../datasources/local_data_provider.dart';
import '../datasources/remote_data_provider.dart';
@Injectable(as: IProductRepository)
class ProductRepository implements IProductRepository {
final ProductRemoteDataProvider _remoteDataProvider;
final ProductLocalDataProvider _localDataProvider;
final _logName = 'ProductRepository';
ProductRepository(this._remoteDataProvider, this._localDataProvider);
@override
Future<Either<ProductFailure, Product>> getProductById(String id) async {
try {
log('πŸ” Getting product by ID from local: $id', name: _logName);
final product = await _localDataProvider.getProductById(id);
if (product.hasData) {
log('❌ Product not found: $id', name: _logName);
return Left(
ProductFailure.dynamicErrorMessage(
'Produk dengan ID $id tidak ditemukan',
),
);
}
final productDomain = product.data!.toDomain();
log('βœ… Product loaded: ${productDomain.name}', name: _logName);
return Right(productDomain);
} catch (e, s) {
log(
'❌ Error getting product by ID',
name: _logName,
error: e,
stackTrace: s,
);
return Left(ProductFailure.localStorageError(e.toString()));
}
}
@override
Future<Either<ProductFailure, ListProduct>> getProducts({
int page = 1,
int limit = 10,
String? categoryId,
String? search,
bool forceRefresh = false,
}) async {
try {
log(
'πŸ“¦ Fetching products from local DB - page: $page, categoryId: $categoryId, search: $search',
name: _logName,
);
// 🧹 Bersihkan cache kedaluwarsa
_localDataProvider.clearExpiredCache();
// ⚑ Ambil data dari cache lokal
final cachedProducts = await _localDataProvider.getCachedProducts(
page: page,
limit: limit,
categoryId: categoryId,
search: search,
);
if (cachedProducts.hasError) {
return left(cachedProducts.error!);
}
// πŸ“Š Hitung total item (untuk pagination)
final totalCount = await _localDataProvider.getTotalCount(
categoryId: categoryId,
search: search,
);
// 🧱 Bangun entity domain ListProduct
final result = ListProduct(
products: cachedProducts.data!.map((p) => p.toDomain()).toList(),
totalCount: totalCount,
page: page,
limit: limit,
totalPages: totalCount > 0 ? (totalCount / limit).ceil() : 0,
);
log(
'βœ… Returned ${cachedProducts.data!.length} local products ($totalCount total)',
name: _logName,
);
return Right(result);
} catch (e, s) {
log(
'❌ Error getting local products',
name: _logName,
error: e,
stackTrace: s,
);
return Left(ProductFailure.localStorageError(e.toString()));
}
}
@override
Future<Either<ProductFailure, ListProduct>> refreshProducts({
String? categoryId,
String? search,
}) async {
try {
log('πŸ”„ Refreshing local products...', name: _logName);
// Bersihkan cache agar hasil baru diambil dari database
_localDataProvider.clearCache();
// Ambil ulang data produk dari lokal database
final cachedProducts = await _localDataProvider.getCachedProducts(
page: 1,
limit: 10,
categoryId: categoryId,
search: search,
);
if (cachedProducts.hasError) {
return left(cachedProducts.error!);
}
final products = cachedProducts.data!.map((p) => p.toDomain()).toList();
final totalCount = await _localDataProvider.getTotalCount(
categoryId: categoryId,
search: search,
);
// 🧱 Bangun entity domain ListProduct
final result = ListProduct(
products: products,
totalCount: totalCount,
page: 1,
limit: 10,
totalPages: totalCount > 0 ? (totalCount / 10).ceil() : 0,
);
log(
'βœ… Refreshed ${cachedProducts.data!.length} local products',
name: _logName,
);
return Right(result);
} catch (e, s) {
log(
'❌ Error refreshing local products',
name: _logName,
error: e,
stackTrace: s,
);
return Left(ProductFailure.localStorageError(e.toString()));
}
}
@override
Future<Either<ProductFailure, List<Product>>> searchProductsOptimized(
String query,
) async {
try {
log('πŸ” Local optimized search for: "$query"', name: _logName);
// πŸ”Ž Cari dari local database
final results = await _localDataProvider.searchProductsOptimized(query);
if (results.hasError) {
return left(results.error!);
}
// βœ… Mapping ke domain entity (kalau hasilnya masih berupa DTO)
final products = results.data!.map((p) => p.toDomain()).toList();
log(
'βœ… Local search completed: ${products.length} results',
name: _logName,
);
return Right(products);
} catch (e, s) {
log('❌ Error in local search', name: _logName, error: e, stackTrace: s);
return Left(ProductFailure.localStorageError(e.toString()));
}
}
@override
Future<Either<ProductFailure, String>> syncAllProducts() async {
try {
log('πŸ”„ Starting manual sync of all products...', name: _logName);
int page = 1;
const limit = 50;
bool hasMore = true;
int totalSynced = 0;
// Clear local DB before fresh sync
await _localDataProvider.clearAllProducts();
while (hasMore) {
log('πŸ“„ Syncing page $page...', name: _logName);
// NOTE: _remoteDatasource.getProducts() returns DC<..., ProductResponseModel>
final remoteResult = await _remoteDataProvider.fetchProducts(
page: page,
limit: limit,
);
// Handle DC result manually (no fold)
if (!remoteResult.hasData) {
// remote returned an error/failure
final remoteFailure = remoteResult.error;
log('❌ Sync failed at page $page: $remoteFailure', name: _logName);
return Left(
ProductFailure.dynamicErrorMessage(remoteFailure.toString()),
);
}
final response = remoteResult.data!;
final products = response.products;
if (products.isNotEmpty) {
// Save page to local DB
await _localDataProvider.saveProductsBatch(
products,
clearFirst: false, // don't clear on subsequent pages
);
totalSynced += products.length;
// Determine if more pages exist
hasMore = page < (response.totalPages);
page++;
log(
'πŸ“¦ Page ${page - 1} synced: ${products.length} products',
name: _logName,
);
} else {
hasMore = false;
}
}
final message = 'Berhasil sinkronisasi $totalSynced produk';
log('βœ… $message', name: _logName);
return Right(message);
} catch (e, s) {
log(
'❌ Gagal sinkronisasi produk',
name: _logName,
error: e,
stackTrace: s,
);
return Left(ProductFailure.localStorageError(e.toString()));
}
}
@override
void clearCache() {
log('🧹 Clearing local cache', name: _logName);
_localDataProvider.clearCache();
}
@override
Future<Either<ProductFailure, Map<String, dynamic>>>
getDatabaseStats() async {
try {
log('πŸ“Š Getting local database stats...', name: _logName);
final stats = await _localDataProvider.getDatabaseStats();
log('βœ… Database stats loaded successfully: $stats', name: _logName);
return Right(stats.data!);
} catch (e, s) {
log(
'❌ Error getting database stats',
name: _logName,
error: e,
stackTrace: s,
);
return Left(ProductFailure.localStorageError(e.toString()));
}
}
@override
Future<bool> hasLocalProducts() async {
final hasProducts = await _localDataProvider.hasProducts();
log('πŸ“Š Has local products: $hasProducts', name: _logName);
return hasProducts;
}
@override
Future<bool> isLocalDatabaseReady() async {
try {
final stats = await _localDataProvider.getDatabaseStats();
final productCount = stats.data!['total_products'] ?? 0;
final isReady = productCount > 0;
log(
'πŸ” Local database ready: $isReady ($productCount products)',
name: _logName,
);
return isReady;
} catch (e) {
log('❌ Error checking database readiness: $e', name: _logName);
return false;
}
}
@override
Future<void> clearAllProducts() async {
try {
log('πŸ—‘οΈ Clearing all products from repository...', name: _logName);
await _localDataProvider.clearAllProducts();
clearCache();
log('βœ… All products cleared successfully', name: _logName);
} catch (e) {
log('❌ Error clearing all products: $e', name: _logName);
rethrow;
}
}
}