sync product to local

This commit is contained in:
efrilm
2025-09-20 03:10:05 +07:00
parent 3022d8de9f
commit f104390141
21 changed files with 4461 additions and 514 deletions
@@ -0,0 +1,185 @@
import 'dart:async';
import 'dart:developer';
import 'package:bloc/bloc.dart';
import 'package:enaklo_pos/data/datasources/product/product_local_datasource.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../data/datasources/product_remote_datasource.dart';
part 'data_sync_event.dart';
part 'data_sync_state.dart';
part 'data_sync_bloc.freezed.dart';
enum SyncStep { products, categories, 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,
});
}
class DataSyncBloc extends Bloc<DataSyncEvent, DataSyncState> {
final ProductRemoteDatasource _remoteDatasource = ProductRemoteDatasource();
final ProductLocalDatasource _localDatasource =
ProductLocalDatasource.instance;
Timer? _progressTimer;
bool _isCancelled = false;
DataSyncBloc() : super(const DataSyncState.initial()) {
on<_StartSync>(_onStartSync);
on<_CancelSync>(_onCancelSync);
}
@override
Future<void> close() {
_progressTimer?.cancel();
return super.close();
}
Future<void> _onStartSync(
_StartSync event,
Emitter<DataSyncState> emit,
) async {
log('🔄 Starting data sync...');
_isCancelled = false;
try {
// Step 1: Clear existing local data
emit(const DataSyncState.syncing(
SyncStep.products, 0.1, 'Membersihkan data lama...'));
await _localDatasource.clearAllProducts();
if (_isCancelled) return;
// Step 2: Sync products
await _syncProducts(emit);
if (_isCancelled) return;
// Step 3: Generate final stats
emit(const DataSyncState.syncing(
SyncStep.completed, 0.9, 'Menyelesaikan sinkronisasi...'));
final stats = await _generateSyncStats();
emit(DataSyncState.completed(stats));
log('✅ Sync completed successfully');
} catch (e) {
log('❌ Sync failed: $e');
emit(DataSyncState.error('Gagal sinkronisasi: $e'));
}
}
Future<void> _syncProducts(Emitter<DataSyncState> emit) async {
log('📦 Syncing products...');
int page = 1;
int totalSynced = 0;
int? totalCount;
int? totalPages;
bool shouldContinue = true;
while (!_isCancelled && shouldContinue) {
// Calculate accurate progress based on total count
double progress = 0.2;
if (totalCount != null && (totalCount ?? 0) > 0) {
progress = 0.2 + (totalSynced / (totalCount ?? 0)) * 0.6;
}
emit(DataSyncState.syncing(
SyncStep.products,
progress,
totalCount != null
? 'Mengunduh produk... ($totalSynced dari $totalCount)'
: 'Mengunduh produk... ($totalSynced produk)',
));
final result = await _remoteDatasource.getProducts(
page: page,
limit: 50, // Bigger batch for sync
);
await result.fold(
(failure) async {
throw Exception(failure);
},
(response) async {
final products = response.data?.products ?? [];
final responseData = response.data;
// Get pagination info from first response
if (page == 1 && responseData != null) {
totalCount = responseData.totalCount;
totalPages = responseData.totalPages;
log('📊 Total products to sync: $totalCount (${totalPages} pages)');
}
if (products.isEmpty) {
shouldContinue = false;
return;
}
// Save to local database in batches
await _localDatasource.saveProductsBatch(products);
totalSynced += products.length;
page++;
log('📦 Synced page ${page - 1}: ${products.length} products (Total: $totalSynced)');
// Check if we reached the end using pagination info
if (totalPages != null && page > (totalPages ?? 0)) {
shouldContinue = false;
return;
}
// Fallback check if pagination info not available
if (products.length < 50) {
shouldContinue = false;
return;
}
// Small delay to prevent overwhelming the server
await Future.delayed(Duration(milliseconds: 100));
},
);
}
emit(DataSyncState.syncing(
SyncStep.completed,
0.8,
'Produk berhasil diunduh ($totalSynced dari ${totalCount ?? totalSynced})',
));
log('✅ Products sync completed: $totalSynced products synced');
}
Future<SyncStats> _generateSyncStats() async {
final dbStats = await _localDatasource.getDatabaseStats();
return SyncStats(
totalProducts: dbStats['total_products'] ?? 0,
totalCategories: dbStats['total_categories'] ?? 0,
totalVariants: dbStats['total_variants'] ?? 0,
databaseSizeMB: dbStats['database_size_mb'] ?? 0.0,
);
}
Future<void> _onCancelSync(
_CancelSync event,
Emitter<DataSyncState> emit,
) async {
log('⏹️ Cancelling sync...');
_isCancelled = true;
_progressTimer?.cancel();
emit(const DataSyncState.initial());
}
}
@@ -0,0 +1,962 @@
// 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 'data_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 _$DataSyncEvent {
@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 $DataSyncEventCopyWith<$Res> {
factory $DataSyncEventCopyWith(
DataSyncEvent value, $Res Function(DataSyncEvent) then) =
_$DataSyncEventCopyWithImpl<$Res, DataSyncEvent>;
}
/// @nodoc
class _$DataSyncEventCopyWithImpl<$Res, $Val extends DataSyncEvent>
implements $DataSyncEventCopyWith<$Res> {
_$DataSyncEventCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of DataSyncEvent
/// 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 _$DataSyncEventCopyWithImpl<$Res, _$StartSyncImpl>
implements _$$StartSyncImplCopyWith<$Res> {
__$$StartSyncImplCopyWithImpl(
_$StartSyncImpl _value, $Res Function(_$StartSyncImpl) _then)
: super(_value, _then);
/// Create a copy of DataSyncEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$StartSyncImpl implements _StartSync {
const _$StartSyncImpl();
@override
String toString() {
return 'DataSyncEvent.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 DataSyncEvent {
const factory _StartSync() = _$StartSyncImpl;
}
/// @nodoc
abstract class _$$CancelSyncImplCopyWith<$Res> {
factory _$$CancelSyncImplCopyWith(
_$CancelSyncImpl value, $Res Function(_$CancelSyncImpl) then) =
__$$CancelSyncImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$CancelSyncImplCopyWithImpl<$Res>
extends _$DataSyncEventCopyWithImpl<$Res, _$CancelSyncImpl>
implements _$$CancelSyncImplCopyWith<$Res> {
__$$CancelSyncImplCopyWithImpl(
_$CancelSyncImpl _value, $Res Function(_$CancelSyncImpl) _then)
: super(_value, _then);
/// Create a copy of DataSyncEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$CancelSyncImpl implements _CancelSync {
const _$CancelSyncImpl();
@override
String toString() {
return 'DataSyncEvent.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 DataSyncEvent {
const factory _CancelSync() = _$CancelSyncImpl;
}
/// @nodoc
mixin _$DataSyncState {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function(SyncStep step, double progress, String message)
syncing,
required TResult Function(SyncStats stats) completed,
required TResult Function(String message) error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function(SyncStep step, double progress, String message)? syncing,
TResult? Function(SyncStats stats)? completed,
TResult? Function(String message)? error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function(SyncStep step, double progress, String message)? syncing,
TResult Function(SyncStats stats)? completed,
TResult Function(String message)? error,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Initial value) initial,
required TResult Function(_Syncing value) syncing,
required TResult Function(_Completed value) completed,
required TResult Function(_Error value) error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Initial value)? initial,
TResult? Function(_Syncing value)? syncing,
TResult? Function(_Completed value)? completed,
TResult? Function(_Error value)? error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Initial value)? initial,
TResult Function(_Syncing value)? syncing,
TResult Function(_Completed value)? completed,
TResult Function(_Error value)? error,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $DataSyncStateCopyWith<$Res> {
factory $DataSyncStateCopyWith(
DataSyncState value, $Res Function(DataSyncState) then) =
_$DataSyncStateCopyWithImpl<$Res, DataSyncState>;
}
/// @nodoc
class _$DataSyncStateCopyWithImpl<$Res, $Val extends DataSyncState>
implements $DataSyncStateCopyWith<$Res> {
_$DataSyncStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
abstract class _$$InitialImplCopyWith<$Res> {
factory _$$InitialImplCopyWith(
_$InitialImpl value, $Res Function(_$InitialImpl) then) =
__$$InitialImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$InitialImplCopyWithImpl<$Res>
extends _$DataSyncStateCopyWithImpl<$Res, _$InitialImpl>
implements _$$InitialImplCopyWith<$Res> {
__$$InitialImplCopyWithImpl(
_$InitialImpl _value, $Res Function(_$InitialImpl) _then)
: super(_value, _then);
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$InitialImpl implements _Initial {
const _$InitialImpl();
@override
String toString() {
return 'DataSyncState.initial()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$InitialImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function(SyncStep step, double progress, String message)
syncing,
required TResult Function(SyncStats stats) completed,
required TResult Function(String message) error,
}) {
return initial();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function(SyncStep step, double progress, String message)? syncing,
TResult? Function(SyncStats stats)? completed,
TResult? Function(String message)? error,
}) {
return initial?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function(SyncStep step, double progress, String message)? syncing,
TResult Function(SyncStats stats)? completed,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (initial != null) {
return initial();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Initial value) initial,
required TResult Function(_Syncing value) syncing,
required TResult Function(_Completed value) completed,
required TResult Function(_Error value) error,
}) {
return initial(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Initial value)? initial,
TResult? Function(_Syncing value)? syncing,
TResult? Function(_Completed value)? completed,
TResult? Function(_Error value)? error,
}) {
return initial?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Initial value)? initial,
TResult Function(_Syncing value)? syncing,
TResult Function(_Completed value)? completed,
TResult Function(_Error value)? error,
required TResult orElse(),
}) {
if (initial != null) {
return initial(this);
}
return orElse();
}
}
abstract class _Initial implements DataSyncState {
const factory _Initial() = _$InitialImpl;
}
/// @nodoc
abstract class _$$SyncingImplCopyWith<$Res> {
factory _$$SyncingImplCopyWith(
_$SyncingImpl value, $Res Function(_$SyncingImpl) then) =
__$$SyncingImplCopyWithImpl<$Res>;
@useResult
$Res call({SyncStep step, double progress, String message});
}
/// @nodoc
class __$$SyncingImplCopyWithImpl<$Res>
extends _$DataSyncStateCopyWithImpl<$Res, _$SyncingImpl>
implements _$$SyncingImplCopyWith<$Res> {
__$$SyncingImplCopyWithImpl(
_$SyncingImpl _value, $Res Function(_$SyncingImpl) _then)
: super(_value, _then);
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? step = null,
Object? progress = null,
Object? message = null,
}) {
return _then(_$SyncingImpl(
null == step
? _value.step
: step // ignore: cast_nullable_to_non_nullable
as SyncStep,
null == progress
? _value.progress
: progress // ignore: cast_nullable_to_non_nullable
as double,
null == message
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
class _$SyncingImpl implements _Syncing {
const _$SyncingImpl(this.step, this.progress, this.message);
@override
final SyncStep step;
@override
final double progress;
@override
final String message;
@override
String toString() {
return 'DataSyncState.syncing(step: $step, progress: $progress, message: $message)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SyncingImpl &&
(identical(other.step, step) || other.step == step) &&
(identical(other.progress, progress) ||
other.progress == progress) &&
(identical(other.message, message) || other.message == message));
}
@override
int get hashCode => Object.hash(runtimeType, step, progress, message);
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$SyncingImplCopyWith<_$SyncingImpl> get copyWith =>
__$$SyncingImplCopyWithImpl<_$SyncingImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function(SyncStep step, double progress, String message)
syncing,
required TResult Function(SyncStats stats) completed,
required TResult Function(String message) error,
}) {
return syncing(step, progress, message);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function(SyncStep step, double progress, String message)? syncing,
TResult? Function(SyncStats stats)? completed,
TResult? Function(String message)? error,
}) {
return syncing?.call(step, progress, message);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function(SyncStep step, double progress, String message)? syncing,
TResult Function(SyncStats stats)? completed,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (syncing != null) {
return syncing(step, progress, message);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Initial value) initial,
required TResult Function(_Syncing value) syncing,
required TResult Function(_Completed value) completed,
required TResult Function(_Error value) error,
}) {
return syncing(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Initial value)? initial,
TResult? Function(_Syncing value)? syncing,
TResult? Function(_Completed value)? completed,
TResult? Function(_Error value)? error,
}) {
return syncing?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Initial value)? initial,
TResult Function(_Syncing value)? syncing,
TResult Function(_Completed value)? completed,
TResult Function(_Error value)? error,
required TResult orElse(),
}) {
if (syncing != null) {
return syncing(this);
}
return orElse();
}
}
abstract class _Syncing implements DataSyncState {
const factory _Syncing(
final SyncStep step, final double progress, final String message) =
_$SyncingImpl;
SyncStep get step;
double get progress;
String get message;
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SyncingImplCopyWith<_$SyncingImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$CompletedImplCopyWith<$Res> {
factory _$$CompletedImplCopyWith(
_$CompletedImpl value, $Res Function(_$CompletedImpl) then) =
__$$CompletedImplCopyWithImpl<$Res>;
@useResult
$Res call({SyncStats stats});
}
/// @nodoc
class __$$CompletedImplCopyWithImpl<$Res>
extends _$DataSyncStateCopyWithImpl<$Res, _$CompletedImpl>
implements _$$CompletedImplCopyWith<$Res> {
__$$CompletedImplCopyWithImpl(
_$CompletedImpl _value, $Res Function(_$CompletedImpl) _then)
: super(_value, _then);
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? stats = null,
}) {
return _then(_$CompletedImpl(
null == stats
? _value.stats
: stats // ignore: cast_nullable_to_non_nullable
as SyncStats,
));
}
}
/// @nodoc
class _$CompletedImpl implements _Completed {
const _$CompletedImpl(this.stats);
@override
final SyncStats stats;
@override
String toString() {
return 'DataSyncState.completed(stats: $stats)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$CompletedImpl &&
(identical(other.stats, stats) || other.stats == stats));
}
@override
int get hashCode => Object.hash(runtimeType, stats);
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$CompletedImplCopyWith<_$CompletedImpl> get copyWith =>
__$$CompletedImplCopyWithImpl<_$CompletedImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function(SyncStep step, double progress, String message)
syncing,
required TResult Function(SyncStats stats) completed,
required TResult Function(String message) error,
}) {
return completed(stats);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function(SyncStep step, double progress, String message)? syncing,
TResult? Function(SyncStats stats)? completed,
TResult? Function(String message)? error,
}) {
return completed?.call(stats);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function(SyncStep step, double progress, String message)? syncing,
TResult Function(SyncStats stats)? completed,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (completed != null) {
return completed(stats);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Initial value) initial,
required TResult Function(_Syncing value) syncing,
required TResult Function(_Completed value) completed,
required TResult Function(_Error value) error,
}) {
return completed(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Initial value)? initial,
TResult? Function(_Syncing value)? syncing,
TResult? Function(_Completed value)? completed,
TResult? Function(_Error value)? error,
}) {
return completed?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Initial value)? initial,
TResult Function(_Syncing value)? syncing,
TResult Function(_Completed value)? completed,
TResult Function(_Error value)? error,
required TResult orElse(),
}) {
if (completed != null) {
return completed(this);
}
return orElse();
}
}
abstract class _Completed implements DataSyncState {
const factory _Completed(final SyncStats stats) = _$CompletedImpl;
SyncStats get stats;
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$CompletedImplCopyWith<_$CompletedImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$ErrorImplCopyWith<$Res> {
factory _$$ErrorImplCopyWith(
_$ErrorImpl value, $Res Function(_$ErrorImpl) then) =
__$$ErrorImplCopyWithImpl<$Res>;
@useResult
$Res call({String message});
}
/// @nodoc
class __$$ErrorImplCopyWithImpl<$Res>
extends _$DataSyncStateCopyWithImpl<$Res, _$ErrorImpl>
implements _$$ErrorImplCopyWith<$Res> {
__$$ErrorImplCopyWithImpl(
_$ErrorImpl _value, $Res Function(_$ErrorImpl) _then)
: super(_value, _then);
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? message = null,
}) {
return _then(_$ErrorImpl(
null == message
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
class _$ErrorImpl implements _Error {
const _$ErrorImpl(this.message);
@override
final String message;
@override
String toString() {
return 'DataSyncState.error(message: $message)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ErrorImpl &&
(identical(other.message, message) || other.message == message));
}
@override
int get hashCode => Object.hash(runtimeType, message);
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ErrorImplCopyWith<_$ErrorImpl> get copyWith =>
__$$ErrorImplCopyWithImpl<_$ErrorImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function(SyncStep step, double progress, String message)
syncing,
required TResult Function(SyncStats stats) completed,
required TResult Function(String message) error,
}) {
return error(message);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function(SyncStep step, double progress, String message)? syncing,
TResult? Function(SyncStats stats)? completed,
TResult? Function(String message)? error,
}) {
return error?.call(message);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function(SyncStep step, double progress, String message)? syncing,
TResult Function(SyncStats stats)? completed,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (error != null) {
return error(message);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Initial value) initial,
required TResult Function(_Syncing value) syncing,
required TResult Function(_Completed value) completed,
required TResult Function(_Error value) error,
}) {
return error(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Initial value)? initial,
TResult? Function(_Syncing value)? syncing,
TResult? Function(_Completed value)? completed,
TResult? Function(_Error value)? error,
}) {
return error?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Initial value)? initial,
TResult Function(_Syncing value)? syncing,
TResult Function(_Completed value)? completed,
TResult Function(_Error value)? error,
required TResult orElse(),
}) {
if (error != null) {
return error(this);
}
return orElse();
}
}
abstract class _Error implements DataSyncState {
const factory _Error(final String message) = _$ErrorImpl;
String get message;
/// Create a copy of DataSyncState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ErrorImplCopyWith<_$ErrorImpl> get copyWith =>
throw _privateConstructorUsedError;
}
@@ -0,0 +1,7 @@
part of 'data_sync_bloc.dart';
@freezed
class DataSyncEvent with _$DataSyncEvent {
const factory DataSyncEvent.startSync() = _StartSync;
const factory DataSyncEvent.cancelSync() = _CancelSync;
}
@@ -0,0 +1,13 @@
part of 'data_sync_bloc.dart';
@freezed
class DataSyncState with _$DataSyncState {
const factory DataSyncState.initial() = _Initial;
const factory DataSyncState.syncing(
SyncStep step,
double progress,
String message,
) = _Syncing;
const factory DataSyncState.completed(SyncStats stats) = _Completed;
const factory DataSyncState.error(String message) = _Error;
}
@@ -0,0 +1,635 @@
// ========================================
// DATA SYNC PAGE - POST LOGIN SYNC
// lib/presentation/sync/pages/data_sync_page.dart
// ========================================
import 'dart:async';
import 'package:enaklo_pos/core/extensions/build_context_ext.dart';
import 'package:enaklo_pos/presentation/home/pages/dashboard_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/components/buttons.dart';
import '../../../core/components/spaces.dart';
import '../../../core/constants/colors.dart';
import '../bloc/data_sync_bloc.dart';
class DataSyncPage extends StatefulWidget {
const DataSyncPage({super.key});
@override
State<DataSyncPage> createState() => _DataSyncPageState();
}
class _DataSyncPageState extends State<DataSyncPage>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _progressAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: Duration(milliseconds: 500),
vsync: this,
);
_progressAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
// Auto start sync
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<DataSyncBloc>().add(const DataSyncEvent.startSync());
});
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.shade50,
body: SafeArea(
child: BlocConsumer<DataSyncBloc, DataSyncState>(
listener: (context, state) {
state.maybeWhen(
orElse: () {},
syncing: (step, progress, message) {
_animationController.animateTo(progress);
},
completed: (stats) {
_animationController.animateTo(1.0);
// Navigate to home after delay
Future.delayed(Duration(seconds: 2), () {
context.pushReplacement(DashboardPage());
});
},
error: (message) {
_animationController.stop();
},
);
},
builder: (context, state) {
return Padding(
padding: EdgeInsets.all(24),
child: Column(
children: [
SpaceHeight(60),
// Header
_buildHeader(),
SpaceHeight(60),
// Sync progress
Expanded(
child: state.when(
initial: () => _buildInitialState(),
syncing: (step, progress, message) =>
_buildSyncingState(step, progress, message),
completed: (stats) => _buildCompletedState(stats),
error: (message) => _buildErrorState(message),
),
),
SpaceHeight(40),
// Actions
_buildActions(state),
],
),
);
},
),
),
);
}
Widget _buildHeader() {
return Column(
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Icon(
Icons.sync,
size: 40,
color: AppColors.primary,
),
),
SpaceHeight(20),
Text(
'Sinkronisasi Data',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
SpaceHeight(8),
Text(
'Mengunduh data terbaru ke perangkat',
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade600,
),
textAlign: TextAlign.center,
),
],
);
}
Widget _buildInitialState() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.download_rounded,
size: 64,
color: Colors.grey.shade400,
),
SpaceHeight(20),
Text(
'Siap untuk sinkronisasi',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
SpaceHeight(8),
Text(
'Tekan tombol mulai untuk mengunduh data',
style: TextStyle(
color: Colors.grey.shade600,
),
),
],
);
}
Widget _buildSyncingState(SyncStep step, double progress, String message) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Progress circle
Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 120,
height: 120,
child: AnimatedBuilder(
animation: _progressAnimation,
builder: (context, child) {
return CircularProgressIndicator(
value: _progressAnimation.value,
strokeWidth: 8,
backgroundColor: Colors.grey.shade200,
valueColor:
AlwaysStoppedAnimation<Color>(AppColors.primary),
);
},
),
),
Column(
children: [
Icon(
_getSyncIcon(step),
size: 32,
color: AppColors.primary,
),
SpaceHeight(4),
AnimatedBuilder(
animation: _progressAnimation,
builder: (context, child) {
return Text(
'${(_progressAnimation.value * 100).toInt()}%',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
);
},
),
],
),
],
),
SpaceHeight(30),
// Step indicator
_buildStepIndicator(step),
SpaceHeight(20),
// Current message
Container(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Text(
message,
style: TextStyle(
color: Colors.blue.shade700,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
SpaceHeight(20),
// Sync details
_buildSyncDetails(step, progress),
],
);
}
Widget _buildStepIndicator(SyncStep currentStep) {
final steps = [
('Produk', SyncStep.products, Icons.inventory_2),
('Kategori', SyncStep.categories, Icons.category),
('Variant', SyncStep.variants, Icons.tune),
('Selesai', SyncStep.completed, Icons.check_circle),
];
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: steps.map((stepData) {
final (label, step, icon) = stepData;
final isActive = step == currentStep;
final isCompleted = step.index < currentStep.index;
return Container(
margin: EdgeInsets.symmetric(horizontal: 4),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isActive
? AppColors.primary.withOpacity(0.1)
: isCompleted
? Colors.green.shade50
: Colors.grey.shade100,
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isCompleted ? Icons.check : icon,
size: 14,
color: isActive
? AppColors.primary
: isCompleted
? Colors.green.shade600
: Colors.grey.shade500,
),
SizedBox(width: 4),
Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
color: isActive
? AppColors.primary
: isCompleted
? Colors.green.shade600
: Colors.grey.shade600,
),
),
],
),
);
}).toList(),
);
}
Widget _buildSyncDetails(SyncStep step, double progress) {
return Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Status:',
style: TextStyle(
fontWeight: FontWeight.w500,
color: Colors.grey.shade700,
),
),
Text(
_getStepLabel(step),
style: TextStyle(
fontWeight: FontWeight.w600,
color: AppColors.primary,
),
),
],
),
SpaceHeight(8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Progress:',
style: TextStyle(
fontWeight: FontWeight.w500,
color: Colors.grey.shade700,
),
),
Text(
'${(progress * 100).toInt()}%',
style: TextStyle(
fontWeight: FontWeight.w600,
color: AppColors.primary,
),
),
],
),
],
),
);
}
Widget _buildCompletedState(SyncStats stats) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Success icon
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(50),
),
child: Icon(
Icons.check_circle,
size: 60,
color: Colors.green.shade600,
),
),
SpaceHeight(30),
Text(
'Sinkronisasi Berhasil!',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.green.shade700,
),
),
SpaceHeight(16),
Text(
'Data berhasil diunduh ke perangkat',
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade600,
),
),
SpaceHeight(30),
// Stats cards
Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
children: [
Text(
'Data yang Diunduh',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
),
SpaceHeight(16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem(
'Produk',
'${stats.totalProducts}',
Icons.inventory_2,
Colors.blue,
),
_buildStatItem(
'Kategori',
'${stats.totalCategories}',
Icons.category,
Colors.green,
),
_buildStatItem(
'Variant',
'${stats.totalVariants}',
Icons.tune,
Colors.orange,
),
],
),
],
),
),
SpaceHeight(20),
Container(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(6),
),
child: Text(
'Mengalihkan ke halaman utama...',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 12,
),
),
),
],
);
}
Widget _buildErrorState(String message) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: Colors.red.shade400,
),
SpaceHeight(20),
Text(
'Sinkronisasi Gagal',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.red.shade600,
),
),
SpaceHeight(12),
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Text(
message,
style: TextStyle(
color: Colors.red.shade700,
),
textAlign: TextAlign.center,
),
),
SpaceHeight(20),
Text(
'Periksa koneksi internet dan coba lagi',
style: TextStyle(
color: Colors.grey.shade600,
),
textAlign: TextAlign.center,
),
],
);
}
Widget _buildStatItem(
String label, String value, IconData icon, Color color) {
return Column(
children: [
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
size: 24,
color: color,
),
),
SpaceHeight(8),
Text(
value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
);
}
Widget _buildActions(DataSyncState state) {
return state.when(
initial: () => Button.filled(
onPressed: () {
context.read<DataSyncBloc>().add(const DataSyncEvent.startSync());
},
label: 'Mulai Sinkronisasi',
),
syncing: (step, progress, message) => Button.outlined(
onPressed: () {
context.read<DataSyncBloc>().add(const DataSyncEvent.cancelSync());
},
label: 'Batalkan',
),
completed: (stats) => Button.filled(
onPressed: () {
Navigator.of(context).pushReplacementNamed('/home');
},
label: 'Lanjutkan ke Aplikasi',
),
error: (message) => Row(
children: [
Expanded(
child: Button.outlined(
onPressed: () {
Navigator.of(context).pushReplacementNamed('/home');
},
label: 'Lewati',
),
),
SizedBox(width: 16),
Expanded(
child: Button.filled(
onPressed: () {
context
.read<DataSyncBloc>()
.add(const DataSyncEvent.startSync());
},
label: 'Coba Lagi',
),
),
],
),
);
}
IconData _getSyncIcon(SyncStep step) {
switch (step) {
case SyncStep.products:
return Icons.inventory_2;
case SyncStep.categories:
return Icons.category;
case SyncStep.variants:
return Icons.tune;
case SyncStep.completed:
return Icons.check_circle;
}
}
String _getStepLabel(SyncStep step) {
switch (step) {
case SyncStep.products:
return 'Mengunduh Produk';
case SyncStep.categories:
return 'Mengunduh Kategori';
case SyncStep.variants:
return 'Mengunduh Variant';
case SyncStep.completed:
return 'Selesai';
}
}
}