diff --git a/lib/application/sync/sync_bloc.dart b/lib/application/sync/sync_bloc.dart new file mode 100644 index 0000000..20ba694 --- /dev/null +++ b/lib/application/sync/sync_bloc.dart @@ -0,0 +1,323 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:injectable/injectable.dart'; + +import '../../domain/category/category.dart'; +import '../../domain/product/product.dart'; + +part 'sync_event.dart'; +part 'sync_state.dart'; +part 'sync_bloc.freezed.dart'; + +enum SyncStep { categories, products, variants, completed } + +class SyncStats { + final int totalProducts; + final int totalCategories; + final int totalVariants; + final double databaseSizeMB; + + SyncStats({ + required this.totalProducts, + required this.totalCategories, + required this.totalVariants, + required this.databaseSizeMB, + }); +} + +@injectable +class SyncBloc extends Bloc { + final IProductRepository _productRepository; + final ICategoryRepository _categoryRepository; + + Timer? _progressTimer; + bool _isCancelled = false; + + SyncBloc(this._productRepository, this._categoryRepository) + : super(SyncState.initial()) { + on(_onSyncEvent); + } + + Future _onSyncEvent(SyncEvent event, Emitter emit) { + return event.map( + startSync: (e) async { + log('๐Ÿ”„ Starting full data sync (categories + products)...'); + _isCancelled = false; + + emit( + state.copyWith( + isSyncing: true, + currentStep: SyncStep.categories, + progress: 0.05, + message: 'Membersihkan data lama...', + errorMessage: null, + ), + ); + + try { + // Step 1: Clear existing local data + await _productRepository.clearAllProducts(); + await _categoryRepository.clearAllCategories(); + + if (_isCancelled) return; + + // Step 2: Sync categories first + await _syncCategories(emit); + + if (_isCancelled) return; + + // Step 3: Sync products + await _syncProducts(emit); + + if (_isCancelled) return; + + // Step 4: Final stats + emit( + state.copyWith( + currentStep: SyncStep.completed, + progress: 0.95, + message: 'Menyelesaikan sinkronisasi...', + ), + ); + + final stats = await _generateSyncStats(); + + emit( + state.copyWith( + isSyncing: false, + stats: stats, + progress: 1.0, + message: 'Sinkronisasi selesai โœ…', + ), + ); + + log('โœ… Full sync completed successfully'); + } catch (e) { + log('โŒ Sync failed: $e'); + emit( + state.copyWith( + isSyncing: false, + errorMessage: 'Gagal sinkronisasi: $e', + message: 'Sinkronisasi gagal โŒ', + ), + ); + } + }, + cancelSync: (e) async { + log('โน๏ธ Cancelling sync...'); + _isCancelled = true; + _progressTimer?.cancel(); + emit(SyncState.initial()); + }, + ); + } + + Future _syncCategories(Emitter emit) async { + log('๐Ÿ“ Syncing categories...'); + + emit( + state.copyWith( + isSyncing: true, + currentStep: SyncStep.categories, + progress: 0.1, + message: 'Mengunduh kategori...', + errorMessage: null, + ), + ); + + try { + // Gunakan CategoryRepository sync method + final result = await _categoryRepository.syncAllCategories(); + + await result.fold( + (failure) async { + throw Exception('Gagal sync kategori: $failure'); + }, + (successMessage) async { + log('โœ… Categories sync completed: $successMessage'); + + emit( + state.copyWith( + currentStep: SyncStep.categories, + progress: 0.2, + message: 'Kategori berhasil diunduh โœ…', + ), + ); + }, + ); + } catch (e) { + log('โŒ Category sync failed: $e'); + emit( + state.copyWith( + isSyncing: false, + errorMessage: 'Gagal sync kategori: $e', + message: 'Sinkronisasi kategori gagal โŒ', + ), + ); + rethrow; // penting agar _onStartSync tahu kalau gagal + } + } + + Future _syncProducts(Emitter emit) async { + log('๐Ÿ“ฆ Syncing products...'); + + int page = 1; + int totalSynced = 0; + int? totalCount; + int? totalPages; + bool shouldContinue = true; + + while (!_isCancelled && shouldContinue) { + // Hitung progress dinamis (kategori 0.0โ€“0.2, produk 0.2โ€“0.9) + double progress = 0.2; + if (totalCount != null && totalCount! > 0) { + progress = 0.2 + (totalSynced / totalCount!) * 0.7; + } + + emit( + state.copyWith( + isSyncing: true, + currentStep: SyncStep.products, + progress: progress, + message: totalCount != null + ? 'Mengunduh produk... ($totalSynced dari $totalCount)' + : 'Mengunduh produk... ($totalSynced produk)', + errorMessage: null, + ), + ); + + final result = await _productRepository.getProducts( + page: page, + limit: 50, // ambil batch besar biar cepat + ); + + await result.fold( + (failure) async { + emit( + state.copyWith( + isSyncing: false, + errorMessage: 'Gagal sync produk: $failure', + message: 'Sinkronisasi produk gagal โŒ', + ), + ); + throw Exception(failure); + }, + (response) async { + final products = response.products; + final responseData = response; + + // Ambil total count & total page dari respons pertama + if (page == 1) { + totalCount = responseData.totalCount; + totalPages = responseData.totalPages; + log('๐Ÿ“Š Total products to sync: $totalCount ($totalPages pages)'); + } + + if (products.isEmpty) { + shouldContinue = false; + return; + } + + // Simpan batch produk ke local DB + await _productRepository.saveProductsBatch(products); + + totalSynced += products.length; + page++; + + log( + '๐Ÿ“ฆ Synced page ${page - 1}: ${products.length} products (Total: $totalSynced)', + ); + + // Cek apakah sudah selesai sync + if (totalPages != null && page > totalPages!) { + shouldContinue = false; + return; + } + + // Fallback jika pagination info tidak lengkap + if (products.length < 50) { + shouldContinue = false; + return; + } + + // Tambahkan delay kecil agar tidak overload server + await Future.delayed(const Duration(milliseconds: 100)); + }, + ); + } + + // Selesai sync produk + emit( + state.copyWith( + progress: 0.9, + message: 'Sinkronisasi produk selesai โœ… ($totalSynced total)', + ), + ); + } + + Future _generateSyncStats() async { + try { + log('๐Ÿ“Š Generating sync statistics via repository...'); + + // Jalankan kedua query secara paralel untuk efisiensi + final results = await Future.wait([ + _productRepository.getDatabaseStats(), + _categoryRepository.getDatabaseStats(), + ]); + + // Default kosong + Map productStats = {}; + Map categoryStats = {}; + + // Ambil hasil product stats + await results[0].fold( + (failure) async { + log('โš ๏ธ Failed to get product stats: $failure'); + }, + (data) async { + productStats = data; + }, + ); + + // Ambil hasil category stats + await results[1].fold( + (failure) async { + log('โš ๏ธ Failed to get category stats: $failure'); + }, + (data) async { + categoryStats = data; + }, + ); + + // Bangun objek SyncStats akhir + final stats = SyncStats( + totalProducts: productStats['total_products'] ?? 0, + totalCategories: categoryStats['total_categories'] ?? 0, + totalVariants: productStats['total_variants'] ?? 0, + databaseSizeMB: + ((productStats['database_size_mb'] ?? 0.0) as num).toDouble() + + ((categoryStats['database_size_mb'] ?? 0.0) as num).toDouble(), + ); + + log('โœ… Sync stats generated: $stats'); + return stats; + } catch (e, stack) { + log('โŒ Error generating sync stats: $e\n$stack'); + return SyncStats( + totalProducts: 0, + totalCategories: 0, + totalVariants: 0, + databaseSizeMB: 0.0, + ); + } + } + + @override + Future close() { + _progressTimer?.cancel(); + return super.close(); + } +} diff --git a/lib/application/sync/sync_bloc.freezed.dart b/lib/application/sync/sync_bloc.freezed.dart new file mode 100644 index 0000000..9a50f1b --- /dev/null +++ b/lib/application/sync/sync_bloc.freezed.dart @@ -0,0 +1,542 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'sync_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +/// @nodoc +mixin _$SyncEvent { + @optionalTypeArgs + TResult when({ + required TResult Function() startSync, + required TResult Function() cancelSync, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? startSync, + TResult? Function()? cancelSync, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? startSync, + TResult Function()? cancelSync, + required TResult orElse(), + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_StartSync value) startSync, + required TResult Function(_CancelSync value) cancelSync, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_StartSync value)? startSync, + TResult? Function(_CancelSync value)? cancelSync, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_StartSync value)? startSync, + TResult Function(_CancelSync value)? cancelSync, + required TResult orElse(), + }) => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SyncEventCopyWith<$Res> { + factory $SyncEventCopyWith(SyncEvent value, $Res Function(SyncEvent) then) = + _$SyncEventCopyWithImpl<$Res, SyncEvent>; +} + +/// @nodoc +class _$SyncEventCopyWithImpl<$Res, $Val extends SyncEvent> + implements $SyncEventCopyWith<$Res> { + _$SyncEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SyncEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc +abstract class _$$StartSyncImplCopyWith<$Res> { + factory _$$StartSyncImplCopyWith( + _$StartSyncImpl value, + $Res Function(_$StartSyncImpl) then, + ) = __$$StartSyncImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$StartSyncImplCopyWithImpl<$Res> + extends _$SyncEventCopyWithImpl<$Res, _$StartSyncImpl> + implements _$$StartSyncImplCopyWith<$Res> { + __$$StartSyncImplCopyWithImpl( + _$StartSyncImpl _value, + $Res Function(_$StartSyncImpl) _then, + ) : super(_value, _then); + + /// Create a copy of SyncEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$StartSyncImpl implements _StartSync { + const _$StartSyncImpl(); + + @override + String toString() { + return 'SyncEvent.startSync()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$StartSyncImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() startSync, + required TResult Function() cancelSync, + }) { + return startSync(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? startSync, + TResult? Function()? cancelSync, + }) { + return startSync?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? startSync, + TResult Function()? cancelSync, + required TResult orElse(), + }) { + if (startSync != null) { + return startSync(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_StartSync value) startSync, + required TResult Function(_CancelSync value) cancelSync, + }) { + return startSync(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_StartSync value)? startSync, + TResult? Function(_CancelSync value)? cancelSync, + }) { + return startSync?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_StartSync value)? startSync, + TResult Function(_CancelSync value)? cancelSync, + required TResult orElse(), + }) { + if (startSync != null) { + return startSync(this); + } + return orElse(); + } +} + +abstract class _StartSync implements SyncEvent { + const factory _StartSync() = _$StartSyncImpl; +} + +/// @nodoc +abstract class _$$CancelSyncImplCopyWith<$Res> { + factory _$$CancelSyncImplCopyWith( + _$CancelSyncImpl value, + $Res Function(_$CancelSyncImpl) then, + ) = __$$CancelSyncImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$CancelSyncImplCopyWithImpl<$Res> + extends _$SyncEventCopyWithImpl<$Res, _$CancelSyncImpl> + implements _$$CancelSyncImplCopyWith<$Res> { + __$$CancelSyncImplCopyWithImpl( + _$CancelSyncImpl _value, + $Res Function(_$CancelSyncImpl) _then, + ) : super(_value, _then); + + /// Create a copy of SyncEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$CancelSyncImpl implements _CancelSync { + const _$CancelSyncImpl(); + + @override + String toString() { + return 'SyncEvent.cancelSync()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$CancelSyncImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() startSync, + required TResult Function() cancelSync, + }) { + return cancelSync(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? startSync, + TResult? Function()? cancelSync, + }) { + return cancelSync?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? startSync, + TResult Function()? cancelSync, + required TResult orElse(), + }) { + if (cancelSync != null) { + return cancelSync(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_StartSync value) startSync, + required TResult Function(_CancelSync value) cancelSync, + }) { + return cancelSync(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_StartSync value)? startSync, + TResult? Function(_CancelSync value)? cancelSync, + }) { + return cancelSync?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_StartSync value)? startSync, + TResult Function(_CancelSync value)? cancelSync, + required TResult orElse(), + }) { + if (cancelSync != null) { + return cancelSync(this); + } + return orElse(); + } +} + +abstract class _CancelSync implements SyncEvent { + const factory _CancelSync() = _$CancelSyncImpl; +} + +/// @nodoc +mixin _$SyncState { + bool get isSyncing => throw _privateConstructorUsedError; + double get progress => throw _privateConstructorUsedError; + SyncStep? get currentStep => throw _privateConstructorUsedError; + String? get message => throw _privateConstructorUsedError; + SyncStats? get stats => throw _privateConstructorUsedError; + String? get errorMessage => throw _privateConstructorUsedError; + + /// Create a copy of SyncState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SyncStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SyncStateCopyWith<$Res> { + factory $SyncStateCopyWith(SyncState value, $Res Function(SyncState) then) = + _$SyncStateCopyWithImpl<$Res, SyncState>; + @useResult + $Res call({ + bool isSyncing, + double progress, + SyncStep? currentStep, + String? message, + SyncStats? stats, + String? errorMessage, + }); +} + +/// @nodoc +class _$SyncStateCopyWithImpl<$Res, $Val extends SyncState> + implements $SyncStateCopyWith<$Res> { + _$SyncStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SyncState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isSyncing = null, + Object? progress = null, + Object? currentStep = freezed, + Object? message = freezed, + Object? stats = freezed, + Object? errorMessage = freezed, + }) { + return _then( + _value.copyWith( + isSyncing: null == isSyncing + ? _value.isSyncing + : isSyncing // ignore: cast_nullable_to_non_nullable + as bool, + progress: null == progress + ? _value.progress + : progress // ignore: cast_nullable_to_non_nullable + as double, + currentStep: freezed == currentStep + ? _value.currentStep + : currentStep // ignore: cast_nullable_to_non_nullable + as SyncStep?, + message: freezed == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String?, + stats: freezed == stats + ? _value.stats + : stats // ignore: cast_nullable_to_non_nullable + as SyncStats?, + errorMessage: freezed == errorMessage + ? _value.errorMessage + : errorMessage // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$SyncStateImplCopyWith<$Res> + implements $SyncStateCopyWith<$Res> { + factory _$$SyncStateImplCopyWith( + _$SyncStateImpl value, + $Res Function(_$SyncStateImpl) then, + ) = __$$SyncStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + bool isSyncing, + double progress, + SyncStep? currentStep, + String? message, + SyncStats? stats, + String? errorMessage, + }); +} + +/// @nodoc +class __$$SyncStateImplCopyWithImpl<$Res> + extends _$SyncStateCopyWithImpl<$Res, _$SyncStateImpl> + implements _$$SyncStateImplCopyWith<$Res> { + __$$SyncStateImplCopyWithImpl( + _$SyncStateImpl _value, + $Res Function(_$SyncStateImpl) _then, + ) : super(_value, _then); + + /// Create a copy of SyncState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isSyncing = null, + Object? progress = null, + Object? currentStep = freezed, + Object? message = freezed, + Object? stats = freezed, + Object? errorMessage = freezed, + }) { + return _then( + _$SyncStateImpl( + isSyncing: null == isSyncing + ? _value.isSyncing + : isSyncing // ignore: cast_nullable_to_non_nullable + as bool, + progress: null == progress + ? _value.progress + : progress // ignore: cast_nullable_to_non_nullable + as double, + currentStep: freezed == currentStep + ? _value.currentStep + : currentStep // ignore: cast_nullable_to_non_nullable + as SyncStep?, + message: freezed == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String?, + stats: freezed == stats + ? _value.stats + : stats // ignore: cast_nullable_to_non_nullable + as SyncStats?, + errorMessage: freezed == errorMessage + ? _value.errorMessage + : errorMessage // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc + +class _$SyncStateImpl implements _SyncState { + const _$SyncStateImpl({ + this.isSyncing = false, + this.progress = 0.0, + this.currentStep, + this.message, + this.stats, + this.errorMessage, + }); + + @override + @JsonKey() + final bool isSyncing; + @override + @JsonKey() + final double progress; + @override + final SyncStep? currentStep; + @override + final String? message; + @override + final SyncStats? stats; + @override + final String? errorMessage; + + @override + String toString() { + return 'SyncState(isSyncing: $isSyncing, progress: $progress, currentStep: $currentStep, message: $message, stats: $stats, errorMessage: $errorMessage)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SyncStateImpl && + (identical(other.isSyncing, isSyncing) || + other.isSyncing == isSyncing) && + (identical(other.progress, progress) || + other.progress == progress) && + (identical(other.currentStep, currentStep) || + other.currentStep == currentStep) && + (identical(other.message, message) || other.message == message) && + (identical(other.stats, stats) || other.stats == stats) && + (identical(other.errorMessage, errorMessage) || + other.errorMessage == errorMessage)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + isSyncing, + progress, + currentStep, + message, + stats, + errorMessage, + ); + + /// Create a copy of SyncState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SyncStateImplCopyWith<_$SyncStateImpl> get copyWith => + __$$SyncStateImplCopyWithImpl<_$SyncStateImpl>(this, _$identity); +} + +abstract class _SyncState implements SyncState { + const factory _SyncState({ + final bool isSyncing, + final double progress, + final SyncStep? currentStep, + final String? message, + final SyncStats? stats, + final String? errorMessage, + }) = _$SyncStateImpl; + + @override + bool get isSyncing; + @override + double get progress; + @override + SyncStep? get currentStep; + @override + String? get message; + @override + SyncStats? get stats; + @override + String? get errorMessage; + + /// Create a copy of SyncState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SyncStateImplCopyWith<_$SyncStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/application/sync/sync_event.dart b/lib/application/sync/sync_event.dart new file mode 100644 index 0000000..73d99d5 --- /dev/null +++ b/lib/application/sync/sync_event.dart @@ -0,0 +1,7 @@ +part of 'sync_bloc.dart'; + +@freezed +class SyncEvent with _$SyncEvent { + const factory SyncEvent.startSync() = _StartSync; + const factory SyncEvent.cancelSync() = _CancelSync; +} diff --git a/lib/application/sync/sync_state.dart b/lib/application/sync/sync_state.dart new file mode 100644 index 0000000..636828d --- /dev/null +++ b/lib/application/sync/sync_state.dart @@ -0,0 +1,15 @@ +part of 'sync_bloc.dart'; + +@freezed +class SyncState with _$SyncState { + const factory SyncState({ + @Default(false) bool isSyncing, + @Default(0.0) double progress, + SyncStep? currentStep, + String? message, + SyncStats? stats, + String? errorMessage, + }) = _SyncState; + + factory SyncState.initial() => const SyncState(); +} diff --git a/lib/domain/category/repositories/i_category_repository.dart b/lib/domain/category/repositories/i_category_repository.dart index 957ffe7..30c9be1 100644 --- a/lib/domain/category/repositories/i_category_repository.dart +++ b/lib/domain/category/repositories/i_category_repository.dart @@ -23,4 +23,6 @@ abstract class ICategoryRepository { Future>> getDatabaseStats(); void clearCache(); + + Future clearAllCategories(); } diff --git a/lib/domain/product/repositories/i_product_repository.dart b/lib/domain/product/repositories/i_product_repository.dart index 6407650..9b5ae6a 100644 --- a/lib/domain/product/repositories/i_product_repository.dart +++ b/lib/domain/product/repositories/i_product_repository.dart @@ -1,6 +1,10 @@ part of '../product.dart'; abstract class IProductRepository { + Future> saveProductsBatch( + List products, + ); + Future> getProducts({ int page = 1, int limit = 10, diff --git a/lib/infrastructure/category/repositories/category_repository.dart b/lib/infrastructure/category/repositories/category_repository.dart index c2c6899..36c67f5 100644 --- a/lib/infrastructure/category/repositories/category_repository.dart +++ b/lib/infrastructure/category/repositories/category_repository.dart @@ -349,4 +349,17 @@ class CategoryRepository implements ICategoryRepository { log('๐Ÿงน Clearing category cache', name: _logName); _localDataProvider.clearCache(); } + + @override + Future clearAllCategories() async { + try { + log('๐Ÿ—‘๏ธ Clearing all categories from repository...', name: _logName); + await _localDataProvider.clearAllCategories(); + clearCache(); + log('โœ… All categories cleared successfully', name: _logName); + } catch (e) { + log('โŒ Error clearing all categories: $e', name: _logName); + rethrow; + } + } } diff --git a/lib/infrastructure/product/dtos/product_dto.dart b/lib/infrastructure/product/dtos/product_dto.dart index b6a4b05..ad9f452 100644 --- a/lib/infrastructure/product/dtos/product_dto.dart +++ b/lib/infrastructure/product/dtos/product_dto.dart @@ -112,6 +112,27 @@ class ProductDto with _$ProductDto { updatedAt: map['updated_at'] as String?, variants: variants, ); + + factory ProductDto.fromDomain(Product product) => ProductDto( + id: product.id, + organizationId: product.organizationId, + categoryId: product.categoryId, + sku: product.sku, + name: product.name, + description: product.description, + price: product.price, + cost: product.cost, + businessType: product.businessType, + imageUrl: product.imageUrl, + printerType: product.printerType, + metadata: product.metadata, + isActive: product.isActive, + createdAt: product.createdAt, + updatedAt: product.updatedAt, + variants: product.variants + .map((v) => ProductVariantDto.fromDomain(v)) + .toList(), + ); } @freezed @@ -170,4 +191,16 @@ class ProductVariantDto with _$ProductVariantDto { createdAt: map['created_at'] as String?, updatedAt: map['updated_at'] as String?, ); + + factory ProductVariantDto.fromDomain(ProductVariant variant) => + ProductVariantDto( + id: variant.id, + productId: variant.productId, + name: variant.name, + priceModifier: variant.priceModifier, + cost: variant.cost, + metadata: variant.metadata, + createdAt: variant.createdAt, + updatedAt: variant.updatedAt, + ); } diff --git a/lib/infrastructure/product/repositories/product_repository.dart b/lib/infrastructure/product/repositories/product_repository.dart index 363444d..c89ab1f 100644 --- a/lib/infrastructure/product/repositories/product_repository.dart +++ b/lib/infrastructure/product/repositories/product_repository.dart @@ -6,6 +6,7 @@ import 'package:injectable/injectable.dart'; import '../../../domain/product/product.dart'; import '../datasources/local_data_provider.dart'; import '../datasources/remote_data_provider.dart'; +import '../product_dtos.dart'; @Injectable(as: IProductRepository) class ProductRepository implements IProductRepository { @@ -328,4 +329,22 @@ class ProductRepository implements IProductRepository { rethrow; } } + + @override + Future> saveProductsBatch( + List products, + ) async { + try { + final productDtos = products.map(ProductDto.fromDomain).toList(); + + // Simpan batch ke local DB + await _localDataProvider.saveProductsBatch(productDtos); + + log('๐Ÿ’พ Saved ${products.length} products to local DB', name: _logName); + return right(unit); + } catch (e, stack) { + log('โŒ Failed to save products batch: $e\n$stack', name: _logName); + return left(ProductFailure.dynamicErrorMessage(e.toString())); + } + } } diff --git a/lib/injection.config.dart b/lib/injection.config.dart index 663cc86..bf7d081 100644 --- a/lib/injection.config.dart +++ b/lib/injection.config.dart @@ -16,6 +16,7 @@ import 'package:apskel_pos_flutter_v2/application/outlet/outlet_loader/outlet_lo as _i76; import 'package:apskel_pos_flutter_v2/application/product/product_loader/product_loader_bloc.dart' as _i13; +import 'package:apskel_pos_flutter_v2/application/sync/sync_bloc.dart' as _i741; import 'package:apskel_pos_flutter_v2/common/api/api_client.dart' as _i457; import 'package:apskel_pos_flutter_v2/common/database/database_helper.dart' as _i487; @@ -158,6 +159,12 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i13.ProductLoaderBloc>( () => _i13.ProductLoaderBloc(gh<_i44.IProductRepository>()), ); + gh.factory<_i741.SyncBloc>( + () => _i741.SyncBloc( + gh<_i44.IProductRepository>(), + gh<_i502.ICategoryRepository>(), + ), + ); return this; } }