dev #1

Merged
aefril merged 108 commits from dev into main 2026-05-15 05:52:26 +00:00
10 changed files with 1159 additions and 74 deletions
Showing only changes of commit 3541fc725c - Show all commits

View File

@ -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<OutletListLoaderEvent, OutletListLoaderState> {
final IOutletRepository _outletRepository;
OutletListLoaderBloc(this._outletRepository)
: super(OutletListLoaderState.initial()) {
on<OutletListLoaderEvent>(_onOutletListLoaderEvent);
}
Future<void> _onOutletListLoaderEvent(
OutletListLoaderEvent event,
Emitter<OutletListLoaderState> 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<OutletListLoaderState> _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;
}
}

View File

@ -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>(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<TResult extends Object?>({
required TResult Function(String search) searchChanged,
required TResult Function(bool? isActive) isActiveChanged,
required TResult Function(bool isRefresh) fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(String search)? searchChanged,
TResult? Function(bool? isActive)? isActiveChanged,
TResult? Function(bool isRefresh)? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(String search)? searchChanged,
TResult Function(bool? isActive)? isActiveChanged,
TResult Function(bool isRefresh)? fetched,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_SearchChanged value) searchChanged,
required TResult Function(_IsActiveChanged value) isActiveChanged,
required TResult Function(_Fetched value) fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_SearchChanged value)? searchChanged,
TResult? Function(_IsActiveChanged value)? isActiveChanged,
TResult? Function(_Fetched value)? fetched,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
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<TResult extends Object?>({
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 extends Object?>({
TResult? Function(String search)? searchChanged,
TResult? Function(bool? isActive)? isActiveChanged,
TResult? Function(bool isRefresh)? fetched,
}) {
return searchChanged?.call(search);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
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<TResult extends Object?>({
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 extends Object?>({
TResult? Function(_SearchChanged value)? searchChanged,
TResult? Function(_IsActiveChanged value)? isActiveChanged,
TResult? Function(_Fetched value)? fetched,
}) {
return searchChanged?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
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<TResult extends Object?>({
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 extends Object?>({
TResult? Function(String search)? searchChanged,
TResult? Function(bool? isActive)? isActiveChanged,
TResult? Function(bool isRefresh)? fetched,
}) {
return isActiveChanged?.call(isActive);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
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<TResult extends Object?>({
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 extends Object?>({
TResult? Function(_SearchChanged value)? searchChanged,
TResult? Function(_IsActiveChanged value)? isActiveChanged,
TResult? Function(_Fetched value)? fetched,
}) {
return isActiveChanged?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
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<TResult extends Object?>({
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 extends Object?>({
TResult? Function(String search)? searchChanged,
TResult? Function(bool? isActive)? isActiveChanged,
TResult? Function(bool isRefresh)? fetched,
}) {
return fetched?.call(isRefresh);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
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<TResult extends Object?>({
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 extends Object?>({
TResult? Function(_SearchChanged value)? searchChanged,
TResult? Function(_IsActiveChanged value)? isActiveChanged,
TResult? Function(_Fetched value)? fetched,
}) {
return fetched?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
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<Outlet> get outlets => throw _privateConstructorUsedError;
Option<OutletFailure> 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<OutletListLoaderState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $OutletListLoaderStateCopyWith<$Res> {
factory $OutletListLoaderStateCopyWith(
OutletListLoaderState value,
$Res Function(OutletListLoaderState) then,
) = _$OutletListLoaderStateCopyWithImpl<$Res, OutletListLoaderState>;
@useResult
$Res call({
List<Outlet> outlets,
Option<OutletFailure> 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<Outlet>,
failureOptionOutlet: null == failureOptionOutlet
? _value.failureOptionOutlet
: failureOptionOutlet // ignore: cast_nullable_to_non_nullable
as Option<OutletFailure>,
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<Outlet> outlets,
Option<OutletFailure> 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<Outlet>,
failureOptionOutlet: null == failureOptionOutlet
? _value.failureOptionOutlet
: failureOptionOutlet // ignore: cast_nullable_to_non_nullable
as Option<OutletFailure>,
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<Outlet> outlets,
required this.failureOptionOutlet,
this.search,
this.isActive,
this.isFetching = false,
this.hasReachedMax = false,
this.page = 1,
}) : _outlets = outlets;
final List<Outlet> _outlets;
@override
List<Outlet> get outlets {
if (_outlets is EqualUnmodifiableListView) return _outlets;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_outlets);
}
@override
final Option<OutletFailure> 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<Outlet> outlets,
required final Option<OutletFailure> failureOptionOutlet,
final String? search,
final bool? isActive,
final bool isFetching,
final bool hasReachedMax,
final int page,
}) = _$OutletListLoaderStateImpl;
@override
List<Outlet> get outlets;
@override
Option<OutletFailure> 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;
}

View File

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

View File

@ -0,0 +1,17 @@
part of 'outlet_list_loader_bloc.dart';
@freezed
class OutletListLoaderState with _$OutletListLoaderState {
const factory OutletListLoaderState({
required List<Outlet> outlets,
required Option<OutletFailure> 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());
}

View File

@ -2,4 +2,11 @@ part of '../outlet.dart';
abstract class IOutletRepository { abstract class IOutletRepository {
Future<Either<OutletFailure, Outlet>> currentOutlet(); Future<Either<OutletFailure, Outlet>> currentOutlet();
Future<Either<OutletFailure, List<Outlet>>> getList({
int page = 1,
int limit = 10,
String? search,
bool? isActive,
});
} }

View File

@ -36,4 +36,39 @@ class OutletRemoteDataProvider {
return DC.error(OutletFailure.serverError(e)); return DC.error(OutletFailure.serverError(e));
} }
} }
Future<DC<OutletFailure, List<OutletDto>>> fetchList({
int page = 1,
int limit = 10,
String? search,
bool? isActive,
}) async {
try {
final Map<String, dynamic> 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));
}
}
} }

View File

@ -33,4 +33,32 @@ class OutletRepository implements IOutletRepository {
return left(const OutletFailure.unexpectedError()); return left(const OutletFailure.unexpectedError());
} }
} }
@override
Future<Either<OutletFailure, List<Outlet>>> 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());
}
}
} }

View File

@ -39,6 +39,8 @@ import 'package:apskel_owner_flutter/application/order/order_loader/order_loader
as _i1058; as _i1058;
import 'package:apskel_owner_flutter/application/outlet/current_outlet_loader/current_outlet_loader_bloc.dart' import 'package:apskel_owner_flutter/application/outlet/current_outlet_loader/current_outlet_loader_bloc.dart'
as _i337; 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' import 'package:apskel_owner_flutter/application/product/product_loader/product_loader_bloc.dart'
as _i458; as _i458;
import 'package:apskel_owner_flutter/application/report/inventory_report/inventory_report_bloc.dart' 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>( gh.factory<_i889.SalesLoaderBloc>(
() => _i889.SalesLoaderBloc(gh<_i477.IAnalyticRepository>()), () => _i889.SalesLoaderBloc(gh<_i477.IAnalyticRepository>()),
); );
gh.factory<_i877.OutletListLoaderBloc>(
() => _i877.OutletListLoaderBloc(gh<_i197.IOutletRepository>()),
);
gh.factory<_i337.CurrentOutletLoaderBloc>( gh.factory<_i337.CurrentOutletLoaderBloc>(
() => _i337.CurrentOutletLoaderBloc(gh<_i197.IOutletRepository>()), () => _i337.CurrentOutletLoaderBloc(gh<_i197.IOutletRepository>()),
); );

View File

@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:line_icons/line_icons.dart'; import 'package:line_icons/line_icons.dart';
import '../../../application/home/home_bloc.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/constant/app_constant.dart';
import '../../../common/theme/theme.dart'; import '../../../common/theme/theme.dart';
import '../../../injection.dart'; import '../../../injection.dart';
@ -22,8 +23,17 @@ class HomePage extends StatefulWidget implements AutoRouteWrapper {
State<HomePage> createState() => _HomePageState(); State<HomePage> createState() => _HomePageState();
@override @override
Widget wrappedRoute(BuildContext context) => BlocProvider( Widget wrappedRoute(BuildContext context) => MultiBlocProvider(
create: (context) => getIt<HomeBloc>()..add(HomeEvent.fetchedDashboard()), providers: [
BlocProvider(
create: (context) =>
getIt<HomeBloc>()..add(HomeEvent.fetchedDashboard()),
),
BlocProvider(
create: (context) => getIt<OutletListLoaderBloc>()
..add(const OutletListLoaderEvent.fetched()),
),
],
child: this, child: this,
); );
} }

View File

@ -1,16 +1,129 @@
import 'package:flutter/material.dart'; 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 '../../../../common/theme/theme.dart';
import '../../../../domain/outlet/outlet.dart';
import '../../../components/spacer/spacer.dart'; import '../../../components/spacer/spacer.dart';
import '../../../components/widgets/particle_card.dart'; import '../../../components/widgets/particle_card.dart';
class HomePromoBanner extends StatelessWidget { class HomePromoBanner extends StatelessWidget {
const HomePromoBanner({super.key}); const HomePromoBanner({super.key});
static const _outlets = [ @override
{'name': 'ENAKLO RAWAMANGUNG', 'isHealthy': true}, Widget build(BuildContext context) {
{'name': 'ENAKLO WOKU PEDAS\nKELAPA GADING', 'isHealthy': true}, return BlocBuilder<OutletListLoaderBloc, OutletListLoaderState>(
]; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -23,82 +136,20 @@ class HomePromoBanner extends StatelessWidget {
), ),
child: Row( child: Row(
children: [ children: [
for (int i = 0; i < _outlets.length; i++) ...[ Expanded(child: _skeletonCard()),
Expanded( const SpaceWidth(12),
child: ParticleCard( Expanded(child: _skeletonCard()),
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),
],
], ],
), ),
); );
} }
Widget _buildHealthIndicator(bool isHealthy) { Widget _skeletonCard() {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), height: 110,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isHealthy color: AppColor.border,
? AppColor.success.withOpacity(0.9)
: AppColor.error.withOpacity(0.9),
borderRadius: BorderRadius.circular(12), 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,
),
),
],
), ),
); );
} }