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