product repo
This commit is contained in:
parent
dea5de8828
commit
4fdd1e44f8
345
lib/application/product/product_loader/product_loader_bloc.dart
Normal file
345
lib/application/product/product_loader/product_loader_bloc.dart
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
|
||||||
|
import '../../../domain/product/product.dart';
|
||||||
|
|
||||||
|
part 'product_loader_event.dart';
|
||||||
|
part 'product_loader_state.dart';
|
||||||
|
part 'product_loader_bloc.freezed.dart';
|
||||||
|
|
||||||
|
@injectable
|
||||||
|
class ProductLoaderBloc extends Bloc<ProductLoaderEvent, ProductLoaderState> {
|
||||||
|
final IProductRepository _productRepository;
|
||||||
|
|
||||||
|
Timer? _loadMoreDebounce;
|
||||||
|
Timer? _searchDebounce;
|
||||||
|
|
||||||
|
ProductLoaderBloc(this._productRepository)
|
||||||
|
: super(ProductLoaderState.initial()) {
|
||||||
|
on<ProductLoaderEvent>(_onProductLoaderEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onProductLoaderEvent(
|
||||||
|
ProductLoaderEvent event,
|
||||||
|
Emitter<ProductLoaderState> emit,
|
||||||
|
) {
|
||||||
|
return event.map(
|
||||||
|
getProduct: (e) async {
|
||||||
|
emit(state.copyWith(isLoadingMore: true));
|
||||||
|
|
||||||
|
log(
|
||||||
|
'📱 Loading local products - categoryId: ${e.categoryId}, search: ${e.search}',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pastikan database lokal sudah siap
|
||||||
|
final isReady = await _productRepository.isLocalDatabaseReady();
|
||||||
|
if (!isReady) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isLoadingMore: false,
|
||||||
|
failureOptionProduct: optionOf(
|
||||||
|
ProductFailure.dynamicErrorMessage(
|
||||||
|
'Database lokal belum siap. Silakan lakukan sinkronisasi data terlebih dahulu.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await _productRepository.getProducts(
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
categoryId: e.categoryId,
|
||||||
|
search: e.search,
|
||||||
|
);
|
||||||
|
|
||||||
|
await result.fold(
|
||||||
|
(failure) async {
|
||||||
|
log('❌ Error loading local products: $failure');
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isLoadingMore: false,
|
||||||
|
failureOptionProduct: optionOf(failure),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(response) async {
|
||||||
|
final products = response.products;
|
||||||
|
final totalPages = response.totalPages;
|
||||||
|
final hasReachedMax = products.length < 10 || 1 >= totalPages;
|
||||||
|
|
||||||
|
log(
|
||||||
|
'✅ Local products loaded: ${products.length}, hasReachedMax: $hasReachedMax, totalPages: $totalPages',
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
products: products,
|
||||||
|
page: 1,
|
||||||
|
hasReachedMax: hasReachedMax,
|
||||||
|
isLoadingMore: false,
|
||||||
|
failureOptionProduct: none(),
|
||||||
|
categoryId: e.categoryId,
|
||||||
|
searchQuery: e.search,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loadMore: (e) async {
|
||||||
|
final currentState = state;
|
||||||
|
|
||||||
|
// Cegah double load
|
||||||
|
if (currentState.isLoadingMore || currentState.hasReachedMax) {
|
||||||
|
log(
|
||||||
|
'⏹️ Load more blocked - isLoadingMore: ${currentState.isLoadingMore}, hasReachedMax: ${currentState.hasReachedMax}',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(currentState.copyWith(isLoadingMore: true));
|
||||||
|
|
||||||
|
final nextPage = currentState.page + 1;
|
||||||
|
log('📄 Loading more local products - page: $nextPage');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await _productRepository.getProducts(
|
||||||
|
page: nextPage,
|
||||||
|
limit: 10,
|
||||||
|
categoryId: currentState.categoryId,
|
||||||
|
search: currentState.searchQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
await result.fold(
|
||||||
|
(failure) async {
|
||||||
|
log('❌ Error loading more local products: $failure');
|
||||||
|
emit(
|
||||||
|
currentState.copyWith(
|
||||||
|
isLoadingMore: false,
|
||||||
|
failureOptionProduct: optionOf(failure),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(response) async {
|
||||||
|
final newProducts = response.products;
|
||||||
|
final totalPages = response.totalPages;
|
||||||
|
|
||||||
|
// Hindari duplikat produk
|
||||||
|
final currentProductIds = currentState.products
|
||||||
|
.map((p) => p.id)
|
||||||
|
.toSet();
|
||||||
|
final filteredNewProducts = newProducts
|
||||||
|
.where((product) => !currentProductIds.contains(product.id))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final allProducts = [
|
||||||
|
...currentState.products,
|
||||||
|
...filteredNewProducts,
|
||||||
|
];
|
||||||
|
|
||||||
|
final hasReachedMax =
|
||||||
|
filteredNewProducts.length < 10 || nextPage >= totalPages;
|
||||||
|
|
||||||
|
log(
|
||||||
|
'✅ More local products loaded: ${filteredNewProducts.length} new, total: ${allProducts.length}, hasReachedMax: $hasReachedMax',
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(
|
||||||
|
currentState.copyWith(
|
||||||
|
products: allProducts,
|
||||||
|
page: nextPage,
|
||||||
|
hasReachedMax: hasReachedMax,
|
||||||
|
isLoadingMore: false,
|
||||||
|
failureOptionProduct: none(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
log('❌ Exception loading more local products: $e');
|
||||||
|
emit(
|
||||||
|
currentState.copyWith(
|
||||||
|
isLoadingMore: false,
|
||||||
|
failureOptionProduct: optionOf(
|
||||||
|
ProductFailure.dynamicErrorMessage(
|
||||||
|
'Gagal memuat produk tambahan: $e',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refresh: (e) async {
|
||||||
|
final categoryId = state.categoryId;
|
||||||
|
final searchQuery = state.searchQuery;
|
||||||
|
|
||||||
|
_loadMoreDebounce?.cancel();
|
||||||
|
_searchDebounce?.cancel();
|
||||||
|
|
||||||
|
log(
|
||||||
|
'🔄 Refreshing local products - categoryId: $categoryId, search: $searchQuery',
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(state.copyWith(isLoadingMore: true));
|
||||||
|
|
||||||
|
try {
|
||||||
|
_productRepository.clearCache();
|
||||||
|
|
||||||
|
final result = await _productRepository.refreshProducts(
|
||||||
|
categoryId: categoryId,
|
||||||
|
search: searchQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
await result.fold(
|
||||||
|
(failure) async {
|
||||||
|
log('❌ Failed to refresh local products: $failure');
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isLoadingMore: false,
|
||||||
|
failureOptionProduct: optionOf(failure),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(response) async {
|
||||||
|
final products = response.products;
|
||||||
|
final totalPages = response.totalPages;
|
||||||
|
final hasReachedMax = products.length < 10 || 1 >= totalPages;
|
||||||
|
|
||||||
|
log('✅ Refreshed local products: ${products.length}');
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
products: products,
|
||||||
|
hasReachedMax: hasReachedMax,
|
||||||
|
page: 1,
|
||||||
|
isLoadingMore: false,
|
||||||
|
failureOptionProduct: none(),
|
||||||
|
categoryId: categoryId,
|
||||||
|
searchQuery: searchQuery,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
log('❌ Exception refreshing local products: $e');
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isLoadingMore: false,
|
||||||
|
failureOptionProduct: optionOf(
|
||||||
|
ProductFailure.dynamicErrorMessage(e.toString()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} finally {}
|
||||||
|
},
|
||||||
|
searchProduct: (e) async {
|
||||||
|
_searchDebounce?.cancel();
|
||||||
|
|
||||||
|
// Debounce ringan agar UX lebih halus
|
||||||
|
_searchDebounce = Timer(const Duration(milliseconds: 150), () async {
|
||||||
|
emit(state.copyWith(isLoadingMore: true));
|
||||||
|
|
||||||
|
log('🔍 Local search: "${e.query}"');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await _productRepository.getProducts(
|
||||||
|
page: 1,
|
||||||
|
limit: 20, // lebih banyak hasil untuk pencarian
|
||||||
|
categoryId: e.categoryId,
|
||||||
|
search: e.query,
|
||||||
|
);
|
||||||
|
|
||||||
|
await result.fold(
|
||||||
|
(failure) async {
|
||||||
|
log('❌ Local search error: $failure');
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isLoadingMore: false,
|
||||||
|
failureOptionProduct: optionOf(failure),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(response) async {
|
||||||
|
final products = response.products;
|
||||||
|
final totalPages = response.totalPages;
|
||||||
|
final hasReachedMax = products.length < 20 || 1 >= totalPages;
|
||||||
|
|
||||||
|
log(
|
||||||
|
'✅ Local search results: ${products.length} products found',
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
products: products,
|
||||||
|
hasReachedMax: hasReachedMax,
|
||||||
|
page: 1,
|
||||||
|
isLoadingMore: false,
|
||||||
|
categoryId: e.categoryId,
|
||||||
|
searchQuery: e.query,
|
||||||
|
failureOptionProduct: none(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
log('❌ Exception during local search: $e');
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isLoadingMore: false,
|
||||||
|
failureOptionProduct: optionOf(
|
||||||
|
ProductFailure.dynamicErrorMessage(e.toString()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getDatabaseStats: (e) async {
|
||||||
|
log('📊 Getting local database stats...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await _productRepository.getDatabaseStats();
|
||||||
|
|
||||||
|
await result.fold(
|
||||||
|
(failure) async {
|
||||||
|
log('❌ Failed to get database stats: $failure');
|
||||||
|
emit(state.copyWith(failureOptionProduct: optionOf(failure)));
|
||||||
|
},
|
||||||
|
(stats) async {
|
||||||
|
log('✅ Local database stats retrieved: $stats');
|
||||||
|
// Jika UI kamu perlu tampilkan, bisa simpan ke state, misalnya:
|
||||||
|
// emit(state.copyWith(databaseStats: some(stats)));
|
||||||
|
// Tapi kalau hanya untuk log/debug, tidak perlu ubah state
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
log(
|
||||||
|
'❌ Exception while getting database stats: $e',
|
||||||
|
error: e,
|
||||||
|
stackTrace: s,
|
||||||
|
);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
failureOptionProduct: optionOf(
|
||||||
|
ProductFailure.dynamicErrorMessage(e.toString()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearCache: (e) async {
|
||||||
|
log('🧹 Manually clearing local cache');
|
||||||
|
_productRepository.clearCache();
|
||||||
|
|
||||||
|
// Refresh current data after cache clear
|
||||||
|
add(const ProductLoaderEvent.refresh());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,26 @@
|
|||||||
|
part of 'product_loader_bloc.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ProductLoaderEvent with _$ProductLoaderEvent {
|
||||||
|
const factory ProductLoaderEvent.getProduct({
|
||||||
|
String? categoryId,
|
||||||
|
String? search, // Added search parameter
|
||||||
|
bool? forceRefresh, // Kept for compatibility but ignored
|
||||||
|
}) = _GetProduct;
|
||||||
|
|
||||||
|
const factory ProductLoaderEvent.loadMore({
|
||||||
|
String? categoryId,
|
||||||
|
String? search,
|
||||||
|
}) = _LoadMore;
|
||||||
|
|
||||||
|
const factory ProductLoaderEvent.refresh() = _Refresh;
|
||||||
|
|
||||||
|
const factory ProductLoaderEvent.searchProduct({
|
||||||
|
String? query,
|
||||||
|
String? categoryId,
|
||||||
|
}) = _SearchProduct;
|
||||||
|
|
||||||
|
const factory ProductLoaderEvent.getDatabaseStats() = _GetDatabaseStats;
|
||||||
|
|
||||||
|
const factory ProductLoaderEvent.clearCache() = _ClearCache;
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
part of 'product_loader_bloc.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ProductLoaderState with _$ProductLoaderState {
|
||||||
|
factory ProductLoaderState({
|
||||||
|
required List<Product> products,
|
||||||
|
required Option<ProductFailure> failureOptionProduct,
|
||||||
|
@Default(false) bool hasReachedMax,
|
||||||
|
@Default(1) int page,
|
||||||
|
@Default(false) bool isLoadingMore,
|
||||||
|
String? searchQuery,
|
||||||
|
String? categoryId,
|
||||||
|
}) = _ProductLoaderState;
|
||||||
|
|
||||||
|
factory ProductLoaderState.initial() =>
|
||||||
|
ProductLoaderState(products: [], failureOptionProduct: none());
|
||||||
|
}
|
||||||
@ -2,4 +2,5 @@ class ApiPath {
|
|||||||
static const String login = '/api/v1/auth/login';
|
static const String login = '/api/v1/auth/login';
|
||||||
static const String outlets = '/api/v1/outlets';
|
static const String outlets = '/api/v1/outlets';
|
||||||
static const String categories = '/api/v1/categories';
|
static const String categories = '/api/v1/categories';
|
||||||
|
static const String products = '/api/v1/products';
|
||||||
}
|
}
|
||||||
|
|||||||
86
lib/domain/product/entities/product_entity.dart
Normal file
86
lib/domain/product/entities/product_entity.dart
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
part of '../product.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ListProduct with _$ListProduct {
|
||||||
|
const factory ListProduct({
|
||||||
|
required List<Product> products,
|
||||||
|
required int totalCount,
|
||||||
|
required int page,
|
||||||
|
required int limit,
|
||||||
|
required int totalPages,
|
||||||
|
}) = _ListProduct;
|
||||||
|
|
||||||
|
factory ListProduct.empty() => const ListProduct(
|
||||||
|
products: [],
|
||||||
|
totalCount: 0,
|
||||||
|
page: 0,
|
||||||
|
limit: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class Product with _$Product {
|
||||||
|
const factory Product({
|
||||||
|
required String id,
|
||||||
|
required String organizationId,
|
||||||
|
required String categoryId,
|
||||||
|
required String sku,
|
||||||
|
required String name,
|
||||||
|
required String description,
|
||||||
|
required double price,
|
||||||
|
required double cost,
|
||||||
|
required String businessType,
|
||||||
|
required String imageUrl,
|
||||||
|
required String printerType,
|
||||||
|
required Map<String, dynamic> metadata,
|
||||||
|
required bool isActive,
|
||||||
|
required String createdAt,
|
||||||
|
required String updatedAt,
|
||||||
|
required List<ProductVariant> variants,
|
||||||
|
}) = _Product;
|
||||||
|
|
||||||
|
factory Product.empty() => const Product(
|
||||||
|
id: '',
|
||||||
|
organizationId: '',
|
||||||
|
categoryId: '',
|
||||||
|
sku: '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
price: 0.0,
|
||||||
|
cost: 0.0,
|
||||||
|
businessType: '',
|
||||||
|
imageUrl: '',
|
||||||
|
printerType: '',
|
||||||
|
metadata: {},
|
||||||
|
isActive: false,
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
variants: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ProductVariant with _$ProductVariant {
|
||||||
|
const factory ProductVariant({
|
||||||
|
required String id,
|
||||||
|
required String productId,
|
||||||
|
required String name,
|
||||||
|
required double priceModifier,
|
||||||
|
required double cost,
|
||||||
|
required Map<String, dynamic> metadata,
|
||||||
|
required String createdAt,
|
||||||
|
required String updatedAt,
|
||||||
|
}) = _ProductVariant;
|
||||||
|
|
||||||
|
factory ProductVariant.empty() => const ProductVariant(
|
||||||
|
id: '',
|
||||||
|
productId: '',
|
||||||
|
name: '',
|
||||||
|
priceModifier: 0.0,
|
||||||
|
cost: 0.0,
|
||||||
|
metadata: {},
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
);
|
||||||
|
}
|
||||||
12
lib/domain/product/failures/product_failure.dart
Normal file
12
lib/domain/product/failures/product_failure.dart
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
part of '../product.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class ProductFailure with _$ProductFailure {
|
||||||
|
const factory ProductFailure.serverError(ApiFailure failure) = _ServerError;
|
||||||
|
const factory ProductFailure.unexpectedError() = _UnexpectedError;
|
||||||
|
const factory ProductFailure.empty() = _Empty;
|
||||||
|
const factory ProductFailure.localStorageError(String erroMessage) =
|
||||||
|
_LocalStorageError;
|
||||||
|
const factory ProductFailure.dynamicErrorMessage(String erroMessage) =
|
||||||
|
_DynamicErrorMessage;
|
||||||
|
}
|
||||||
11
lib/domain/product/product.dart
Normal file
11
lib/domain/product/product.dart
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
import '../../common/api/api_failure.dart';
|
||||||
|
|
||||||
|
part 'product.freezed.dart';
|
||||||
|
|
||||||
|
part 'entities/product_entity.dart';
|
||||||
|
part 'failures/product_failure.dart';
|
||||||
|
part 'repositories/i_product_repository.dart';
|
||||||
1964
lib/domain/product/product.freezed.dart
Normal file
1964
lib/domain/product/product.freezed.dart
Normal file
File diff suppressed because it is too large
Load Diff
34
lib/domain/product/repositories/i_product_repository.dart
Normal file
34
lib/domain/product/repositories/i_product_repository.dart
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
part of '../product.dart';
|
||||||
|
|
||||||
|
abstract class IProductRepository {
|
||||||
|
Future<Either<ProductFailure, ListProduct>> getProducts({
|
||||||
|
int page = 1,
|
||||||
|
int limit = 10,
|
||||||
|
String? categoryId,
|
||||||
|
String? search,
|
||||||
|
bool forceRefresh = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<Either<ProductFailure, List<Product>>> searchProductsOptimized(
|
||||||
|
String query,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<Either<ProductFailure, String>> syncAllProducts();
|
||||||
|
|
||||||
|
Future<Either<ProductFailure, ListProduct>> refreshProducts({
|
||||||
|
String? categoryId,
|
||||||
|
String? search,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<Either<ProductFailure, Product>> getProductById(String id);
|
||||||
|
|
||||||
|
Future<bool> hasLocalProducts();
|
||||||
|
|
||||||
|
Future<Either<ProductFailure, Map<String, dynamic>>> getDatabaseStats();
|
||||||
|
|
||||||
|
void clearCache();
|
||||||
|
|
||||||
|
Future<bool> isLocalDatabaseReady();
|
||||||
|
|
||||||
|
Future<void> clearAllProducts();
|
||||||
|
}
|
||||||
486
lib/infrastructure/product/datasources/local_data_provider.dart
Normal file
486
lib/infrastructure/product/datasources/local_data_provider.dart
Normal file
@ -0,0 +1,486 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:data_channel/data_channel.dart';
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
import '../../../common/constant/app_constant.dart';
|
||||||
|
import '../../../common/database/database_helper.dart';
|
||||||
|
import '../../../domain/product/product.dart';
|
||||||
|
import '../product_dtos.dart';
|
||||||
|
|
||||||
|
@injectable
|
||||||
|
class ProductLocalDataProvider {
|
||||||
|
final DatabaseHelper _databaseHelper;
|
||||||
|
final _logName = 'ProductLocalDataProvider';
|
||||||
|
|
||||||
|
final Map<String, List<ProductDto>> _queryCache = {};
|
||||||
|
final Duration _cacheExpiry = Duration(minutes: AppConstant.cacheExpire);
|
||||||
|
final Map<String, DateTime> _cacheTimestamps = {};
|
||||||
|
|
||||||
|
ProductLocalDataProvider(this._databaseHelper);
|
||||||
|
|
||||||
|
Future<DC<ProductFailure, void>> saveProductsBatch(
|
||||||
|
List<ProductDto> products, {
|
||||||
|
bool clearFirst = false,
|
||||||
|
}) async {
|
||||||
|
final db = await _databaseHelper.database;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.transaction((txn) async {
|
||||||
|
if (clearFirst) {
|
||||||
|
log('🗑️ Clearing existing products...', name: _logName);
|
||||||
|
await txn.delete('product_variants');
|
||||||
|
await txn.delete('products');
|
||||||
|
}
|
||||||
|
|
||||||
|
log('💾 Batch saving ${products.length} products...', name: _logName);
|
||||||
|
|
||||||
|
// ✅ Batch insert products
|
||||||
|
final productBatch = txn.batch();
|
||||||
|
for (final product in products) {
|
||||||
|
productBatch.insert(
|
||||||
|
'products',
|
||||||
|
product.toMap(),
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await productBatch.commit(noResult: true);
|
||||||
|
|
||||||
|
// ✅ Batch insert variants
|
||||||
|
final variantBatch = txn.batch();
|
||||||
|
for (final product in products) {
|
||||||
|
if (product.variants?.isNotEmpty == true) {
|
||||||
|
// Delete existing variants
|
||||||
|
variantBatch.delete(
|
||||||
|
'product_variants',
|
||||||
|
where: 'product_id = ?',
|
||||||
|
whereArgs: [product.id],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert variants
|
||||||
|
for (final variant in product.variants!) {
|
||||||
|
variantBatch.insert(
|
||||||
|
'product_variants',
|
||||||
|
variant.toMap(),
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await variantBatch.commit(noResult: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ Clear cache
|
||||||
|
clearCache();
|
||||||
|
log(
|
||||||
|
'✅ Successfully batch saved ${products.length} products',
|
||||||
|
name: _logName,
|
||||||
|
);
|
||||||
|
|
||||||
|
return DC.data(null);
|
||||||
|
} catch (e, s) {
|
||||||
|
log(
|
||||||
|
'❌ Error batch saving products',
|
||||||
|
name: _logName,
|
||||||
|
error: e,
|
||||||
|
stackTrace: s,
|
||||||
|
);
|
||||||
|
|
||||||
|
return DC.error(ProductFailure.dynamicErrorMessage(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<DC<ProductFailure, List<ProductDto>>> getCachedProducts({
|
||||||
|
int page = 1,
|
||||||
|
int limit = 10,
|
||||||
|
String? categoryId,
|
||||||
|
String? search,
|
||||||
|
}) async {
|
||||||
|
final cacheKey = _generateCacheKey(page, limit, categoryId, 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) {
|
||||||
|
final cachedProducts = _queryCache[cacheKey]!;
|
||||||
|
log(
|
||||||
|
'🚀 Cache HIT: $cacheKey (${cachedProducts.length} products)',
|
||||||
|
name: _logName,
|
||||||
|
);
|
||||||
|
return DC.data(cachedProducts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log('📀 Cache MISS: $cacheKey, querying database...', name: _logName);
|
||||||
|
|
||||||
|
// Cache miss → query database
|
||||||
|
final result = await getProducts(
|
||||||
|
page: page,
|
||||||
|
limit: limit,
|
||||||
|
categoryId: categoryId,
|
||||||
|
search: search,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ✅ Handle data/error dari getProducts()
|
||||||
|
if (result.hasData) {
|
||||||
|
final products = result.data!;
|
||||||
|
|
||||||
|
// Simpan ke cache
|
||||||
|
_queryCache[cacheKey] = products;
|
||||||
|
_cacheTimestamps[cacheKey] = now;
|
||||||
|
|
||||||
|
log(
|
||||||
|
'💾 Cached ${products.length} products for key: $cacheKey',
|
||||||
|
name: _logName,
|
||||||
|
);
|
||||||
|
|
||||||
|
return DC.data(products);
|
||||||
|
} else {
|
||||||
|
// Kalau error dari getProducts
|
||||||
|
return DC.error(result.error!);
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
log(
|
||||||
|
'❌ Error getting cached products',
|
||||||
|
name: _logName,
|
||||||
|
error: e,
|
||||||
|
stackTrace: s,
|
||||||
|
);
|
||||||
|
|
||||||
|
return DC.error(ProductFailure.localStorageError(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<DC<ProductFailure, List<ProductDto>>> getProducts({
|
||||||
|
int page = 1,
|
||||||
|
int limit = 10,
|
||||||
|
String? categoryId,
|
||||||
|
String? search,
|
||||||
|
}) async {
|
||||||
|
final db = await _databaseHelper.database;
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<ProductDto> products = [];
|
||||||
|
for (final map in maps) {
|
||||||
|
final variants = await _getProductVariants(db, map['id']);
|
||||||
|
final product = ProductDto.fromMap(map, variants);
|
||||||
|
products.add(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
log(
|
||||||
|
'📊 Retrieved ${products.length} products from database',
|
||||||
|
name: _logName,
|
||||||
|
);
|
||||||
|
|
||||||
|
return DC.data(products);
|
||||||
|
} catch (e, s) {
|
||||||
|
log('❌ Error getting products', name: _logName, error: e, stackTrace: s);
|
||||||
|
return DC.error(ProductFailure.localStorageError(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<DC<ProductFailure, List<ProductDto>>> searchProductsOptimized(
|
||||||
|
String query,
|
||||||
|
) async {
|
||||||
|
final db = await _databaseHelper.database;
|
||||||
|
|
||||||
|
try {
|
||||||
|
log('🔍 Optimized search for: "$query"', name: _logName);
|
||||||
|
|
||||||
|
// ✅ 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
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<ProductDto> products = [];
|
||||||
|
for (final map in maps) {
|
||||||
|
final variants = await _getProductVariants(db, map['id']);
|
||||||
|
final product = ProductDto.fromMap(map, variants);
|
||||||
|
products.add(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
log(
|
||||||
|
'🎯 Optimized search found ${products.length} results',
|
||||||
|
name: _logName,
|
||||||
|
);
|
||||||
|
|
||||||
|
return DC.data(products);
|
||||||
|
} catch (e, s) {
|
||||||
|
log(
|
||||||
|
'❌ Error in optimized search',
|
||||||
|
name: _logName,
|
||||||
|
error: e,
|
||||||
|
stackTrace: s,
|
||||||
|
);
|
||||||
|
return DC.error(ProductFailure.localStorageError(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<DC<ProductFailure, ProductDto>> getProductById(String id) async {
|
||||||
|
final db = await _databaseHelper.database;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final List<Map<String, dynamic>> maps = await db.query(
|
||||||
|
'products',
|
||||||
|
where: 'id = ?',
|
||||||
|
whereArgs: [id],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (maps.isEmpty) {
|
||||||
|
log('❌ Product not found: $id', name: _logName);
|
||||||
|
return DC.error(ProductFailure.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
final variants = await _getProductVariants(db, id);
|
||||||
|
final product = ProductDto.fromMap(maps.first, variants);
|
||||||
|
|
||||||
|
log('✅ Product found: ${product.name}', name: _logName);
|
||||||
|
return DC.data(product);
|
||||||
|
} catch (e, s) {
|
||||||
|
log(
|
||||||
|
'❌ Error getting product by ID',
|
||||||
|
name: _logName,
|
||||||
|
error: e,
|
||||||
|
stackTrace: s,
|
||||||
|
);
|
||||||
|
return DC.error(ProductFailure.localStorageError(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<DC<ProductFailure, Map<String, dynamic>>> getDatabaseStats() async {
|
||||||
|
final db = await _databaseHelper.database;
|
||||||
|
|
||||||
|
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', name: _logName);
|
||||||
|
|
||||||
|
return DC.data(stats);
|
||||||
|
} catch (e, s) {
|
||||||
|
log(
|
||||||
|
'❌ Error getting database stats',
|
||||||
|
name: _logName,
|
||||||
|
error: e,
|
||||||
|
stackTrace: s,
|
||||||
|
);
|
||||||
|
return DC.error(ProductFailure.localStorageError(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getTotalCount({String? categoryId, String? search}) async {
|
||||||
|
final db = await _databaseHelper.database;
|
||||||
|
|
||||||
|
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)',
|
||||||
|
name: _logName,
|
||||||
|
);
|
||||||
|
return count;
|
||||||
|
} catch (e) {
|
||||||
|
log('❌ Error getting total count: $e', name: _logName);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> hasProducts() async {
|
||||||
|
final count = await getTotalCount();
|
||||||
|
final hasData = count > 0;
|
||||||
|
log('🔍 Has products: $hasData ($count products)', name: _logName);
|
||||||
|
return hasData;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearAllProducts() async {
|
||||||
|
final db = await _databaseHelper.database;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.transaction((txn) async {
|
||||||
|
await txn.delete('product_variants');
|
||||||
|
await txn.delete('products');
|
||||||
|
});
|
||||||
|
clearCache();
|
||||||
|
log('🗑️ All products cleared from local DB', name: _logName);
|
||||||
|
} catch (e) {
|
||||||
|
log('❌ Error clearing products: $e', name: _logName);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<double> _getDatabaseSize() async {
|
||||||
|
try {
|
||||||
|
final dbPath = p.join(await getDatabasesPath(), 'db_pos.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', name: _logName);
|
||||||
|
}
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ProductVariantDto>> _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) => ProductVariantDto.fromMap(map)).toList();
|
||||||
|
} catch (e) {
|
||||||
|
log(
|
||||||
|
'❌ Error getting variants for product $productId: $e',
|
||||||
|
name: _logName,
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _generateCacheKey(
|
||||||
|
int page,
|
||||||
|
int limit,
|
||||||
|
String? categoryId,
|
||||||
|
String? search,
|
||||||
|
) {
|
||||||
|
return 'products_${page}_${limit}_${categoryId ?? 'null'}_${search ?? 'null'}';
|
||||||
|
}
|
||||||
|
|
||||||
|
double _getCacheSize() {
|
||||||
|
double totalSize = 0;
|
||||||
|
_queryCache.forEach((key, products) {
|
||||||
|
totalSize += products.length * 0.001; // Rough estimate in MB
|
||||||
|
});
|
||||||
|
return totalSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearCache() {
|
||||||
|
final count = _queryCache.length;
|
||||||
|
_queryCache.clear();
|
||||||
|
_cacheTimestamps.clear();
|
||||||
|
log('🧹 Cache cleared: $count entries removed', name: _logName);
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
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/product/product.dart';
|
||||||
|
import '../product_dtos.dart';
|
||||||
|
|
||||||
|
@injectable
|
||||||
|
class ProductRemoteDataProvider {
|
||||||
|
final ApiClient _apiClient;
|
||||||
|
final _logName = 'ProductRemoteDataProvider';
|
||||||
|
|
||||||
|
ProductRemoteDataProvider(this._apiClient);
|
||||||
|
|
||||||
|
Future<DC<ProductFailure, ListProductDto>> fetchProducts({
|
||||||
|
int page = 1,
|
||||||
|
int limit = 10,
|
||||||
|
String? categoryId,
|
||||||
|
String? search,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
Map<String, dynamic> queryParameters = {'page': page, 'limit': limit};
|
||||||
|
|
||||||
|
if (categoryId != null) {
|
||||||
|
queryParameters['category_id'] = categoryId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search != null && search.isNotEmpty) {
|
||||||
|
queryParameters['search'] = search;
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await _apiClient.get(
|
||||||
|
ApiPath.products,
|
||||||
|
params: queryParameters,
|
||||||
|
headers: getAuthorizationHeader(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data['data'] == null) {
|
||||||
|
return DC.error(ProductFailure.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
final categories = ListProductDto.fromJson(
|
||||||
|
response.data['data'] as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
|
||||||
|
return DC.data(categories);
|
||||||
|
} on ApiFailure catch (e, s) {
|
||||||
|
log('fetchProductError', name: _logName, error: e, stackTrace: s);
|
||||||
|
return DC.error(ProductFailure.serverError(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
173
lib/infrastructure/product/dtos/product_dto.dart
Normal file
173
lib/infrastructure/product/dtos/product_dto.dart
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
part of '../product_dtos.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ListProductDto with _$ListProductDto {
|
||||||
|
const ListProductDto._();
|
||||||
|
|
||||||
|
const factory ListProductDto({
|
||||||
|
@JsonKey(name: "products") required List<ProductDto> products,
|
||||||
|
@JsonKey(name: "total_count") required int totalCount,
|
||||||
|
@JsonKey(name: "page") required int page,
|
||||||
|
@JsonKey(name: "limit") required int limit,
|
||||||
|
@JsonKey(name: "total_pages") required int totalPages,
|
||||||
|
}) = _ListProductDto;
|
||||||
|
|
||||||
|
factory ListProductDto.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ListProductDtoFromJson(json);
|
||||||
|
|
||||||
|
ListProduct toDomain() => ListProduct(
|
||||||
|
products: products.map((dto) => dto.toDomain()).toList(),
|
||||||
|
totalCount: totalCount,
|
||||||
|
page: page,
|
||||||
|
limit: limit,
|
||||||
|
totalPages: totalPages,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ProductDto with _$ProductDto {
|
||||||
|
const ProductDto._();
|
||||||
|
|
||||||
|
const factory ProductDto({
|
||||||
|
@JsonKey(name: "id") String? id,
|
||||||
|
@JsonKey(name: "organization_id") String? organizationId,
|
||||||
|
@JsonKey(name: "category_id") String? categoryId,
|
||||||
|
@JsonKey(name: "sku") String? sku,
|
||||||
|
@JsonKey(name: "name") String? name,
|
||||||
|
@JsonKey(name: "description") String? description,
|
||||||
|
@JsonKey(name: "price") double? price,
|
||||||
|
@JsonKey(name: "cost") double? cost,
|
||||||
|
@JsonKey(name: "business_type") String? businessType,
|
||||||
|
@JsonKey(name: "image_url") String? imageUrl,
|
||||||
|
@JsonKey(name: "printer_type") String? printerType,
|
||||||
|
@JsonKey(name: "metadata") Map<String, dynamic>? metadata,
|
||||||
|
@JsonKey(name: "is_active") bool? isActive,
|
||||||
|
@JsonKey(name: "created_at") String? createdAt,
|
||||||
|
@JsonKey(name: "updated_at") String? updatedAt,
|
||||||
|
@JsonKey(name: "variants") List<ProductVariantDto>? variants,
|
||||||
|
}) = _ProductDto;
|
||||||
|
|
||||||
|
factory ProductDto.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ProductDtoFromJson(json);
|
||||||
|
|
||||||
|
/// Mapping ke domain
|
||||||
|
Product toDomain() => Product(
|
||||||
|
id: id ?? '',
|
||||||
|
organizationId: organizationId ?? '',
|
||||||
|
categoryId: categoryId ?? '',
|
||||||
|
sku: sku ?? '',
|
||||||
|
name: name ?? '',
|
||||||
|
description: description ?? '',
|
||||||
|
price: price ?? 0.0,
|
||||||
|
cost: cost ?? 0.0,
|
||||||
|
businessType: businessType ?? '',
|
||||||
|
imageUrl: imageUrl ?? '',
|
||||||
|
printerType: printerType ?? '',
|
||||||
|
metadata: metadata ?? {},
|
||||||
|
isActive: isActive ?? false,
|
||||||
|
createdAt: createdAt ?? '',
|
||||||
|
updatedAt: updatedAt ?? '',
|
||||||
|
variants: variants?.map((v) => v.toDomain()).toList() ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {
|
||||||
|
'id': id,
|
||||||
|
'organization_id': organizationId,
|
||||||
|
'category_id': categoryId,
|
||||||
|
'sku': sku,
|
||||||
|
'name': name,
|
||||||
|
'description': description,
|
||||||
|
'price': price,
|
||||||
|
'cost': cost,
|
||||||
|
'business_type': businessType,
|
||||||
|
'image_url': imageUrl,
|
||||||
|
'printer_type': printerType,
|
||||||
|
'metadata': metadata != null ? jsonEncode(metadata) : null,
|
||||||
|
'is_active': isActive == true ? 1 : 0,
|
||||||
|
'created_at': createdAt,
|
||||||
|
'updated_at': updatedAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
factory ProductDto.fromMap(
|
||||||
|
Map<String, dynamic> map,
|
||||||
|
List<ProductVariantDto> variants,
|
||||||
|
) => ProductDto(
|
||||||
|
id: map['id'] as String?,
|
||||||
|
organizationId: map['organization_id'] as String?,
|
||||||
|
categoryId: map['category_id'] as String?,
|
||||||
|
sku: map['sku'] as String?,
|
||||||
|
name: map['name'] as String?,
|
||||||
|
description: map['description'] as String?,
|
||||||
|
price: map['price'] != null ? (map['price'] as num).toDouble() : null,
|
||||||
|
cost: map['cost'] != null ? (map['cost'] as num).toDouble() : null,
|
||||||
|
businessType: map['business_type'] as String?,
|
||||||
|
imageUrl: map['image_url'] as String?,
|
||||||
|
printerType: map['printer_type'] as String?,
|
||||||
|
metadata: map['metadata'] != null
|
||||||
|
? jsonDecode(map['metadata'] as String) as Map<String, dynamic>
|
||||||
|
: null,
|
||||||
|
isActive: map['is_active'] != null ? (map['is_active'] as int) == 1 : null,
|
||||||
|
|
||||||
|
createdAt: map['created_at'] as String?,
|
||||||
|
updatedAt: map['updated_at'] as String?,
|
||||||
|
variants: variants,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ProductVariantDto with _$ProductVariantDto {
|
||||||
|
const ProductVariantDto._();
|
||||||
|
|
||||||
|
const factory ProductVariantDto({
|
||||||
|
@JsonKey(name: "id") String? id,
|
||||||
|
@JsonKey(name: "product_id") String? productId,
|
||||||
|
@JsonKey(name: "name") String? name,
|
||||||
|
@JsonKey(name: "price_modifier") double? priceModifier,
|
||||||
|
@JsonKey(name: "cost") double? cost,
|
||||||
|
@JsonKey(name: "metadata") Map<String, dynamic>? metadata,
|
||||||
|
@JsonKey(name: "created_at") String? createdAt,
|
||||||
|
@JsonKey(name: "updated_at") String? updatedAt,
|
||||||
|
}) = _ProductVariantDto;
|
||||||
|
|
||||||
|
factory ProductVariantDto.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ProductVariantDtoFromJson(json);
|
||||||
|
|
||||||
|
/// Mapping ke domain
|
||||||
|
ProductVariant toDomain() => ProductVariant(
|
||||||
|
id: id ?? '',
|
||||||
|
productId: productId ?? '',
|
||||||
|
name: name ?? '',
|
||||||
|
priceModifier: priceModifier ?? 0.0,
|
||||||
|
cost: cost ?? 0.0,
|
||||||
|
metadata: metadata ?? {},
|
||||||
|
createdAt: createdAt ?? '',
|
||||||
|
updatedAt: updatedAt ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {
|
||||||
|
'id': id,
|
||||||
|
'product_id': productId,
|
||||||
|
'name': name,
|
||||||
|
'price_modifier': priceModifier,
|
||||||
|
'cost': cost,
|
||||||
|
'metadata': metadata != null ? jsonEncode(metadata) : null,
|
||||||
|
'created_at': createdAt,
|
||||||
|
'updated_at': updatedAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
factory ProductVariantDto.fromMap(Map<String, dynamic> map) =>
|
||||||
|
ProductVariantDto(
|
||||||
|
id: map['id'] as String?,
|
||||||
|
productId: map['product_id'] as String?,
|
||||||
|
name: map['name'] as String?,
|
||||||
|
priceModifier: map['price_modifier'] != null
|
||||||
|
? (map['price_modifier'] as num).toDouble()
|
||||||
|
: null,
|
||||||
|
cost: map['cost'] != null ? (map['cost'] as num).toDouble() : null,
|
||||||
|
metadata: map['metadata'] != null
|
||||||
|
? jsonDecode(map['metadata'] as String) as Map<String, dynamic>
|
||||||
|
: null,
|
||||||
|
createdAt: map['created_at'] as String?,
|
||||||
|
updatedAt: map['updated_at'] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
10
lib/infrastructure/product/product_dtos.dart
Normal file
10
lib/infrastructure/product/product_dtos.dart
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
import '../../domain/product/product.dart';
|
||||||
|
|
||||||
|
part 'product_dtos.freezed.dart';
|
||||||
|
part 'product_dtos.g.dart';
|
||||||
|
|
||||||
|
part 'dtos/product_dto.dart';
|
||||||
1204
lib/infrastructure/product/product_dtos.freezed.dart
Normal file
1204
lib/infrastructure/product/product_dtos.freezed.dart
Normal file
File diff suppressed because it is too large
Load Diff
96
lib/infrastructure/product/product_dtos.g.dart
Normal file
96
lib/infrastructure/product/product_dtos.g.dart
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'product_dtos.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$ListProductDtoImpl _$$ListProductDtoImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ListProductDtoImpl(
|
||||||
|
products: (json['products'] as List<dynamic>)
|
||||||
|
.map((e) => ProductDto.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
totalCount: (json['total_count'] as num).toInt(),
|
||||||
|
page: (json['page'] as num).toInt(),
|
||||||
|
limit: (json['limit'] as num).toInt(),
|
||||||
|
totalPages: (json['total_pages'] as num).toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$ListProductDtoImplToJson(
|
||||||
|
_$ListProductDtoImpl instance,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'products': instance.products,
|
||||||
|
'total_count': instance.totalCount,
|
||||||
|
'page': instance.page,
|
||||||
|
'limit': instance.limit,
|
||||||
|
'total_pages': instance.totalPages,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$ProductDtoImpl _$$ProductDtoImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ProductDtoImpl(
|
||||||
|
id: json['id'] as String?,
|
||||||
|
organizationId: json['organization_id'] as String?,
|
||||||
|
categoryId: json['category_id'] as String?,
|
||||||
|
sku: json['sku'] as String?,
|
||||||
|
name: json['name'] as String?,
|
||||||
|
description: json['description'] as String?,
|
||||||
|
price: (json['price'] as num?)?.toDouble(),
|
||||||
|
cost: (json['cost'] as num?)?.toDouble(),
|
||||||
|
businessType: json['business_type'] as String?,
|
||||||
|
imageUrl: json['image_url'] as String?,
|
||||||
|
printerType: json['printer_type'] as String?,
|
||||||
|
metadata: json['metadata'] as Map<String, dynamic>?,
|
||||||
|
isActive: json['is_active'] as bool?,
|
||||||
|
createdAt: json['created_at'] as String?,
|
||||||
|
updatedAt: json['updated_at'] as String?,
|
||||||
|
variants: (json['variants'] as List<dynamic>?)
|
||||||
|
?.map((e) => ProductVariantDto.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$ProductDtoImplToJson(_$ProductDtoImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'organization_id': instance.organizationId,
|
||||||
|
'category_id': instance.categoryId,
|
||||||
|
'sku': instance.sku,
|
||||||
|
'name': instance.name,
|
||||||
|
'description': instance.description,
|
||||||
|
'price': instance.price,
|
||||||
|
'cost': instance.cost,
|
||||||
|
'business_type': instance.businessType,
|
||||||
|
'image_url': instance.imageUrl,
|
||||||
|
'printer_type': instance.printerType,
|
||||||
|
'metadata': instance.metadata,
|
||||||
|
'is_active': instance.isActive,
|
||||||
|
'created_at': instance.createdAt,
|
||||||
|
'updated_at': instance.updatedAt,
|
||||||
|
'variants': instance.variants,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$ProductVariantDtoImpl _$$ProductVariantDtoImplFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => _$ProductVariantDtoImpl(
|
||||||
|
id: json['id'] as String?,
|
||||||
|
productId: json['product_id'] as String?,
|
||||||
|
name: json['name'] as String?,
|
||||||
|
priceModifier: (json['price_modifier'] as num?)?.toDouble(),
|
||||||
|
cost: (json['cost'] as num?)?.toDouble(),
|
||||||
|
metadata: json['metadata'] as Map<String, dynamic>?,
|
||||||
|
createdAt: json['created_at'] as String?,
|
||||||
|
updatedAt: json['updated_at'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$ProductVariantDtoImplToJson(
|
||||||
|
_$ProductVariantDtoImpl instance,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'product_id': instance.productId,
|
||||||
|
'name': instance.name,
|
||||||
|
'price_modifier': instance.priceModifier,
|
||||||
|
'cost': instance.cost,
|
||||||
|
'metadata': instance.metadata,
|
||||||
|
'created_at': instance.createdAt,
|
||||||
|
'updated_at': instance.updatedAt,
|
||||||
|
};
|
||||||
331
lib/infrastructure/product/repositories/product_repository.dart
Normal file
331
lib/infrastructure/product/repositories/product_repository.dart
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
|
||||||
|
import '../../../domain/product/product.dart';
|
||||||
|
import '../datasources/local_data_provider.dart';
|
||||||
|
import '../datasources/remote_data_provider.dart';
|
||||||
|
|
||||||
|
@Injectable(as: IProductRepository)
|
||||||
|
class ProductRepository implements IProductRepository {
|
||||||
|
final ProductRemoteDataProvider _remoteDataProvider;
|
||||||
|
final ProductLocalDataProvider _localDataProvider;
|
||||||
|
|
||||||
|
final _logName = 'ProductRepository';
|
||||||
|
|
||||||
|
ProductRepository(this._remoteDataProvider, this._localDataProvider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<ProductFailure, Product>> getProductById(String id) async {
|
||||||
|
try {
|
||||||
|
log('🔍 Getting product by ID from local: $id', name: _logName);
|
||||||
|
|
||||||
|
final product = await _localDataProvider.getProductById(id);
|
||||||
|
|
||||||
|
if (product.hasData) {
|
||||||
|
log('❌ Product not found: $id', name: _logName);
|
||||||
|
return Left(
|
||||||
|
ProductFailure.dynamicErrorMessage(
|
||||||
|
'Produk dengan ID $id tidak ditemukan',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final productDomain = product.data!.toDomain();
|
||||||
|
|
||||||
|
log('✅ Product loaded: ${productDomain.name}', name: _logName);
|
||||||
|
return Right(productDomain);
|
||||||
|
} catch (e, s) {
|
||||||
|
log(
|
||||||
|
'❌ Error getting product by ID',
|
||||||
|
name: _logName,
|
||||||
|
error: e,
|
||||||
|
stackTrace: s,
|
||||||
|
);
|
||||||
|
return Left(ProductFailure.localStorageError(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<ProductFailure, ListProduct>> getProducts({
|
||||||
|
int page = 1,
|
||||||
|
int limit = 10,
|
||||||
|
String? categoryId,
|
||||||
|
String? search,
|
||||||
|
bool forceRefresh = false,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
log(
|
||||||
|
'📦 Fetching products from local DB - page: $page, categoryId: $categoryId, search: $search',
|
||||||
|
name: _logName,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🧹 Bersihkan cache kedaluwarsa
|
||||||
|
_localDataProvider.clearExpiredCache();
|
||||||
|
|
||||||
|
// ⚡ Ambil data dari cache lokal
|
||||||
|
final cachedProducts = await _localDataProvider.getCachedProducts(
|
||||||
|
page: page,
|
||||||
|
limit: limit,
|
||||||
|
categoryId: categoryId,
|
||||||
|
search: search,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cachedProducts.hasError) {
|
||||||
|
return left(cachedProducts.error!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 📊 Hitung total item (untuk pagination)
|
||||||
|
final totalCount = await _localDataProvider.getTotalCount(
|
||||||
|
categoryId: categoryId,
|
||||||
|
search: search,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🧱 Bangun entity domain ListProduct
|
||||||
|
final result = ListProduct(
|
||||||
|
products: cachedProducts.data!.map((p) => p.toDomain()).toList(),
|
||||||
|
totalCount: totalCount,
|
||||||
|
page: page,
|
||||||
|
limit: limit,
|
||||||
|
totalPages: totalCount > 0 ? (totalCount / limit).ceil() : 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
log(
|
||||||
|
'✅ Returned ${cachedProducts.data!.length} local products ($totalCount total)',
|
||||||
|
name: _logName,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Right(result);
|
||||||
|
} catch (e, s) {
|
||||||
|
log(
|
||||||
|
'❌ Error getting local products',
|
||||||
|
name: _logName,
|
||||||
|
error: e,
|
||||||
|
stackTrace: s,
|
||||||
|
);
|
||||||
|
return Left(ProductFailure.localStorageError(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<ProductFailure, ListProduct>> refreshProducts({
|
||||||
|
String? categoryId,
|
||||||
|
String? search,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
log('🔄 Refreshing local products...', name: _logName);
|
||||||
|
|
||||||
|
// Bersihkan cache agar hasil baru diambil dari database
|
||||||
|
_localDataProvider.clearCache();
|
||||||
|
|
||||||
|
// Ambil ulang data produk dari lokal database
|
||||||
|
final cachedProducts = await _localDataProvider.getCachedProducts(
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
categoryId: categoryId,
|
||||||
|
search: search,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cachedProducts.hasError) {
|
||||||
|
return left(cachedProducts.error!);
|
||||||
|
}
|
||||||
|
|
||||||
|
final products = cachedProducts.data!.map((p) => p.toDomain()).toList();
|
||||||
|
|
||||||
|
final totalCount = await _localDataProvider.getTotalCount(
|
||||||
|
categoryId: categoryId,
|
||||||
|
search: search,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🧱 Bangun entity domain ListProduct
|
||||||
|
final result = ListProduct(
|
||||||
|
products: products,
|
||||||
|
totalCount: totalCount,
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
totalPages: totalCount > 0 ? (totalCount / 10).ceil() : 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
log(
|
||||||
|
'✅ Refreshed ${cachedProducts.data!.length} local products',
|
||||||
|
name: _logName,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Right(result);
|
||||||
|
} catch (e, s) {
|
||||||
|
log(
|
||||||
|
'❌ Error refreshing local products',
|
||||||
|
name: _logName,
|
||||||
|
error: e,
|
||||||
|
stackTrace: s,
|
||||||
|
);
|
||||||
|
return Left(ProductFailure.localStorageError(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<ProductFailure, List<Product>>> searchProductsOptimized(
|
||||||
|
String query,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
log('🔍 Local optimized search for: "$query"', name: _logName);
|
||||||
|
|
||||||
|
// 🔎 Cari dari local database
|
||||||
|
final results = await _localDataProvider.searchProductsOptimized(query);
|
||||||
|
|
||||||
|
if (results.hasError) {
|
||||||
|
return left(results.error!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Mapping ke domain entity (kalau hasilnya masih berupa DTO)
|
||||||
|
final products = results.data!.map((p) => p.toDomain()).toList();
|
||||||
|
|
||||||
|
log(
|
||||||
|
'✅ Local search completed: ${products.length} results',
|
||||||
|
name: _logName,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Right(products);
|
||||||
|
} catch (e, s) {
|
||||||
|
log('❌ Error in local search', name: _logName, error: e, stackTrace: s);
|
||||||
|
return Left(ProductFailure.localStorageError(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<ProductFailure, String>> syncAllProducts() async {
|
||||||
|
try {
|
||||||
|
log('🔄 Starting manual sync of all products...', name: _logName);
|
||||||
|
|
||||||
|
int page = 1;
|
||||||
|
const limit = 50;
|
||||||
|
bool hasMore = true;
|
||||||
|
int totalSynced = 0;
|
||||||
|
|
||||||
|
// Clear local DB before fresh sync
|
||||||
|
await _localDataProvider.clearAllProducts();
|
||||||
|
|
||||||
|
while (hasMore) {
|
||||||
|
log('📄 Syncing page $page...', name: _logName);
|
||||||
|
|
||||||
|
// NOTE: _remoteDatasource.getProducts() returns DC<..., ProductResponseModel>
|
||||||
|
final remoteResult = await _remoteDataProvider.fetchProducts(
|
||||||
|
page: page,
|
||||||
|
limit: limit,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle DC result manually (no fold)
|
||||||
|
if (!remoteResult.hasData) {
|
||||||
|
// remote returned an error/failure
|
||||||
|
final remoteFailure = remoteResult.error;
|
||||||
|
log('❌ Sync failed at page $page: $remoteFailure', name: _logName);
|
||||||
|
return Left(
|
||||||
|
ProductFailure.dynamicErrorMessage(remoteFailure.toString()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = remoteResult.data!;
|
||||||
|
final products = response.products;
|
||||||
|
|
||||||
|
if (products.isNotEmpty) {
|
||||||
|
// Save page to local DB
|
||||||
|
await _localDataProvider.saveProductsBatch(
|
||||||
|
products,
|
||||||
|
clearFirst: false, // don't clear on subsequent pages
|
||||||
|
);
|
||||||
|
|
||||||
|
totalSynced += products.length;
|
||||||
|
|
||||||
|
// Determine if more pages exist
|
||||||
|
hasMore = page < (response.totalPages);
|
||||||
|
page++;
|
||||||
|
|
||||||
|
log(
|
||||||
|
'📦 Page ${page - 1} synced: ${products.length} products',
|
||||||
|
name: _logName,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
hasMore = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final message = 'Berhasil sinkronisasi $totalSynced produk';
|
||||||
|
log('✅ $message', name: _logName);
|
||||||
|
return Right(message);
|
||||||
|
} catch (e, s) {
|
||||||
|
log(
|
||||||
|
'❌ Gagal sinkronisasi produk',
|
||||||
|
name: _logName,
|
||||||
|
error: e,
|
||||||
|
stackTrace: s,
|
||||||
|
);
|
||||||
|
return Left(ProductFailure.localStorageError(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void clearCache() {
|
||||||
|
log('🧹 Clearing local cache', name: _logName);
|
||||||
|
_localDataProvider.clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<ProductFailure, Map<String, dynamic>>>
|
||||||
|
getDatabaseStats() async {
|
||||||
|
try {
|
||||||
|
log('📊 Getting local database stats...', name: _logName);
|
||||||
|
|
||||||
|
final stats = await _localDataProvider.getDatabaseStats();
|
||||||
|
|
||||||
|
log('✅ Database stats loaded successfully: $stats', name: _logName);
|
||||||
|
|
||||||
|
return Right(stats.data!);
|
||||||
|
} catch (e, s) {
|
||||||
|
log(
|
||||||
|
'❌ Error getting database stats',
|
||||||
|
name: _logName,
|
||||||
|
error: e,
|
||||||
|
stackTrace: s,
|
||||||
|
);
|
||||||
|
return Left(ProductFailure.localStorageError(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> hasLocalProducts() async {
|
||||||
|
final hasProducts = await _localDataProvider.hasProducts();
|
||||||
|
log('📊 Has local products: $hasProducts', name: _logName);
|
||||||
|
return hasProducts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> isLocalDatabaseReady() async {
|
||||||
|
try {
|
||||||
|
final stats = await _localDataProvider.getDatabaseStats();
|
||||||
|
final productCount = stats.data!['total_products'] ?? 0;
|
||||||
|
final isReady = productCount > 0;
|
||||||
|
log(
|
||||||
|
'🔍 Local database ready: $isReady ($productCount products)',
|
||||||
|
name: _logName,
|
||||||
|
);
|
||||||
|
return isReady;
|
||||||
|
} catch (e) {
|
||||||
|
log('❌ Error checking database readiness: $e', name: _logName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> clearAllProducts() async {
|
||||||
|
try {
|
||||||
|
log('🗑️ Clearing all products from repository...', name: _logName);
|
||||||
|
await _localDataProvider.clearAllProducts();
|
||||||
|
clearCache();
|
||||||
|
log('✅ All products cleared successfully', name: _logName);
|
||||||
|
} catch (e) {
|
||||||
|
log('❌ Error clearing all products: $e', name: _logName);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,6 +14,8 @@ import 'package:apskel_pos_flutter_v2/application/auth/login_form/login_form_blo
|
|||||||
as _i46;
|
as _i46;
|
||||||
import 'package:apskel_pos_flutter_v2/application/outlet/outlet_loader/outlet_loader_bloc.dart'
|
import 'package:apskel_pos_flutter_v2/application/outlet/outlet_loader/outlet_loader_bloc.dart'
|
||||||
as _i76;
|
as _i76;
|
||||||
|
import 'package:apskel_pos_flutter_v2/application/product/product_loader/product_loader_bloc.dart'
|
||||||
|
as _i13;
|
||||||
import 'package:apskel_pos_flutter_v2/common/api/api_client.dart' as _i457;
|
import 'package:apskel_pos_flutter_v2/common/api/api_client.dart' as _i457;
|
||||||
import 'package:apskel_pos_flutter_v2/common/database/database_helper.dart'
|
import 'package:apskel_pos_flutter_v2/common/database/database_helper.dart'
|
||||||
as _i487;
|
as _i487;
|
||||||
@ -28,6 +30,7 @@ import 'package:apskel_pos_flutter_v2/common/network/network_client.dart'
|
|||||||
import 'package:apskel_pos_flutter_v2/domain/auth/auth.dart' as _i776;
|
import 'package:apskel_pos_flutter_v2/domain/auth/auth.dart' as _i776;
|
||||||
import 'package:apskel_pos_flutter_v2/domain/category/category.dart' as _i502;
|
import 'package:apskel_pos_flutter_v2/domain/category/category.dart' as _i502;
|
||||||
import 'package:apskel_pos_flutter_v2/domain/outlet/outlet.dart' as _i552;
|
import 'package:apskel_pos_flutter_v2/domain/outlet/outlet.dart' as _i552;
|
||||||
|
import 'package:apskel_pos_flutter_v2/domain/product/product.dart' as _i44;
|
||||||
import 'package:apskel_pos_flutter_v2/env.dart' as _i923;
|
import 'package:apskel_pos_flutter_v2/env.dart' as _i923;
|
||||||
import 'package:apskel_pos_flutter_v2/infrastructure/auth/datasources/local_data_provider.dart'
|
import 'package:apskel_pos_flutter_v2/infrastructure/auth/datasources/local_data_provider.dart'
|
||||||
as _i204;
|
as _i204;
|
||||||
@ -47,6 +50,12 @@ import 'package:apskel_pos_flutter_v2/infrastructure/outlet/datasources/remote_d
|
|||||||
as _i132;
|
as _i132;
|
||||||
import 'package:apskel_pos_flutter_v2/infrastructure/outlet/repositories/outlet_repository.dart'
|
import 'package:apskel_pos_flutter_v2/infrastructure/outlet/repositories/outlet_repository.dart'
|
||||||
as _i845;
|
as _i845;
|
||||||
|
import 'package:apskel_pos_flutter_v2/infrastructure/product/datasources/local_data_provider.dart'
|
||||||
|
as _i464;
|
||||||
|
import 'package:apskel_pos_flutter_v2/infrastructure/product/datasources/remote_data_provider.dart'
|
||||||
|
as _i707;
|
||||||
|
import 'package:apskel_pos_flutter_v2/infrastructure/product/repositories/product_repository.dart'
|
||||||
|
as _i763;
|
||||||
import 'package:apskel_pos_flutter_v2/presentation/router/app_router.dart'
|
import 'package:apskel_pos_flutter_v2/presentation/router/app_router.dart'
|
||||||
as _i800;
|
as _i800;
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart' as _i895;
|
import 'package:connectivity_plus/connectivity_plus.dart' as _i895;
|
||||||
@ -85,6 +94,9 @@ extension GetItInjectableX on _i174.GetIt {
|
|||||||
gh.factory<_i708.CategoryLocalDataProvider>(
|
gh.factory<_i708.CategoryLocalDataProvider>(
|
||||||
() => _i708.CategoryLocalDataProvider(gh<_i487.DatabaseHelper>()),
|
() => _i708.CategoryLocalDataProvider(gh<_i487.DatabaseHelper>()),
|
||||||
);
|
);
|
||||||
|
gh.factory<_i464.ProductLocalDataProvider>(
|
||||||
|
() => _i464.ProductLocalDataProvider(gh<_i487.DatabaseHelper>()),
|
||||||
|
);
|
||||||
gh.factory<_i204.AuthLocalDataProvider>(
|
gh.factory<_i204.AuthLocalDataProvider>(
|
||||||
() => _i204.AuthLocalDataProvider(gh<_i460.SharedPreferences>()),
|
() => _i204.AuthLocalDataProvider(gh<_i460.SharedPreferences>()),
|
||||||
);
|
);
|
||||||
@ -104,6 +116,9 @@ extension GetItInjectableX on _i174.GetIt {
|
|||||||
gh.factory<_i132.OutletRemoteDataProvider>(
|
gh.factory<_i132.OutletRemoteDataProvider>(
|
||||||
() => _i132.OutletRemoteDataProvider(gh<_i457.ApiClient>()),
|
() => _i132.OutletRemoteDataProvider(gh<_i457.ApiClient>()),
|
||||||
);
|
);
|
||||||
|
gh.factory<_i707.ProductRemoteDataProvider>(
|
||||||
|
() => _i707.ProductRemoteDataProvider(gh<_i457.ApiClient>()),
|
||||||
|
);
|
||||||
gh.factory<_i776.IAuthRepository>(
|
gh.factory<_i776.IAuthRepository>(
|
||||||
() => _i941.AuthRepository(
|
() => _i941.AuthRepository(
|
||||||
gh<_i370.AuthRemoteDataProvider>(),
|
gh<_i370.AuthRemoteDataProvider>(),
|
||||||
@ -116,6 +131,12 @@ extension GetItInjectableX on _i174.GetIt {
|
|||||||
gh<_i708.CategoryLocalDataProvider>(),
|
gh<_i708.CategoryLocalDataProvider>(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
gh.factory<_i44.IProductRepository>(
|
||||||
|
() => _i763.ProductRepository(
|
||||||
|
gh<_i707.ProductRemoteDataProvider>(),
|
||||||
|
gh<_i464.ProductLocalDataProvider>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
gh.factory<_i552.IOutletRepository>(
|
gh.factory<_i552.IOutletRepository>(
|
||||||
() => _i845.OutletRepository(
|
() => _i845.OutletRepository(
|
||||||
gh<_i132.OutletRemoteDataProvider>(),
|
gh<_i132.OutletRemoteDataProvider>(),
|
||||||
@ -134,6 +155,9 @@ extension GetItInjectableX on _i174.GetIt {
|
|||||||
gh.factory<_i76.OutletLoaderBloc>(
|
gh.factory<_i76.OutletLoaderBloc>(
|
||||||
() => _i76.OutletLoaderBloc(gh<_i552.IOutletRepository>()),
|
() => _i76.OutletLoaderBloc(gh<_i552.IOutletRepository>()),
|
||||||
);
|
);
|
||||||
|
gh.factory<_i13.ProductLoaderBloc>(
|
||||||
|
() => _i13.ProductLoaderBloc(gh<_i44.IProductRepository>()),
|
||||||
|
);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user