feat: transaction report

This commit is contained in:
efrilm 2025-08-19 17:39:00 +07:00
parent 1aa65d1732
commit f07d07b3a8
8 changed files with 1927 additions and 38 deletions

View File

@ -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<TransactionReportEvent, TransactionReportState> {
final IAnalyticRepository _analyticRepository;
final IOutletRepository _outletRepository;
TransactionReportBloc(this._analyticRepository, this._outletRepository)
: super(TransactionReportState.initial()) {
on<TransactionReportEvent>(_onTransactionReportEvent);
}
Future<void> _onTransactionReportEvent(
TransactionReportEvent event,
Emitter<TransactionReportState> 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));
},
);
}
}

View File

@ -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>(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<TResult extends Object?>({
required TResult Function() fetchedOutlet,
required TResult Function(DateTime dateFrom, DateTime dateTo)
fetchedTransaction,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? fetchedOutlet,
TResult? Function(DateTime dateFrom, DateTime dateTo)? fetchedTransaction,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? fetchedOutlet,
TResult Function(DateTime dateFrom, DateTime dateTo)? fetchedTransaction,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_FetchedOutlet value) fetchedOutlet,
required TResult Function(_FetchedTransaction value) fetchedTransaction,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_FetchedOutlet value)? fetchedOutlet,
TResult? Function(_FetchedTransaction value)? fetchedTransaction,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
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<TResult extends Object?>({
required TResult Function() fetchedOutlet,
required TResult Function(DateTime dateFrom, DateTime dateTo)
fetchedTransaction,
}) {
return fetchedOutlet();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? fetchedOutlet,
TResult? Function(DateTime dateFrom, DateTime dateTo)? fetchedTransaction,
}) {
return fetchedOutlet?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? fetchedOutlet,
TResult Function(DateTime dateFrom, DateTime dateTo)? fetchedTransaction,
required TResult orElse(),
}) {
if (fetchedOutlet != null) {
return fetchedOutlet();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_FetchedOutlet value) fetchedOutlet,
required TResult Function(_FetchedTransaction value) fetchedTransaction,
}) {
return fetchedOutlet(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_FetchedOutlet value)? fetchedOutlet,
TResult? Function(_FetchedTransaction value)? fetchedTransaction,
}) {
return fetchedOutlet?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
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<TResult extends Object?>({
required TResult Function() fetchedOutlet,
required TResult Function(DateTime dateFrom, DateTime dateTo)
fetchedTransaction,
}) {
return fetchedTransaction(dateFrom, dateTo);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? fetchedOutlet,
TResult? Function(DateTime dateFrom, DateTime dateTo)? fetchedTransaction,
}) {
return fetchedTransaction?.call(dateFrom, dateTo);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
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<TResult extends Object?>({
required TResult Function(_FetchedOutlet value) fetchedOutlet,
required TResult Function(_FetchedTransaction value) fetchedTransaction,
}) {
return fetchedTransaction(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_FetchedOutlet value)? fetchedOutlet,
TResult? Function(_FetchedTransaction value)? fetchedTransaction,
}) {
return fetchedTransaction?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
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<AnalyticFailure> get failureOptionAnalytic =>
throw _privateConstructorUsedError;
Outlet get outlet => throw _privateConstructorUsedError;
Option<OutletFailure> 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<TransactionReportState> 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<AnalyticFailure> failureOptionAnalytic,
Outlet outlet,
Option<OutletFailure> 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<AnalyticFailure>,
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<OutletFailure>,
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<AnalyticFailure> failureOptionAnalytic,
Outlet outlet,
Option<OutletFailure> 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<AnalyticFailure>,
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<OutletFailure>,
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<AnalyticFailure> failureOptionAnalytic;
@override
final Outlet outlet;
@override
final Option<OutletFailure> 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<AnalyticFailure> failureOptionAnalytic,
required final Outlet outlet,
required final Option<OutletFailure> 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<AnalyticFailure> get failureOptionAnalytic;
@override
Outlet get outlet;
@override
Option<OutletFailure> 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;
}

View File

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

View File

@ -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<AnalyticFailure> failureOptionAnalytic,
required Outlet outlet,
required Option<OutletFailure> 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(),
);
}

View File

@ -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>(),

View File

@ -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<File> 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<num>(
0,
(sum, item) => sum + (item.revenue),
),
'totalCost': profitLossData.productData.fold<num>(
0,
(sum, item) => sum + (item.cost),
),
'totalGrossProfit': profitLossData.productData.fold<num>(
0,
(sum, item) => sum + (item.grossProfit),
),
'totalQuantity': profitLossData.productData.fold<num>(
0,
(sum, item) => sum + (item.quantitySold),
),
};
final categorySummary = {
'totalRevenue': categoryAnalyticData.data.fold<num>(
0,
(sum, item) => sum + (item.totalRevenue),
),
'orderCount': categoryAnalyticData.data.fold<num>(
0,
(sum, item) => sum + (item.orderCount),
),
'productCount': categoryAnalyticData.data.fold<num>(
0,
(sum, item) => sum + (item.productCount),
),
'totalQuantity': categoryAnalyticData.data.fold<num>(
0,
(sum, item) => sum + (item.totalQuantity),
),
};
final productItemSummary = {
'totalRevenue': productAnalyticData.data.fold<num>(
0,
(sum, item) => sum + (item.revenue),
),
'orderCount': productAnalyticData.data.fold<num>(
0,
(sum, item) => sum + (item.orderCount),
),
'totalQuantitySold': productAnalyticData.data.fold<num>(
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),
],
);
}
}

View File

@ -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<DownloadReportPage> createState() => _DownloadReportPageState();
@override
Widget wrappedRoute(BuildContext context) => BlocProvider(
Widget wrappedRoute(BuildContext context) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) =>
getIt<InventoryReportBloc>()..add(InventoryReportEvent.fetchedOutlet()),
getIt<InventoryReportBloc>()
..add(InventoryReportEvent.fetchedOutlet()),
),
BlocProvider(
create: (context) =>
getIt<TransactionReportBloc>()
..add(TransactionReportEvent.fetchedOutlet()),
),
],
child: this,
);
}
@ -88,17 +100,6 @@ class _DownloadReportPageState extends State<DownloadReportPage>
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,7 +128,12 @@ class _DownloadReportPageState extends State<DownloadReportPage>
child: Column(
children: [
// Transaction Report Card
_ReportOptionCard(
BlocBuilder<
TransactionReportBloc,
TransactionReportState
>(
builder: (context, state) {
return _ReportOptionCard(
title: 'Transaction Report',
subtitle:
'Export all transaction data with detailed analytics',
@ -138,19 +144,59 @@ class _DownloadReportPageState extends State<DownloadReportPage>
],
startDate: _transactionStartDate,
endDate: _transactionEndDate,
isLoading: false,
isLoading: state.isFetching,
onDateRangeChanged: (start, end) {
setState(() {
_transactionStartDate = start;
_transactionEndDate = end;
});
},
onDownload: () => _downloadReport(
'Transaction Report',
_transactionStartDate,
_transactionEndDate,
if (start != null || end != null) {
context.read<TransactionReportBloc>().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,
);
},
),
const SizedBox(height: 20),

View File

@ -139,7 +139,7 @@ class DownloadReportRoute extends _i26.PageRouteInfo<void> {
static _i26.PageInfo page = _i26.PageInfo(
name,
builder: (data) {
return const _i5.DownloadReportPage();
return _i26.WrappedRoute(child: const _i5.DownloadReportPage());
},
);
}