2025-10-24 23:20:41 +07:00

487 lines
13 KiB
Dart

import 'dart:developer';
import 'dart:io';
import 'package:data_channel/data_channel.dart';
import 'package:injectable/injectable.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart' as p;
import '../../../common/constant/app_constant.dart';
import '../../../common/database/database_helper.dart';
import '../../../domain/product/product.dart';
import '../product_dtos.dart';
@injectable
class ProductLocalDataProvider {
final DatabaseHelper _databaseHelper;
final _logName = 'ProductLocalDataProvider';
final Map<String, List<ProductDto>> _queryCache = {};
final Duration _cacheExpiry = Duration(minutes: AppConstant.cacheExpire);
final Map<String, DateTime> _cacheTimestamps = {};
ProductLocalDataProvider(this._databaseHelper);
Future<DC<ProductFailure, void>> saveProductsBatch(
List<ProductDto> products, {
bool clearFirst = false,
}) async {
final db = await _databaseHelper.database;
try {
await db.transaction((txn) async {
if (clearFirst) {
log('๐Ÿ—‘๏ธ Clearing existing products...', name: _logName);
await txn.delete('product_variants');
await txn.delete('products');
}
log('๐Ÿ’พ Batch saving ${products.length} products...', name: _logName);
// โœ… Batch insert products
final productBatch = txn.batch();
for (final product in products) {
productBatch.insert(
'products',
product.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await productBatch.commit(noResult: true);
// โœ… Batch insert variants
final variantBatch = txn.batch();
for (final product in products) {
if (product.variants?.isNotEmpty == true) {
// Delete existing variants
variantBatch.delete(
'product_variants',
where: 'product_id = ?',
whereArgs: [product.id],
);
// Insert variants
for (final variant in product.variants!) {
variantBatch.insert(
'product_variants',
variant.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
}
}
await variantBatch.commit(noResult: true);
});
// โœ… Clear cache
clearCache();
log(
'โœ… Successfully batch saved ${products.length} products',
name: _logName,
);
return DC.data(null);
} catch (e, s) {
log(
'โŒ Error batch saving products',
name: _logName,
error: e,
stackTrace: s,
);
return DC.error(ProductFailure.dynamicErrorMessage(e.toString()));
}
}
Future<DC<ProductFailure, List<ProductDto>>> getCachedProducts({
int page = 1,
int limit = 10,
String? categoryId,
String? search,
}) async {
final cacheKey = _generateCacheKey(page, limit, categoryId, search);
final now = DateTime.now();
try {
// โœ… CHECK CACHE FIRST
if (_queryCache.containsKey(cacheKey) &&
_cacheTimestamps.containsKey(cacheKey)) {
final cacheTime = _cacheTimestamps[cacheKey]!;
if (now.difference(cacheTime) < _cacheExpiry) {
final cachedProducts = _queryCache[cacheKey]!;
log(
'๐Ÿš€ Cache HIT: $cacheKey (${cachedProducts.length} products)',
name: _logName,
);
return DC.data(cachedProducts);
}
}
log('๐Ÿ“€ Cache MISS: $cacheKey, querying database...', name: _logName);
// Cache miss โ†’ query database
final result = await getProducts(
page: page,
limit: limit,
categoryId: categoryId,
search: search,
);
// โœ… Handle data/error dari getProducts()
if (result.hasData) {
final products = result.data!;
// Simpan ke cache
_queryCache[cacheKey] = products;
_cacheTimestamps[cacheKey] = now;
log(
'๐Ÿ’พ Cached ${products.length} products for key: $cacheKey',
name: _logName,
);
return DC.data(products);
} else {
// Kalau error dari getProducts
return DC.error(result.error!);
}
} catch (e, s) {
log(
'โŒ Error getting cached products',
name: _logName,
error: e,
stackTrace: s,
);
return DC.error(ProductFailure.localStorageError(e.toString()));
}
}
Future<DC<ProductFailure, List<ProductDto>>> getProducts({
int page = 1,
int limit = 10,
String? categoryId,
String? search,
}) async {
final db = await _databaseHelper.database;
try {
String query = 'SELECT * FROM products WHERE 1=1';
List<dynamic> whereArgs = [];
if (categoryId != null && categoryId.isNotEmpty) {
query += ' AND category_id = ?';
whereArgs.add(categoryId);
}
if (search != null && search.isNotEmpty) {
query += ' AND (name LIKE ? OR sku LIKE ? OR description LIKE ?)';
whereArgs.add('%$search%');
whereArgs.add('%$search%');
whereArgs.add('%$search%');
}
query += ' ORDER BY created_at DESC';
if (limit > 0) {
query += ' LIMIT ?';
whereArgs.add(limit);
if (page > 1) {
query += ' OFFSET ?';
whereArgs.add((page - 1) * limit);
}
}
final List<Map<String, dynamic>> maps = await db.rawQuery(
query,
whereArgs,
);
final List<ProductDto> products = [];
for (final map in maps) {
final variants = await _getProductVariants(db, map['id']);
final product = ProductDto.fromMap(map, variants);
products.add(product);
}
log(
'๐Ÿ“Š Retrieved ${products.length} products from database',
name: _logName,
);
return DC.data(products);
} catch (e, s) {
log('โŒ Error getting products', name: _logName, error: e, stackTrace: s);
return DC.error(ProductFailure.localStorageError(e.toString()));
}
}
Future<DC<ProductFailure, List<ProductDto>>> searchProductsOptimized(
String query,
) async {
final db = await _databaseHelper.database;
try {
log('๐Ÿ” Optimized search for: "$query"', name: _logName);
// โœ… Smart query with prioritization
final List<Map<String, dynamic>> maps = await db.rawQuery(
'''
SELECT * FROM products
WHERE name LIKE ? OR sku LIKE ? OR description LIKE ?
ORDER BY
CASE
WHEN name LIKE ? THEN 1 -- Highest priority: name match
WHEN sku LIKE ? THEN 2 -- Second priority: SKU match
ELSE 3 -- Lowest priority: description
END,
name ASC
LIMIT 50
''',
[
'%$query%', '%$query%', '%$query%',
'$query%', '$query%', // Prioritize results that start with query
],
);
final List<ProductDto> products = [];
for (final map in maps) {
final variants = await _getProductVariants(db, map['id']);
final product = ProductDto.fromMap(map, variants);
products.add(product);
}
log(
'๐ŸŽฏ Optimized search found ${products.length} results',
name: _logName,
);
return DC.data(products);
} catch (e, s) {
log(
'โŒ Error in optimized search',
name: _logName,
error: e,
stackTrace: s,
);
return DC.error(ProductFailure.localStorageError(e.toString()));
}
}
Future<DC<ProductFailure, ProductDto>> getProductById(String id) async {
final db = await _databaseHelper.database;
try {
final List<Map<String, dynamic>> maps = await db.query(
'products',
where: 'id = ?',
whereArgs: [id],
);
if (maps.isEmpty) {
log('โŒ Product not found: $id', name: _logName);
return DC.error(ProductFailure.empty());
}
final variants = await _getProductVariants(db, id);
final product = ProductDto.fromMap(maps.first, variants);
log('โœ… Product found: ${product.name}', name: _logName);
return DC.data(product);
} catch (e, s) {
log(
'โŒ Error getting product by ID',
name: _logName,
error: e,
stackTrace: s,
);
return DC.error(ProductFailure.localStorageError(e.toString()));
}
}
Future<DC<ProductFailure, Map<String, dynamic>>> getDatabaseStats() async {
final db = await _databaseHelper.database;
try {
final productCount =
Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM products'),
) ??
0;
final variantCount =
Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM product_variants'),
) ??
0;
final categoryCount =
Sqflite.firstIntValue(
await db.rawQuery(
'SELECT COUNT(DISTINCT category_id) FROM products WHERE category_id IS NOT NULL',
),
) ??
0;
final dbSize = await _getDatabaseSize();
final stats = {
'total_products': productCount,
'total_variants': variantCount,
'total_categories': categoryCount,
'database_size_mb': dbSize,
'cache_entries': _queryCache.length,
'cache_size_mb': _getCacheSize(),
};
log('๐Ÿ“Š Database Stats: $stats', name: _logName);
return DC.data(stats);
} catch (e, s) {
log(
'โŒ Error getting database stats',
name: _logName,
error: e,
stackTrace: s,
);
return DC.error(ProductFailure.localStorageError(e.toString()));
}
}
Future<int> getTotalCount({String? categoryId, String? search}) async {
final db = await _databaseHelper.database;
try {
String query = 'SELECT COUNT(*) FROM products WHERE 1=1';
List<dynamic> whereArgs = [];
if (categoryId != null && categoryId.isNotEmpty) {
query += ' AND category_id = ?';
whereArgs.add(categoryId);
}
if (search != null && search.isNotEmpty) {
query += ' AND (name LIKE ? OR sku LIKE ? OR description LIKE ?)';
whereArgs.add('%$search%');
whereArgs.add('%$search%');
whereArgs.add('%$search%');
}
final result = await db.rawQuery(query, whereArgs);
final count = Sqflite.firstIntValue(result) ?? 0;
log(
'๐Ÿ“Š Total count: $count (categoryId: $categoryId, search: $search)',
name: _logName,
);
return count;
} catch (e) {
log('โŒ Error getting total count: $e', name: _logName);
return 0;
}
}
Future<bool> hasProducts() async {
final count = await getTotalCount();
final hasData = count > 0;
log('๐Ÿ” Has products: $hasData ($count products)', name: _logName);
return hasData;
}
Future<void> clearAllProducts() async {
final db = await _databaseHelper.database;
try {
await db.transaction((txn) async {
await txn.delete('product_variants');
await txn.delete('products');
});
clearCache();
log('๐Ÿ—‘๏ธ All products cleared from local DB', name: _logName);
} catch (e) {
log('โŒ Error clearing products: $e', name: _logName);
rethrow;
}
}
Future<double> _getDatabaseSize() async {
try {
final dbPath = p.join(await getDatabasesPath(), AppConstant.dbName);
final file = File(dbPath);
if (await file.exists()) {
final size = await file.length();
return size / (1024 * 1024); // Convert to MB
}
} catch (e) {
log('Error getting database size: $e', name: _logName);
}
return 0.0;
}
Future<List<ProductVariantDto>> _getProductVariants(
Database db,
String productId,
) async {
try {
final List<Map<String, dynamic>> maps = await db.query(
'product_variants',
where: 'product_id = ?',
whereArgs: [productId],
orderBy: 'name ASC',
);
return maps.map((map) => ProductVariantDto.fromMap(map)).toList();
} catch (e) {
log(
'โŒ Error getting variants for product $productId: $e',
name: _logName,
);
return [];
}
}
String _generateCacheKey(
int page,
int limit,
String? categoryId,
String? search,
) {
return 'products_${page}_${limit}_${categoryId ?? 'null'}_${search ?? 'null'}';
}
double _getCacheSize() {
double totalSize = 0;
_queryCache.forEach((key, products) {
totalSize += products.length * 0.001; // Rough estimate in MB
});
return totalSize;
}
void clearCache() {
final count = _queryCache.length;
_queryCache.clear();
_cacheTimestamps.clear();
log('๐Ÿงน Cache cleared: $count entries removed', name: _logName);
}
void clearExpiredCache() {
final now = DateTime.now();
final expiredKeys = <String>[];
_cacheTimestamps.forEach((key, timestamp) {
if (now.difference(timestamp) > _cacheExpiry) {
expiredKeys.add(key);
}
});
for (final key in expiredKeys) {
_queryCache.remove(key);
_cacheTimestamps.remove(key);
}
if (expiredKeys.isNotEmpty) {
log('โฐ Expired cache cleared: ${expiredKeys.length} entries');
}
}
}