apskel-pos-flutter/lib/data/datasources/product/product_local_datasource.dart
2025-09-20 03:10:05 +07:00

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,
);
}
}