diff --git a/lib/application/outlet/outlet_loader/outlet_loader_bloc.dart b/lib/application/outlet/outlet_loader/outlet_loader_bloc.dart new file mode 100644 index 0000000..51178f1 --- /dev/null +++ b/lib/application/outlet/outlet_loader/outlet_loader_bloc.dart @@ -0,0 +1,84 @@ +import 'package:bloc/bloc.dart'; +import 'package:dartz/dartz.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:injectable/injectable.dart'; + +import '../../../domain/outlet/outlet.dart'; + +part 'outlet_loader_event.dart'; +part 'outlet_loader_state.dart'; +part 'outlet_loader_bloc.freezed.dart'; + +@injectable +class OutletLoaderBloc extends Bloc { + final IOutletRepository _outletRepository; + + OutletLoaderBloc(this._outletRepository) + : super(OutletLoaderState.initial()) { + on(_onOutletLoaderEvent); + } + + Future _onOutletLoaderEvent( + OutletLoaderEvent event, + Emitter emit, + ) { + return event.map( + 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( + OutletLoaderState 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 failureOrOutlet = await _outletRepository.getOutlets( + page: state.page, + ); + + state = failureOrOutlet.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_loader/outlet_loader_bloc.freezed.dart b/lib/application/outlet/outlet_loader/outlet_loader_bloc.freezed.dart new file mode 100644 index 0000000..abbe8b7 --- /dev/null +++ b/lib/application/outlet/outlet_loader/outlet_loader_bloc.freezed.dart @@ -0,0 +1,479 @@ +// 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_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 _$OutletLoaderEvent { + bool get isRefresh => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function(bool isRefresh) fetched, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(bool isRefresh)? fetched, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(bool isRefresh)? fetched, + required TResult orElse(), + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Fetched value) fetched, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Fetched value)? fetched, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Fetched value)? fetched, + required TResult orElse(), + }) => throw _privateConstructorUsedError; + + /// Create a copy of OutletLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $OutletLoaderEventCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $OutletLoaderEventCopyWith<$Res> { + factory $OutletLoaderEventCopyWith( + OutletLoaderEvent value, + $Res Function(OutletLoaderEvent) then, + ) = _$OutletLoaderEventCopyWithImpl<$Res, OutletLoaderEvent>; + @useResult + $Res call({bool isRefresh}); +} + +/// @nodoc +class _$OutletLoaderEventCopyWithImpl<$Res, $Val extends OutletLoaderEvent> + implements $OutletLoaderEventCopyWith<$Res> { + _$OutletLoaderEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of OutletLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? isRefresh = null}) { + return _then( + _value.copyWith( + isRefresh: null == isRefresh + ? _value.isRefresh + : isRefresh // ignore: cast_nullable_to_non_nullable + as bool, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$FetchedImplCopyWith<$Res> + implements $OutletLoaderEventCopyWith<$Res> { + factory _$$FetchedImplCopyWith( + _$FetchedImpl value, + $Res Function(_$FetchedImpl) then, + ) = __$$FetchedImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({bool isRefresh}); +} + +/// @nodoc +class __$$FetchedImplCopyWithImpl<$Res> + extends _$OutletLoaderEventCopyWithImpl<$Res, _$FetchedImpl> + implements _$$FetchedImplCopyWith<$Res> { + __$$FetchedImplCopyWithImpl( + _$FetchedImpl _value, + $Res Function(_$FetchedImpl) _then, + ) : super(_value, _then); + + /// Create a copy of OutletLoaderEvent + /// 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 'OutletLoaderEvent.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 OutletLoaderEvent + /// 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(bool isRefresh) fetched, + }) { + return fetched(isRefresh); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(bool isRefresh)? fetched, + }) { + return fetched?.call(isRefresh); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(bool isRefresh)? fetched, + required TResult orElse(), + }) { + if (fetched != null) { + return fetched(isRefresh); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Fetched value) fetched, + }) { + return fetched(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Fetched value)? fetched, + }) { + return fetched?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Fetched value)? fetched, + required TResult orElse(), + }) { + if (fetched != null) { + return fetched(this); + } + return orElse(); + } +} + +abstract class _Fetched implements OutletLoaderEvent { + const factory _Fetched({final bool isRefresh}) = _$FetchedImpl; + + @override + bool get isRefresh; + + /// Create a copy of OutletLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$FetchedImplCopyWith<_$FetchedImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$OutletLoaderState { + List get outlets => throw _privateConstructorUsedError; + Option get failureOptionOutlet => + throw _privateConstructorUsedError; + bool get isFetching => throw _privateConstructorUsedError; + bool get hasReachedMax => throw _privateConstructorUsedError; + int get page => throw _privateConstructorUsedError; + + /// Create a copy of OutletLoaderState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $OutletLoaderStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $OutletLoaderStateCopyWith<$Res> { + factory $OutletLoaderStateCopyWith( + OutletLoaderState value, + $Res Function(OutletLoaderState) then, + ) = _$OutletLoaderStateCopyWithImpl<$Res, OutletLoaderState>; + @useResult + $Res call({ + List outlets, + Option failureOptionOutlet, + bool isFetching, + bool hasReachedMax, + int page, + }); +} + +/// @nodoc +class _$OutletLoaderStateCopyWithImpl<$Res, $Val extends OutletLoaderState> + implements $OutletLoaderStateCopyWith<$Res> { + _$OutletLoaderStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of OutletLoaderState + /// 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? 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, + 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 _$$OutletLoaderStateImplCopyWith<$Res> + implements $OutletLoaderStateCopyWith<$Res> { + factory _$$OutletLoaderStateImplCopyWith( + _$OutletLoaderStateImpl value, + $Res Function(_$OutletLoaderStateImpl) then, + ) = __$$OutletLoaderStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + List outlets, + Option failureOptionOutlet, + bool isFetching, + bool hasReachedMax, + int page, + }); +} + +/// @nodoc +class __$$OutletLoaderStateImplCopyWithImpl<$Res> + extends _$OutletLoaderStateCopyWithImpl<$Res, _$OutletLoaderStateImpl> + implements _$$OutletLoaderStateImplCopyWith<$Res> { + __$$OutletLoaderStateImplCopyWithImpl( + _$OutletLoaderStateImpl _value, + $Res Function(_$OutletLoaderStateImpl) _then, + ) : super(_value, _then); + + /// Create a copy of OutletLoaderState + /// 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? isFetching = null, + Object? hasReachedMax = null, + Object? page = null, + }) { + return _then( + _$OutletLoaderStateImpl( + 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, + 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 _$OutletLoaderStateImpl implements _OutletLoaderState { + const _$OutletLoaderStateImpl({ + required final List outlets, + required this.failureOptionOutlet, + 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 + @JsonKey() + final bool isFetching; + @override + @JsonKey() + final bool hasReachedMax; + @override + @JsonKey() + final int page; + + @override + String toString() { + return 'OutletLoaderState(outlets: $outlets, failureOptionOutlet: $failureOptionOutlet, isFetching: $isFetching, hasReachedMax: $hasReachedMax, page: $page)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$OutletLoaderStateImpl && + const DeepCollectionEquality().equals(other._outlets, _outlets) && + (identical(other.failureOptionOutlet, failureOptionOutlet) || + other.failureOptionOutlet == failureOptionOutlet) && + (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, + isFetching, + hasReachedMax, + page, + ); + + /// Create a copy of OutletLoaderState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$OutletLoaderStateImplCopyWith<_$OutletLoaderStateImpl> get copyWith => + __$$OutletLoaderStateImplCopyWithImpl<_$OutletLoaderStateImpl>( + this, + _$identity, + ); +} + +abstract class _OutletLoaderState implements OutletLoaderState { + const factory _OutletLoaderState({ + required final List outlets, + required final Option failureOptionOutlet, + final bool isFetching, + final bool hasReachedMax, + final int page, + }) = _$OutletLoaderStateImpl; + + @override + List get outlets; + @override + Option get failureOptionOutlet; + @override + bool get isFetching; + @override + bool get hasReachedMax; + @override + int get page; + + /// Create a copy of OutletLoaderState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$OutletLoaderStateImplCopyWith<_$OutletLoaderStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/application/outlet/outlet_loader/outlet_loader_event.dart b/lib/application/outlet/outlet_loader/outlet_loader_event.dart new file mode 100644 index 0000000..ff04ccc --- /dev/null +++ b/lib/application/outlet/outlet_loader/outlet_loader_event.dart @@ -0,0 +1,7 @@ +part of 'outlet_loader_bloc.dart'; + +@freezed +class OutletLoaderEvent with _$OutletLoaderEvent { + const factory OutletLoaderEvent.fetched({@Default(false) bool isRefresh}) = + _Fetched; +} diff --git a/lib/application/outlet/outlet_loader/outlet_loader_state.dart b/lib/application/outlet/outlet_loader/outlet_loader_state.dart new file mode 100644 index 0000000..c7a4feb --- /dev/null +++ b/lib/application/outlet/outlet_loader/outlet_loader_state.dart @@ -0,0 +1,15 @@ +part of 'outlet_loader_bloc.dart'; + +@freezed +class OutletLoaderState with _$OutletLoaderState { + const factory OutletLoaderState({ + required List outlets, + required Option failureOptionOutlet, + @Default(false) bool isFetching, + @Default(false) bool hasReachedMax, + @Default(1) int page, + }) = _OutletLoaderState; + + factory OutletLoaderState.initial() => + OutletLoaderState(outlets: [], failureOptionOutlet: none()); +} diff --git a/lib/infrastructure/outlet/datasources/remote_data_provider.dart b/lib/infrastructure/outlet/datasources/remote_data_provider.dart index a61e830..7f95055 100644 --- a/lib/infrastructure/outlet/datasources/remote_data_provider.dart +++ b/lib/infrastructure/outlet/datasources/remote_data_provider.dart @@ -32,7 +32,7 @@ class OutletRemoteDataProvider { return DC.error(OutletFailure.empty()); } - final outlets = (response.data['data'] as List) + final outlets = (response.data['data']['outlets'] as List) .map((e) => OutletDto.fromJson(e as Map)) .toList(); diff --git a/lib/injection.config.dart b/lib/injection.config.dart index 4c636e8..044ec1c 100644 --- a/lib/injection.config.dart +++ b/lib/injection.config.dart @@ -12,6 +12,8 @@ import 'package:apskel_pos_flutter_v2/application/auth/auth_bloc.dart' as _i343; import 'package:apskel_pos_flutter_v2/application/auth/login_form/login_form_bloc.dart' as _i46; +import 'package:apskel_pos_flutter_v2/application/outlet/outlet_loader/outlet_loader_bloc.dart' + as _i76; import 'package:apskel_pos_flutter_v2/common/api/api_client.dart' as _i457; import 'package:apskel_pos_flutter_v2/common/di/di_auto_route.dart' as _i729; import 'package:apskel_pos_flutter_v2/common/di/di_connectivity.dart' as _i807; @@ -105,6 +107,9 @@ extension GetItInjectableX on _i174.GetIt { gh<_i552.IOutletRepository>(), ), ); + gh.factory<_i76.OutletLoaderBloc>( + () => _i76.OutletLoaderBloc(gh<_i552.IOutletRepository>()), + ); return this; } } diff --git a/lib/presentation/app_widget.dart b/lib/presentation/app_widget.dart index 5e26317..5a21363 100644 --- a/lib/presentation/app_widget.dart +++ b/lib/presentation/app_widget.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../application/auth/auth_bloc.dart'; +import '../application/outlet/outlet_loader/outlet_loader_bloc.dart'; import '../common/theme/theme.dart'; import '../common/constant/app_constant.dart'; import '../injection.dart'; @@ -20,8 +21,11 @@ class _AppWidgetState extends State { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => getIt(), + return MultiBlocProvider( + providers: [ + BlocProvider(create: (context) => getIt()), + BlocProvider(create: (context) => getIt()), + ], child: MaterialApp.router( debugShowCheckedModeBanner: false, title: AppConstant.appName, diff --git a/lib/presentation/components/card/outlet_card.dart b/lib/presentation/components/card/outlet_card.dart new file mode 100644 index 0000000..f5ef0b1 --- /dev/null +++ b/lib/presentation/components/card/outlet_card.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +import '../../../common/theme/theme.dart'; +import '../../../domain/outlet/outlet.dart'; +import '../spaces/space.dart'; + +class OutletCard extends StatelessWidget { + final Outlet outlet; + final bool isSelected; + const OutletCard({super.key, required this.outlet, required this.isSelected}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + margin: EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: isSelected ? AppColor.primary.withOpacity(0.1) : AppColor.white, + border: Border.all(color: AppColor.primary), + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + ), + child: Row( + children: [ + Icon(Icons.store, color: AppColor.primary), + SpaceWidth(12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + outlet.name, + style: AppStyle.lg.copyWith(fontWeight: FontWeight.w600), + ), + Text( + outlet.address, + style: AppStyle.sm, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/components/dialog/custom_modal_dialog.dart b/lib/presentation/components/dialog/custom_modal_dialog.dart new file mode 100644 index 0000000..d3af021 --- /dev/null +++ b/lib/presentation/components/dialog/custom_modal_dialog.dart @@ -0,0 +1,105 @@ +part of 'dialog.dart'; + +class CustomModalDialog extends StatelessWidget { + final String title; + final String? subtitle; + final Widget child; + final VoidCallback? onClose; + final double? minWidth; + final double? maxWidth; + final double? minHeight; + final double? maxHeight; + final EdgeInsets? contentPadding; + + const CustomModalDialog({ + super.key, + required this.title, + this.subtitle, + required this.child, + this.onClose, + this.minWidth, + this.maxWidth, + this.minHeight, + this.maxHeight, + this.contentPadding, + }); + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: AppColor.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: minWidth ?? context.deviceWidth * 0.3, + maxWidth: maxWidth ?? context.deviceWidth * 0.8, + minHeight: minHeight ?? context.deviceHeight * 0.3, + maxHeight: maxHeight ?? context.deviceHeight * 0.8, + ), + child: IntrinsicWidth( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(16), + width: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: AppColor.primaryGradient, + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: AppStyle.xxl.copyWith( + color: AppColor.white, + fontWeight: FontWeight.bold, + ), + ), + if (subtitle != null) + Text( + subtitle ?? '', + style: AppStyle.lg.copyWith( + color: AppColor.textSecondary, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + SpaceWidth(12), + IconButton( + icon: Icon(Icons.close, color: AppColor.white), + onPressed: () { + if (onClose != null) { + onClose!(); + } else { + context.maybePop(); + } + }, + ), + ], + ), + ), + Flexible( + child: SingleChildScrollView( + padding: contentPadding ?? EdgeInsets.zero, + child: child, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/presentation/components/dialog/dialog.dart b/lib/presentation/components/dialog/dialog.dart new file mode 100644 index 0000000..7b2af66 --- /dev/null +++ b/lib/presentation/components/dialog/dialog.dart @@ -0,0 +1,14 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../application/outlet/outlet_loader/outlet_loader_bloc.dart'; +import '../../../common/extension/extension.dart'; +import '../../../common/theme/theme.dart'; +import '../button/button.dart'; +import '../card/outlet_card.dart'; +import '../loader/loader_with_text.dart'; +import '../spaces/space.dart'; + +part 'custom_modal_dialog.dart'; +part 'outlet_dialog.dart'; diff --git a/lib/presentation/components/dialog/outlet_dialog.dart b/lib/presentation/components/dialog/outlet_dialog.dart new file mode 100644 index 0000000..5012212 --- /dev/null +++ b/lib/presentation/components/dialog/outlet_dialog.dart @@ -0,0 +1,57 @@ +part of 'dialog.dart'; + +class OutletDialog extends StatefulWidget { + const OutletDialog({super.key}); + + @override + State createState() => _OutletDialogState(); +} + +class _OutletDialogState extends State { + @override + void initState() { + super.initState(); + context.read().add( + OutletLoaderEvent.fetched(isRefresh: true), + ); + } + + @override + Widget build(BuildContext context) { + return CustomModalDialog( + title: 'Outlet', + subtitle: 'Silahkan pilih outlet', + minWidth: context.deviceWidth * 0.4, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 24.0, + ), + child: BlocBuilder( + builder: (context, state) { + if (state.isFetching) { + return LoaderWithText(); + } + + return Column( + children: [ + ...List.generate( + state.outlets.length, + (index) => GestureDetector( + onTap: () { + // selectOutlet(outlets[index]); + }, + child: OutletCard( + outlet: state.outlets[index], + isSelected: false, + ), + ), + ), + SpaceHeight(24), + AppElevatedButton.filled(onPressed: null, label: 'Terapkan'), + ], + ); + }, + ), + ); + } +} diff --git a/lib/presentation/components/loader/loader_with_text.dart b/lib/presentation/components/loader/loader_with_text.dart index 5458b3f..ab5a156 100644 --- a/lib/presentation/components/loader/loader_with_text.dart +++ b/lib/presentation/components/loader/loader_with_text.dart @@ -12,7 +12,7 @@ class LoaderWithText extends StatelessWidget { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - SpinKitFadingCircle(color: AppColor.primary), + SpinKitFadingCircle(color: AppColor.primary, size: 24), SpaceWidth(10), Text( 'Loading...', diff --git a/lib/presentation/pages/main/pages/home/widgets/home_title.dart b/lib/presentation/pages/main/pages/home/widgets/home_title.dart index f3cc68d..f76c0ff 100644 --- a/lib/presentation/pages/main/pages/home/widgets/home_title.dart +++ b/lib/presentation/pages/main/pages/home/widgets/home_title.dart @@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../../../application/auth/auth_bloc.dart'; import '../../../../../../common/extension/extension.dart'; import '../../../../../../common/theme/theme.dart'; +import '../../../../../components/dialog/dialog.dart'; import '../../../../../components/field/field.dart'; import '../../../../../components/spaces/space.dart'; @@ -23,25 +24,31 @@ class HomeTitle extends StatelessWidget { children: [ BlocBuilder( builder: (context, state) { - return Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - state.outlet.name, - style: AppStyle.xl.copyWith( - color: AppColor.primary, - fontWeight: FontWeight.w600, + return GestureDetector( + onTap: () => showDialog( + context: context, + builder: (context) => OutletDialog(), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + state.outlet.name, + style: AppStyle.xl.copyWith( + color: AppColor.primary, + fontWeight: FontWeight.w600, + ), ), - ), - SpaceWidth(2), - Icon( - Icons.keyboard_arrow_down, - color: AppColor.primary, - size: 18, - ), - ], + SpaceWidth(2), + Icon( + Icons.keyboard_arrow_down, + color: AppColor.primary, + size: 18, + ), + ], + ), ); }, ),