diff --git a/lib/application/outlet/outlet_list_loader/outlet_list_loader_bloc.dart b/lib/application/outlet/outlet_list_loader/outlet_list_loader_bloc.dart new file mode 100644 index 0000000..4b8ea49 --- /dev/null +++ b/lib/application/outlet/outlet_list_loader/outlet_list_loader_bloc.dart @@ -0,0 +1,92 @@ +import 'package:dartz/dartz.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:injectable/injectable.dart'; + +import '../../../domain/outlet/outlet.dart'; + +part 'outlet_list_loader_event.dart'; +part 'outlet_list_loader_state.dart'; +part 'outlet_list_loader_bloc.freezed.dart'; + +@injectable +class OutletListLoaderBloc + extends Bloc { + final IOutletRepository _outletRepository; + + OutletListLoaderBloc(this._outletRepository) + : super(OutletListLoaderState.initial()) { + on(_onOutletListLoaderEvent); + } + + Future _onOutletListLoaderEvent( + OutletListLoaderEvent event, + Emitter emit, + ) { + return event.map( + searchChanged: (e) async { + emit(state.copyWith(search: e.search)); + }, + isActiveChanged: (e) async { + emit(state.copyWith(isActive: e.isActive)); + }, + fetched: (e) async { + var newState = state; + + if (e.isRefresh) { + newState = state.copyWith(isFetching: true); + emit(newState); + } + + newState = await _mapFetchedToState(state, isRefresh: e.isRefresh); + + emit(newState); + }, + ); + } + + Future _mapFetchedToState( + OutletListLoaderState state, { + bool isRefresh = false, + }) async { + state = state.copyWith(isFetching: false); + + if (state.hasReachedMax && state.outlets.isNotEmpty && !isRefresh) { + return state; + } + + if (isRefresh) { + state = state.copyWith( + page: 1, + failureOptionOutlet: none(), + hasReachedMax: false, + outlets: [], + ); + } + + final failureOrOutlets = await _outletRepository.getList( + page: state.page, + search: state.search, + isActive: state.isActive, + ); + + state = failureOrOutlets.fold( + (f) { + if (state.outlets.isNotEmpty) { + return state.copyWith(hasReachedMax: true); + } + return state.copyWith(failureOptionOutlet: optionOf(f)); + }, + (outlets) { + return state.copyWith( + outlets: List.from(state.outlets)..addAll(outlets), + failureOptionOutlet: none(), + page: state.page + 1, + hasReachedMax: outlets.length < 10, + ); + }, + ); + + return state; + } +} diff --git a/lib/application/outlet/outlet_list_loader/outlet_list_loader_bloc.freezed.dart b/lib/application/outlet/outlet_list_loader/outlet_list_loader_bloc.freezed.dart new file mode 100644 index 0000000..65436de --- /dev/null +++ b/lib/application/outlet/outlet_list_loader/outlet_list_loader_bloc.freezed.dart @@ -0,0 +1,828 @@ +// 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 'outlet_list_loader_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +/// @nodoc +mixin _$OutletListLoaderEvent { + @optionalTypeArgs + TResult when({ + required TResult Function(String search) searchChanged, + required TResult Function(bool? isActive) isActiveChanged, + required TResult Function(bool isRefresh) fetched, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String search)? searchChanged, + TResult? Function(bool? isActive)? isActiveChanged, + TResult? Function(bool isRefresh)? fetched, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String search)? searchChanged, + TResult Function(bool? isActive)? isActiveChanged, + TResult Function(bool isRefresh)? fetched, + required TResult orElse(), + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_SearchChanged value) searchChanged, + required TResult Function(_IsActiveChanged value) isActiveChanged, + required TResult Function(_Fetched value) fetched, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_SearchChanged value)? searchChanged, + TResult? Function(_IsActiveChanged value)? isActiveChanged, + TResult? Function(_Fetched value)? fetched, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_SearchChanged value)? searchChanged, + TResult Function(_IsActiveChanged value)? isActiveChanged, + TResult Function(_Fetched value)? fetched, + required TResult orElse(), + }) => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $OutletListLoaderEventCopyWith<$Res> { + factory $OutletListLoaderEventCopyWith( + OutletListLoaderEvent value, + $Res Function(OutletListLoaderEvent) then, + ) = _$OutletListLoaderEventCopyWithImpl<$Res, OutletListLoaderEvent>; +} + +/// @nodoc +class _$OutletListLoaderEventCopyWithImpl< + $Res, + $Val extends OutletListLoaderEvent +> + implements $OutletListLoaderEventCopyWith<$Res> { + _$OutletListLoaderEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of OutletListLoaderEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc +abstract class _$$SearchChangedImplCopyWith<$Res> { + factory _$$SearchChangedImplCopyWith( + _$SearchChangedImpl value, + $Res Function(_$SearchChangedImpl) then, + ) = __$$SearchChangedImplCopyWithImpl<$Res>; + @useResult + $Res call({String search}); +} + +/// @nodoc +class __$$SearchChangedImplCopyWithImpl<$Res> + extends _$OutletListLoaderEventCopyWithImpl<$Res, _$SearchChangedImpl> + implements _$$SearchChangedImplCopyWith<$Res> { + __$$SearchChangedImplCopyWithImpl( + _$SearchChangedImpl _value, + $Res Function(_$SearchChangedImpl) _then, + ) : super(_value, _then); + + /// Create a copy of OutletListLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? search = null}) { + return _then( + _$SearchChangedImpl( + null == search + ? _value.search + : search // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc + +class _$SearchChangedImpl implements _SearchChanged { + const _$SearchChangedImpl(this.search); + + @override + final String search; + + @override + String toString() { + return 'OutletListLoaderEvent.searchChanged(search: $search)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SearchChangedImpl && + (identical(other.search, search) || other.search == search)); + } + + @override + int get hashCode => Object.hash(runtimeType, search); + + /// Create a copy of OutletListLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SearchChangedImplCopyWith<_$SearchChangedImpl> get copyWith => + __$$SearchChangedImplCopyWithImpl<_$SearchChangedImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String search) searchChanged, + required TResult Function(bool? isActive) isActiveChanged, + required TResult Function(bool isRefresh) fetched, + }) { + return searchChanged(search); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String search)? searchChanged, + TResult? Function(bool? isActive)? isActiveChanged, + TResult? Function(bool isRefresh)? fetched, + }) { + return searchChanged?.call(search); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String search)? searchChanged, + TResult Function(bool? isActive)? isActiveChanged, + TResult Function(bool isRefresh)? fetched, + required TResult orElse(), + }) { + if (searchChanged != null) { + return searchChanged(search); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_SearchChanged value) searchChanged, + required TResult Function(_IsActiveChanged value) isActiveChanged, + required TResult Function(_Fetched value) fetched, + }) { + return searchChanged(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_SearchChanged value)? searchChanged, + TResult? Function(_IsActiveChanged value)? isActiveChanged, + TResult? Function(_Fetched value)? fetched, + }) { + return searchChanged?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_SearchChanged value)? searchChanged, + TResult Function(_IsActiveChanged value)? isActiveChanged, + TResult Function(_Fetched value)? fetched, + required TResult orElse(), + }) { + if (searchChanged != null) { + return searchChanged(this); + } + return orElse(); + } +} + +abstract class _SearchChanged implements OutletListLoaderEvent { + const factory _SearchChanged(final String search) = _$SearchChangedImpl; + + String get search; + + /// Create a copy of OutletListLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SearchChangedImplCopyWith<_$SearchChangedImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$IsActiveChangedImplCopyWith<$Res> { + factory _$$IsActiveChangedImplCopyWith( + _$IsActiveChangedImpl value, + $Res Function(_$IsActiveChangedImpl) then, + ) = __$$IsActiveChangedImplCopyWithImpl<$Res>; + @useResult + $Res call({bool? isActive}); +} + +/// @nodoc +class __$$IsActiveChangedImplCopyWithImpl<$Res> + extends _$OutletListLoaderEventCopyWithImpl<$Res, _$IsActiveChangedImpl> + implements _$$IsActiveChangedImplCopyWith<$Res> { + __$$IsActiveChangedImplCopyWithImpl( + _$IsActiveChangedImpl _value, + $Res Function(_$IsActiveChangedImpl) _then, + ) : super(_value, _then); + + /// Create a copy of OutletListLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? isActive = freezed}) { + return _then( + _$IsActiveChangedImpl( + freezed == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool?, + ), + ); + } +} + +/// @nodoc + +class _$IsActiveChangedImpl implements _IsActiveChanged { + const _$IsActiveChangedImpl(this.isActive); + + @override + final bool? isActive; + + @override + String toString() { + return 'OutletListLoaderEvent.isActiveChanged(isActive: $isActive)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$IsActiveChangedImpl && + (identical(other.isActive, isActive) || + other.isActive == isActive)); + } + + @override + int get hashCode => Object.hash(runtimeType, isActive); + + /// Create a copy of OutletListLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$IsActiveChangedImplCopyWith<_$IsActiveChangedImpl> get copyWith => + __$$IsActiveChangedImplCopyWithImpl<_$IsActiveChangedImpl>( + this, + _$identity, + ); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String search) searchChanged, + required TResult Function(bool? isActive) isActiveChanged, + required TResult Function(bool isRefresh) fetched, + }) { + return isActiveChanged(isActive); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String search)? searchChanged, + TResult? Function(bool? isActive)? isActiveChanged, + TResult? Function(bool isRefresh)? fetched, + }) { + return isActiveChanged?.call(isActive); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String search)? searchChanged, + TResult Function(bool? isActive)? isActiveChanged, + TResult Function(bool isRefresh)? fetched, + required TResult orElse(), + }) { + if (isActiveChanged != null) { + return isActiveChanged(isActive); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_SearchChanged value) searchChanged, + required TResult Function(_IsActiveChanged value) isActiveChanged, + required TResult Function(_Fetched value) fetched, + }) { + return isActiveChanged(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_SearchChanged value)? searchChanged, + TResult? Function(_IsActiveChanged value)? isActiveChanged, + TResult? Function(_Fetched value)? fetched, + }) { + return isActiveChanged?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_SearchChanged value)? searchChanged, + TResult Function(_IsActiveChanged value)? isActiveChanged, + TResult Function(_Fetched value)? fetched, + required TResult orElse(), + }) { + if (isActiveChanged != null) { + return isActiveChanged(this); + } + return orElse(); + } +} + +abstract class _IsActiveChanged implements OutletListLoaderEvent { + const factory _IsActiveChanged(final bool? isActive) = _$IsActiveChangedImpl; + + bool? get isActive; + + /// Create a copy of OutletListLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$IsActiveChangedImplCopyWith<_$IsActiveChangedImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$FetchedImplCopyWith<$Res> { + factory _$$FetchedImplCopyWith( + _$FetchedImpl value, + $Res Function(_$FetchedImpl) then, + ) = __$$FetchedImplCopyWithImpl<$Res>; + @useResult + $Res call({bool isRefresh}); +} + +/// @nodoc +class __$$FetchedImplCopyWithImpl<$Res> + extends _$OutletListLoaderEventCopyWithImpl<$Res, _$FetchedImpl> + implements _$$FetchedImplCopyWith<$Res> { + __$$FetchedImplCopyWithImpl( + _$FetchedImpl _value, + $Res Function(_$FetchedImpl) _then, + ) : super(_value, _then); + + /// Create a copy of OutletListLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? isRefresh = null}) { + return _then( + _$FetchedImpl( + isRefresh: null == isRefresh + ? _value.isRefresh + : isRefresh // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} + +/// @nodoc + +class _$FetchedImpl implements _Fetched { + const _$FetchedImpl({this.isRefresh = false}); + + @override + @JsonKey() + final bool isRefresh; + + @override + String toString() { + return 'OutletListLoaderEvent.fetched(isRefresh: $isRefresh)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$FetchedImpl && + (identical(other.isRefresh, isRefresh) || + other.isRefresh == isRefresh)); + } + + @override + int get hashCode => Object.hash(runtimeType, isRefresh); + + /// Create a copy of OutletListLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$FetchedImplCopyWith<_$FetchedImpl> get copyWith => + __$$FetchedImplCopyWithImpl<_$FetchedImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String search) searchChanged, + required TResult Function(bool? isActive) isActiveChanged, + required TResult Function(bool isRefresh) fetched, + }) { + return fetched(isRefresh); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String search)? searchChanged, + TResult? Function(bool? isActive)? isActiveChanged, + TResult? Function(bool isRefresh)? fetched, + }) { + return fetched?.call(isRefresh); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String search)? searchChanged, + TResult Function(bool? isActive)? isActiveChanged, + TResult Function(bool isRefresh)? fetched, + required TResult orElse(), + }) { + if (fetched != null) { + return fetched(isRefresh); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_SearchChanged value) searchChanged, + required TResult Function(_IsActiveChanged value) isActiveChanged, + required TResult Function(_Fetched value) fetched, + }) { + return fetched(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_SearchChanged value)? searchChanged, + TResult? Function(_IsActiveChanged value)? isActiveChanged, + TResult? Function(_Fetched value)? fetched, + }) { + return fetched?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_SearchChanged value)? searchChanged, + TResult Function(_IsActiveChanged value)? isActiveChanged, + TResult Function(_Fetched value)? fetched, + required TResult orElse(), + }) { + if (fetched != null) { + return fetched(this); + } + return orElse(); + } +} + +abstract class _Fetched implements OutletListLoaderEvent { + const factory _Fetched({final bool isRefresh}) = _$FetchedImpl; + + bool get isRefresh; + + /// Create a copy of OutletListLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$FetchedImplCopyWith<_$FetchedImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$OutletListLoaderState { + List get outlets => throw _privateConstructorUsedError; + Option get failureOptionOutlet => + throw _privateConstructorUsedError; + String? get search => throw _privateConstructorUsedError; + bool? get isActive => throw _privateConstructorUsedError; + bool get isFetching => throw _privateConstructorUsedError; + bool get hasReachedMax => throw _privateConstructorUsedError; + int get page => throw _privateConstructorUsedError; + + /// Create a copy of OutletListLoaderState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $OutletListLoaderStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $OutletListLoaderStateCopyWith<$Res> { + factory $OutletListLoaderStateCopyWith( + OutletListLoaderState value, + $Res Function(OutletListLoaderState) then, + ) = _$OutletListLoaderStateCopyWithImpl<$Res, OutletListLoaderState>; + @useResult + $Res call({ + List outlets, + Option failureOptionOutlet, + String? search, + bool? isActive, + bool isFetching, + bool hasReachedMax, + int page, + }); +} + +/// @nodoc +class _$OutletListLoaderStateCopyWithImpl< + $Res, + $Val extends OutletListLoaderState +> + implements $OutletListLoaderStateCopyWith<$Res> { + _$OutletListLoaderStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of OutletListLoaderState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? outlets = null, + Object? failureOptionOutlet = null, + Object? search = freezed, + Object? isActive = freezed, + Object? isFetching = null, + Object? hasReachedMax = null, + Object? page = null, + }) { + return _then( + _value.copyWith( + outlets: null == outlets + ? _value.outlets + : outlets // ignore: cast_nullable_to_non_nullable + as List, + failureOptionOutlet: null == failureOptionOutlet + ? _value.failureOptionOutlet + : failureOptionOutlet // ignore: cast_nullable_to_non_nullable + as Option, + search: freezed == search + ? _value.search + : search // ignore: cast_nullable_to_non_nullable + as String?, + isActive: freezed == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool?, + isFetching: null == isFetching + ? _value.isFetching + : isFetching // ignore: cast_nullable_to_non_nullable + as bool, + hasReachedMax: null == hasReachedMax + ? _value.hasReachedMax + : hasReachedMax // ignore: cast_nullable_to_non_nullable + as bool, + page: null == page + ? _value.page + : page // ignore: cast_nullable_to_non_nullable + as int, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$OutletListLoaderStateImplCopyWith<$Res> + implements $OutletListLoaderStateCopyWith<$Res> { + factory _$$OutletListLoaderStateImplCopyWith( + _$OutletListLoaderStateImpl value, + $Res Function(_$OutletListLoaderStateImpl) then, + ) = __$$OutletListLoaderStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + List outlets, + Option failureOptionOutlet, + String? search, + bool? isActive, + bool isFetching, + bool hasReachedMax, + int page, + }); +} + +/// @nodoc +class __$$OutletListLoaderStateImplCopyWithImpl<$Res> + extends + _$OutletListLoaderStateCopyWithImpl<$Res, _$OutletListLoaderStateImpl> + implements _$$OutletListLoaderStateImplCopyWith<$Res> { + __$$OutletListLoaderStateImplCopyWithImpl( + _$OutletListLoaderStateImpl _value, + $Res Function(_$OutletListLoaderStateImpl) _then, + ) : super(_value, _then); + + /// Create a copy of OutletListLoaderState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? outlets = null, + Object? failureOptionOutlet = null, + Object? search = freezed, + Object? isActive = freezed, + Object? isFetching = null, + Object? hasReachedMax = null, + Object? page = null, + }) { + return _then( + _$OutletListLoaderStateImpl( + outlets: null == outlets + ? _value._outlets + : outlets // ignore: cast_nullable_to_non_nullable + as List, + failureOptionOutlet: null == failureOptionOutlet + ? _value.failureOptionOutlet + : failureOptionOutlet // ignore: cast_nullable_to_non_nullable + as Option, + search: freezed == search + ? _value.search + : search // ignore: cast_nullable_to_non_nullable + as String?, + isActive: freezed == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool?, + isFetching: null == isFetching + ? _value.isFetching + : isFetching // ignore: cast_nullable_to_non_nullable + as bool, + hasReachedMax: null == hasReachedMax + ? _value.hasReachedMax + : hasReachedMax // ignore: cast_nullable_to_non_nullable + as bool, + page: null == page + ? _value.page + : page // ignore: cast_nullable_to_non_nullable + as int, + ), + ); + } +} + +/// @nodoc + +class _$OutletListLoaderStateImpl implements _OutletListLoaderState { + const _$OutletListLoaderStateImpl({ + required final List outlets, + required this.failureOptionOutlet, + this.search, + this.isActive, + this.isFetching = false, + this.hasReachedMax = false, + this.page = 1, + }) : _outlets = outlets; + + final List _outlets; + @override + List get outlets { + if (_outlets is EqualUnmodifiableListView) return _outlets; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_outlets); + } + + @override + final Option failureOptionOutlet; + @override + final String? search; + @override + final bool? isActive; + @override + @JsonKey() + final bool isFetching; + @override + @JsonKey() + final bool hasReachedMax; + @override + @JsonKey() + final int page; + + @override + String toString() { + return 'OutletListLoaderState(outlets: $outlets, failureOptionOutlet: $failureOptionOutlet, search: $search, isActive: $isActive, isFetching: $isFetching, hasReachedMax: $hasReachedMax, page: $page)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$OutletListLoaderStateImpl && + const DeepCollectionEquality().equals(other._outlets, _outlets) && + (identical(other.failureOptionOutlet, failureOptionOutlet) || + other.failureOptionOutlet == failureOptionOutlet) && + (identical(other.search, search) || other.search == search) && + (identical(other.isActive, isActive) || + other.isActive == isActive) && + (identical(other.isFetching, isFetching) || + other.isFetching == isFetching) && + (identical(other.hasReachedMax, hasReachedMax) || + other.hasReachedMax == hasReachedMax) && + (identical(other.page, page) || other.page == page)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_outlets), + failureOptionOutlet, + search, + isActive, + isFetching, + hasReachedMax, + page, + ); + + /// Create a copy of OutletListLoaderState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$OutletListLoaderStateImplCopyWith<_$OutletListLoaderStateImpl> + get copyWith => + __$$OutletListLoaderStateImplCopyWithImpl<_$OutletListLoaderStateImpl>( + this, + _$identity, + ); +} + +abstract class _OutletListLoaderState implements OutletListLoaderState { + const factory _OutletListLoaderState({ + required final List outlets, + required final Option failureOptionOutlet, + final String? search, + final bool? isActive, + final bool isFetching, + final bool hasReachedMax, + final int page, + }) = _$OutletListLoaderStateImpl; + + @override + List get outlets; + @override + Option get failureOptionOutlet; + @override + String? get search; + @override + bool? get isActive; + @override + bool get isFetching; + @override + bool get hasReachedMax; + @override + int get page; + + /// Create a copy of OutletListLoaderState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$OutletListLoaderStateImplCopyWith<_$OutletListLoaderStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/application/outlet/outlet_list_loader/outlet_list_loader_event.dart b/lib/application/outlet/outlet_list_loader/outlet_list_loader_event.dart new file mode 100644 index 0000000..4576c20 --- /dev/null +++ b/lib/application/outlet/outlet_list_loader/outlet_list_loader_event.dart @@ -0,0 +1,12 @@ +part of 'outlet_list_loader_bloc.dart'; + +@freezed +class OutletListLoaderEvent with _$OutletListLoaderEvent { + const factory OutletListLoaderEvent.searchChanged(String search) = + _SearchChanged; + const factory OutletListLoaderEvent.isActiveChanged(bool? isActive) = + _IsActiveChanged; + const factory OutletListLoaderEvent.fetched({ + @Default(false) bool isRefresh, + }) = _Fetched; +} diff --git a/lib/application/outlet/outlet_list_loader/outlet_list_loader_state.dart b/lib/application/outlet/outlet_list_loader/outlet_list_loader_state.dart new file mode 100644 index 0000000..9a699e1 --- /dev/null +++ b/lib/application/outlet/outlet_list_loader/outlet_list_loader_state.dart @@ -0,0 +1,17 @@ +part of 'outlet_list_loader_bloc.dart'; + +@freezed +class OutletListLoaderState with _$OutletListLoaderState { + const factory OutletListLoaderState({ + required List outlets, + required Option failureOptionOutlet, + String? search, + bool? isActive, + @Default(false) bool isFetching, + @Default(false) bool hasReachedMax, + @Default(1) int page, + }) = _OutletListLoaderState; + + factory OutletListLoaderState.initial() => + OutletListLoaderState(outlets: [], failureOptionOutlet: none()); +} diff --git a/lib/domain/outlet/repositories/i_outlet_repository.dart b/lib/domain/outlet/repositories/i_outlet_repository.dart index 9514707..4f12e5c 100644 --- a/lib/domain/outlet/repositories/i_outlet_repository.dart +++ b/lib/domain/outlet/repositories/i_outlet_repository.dart @@ -2,4 +2,11 @@ part of '../outlet.dart'; abstract class IOutletRepository { Future> currentOutlet(); + + Future>> getList({ + int page = 1, + int limit = 10, + String? search, + bool? isActive, + }); } diff --git a/lib/infrastructure/outlet/datasource/remote_data_provider.dart b/lib/infrastructure/outlet/datasource/remote_data_provider.dart index 622c548..b7d406a 100644 --- a/lib/infrastructure/outlet/datasource/remote_data_provider.dart +++ b/lib/infrastructure/outlet/datasource/remote_data_provider.dart @@ -36,4 +36,39 @@ class OutletRemoteDataProvider { return DC.error(OutletFailure.serverError(e)); } } + + Future>> fetchList({ + int page = 1, + int limit = 10, + String? search, + bool? isActive, + }) async { + try { + final Map params = { + 'page': page, + 'limit': limit, + 'search': search ?? 'null', + 'is_active': isActive != null ? isActive.toString() : 'null', + }; + + final response = await _apiClient.get( + '${ApiPath.outlet}/list', + params: params, + headers: getAuthorizationHeader(), + ); + + if (response.data['data'] == null) { + return DC.error(OutletFailure.empty()); + } + + final dto = (response.data['data']['outlets'] as List) + .map((item) => OutletDto.fromJson(item)) + .toList(); + + return DC.data(dto); + } on ApiFailure catch (e, s) { + log('fetchOutletListError', name: _logName, error: e, stackTrace: s); + return DC.error(OutletFailure.serverError(e)); + } + } } diff --git a/lib/infrastructure/outlet/repositories/outlet_repository.dart b/lib/infrastructure/outlet/repositories/outlet_repository.dart index a3cc1a1..857ef71 100644 --- a/lib/infrastructure/outlet/repositories/outlet_repository.dart +++ b/lib/infrastructure/outlet/repositories/outlet_repository.dart @@ -33,4 +33,32 @@ class OutletRepository implements IOutletRepository { return left(const OutletFailure.unexpectedError()); } } + + @override + Future>> getList({ + int page = 1, + int limit = 10, + String? search, + bool? isActive, + }) async { + try { + final result = await _dataProvider.fetchList( + page: page, + limit: limit, + search: search, + isActive: isActive, + ); + + if (result.hasError) { + return left(result.error!); + } + + final outlets = result.data!.map((e) => e.toDomain()).toList(); + + return right(outlets); + } catch (e, s) { + log('getOutletListError', name: _logName, error: e, stackTrace: s); + return left(const OutletFailure.unexpectedError()); + } + } } diff --git a/lib/injection.config.dart b/lib/injection.config.dart index 642318a..6f2079d 100644 --- a/lib/injection.config.dart +++ b/lib/injection.config.dart @@ -39,6 +39,8 @@ import 'package:apskel_owner_flutter/application/order/order_loader/order_loader as _i1058; import 'package:apskel_owner_flutter/application/outlet/current_outlet_loader/current_outlet_loader_bloc.dart' as _i337; +import 'package:apskel_owner_flutter/application/outlet/outlet_list_loader/outlet_list_loader_bloc.dart' + as _i877; import 'package:apskel_owner_flutter/application/product/product_loader/product_loader_bloc.dart' as _i458; import 'package:apskel_owner_flutter/application/report/inventory_report/inventory_report_bloc.dart' @@ -238,6 +240,9 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i889.SalesLoaderBloc>( () => _i889.SalesLoaderBloc(gh<_i477.IAnalyticRepository>()), ); + gh.factory<_i877.OutletListLoaderBloc>( + () => _i877.OutletListLoaderBloc(gh<_i197.IOutletRepository>()), + ); gh.factory<_i337.CurrentOutletLoaderBloc>( () => _i337.CurrentOutletLoaderBloc(gh<_i197.IOutletRepository>()), ); diff --git a/lib/presentation/pages/home/home_page.dart b/lib/presentation/pages/home/home_page.dart index 6bd05f5..721e904 100644 --- a/lib/presentation/pages/home/home_page.dart +++ b/lib/presentation/pages/home/home_page.dart @@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:line_icons/line_icons.dart'; import '../../../application/home/home_bloc.dart'; +import '../../../application/outlet/outlet_list_loader/outlet_list_loader_bloc.dart'; import '../../../common/constant/app_constant.dart'; import '../../../common/theme/theme.dart'; import '../../../injection.dart'; @@ -22,8 +23,17 @@ class HomePage extends StatefulWidget implements AutoRouteWrapper { State createState() => _HomePageState(); @override - Widget wrappedRoute(BuildContext context) => BlocProvider( - create: (context) => getIt()..add(HomeEvent.fetchedDashboard()), + Widget wrappedRoute(BuildContext context) => MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => + getIt()..add(HomeEvent.fetchedDashboard()), + ), + BlocProvider( + create: (context) => getIt() + ..add(const OutletListLoaderEvent.fetched()), + ), + ], child: this, ); } diff --git a/lib/presentation/pages/home/widgets/promo_banner.dart b/lib/presentation/pages/home/widgets/promo_banner.dart index f00fdbf..a3a370a 100644 --- a/lib/presentation/pages/home/widgets/promo_banner.dart +++ b/lib/presentation/pages/home/widgets/promo_banner.dart @@ -1,16 +1,129 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../../application/outlet/outlet_list_loader/outlet_list_loader_bloc.dart'; import '../../../../common/theme/theme.dart'; +import '../../../../domain/outlet/outlet.dart'; import '../../../components/spacer/spacer.dart'; import '../../../components/widgets/particle_card.dart'; class HomePromoBanner extends StatelessWidget { const HomePromoBanner({super.key}); - static const _outlets = [ - {'name': 'ENAKLO RAWAMANGUNG', 'isHealthy': true}, - {'name': 'ENAKLO WOKU PEDAS\nKELAPA GADING', 'isHealthy': true}, - ]; + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state.isFetching && state.outlets.isEmpty) { + return const _PromoBannerSkeleton(); + } + + if (state.outlets.isEmpty) { + return const SizedBox.shrink(); + } + + return Padding( + padding: const EdgeInsets.fromLTRB( + AppValue.padding, + 24, + AppValue.padding, + 0, + ), + child: Row( + children: [ + for (int i = 0; i < state.outlets.length; i++) ...[ + Expanded( + child: _OutletCard(outlet: state.outlets[i]), + ), + if (i < state.outlets.length - 1) const SpaceWidth(12), + ], + ], + ), + ); + }, + ); + } +} + +class _OutletCard extends StatelessWidget { + final Outlet outlet; + + const _OutletCard({required this.outlet}); + + @override + Widget build(BuildContext context) { + return ParticleCard( + height: 110, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + decorationOpacity: 0.8, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + const Spacer(), + _buildHealthIndicator(outlet.isActive), + ], + ), + const SpaceHeight(6), + Text( + outlet.name, + style: AppStyle.sm.copyWith( + color: AppColor.white, + fontWeight: FontWeight.w800, + height: 1.25, + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ); + } + + Widget _buildHealthIndicator(bool isActive) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: isActive + ? AppColor.success.withOpacity(0.9) + : AppColor.error.withOpacity(0.9), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: (isActive ? AppColor.success : AppColor.error) + .withOpacity(0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isActive ? Icons.check_circle : Icons.warning_rounded, + color: AppColor.white, + size: 12, + ), + const SpaceWidth(4), + Text( + isActive ? 'Sehat' : 'Tidak Sehat', + style: AppStyle.xs.copyWith( + color: AppColor.white, + fontWeight: FontWeight.w700, + fontSize: 9, + ), + ), + ], + ), + ); + } +} + +class _PromoBannerSkeleton extends StatelessWidget { + const _PromoBannerSkeleton(); @override Widget build(BuildContext context) { @@ -23,82 +136,20 @@ class HomePromoBanner extends StatelessWidget { ), child: Row( children: [ - for (int i = 0; i < _outlets.length; i++) ...[ - Expanded( - child: ParticleCard( - height: 110, - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 12, - ), - decorationOpacity: 0.8, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Spacer(), - _buildHealthIndicator(_outlets[i]['isHealthy'] as bool), - ], - ), - const SpaceHeight(6), - Text( - _outlets[i]['name'] as String, - style: AppStyle.sm.copyWith( - color: AppColor.white, - fontWeight: FontWeight.w800, - height: 1.25, - ), - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ), - if (i < _outlets.length - 1) const SpaceWidth(12), - ], + Expanded(child: _skeletonCard()), + const SpaceWidth(12), + Expanded(child: _skeletonCard()), ], ), ); } - Widget _buildHealthIndicator(bool isHealthy) { + Widget _skeletonCard() { return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + height: 110, decoration: BoxDecoration( - color: isHealthy - ? AppColor.success.withOpacity(0.9) - : AppColor.error.withOpacity(0.9), + color: AppColor.border, borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: (isHealthy ? AppColor.success : AppColor.error) - .withOpacity(0.3), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - isHealthy ? Icons.check_circle : Icons.warning_rounded, - color: AppColor.white, - size: 12, - ), - const SpaceWidth(4), - Text( - isHealthy ? 'Sehat' : 'Tidak Sehat', - style: AppStyle.xs.copyWith( - color: AppColor.white, - fontWeight: FontWeight.w700, - fontSize: 9, - ), - ), - ], ), ); }