From ac8c2c0f54b577f58b87040230404a8e2cb2883c Mon Sep 17 00:00:00 2001 From: efrilm Date: Mon, 18 Aug 2025 16:48:35 +0700 Subject: [PATCH] feat: profit loss range date --- .../profit_loss_loader_bloc.dart | 7 +- .../profit_loss_loader_bloc.freezed.dart | 235 +++++++++++++++++- .../profit_loss_loader_event.dart | 4 + .../profit_loss_loader_state.dart | 4 + .../pages/finance/finance_page.dart | 227 +++++++---------- 5 files changed, 338 insertions(+), 139 deletions(-) diff --git a/lib/application/analytic/profit_loss_loader/profit_loss_loader_bloc.dart b/lib/application/analytic/profit_loss_loader/profit_loss_loader_bloc.dart index 85758ce..a16ffd7 100644 --- a/lib/application/analytic/profit_loss_loader/profit_loss_loader_bloc.dart +++ b/lib/application/analytic/profit_loss_loader/profit_loss_loader_bloc.dart @@ -24,12 +24,15 @@ class ProfitLossLoaderBloc Emitter emit, ) { return event.map( + rangeDateChanged: (e) async { + emit(state.copyWith(dateFrom: e.dateFrom, dateTo: e.dateTo)); + }, fetched: (e) async { emit(state.copyWith(isFetching: true, failureOptionProfitLoss: none())); final result = await _repository.getProfitLoss( - dateFrom: DateTime.now().subtract(const Duration(days: 30)), - dateTo: DateTime.now(), + dateFrom: state.dateFrom, + dateTo: state.dateTo, ); var data = result.fold( diff --git a/lib/application/analytic/profit_loss_loader/profit_loss_loader_bloc.freezed.dart b/lib/application/analytic/profit_loss_loader/profit_loss_loader_bloc.freezed.dart index 2fb5db5..601e958 100644 --- a/lib/application/analytic/profit_loss_loader/profit_loss_loader_bloc.freezed.dart +++ b/lib/application/analytic/profit_loss_loader/profit_loss_loader_bloc.freezed.dart @@ -19,27 +19,34 @@ final _privateConstructorUsedError = UnsupportedError( mixin _$ProfitLossLoaderEvent { @optionalTypeArgs TResult when({ + required TResult Function(DateTime dateFrom, DateTime dateTo) + rangeDateChanged, required TResult Function() fetched, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull({ + TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged, TResult? Function()? fetched, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeWhen({ + TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged, TResult Function()? fetched, required TResult orElse(), }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult map({ + required TResult Function(_RangeDateChanged value) rangeDateChanged, required TResult Function(_Fetched value) fetched, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? mapOrNull({ + TResult? Function(_RangeDateChanged value)? rangeDateChanged, TResult? Function(_Fetched value)? fetched, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeMap({ + TResult Function(_RangeDateChanged value)? rangeDateChanged, TResult Function(_Fetched value)? fetched, required TResult orElse(), }) => throw _privateConstructorUsedError; @@ -70,6 +77,164 @@ class _$ProfitLossLoaderEventCopyWithImpl< /// with the given fields replaced by the non-null parameter values. } +/// @nodoc +abstract class _$$RangeDateChangedImplCopyWith<$Res> { + factory _$$RangeDateChangedImplCopyWith( + _$RangeDateChangedImpl value, + $Res Function(_$RangeDateChangedImpl) then, + ) = __$$RangeDateChangedImplCopyWithImpl<$Res>; + @useResult + $Res call({DateTime dateFrom, DateTime dateTo}); +} + +/// @nodoc +class __$$RangeDateChangedImplCopyWithImpl<$Res> + extends _$ProfitLossLoaderEventCopyWithImpl<$Res, _$RangeDateChangedImpl> + implements _$$RangeDateChangedImplCopyWith<$Res> { + __$$RangeDateChangedImplCopyWithImpl( + _$RangeDateChangedImpl _value, + $Res Function(_$RangeDateChangedImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ProfitLossLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? dateFrom = null, Object? dateTo = null}) { + return _then( + _$RangeDateChangedImpl( + null == dateFrom + ? _value.dateFrom + : dateFrom // ignore: cast_nullable_to_non_nullable + as DateTime, + null == dateTo + ? _value.dateTo + : dateTo // ignore: cast_nullable_to_non_nullable + as DateTime, + ), + ); + } +} + +/// @nodoc + +class _$RangeDateChangedImpl implements _RangeDateChanged { + const _$RangeDateChangedImpl(this.dateFrom, this.dateTo); + + @override + final DateTime dateFrom; + @override + final DateTime dateTo; + + @override + String toString() { + return 'ProfitLossLoaderEvent.rangeDateChanged(dateFrom: $dateFrom, dateTo: $dateTo)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$RangeDateChangedImpl && + (identical(other.dateFrom, dateFrom) || + other.dateFrom == dateFrom) && + (identical(other.dateTo, dateTo) || other.dateTo == dateTo)); + } + + @override + int get hashCode => Object.hash(runtimeType, dateFrom, dateTo); + + /// Create a copy of ProfitLossLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$RangeDateChangedImplCopyWith<_$RangeDateChangedImpl> get copyWith => + __$$RangeDateChangedImplCopyWithImpl<_$RangeDateChangedImpl>( + this, + _$identity, + ); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(DateTime dateFrom, DateTime dateTo) + rangeDateChanged, + required TResult Function() fetched, + }) { + return rangeDateChanged(dateFrom, dateTo); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged, + TResult? Function()? fetched, + }) { + return rangeDateChanged?.call(dateFrom, dateTo); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged, + TResult Function()? fetched, + required TResult orElse(), + }) { + if (rangeDateChanged != null) { + return rangeDateChanged(dateFrom, dateTo); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_RangeDateChanged value) rangeDateChanged, + required TResult Function(_Fetched value) fetched, + }) { + return rangeDateChanged(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_RangeDateChanged value)? rangeDateChanged, + TResult? Function(_Fetched value)? fetched, + }) { + return rangeDateChanged?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_RangeDateChanged value)? rangeDateChanged, + TResult Function(_Fetched value)? fetched, + required TResult orElse(), + }) { + if (rangeDateChanged != null) { + return rangeDateChanged(this); + } + return orElse(); + } +} + +abstract class _RangeDateChanged implements ProfitLossLoaderEvent { + const factory _RangeDateChanged( + final DateTime dateFrom, + final DateTime dateTo, + ) = _$RangeDateChangedImpl; + + DateTime get dateFrom; + DateTime get dateTo; + + /// Create a copy of ProfitLossLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$RangeDateChangedImplCopyWith<_$RangeDateChangedImpl> get copyWith => + throw _privateConstructorUsedError; +} + /// @nodoc abstract class _$$FetchedImplCopyWith<$Res> { factory _$$FetchedImplCopyWith( @@ -112,19 +277,27 @@ class _$FetchedImpl implements _Fetched { @override @optionalTypeArgs - TResult when({required TResult Function() fetched}) { + TResult when({ + required TResult Function(DateTime dateFrom, DateTime dateTo) + rangeDateChanged, + required TResult Function() fetched, + }) { return fetched(); } @override @optionalTypeArgs - TResult? whenOrNull({TResult? Function()? fetched}) { + TResult? whenOrNull({ + TResult? Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged, + TResult? Function()? fetched, + }) { return fetched?.call(); } @override @optionalTypeArgs TResult maybeWhen({ + TResult Function(DateTime dateFrom, DateTime dateTo)? rangeDateChanged, TResult Function()? fetched, required TResult orElse(), }) { @@ -137,6 +310,7 @@ class _$FetchedImpl implements _Fetched { @override @optionalTypeArgs TResult map({ + required TResult Function(_RangeDateChanged value) rangeDateChanged, required TResult Function(_Fetched value) fetched, }) { return fetched(this); @@ -145,6 +319,7 @@ class _$FetchedImpl implements _Fetched { @override @optionalTypeArgs TResult? mapOrNull({ + TResult? Function(_RangeDateChanged value)? rangeDateChanged, TResult? Function(_Fetched value)? fetched, }) { return fetched?.call(this); @@ -153,6 +328,7 @@ class _$FetchedImpl implements _Fetched { @override @optionalTypeArgs TResult maybeMap({ + TResult Function(_RangeDateChanged value)? rangeDateChanged, TResult Function(_Fetched value)? fetched, required TResult orElse(), }) { @@ -173,6 +349,8 @@ mixin _$ProfitLossLoaderState { Option get failureOptionProfitLoss => throw _privateConstructorUsedError; bool get isFetching => throw _privateConstructorUsedError; + DateTime get dateFrom => throw _privateConstructorUsedError; + DateTime get dateTo => throw _privateConstructorUsedError; /// Create a copy of ProfitLossLoaderState /// with the given fields replaced by the non-null parameter values. @@ -192,6 +370,8 @@ abstract class $ProfitLossLoaderStateCopyWith<$Res> { ProfitLossAnalytic profitLoss, Option failureOptionProfitLoss, bool isFetching, + DateTime dateFrom, + DateTime dateTo, }); $ProfitLossAnalyticCopyWith<$Res> get profitLoss; @@ -218,6 +398,8 @@ class _$ProfitLossLoaderStateCopyWithImpl< Object? profitLoss = null, Object? failureOptionProfitLoss = null, Object? isFetching = null, + Object? dateFrom = null, + Object? dateTo = null, }) { return _then( _value.copyWith( @@ -233,6 +415,14 @@ class _$ProfitLossLoaderStateCopyWithImpl< ? _value.isFetching : isFetching // ignore: cast_nullable_to_non_nullable as bool, + dateFrom: null == dateFrom + ? _value.dateFrom + : dateFrom // ignore: cast_nullable_to_non_nullable + as DateTime, + dateTo: null == dateTo + ? _value.dateTo + : dateTo // ignore: cast_nullable_to_non_nullable + as DateTime, ) as $Val, ); @@ -262,6 +452,8 @@ abstract class _$$ProfitLossLoaderStateImplCopyWith<$Res> ProfitLossAnalytic profitLoss, Option failureOptionProfitLoss, bool isFetching, + DateTime dateFrom, + DateTime dateTo, }); @override @@ -286,6 +478,8 @@ class __$$ProfitLossLoaderStateImplCopyWithImpl<$Res> Object? profitLoss = null, Object? failureOptionProfitLoss = null, Object? isFetching = null, + Object? dateFrom = null, + Object? dateTo = null, }) { return _then( _$ProfitLossLoaderStateImpl( @@ -301,6 +495,14 @@ class __$$ProfitLossLoaderStateImplCopyWithImpl<$Res> ? _value.isFetching : isFetching // ignore: cast_nullable_to_non_nullable as bool, + dateFrom: null == dateFrom + ? _value.dateFrom + : dateFrom // ignore: cast_nullable_to_non_nullable + as DateTime, + dateTo: null == dateTo + ? _value.dateTo + : dateTo // ignore: cast_nullable_to_non_nullable + as DateTime, ), ); } @@ -313,6 +515,8 @@ class _$ProfitLossLoaderStateImpl implements _ProfitLossLoaderState { required this.profitLoss, required this.failureOptionProfitLoss, this.isFetching = false, + required this.dateFrom, + required this.dateTo, }); @override @@ -322,10 +526,14 @@ class _$ProfitLossLoaderStateImpl implements _ProfitLossLoaderState { @override @JsonKey() final bool isFetching; + @override + final DateTime dateFrom; + @override + final DateTime dateTo; @override String toString() { - return 'ProfitLossLoaderState(profitLoss: $profitLoss, failureOptionProfitLoss: $failureOptionProfitLoss, isFetching: $isFetching)'; + return 'ProfitLossLoaderState(profitLoss: $profitLoss, failureOptionProfitLoss: $failureOptionProfitLoss, isFetching: $isFetching, dateFrom: $dateFrom, dateTo: $dateTo)'; } @override @@ -341,12 +549,21 @@ class _$ProfitLossLoaderStateImpl implements _ProfitLossLoaderState { ) || other.failureOptionProfitLoss == failureOptionProfitLoss) && (identical(other.isFetching, isFetching) || - other.isFetching == isFetching)); + other.isFetching == isFetching) && + (identical(other.dateFrom, dateFrom) || + other.dateFrom == dateFrom) && + (identical(other.dateTo, dateTo) || other.dateTo == dateTo)); } @override - int get hashCode => - Object.hash(runtimeType, profitLoss, failureOptionProfitLoss, isFetching); + int get hashCode => Object.hash( + runtimeType, + profitLoss, + failureOptionProfitLoss, + isFetching, + dateFrom, + dateTo, + ); /// Create a copy of ProfitLossLoaderState /// with the given fields replaced by the non-null parameter values. @@ -366,6 +583,8 @@ abstract class _ProfitLossLoaderState implements ProfitLossLoaderState { required final ProfitLossAnalytic profitLoss, required final Option failureOptionProfitLoss, final bool isFetching, + required final DateTime dateFrom, + required final DateTime dateTo, }) = _$ProfitLossLoaderStateImpl; @override @@ -374,6 +593,10 @@ abstract class _ProfitLossLoaderState implements ProfitLossLoaderState { Option get failureOptionProfitLoss; @override bool get isFetching; + @override + DateTime get dateFrom; + @override + DateTime get dateTo; /// Create a copy of ProfitLossLoaderState /// with the given fields replaced by the non-null parameter values. diff --git a/lib/application/analytic/profit_loss_loader/profit_loss_loader_event.dart b/lib/application/analytic/profit_loss_loader/profit_loss_loader_event.dart index 2a69fcc..5164378 100644 --- a/lib/application/analytic/profit_loss_loader/profit_loss_loader_event.dart +++ b/lib/application/analytic/profit_loss_loader/profit_loss_loader_event.dart @@ -2,5 +2,9 @@ part of 'profit_loss_loader_bloc.dart'; @freezed class ProfitLossLoaderEvent with _$ProfitLossLoaderEvent { + const factory ProfitLossLoaderEvent.rangeDateChanged( + DateTime dateFrom, + DateTime dateTo, + ) = _RangeDateChanged; const factory ProfitLossLoaderEvent.fetched() = _Fetched; } diff --git a/lib/application/analytic/profit_loss_loader/profit_loss_loader_state.dart b/lib/application/analytic/profit_loss_loader/profit_loss_loader_state.dart index 783bfe4..1465648 100644 --- a/lib/application/analytic/profit_loss_loader/profit_loss_loader_state.dart +++ b/lib/application/analytic/profit_loss_loader/profit_loss_loader_state.dart @@ -6,10 +6,14 @@ class ProfitLossLoaderState with _$ProfitLossLoaderState { required ProfitLossAnalytic profitLoss, required Option failureOptionProfitLoss, @Default(false) bool isFetching, + required DateTime dateFrom, + required DateTime dateTo, }) = _ProfitLossLoaderState; factory ProfitLossLoaderState.initial() => ProfitLossLoaderState( profitLoss: ProfitLossAnalytic.empty(), failureOptionProfitLoss: none(), + dateFrom: DateTime.now().subtract(const Duration(days: 30)), + dateTo: DateTime.now(), ); } diff --git a/lib/presentation/pages/finance/finance_page.dart b/lib/presentation/pages/finance/finance_page.dart index b165172..9a99ad0 100644 --- a/lib/presentation/pages/finance/finance_page.dart +++ b/lib/presentation/pages/finance/finance_page.dart @@ -10,6 +10,7 @@ import '../../../common/theme/theme.dart'; import '../../../domain/analytic/analytic.dart'; import '../../../injection.dart'; import '../../components/appbar/appbar.dart'; +import '../../components/field/date_range_picker_field.dart'; import 'widgets/cash_flow.dart'; import 'widgets/category.dart'; import 'widgets/product.dart'; @@ -50,14 +51,6 @@ class _FinancePageState extends State late Animation _fadeAnimation; late Animation _scaleAnimation; - String selectedPeriod = 'Hari ini'; - final List periods = [ - 'Hari ini', - 'Minggu ini', - 'Bulan ini', - 'Tahun ini', - ]; - @override void initState() { super.initState(); @@ -113,136 +106,108 @@ class _FinancePageState extends State Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColor.background, - body: BlocBuilder( - builder: (context, state) { - return CustomScrollView( - slivers: [ - // SliverAppBar with animated background - SliverAppBar( - expandedHeight: 120, - floating: false, - pinned: true, - backgroundColor: AppColor.primary, - elevation: 0, - flexibleSpace: CustomAppBar(title: 'Keuangan'), - ), - - // Header dengan filter periode - SliverToBoxAdapter( - child: FadeTransition( - opacity: _fadeAnimation, - child: _buildPeriodSelector(), - ), - ), - - // Summary Cards - SliverToBoxAdapter( - child: SlideTransition( - position: _slideAnimation, - child: _buildSummaryCards(state.profitLoss.summary), - ), - ), - - // Cash Flow Analysis - SliverToBoxAdapter( - child: ScaleTransition( - scale: _scaleAnimation, - child: FinanceCashFlow(dailyData: state.profitLoss.data), - ), - ), - - // Profit Loss Detail - SliverToBoxAdapter( - child: FadeTransition( - opacity: _fadeAnimation, - child: FinanceProfitLoss(data: state.profitLoss.summary), - ), - ), - - BlocBuilder< - CategoryAnalyticLoaderBloc, - CategoryAnalyticLoaderState - >( - builder: (context, stateCategory) { - return SliverToBoxAdapter( - child: SlideTransition( - position: _slideAnimation, - child: FinanceCategory( - categories: stateCategory.categoryAnalytic.data, - ), - ), - ); - }, - ), - - // Product Analysis Section - SliverToBoxAdapter( - child: SlideTransition( - position: _slideAnimation, - child: _buildProductAnalysis(state.profitLoss.productData), - ), - ), - - // Transaction Categories - - // Bottom spacing - const SliverToBoxAdapter(child: SizedBox(height: 100)), - ], + body: BlocListener( + listenWhen: (previous, current) => + previous.dateFrom != current.dateFrom && + previous.dateTo != current.dateTo, + listener: (context, state) { + context.read().add( + ProfitLossLoaderEvent.fetched(), ); }, - ), - ); - } + child: BlocBuilder( + builder: (context, state) { + return CustomScrollView( + slivers: [ + // SliverAppBar with animated background + SliverAppBar( + expandedHeight: 120, + floating: false, + pinned: true, + backgroundColor: AppColor.primary, + elevation: 0, + flexibleSpace: CustomAppBar(title: 'Keuangan'), + ), - Widget _buildPeriodSelector() { - return Container( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: AppColor.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColor.border), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: selectedPeriod, - isExpanded: true, - icon: const Icon( - LineIcons.angleDown, - color: AppColor.primary, + // Header dengan filter periode + SliverToBoxAdapter( + child: FadeTransition( + opacity: _fadeAnimation, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: DateRangePickerField( + maxDate: DateTime.now(), + startDate: state.dateFrom, + endDate: state.dateTo, + onChanged: (startDate, endDate) { + context.read().add( + ProfitLossLoaderEvent.rangeDateChanged( + startDate!, + endDate!, + ), + ); + }, + ), + ), ), - style: AppStyle.md, - items: periods.map((String period) { - return DropdownMenuItem( - value: period, - child: Text(period), + ), + + // Summary Cards + SliverToBoxAdapter( + child: SlideTransition( + position: _slideAnimation, + child: _buildSummaryCards(state.profitLoss.summary), + ), + ), + + // Cash Flow Analysis + SliverToBoxAdapter( + child: ScaleTransition( + scale: _scaleAnimation, + child: FinanceCashFlow(dailyData: state.profitLoss.data), + ), + ), + + // Profit Loss Detail + SliverToBoxAdapter( + child: FadeTransition( + opacity: _fadeAnimation, + child: FinanceProfitLoss(data: state.profitLoss.summary), + ), + ), + + BlocBuilder< + CategoryAnalyticLoaderBloc, + CategoryAnalyticLoaderState + >( + builder: (context, stateCategory) { + return SliverToBoxAdapter( + child: SlideTransition( + position: _slideAnimation, + child: FinanceCategory( + categories: stateCategory.categoryAnalytic.data, + ), + ), ); - }).toList(), - onChanged: (String? newValue) { - setState(() { - selectedPeriod = newValue!; - }); }, ), - ), - ), - ), - const SizedBox(width: 12), - Container( - decoration: BoxDecoration( - color: AppColor.primary, - borderRadius: BorderRadius.circular(12), - ), - child: IconButton( - onPressed: () {}, - icon: const Icon(LineIcons.calendar, color: AppColor.white), - ), - ), - ], + + // Product Analysis Section + SliverToBoxAdapter( + child: SlideTransition( + position: _slideAnimation, + child: _buildProductAnalysis(state.profitLoss.productData), + ), + ), + + // Transaction Categories + + // Bottom spacing + const SliverToBoxAdapter(child: SizedBox(height: 100)), + ], + ); + }, + ), ), ); }