category repo

This commit is contained in:
efrilm
2025-10-24 20:06:42 +07:00
parent 683fff6eeb
commit 71fa4823fc
29 changed files with 5434 additions and 30 deletions
@@ -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));
}
}
}