sync bloc

This commit is contained in:
efrilm 2025-10-24 22:25:01 +07:00
parent 4fdd1e44f8
commit 6892895021
10 changed files with 965 additions and 0 deletions

View 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.00.2, produk 0.20.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();
}
}

View 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;
}

View 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;
}

View 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();
}

View File

@ -23,4 +23,6 @@ abstract class ICategoryRepository {
Future<Either<CategoryFailure, Map<String, dynamic>>> getDatabaseStats();
void clearCache();
Future<void> clearAllCategories();
}

View File

@ -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,

View File

@ -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;
}
}
}

View File

@ -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,
);
}

View File

@ -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()));
}
}
}

View File

@ -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;
}
}