sync bloc
This commit is contained in:
parent
4fdd1e44f8
commit
6892895021
323
lib/application/sync/sync_bloc.dart
Normal file
323
lib/application/sync/sync_bloc.dart
Normal file
@ -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<SyncEvent, SyncState> {
|
||||
final IProductRepository _productRepository;
|
||||
final ICategoryRepository _categoryRepository;
|
||||
|
||||
Timer? _progressTimer;
|
||||
bool _isCancelled = false;
|
||||
|
||||
SyncBloc(this._productRepository, this._categoryRepository)
|
||||
: super(SyncState.initial()) {
|
||||
on<SyncEvent>(_onSyncEvent);
|
||||
}
|
||||
|
||||
Future<void> _onSyncEvent(SyncEvent event, Emitter<SyncState> 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<void> _syncCategories(Emitter<SyncState> 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<void> _syncProducts(Emitter<SyncState> 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<SyncStats> _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<String, dynamic> productStats = {};
|
||||
Map<String, dynamic> 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<void> close() {
|
||||
_progressTimer?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
542
lib/application/sync/sync_bloc.freezed.dart
Normal file
542
lib/application/sync/sync_bloc.freezed.dart
Normal file
@ -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>(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<TResult extends Object?>({
|
||||
required TResult Function() startSync,
|
||||
required TResult Function() cancelSync,
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? startSync,
|
||||
TResult? Function()? cancelSync,
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? startSync,
|
||||
TResult Function()? cancelSync,
|
||||
required TResult orElse(),
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(_StartSync value) startSync,
|
||||
required TResult Function(_CancelSync value) cancelSync,
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult? Function(_StartSync value)? startSync,
|
||||
TResult? Function(_CancelSync value)? cancelSync,
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
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<TResult extends Object?>({
|
||||
required TResult Function() startSync,
|
||||
required TResult Function() cancelSync,
|
||||
}) {
|
||||
return startSync();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? startSync,
|
||||
TResult? Function()? cancelSync,
|
||||
}) {
|
||||
return startSync?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? startSync,
|
||||
TResult Function()? cancelSync,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (startSync != null) {
|
||||
return startSync();
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(_StartSync value) startSync,
|
||||
required TResult Function(_CancelSync value) cancelSync,
|
||||
}) {
|
||||
return startSync(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult? Function(_StartSync value)? startSync,
|
||||
TResult? Function(_CancelSync value)? cancelSync,
|
||||
}) {
|
||||
return startSync?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
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<TResult extends Object?>({
|
||||
required TResult Function() startSync,
|
||||
required TResult Function() cancelSync,
|
||||
}) {
|
||||
return cancelSync();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? startSync,
|
||||
TResult? Function()? cancelSync,
|
||||
}) {
|
||||
return cancelSync?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? startSync,
|
||||
TResult Function()? cancelSync,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (cancelSync != null) {
|
||||
return cancelSync();
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(_StartSync value) startSync,
|
||||
required TResult Function(_CancelSync value) cancelSync,
|
||||
}) {
|
||||
return cancelSync(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult? Function(_StartSync value)? startSync,
|
||||
TResult? Function(_CancelSync value)? cancelSync,
|
||||
}) {
|
||||
return cancelSync?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
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<SyncState> 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;
|
||||
}
|
||||
7
lib/application/sync/sync_event.dart
Normal file
7
lib/application/sync/sync_event.dart
Normal file
@ -0,0 +1,7 @@
|
||||
part of 'sync_bloc.dart';
|
||||
|
||||
@freezed
|
||||
class SyncEvent with _$SyncEvent {
|
||||
const factory SyncEvent.startSync() = _StartSync;
|
||||
const factory SyncEvent.cancelSync() = _CancelSync;
|
||||
}
|
||||
15
lib/application/sync/sync_state.dart
Normal file
15
lib/application/sync/sync_state.dart
Normal file
@ -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();
|
||||
}
|
||||
@ -23,4 +23,6 @@ abstract class ICategoryRepository {
|
||||
Future<Either<CategoryFailure, Map<String, dynamic>>> getDatabaseStats();
|
||||
|
||||
void clearCache();
|
||||
|
||||
Future<void> clearAllCategories();
|
||||
}
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
part of '../product.dart';
|
||||
|
||||
abstract class IProductRepository {
|
||||
Future<Either<ProductFailure, Unit>> saveProductsBatch(
|
||||
List<Product> products,
|
||||
);
|
||||
|
||||
Future<Either<ProductFailure, ListProduct>> getProducts({
|
||||
int page = 1,
|
||||
int limit = 10,
|
||||
|
||||
@ -349,4 +349,17 @@ class CategoryRepository implements ICategoryRepository {
|
||||
log('🧹 Clearing category cache', name: _logName);
|
||||
_localDataProvider.clearCache();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<Either<ProductFailure, Unit>> saveProductsBatch(
|
||||
List<Product> 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user