category local
This commit is contained in:
@@ -0,0 +1,373 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'package:enaklo_pos/core/database/database_handler.dart';
|
||||
import 'package:enaklo_pos/data/models/response/category_response_model.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
||||
class CategoryLocalDatasource {
|
||||
static CategoryLocalDatasource? _instance;
|
||||
|
||||
CategoryLocalDatasource._internal();
|
||||
|
||||
static CategoryLocalDatasource get instance {
|
||||
_instance ??= CategoryLocalDatasource._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Future<Database> get _db async => await DatabaseHelper.instance.database;
|
||||
|
||||
// ========================================
|
||||
// CACHING SYSTEM
|
||||
// ========================================
|
||||
final Map<String, List<CategoryModel>> _queryCache = {};
|
||||
final Duration _cacheExpiry =
|
||||
Duration(minutes: 10); // Lebih lama untuk categories
|
||||
final Map<String, DateTime> _cacheTimestamps = {};
|
||||
|
||||
// ========================================
|
||||
// BATCH SAVE CATEGORIES
|
||||
// ========================================
|
||||
Future<void> saveCategoriesBatch(List<CategoryModel> categories,
|
||||
{bool clearFirst = false}) async {
|
||||
final db = await _db;
|
||||
|
||||
try {
|
||||
await db.transaction((txn) async {
|
||||
if (clearFirst) {
|
||||
log('ποΈ Clearing existing categories...');
|
||||
await txn.delete('categories');
|
||||
}
|
||||
|
||||
log('πΎ Batch saving ${categories.length} categories...');
|
||||
|
||||
// Batch insert categories
|
||||
final batch = txn.batch();
|
||||
for (final category in categories) {
|
||||
batch.insert(
|
||||
'categories',
|
||||
_categoryToMap(category),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
await batch.commit(noResult: true);
|
||||
});
|
||||
|
||||
// Clear cache after update
|
||||
clearCache();
|
||||
log('β
Successfully batch saved ${categories.length} categories');
|
||||
} catch (e) {
|
||||
log('β Error batch saving categories: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CACHED QUERY
|
||||
// ========================================
|
||||
Future<List<CategoryModel>> getCachedCategories({
|
||||
int page = 1,
|
||||
int limit = 10,
|
||||
bool isActive = true,
|
||||
String? search,
|
||||
}) async {
|
||||
final cacheKey = _generateCacheKey(page, limit, isActive, 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} categories)');
|
||||
return _queryCache[cacheKey]!;
|
||||
}
|
||||
}
|
||||
|
||||
log('π Cache MISS: $cacheKey, querying database...');
|
||||
|
||||
// Cache miss, query database
|
||||
final categories = await getCategories(
|
||||
page: page,
|
||||
limit: limit,
|
||||
isActive: isActive,
|
||||
search: search,
|
||||
);
|
||||
|
||||
// Store in cache
|
||||
_queryCache[cacheKey] = categories;
|
||||
_cacheTimestamps[cacheKey] = now;
|
||||
|
||||
log('πΎ Cached ${categories.length} categories for key: $cacheKey');
|
||||
return categories;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// REGULAR GET CATEGORIES
|
||||
// ========================================
|
||||
Future<List<CategoryModel>> getCategories({
|
||||
int page = 1,
|
||||
int limit = 10,
|
||||
bool isActive = true,
|
||||
String? search,
|
||||
}) async {
|
||||
final db = await _db;
|
||||
|
||||
try {
|
||||
String query = 'SELECT * FROM categories WHERE 1=1';
|
||||
List<dynamic> whereArgs = [];
|
||||
|
||||
// Note: Assuming is_active will be added to database schema
|
||||
if (isActive) {
|
||||
query += ' AND is_active = ?';
|
||||
whereArgs.add(1);
|
||||
}
|
||||
|
||||
if (search != null && search.isNotEmpty) {
|
||||
query += ' AND (name LIKE ? OR description LIKE ?)';
|
||||
whereArgs.add('%$search%');
|
||||
whereArgs.add('%$search%');
|
||||
}
|
||||
|
||||
query += ' ORDER BY name ASC';
|
||||
|
||||
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<CategoryModel> categories = [];
|
||||
for (final map in maps) {
|
||||
categories.add(_mapToCategory(map));
|
||||
}
|
||||
|
||||
log('π Retrieved ${categories.length} categories from database');
|
||||
return categories;
|
||||
} catch (e) {
|
||||
log('β Error getting categories: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// GET ALL CATEGORIES (For dropdowns)
|
||||
// ========================================
|
||||
Future<List<CategoryModel>> getAllCategories() async {
|
||||
const cacheKey = 'all_categories';
|
||||
final now = DateTime.now();
|
||||
|
||||
// Check cache
|
||||
if (_queryCache.containsKey(cacheKey) &&
|
||||
_cacheTimestamps.containsKey(cacheKey)) {
|
||||
final cacheTime = _cacheTimestamps[cacheKey]!;
|
||||
if (now.difference(cacheTime) < _cacheExpiry) {
|
||||
return _queryCache[cacheKey]!;
|
||||
}
|
||||
}
|
||||
|
||||
final db = await _db;
|
||||
|
||||
try {
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'categories',
|
||||
orderBy: 'name ASC',
|
||||
);
|
||||
|
||||
final categories = maps.map((map) => _mapToCategory(map)).toList();
|
||||
|
||||
// Cache all categories
|
||||
_queryCache[cacheKey] = categories;
|
||||
_cacheTimestamps[cacheKey] = now;
|
||||
|
||||
log('π Retrieved ${categories.length} total categories');
|
||||
return categories;
|
||||
} catch (e) {
|
||||
log('β Error getting all categories: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// GET CATEGORY BY ID
|
||||
// ========================================
|
||||
Future<CategoryModel?> getCategoryById(String id) async {
|
||||
final db = await _db;
|
||||
|
||||
try {
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'categories',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
|
||||
if (maps.isEmpty) {
|
||||
log('β Category not found: $id');
|
||||
return null;
|
||||
}
|
||||
|
||||
final category = _mapToCategory(maps.first);
|
||||
log('β
Category found: ${category.name}');
|
||||
return category;
|
||||
} catch (e) {
|
||||
log('β Error getting category by ID: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// GET TOTAL COUNT
|
||||
// ========================================
|
||||
Future<int> getTotalCount({bool isActive = true, String? search}) async {
|
||||
final db = await _db;
|
||||
|
||||
try {
|
||||
String query = 'SELECT COUNT(*) FROM categories WHERE 1=1';
|
||||
List<dynamic> whereArgs = [];
|
||||
|
||||
if (isActive) {
|
||||
query += ' AND is_active = ?';
|
||||
whereArgs.add(1);
|
||||
}
|
||||
|
||||
if (search != null && search.isNotEmpty) {
|
||||
query += ' AND (name LIKE ? OR description LIKE ?)';
|
||||
whereArgs.add('%$search%');
|
||||
whereArgs.add('%$search%');
|
||||
}
|
||||
|
||||
final result = await db.rawQuery(query, whereArgs);
|
||||
final count = Sqflite.firstIntValue(result) ?? 0;
|
||||
log('π Category total count: $count (isActive: $isActive, search: $search)');
|
||||
return count;
|
||||
} catch (e) {
|
||||
log('β Error getting category total count: $e');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// HAS CATEGORIES
|
||||
// ========================================
|
||||
Future<bool> hasCategories() async {
|
||||
final count = await getTotalCount();
|
||||
final hasData = count > 0;
|
||||
log('π Has categories: $hasData ($count categories)');
|
||||
return hasData;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CLEAR ALL CATEGORIES
|
||||
// ========================================
|
||||
Future<void> clearAllCategories() async {
|
||||
final db = await _db;
|
||||
|
||||
try {
|
||||
await db.delete('categories');
|
||||
clearCache();
|
||||
log('ποΈ All categories cleared from local DB');
|
||||
} catch (e) {
|
||||
log('β Error clearing categories: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CACHE MANAGEMENT
|
||||
// ========================================
|
||||
String _generateCacheKey(int page, int limit, bool isActive, String? search) {
|
||||
return 'categories_${page}_${limit}_${isActive}_${search ?? 'null'}';
|
||||
}
|
||||
|
||||
void clearCache() {
|
||||
final count = _queryCache.length;
|
||||
_queryCache.clear();
|
||||
_cacheTimestamps.clear();
|
||||
log('π§Ή Category 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 category cache cleared: ${expiredKeys.length} entries');
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// DATABASE STATS
|
||||
// ========================================
|
||||
Future<Map<String, dynamic>> getDatabaseStats() async {
|
||||
final db = await _db;
|
||||
|
||||
try {
|
||||
final categoryCount = Sqflite.firstIntValue(
|
||||
await db.rawQuery('SELECT COUNT(*) FROM categories')) ??
|
||||
0;
|
||||
|
||||
final activeCount = Sqflite.firstIntValue(await db.rawQuery(
|
||||
'SELECT COUNT(*) FROM categories WHERE is_active = 1')) ??
|
||||
0;
|
||||
|
||||
final stats = {
|
||||
'total_categories': categoryCount,
|
||||
'active_categories': activeCount,
|
||||
'cache_entries': _queryCache.length,
|
||||
};
|
||||
|
||||
log('π Category Database Stats: $stats');
|
||||
return stats;
|
||||
} catch (e) {
|
||||
log('β Error getting category database stats: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// HELPER METHODS
|
||||
// ========================================
|
||||
Map<String, dynamic> _categoryToMap(CategoryModel category) {
|
||||
return {
|
||||
'id': category.id,
|
||||
'organization_id': category.organizationId,
|
||||
'name': category.name,
|
||||
'description': category.description,
|
||||
'business_type': category.businessType,
|
||||
'metadata': json.encode(category.metadata),
|
||||
'is_active': 1, // Assuming all synced categories are active
|
||||
'created_at': category.createdAt.toIso8601String(),
|
||||
'updated_at': category.updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
CategoryModel _mapToCategory(Map<String, dynamic> map) {
|
||||
return CategoryModel(
|
||||
id: map['id'],
|
||||
organizationId: map['organization_id'],
|
||||
name: map['name'],
|
||||
description: map['description'],
|
||||
businessType: map['business_type'],
|
||||
metadata: map['metadata'] != null ? json.decode(map['metadata']) : {},
|
||||
createdAt: DateTime.parse(map['created_at']),
|
||||
updatedAt: DateTime.parse(map['updated_at']),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
import 'dart:developer';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:enaklo_pos/data/datasources/category/category_local_datasource.dart';
|
||||
import 'package:enaklo_pos/data/datasources/category/category_remote_datasource.dart';
|
||||
import 'package:enaklo_pos/data/models/response/category_response_model.dart';
|
||||
|
||||
class CategoryRepository {
|
||||
static CategoryRepository? _instance;
|
||||
|
||||
final CategoryLocalDatasource _localDatasource;
|
||||
final CategoryRemoteDatasource _remoteDatasource;
|
||||
|
||||
CategoryRepository._internal()
|
||||
: _localDatasource = CategoryLocalDatasource.instance,
|
||||
_remoteDatasource = CategoryRemoteDatasource();
|
||||
|
||||
static CategoryRepository get instance {
|
||||
_instance ??= CategoryRepository._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SYNC STRATEGY: REMOTE-FIRST WITH LOCAL FALLBACK
|
||||
// ========================================
|
||||
Future<Either<String, CategoryResponseModel>> getCategories({
|
||||
int page = 1,
|
||||
int limit = 10,
|
||||
bool isActive = true,
|
||||
String? search,
|
||||
bool forceRemote = false,
|
||||
}) async {
|
||||
try {
|
||||
log('π± Getting categories - page: $page, isActive: $isActive, search: $search, forceRemote: $forceRemote');
|
||||
|
||||
// Clean expired cache
|
||||
_localDatasource.clearExpiredCache();
|
||||
|
||||
// Check if we should try remote first
|
||||
if (forceRemote || !await _localDatasource.hasCategories()) {
|
||||
log('π Attempting remote fetch first...');
|
||||
|
||||
final remoteResult = await _getRemoteCategories(
|
||||
page: page,
|
||||
limit: limit,
|
||||
isActive: isActive,
|
||||
);
|
||||
|
||||
return await remoteResult.fold(
|
||||
(failure) async {
|
||||
log('β Remote fetch failed: $failure');
|
||||
log('π± Falling back to local data...');
|
||||
return _getLocalCategories(
|
||||
page: page,
|
||||
limit: limit,
|
||||
isActive: isActive,
|
||||
search: search,
|
||||
);
|
||||
},
|
||||
(response) async {
|
||||
log('β
Remote fetch successful, syncing to local...');
|
||||
|
||||
// Sync remote data to local
|
||||
if (response.data.categories.isNotEmpty) {
|
||||
await _syncToLocal(response.data.categories,
|
||||
clearFirst: page == 1);
|
||||
}
|
||||
|
||||
return Right(response);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
log('π± Using local data (cache available)...');
|
||||
return _getLocalCategories(
|
||||
page: page,
|
||||
limit: limit,
|
||||
isActive: isActive,
|
||||
search: search,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
log('β Error in getCategories: $e');
|
||||
return Left('Gagal memuat kategori: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// PURE LOCAL OPERATIONS
|
||||
// ========================================
|
||||
Future<Either<String, CategoryResponseModel>> _getLocalCategories({
|
||||
int page = 1,
|
||||
int limit = 10,
|
||||
bool isActive = true,
|
||||
String? search,
|
||||
}) async {
|
||||
try {
|
||||
final cachedCategories = await _localDatasource.getCachedCategories(
|
||||
page: page,
|
||||
limit: limit,
|
||||
isActive: isActive,
|
||||
search: search,
|
||||
);
|
||||
|
||||
final totalCount = await _localDatasource.getTotalCount(
|
||||
isActive: isActive,
|
||||
search: search,
|
||||
);
|
||||
|
||||
final categoryData = CategoryData(
|
||||
categories: cachedCategories,
|
||||
totalCount: totalCount,
|
||||
page: page,
|
||||
limit: limit,
|
||||
totalPages: totalCount > 0 ? (totalCount / limit).ceil() : 0,
|
||||
);
|
||||
|
||||
final response = CategoryResponseModel(
|
||||
success: true,
|
||||
data: categoryData,
|
||||
);
|
||||
|
||||
log('β
Returned ${cachedCategories.length} local categories (${totalCount} total)');
|
||||
return Right(response);
|
||||
} catch (e) {
|
||||
log('β Error getting local categories: $e');
|
||||
return Left('Gagal memuat kategori dari database lokal: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// REMOTE FETCH
|
||||
// ========================================
|
||||
Future<Either<String, CategoryResponseModel>> _getRemoteCategories({
|
||||
int page = 1,
|
||||
int limit = 10,
|
||||
bool isActive = true,
|
||||
}) async {
|
||||
try {
|
||||
log('π Fetching categories from remote...');
|
||||
return await _remoteDatasource.getCategories(
|
||||
page: page,
|
||||
limit: limit,
|
||||
isActive: isActive,
|
||||
);
|
||||
} catch (e) {
|
||||
log('β Remote fetch error: $e');
|
||||
return Left('Gagal mengambil data dari server: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SYNC TO LOCAL
|
||||
// ========================================
|
||||
Future<void> _syncToLocal(List<CategoryModel> categories,
|
||||
{bool clearFirst = false}) async {
|
||||
try {
|
||||
log('πΎ Syncing ${categories.length} categories to local database...');
|
||||
await _localDatasource.saveCategoriesBatch(categories,
|
||||
clearFirst: clearFirst);
|
||||
log('β
Categories synced to local successfully');
|
||||
} catch (e) {
|
||||
log('β Error syncing categories to local: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MANUAL SYNC OPERATIONS
|
||||
// ========================================
|
||||
Future<Either<String, String>> syncAllCategories() async {
|
||||
try {
|
||||
log('π Starting manual sync of all categories...');
|
||||
|
||||
int page = 1;
|
||||
const limit = 50; // Higher limit for bulk sync
|
||||
bool hasMore = true;
|
||||
int totalSynced = 0;
|
||||
|
||||
// Clear local data first for fresh sync
|
||||
await _localDatasource.clearAllCategories();
|
||||
|
||||
while (hasMore) {
|
||||
log('π Syncing page $page...');
|
||||
|
||||
final result = await _remoteDatasource.getCategories(
|
||||
page: page,
|
||||
limit: limit,
|
||||
isActive: true,
|
||||
);
|
||||
|
||||
await result.fold(
|
||||
(failure) async {
|
||||
log('β Sync failed at page $page: $failure');
|
||||
throw Exception(failure);
|
||||
},
|
||||
(response) async {
|
||||
final categories = response.data.categories;
|
||||
|
||||
if (categories.isNotEmpty) {
|
||||
await _localDatasource.saveCategoriesBatch(
|
||||
categories,
|
||||
clearFirst: false, // Don't clear on subsequent pages
|
||||
);
|
||||
totalSynced += categories.length;
|
||||
|
||||
// Check if we have more pages
|
||||
hasMore = page < response.data.totalPages;
|
||||
page++;
|
||||
|
||||
log('π¦ Page $page synced: ${categories.length} categories');
|
||||
} else {
|
||||
hasMore = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final message = 'Berhasil sinkronisasi $totalSynced kategori';
|
||||
log('β
$message');
|
||||
return Right(message);
|
||||
} catch (e) {
|
||||
final error = 'Gagal sinkronisasi kategori: $e';
|
||||
log('β $error');
|
||||
return Left(error);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// UTILITY METHODS
|
||||
// ========================================
|
||||
Future<Either<String, CategoryResponseModel>> refreshCategories({
|
||||
bool isActive = true,
|
||||
String? search,
|
||||
}) async {
|
||||
log('π Refreshing categories...');
|
||||
clearCache();
|
||||
|
||||
return await getCategories(
|
||||
page: 1,
|
||||
limit: 10,
|
||||
isActive: isActive,
|
||||
search: search,
|
||||
forceRemote: true, // Force remote refresh
|
||||
);
|
||||
}
|
||||
|
||||
Future<CategoryModel?> getCategoryById(String id) async {
|
||||
log('π Getting category by ID: $id');
|
||||
return await _localDatasource.getCategoryById(id);
|
||||
}
|
||||
|
||||
Future<List<CategoryModel>> getAllCategories() async {
|
||||
log('π Getting all categories for dropdown...');
|
||||
return await _localDatasource.getAllCategories();
|
||||
}
|
||||
|
||||
Future<bool> hasLocalCategories() async {
|
||||
final hasCategories = await _localDatasource.hasCategories();
|
||||
log('π Has local categories: $hasCategories');
|
||||
return hasCategories;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getDatabaseStats() async {
|
||||
final stats = await _localDatasource.getDatabaseStats();
|
||||
log('π Category database stats: $stats');
|
||||
return stats;
|
||||
}
|
||||
|
||||
void clearCache() {
|
||||
log('π§Ή Clearing category cache');
|
||||
_localDatasource.clearCache();
|
||||
}
|
||||
|
||||
Future<bool> isLocalDatabaseReady() async {
|
||||
try {
|
||||
final stats = await getDatabaseStats();
|
||||
final categoryCount = stats['total_categories'] ?? 0;
|
||||
final isReady = categoryCount > 0;
|
||||
log('π Category database ready: $isReady ($categoryCount categories)');
|
||||
return isReady;
|
||||
} catch (e) {
|
||||
log('β Error checking category database readiness: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user