category repo
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:data_channel/data_channel.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
||||
import '../../../common/constant/app_constant.dart';
|
||||
import '../../../common/database/database_helper.dart';
|
||||
import '../../../domain/category/category.dart';
|
||||
import '../category_dtos.dart';
|
||||
|
||||
@injectable
|
||||
class CategoryLocalDataProvider {
|
||||
final DatabaseHelper _databaseHelper;
|
||||
final _logName = 'CategoryLocalDataProvider';
|
||||
|
||||
CategoryLocalDataProvider(this._databaseHelper);
|
||||
|
||||
final Map<String, List<CategoryDto>> _queryCache = {};
|
||||
final Duration _cacheExpiry = Duration(minutes: AppConstant.cacheExpire);
|
||||
final Map<String, DateTime> _cacheTimestamps = {};
|
||||
|
||||
Future<DC<CategoryFailure, void>> saveCategoriesBatch(
|
||||
List<CategoryDto> categories, {
|
||||
bool clearFirst = false,
|
||||
}) async {
|
||||
final db = await _databaseHelper.database;
|
||||
|
||||
try {
|
||||
await db.transaction((txn) async {
|
||||
if (clearFirst) {
|
||||
log('🗑️ Clearing existing categories...', name: _logName);
|
||||
await txn.delete('categories');
|
||||
}
|
||||
|
||||
log(
|
||||
'💾 Batch saving ${categories.length} categories...',
|
||||
name: _logName,
|
||||
);
|
||||
|
||||
// Batch insert categories
|
||||
final batch = txn.batch();
|
||||
for (final category in categories) {
|
||||
batch.insert(
|
||||
'categories',
|
||||
category.toMap(),
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
await batch.commit(noResult: true);
|
||||
});
|
||||
|
||||
// Clear cache after update
|
||||
clearCache();
|
||||
log(
|
||||
'✅ Successfully batch saved ${categories.length} categories',
|
||||
name: _logName,
|
||||
);
|
||||
|
||||
return DC.data(null);
|
||||
} catch (e, s) {
|
||||
log(
|
||||
'❌ Error batch saving categories',
|
||||
name: _logName,
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
return DC.error(CategoryFailure.dynamicErrorMessage(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<DC<CategoryFailure, List<CategoryDto>>> getCachedCategories({
|
||||
int page = 1,
|
||||
int limit = 10,
|
||||
bool isActive = true,
|
||||
String? search,
|
||||
}) async {
|
||||
final cacheKey = _generateCacheKey(page, limit, isActive, search);
|
||||
final now = DateTime.now();
|
||||
|
||||
try {
|
||||
// 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)',
|
||||
name: _logName,
|
||||
);
|
||||
return DC.data(_queryCache[cacheKey]!);
|
||||
}
|
||||
}
|
||||
|
||||
log('📀 Cache MISS: $cacheKey, querying database...', name: _logName);
|
||||
|
||||
// Cache miss, query database
|
||||
final result = await getCategories(
|
||||
page: page,
|
||||
limit: limit,
|
||||
isActive: isActive,
|
||||
search: search,
|
||||
);
|
||||
|
||||
// Check if result has data or error
|
||||
if (result.hasData) {
|
||||
final categories = result.data!;
|
||||
|
||||
// Store in cache
|
||||
_queryCache[cacheKey] = categories;
|
||||
_cacheTimestamps[cacheKey] = now;
|
||||
|
||||
log(
|
||||
'💾 Cached ${categories.length} categories for key: $cacheKey',
|
||||
name: _logName,
|
||||
);
|
||||
|
||||
return DC.data(categories);
|
||||
} else {
|
||||
// Return error from database query
|
||||
return DC.error(result.error!);
|
||||
}
|
||||
} catch (e, s) {
|
||||
log(
|
||||
'❌ Error getting cached categories',
|
||||
name: _logName,
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
return DC.error(CategoryFailure.localStorageError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<DC<CategoryFailure, List<CategoryDto>>> getCategories({
|
||||
int page = 1,
|
||||
int limit = 10,
|
||||
bool isActive = true,
|
||||
String? search,
|
||||
}) async {
|
||||
final db = await _databaseHelper.database;
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
final categories = maps.map((map) => CategoryDto.fromMap(map)).toList();
|
||||
|
||||
log(
|
||||
'📊 Retrieved ${categories.length} categories from database',
|
||||
name: _logName,
|
||||
);
|
||||
|
||||
return DC.data(categories);
|
||||
} catch (e, s) {
|
||||
log(
|
||||
'❌ Error getting categories',
|
||||
name: _logName,
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
return DC.error(CategoryFailure.localStorageError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<DC<CategoryFailure, CategoryDto>> getCategoryById(String id) async {
|
||||
final db = await _databaseHelper.database;
|
||||
|
||||
try {
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
'categories',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
|
||||
if (maps.isEmpty) {
|
||||
log('❌ Category not found: $id', name: _logName);
|
||||
return DC.error(CategoryFailure.empty());
|
||||
}
|
||||
|
||||
final category = CategoryDto.fromMap(maps.first);
|
||||
log('✅ Category found: ${category.name}', name: _logName);
|
||||
return DC.data(category);
|
||||
} catch (e, s) {
|
||||
log(
|
||||
'❌ Error getting category by ID',
|
||||
name: _logName,
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
return DC.error(CategoryFailure.localStorageError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> getTotalCount({bool isActive = true, String? search}) async {
|
||||
final db = await _databaseHelper.database;
|
||||
|
||||
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)',
|
||||
name: _logName,
|
||||
);
|
||||
return count;
|
||||
} catch (e) {
|
||||
log('❌ Error getting category total count: $e', name: _logName);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> hasCategories() async {
|
||||
final count = await getTotalCount();
|
||||
final hasData = count > 0;
|
||||
log('🔍 Has categories: $hasData ($count categories)', name: _logName);
|
||||
return hasData;
|
||||
}
|
||||
|
||||
Future<DC<CategoryFailure, Map<String, dynamic>>> getDatabaseStats() async {
|
||||
final db = await _databaseHelper.database;
|
||||
|
||||
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,
|
||||
'last_updated': DateTime.now().toIso8601String(),
|
||||
};
|
||||
|
||||
log('📊 Category Database Stats: $stats', name: _logName);
|
||||
return DC.data(stats);
|
||||
} catch (e, s) {
|
||||
log(
|
||||
'❌ Error getting category database stats',
|
||||
name: _logName,
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
return DC.error(
|
||||
CategoryFailure.localStorageError(
|
||||
'Gagal memuat statistik database: $e',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearAllCategories() async {
|
||||
final db = await _databaseHelper.database;
|
||||
|
||||
try {
|
||||
await db.delete('categories');
|
||||
clearCache();
|
||||
log('🗑️ All categories cleared from local DB');
|
||||
} catch (e) {
|
||||
log('❌ Error clearing categories: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void clearCache() {
|
||||
final count = _queryCache.length;
|
||||
_queryCache.clear();
|
||||
_cacheTimestamps.clear();
|
||||
log('🧹 Category cache cleared: $count entries removed', name: _logName);
|
||||
}
|
||||
|
||||
String _generateCacheKey(int page, int limit, bool isActive, String? search) {
|
||||
return 'categories_${page}_${limit}_${isActive}_${search ?? 'null'}';
|
||||
}
|
||||
|
||||
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',
|
||||
name: _logName,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:data_channel/data_channel.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import '../../../common/api/api_client.dart';
|
||||
import '../../../common/api/api_failure.dart';
|
||||
import '../../../common/function/app_function.dart';
|
||||
import '../../../common/url/api_path.dart';
|
||||
import '../../../domain/category/category.dart';
|
||||
import '../category_dtos.dart';
|
||||
|
||||
@injectable
|
||||
class CategoryRemoteDataProvider {
|
||||
final ApiClient _apiClient;
|
||||
final _logName = 'CategoryRemoteDataProvider';
|
||||
|
||||
CategoryRemoteDataProvider(this._apiClient);
|
||||
|
||||
Future<DC<CategoryFailure, ListCategoryDto>> fetchCategories({
|
||||
int page = 1,
|
||||
int limit = 10,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
ApiPath.categories,
|
||||
params: {'page': page, 'limit': limit},
|
||||
headers: getAuthorizationHeader(),
|
||||
);
|
||||
|
||||
if (response.data['data'] == null) {
|
||||
return DC.error(CategoryFailure.empty());
|
||||
}
|
||||
|
||||
final categories = ListCategoryDto.fromJson(
|
||||
response.data['data'] as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
return DC.data(categories);
|
||||
} on ApiFailure catch (e, s) {
|
||||
log('fetchCategoryError', name: _logName, error: e, stackTrace: s);
|
||||
return DC.error(CategoryFailure.serverError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user