report profit loss
This commit is contained in:
parent
5a7892aa99
commit
07932d688f
@ -0,0 +1,52 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import '../../../domain/analytic/analytic.dart';
|
||||
|
||||
part 'profit_loss_analytic_loader_event.dart';
|
||||
part 'profit_loss_analytic_loader_state.dart';
|
||||
part 'profit_loss_analytic_loader_bloc.freezed.dart';
|
||||
|
||||
@injectable
|
||||
class ProfitLossAnalyticLoaderBloc
|
||||
extends Bloc<ProfitLossAnalyticLoaderEvent, ProfitLossAnalyticLoaderState> {
|
||||
final IAnalyticRepository _analyticRepository;
|
||||
ProfitLossAnalyticLoaderBloc(this._analyticRepository)
|
||||
: super(ProfitLossAnalyticLoaderState.initial()) {
|
||||
on<ProfitLossAnalyticLoaderEvent>(_onProfitLossAnalyticLoaderEvent);
|
||||
}
|
||||
|
||||
Future<void> _onProfitLossAnalyticLoaderEvent(
|
||||
ProfitLossAnalyticLoaderEvent event,
|
||||
Emitter<ProfitLossAnalyticLoaderState> emit,
|
||||
) {
|
||||
return event.map(
|
||||
fetched: (e) async {
|
||||
emit(state.copyWith(isFetching: true, failureOption: none()));
|
||||
|
||||
final result = await _analyticRepository.getProfitLoss(
|
||||
dateFrom: e.startDate,
|
||||
dateTo: e.endDate,
|
||||
);
|
||||
|
||||
await result.fold(
|
||||
(failure) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isFetching: false,
|
||||
failureOption: optionOf(failure),
|
||||
),
|
||||
);
|
||||
},
|
||||
(profitLoss) async {
|
||||
emit(
|
||||
state.copyWith(isFetching: false, profitLossAnalytic: profitLoss),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,483 @@
|
||||
// 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 'profit_loss_analytic_loader_bloc.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
|
||||
);
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ProfitLossAnalyticLoaderEvent {
|
||||
DateTime get startDate => throw _privateConstructorUsedError;
|
||||
DateTime get endDate => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(DateTime startDate, DateTime endDate) fetched,
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function(DateTime startDate, DateTime endDate)? fetched,
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(DateTime startDate, DateTime endDate)? fetched,
|
||||
required TResult orElse(),
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(_Fetched value) fetched,
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult? Function(_Fetched value)? fetched,
|
||||
}) => throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(_Fetched value)? fetched,
|
||||
required TResult orElse(),
|
||||
}) => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of ProfitLossAnalyticLoaderEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$ProfitLossAnalyticLoaderEventCopyWith<ProfitLossAnalyticLoaderEvent>
|
||||
get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $ProfitLossAnalyticLoaderEventCopyWith<$Res> {
|
||||
factory $ProfitLossAnalyticLoaderEventCopyWith(
|
||||
ProfitLossAnalyticLoaderEvent value,
|
||||
$Res Function(ProfitLossAnalyticLoaderEvent) then,
|
||||
) =
|
||||
_$ProfitLossAnalyticLoaderEventCopyWithImpl<
|
||||
$Res,
|
||||
ProfitLossAnalyticLoaderEvent
|
||||
>;
|
||||
@useResult
|
||||
$Res call({DateTime startDate, DateTime endDate});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$ProfitLossAnalyticLoaderEventCopyWithImpl<
|
||||
$Res,
|
||||
$Val extends ProfitLossAnalyticLoaderEvent
|
||||
>
|
||||
implements $ProfitLossAnalyticLoaderEventCopyWith<$Res> {
|
||||
_$ProfitLossAnalyticLoaderEventCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of ProfitLossAnalyticLoaderEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({Object? startDate = null, Object? endDate = null}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
startDate: null == startDate
|
||||
? _value.startDate
|
||||
: startDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
endDate: null == endDate
|
||||
? _value.endDate
|
||||
: endDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$FetchedImplCopyWith<$Res>
|
||||
implements $ProfitLossAnalyticLoaderEventCopyWith<$Res> {
|
||||
factory _$$FetchedImplCopyWith(
|
||||
_$FetchedImpl value,
|
||||
$Res Function(_$FetchedImpl) then,
|
||||
) = __$$FetchedImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({DateTime startDate, DateTime endDate});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$FetchedImplCopyWithImpl<$Res>
|
||||
extends _$ProfitLossAnalyticLoaderEventCopyWithImpl<$Res, _$FetchedImpl>
|
||||
implements _$$FetchedImplCopyWith<$Res> {
|
||||
__$$FetchedImplCopyWithImpl(
|
||||
_$FetchedImpl _value,
|
||||
$Res Function(_$FetchedImpl) _then,
|
||||
) : super(_value, _then);
|
||||
|
||||
/// Create a copy of ProfitLossAnalyticLoaderEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({Object? startDate = null, Object? endDate = null}) {
|
||||
return _then(
|
||||
_$FetchedImpl(
|
||||
startDate: null == startDate
|
||||
? _value.startDate
|
||||
: startDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
endDate: null == endDate
|
||||
? _value.endDate
|
||||
: endDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$FetchedImpl implements _Fetched {
|
||||
const _$FetchedImpl({required this.startDate, required this.endDate});
|
||||
|
||||
@override
|
||||
final DateTime startDate;
|
||||
@override
|
||||
final DateTime endDate;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ProfitLossAnalyticLoaderEvent.fetched(startDate: $startDate, endDate: $endDate)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$FetchedImpl &&
|
||||
(identical(other.startDate, startDate) ||
|
||||
other.startDate == startDate) &&
|
||||
(identical(other.endDate, endDate) || other.endDate == endDate));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, startDate, endDate);
|
||||
|
||||
/// Create a copy of ProfitLossAnalyticLoaderEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$FetchedImplCopyWith<_$FetchedImpl> get copyWith =>
|
||||
__$$FetchedImplCopyWithImpl<_$FetchedImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function(DateTime startDate, DateTime endDate) fetched,
|
||||
}) {
|
||||
return fetched(startDate, endDate);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function(DateTime startDate, DateTime endDate)? fetched,
|
||||
}) {
|
||||
return fetched?.call(startDate, endDate);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function(DateTime startDate, DateTime endDate)? fetched,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (fetched != null) {
|
||||
return fetched(startDate, endDate);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult map<TResult extends Object?>({
|
||||
required TResult Function(_Fetched value) fetched,
|
||||
}) {
|
||||
return fetched(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult? mapOrNull<TResult extends Object?>({
|
||||
TResult? Function(_Fetched value)? fetched,
|
||||
}) {
|
||||
return fetched?.call(this);
|
||||
}
|
||||
|
||||
@override
|
||||
@optionalTypeArgs
|
||||
TResult maybeMap<TResult extends Object?>({
|
||||
TResult Function(_Fetched value)? fetched,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (fetched != null) {
|
||||
return fetched(this);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _Fetched implements ProfitLossAnalyticLoaderEvent {
|
||||
const factory _Fetched({
|
||||
required final DateTime startDate,
|
||||
required final DateTime endDate,
|
||||
}) = _$FetchedImpl;
|
||||
|
||||
@override
|
||||
DateTime get startDate;
|
||||
@override
|
||||
DateTime get endDate;
|
||||
|
||||
/// Create a copy of ProfitLossAnalyticLoaderEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$FetchedImplCopyWith<_$FetchedImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ProfitLossAnalyticLoaderState {
|
||||
ProfitLossAnalytic get profitLossAnalytic =>
|
||||
throw _privateConstructorUsedError;
|
||||
Option<AnalyticFailure> get failureOption =>
|
||||
throw _privateConstructorUsedError;
|
||||
bool get isFetching => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of ProfitLossAnalyticLoaderState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$ProfitLossAnalyticLoaderStateCopyWith<ProfitLossAnalyticLoaderState>
|
||||
get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $ProfitLossAnalyticLoaderStateCopyWith<$Res> {
|
||||
factory $ProfitLossAnalyticLoaderStateCopyWith(
|
||||
ProfitLossAnalyticLoaderState value,
|
||||
$Res Function(ProfitLossAnalyticLoaderState) then,
|
||||
) =
|
||||
_$ProfitLossAnalyticLoaderStateCopyWithImpl<
|
||||
$Res,
|
||||
ProfitLossAnalyticLoaderState
|
||||
>;
|
||||
@useResult
|
||||
$Res call({
|
||||
ProfitLossAnalytic profitLossAnalytic,
|
||||
Option<AnalyticFailure> failureOption,
|
||||
bool isFetching,
|
||||
});
|
||||
|
||||
$ProfitLossAnalyticCopyWith<$Res> get profitLossAnalytic;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$ProfitLossAnalyticLoaderStateCopyWithImpl<
|
||||
$Res,
|
||||
$Val extends ProfitLossAnalyticLoaderState
|
||||
>
|
||||
implements $ProfitLossAnalyticLoaderStateCopyWith<$Res> {
|
||||
_$ProfitLossAnalyticLoaderStateCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of ProfitLossAnalyticLoaderState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? profitLossAnalytic = null,
|
||||
Object? failureOption = null,
|
||||
Object? isFetching = null,
|
||||
}) {
|
||||
return _then(
|
||||
_value.copyWith(
|
||||
profitLossAnalytic: null == profitLossAnalytic
|
||||
? _value.profitLossAnalytic
|
||||
: profitLossAnalytic // ignore: cast_nullable_to_non_nullable
|
||||
as ProfitLossAnalytic,
|
||||
failureOption: null == failureOption
|
||||
? _value.failureOption
|
||||
: failureOption // ignore: cast_nullable_to_non_nullable
|
||||
as Option<AnalyticFailure>,
|
||||
isFetching: null == isFetching
|
||||
? _value.isFetching
|
||||
: isFetching // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
)
|
||||
as $Val,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a copy of ProfitLossAnalyticLoaderState
|
||||
/// 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$ProfitLossAnalyticLoaderStateImplCopyWith<$Res>
|
||||
implements $ProfitLossAnalyticLoaderStateCopyWith<$Res> {
|
||||
factory _$$ProfitLossAnalyticLoaderStateImplCopyWith(
|
||||
_$ProfitLossAnalyticLoaderStateImpl value,
|
||||
$Res Function(_$ProfitLossAnalyticLoaderStateImpl) then,
|
||||
) = __$$ProfitLossAnalyticLoaderStateImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({
|
||||
ProfitLossAnalytic profitLossAnalytic,
|
||||
Option<AnalyticFailure> failureOption,
|
||||
bool isFetching,
|
||||
});
|
||||
|
||||
@override
|
||||
$ProfitLossAnalyticCopyWith<$Res> get profitLossAnalytic;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$ProfitLossAnalyticLoaderStateImplCopyWithImpl<$Res>
|
||||
extends
|
||||
_$ProfitLossAnalyticLoaderStateCopyWithImpl<
|
||||
$Res,
|
||||
_$ProfitLossAnalyticLoaderStateImpl
|
||||
>
|
||||
implements _$$ProfitLossAnalyticLoaderStateImplCopyWith<$Res> {
|
||||
__$$ProfitLossAnalyticLoaderStateImplCopyWithImpl(
|
||||
_$ProfitLossAnalyticLoaderStateImpl _value,
|
||||
$Res Function(_$ProfitLossAnalyticLoaderStateImpl) _then,
|
||||
) : super(_value, _then);
|
||||
|
||||
/// Create a copy of ProfitLossAnalyticLoaderState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? profitLossAnalytic = null,
|
||||
Object? failureOption = null,
|
||||
Object? isFetching = null,
|
||||
}) {
|
||||
return _then(
|
||||
_$ProfitLossAnalyticLoaderStateImpl(
|
||||
profitLossAnalytic: null == profitLossAnalytic
|
||||
? _value.profitLossAnalytic
|
||||
: profitLossAnalytic // ignore: cast_nullable_to_non_nullable
|
||||
as ProfitLossAnalytic,
|
||||
failureOption: null == failureOption
|
||||
? _value.failureOption
|
||||
: failureOption // ignore: cast_nullable_to_non_nullable
|
||||
as Option<AnalyticFailure>,
|
||||
isFetching: null == isFetching
|
||||
? _value.isFetching
|
||||
: isFetching // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$ProfitLossAnalyticLoaderStateImpl
|
||||
implements _ProfitLossAnalyticLoaderState {
|
||||
_$ProfitLossAnalyticLoaderStateImpl({
|
||||
required this.profitLossAnalytic,
|
||||
required this.failureOption,
|
||||
this.isFetching = false,
|
||||
});
|
||||
|
||||
@override
|
||||
final ProfitLossAnalytic profitLossAnalytic;
|
||||
@override
|
||||
final Option<AnalyticFailure> failureOption;
|
||||
@override
|
||||
@JsonKey()
|
||||
final bool isFetching;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ProfitLossAnalyticLoaderState(profitLossAnalytic: $profitLossAnalytic, failureOption: $failureOption, isFetching: $isFetching)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$ProfitLossAnalyticLoaderStateImpl &&
|
||||
(identical(other.profitLossAnalytic, profitLossAnalytic) ||
|
||||
other.profitLossAnalytic == profitLossAnalytic) &&
|
||||
(identical(other.failureOption, failureOption) ||
|
||||
other.failureOption == failureOption) &&
|
||||
(identical(other.isFetching, isFetching) ||
|
||||
other.isFetching == isFetching));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, profitLossAnalytic, failureOption, isFetching);
|
||||
|
||||
/// Create a copy of ProfitLossAnalyticLoaderState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$ProfitLossAnalyticLoaderStateImplCopyWith<
|
||||
_$ProfitLossAnalyticLoaderStateImpl
|
||||
>
|
||||
get copyWith =>
|
||||
__$$ProfitLossAnalyticLoaderStateImplCopyWithImpl<
|
||||
_$ProfitLossAnalyticLoaderStateImpl
|
||||
>(this, _$identity);
|
||||
}
|
||||
|
||||
abstract class _ProfitLossAnalyticLoaderState
|
||||
implements ProfitLossAnalyticLoaderState {
|
||||
factory _ProfitLossAnalyticLoaderState({
|
||||
required final ProfitLossAnalytic profitLossAnalytic,
|
||||
required final Option<AnalyticFailure> failureOption,
|
||||
final bool isFetching,
|
||||
}) = _$ProfitLossAnalyticLoaderStateImpl;
|
||||
|
||||
@override
|
||||
ProfitLossAnalytic get profitLossAnalytic;
|
||||
@override
|
||||
Option<AnalyticFailure> get failureOption;
|
||||
@override
|
||||
bool get isFetching;
|
||||
|
||||
/// Create a copy of ProfitLossAnalyticLoaderState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$ProfitLossAnalyticLoaderStateImplCopyWith<
|
||||
_$ProfitLossAnalyticLoaderStateImpl
|
||||
>
|
||||
get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
part of 'profit_loss_analytic_loader_bloc.dart';
|
||||
|
||||
@freezed
|
||||
class ProfitLossAnalyticLoaderEvent with _$ProfitLossAnalyticLoaderEvent {
|
||||
const factory ProfitLossAnalyticLoaderEvent.fetched({
|
||||
required DateTime startDate,
|
||||
required DateTime endDate,
|
||||
}) = _Fetched;
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
part of 'profit_loss_analytic_loader_bloc.dart';
|
||||
|
||||
@freezed
|
||||
class ProfitLossAnalyticLoaderState with _$ProfitLossAnalyticLoaderState {
|
||||
factory ProfitLossAnalyticLoaderState({
|
||||
required ProfitLossAnalytic profitLossAnalytic,
|
||||
required Option<AnalyticFailure> failureOption,
|
||||
@Default(false) bool isFetching,
|
||||
}) = _ProfitLossAnalyticLoaderState;
|
||||
|
||||
factory ProfitLossAnalyticLoaderState.initial() =>
|
||||
ProfitLossAnalyticLoaderState(
|
||||
profitLossAnalytic: ProfitLossAnalytic.empty(),
|
||||
failureOption: none(),
|
||||
);
|
||||
}
|
||||
@ -44,3 +44,13 @@ Map<String, int> getChairDistribution(int capacity) {
|
||||
return {'top': side, 'bottom': side, 'left': 1, 'right': 1};
|
||||
}
|
||||
}
|
||||
|
||||
double safeDouble(double value) {
|
||||
if (value.isNaN || value.isInfinite) return 0.0;
|
||||
return value;
|
||||
}
|
||||
|
||||
int safeRound(double value) {
|
||||
if (value.isNaN || value.isInfinite) return 0;
|
||||
return value.round();
|
||||
}
|
||||
|
||||
@ -13,4 +13,5 @@ class ApiPath {
|
||||
static const String analyticProducts = '/api/v1/analytics/products';
|
||||
static const String analyticPaymentMethods =
|
||||
'/api/v1/analytics/payment-methods';
|
||||
static const String analyticProfitLoss = '/api/v1/analytics/profit-loss';
|
||||
}
|
||||
|
||||
@ -9,5 +9,6 @@ part 'entities/dashboard_entity.dart';
|
||||
part 'entities/sales_entity.dart';
|
||||
part 'entities/product_analytic_entity.dart';
|
||||
part 'entities/payment_method_analytic_entity.dart';
|
||||
part 'entities/profit_loss_analytic_entity.dart';
|
||||
part 'failures/analytic_failure.dart';
|
||||
part 'repositories/i_analytic_repository.dart';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
117
lib/domain/analytic/entities/profit_loss_analytic_entity.dart
Normal file
117
lib/domain/analytic/entities/profit_loss_analytic_entity.dart
Normal file
@ -0,0 +1,117 @@
|
||||
part of '../analytic.dart';
|
||||
|
||||
@freezed
|
||||
class ProfitLossAnalytic with _$ProfitLossAnalytic {
|
||||
const factory ProfitLossAnalytic({
|
||||
required String organizationId,
|
||||
required String dateFrom,
|
||||
required String dateTo,
|
||||
required String groupBy,
|
||||
required ProfitLossAnalyticSummary summary,
|
||||
required List<ProfitLossAnalyticItem> data,
|
||||
required List<ProfitLossAnalyticProduct> productData,
|
||||
}) = _ProfitLossAnalytic;
|
||||
|
||||
factory ProfitLossAnalytic.empty() => ProfitLossAnalytic(
|
||||
organizationId: '',
|
||||
dateFrom: '',
|
||||
dateTo: '',
|
||||
groupBy: 'day',
|
||||
summary: ProfitLossAnalyticSummary.empty(),
|
||||
data: const [],
|
||||
productData: const [],
|
||||
);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ProfitLossAnalyticSummary with _$ProfitLossAnalyticSummary {
|
||||
const factory ProfitLossAnalyticSummary({
|
||||
required int totalRevenue,
|
||||
required num totalCost,
|
||||
required num grossProfit,
|
||||
required double grossProfitMargin,
|
||||
required int totalTax,
|
||||
required int totalDiscount,
|
||||
required num netProfit,
|
||||
required double netProfitMargin,
|
||||
required int totalOrders,
|
||||
required double averageProfit,
|
||||
required double profitabilityRatio,
|
||||
}) = _ProfitLossAnalyticSummary;
|
||||
|
||||
factory ProfitLossAnalyticSummary.empty() => const ProfitLossAnalyticSummary(
|
||||
totalRevenue: 0,
|
||||
totalCost: 0,
|
||||
grossProfit: 0,
|
||||
grossProfitMargin: 0.0,
|
||||
totalTax: 0,
|
||||
totalDiscount: 0,
|
||||
netProfit: 0,
|
||||
netProfitMargin: 0.0,
|
||||
totalOrders: 0,
|
||||
averageProfit: 0.0,
|
||||
profitabilityRatio: 0.0,
|
||||
);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ProfitLossAnalyticItem with _$ProfitLossAnalyticItem {
|
||||
const factory ProfitLossAnalyticItem({
|
||||
required String date,
|
||||
required int revenue,
|
||||
required num cost,
|
||||
required num grossProfit,
|
||||
required double grossProfitMargin,
|
||||
required int tax,
|
||||
required int discount,
|
||||
required num netProfit,
|
||||
required double netProfitMargin,
|
||||
required int orders,
|
||||
}) = _ProfitLossAnalyticItem;
|
||||
|
||||
factory ProfitLossAnalyticItem.empty() => const ProfitLossAnalyticItem(
|
||||
date: '',
|
||||
revenue: 0,
|
||||
cost: 0,
|
||||
grossProfit: 0,
|
||||
grossProfitMargin: 0.0,
|
||||
tax: 0,
|
||||
discount: 0,
|
||||
netProfit: 0,
|
||||
netProfitMargin: 0.0,
|
||||
orders: 0,
|
||||
);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ProfitLossAnalyticProduct with _$ProfitLossAnalyticProduct {
|
||||
const factory ProfitLossAnalyticProduct({
|
||||
required String productId,
|
||||
required String productName,
|
||||
required String categoryId,
|
||||
required String categoryName,
|
||||
required int quantitySold,
|
||||
required int revenue,
|
||||
required num cost,
|
||||
required num grossProfit,
|
||||
required double grossProfitMargin,
|
||||
required int averagePrice,
|
||||
required num averageCost,
|
||||
required num profitPerUnit,
|
||||
}) = _ProfitLossAnalyticProduct;
|
||||
|
||||
factory ProfitLossAnalyticProduct.empty() => const ProfitLossAnalyticProduct(
|
||||
productId: '',
|
||||
productName: '',
|
||||
categoryId: '',
|
||||
categoryName: '',
|
||||
quantitySold: 0,
|
||||
revenue: 0,
|
||||
cost: 0,
|
||||
grossProfit: 0,
|
||||
grossProfitMargin: 0.0,
|
||||
averagePrice: 0,
|
||||
averageCost: 0,
|
||||
profitPerUnit: 0,
|
||||
);
|
||||
}
|
||||
@ -17,4 +17,8 @@ abstract class IAnalyticRepository {
|
||||
required DateTime dateFrom,
|
||||
required DateTime dateTo,
|
||||
});
|
||||
Future<Either<AnalyticFailure, ProfitLossAnalytic>> getProfitLoss({
|
||||
required DateTime dateFrom,
|
||||
required DateTime dateTo,
|
||||
});
|
||||
}
|
||||
|
||||
@ -9,3 +9,4 @@ part 'dtos/dashboard_dto.dart';
|
||||
part 'dtos/sales_dto.dart';
|
||||
part 'dtos/product_analytic_dto.dart';
|
||||
part 'dtos/payment_method_analytic_dto.dart';
|
||||
part 'dtos/profit_loss_analytic_dto.dart';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -347,3 +347,135 @@ Map<String, dynamic> _$$PaymentMethodAnalyticSummaryDtoImplToJson(
|
||||
'total_payments': instance.totalPayments,
|
||||
'average_order_value': instance.averageOrderValue,
|
||||
};
|
||||
|
||||
_$ProfitLossAnalyticDtoImpl _$$ProfitLossAnalyticDtoImplFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _$ProfitLossAnalyticDtoImpl(
|
||||
organizationId: json['organization_id'] as String?,
|
||||
dateFrom: json['date_from'] as String?,
|
||||
dateTo: json['date_to'] as String?,
|
||||
groupBy: json['group_by'] as String?,
|
||||
summary: json['summary'] == null
|
||||
? null
|
||||
: ProfitLossAnalyticSummaryDto.fromJson(
|
||||
json['summary'] as Map<String, dynamic>,
|
||||
),
|
||||
data: (json['data'] as List<dynamic>?)
|
||||
?.map(
|
||||
(e) => ProfitLossAnalyticItemDto.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList(),
|
||||
productData: (json['product_data'] as List<dynamic>?)
|
||||
?.map(
|
||||
(e) => ProfitLossAnalyticProductDto.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$ProfitLossAnalyticDtoImplToJson(
|
||||
_$ProfitLossAnalyticDtoImpl instance,
|
||||
) => <String, dynamic>{
|
||||
'organization_id': instance.organizationId,
|
||||
'date_from': instance.dateFrom,
|
||||
'date_to': instance.dateTo,
|
||||
'group_by': instance.groupBy,
|
||||
'summary': instance.summary,
|
||||
'data': instance.data,
|
||||
'product_data': instance.productData,
|
||||
};
|
||||
|
||||
_$ProfitLossAnalyticSummaryDtoImpl _$$ProfitLossAnalyticSummaryDtoImplFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _$ProfitLossAnalyticSummaryDtoImpl(
|
||||
totalRevenue: (json['total_revenue'] as num?)?.toInt(),
|
||||
totalCost: json['total_cost'] as num?,
|
||||
grossProfit: json['gross_profit'] as num?,
|
||||
grossProfitMargin: (json['gross_profit_margin'] as num?)?.toDouble(),
|
||||
totalTax: (json['total_tax'] as num?)?.toInt(),
|
||||
totalDiscount: (json['total_discount'] as num?)?.toInt(),
|
||||
netProfit: json['net_profit'] as num?,
|
||||
netProfitMargin: (json['net_profit_margin'] as num?)?.toDouble(),
|
||||
totalOrders: (json['total_orders'] as num?)?.toInt(),
|
||||
averageProfit: (json['average_profit'] as num?)?.toDouble(),
|
||||
profitabilityRatio: (json['profitability_ratio'] as num?)?.toDouble(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$ProfitLossAnalyticSummaryDtoImplToJson(
|
||||
_$ProfitLossAnalyticSummaryDtoImpl instance,
|
||||
) => <String, dynamic>{
|
||||
'total_revenue': instance.totalRevenue,
|
||||
'total_cost': instance.totalCost,
|
||||
'gross_profit': instance.grossProfit,
|
||||
'gross_profit_margin': instance.grossProfitMargin,
|
||||
'total_tax': instance.totalTax,
|
||||
'total_discount': instance.totalDiscount,
|
||||
'net_profit': instance.netProfit,
|
||||
'net_profit_margin': instance.netProfitMargin,
|
||||
'total_orders': instance.totalOrders,
|
||||
'average_profit': instance.averageProfit,
|
||||
'profitability_ratio': instance.profitabilityRatio,
|
||||
};
|
||||
|
||||
_$ProfitLossAnalyticItemDtoImpl _$$ProfitLossAnalyticItemDtoImplFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _$ProfitLossAnalyticItemDtoImpl(
|
||||
date: json['date'] as String?,
|
||||
revenue: (json['revenue'] as num?)?.toInt(),
|
||||
cost: json['cost'] as num?,
|
||||
grossProfit: json['gross_profit'] as num?,
|
||||
grossProfitMargin: (json['gross_profit_margin'] as num?)?.toDouble(),
|
||||
tax: (json['tax'] as num?)?.toInt(),
|
||||
discount: (json['discount'] as num?)?.toInt(),
|
||||
netProfit: json['net_profit'] as num?,
|
||||
netProfitMargin: (json['net_profit_margin'] as num?)?.toDouble(),
|
||||
orders: (json['orders'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$ProfitLossAnalyticItemDtoImplToJson(
|
||||
_$ProfitLossAnalyticItemDtoImpl instance,
|
||||
) => <String, dynamic>{
|
||||
'date': instance.date,
|
||||
'revenue': instance.revenue,
|
||||
'cost': instance.cost,
|
||||
'gross_profit': instance.grossProfit,
|
||||
'gross_profit_margin': instance.grossProfitMargin,
|
||||
'tax': instance.tax,
|
||||
'discount': instance.discount,
|
||||
'net_profit': instance.netProfit,
|
||||
'net_profit_margin': instance.netProfitMargin,
|
||||
'orders': instance.orders,
|
||||
};
|
||||
|
||||
_$ProfitLossAnalyticProductDtoImpl _$$ProfitLossAnalyticProductDtoImplFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _$ProfitLossAnalyticProductDtoImpl(
|
||||
productId: json['product_id'] as String?,
|
||||
productName: json['product_name'] as String?,
|
||||
categoryId: json['category_id'] as String?,
|
||||
categoryName: json['category_name'] as String?,
|
||||
quantitySold: (json['quantity_sold'] as num?)?.toInt(),
|
||||
revenue: (json['revenue'] as num?)?.toInt(),
|
||||
cost: json['cost'] as num?,
|
||||
grossProfit: json['gross_profit'] as num?,
|
||||
grossProfitMargin: (json['gross_profit_margin'] as num?)?.toDouble(),
|
||||
averagePrice: (json['average_price'] as num?)?.toInt(),
|
||||
averageCost: json['average_cost'] as num?,
|
||||
profitPerUnit: json['profit_per_unit'] as num?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$ProfitLossAnalyticProductDtoImplToJson(
|
||||
_$ProfitLossAnalyticProductDtoImpl instance,
|
||||
) => <String, dynamic>{
|
||||
'product_id': instance.productId,
|
||||
'product_name': instance.productName,
|
||||
'category_id': instance.categoryId,
|
||||
'category_name': instance.categoryName,
|
||||
'quantity_sold': instance.quantitySold,
|
||||
'revenue': instance.revenue,
|
||||
'cost': instance.cost,
|
||||
'gross_profit': instance.grossProfit,
|
||||
'gross_profit_margin': instance.grossProfitMargin,
|
||||
'average_price': instance.averagePrice,
|
||||
'average_cost': instance.averageCost,
|
||||
'profit_per_unit': instance.profitPerUnit,
|
||||
};
|
||||
|
||||
@ -134,4 +134,33 @@ class AnalyticRemoteDataProvider {
|
||||
return DC.error(AnalyticFailure.serverError(e));
|
||||
}
|
||||
}
|
||||
|
||||
Future<DC<AnalyticFailure, ProfitLossAnalyticDto>> fetchProfitLoss({
|
||||
required DateTime dateFrom,
|
||||
required DateTime dateTo,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
ApiPath.analyticProfitLoss,
|
||||
params: {
|
||||
'date_from': dateFrom.toServerDate(),
|
||||
'date_to': dateTo.toServerDate(),
|
||||
},
|
||||
headers: getAuthorizationHeader(),
|
||||
);
|
||||
|
||||
if (response.data['success'] == false) {
|
||||
return DC.error(AnalyticFailure.unexpectedError());
|
||||
}
|
||||
|
||||
final profitLoss = ProfitLossAnalyticDto.fromJson(
|
||||
response.data['data'] as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
return DC.data(profitLoss);
|
||||
} on ApiFailure catch (e, s) {
|
||||
log('fetchProfitLoss', name: _logName, error: e, stackTrace: s);
|
||||
return DC.error(AnalyticFailure.serverError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
142
lib/infrastructure/analytic/dtos/profit_loss_analytic_dto.dart
Normal file
142
lib/infrastructure/analytic/dtos/profit_loss_analytic_dto.dart
Normal file
@ -0,0 +1,142 @@
|
||||
part of '../analytic_dtos.dart';
|
||||
|
||||
@freezed
|
||||
class ProfitLossAnalyticDto with _$ProfitLossAnalyticDto {
|
||||
const ProfitLossAnalyticDto._();
|
||||
|
||||
const factory ProfitLossAnalyticDto({
|
||||
@JsonKey(name: "organization_id") String? organizationId,
|
||||
@JsonKey(name: "date_from") String? dateFrom,
|
||||
@JsonKey(name: "date_to") String? dateTo,
|
||||
@JsonKey(name: "group_by") String? groupBy,
|
||||
@JsonKey(name: "summary") ProfitLossAnalyticSummaryDto? summary,
|
||||
@JsonKey(name: "data") List<ProfitLossAnalyticItemDto>? data,
|
||||
@JsonKey(name: "product_data")
|
||||
List<ProfitLossAnalyticProductDto>? productData,
|
||||
}) = _ProfitLossAnalyticDto;
|
||||
|
||||
factory ProfitLossAnalyticDto.fromJson(Map<String, dynamic> json) =>
|
||||
_$ProfitLossAnalyticDtoFromJson(json);
|
||||
|
||||
// Optional: mapper ke domain entity
|
||||
ProfitLossAnalytic toDomain() => ProfitLossAnalytic(
|
||||
organizationId: organizationId ?? '',
|
||||
dateFrom: dateFrom ?? '',
|
||||
dateTo: dateTo ?? '',
|
||||
groupBy: groupBy ?? 'day',
|
||||
summary: summary?.toDomain() ?? ProfitLossAnalyticSummary.empty(),
|
||||
data: data?.map((e) => e.toDomain()).toList() ?? [],
|
||||
productData: productData?.map((e) => e.toDomain()).toList() ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ProfitLossAnalyticSummaryDto with _$ProfitLossAnalyticSummaryDto {
|
||||
const ProfitLossAnalyticSummaryDto._();
|
||||
|
||||
const factory ProfitLossAnalyticSummaryDto({
|
||||
@JsonKey(name: "total_revenue") int? totalRevenue,
|
||||
@JsonKey(name: "total_cost") num? totalCost,
|
||||
@JsonKey(name: "gross_profit") num? grossProfit,
|
||||
@JsonKey(name: "gross_profit_margin") double? grossProfitMargin,
|
||||
@JsonKey(name: "total_tax") int? totalTax,
|
||||
@JsonKey(name: "total_discount") int? totalDiscount,
|
||||
@JsonKey(name: "net_profit") num? netProfit,
|
||||
@JsonKey(name: "net_profit_margin") double? netProfitMargin,
|
||||
@JsonKey(name: "total_orders") int? totalOrders,
|
||||
@JsonKey(name: "average_profit") double? averageProfit,
|
||||
@JsonKey(name: "profitability_ratio") double? profitabilityRatio,
|
||||
}) = _ProfitLossAnalyticSummaryDto;
|
||||
|
||||
factory ProfitLossAnalyticSummaryDto.fromJson(Map<String, dynamic> json) =>
|
||||
_$ProfitLossAnalyticSummaryDtoFromJson(json);
|
||||
|
||||
// Optional mapper ke domain
|
||||
ProfitLossAnalyticSummary toDomain() => ProfitLossAnalyticSummary(
|
||||
totalRevenue: totalRevenue ?? 0,
|
||||
totalCost: totalCost ?? 0,
|
||||
grossProfit: grossProfit ?? 0,
|
||||
grossProfitMargin: grossProfitMargin ?? 0.0,
|
||||
totalTax: totalTax ?? 0,
|
||||
totalDiscount: totalDiscount ?? 0,
|
||||
netProfit: netProfit ?? 0,
|
||||
netProfitMargin: netProfitMargin ?? 0.0,
|
||||
totalOrders: totalOrders ?? 0,
|
||||
averageProfit: averageProfit ?? 0.0,
|
||||
profitabilityRatio: profitabilityRatio ?? 0.0,
|
||||
);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ProfitLossAnalyticItemDto with _$ProfitLossAnalyticItemDto {
|
||||
const ProfitLossAnalyticItemDto._();
|
||||
|
||||
const factory ProfitLossAnalyticItemDto({
|
||||
@JsonKey(name: "date") String? date,
|
||||
@JsonKey(name: "revenue") int? revenue,
|
||||
@JsonKey(name: "cost") num? cost,
|
||||
@JsonKey(name: "gross_profit") num? grossProfit,
|
||||
@JsonKey(name: "gross_profit_margin") double? grossProfitMargin,
|
||||
@JsonKey(name: "tax") int? tax,
|
||||
@JsonKey(name: "discount") int? discount,
|
||||
@JsonKey(name: "net_profit") num? netProfit,
|
||||
@JsonKey(name: "net_profit_margin") double? netProfitMargin,
|
||||
@JsonKey(name: "orders") int? orders,
|
||||
}) = _ProfitLossAnalyticItemDto;
|
||||
|
||||
factory ProfitLossAnalyticItemDto.fromJson(Map<String, dynamic> json) =>
|
||||
_$ProfitLossAnalyticItemDtoFromJson(json);
|
||||
|
||||
// Optional mapper ke domain
|
||||
ProfitLossAnalyticItem toDomain() => ProfitLossAnalyticItem(
|
||||
date: date ?? '',
|
||||
revenue: revenue ?? 0,
|
||||
cost: cost ?? 0,
|
||||
grossProfit: grossProfit ?? 0,
|
||||
grossProfitMargin: grossProfitMargin ?? 0.0,
|
||||
tax: tax ?? 0,
|
||||
discount: discount ?? 0,
|
||||
netProfit: netProfit ?? 0,
|
||||
netProfitMargin: netProfitMargin ?? 0.0,
|
||||
orders: orders ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ProfitLossAnalyticProductDto with _$ProfitLossAnalyticProductDto {
|
||||
const ProfitLossAnalyticProductDto._();
|
||||
|
||||
const factory ProfitLossAnalyticProductDto({
|
||||
@JsonKey(name: "product_id") String? productId,
|
||||
@JsonKey(name: "product_name") String? productName,
|
||||
@JsonKey(name: "category_id") String? categoryId,
|
||||
@JsonKey(name: "category_name") String? categoryName,
|
||||
@JsonKey(name: "quantity_sold") int? quantitySold,
|
||||
@JsonKey(name: "revenue") int? revenue,
|
||||
@JsonKey(name: "cost") num? cost,
|
||||
@JsonKey(name: "gross_profit") num? grossProfit,
|
||||
@JsonKey(name: "gross_profit_margin") double? grossProfitMargin,
|
||||
@JsonKey(name: "average_price") int? averagePrice,
|
||||
@JsonKey(name: "average_cost") num? averageCost,
|
||||
@JsonKey(name: "profit_per_unit") num? profitPerUnit,
|
||||
}) = _ProfitLossAnalyticProductDto;
|
||||
|
||||
factory ProfitLossAnalyticProductDto.fromJson(Map<String, dynamic> json) =>
|
||||
_$ProfitLossAnalyticProductDtoFromJson(json);
|
||||
|
||||
// Optional mapper ke domain
|
||||
ProfitLossAnalyticProduct toDomain() => ProfitLossAnalyticProduct(
|
||||
productId: productId ?? '',
|
||||
productName: productName ?? '',
|
||||
categoryId: categoryId ?? '',
|
||||
categoryName: categoryName ?? '',
|
||||
quantitySold: quantitySold ?? 0,
|
||||
revenue: revenue ?? 0,
|
||||
cost: cost ?? 0,
|
||||
grossProfit: grossProfit ?? 0,
|
||||
grossProfitMargin: grossProfitMargin ?? 0.0,
|
||||
averagePrice: averagePrice ?? 0,
|
||||
averageCost: averageCost ?? 0,
|
||||
profitPerUnit: profitPerUnit ?? 0,
|
||||
);
|
||||
}
|
||||
@ -109,4 +109,28 @@ class AnalyticRepository implements IAnalyticRepository {
|
||||
return left(const AnalyticFailure.unexpectedError());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<AnalyticFailure, ProfitLossAnalytic>> getProfitLoss({
|
||||
required DateTime dateFrom,
|
||||
required DateTime dateTo,
|
||||
}) async {
|
||||
try {
|
||||
final result = await _dataProvider.fetchProfitLoss(
|
||||
dateFrom: dateFrom,
|
||||
dateTo: dateTo,
|
||||
);
|
||||
|
||||
if (result.hasError) {
|
||||
return left(result.error!);
|
||||
}
|
||||
|
||||
final profitLoss = result.data!.toDomain();
|
||||
|
||||
return right(profitLoss);
|
||||
} catch (e) {
|
||||
log('getProfitLossError', name: _logName, error: e);
|
||||
return left(const AnalyticFailure.unexpectedError());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,8 @@ import 'package:apskel_pos_flutter_v2/application/analytic/payment_method_analyt
|
||||
as _i733;
|
||||
import 'package:apskel_pos_flutter_v2/application/analytic/product_analytic_loader/product_analytic_loader_bloc.dart'
|
||||
as _i268;
|
||||
import 'package:apskel_pos_flutter_v2/application/analytic/profit_loss_analytic_loader/profit_loss_analytic_loader_bloc.dart'
|
||||
as _i741;
|
||||
import 'package:apskel_pos_flutter_v2/application/analytic/sales_analytic_loader/sales_analytic_loader_bloc.dart'
|
||||
as _i413;
|
||||
import 'package:apskel_pos_flutter_v2/application/auth/auth_bloc.dart' as _i343;
|
||||
@ -310,6 +312,9 @@ extension GetItInjectableX on _i174.GetIt {
|
||||
gh<_i346.IAnalyticRepository>(),
|
||||
),
|
||||
);
|
||||
gh.factory<_i741.ProfitLossAnalyticLoaderBloc>(
|
||||
() => _i741.ProfitLossAnalyticLoaderBloc(gh<_i346.IAnalyticRepository>()),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../../application/analytic/dashboard_analytic_loader/dashboard_analytic_loader_bloc.dart';
|
||||
import '../../../../../application/analytic/payment_method_analytic_loader/payment_method_analytic_loader_bloc.dart';
|
||||
import '../../../../../application/analytic/product_analytic_loader/product_analytic_loader_bloc.dart';
|
||||
import '../../../../../application/analytic/profit_loss_analytic_loader/profit_loss_analytic_loader_bloc.dart';
|
||||
import '../../../../../application/analytic/sales_analytic_loader/sales_analytic_loader_bloc.dart';
|
||||
import '../../../../../application/report/report_bloc.dart';
|
||||
import '../../../../../common/data/report_menu.dart';
|
||||
@ -18,6 +19,7 @@ import '../../../../router/app_router.gr.dart';
|
||||
import 'sections/report_dashboard_section.dart';
|
||||
import 'sections/report_payment_method_section.dart';
|
||||
import 'sections/report_product_section.dart';
|
||||
import 'sections/report_profit_loss_section.dart';
|
||||
import 'sections/report_sales_section.dart';
|
||||
import 'widgets/report_menu_card.dart';
|
||||
|
||||
@ -167,7 +169,20 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
|
||||
);
|
||||
},
|
||||
),
|
||||
5 => Text(state.title),
|
||||
5 =>
|
||||
BlocBuilder<
|
||||
ProfitLossAnalyticLoaderBloc,
|
||||
ProfitLossAnalyticLoaderState
|
||||
>(
|
||||
builder: (context, profitLoss) {
|
||||
return ReportProfitLossSection(
|
||||
menu: reportMenus[state.selectedMenu],
|
||||
state: profitLoss,
|
||||
startDate: state.startDate,
|
||||
endDate: state.endDate,
|
||||
);
|
||||
},
|
||||
),
|
||||
6 => Text(state.title),
|
||||
7 => Text(state.title),
|
||||
_ => Container(),
|
||||
@ -218,6 +233,12 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
|
||||
);
|
||||
|
||||
case 5:
|
||||
return context.read<ProfitLossAnalyticLoaderBloc>().add(
|
||||
ProfitLossAnalyticLoaderEvent.fetched(
|
||||
startDate: state.startDate,
|
||||
endDate: state.endDate,
|
||||
),
|
||||
);
|
||||
case 6:
|
||||
case 7:
|
||||
default:
|
||||
@ -265,6 +286,15 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
|
||||
),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => getIt<ProfitLossAnalyticLoaderBloc>()
|
||||
..add(
|
||||
ProfitLossAnalyticLoaderEvent.fetched(
|
||||
startDate: DateTime.now().subtract(const Duration(days: 30)),
|
||||
endDate: DateTime.now(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: this,
|
||||
);
|
||||
|
||||
@ -0,0 +1,157 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../../application/analytic/profit_loss_analytic_loader/profit_loss_analytic_loader_bloc.dart';
|
||||
import '../../../../../../common/data/report_menu.dart';
|
||||
import '../../../../../../common/extension/extension.dart';
|
||||
import '../../../../../../common/theme/theme.dart';
|
||||
import '../../../../../components/error/analytic_error_state_widget.dart';
|
||||
import '../../../../../components/loader/loader_with_text.dart';
|
||||
import '../../../../../components/spaces/space.dart';
|
||||
import '../../../../../components/widgets/report/report_header.dart';
|
||||
import '../../../../../components/widgets/report/report_summary_card.dart';
|
||||
import '../widgets/profit_loss/report_profit_loss_breakdown.dart';
|
||||
import '../widgets/profit_loss/report_profit_loss_metric.dart';
|
||||
import '../widgets/profit_loss/report_profit_loss_product_profitability.dart';
|
||||
import '../widgets/profit_loss/report_profit_loss_trend_chart.dart';
|
||||
|
||||
class ReportProfitLossSection extends StatelessWidget {
|
||||
final ReportMenu menu;
|
||||
final ProfitLossAnalyticLoaderState state;
|
||||
final DateTime startDate;
|
||||
final DateTime endDate;
|
||||
|
||||
const ReportProfitLossSection({
|
||||
super.key,
|
||||
required this.menu,
|
||||
required this.state,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (state.isFetching) {
|
||||
return const Center(child: LoaderWithText());
|
||||
}
|
||||
|
||||
return state.failureOption.fold(
|
||||
() => RefreshIndicator(
|
||||
onRefresh: () {
|
||||
context.read<ProfitLossAnalyticLoaderBloc>().add(
|
||||
ProfitLossAnalyticLoaderEvent.fetched(
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
),
|
||||
);
|
||||
return Future.value();
|
||||
},
|
||||
child: ListView(
|
||||
padding: EdgeInsets.all(16),
|
||||
children: [
|
||||
ReportHeader(menu: menu, endDate: endDate, startDate: startDate),
|
||||
_buildSummary(),
|
||||
ReportProfitLossTrendChart(data: state.profitLossAnalytic.data),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: ReportProfitLossProductProfitability(
|
||||
data: state.profitLossAnalytic.productData,
|
||||
),
|
||||
),
|
||||
SpaceWidth(12),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ReportProfitLossBreakdown(
|
||||
data: state.profitLossAnalytic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ReportProfitLossMetric(data: state.profitLossAnalytic),
|
||||
],
|
||||
),
|
||||
),
|
||||
(f) => AnalyticErrorStateWidget(
|
||||
failure: f,
|
||||
menu: menu,
|
||||
onRefresh: () {
|
||||
context.read<ProfitLossAnalyticLoaderBloc>().add(
|
||||
ProfitLossAnalyticLoaderEvent.fetched(
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Padding _buildSummary() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ReportSummaryCard(
|
||||
color: AppColor.success,
|
||||
icon: Icons.attach_money,
|
||||
title: 'Jumlah Pendapatan',
|
||||
value: state
|
||||
.profitLossAnalytic
|
||||
.summary
|
||||
.totalRevenue
|
||||
.currencyFormatRpV2,
|
||||
),
|
||||
),
|
||||
SpaceWidth(12),
|
||||
Expanded(
|
||||
child: ReportSummaryCard(
|
||||
color: AppColor.info,
|
||||
icon: Icons.receipt_outlined,
|
||||
title: 'Jumlah Biaya',
|
||||
value: state.profitLossAnalytic.summary.totalCost
|
||||
.toDouble()
|
||||
.currencyFormatRpV2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SpaceHeight(12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ReportSummaryCard(
|
||||
color: AppColor.warning,
|
||||
icon: Icons.trending_up_outlined,
|
||||
title: 'Laba Kotor',
|
||||
value: state.profitLossAnalytic.summary.grossProfit
|
||||
.toDouble()
|
||||
.currencyFormatRpV2,
|
||||
),
|
||||
),
|
||||
SpaceWidth(12),
|
||||
Expanded(
|
||||
child: ReportSummaryCard(
|
||||
color: AppColor.primary,
|
||||
icon: Icons.account_balance_outlined,
|
||||
title: 'Laba Bersih',
|
||||
value: state.profitLossAnalytic.summary.netProfit
|
||||
.toString()
|
||||
.currencyFormatRpV2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,185 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../../../../common/extension/extension.dart';
|
||||
import '../../../../../../../common/function/app_function.dart';
|
||||
import '../../../../../../../common/theme/theme.dart';
|
||||
import '../../../../../../../domain/analytic/analytic.dart';
|
||||
import '../../../../../../components/spaces/space.dart';
|
||||
|
||||
class ReportProfitLossBreakdown extends StatelessWidget {
|
||||
final ProfitLossAnalytic data;
|
||||
const ReportProfitLossBreakdown({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final breakdownData = [
|
||||
{
|
||||
'label': 'Laba Kotor',
|
||||
'value': data.summary.grossProfit,
|
||||
'color': AppColor.success,
|
||||
},
|
||||
{
|
||||
'label': 'Pajak',
|
||||
'value': data.summary.totalTax,
|
||||
'color': AppColor.warning,
|
||||
},
|
||||
{
|
||||
'label': 'Diskon',
|
||||
'value': data.summary.totalDiscount,
|
||||
'color': AppColor.info,
|
||||
},
|
||||
{
|
||||
'label': 'Laba Bersih',
|
||||
'value': data.summary.netProfit,
|
||||
'color': AppColor.primary,
|
||||
},
|
||||
];
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Rincian Keuntungan',
|
||||
style: AppStyle.xl.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
SpaceHeight(4),
|
||||
Text(
|
||||
'Komponen pembentuk profit',
|
||||
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
|
||||
),
|
||||
SpaceHeight(20),
|
||||
|
||||
// Grafik Donut
|
||||
SizedBox(
|
||||
height: 160,
|
||||
child: PieChart(
|
||||
PieChartData(
|
||||
sectionsSpace: 3,
|
||||
centerSpaceRadius: 50,
|
||||
startDegreeOffset: -90,
|
||||
sections: breakdownData.asMap().entries.map((entry) {
|
||||
final item = entry.value;
|
||||
final grossProfit = data.summary.grossProfit;
|
||||
final value = item['value'] as num;
|
||||
|
||||
// Handle division by zero
|
||||
final percentage = grossProfit > 0
|
||||
? (value / grossProfit * 100)
|
||||
: 0.0;
|
||||
|
||||
return PieChartSectionData(
|
||||
color: item['color'] as Color,
|
||||
value: value.toDouble(),
|
||||
title: '${safeRound(percentage)}%',
|
||||
radius: 40,
|
||||
titleStyle: AppStyle.xs.copyWith(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SpaceHeight(16),
|
||||
|
||||
// Legenda
|
||||
Column(
|
||||
children: breakdownData.map((item) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: item['color'] as Color,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item['label'] as String,
|
||||
style: AppStyle.sm.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
(item['value'] as num).toString().currencyFormatRpV2,
|
||||
style: AppStyle.sm.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
SpaceHeight(16),
|
||||
|
||||
// Rasio Profitabilitas
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
AppColor.primary.withOpacity(0.1),
|
||||
AppColor.secondary.withOpacity(0.1),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Rasio Profitabilitas',
|
||||
style: AppStyle.sm.copyWith(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
SpaceHeight(8),
|
||||
Text(
|
||||
'${safeRound(data.summary.profitabilityRatio)}%',
|
||||
style: AppStyle.h5.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
),
|
||||
SpaceHeight(4),
|
||||
Text(
|
||||
'Tingkat Pengembalian Pendapatan',
|
||||
style: AppStyle.xs.copyWith(
|
||||
fontSize: 10,
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,300 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../../../../../common/extension/extension.dart';
|
||||
import '../../../../../../../common/function/app_function.dart';
|
||||
import '../../../../../../../common/theme/theme.dart';
|
||||
import '../../../../../../../domain/analytic/analytic.dart';
|
||||
import '../../../../../../components/spaces/space.dart';
|
||||
|
||||
class ReportProfitLossMetric extends StatelessWidget {
|
||||
final ProfitLossAnalytic data;
|
||||
const ReportProfitLossMetric({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: EdgeInsets.only(top: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Metrik Kinerja Terperinci',
|
||||
style: AppStyle.xl.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
SpaceHeight(16),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildMetricCard(
|
||||
'Nilai Rata-rata Pesanan',
|
||||
_safeCalculateAverageOrder().currencyFormatRpV2,
|
||||
'Per transaksi',
|
||||
Icons.shopping_cart_outlined,
|
||||
AppColor.info,
|
||||
),
|
||||
),
|
||||
SpaceWidth(12),
|
||||
Expanded(
|
||||
child: _buildMetricCard(
|
||||
'Keuntungan Rata-rata',
|
||||
"${data.summary.averageProfit.round()}%",
|
||||
'Per pesanan',
|
||||
Icons.trending_up,
|
||||
AppColor.success,
|
||||
),
|
||||
),
|
||||
SpaceWidth(12),
|
||||
Expanded(
|
||||
child: _buildMetricCard(
|
||||
'Rasio Biaya',
|
||||
'${_safeCalculateCostRatio()}%',
|
||||
'Dari total pendapatan',
|
||||
Icons.pie_chart,
|
||||
AppColor.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SpaceHeight(16),
|
||||
|
||||
// Tabel rincian harian
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[100]!),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primary.withOpacity(0.05),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
'Tanggal',
|
||||
style: AppStyle.sm.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Pendapatan',
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.sm.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Biaya',
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.sm.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Laba Bersih',
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.sm.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Margin',
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.sm.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
...data.data.map((item) {
|
||||
final date = DateTime.parse(item.date);
|
||||
final dateStr = DateFormat('dd MMM').format(date);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Colors.grey[100]!),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
dateStr,
|
||||
style: AppStyle.sm.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.revenue.currencyFormatRpV2,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.xs.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColor.info,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.cost.toString().currencyFormatRpV2,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.xs.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColor.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.netProfit.toString().currencyFormatRpV2,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.xs.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColor.success,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _getMarginColor(
|
||||
item.netProfitMargin,
|
||||
).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'${safeRound(item.netProfitMargin)}%',
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.xs.copyWith(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _getMarginColor(item.netProfitMargin),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMetricCard(
|
||||
String title,
|
||||
String value,
|
||||
String subtitle,
|
||||
IconData icon,
|
||||
Color color,
|
||||
) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.06),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: color.withOpacity(0.2)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 24),
|
||||
SpaceHeight(8),
|
||||
Text(
|
||||
value,
|
||||
style: AppStyle.lg.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
SpaceHeight(4),
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.sm.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
SpaceHeight(2),
|
||||
Text(
|
||||
subtitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.xs.copyWith(
|
||||
fontSize: 10,
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
int _safeCalculateAverageOrder() {
|
||||
if (data.summary.totalOrders == 0) return 0;
|
||||
final average = data.summary.totalRevenue / data.summary.totalOrders;
|
||||
if (average.isNaN || average.isInfinite) return 0;
|
||||
return average.round();
|
||||
}
|
||||
|
||||
int _safeCalculateCostRatio() {
|
||||
if (data.summary.totalRevenue == 0) return 0;
|
||||
final ratio = (data.summary.totalCost / data.summary.totalRevenue) * 100;
|
||||
if (ratio.isNaN || ratio.isInfinite) return 0;
|
||||
return ratio.round();
|
||||
}
|
||||
|
||||
Color _getMarginColor(double margin) {
|
||||
final safeMargin = safeDouble(margin);
|
||||
if (safeMargin >= 25) return AppColor.success;
|
||||
if (safeMargin >= 15) return AppColor.warning;
|
||||
return AppColor.error;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,184 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../../../../common/extension/extension.dart';
|
||||
import '../../../../../../../common/function/app_function.dart';
|
||||
import '../../../../../../../common/theme/theme.dart';
|
||||
import '../../../../../../../domain/analytic/analytic.dart';
|
||||
import '../../../../../../components/spaces/space.dart';
|
||||
|
||||
class ReportProfitLossProductProfitability extends StatelessWidget {
|
||||
final List<ProfitLossAnalyticProduct> data;
|
||||
const ReportProfitLossProductProfitability({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Profitabilitas Produk',
|
||||
style: AppStyle.xl.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
SpaceHeight(4),
|
||||
Text(
|
||||
'Analisis margin keuntungan per produk',
|
||||
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
|
||||
),
|
||||
SpaceHeight(12),
|
||||
Column(
|
||||
children: data.take(4).map((product) {
|
||||
return _buildProductItem(product);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProductItem(ProfitLossAnalyticProduct product) {
|
||||
final profitMargin = safeDouble(product.grossProfitMargin);
|
||||
final profitColor = profitMargin >= 35
|
||||
? AppColor.success
|
||||
: profitMargin >= 25
|
||||
? AppColor.warning
|
||||
: AppColor.error;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey[100]!),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
product.productName,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF0F172A),
|
||||
),
|
||||
),
|
||||
SpaceHeight(2),
|
||||
Text(
|
||||
'${product.quantitySold} unit • ${product.categoryName}',
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: profitColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
'${safeRound(profitMargin)}%',
|
||||
style: AppStyle.sm.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: profitColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
SpaceHeight(4),
|
||||
Text(
|
||||
product.grossProfit.toString().currencyFormatRpV2,
|
||||
style: AppStyle.md.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColor.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
SpaceHeight(12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildProductMetric(
|
||||
'Pendapatan',
|
||||
product.revenue,
|
||||
AppColor.info,
|
||||
),
|
||||
),
|
||||
SpaceWidth(8),
|
||||
Expanded(
|
||||
child: _buildProductMetric(
|
||||
'Biaya',
|
||||
product.cost,
|
||||
AppColor.error,
|
||||
),
|
||||
),
|
||||
SpaceWidth(8),
|
||||
Expanded(
|
||||
child: _buildProductMetric(
|
||||
'Laba/Unit',
|
||||
product.profitPerUnit,
|
||||
AppColor.success,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProductMetric(String label, num value, Color color) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
SpaceHeight(2),
|
||||
Text(
|
||||
value.toString().currencyFormatRpV2,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,185 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../../../../common/theme/theme.dart';
|
||||
import '../../../../../../../domain/analytic/analytic.dart';
|
||||
|
||||
class ReportProfitLossTrendChart extends StatelessWidget {
|
||||
final List<ProfitLossAnalyticItem> data;
|
||||
const ReportProfitLossTrendChart({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: EdgeInsets.only(top: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Analisis Tren Keuntungan',
|
||||
style: AppStyle.xl.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
_buildLegendItem('Pendapatan', AppColor.info),
|
||||
const SizedBox(width: 16),
|
||||
_buildLegendItem('Biaya', AppColor.error),
|
||||
const SizedBox(width: 16),
|
||||
_buildLegendItem('Laba Bersih', AppColor.success),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawHorizontalLine: true,
|
||||
drawVerticalLine: false,
|
||||
horizontalInterval: 100000,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return FlLine(color: Colors.grey[100]!, strokeWidth: 1);
|
||||
},
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 50,
|
||||
getTitlesWidget: (value, meta) {
|
||||
final kValue = (value / 1000);
|
||||
if (kValue.isFinite) {
|
||||
return Text(
|
||||
'${kValue.toInt()}K',
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
fontSize: 10,
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, meta) {
|
||||
final index = value.toInt();
|
||||
if (index >= 0 && index < data.length) {
|
||||
final date = DateTime.parse(data[index].date);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(
|
||||
'${date.day}/${date.month}',
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
lineBarsData: [
|
||||
// Garis Pendapatan
|
||||
LineChartBarData(
|
||||
spots: data.asMap().entries.map((entry) {
|
||||
final revenue = entry.value.revenue.toDouble();
|
||||
return FlSpot(
|
||||
entry.key.toDouble(),
|
||||
revenue.isFinite ? revenue : 0,
|
||||
);
|
||||
}).toList(),
|
||||
isCurved: true,
|
||||
color: AppColor.info,
|
||||
dotData: const FlDotData(show: false),
|
||||
),
|
||||
// Garis Biaya
|
||||
LineChartBarData(
|
||||
spots: data.asMap().entries.map((entry) {
|
||||
final cost = entry.value.cost.toDouble();
|
||||
return FlSpot(
|
||||
entry.key.toDouble(),
|
||||
cost.isFinite ? cost : 0,
|
||||
);
|
||||
}).toList(),
|
||||
isCurved: true,
|
||||
color: AppColor.error,
|
||||
dotData: const FlDotData(show: false),
|
||||
),
|
||||
// Garis Laba Bersih
|
||||
LineChartBarData(
|
||||
spots: data.asMap().entries.map((entry) {
|
||||
final netProfit = entry.value.netProfit.toDouble();
|
||||
return FlSpot(
|
||||
entry.key.toDouble(),
|
||||
netProfit.isFinite ? netProfit : 0,
|
||||
);
|
||||
}).toList(),
|
||||
isCurved: true,
|
||||
color: AppColor.success,
|
||||
dotData: const FlDotData(show: true),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: AppColor.success.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLegendItem(String label, Color color) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: AppStyle.sm.copyWith(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColor.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user