product repo
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user