diff --git a/lib/application/report/transaction_report/transaction_report_bloc.dart b/lib/application/report/transaction_report/transaction_report_bloc.dart new file mode 100644 index 0000000..b7b85d0 --- /dev/null +++ b/lib/application/report/transaction_report/transaction_report_bloc.dart @@ -0,0 +1,91 @@ +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/analytic/analytic.dart'; +import '../../../domain/analytic/repositories/i_analytic_repository.dart'; +import '../../../domain/outlet/outlet.dart'; + +part 'transaction_report_event.dart'; +part 'transaction_report_state.dart'; +part 'transaction_report_bloc.freezed.dart'; + +@injectable +class TransactionReportBloc + extends Bloc { + final IAnalyticRepository _analyticRepository; + final IOutletRepository _outletRepository; + + TransactionReportBloc(this._analyticRepository, this._outletRepository) + : super(TransactionReportState.initial()) { + on(_onTransactionReportEvent); + } + + Future _onTransactionReportEvent( + TransactionReportEvent event, + Emitter emit, + ) { + return event.map( + fetchedOutlet: (e) async { + emit( + state.copyWith(isFetchingOutlet: true, failureOptionOutlet: none()), + ); + + final result = await _outletRepository.currentOutlet(); + + var data = result.fold( + (f) => state.copyWith(failureOptionOutlet: optionOf(f)), + (currentOutlet) => state.copyWith(outlet: currentOutlet), + ); + + emit(data.copyWith(isFetchingOutlet: false)); + }, + fetchedTransaction: (e) async { + emit(state.copyWith(isFetching: true, failureOptionAnalytic: none())); + + var newState = state; + + final category = await _analyticRepository.getCategory( + dateFrom: e.dateFrom, + dateTo: e.dateTo, + ); + final profitLoss = await _analyticRepository.getProfitLoss( + dateFrom: e.dateFrom, + dateTo: e.dateTo, + ); + final paymentMethod = await _analyticRepository.getPaymentMethod( + dateFrom: e.dateFrom, + dateTo: e.dateTo, + ); + final product = await _analyticRepository.getProduct( + dateFrom: e.dateFrom, + dateTo: e.dateTo, + ); + + newState = category.fold( + (f) => newState.copyWith(failureOptionAnalytic: optionOf(f)), + (categoryAnalytic) => + newState.copyWith(categoryAnalytic: categoryAnalytic), + ); + newState = profitLoss.fold( + (f) => newState.copyWith(failureOptionAnalytic: optionOf(f)), + (profitLossAnalytic) => + newState.copyWith(profitLossAnalytic: profitLossAnalytic), + ); + newState = paymentMethod.fold( + (f) => newState.copyWith(failureOptionAnalytic: optionOf(f)), + (paymentMethodAnalytic) => + newState.copyWith(paymentMethodAnalytic: paymentMethodAnalytic), + ); + newState = product.fold( + (f) => newState.copyWith(failureOptionAnalytic: optionOf(f)), + (productAnalytic) => + newState.copyWith(productAnalytic: productAnalytic), + ); + + emit(newState.copyWith(isFetching: false)); + }, + ); + } +} diff --git a/lib/application/report/transaction_report/transaction_report_bloc.freezed.dart b/lib/application/report/transaction_report/transaction_report_bloc.freezed.dart new file mode 100644 index 0000000..b60a09b --- /dev/null +++ b/lib/application/report/transaction_report/transaction_report_bloc.freezed.dart @@ -0,0 +1,752 @@ +// 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 'transaction_report_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 _$TransactionReportEvent { + @optionalTypeArgs + TResult when({ + required TResult Function() fetchedOutlet, + required TResult Function(DateTime dateFrom, DateTime dateTo) + fetchedTransaction, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? fetchedOutlet, + TResult? Function(DateTime dateFrom, DateTime dateTo)? fetchedTransaction, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? fetchedOutlet, + TResult Function(DateTime dateFrom, DateTime dateTo)? fetchedTransaction, + required TResult orElse(), + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_FetchedOutlet value) fetchedOutlet, + required TResult Function(_FetchedTransaction value) fetchedTransaction, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_FetchedOutlet value)? fetchedOutlet, + TResult? Function(_FetchedTransaction value)? fetchedTransaction, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_FetchedOutlet value)? fetchedOutlet, + TResult Function(_FetchedTransaction value)? fetchedTransaction, + required TResult orElse(), + }) => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $TransactionReportEventCopyWith<$Res> { + factory $TransactionReportEventCopyWith( + TransactionReportEvent value, + $Res Function(TransactionReportEvent) then, + ) = _$TransactionReportEventCopyWithImpl<$Res, TransactionReportEvent>; +} + +/// @nodoc +class _$TransactionReportEventCopyWithImpl< + $Res, + $Val extends TransactionReportEvent +> + implements $TransactionReportEventCopyWith<$Res> { + _$TransactionReportEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of TransactionReportEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc +abstract class _$$FetchedOutletImplCopyWith<$Res> { + factory _$$FetchedOutletImplCopyWith( + _$FetchedOutletImpl value, + $Res Function(_$FetchedOutletImpl) then, + ) = __$$FetchedOutletImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$FetchedOutletImplCopyWithImpl<$Res> + extends _$TransactionReportEventCopyWithImpl<$Res, _$FetchedOutletImpl> + implements _$$FetchedOutletImplCopyWith<$Res> { + __$$FetchedOutletImplCopyWithImpl( + _$FetchedOutletImpl _value, + $Res Function(_$FetchedOutletImpl) _then, + ) : super(_value, _then); + + /// Create a copy of TransactionReportEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$FetchedOutletImpl implements _FetchedOutlet { + const _$FetchedOutletImpl(); + + @override + String toString() { + return 'TransactionReportEvent.fetchedOutlet()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$FetchedOutletImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() fetchedOutlet, + required TResult Function(DateTime dateFrom, DateTime dateTo) + fetchedTransaction, + }) { + return fetchedOutlet(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? fetchedOutlet, + TResult? Function(DateTime dateFrom, DateTime dateTo)? fetchedTransaction, + }) { + return fetchedOutlet?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? fetchedOutlet, + TResult Function(DateTime dateFrom, DateTime dateTo)? fetchedTransaction, + required TResult orElse(), + }) { + if (fetchedOutlet != null) { + return fetchedOutlet(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_FetchedOutlet value) fetchedOutlet, + required TResult Function(_FetchedTransaction value) fetchedTransaction, + }) { + return fetchedOutlet(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_FetchedOutlet value)? fetchedOutlet, + TResult? Function(_FetchedTransaction value)? fetchedTransaction, + }) { + return fetchedOutlet?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_FetchedOutlet value)? fetchedOutlet, + TResult Function(_FetchedTransaction value)? fetchedTransaction, + required TResult orElse(), + }) { + if (fetchedOutlet != null) { + return fetchedOutlet(this); + } + return orElse(); + } +} + +abstract class _FetchedOutlet implements TransactionReportEvent { + const factory _FetchedOutlet() = _$FetchedOutletImpl; +} + +/// @nodoc +abstract class _$$FetchedTransactionImplCopyWith<$Res> { + factory _$$FetchedTransactionImplCopyWith( + _$FetchedTransactionImpl value, + $Res Function(_$FetchedTransactionImpl) then, + ) = __$$FetchedTransactionImplCopyWithImpl<$Res>; + @useResult + $Res call({DateTime dateFrom, DateTime dateTo}); +} + +/// @nodoc +class __$$FetchedTransactionImplCopyWithImpl<$Res> + extends _$TransactionReportEventCopyWithImpl<$Res, _$FetchedTransactionImpl> + implements _$$FetchedTransactionImplCopyWith<$Res> { + __$$FetchedTransactionImplCopyWithImpl( + _$FetchedTransactionImpl _value, + $Res Function(_$FetchedTransactionImpl) _then, + ) : super(_value, _then); + + /// Create a copy of TransactionReportEvent + /// 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( + _$FetchedTransactionImpl( + 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 _$FetchedTransactionImpl implements _FetchedTransaction { + const _$FetchedTransactionImpl(this.dateFrom, this.dateTo); + + @override + final DateTime dateFrom; + @override + final DateTime dateTo; + + @override + String toString() { + return 'TransactionReportEvent.fetchedTransaction(dateFrom: $dateFrom, dateTo: $dateTo)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$FetchedTransactionImpl && + (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 TransactionReportEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$FetchedTransactionImplCopyWith<_$FetchedTransactionImpl> get copyWith => + __$$FetchedTransactionImplCopyWithImpl<_$FetchedTransactionImpl>( + this, + _$identity, + ); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() fetchedOutlet, + required TResult Function(DateTime dateFrom, DateTime dateTo) + fetchedTransaction, + }) { + return fetchedTransaction(dateFrom, dateTo); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? fetchedOutlet, + TResult? Function(DateTime dateFrom, DateTime dateTo)? fetchedTransaction, + }) { + return fetchedTransaction?.call(dateFrom, dateTo); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? fetchedOutlet, + TResult Function(DateTime dateFrom, DateTime dateTo)? fetchedTransaction, + required TResult orElse(), + }) { + if (fetchedTransaction != null) { + return fetchedTransaction(dateFrom, dateTo); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_FetchedOutlet value) fetchedOutlet, + required TResult Function(_FetchedTransaction value) fetchedTransaction, + }) { + return fetchedTransaction(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_FetchedOutlet value)? fetchedOutlet, + TResult? Function(_FetchedTransaction value)? fetchedTransaction, + }) { + return fetchedTransaction?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_FetchedOutlet value)? fetchedOutlet, + TResult Function(_FetchedTransaction value)? fetchedTransaction, + required TResult orElse(), + }) { + if (fetchedTransaction != null) { + return fetchedTransaction(this); + } + return orElse(); + } +} + +abstract class _FetchedTransaction implements TransactionReportEvent { + const factory _FetchedTransaction( + final DateTime dateFrom, + final DateTime dateTo, + ) = _$FetchedTransactionImpl; + + DateTime get dateFrom; + DateTime get dateTo; + + /// Create a copy of TransactionReportEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$FetchedTransactionImplCopyWith<_$FetchedTransactionImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$TransactionReportState { + CategoryAnalytic get categoryAnalytic => throw _privateConstructorUsedError; + ProfitLossAnalytic get profitLossAnalytic => + throw _privateConstructorUsedError; + PaymentMethodAnalytic get paymentMethodAnalytic => + throw _privateConstructorUsedError; + ProductAnalytic get productAnalytic => throw _privateConstructorUsedError; + Option get failureOptionAnalytic => + throw _privateConstructorUsedError; + Outlet get outlet => throw _privateConstructorUsedError; + Option get failureOptionOutlet => + throw _privateConstructorUsedError; + bool get isFetching => throw _privateConstructorUsedError; + bool get isFetchingOutlet => throw _privateConstructorUsedError; + + /// Create a copy of TransactionReportState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $TransactionReportStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $TransactionReportStateCopyWith<$Res> { + factory $TransactionReportStateCopyWith( + TransactionReportState value, + $Res Function(TransactionReportState) then, + ) = _$TransactionReportStateCopyWithImpl<$Res, TransactionReportState>; + @useResult + $Res call({ + CategoryAnalytic categoryAnalytic, + ProfitLossAnalytic profitLossAnalytic, + PaymentMethodAnalytic paymentMethodAnalytic, + ProductAnalytic productAnalytic, + Option failureOptionAnalytic, + Outlet outlet, + Option failureOptionOutlet, + bool isFetching, + bool isFetchingOutlet, + }); + + $CategoryAnalyticCopyWith<$Res> get categoryAnalytic; + $ProfitLossAnalyticCopyWith<$Res> get profitLossAnalytic; + $PaymentMethodAnalyticCopyWith<$Res> get paymentMethodAnalytic; + $ProductAnalyticCopyWith<$Res> get productAnalytic; + $OutletCopyWith<$Res> get outlet; +} + +/// @nodoc +class _$TransactionReportStateCopyWithImpl< + $Res, + $Val extends TransactionReportState +> + implements $TransactionReportStateCopyWith<$Res> { + _$TransactionReportStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of TransactionReportState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? categoryAnalytic = null, + Object? profitLossAnalytic = null, + Object? paymentMethodAnalytic = null, + Object? productAnalytic = null, + Object? failureOptionAnalytic = null, + Object? outlet = null, + Object? failureOptionOutlet = null, + Object? isFetching = null, + Object? isFetchingOutlet = null, + }) { + return _then( + _value.copyWith( + categoryAnalytic: null == categoryAnalytic + ? _value.categoryAnalytic + : categoryAnalytic // ignore: cast_nullable_to_non_nullable + as CategoryAnalytic, + profitLossAnalytic: null == profitLossAnalytic + ? _value.profitLossAnalytic + : profitLossAnalytic // ignore: cast_nullable_to_non_nullable + as ProfitLossAnalytic, + paymentMethodAnalytic: null == paymentMethodAnalytic + ? _value.paymentMethodAnalytic + : paymentMethodAnalytic // ignore: cast_nullable_to_non_nullable + as PaymentMethodAnalytic, + productAnalytic: null == productAnalytic + ? _value.productAnalytic + : productAnalytic // ignore: cast_nullable_to_non_nullable + as ProductAnalytic, + failureOptionAnalytic: null == failureOptionAnalytic + ? _value.failureOptionAnalytic + : failureOptionAnalytic // ignore: cast_nullable_to_non_nullable + as Option, + outlet: null == outlet + ? _value.outlet + : outlet // ignore: cast_nullable_to_non_nullable + as Outlet, + 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, + isFetchingOutlet: null == isFetchingOutlet + ? _value.isFetchingOutlet + : isFetchingOutlet // ignore: cast_nullable_to_non_nullable + as bool, + ) + as $Val, + ); + } + + /// Create a copy of TransactionReportState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $CategoryAnalyticCopyWith<$Res> get categoryAnalytic { + return $CategoryAnalyticCopyWith<$Res>(_value.categoryAnalytic, (value) { + return _then(_value.copyWith(categoryAnalytic: value) as $Val); + }); + } + + /// Create a copy of TransactionReportState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ProfitLossAnalyticCopyWith<$Res> get profitLossAnalytic { + return $ProfitLossAnalyticCopyWith<$Res>(_value.profitLossAnalytic, ( + value, + ) { + return _then(_value.copyWith(profitLossAnalytic: value) as $Val); + }); + } + + /// Create a copy of TransactionReportState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $PaymentMethodAnalyticCopyWith<$Res> get paymentMethodAnalytic { + return $PaymentMethodAnalyticCopyWith<$Res>(_value.paymentMethodAnalytic, ( + value, + ) { + return _then(_value.copyWith(paymentMethodAnalytic: value) as $Val); + }); + } + + /// Create a copy of TransactionReportState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ProductAnalyticCopyWith<$Res> get productAnalytic { + return $ProductAnalyticCopyWith<$Res>(_value.productAnalytic, (value) { + return _then(_value.copyWith(productAnalytic: value) as $Val); + }); + } + + /// Create a copy of TransactionReportState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $OutletCopyWith<$Res> get outlet { + return $OutletCopyWith<$Res>(_value.outlet, (value) { + return _then(_value.copyWith(outlet: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$TransactionReportStateImplCopyWith<$Res> + implements $TransactionReportStateCopyWith<$Res> { + factory _$$TransactionReportStateImplCopyWith( + _$TransactionReportStateImpl value, + $Res Function(_$TransactionReportStateImpl) then, + ) = __$$TransactionReportStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + CategoryAnalytic categoryAnalytic, + ProfitLossAnalytic profitLossAnalytic, + PaymentMethodAnalytic paymentMethodAnalytic, + ProductAnalytic productAnalytic, + Option failureOptionAnalytic, + Outlet outlet, + Option failureOptionOutlet, + bool isFetching, + bool isFetchingOutlet, + }); + + @override + $CategoryAnalyticCopyWith<$Res> get categoryAnalytic; + @override + $ProfitLossAnalyticCopyWith<$Res> get profitLossAnalytic; + @override + $PaymentMethodAnalyticCopyWith<$Res> get paymentMethodAnalytic; + @override + $ProductAnalyticCopyWith<$Res> get productAnalytic; + @override + $OutletCopyWith<$Res> get outlet; +} + +/// @nodoc +class __$$TransactionReportStateImplCopyWithImpl<$Res> + extends + _$TransactionReportStateCopyWithImpl<$Res, _$TransactionReportStateImpl> + implements _$$TransactionReportStateImplCopyWith<$Res> { + __$$TransactionReportStateImplCopyWithImpl( + _$TransactionReportStateImpl _value, + $Res Function(_$TransactionReportStateImpl) _then, + ) : super(_value, _then); + + /// Create a copy of TransactionReportState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? categoryAnalytic = null, + Object? profitLossAnalytic = null, + Object? paymentMethodAnalytic = null, + Object? productAnalytic = null, + Object? failureOptionAnalytic = null, + Object? outlet = null, + Object? failureOptionOutlet = null, + Object? isFetching = null, + Object? isFetchingOutlet = null, + }) { + return _then( + _$TransactionReportStateImpl( + categoryAnalytic: null == categoryAnalytic + ? _value.categoryAnalytic + : categoryAnalytic // ignore: cast_nullable_to_non_nullable + as CategoryAnalytic, + profitLossAnalytic: null == profitLossAnalytic + ? _value.profitLossAnalytic + : profitLossAnalytic // ignore: cast_nullable_to_non_nullable + as ProfitLossAnalytic, + paymentMethodAnalytic: null == paymentMethodAnalytic + ? _value.paymentMethodAnalytic + : paymentMethodAnalytic // ignore: cast_nullable_to_non_nullable + as PaymentMethodAnalytic, + productAnalytic: null == productAnalytic + ? _value.productAnalytic + : productAnalytic // ignore: cast_nullable_to_non_nullable + as ProductAnalytic, + failureOptionAnalytic: null == failureOptionAnalytic + ? _value.failureOptionAnalytic + : failureOptionAnalytic // ignore: cast_nullable_to_non_nullable + as Option, + outlet: null == outlet + ? _value.outlet + : outlet // ignore: cast_nullable_to_non_nullable + as Outlet, + 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, + isFetchingOutlet: null == isFetchingOutlet + ? _value.isFetchingOutlet + : isFetchingOutlet // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} + +/// @nodoc + +class _$TransactionReportStateImpl implements _TransactionReportState { + const _$TransactionReportStateImpl({ + required this.categoryAnalytic, + required this.profitLossAnalytic, + required this.paymentMethodAnalytic, + required this.productAnalytic, + required this.failureOptionAnalytic, + required this.outlet, + required this.failureOptionOutlet, + this.isFetching = false, + this.isFetchingOutlet = false, + }); + + @override + final CategoryAnalytic categoryAnalytic; + @override + final ProfitLossAnalytic profitLossAnalytic; + @override + final PaymentMethodAnalytic paymentMethodAnalytic; + @override + final ProductAnalytic productAnalytic; + @override + final Option failureOptionAnalytic; + @override + final Outlet outlet; + @override + final Option failureOptionOutlet; + @override + @JsonKey() + final bool isFetching; + @override + @JsonKey() + final bool isFetchingOutlet; + + @override + String toString() { + return 'TransactionReportState(categoryAnalytic: $categoryAnalytic, profitLossAnalytic: $profitLossAnalytic, paymentMethodAnalytic: $paymentMethodAnalytic, productAnalytic: $productAnalytic, failureOptionAnalytic: $failureOptionAnalytic, outlet: $outlet, failureOptionOutlet: $failureOptionOutlet, isFetching: $isFetching, isFetchingOutlet: $isFetchingOutlet)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$TransactionReportStateImpl && + (identical(other.categoryAnalytic, categoryAnalytic) || + other.categoryAnalytic == categoryAnalytic) && + (identical(other.profitLossAnalytic, profitLossAnalytic) || + other.profitLossAnalytic == profitLossAnalytic) && + (identical(other.paymentMethodAnalytic, paymentMethodAnalytic) || + other.paymentMethodAnalytic == paymentMethodAnalytic) && + (identical(other.productAnalytic, productAnalytic) || + other.productAnalytic == productAnalytic) && + (identical(other.failureOptionAnalytic, failureOptionAnalytic) || + other.failureOptionAnalytic == failureOptionAnalytic) && + (identical(other.outlet, outlet) || other.outlet == outlet) && + (identical(other.failureOptionOutlet, failureOptionOutlet) || + other.failureOptionOutlet == failureOptionOutlet) && + (identical(other.isFetching, isFetching) || + other.isFetching == isFetching) && + (identical(other.isFetchingOutlet, isFetchingOutlet) || + other.isFetchingOutlet == isFetchingOutlet)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + categoryAnalytic, + profitLossAnalytic, + paymentMethodAnalytic, + productAnalytic, + failureOptionAnalytic, + outlet, + failureOptionOutlet, + isFetching, + isFetchingOutlet, + ); + + /// Create a copy of TransactionReportState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$TransactionReportStateImplCopyWith<_$TransactionReportStateImpl> + get copyWith => + __$$TransactionReportStateImplCopyWithImpl<_$TransactionReportStateImpl>( + this, + _$identity, + ); +} + +abstract class _TransactionReportState implements TransactionReportState { + const factory _TransactionReportState({ + required final CategoryAnalytic categoryAnalytic, + required final ProfitLossAnalytic profitLossAnalytic, + required final PaymentMethodAnalytic paymentMethodAnalytic, + required final ProductAnalytic productAnalytic, + required final Option failureOptionAnalytic, + required final Outlet outlet, + required final Option failureOptionOutlet, + final bool isFetching, + final bool isFetchingOutlet, + }) = _$TransactionReportStateImpl; + + @override + CategoryAnalytic get categoryAnalytic; + @override + ProfitLossAnalytic get profitLossAnalytic; + @override + PaymentMethodAnalytic get paymentMethodAnalytic; + @override + ProductAnalytic get productAnalytic; + @override + Option get failureOptionAnalytic; + @override + Outlet get outlet; + @override + Option get failureOptionOutlet; + @override + bool get isFetching; + @override + bool get isFetchingOutlet; + + /// Create a copy of TransactionReportState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$TransactionReportStateImplCopyWith<_$TransactionReportStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/application/report/transaction_report/transaction_report_event.dart b/lib/application/report/transaction_report/transaction_report_event.dart new file mode 100644 index 0000000..5b744e6 --- /dev/null +++ b/lib/application/report/transaction_report/transaction_report_event.dart @@ -0,0 +1,10 @@ +part of 'transaction_report_bloc.dart'; + +@freezed +class TransactionReportEvent with _$TransactionReportEvent { + const factory TransactionReportEvent.fetchedOutlet() = _FetchedOutlet; + const factory TransactionReportEvent.fetchedTransaction( + DateTime dateFrom, + DateTime dateTo, + ) = _FetchedTransaction; +} diff --git a/lib/application/report/transaction_report/transaction_report_state.dart b/lib/application/report/transaction_report/transaction_report_state.dart new file mode 100644 index 0000000..8a54c88 --- /dev/null +++ b/lib/application/report/transaction_report/transaction_report_state.dart @@ -0,0 +1,26 @@ +part of 'transaction_report_bloc.dart'; + +@freezed +class TransactionReportState with _$TransactionReportState { + const factory TransactionReportState({ + required CategoryAnalytic categoryAnalytic, + required ProfitLossAnalytic profitLossAnalytic, + required PaymentMethodAnalytic paymentMethodAnalytic, + required ProductAnalytic productAnalytic, + required Option failureOptionAnalytic, + required Outlet outlet, + required Option failureOptionOutlet, + @Default(false) bool isFetching, + @Default(false) bool isFetchingOutlet, + }) = _TransactionReportState; + + factory TransactionReportState.initial() => TransactionReportState( + failureOptionAnalytic: none(), + outlet: Outlet.empty(), + failureOptionOutlet: none(), + categoryAnalytic: CategoryAnalytic.empty(), + profitLossAnalytic: ProfitLossAnalytic.empty(), + paymentMethodAnalytic: PaymentMethodAnalytic.empty(), + productAnalytic: ProductAnalytic.empty(), + ); +} diff --git a/lib/injection.config.dart b/lib/injection.config.dart index 50ea38f..046ae07 100644 --- a/lib/injection.config.dart +++ b/lib/injection.config.dart @@ -43,6 +43,8 @@ import 'package:apskel_owner_flutter/application/product/product_loader/product_ as _i458; import 'package:apskel_owner_flutter/application/report/inventory_report/inventory_report_bloc.dart' as _i346; +import 'package:apskel_owner_flutter/application/report/transaction_report/transaction_report_bloc.dart' + as _i605; import 'package:apskel_owner_flutter/application/user/change_password_form/change_password_form_bloc.dart' as _i1030; import 'package:apskel_owner_flutter/application/user/user_edit_form/user_edit_form_bloc.dart' @@ -265,6 +267,12 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i1030.ChangePasswordFormBloc>( () => _i1030.ChangePasswordFormBloc(gh<_i635.IUserRepository>()), ); + gh.factory<_i605.TransactionReportBloc>( + () => _i605.TransactionReportBloc( + gh<_i477.IAnalyticRepository>(), + gh<_i197.IOutletRepository>(), + ), + ); gh.factory<_i346.InventoryReportBloc>( () => _i346.InventoryReportBloc( gh<_i477.IAnalyticRepository>(), diff --git a/lib/presentation/components/report/transaction_report.dart b/lib/presentation/components/report/transaction_report.dart new file mode 100644 index 0000000..fcc3e82 --- /dev/null +++ b/lib/presentation/components/report/transaction_report.dart @@ -0,0 +1,956 @@ +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:pdf/pdf.dart'; +import 'package:pdf/widgets.dart' as pw; + +import '../../../common/extension/extension.dart'; +import '../../../common/utils/pdf_service.dart'; +import '../../../domain/analytic/analytic.dart'; +import '../../../domain/outlet/outlet.dart'; + +class TransactionReport { + static final primaryColor = PdfColor.fromHex("36175e"); + + static Future previewPdf({ + required Outlet outlet, + required String searchDateFormatted, + required CategoryAnalytic categoryAnalyticData, + required ProfitLossAnalytic profitLossData, + required PaymentMethodAnalytic paymentMethodAnalyticData, + required ProductAnalytic productAnalyticData, + }) async { + final pdf = pw.Document(); + final ByteData dataImage = await rootBundle.load('assets/images/logo.png'); + final Uint8List bytes = dataImage.buffer.asUint8List(); + + final profitLossProductSummary = { + 'totalRevenue': profitLossData.productData.fold( + 0, + (sum, item) => sum + (item.revenue), + ), + 'totalCost': profitLossData.productData.fold( + 0, + (sum, item) => sum + (item.cost), + ), + 'totalGrossProfit': profitLossData.productData.fold( + 0, + (sum, item) => sum + (item.grossProfit), + ), + 'totalQuantity': profitLossData.productData.fold( + 0, + (sum, item) => sum + (item.quantitySold), + ), + }; + + final categorySummary = { + 'totalRevenue': categoryAnalyticData.data.fold( + 0, + (sum, item) => sum + (item.totalRevenue), + ), + 'orderCount': categoryAnalyticData.data.fold( + 0, + (sum, item) => sum + (item.orderCount), + ), + 'productCount': categoryAnalyticData.data.fold( + 0, + (sum, item) => sum + (item.productCount), + ), + 'totalQuantity': categoryAnalyticData.data.fold( + 0, + (sum, item) => sum + (item.totalQuantity), + ), + }; + + final productItemSummary = { + 'totalRevenue': productAnalyticData.data.fold( + 0, + (sum, item) => sum + (item.revenue), + ), + 'orderCount': productAnalyticData.data.fold( + 0, + (sum, item) => sum + (item.orderCount), + ), + 'totalQuantitySold': productAnalyticData.data.fold( + 0, + (sum, item) => sum + (item.quantitySold), + ), + }; + + // Membuat objek Image dari gambar + final image = pw.MemoryImage(bytes); + pdf.addPage( + pw.MultiPage( + pageFormat: PdfPageFormat.a4, + margin: pw.EdgeInsets.zero, + build: (pw.Context context) { + return [ + pw.Container( + padding: pw.EdgeInsets.all(20), + child: pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + // Bagian kiri - Logo dan Info Perusahaan + pw.Row( + crossAxisAlignment: pw.CrossAxisAlignment.center, + children: [ + // Icon/Logo placeholder (bisa diganti dengan gambar logo) + pw.Container( + width: 40, + height: 40, + child: pw.Image(image), + ), + pw.SizedBox(width: 15), + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text( + 'Apskel', + style: pw.TextStyle( + fontSize: 28, + fontWeight: pw.FontWeight.bold, + color: primaryColor, + ), + ), + pw.SizedBox(height: 4), + pw.Text( + outlet.name, + style: pw.TextStyle( + fontSize: 16, + color: PdfColors.grey700, + ), + ), + pw.SizedBox(height: 2), + pw.Text( + outlet.address, + style: pw.TextStyle( + fontSize: 12, + color: PdfColors.grey600, + ), + ), + ], + ), + ], + ), + // Bagian kanan - Info Laporan + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.end, + children: [ + pw.Text( + 'Laporan Transaksi', + style: pw.TextStyle( + fontSize: 24, + fontWeight: pw.FontWeight.bold, + color: PdfColors.grey800, + ), + ), + pw.SizedBox(height: 8), + pw.Text( + searchDateFormatted, + style: pw.TextStyle( + fontSize: 14, + color: PdfColors.grey600, + ), + ), + pw.SizedBox(height: 4), + pw.Text( + 'Laporan', + style: pw.TextStyle( + fontSize: 12, + color: PdfColors.grey500, + ), + ), + ], + ), + ], + ), + ), + pw.Container( + width: double.infinity, + height: 3, + color: primaryColor, + ), + + // Summary + pw.Container( + padding: pw.EdgeInsets.all(20), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + _buildSectionWidget('1. Ringkasan'), + pw.SizedBox(height: 30), + pw.Row( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Expanded( + flex: 1, + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + _buildSummaryItem( + 'Total Penjualan (termasuk rasik)', + (profitLossData.summary.totalRevenue) + .toString() + .currencyFormatRpV2, + ), + _buildSummaryItem( + 'Total Terjual', + (profitLossData.summary.totalOrders).toString(), + ), + _buildSummaryItem( + 'HPP', + '${safeCurrency(profitLossData.summary.totalCost)} | ${safePercentage(profitLossData.summary.totalCost, profitLossData.summary.totalRevenue)}', + ), + _buildSummaryItem( + 'Laba Kotor', + '${safeCurrency(profitLossData.summary.grossProfit)} | ${safeRound(profitLossData.summary.grossProfitMargin)}%', + valueStyle: pw.TextStyle( + color: PdfColors.green800, + fontWeight: pw.FontWeight.bold, + fontSize: 16, + ), + labelStyle: pw.TextStyle( + color: PdfColors.green800, + fontWeight: pw.FontWeight.bold, + fontSize: 16, + ), + ), + ], + ), + ), + pw.SizedBox(width: 20), + pw.Expanded( + flex: 1, + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + _buildSummaryItem( + 'Biaya Lain lain', + '${safeCurrency(profitLossData.summary.totalTax)} | ${safePercentage(profitLossData.summary.totalTax, profitLossData.summary.totalRevenue)}', + ), + _buildSummaryItem( + 'Laba/Rugi', + '${safeCurrency(profitLossData.summary.netProfit)} | ${safeRound(profitLossData.summary.netProfitMargin)}%', + valueStyle: pw.TextStyle( + color: PdfColors.blue800, + fontWeight: pw.FontWeight.bold, + fontSize: 16, + ), + labelStyle: pw.TextStyle( + color: PdfColors.blue800, + fontWeight: pw.FontWeight.bold, + fontSize: 16, + ), + ), + ], + ), + ), + ], + ), + pw.SizedBox(height: 16), + pw.Text( + "Laba Rugi Perproduk", + style: pw.TextStyle( + fontSize: 16, + fontWeight: pw.FontWeight.bold, + color: primaryColor, + ), + ), + pw.SizedBox(height: 20), + pw.Column( + children: [ + pw.Container( + decoration: pw.BoxDecoration( + color: primaryColor, // Purple color + borderRadius: pw.BorderRadius.only( + topLeft: pw.Radius.circular(8), + topRight: pw.Radius.circular(8), + ), + ), + child: pw.Table( + columnWidths: const { + 0: pw.FlexColumnWidth(2.5), // Produk + 1: pw.FlexColumnWidth(1), // Qty + 2: pw.FlexColumnWidth(2.5), // Pendapatan + 3: pw.FlexColumnWidth(2), // HPP + 4: pw.FlexColumnWidth(2), // Laba Kotor + 5: pw.FlexColumnWidth(2), // Margin (%) + }, + children: [ + pw.TableRow( + children: [ + _buildHeaderCell('Produk'), + _buildHeaderCell('Qty'), + _buildHeaderCell('Pendapatan'), + _buildHeaderCell('HPP'), + _buildHeaderCell('Laba Kotor'), + _buildHeaderCell('Margin (%)'), + ], + ), + ], + ), + ), + pw.Container( + decoration: pw.BoxDecoration(color: PdfColors.white), + child: pw.Table( + columnWidths: { + 0: pw.FlexColumnWidth(2.5), // Produk + 1: pw.FlexColumnWidth(1), // Qty + 2: pw.FlexColumnWidth(2.5), // Pendapatan + 3: pw.FlexColumnWidth(2), // HPP + 4: pw.FlexColumnWidth(2), // Laba Kotor + 5: pw.FlexColumnWidth(2), // Margin (%) + }, + children: profitLossData.productData + .map( + (profitLoss) => _buildPerProductDataRow( + product: profitLoss.productName, + qty: profitLoss.quantitySold.toString(), + pendapatan: profitLoss.revenue + .toString() + .currencyFormatRpV2, + hpp: profitLoss.cost + .toString() + .currencyFormatRpV2, + labaKotor: profitLoss.grossProfit + .toString() + .currencyFormatRpV2, + margin: + '${safeRound(profitLoss.grossProfitMargin)}%', + isEven: + profitLossData.productData.indexOf( + profitLoss, + ) % + 2 == + 0, + ), + ) + .toList(), + ), + ), + pw.Container( + decoration: pw.BoxDecoration( + color: primaryColor, // Purple color + borderRadius: pw.BorderRadius.only( + bottomLeft: pw.Radius.circular(8), + bottomRight: pw.Radius.circular(8), + ), + ), + child: pw.Table( + columnWidths: const { + 0: pw.FlexColumnWidth(2.5), // Produk + 1: pw.FlexColumnWidth(1), // Qty + 2: pw.FlexColumnWidth(2.5), // Pendapatan + 3: pw.FlexColumnWidth(2), // HPP + 4: pw.FlexColumnWidth(2), // Laba Kotor + 5: pw.FlexColumnWidth(2), // Margin (%) + }, + children: [ + pw.TableRow( + children: [ + _buildTotalCell('TOTAL'), + _buildTotalCell( + profitLossProductSummary['totalQuantity'] + .toString(), + ), + _buildTotalCell( + profitLossProductSummary['totalRevenue'] + .toString() + .currencyFormatRpV2, + ), + _buildTotalCell( + profitLossProductSummary['totalCost'] + .toString() + .currencyFormatRpV2, + ), + _buildTotalCell( + profitLossProductSummary['totalGrossProfit'] + .toString() + .currencyFormatRpV2, + ), + _buildTotalCell(''), + ], + ), + ], + ), + ), + ], + ), + ], + ), + ), + + // Summary Payment Method + pw.Container( + padding: pw.EdgeInsets.all(20), + child: pw.Column( + children: [ + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + _buildSectionWidget('2. Ringkasan Metode Pembayaran'), + pw.SizedBox(height: 30), + pw.Container( + decoration: pw.BoxDecoration( + color: primaryColor, // Purple color + borderRadius: pw.BorderRadius.only( + topLeft: pw.Radius.circular(8), + topRight: pw.Radius.circular(8), + ), + ), + child: pw.Table( + columnWidths: const { + 0: pw.FlexColumnWidth(2.5), // Nama + 1: pw.FlexColumnWidth(1), // Tipe + 2: pw.FlexColumnWidth(2.5), // Jumlah Order + 3: pw.FlexColumnWidth(2), // Total Amount + 4: pw.FlexColumnWidth(2), // Presentase + }, + children: [ + pw.TableRow( + children: [ + _buildHeaderCell('Nama'), + _buildHeaderCell('Tipe'), + _buildHeaderCell('Jumlah Order'), + _buildHeaderCell('Total Amount'), + _buildHeaderCell('Presentase'), + ], + ), + ], + ), + ), + pw.Container( + decoration: pw.BoxDecoration(color: PdfColors.white), + child: pw.Table( + columnWidths: { + 0: pw.FlexColumnWidth(2.5), // Nama + 1: pw.FlexColumnWidth(1), // Tipe + 2: pw.FlexColumnWidth(2.5), // Jumlah Order + 3: pw.FlexColumnWidth(2), // Total Amount + 4: pw.FlexColumnWidth(2), // Presentase + }, + children: paymentMethodAnalyticData.data + .map( + (payment) => _buildPaymentMethodDataRow( + name: payment.paymentMethodName, + tipe: payment.paymentMethodType.toTitleCase, + jumlahOrder: payment.orderCount.toString(), + totalAmount: payment.totalAmount + .toString() + .currencyFormatRpV2, + presentase: + '${safeRound(payment.percentage)}%', + isEven: + paymentMethodAnalyticData.data.indexOf( + payment, + ) % + 2 == + 0, + ), + ) + .toList(), + ), + ), + pw.Container( + decoration: pw.BoxDecoration( + color: primaryColor, // Purple color + borderRadius: pw.BorderRadius.only( + bottomLeft: pw.Radius.circular(8), + bottomRight: pw.Radius.circular(8), + ), + ), + child: pw.Table( + columnWidths: const { + 0: pw.FlexColumnWidth(2.5), // Produk + 1: pw.FlexColumnWidth(1), // Qty + 2: pw.FlexColumnWidth(2.5), // Pendapatan + 3: pw.FlexColumnWidth(2), // HPP + 4: pw.FlexColumnWidth(2), // Laba Kotor + 5: pw.FlexColumnWidth(2), // Margin (%) + }, + children: [ + pw.TableRow( + children: [ + _buildTotalCell('TOTAL'), + _buildTotalCell(''), + _buildTotalCell( + (paymentMethodAnalyticData + .summary + .totalOrders) + .toString(), + ), + _buildTotalCell( + (paymentMethodAnalyticData + .summary + .totalAmount) + .toString() + .currencyFormatRpV2, + ), + _buildTotalCell(''), + ], + ), + ], + ), + ), + ], + ), + ], + ), + ), + + // Summary Category + pw.Container( + padding: pw.EdgeInsets.all(20), + child: pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + _buildSectionWidget('3. Ringkasan Kategori'), + pw.SizedBox(height: 30), + pw.Column( + children: [ + pw.Container( + decoration: pw.BoxDecoration( + color: primaryColor, // Purple color + borderRadius: pw.BorderRadius.only( + topLeft: pw.Radius.circular(8), + topRight: pw.Radius.circular(8), + ), + ), + child: pw.Table( + columnWidths: const { + 0: pw.FlexColumnWidth(2.5), // Nama + 1: pw.FlexColumnWidth(2), // Total Product + 2: pw.FlexColumnWidth(1), // qty + 3: pw.FlexColumnWidth(2), // Jumlah Order + 4: pw.FlexColumnWidth(2.5), // Presentase + }, + children: [ + pw.TableRow( + children: [ + _buildHeaderCell('Nama'), + _buildHeaderCell('Total Produk'), + _buildHeaderCell('Qty'), + _buildHeaderCell('Jumlah Order'), + _buildHeaderCell('Pendapatan'), + ], + ), + ], + ), + ), + pw.Container( + decoration: pw.BoxDecoration(color: PdfColors.white), + child: pw.Table( + columnWidths: { + 0: pw.FlexColumnWidth(2.5), // Nama + 1: pw.FlexColumnWidth(2), // Total Product + 2: pw.FlexColumnWidth(1), // qty + 3: pw.FlexColumnWidth(2), // Jumlah Order + 4: pw.FlexColumnWidth(2.5), // Presentase + }, + children: categoryAnalyticData.data + .map( + (category) => _buildCategoryDataRow( + name: category.categoryName, + totalProduct: category.productCount + .toString(), + qty: category.totalQuantity.toString(), + jumlahOrder: category.orderCount.toString(), + pendapatan: category.totalRevenue + .toString() + .currencyFormatRpV2, + isEven: + categoryAnalyticData.data.indexOf( + category, + ) % + 2 == + 0, + ), + ) + .toList(), + ), + ), + pw.Container( + decoration: pw.BoxDecoration( + color: primaryColor, // Purple color + borderRadius: pw.BorderRadius.only( + bottomLeft: pw.Radius.circular(8), + bottomRight: pw.Radius.circular(8), + ), + ), + child: pw.Table( + columnWidths: const { + 0: pw.FlexColumnWidth(2.5), // Nama + 1: pw.FlexColumnWidth(2), // Total Product + 2: pw.FlexColumnWidth(1), // qty + 3: pw.FlexColumnWidth(2), // Jumlah Order + 4: pw.FlexColumnWidth(2.5), // Presentase + }, + children: [ + pw.TableRow( + children: [ + _buildTotalCell('TOTAL'), + _buildTotalCell( + categorySummary['productCount'].toString(), + ), + _buildTotalCell( + categorySummary['totalQuantity'].toString(), + ), + _buildTotalCell( + categorySummary['orderCount'].toString(), + ), + _buildTotalCell( + categorySummary['totalRevenue'] + .toString() + .currencyFormatRpV2, + ), + ], + ), + ], + ), + ), + ], + ), + ], + ), + ), + + // Summary Item + pw.Container( + padding: pw.EdgeInsets.all(20), + child: pw.Column( + children: [ + pw.Column( + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + _buildSectionWidget('4. Ringkasan Item'), + pw.SizedBox(height: 30), + pw.Container( + decoration: pw.BoxDecoration( + color: primaryColor, // Purple color + borderRadius: pw.BorderRadius.only( + topLeft: pw.Radius.circular(8), + topRight: pw.Radius.circular(8), + ), + ), + child: pw.Table( + columnWidths: const { + 0: pw.FlexColumnWidth(2.5), // Produk + 1: pw.FlexColumnWidth(2), // Kategori + 2: pw.FlexColumnWidth(1), // qty + 3: pw.FlexColumnWidth(2), // Order + 4: pw.FlexColumnWidth(2), // Pendapatan + 5: pw.FlexColumnWidth(2), // Average + }, + children: [ + pw.TableRow( + children: [ + _buildHeaderCell('Produk'), + _buildHeaderCell('Kategori'), + _buildHeaderCell('Qty'), + _buildHeaderCell('Order'), + _buildHeaderCell('Pendapatan'), + _buildHeaderCell('Rata Rata'), + ], + ), + ], + ), + ), + pw.Container( + decoration: pw.BoxDecoration(color: PdfColors.white), + child: pw.Table( + columnWidths: { + 0: pw.FlexColumnWidth(2.5), // Produk + 1: pw.FlexColumnWidth(2), // Kategori + 2: pw.FlexColumnWidth(1), // qty + 3: pw.FlexColumnWidth(2), // Order + 4: pw.FlexColumnWidth(2), // Pendapatan + 5: pw.FlexColumnWidth(2), // Average + }, + children: productAnalyticData.data + .map( + (item) => _buildItemDataRow( + product: item.productName, + category: item.categoryName, + qty: item.quantitySold.toString(), + order: item.orderCount.toString(), + pendapatan: item.revenue + .toString() + .currencyFormatRpV2, + average: safeCurrency( + item.averagePrice.round(), + ), + isEven: + productAnalyticData.data.indexOf(item) % + 2 == + 0, + ), + ) + .toList(), + ), + ), + pw.Container( + decoration: pw.BoxDecoration( + color: primaryColor, // Purple color + borderRadius: pw.BorderRadius.only( + bottomLeft: pw.Radius.circular(8), + bottomRight: pw.Radius.circular(8), + ), + ), + child: pw.Table( + columnWidths: const { + 0: pw.FlexColumnWidth(2.5), // Produk + 1: pw.FlexColumnWidth(2), // Kategori + 2: pw.FlexColumnWidth(1), // qty + 3: pw.FlexColumnWidth(2), // Order + 4: pw.FlexColumnWidth(2), // Pendapatan + 5: pw.FlexColumnWidth(2), // Average + }, + children: [ + pw.TableRow( + children: [ + _buildTotalCell('TOTAL'), + _buildTotalCell(''), + _buildTotalCell( + productItemSummary['totalQuantitySold'] + .toString(), + ), + _buildTotalCell( + productItemSummary['orderCount'].toString(), + ), + _buildTotalCell( + productItemSummary['totalRevenue'] + .toString() + .currencyFormatRpV2, + ), + _buildTotalCell(''), + ], + ), + ], + ), + ), + ], + ), + ], + ), + ), + ]; + }, + ), + ); + + return HelperPdfService.saveDocument( + name: 'Laporan Transaksi | $searchDateFormatted.pdf', + pdf: pdf, + ); + } + + static String safePercentage(num numerator, num denominator) { + if (denominator == 0 || + numerator.isInfinite || + numerator.isNaN || + denominator.isInfinite || + denominator.isNaN) { + return '0%'; + } + final result = (numerator / denominator) * 100; + if (result.isInfinite || result.isNaN) { + return '0%'; + } + return '${result.round()}%'; + } + + static String safeRound(num value) { + if (value.isInfinite || value.isNaN) { + return '0'; + } + return value.round().toString(); + } + + static String safeCurrency(num value) { + if (value.isInfinite || value.isNaN) { + return '0'.currencyFormatRpV2; + } + return value.toString().currencyFormatRpV2; + } + + static pw.Widget _buildSectionWidget(String title) { + return pw.Text( + title, + style: pw.TextStyle( + fontSize: 20, + fontWeight: pw.FontWeight.bold, + color: primaryColor, + ), + ); + } + + static pw.Widget _buildSummaryItem( + String label, + String value, { + pw.TextStyle? valueStyle, + pw.TextStyle? labelStyle, + }) { + return pw.Container( + padding: pw.EdgeInsets.only(bottom: 8), + margin: pw.EdgeInsets.only(bottom: 16), + decoration: pw.BoxDecoration( + border: pw.Border(bottom: pw.BorderSide(color: PdfColors.grey300)), + ), + child: pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text(label, style: labelStyle), + pw.Text( + value, + style: valueStyle ?? pw.TextStyle(fontWeight: pw.FontWeight.bold), + ), + ], + ), + ); + } + + static pw.Widget _buildHeaderCell(String text) { + return pw.Container( + padding: pw.EdgeInsets.symmetric(horizontal: 12, vertical: 16), + child: pw.Text( + text, + style: pw.TextStyle( + color: PdfColors.white, + fontWeight: pw.FontWeight.bold, + fontSize: 12, + ), + textAlign: pw.TextAlign.center, + ), + ); + } + + static pw.Widget _buildDataCell( + String text, { + pw.Alignment alignment = pw.Alignment.center, + PdfColor? textColor, + }) { + return pw.Container( + padding: pw.EdgeInsets.symmetric(horizontal: 12, vertical: 16), + alignment: alignment, + child: pw.Text( + text, + style: pw.TextStyle( + fontSize: 12, + color: textColor ?? PdfColors.black, + fontWeight: pw.FontWeight.normal, + ), + textAlign: alignment == pw.Alignment.centerLeft + ? pw.TextAlign.left + : pw.TextAlign.center, + ), + ); + } + + static pw.Widget _buildTotalCell(String text) { + return pw.Container( + padding: pw.EdgeInsets.symmetric(horizontal: 12, vertical: 16), + child: pw.Text( + text, + style: pw.TextStyle( + color: PdfColors.white, + fontWeight: pw.FontWeight.bold, + fontSize: 12, + ), + textAlign: pw.TextAlign.center, + ), + ); + } + + static pw.TableRow _buildPerProductDataRow({ + required String product, + required String qty, + required String pendapatan, + required String hpp, + required String labaKotor, + required String margin, + required bool isEven, + }) { + return pw.TableRow( + decoration: pw.BoxDecoration( + color: isEven ? PdfColors.grey50 : PdfColors.white, + ), + children: [ + _buildDataCell(product, alignment: pw.Alignment.centerLeft), + _buildDataCell(qty), + _buildDataCell(pendapatan), + _buildDataCell(hpp, textColor: PdfColors.red600), + _buildDataCell(labaKotor, textColor: PdfColors.green600), + _buildDataCell(margin), + ], + ); + } + + static pw.TableRow _buildPaymentMethodDataRow({ + required String name, + required String tipe, + required String jumlahOrder, + required String totalAmount, + required String presentase, + required bool isEven, + }) { + return pw.TableRow( + decoration: pw.BoxDecoration( + color: isEven ? PdfColors.grey50 : PdfColors.white, + ), + children: [ + _buildDataCell(name, alignment: pw.Alignment.centerLeft), + _buildDataCell(tipe), + _buildDataCell(jumlahOrder), + _buildDataCell(totalAmount), + _buildDataCell(presentase), + ], + ); + } + + static pw.TableRow _buildCategoryDataRow({ + required String name, + required String totalProduct, + required String qty, + required String jumlahOrder, + required String pendapatan, + required bool isEven, + }) { + return pw.TableRow( + decoration: pw.BoxDecoration( + color: isEven ? PdfColors.grey50 : PdfColors.white, + ), + children: [ + _buildDataCell(name, alignment: pw.Alignment.centerLeft), + _buildDataCell(totalProduct), + _buildDataCell(qty), + _buildDataCell(jumlahOrder), + _buildDataCell(pendapatan), + ], + ); + } + + static pw.TableRow _buildItemDataRow({ + required String product, + required String category, + required String qty, + required String order, + required String pendapatan, + required String average, + required bool isEven, + }) { + return pw.TableRow( + decoration: pw.BoxDecoration( + color: isEven ? PdfColors.grey50 : PdfColors.white, + ), + children: [ + _buildDataCell(product, alignment: pw.Alignment.centerLeft), + _buildDataCell(category, alignment: pw.Alignment.centerLeft), + _buildDataCell(qty), + _buildDataCell(order), + _buildDataCell(pendapatan), + _buildDataCell(average), + ], + ); + } +} diff --git a/lib/presentation/pages/download/download_report_page.dart b/lib/presentation/pages/download/download_report_page.dart index ee111dc..3e02303 100644 --- a/lib/presentation/pages/download/download_report_page.dart +++ b/lib/presentation/pages/download/download_report_page.dart @@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import '../../../application/report/inventory_report/inventory_report_bloc.dart'; +import '../../../application/report/transaction_report/transaction_report_bloc.dart'; import '../../../common/extension/extension.dart'; import '../../../common/theme/theme.dart'; import '../../../common/utils/pdf_service.dart'; @@ -14,6 +15,7 @@ import '../../../injection.dart'; import '../../components/appbar/appbar.dart'; import '../../components/field/date_range_picker_field.dart'; import '../../components/report/inventory_report.dart'; +import '../../components/report/transaction_report.dart'; import '../../components/toast/flushbar.dart'; @RoutePage() @@ -24,9 +26,19 @@ class DownloadReportPage extends StatefulWidget implements AutoRouteWrapper { State createState() => _DownloadReportPageState(); @override - Widget wrappedRoute(BuildContext context) => BlocProvider( - create: (context) => - getIt()..add(InventoryReportEvent.fetchedOutlet()), + Widget wrappedRoute(BuildContext context) => MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => + getIt() + ..add(InventoryReportEvent.fetchedOutlet()), + ), + BlocProvider( + create: (context) => + getIt() + ..add(TransactionReportEvent.fetchedOutlet()), + ), + ], child: this, ); } @@ -88,17 +100,6 @@ class _DownloadReportPageState extends State super.dispose(); } - void _downloadReport( - String reportType, - DateTime? startDate, - DateTime? endDate, - ) { - if (startDate == null || endDate == null) { - AppFlushbar.showError(context, 'Please select both start and end dates'); - return; - } - } - @override Widget build(BuildContext context) { return Scaffold( @@ -127,30 +128,75 @@ class _DownloadReportPageState extends State child: Column( children: [ // Transaction Report Card - _ReportOptionCard( - title: 'Transaction Report', - subtitle: - 'Export all transaction data with detailed analytics', - icon: Icons.receipt_long_outlined, - gradient: const [ - AppColor.primary, - AppColor.primaryLight, - ], - startDate: _transactionStartDate, - endDate: _transactionEndDate, - isLoading: false, - onDateRangeChanged: (start, end) { - setState(() { - _transactionStartDate = start; - _transactionEndDate = end; - }); + BlocBuilder< + TransactionReportBloc, + TransactionReportState + >( + builder: (context, state) { + return _ReportOptionCard( + title: 'Transaction Report', + subtitle: + 'Export all transaction data with detailed analytics', + icon: Icons.receipt_long_outlined, + gradient: const [ + AppColor.primary, + AppColor.primaryLight, + ], + startDate: _transactionStartDate, + endDate: _transactionEndDate, + isLoading: state.isFetching, + onDateRangeChanged: (start, end) { + setState(() { + _transactionStartDate = start; + _transactionEndDate = end; + }); + if (start != null || end != null) { + context.read().add( + TransactionReportEvent.fetchedTransaction( + start!, + end!, + ), + ); + } + }, + onDownload: () async { + try { + final status = await PermessionHelper() + .checkPermission(); + if (status) { + final pdfFile = + await TransactionReport.previewPdf( + searchDateFormatted: + "${_transactionStartDate?.toServerDate} - ${_transactionEndDate?.toServerDate}", + outlet: state.outlet, + categoryAnalyticData: + state.categoryAnalytic, + profitLossData: + state.profitLossAnalytic, + paymentMethodAnalyticData: + state.paymentMethodAnalytic, + productAnalyticData: + state.productAnalytic, + ); + log("pdfFile: $pdfFile"); + await HelperPdfService.openFile(pdfFile); + } else { + AppFlushbar.showError( + context, + 'Storage permission is required to save PDF', + ); + } + } catch (e) { + log("Error generating PDF: $e"); + AppFlushbar.showError( + context, + 'Failed to generate PDF: $e', + ); + } + }, + delay: 200, + ); }, - onDownload: () => _downloadReport( - 'Transaction Report', - _transactionStartDate, - _transactionEndDate, - ), - delay: 200, ), const SizedBox(height: 20), diff --git a/lib/presentation/router/app_router.gr.dart b/lib/presentation/router/app_router.gr.dart index 9d4c8bb..00f4aaf 100644 --- a/lib/presentation/router/app_router.gr.dart +++ b/lib/presentation/router/app_router.gr.dart @@ -139,7 +139,7 @@ class DownloadReportRoute extends _i26.PageRouteInfo { static _i26.PageInfo page = _i26.PageInfo( name, builder: (data) { - return const _i5.DownloadReportPage(); + return _i26.WrappedRoute(child: const _i5.DownloadReportPage()); }, ); }