sync product to local

This commit is contained in:
efrilm
2025-09-20 03:10:05 +07:00
parent 3022d8de9f
commit f104390141
21 changed files with 4461 additions and 514 deletions
@@ -0,0 +1,496 @@
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:enaklo_pos/core/database/database_handler.dart';
import 'package:enaklo_pos/data/models/response/product_response_model.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart' as p;
class ProductLocalDatasource {
static ProductLocalDatasource? _instance;
ProductLocalDatasource._internal();
static ProductLocalDatasource get instance {
_instance ??= ProductLocalDatasource._internal();
return _instance!;
}
Future<Database> get _db async => await DatabaseHelper.instance.database;
// ========================================
// CACHING SYSTEM
// ========================================
final Map<String, List<Product>> _queryCache = {};
final Duration _cacheExpiry = Duration(minutes: 5);
final Map<String, DateTime> _cacheTimestamps = {};
// ========================================
// ENHANCED BATCH SAVE
// ========================================
Future<void> saveProductsBatch(List<Product> products,
{bool clearFirst = false}) async {
final db = await _db;
try {
await db.transaction((txn) async {
if (clearFirst) {
log('🗑️ Clearing existing products...');
await txn.delete('product_variants');
await txn.delete('products');
}
log('💾 Batch saving ${products.length} products...');
// ✅ BATCH INSERT PRODUCTS - Much faster than individual inserts
final batch = txn.batch();
for (final product in products) {
batch.insert(
'products',
_productToMap(product),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await batch.commit(noResult: true);
// ✅ BATCH INSERT VARIANTS
final variantBatch = txn.batch();
for (final product in products) {
if (product.variants?.isNotEmpty == true) {
// Delete existing variants in batch
variantBatch.delete(
'product_variants',
where: 'product_id = ?',
whereArgs: [product.id],
);
// Insert new variants
for (final variant in product.variants!) {
variantBatch.insert(
'product_variants',
_variantToMap(variant),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
}
}
await variantBatch.commit(noResult: true);
});
// Clear cache after update
clearCache();
log('✅ Successfully batch saved ${products.length} products');
} catch (e) {
log('❌ Error batch saving products: $e');
rethrow;
}
}
// ========================================
// CACHED QUERY - HIGH PERFORMANCE
// ========================================
Future<List<Product>> getCachedProducts({
int page = 1,
int limit = 10,
String? categoryId,
String? search,
}) async {
final cacheKey = _generateCacheKey(page, limit, categoryId, search);
final now = DateTime.now();
// ✅ CHECK CACHE FIRST
if (_queryCache.containsKey(cacheKey) &&
_cacheTimestamps.containsKey(cacheKey)) {
final cacheTime = _cacheTimestamps[cacheKey]!;
if (now.difference(cacheTime) < _cacheExpiry) {
log('🚀 Cache HIT: $cacheKey (${_queryCache[cacheKey]!.length} products)');
return _queryCache[cacheKey]!; // Return from cache - SUPER FAST
}
}
log('📀 Cache MISS: $cacheKey, querying database...');
// Cache miss, query database
final products = await getProducts(
page: page,
limit: limit,
categoryId: categoryId,
search: search,
);
// ✅ STORE IN CACHE for next time
_queryCache[cacheKey] = products;
_cacheTimestamps[cacheKey] = now;
log('💾 Cached ${products.length} products for key: $cacheKey');
return products;
}
// ========================================
// REGULAR GET PRODUCTS (No Cache)
// ========================================
Future<List<Product>> getProducts({
int page = 1,
int limit = 10,
String? categoryId,
String? search,
}) async {
final db = await _db;
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);
List<Product> products = [];
for (final map in maps) {
final variants = await _getProductVariants(db, map['id']);
final product = _mapToProduct(map, variants);
products.add(product);
}
log('📊 Retrieved ${products.length} products from database');
return products;
} catch (e) {
log('❌ Error getting products: $e');
return [];
}
}
// ========================================
// OPTIMIZED SEARCH with RANKING
// ========================================
Future<List<Product>> searchProductsOptimized(String query) async {
final db = await _db;
try {
log('🔍 Optimized search for: "$query"');
// ✅ 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
]);
List<Product> products = [];
for (final map in maps) {
final variants = await _getProductVariants(db, map['id']);
products.add(_mapToProduct(map, variants));
}
log('🎯 Optimized search found ${products.length} results');
return products;
} catch (e) {
log('❌ Error in optimized search: $e');
return [];
}
}
// ========================================
// DATABASE ANALYTICS & MONITORING
// ========================================
Future<Map<String, dynamic>> getDatabaseStats() async {
final db = await _db;
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');
return stats;
} catch (e) {
log('❌ Error getting database stats: $e');
return {};
}
}
Future<double> _getDatabaseSize() async {
try {
final dbPath = p.join(await getDatabasesPath(), 'pos_database.db');
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');
}
return 0.0;
}
double _getCacheSize() {
double totalSize = 0;
_queryCache.forEach((key, products) {
totalSize += products.length * 0.001; // Rough estimate in MB
});
return totalSize;
}
// ========================================
// CACHE MANAGEMENT
// ========================================
String _generateCacheKey(
int page, int limit, String? categoryId, String? search) {
return 'products_${page}_${limit}_${categoryId ?? 'null'}_${search ?? 'null'}';
}
void clearCache() {
final count = _queryCache.length;
_queryCache.clear();
_cacheTimestamps.clear();
log('🧹 Cache cleared: $count entries removed');
}
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');
}
}
// ========================================
// OTHER METHODS (Same as basic but with enhanced logging)
// ========================================
Future<Product?> getProductById(String id) async {
final db = await _db;
try {
final List<Map<String, dynamic>> maps = await db.query(
'products',
where: 'id = ?',
whereArgs: [id],
);
if (maps.isEmpty) {
log('❌ Product not found: $id');
return null;
}
final variants = await _getProductVariants(db, id);
final product = _mapToProduct(maps.first, variants);
log('✅ Product found: ${product.name}');
return product;
} catch (e) {
log('❌ Error getting product by ID: $e');
return null;
}
}
Future<int> getTotalCount({String? categoryId, String? search}) async {
final db = await _db;
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)');
return count;
} catch (e) {
log('❌ Error getting total count: $e');
return 0;
}
}
Future<bool> hasProducts() async {
final count = await getTotalCount();
final hasData = count > 0;
log('🔍 Has products: $hasData ($count products)');
return hasData;
}
Future<void> clearAllProducts() async {
final db = await _db;
try {
await db.transaction((txn) async {
await txn.delete('product_variants');
await txn.delete('products');
});
clearCache();
log('🗑️ All products cleared from local DB');
} catch (e) {
log('❌ Error clearing products: $e');
rethrow;
}
}
// ========================================
// HELPER METHODS
// ========================================
Future<List<ProductVariant>> _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) => _mapToVariant(map)).toList();
} catch (e) {
log('❌ Error getting variants for product $productId: $e');
return [];
}
}
Map<String, dynamic> _productToMap(Product product) {
return {
'id': product.id,
'organization_id': product.organizationId,
'category_id': product.categoryId,
'sku': product.sku,
'name': product.name,
'description': product.description,
'price': product.price,
'cost': product.cost,
'business_type': product.businessType,
'image_url': product.imageUrl,
'printer_type': product.printerType,
'metadata':
product.metadata != null ? json.encode(product.metadata) : null,
'is_active': product.isActive == true ? 1 : 0,
'created_at': product.createdAt?.toIso8601String(),
'updated_at': product.updatedAt?.toIso8601String(),
};
}
Map<String, dynamic> _variantToMap(ProductVariant variant) {
return {
'id': variant.id,
'product_id': variant.productId,
'name': variant.name,
'price_modifier': variant.priceModifier,
'cost': variant.cost,
'metadata':
variant.metadata != null ? json.encode(variant.metadata) : null,
'created_at': variant.createdAt?.toIso8601String(),
'updated_at': variant.updatedAt?.toIso8601String(),
};
}
Product _mapToProduct(
Map<String, dynamic> map, List<ProductVariant> variants) {
return Product(
id: map['id'],
organizationId: map['organization_id'],
categoryId: map['category_id'],
sku: map['sku'],
name: map['name'],
description: map['description'],
price: map['price'],
cost: map['cost'],
businessType: map['business_type'],
imageUrl: map['image_url'],
printerType: map['printer_type'],
metadata: map['metadata'] != null ? json.decode(map['metadata']) : null,
isActive: map['is_active'] == 1,
createdAt:
map['created_at'] != null ? DateTime.parse(map['created_at']) : null,
updatedAt:
map['updated_at'] != null ? DateTime.parse(map['updated_at']) : null,
variants: variants,
);
}
ProductVariant _mapToVariant(Map<String, dynamic> map) {
return ProductVariant(
id: map['id'],
productId: map['product_id'],
name: map['name'],
priceModifier: map['price_modifier'],
cost: map['cost'],
metadata: map['metadata'] != null ? json.decode(map['metadata']) : null,
createdAt:
map['created_at'] != null ? DateTime.parse(map['created_at']) : null,
updatedAt:
map['updated_at'] != null ? DateTime.parse(map['updated_at']) : null,
);
}
}
@@ -0,0 +1,145 @@
import 'dart:developer';
import 'package:dartz/dartz.dart';
import 'package:enaklo_pos/data/datasources/product/product_local_datasource.dart';
import 'package:enaklo_pos/data/models/response/product_response_model.dart';
class ProductRepository {
static ProductRepository? _instance;
final ProductLocalDatasource _localDatasource;
ProductRepository._internal()
: _localDatasource = ProductLocalDatasource.instance;
static ProductRepository get instance {
_instance ??= ProductRepository._internal();
return _instance!;
}
// ========================================
// PURE LOCAL DATABASE OPERATIONS
// ========================================
Future<Either<String, ProductResponseModel>> getProducts({
int page = 1,
int limit = 10,
String? categoryId,
String? search,
bool forceRefresh = false, // Ignored - kept for compatibility
}) async {
try {
log('📱 Getting products from local database - page: $page, categoryId: $categoryId, search: $search');
// Clean expired cache for optimal performance
_localDatasource.clearExpiredCache();
// Use cached query for maximum performance
final cachedProducts = await _localDatasource.getCachedProducts(
page: page,
limit: limit,
categoryId: categoryId,
search: search,
);
final totalCount = await _localDatasource.getTotalCount(
categoryId: categoryId,
search: search,
);
final productData = ProductData(
products: cachedProducts,
totalCount: totalCount,
page: page,
limit: limit,
totalPages: totalCount > 0 ? (totalCount / limit).ceil() : 0,
);
final response = ProductResponseModel(
success: true,
data: productData,
errors: null,
);
log('✅ Returned ${cachedProducts.length} local products (${totalCount} total)');
return Right(response);
} catch (e) {
log('❌ Error getting local products: $e');
return Left('Gagal memuat produk dari database lokal: $e');
}
}
// ========================================
// OPTIMIZED LOCAL SEARCH
// ========================================
Future<Either<String, List<Product>>> searchProductsOptimized(
String query) async {
try {
log('🔍 Local optimized search for: "$query"');
final products = await _localDatasource.searchProductsOptimized(query);
log('✅ Local search completed: ${products.length} results');
return Right(products);
} catch (e) {
log('❌ Error in local search: $e');
return Left('Pencarian lokal gagal: $e');
}
}
// ========================================
// LOCAL DATABASE OPERATIONS
// ========================================
// Refresh just cleans cache and reloads from local
Future<Either<String, ProductResponseModel>> refreshProducts({
String? categoryId,
String? search,
}) async {
log('🔄 Refreshing local products...');
// Clear cache for fresh local data
clearCache();
return await getProducts(
page: 1,
limit: 10,
categoryId: categoryId,
search: search,
);
}
Future<Product?> getProductById(String id) async {
log('🔍 Getting product by ID from local: $id');
return await _localDatasource.getProductById(id);
}
Future<bool> hasLocalProducts() async {
final hasProducts = await _localDatasource.hasProducts();
log('📊 Has local products: $hasProducts');
return hasProducts;
}
Future<Map<String, dynamic>> getDatabaseStats() async {
final stats = await _localDatasource.getDatabaseStats();
log('📊 Database stats: $stats');
return stats;
}
void clearCache() {
log('🧹 Clearing local cache');
_localDatasource.clearCache();
}
// Helper method to check if local database is populated
Future<bool> isLocalDatabaseReady() async {
try {
final stats = await getDatabaseStats();
final productCount = stats['total_products'] ?? 0;
final isReady = productCount > 0;
log('🔍 Local database ready: $isReady ($productCount products)');
return isReady;
} catch (e) {
log('❌ Error checking database readiness: $e');
return false;
}
}
}
+79
View File
@@ -0,0 +1,79 @@
import 'dart:async';
import 'dart:developer';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:enaklo_pos/data/repositories/product/product_repository.dart';
class SyncManager {
final ProductRepository _productRepository;
final Connectivity _connectivity = Connectivity();
Timer? _syncTimer;
bool _isSyncing = false;
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
SyncManager(this._productRepository) {
_startPeriodicSync();
_listenToConnectivityChanges();
}
void _startPeriodicSync() {
// Sync setiap 5 menit jika ada koneksi
_syncTimer = Timer.periodic(Duration(minutes: 5), (timer) {
_performBackgroundSync();
});
}
void _listenToConnectivityChanges() {
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(
(List<ConnectivityResult> results) {
// Check if any connection is available
final hasConnection =
results.any((result) => result != ConnectivityResult.none);
if (hasConnection) {
log('Connection restored, starting background sync');
_performBackgroundSync();
} else {
log('Connection lost');
}
},
);
}
Future<void> _performBackgroundSync() async {
if (_isSyncing) return;
// Check current connectivity before syncing
final connectivityResults = await _connectivity.checkConnectivity();
final hasConnection =
connectivityResults.any((result) => result != ConnectivityResult.none);
if (!hasConnection) {
log('No internet connection, skipping sync');
return;
}
try {
_isSyncing = true;
log('Starting background sync');
await _productRepository.refreshProducts();
log('Background sync completed');
} catch (e) {
log('Background sync failed: $e');
} finally {
_isSyncing = false;
}
}
// Public method untuk manual sync
Future<void> performManualSync() async {
await _performBackgroundSync();
}
void dispose() {
_syncTimer?.cancel();
_connectivitySubscription?.cancel();
}
}