497 lines
15 KiB
Dart
497 lines
15 KiB
Dart
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,
|
|
);
|
|
}
|
|
}
|