report dashboard
This commit is contained in:
parent
069c2296de
commit
b4a9cf31fc
@ -0,0 +1,53 @@
|
|||||||
|
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 'dashboard_analytic_loader_event.dart';
|
||||||
|
part 'dashboard_analytic_loader_state.dart';
|
||||||
|
part 'dashboard_analytic_loader_bloc.freezed.dart';
|
||||||
|
|
||||||
|
@injectable
|
||||||
|
class DashboardAnalyticLoaderBloc
|
||||||
|
extends Bloc<DashboardAnalyticLoaderEvent, DashboardAnalyticLoaderState> {
|
||||||
|
final IAnalyticRepository _analyticRepository;
|
||||||
|
|
||||||
|
DashboardAnalyticLoaderBloc(this._analyticRepository)
|
||||||
|
: super(DashboardAnalyticLoaderState.initial()) {
|
||||||
|
on<DashboardAnalyticLoaderEvent>(_onDashboardAnalyticLoader);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onDashboardAnalyticLoader(
|
||||||
|
DashboardAnalyticLoaderEvent event,
|
||||||
|
Emitter<DashboardAnalyticLoaderState> emit,
|
||||||
|
) {
|
||||||
|
return event.map(
|
||||||
|
fetched: (e) async {
|
||||||
|
emit(state.copyWith(isFetching: true, failureOption: none()));
|
||||||
|
|
||||||
|
final result = await _analyticRepository.getDashboard(
|
||||||
|
dateFrom: e.startDate,
|
||||||
|
dateTo: e.endDate,
|
||||||
|
);
|
||||||
|
|
||||||
|
await result.fold(
|
||||||
|
(failure) async {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isFetching: false,
|
||||||
|
failureOption: optionOf(failure),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(dashboard) async {
|
||||||
|
emit(
|
||||||
|
state.copyWith(isFetching: false, dashboardAnalytic: dashboard),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,480 @@
|
|||||||
|
// 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 'dashboard_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 _$DashboardAnalyticLoaderEvent {
|
||||||
|
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 DashboardAnalyticLoaderEvent
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$DashboardAnalyticLoaderEventCopyWith<DashboardAnalyticLoaderEvent>
|
||||||
|
get copyWith => throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $DashboardAnalyticLoaderEventCopyWith<$Res> {
|
||||||
|
factory $DashboardAnalyticLoaderEventCopyWith(
|
||||||
|
DashboardAnalyticLoaderEvent value,
|
||||||
|
$Res Function(DashboardAnalyticLoaderEvent) then,
|
||||||
|
) =
|
||||||
|
_$DashboardAnalyticLoaderEventCopyWithImpl<
|
||||||
|
$Res,
|
||||||
|
DashboardAnalyticLoaderEvent
|
||||||
|
>;
|
||||||
|
@useResult
|
||||||
|
$Res call({DateTime startDate, DateTime endDate});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$DashboardAnalyticLoaderEventCopyWithImpl<
|
||||||
|
$Res,
|
||||||
|
$Val extends DashboardAnalyticLoaderEvent
|
||||||
|
>
|
||||||
|
implements $DashboardAnalyticLoaderEventCopyWith<$Res> {
|
||||||
|
_$DashboardAnalyticLoaderEventCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of DashboardAnalyticLoaderEvent
|
||||||
|
/// 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 $DashboardAnalyticLoaderEventCopyWith<$Res> {
|
||||||
|
factory _$$FetchedImplCopyWith(
|
||||||
|
_$FetchedImpl value,
|
||||||
|
$Res Function(_$FetchedImpl) then,
|
||||||
|
) = __$$FetchedImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call({DateTime startDate, DateTime endDate});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$FetchedImplCopyWithImpl<$Res>
|
||||||
|
extends _$DashboardAnalyticLoaderEventCopyWithImpl<$Res, _$FetchedImpl>
|
||||||
|
implements _$$FetchedImplCopyWith<$Res> {
|
||||||
|
__$$FetchedImplCopyWithImpl(
|
||||||
|
_$FetchedImpl _value,
|
||||||
|
$Res Function(_$FetchedImpl) _then,
|
||||||
|
) : super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of DashboardAnalyticLoaderEvent
|
||||||
|
/// 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 'DashboardAnalyticLoaderEvent.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 DashboardAnalyticLoaderEvent
|
||||||
|
/// 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 DashboardAnalyticLoaderEvent {
|
||||||
|
const factory _Fetched({
|
||||||
|
required final DateTime startDate,
|
||||||
|
required final DateTime endDate,
|
||||||
|
}) = _$FetchedImpl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
DateTime get startDate;
|
||||||
|
@override
|
||||||
|
DateTime get endDate;
|
||||||
|
|
||||||
|
/// Create a copy of DashboardAnalyticLoaderEvent
|
||||||
|
/// 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 _$DashboardAnalyticLoaderState {
|
||||||
|
DashboardAnalytic get dashboardAnalytic => throw _privateConstructorUsedError;
|
||||||
|
Option<AnalyticFailure> get failureOption =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
bool get isFetching => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of DashboardAnalyticLoaderState
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$DashboardAnalyticLoaderStateCopyWith<DashboardAnalyticLoaderState>
|
||||||
|
get copyWith => throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $DashboardAnalyticLoaderStateCopyWith<$Res> {
|
||||||
|
factory $DashboardAnalyticLoaderStateCopyWith(
|
||||||
|
DashboardAnalyticLoaderState value,
|
||||||
|
$Res Function(DashboardAnalyticLoaderState) then,
|
||||||
|
) =
|
||||||
|
_$DashboardAnalyticLoaderStateCopyWithImpl<
|
||||||
|
$Res,
|
||||||
|
DashboardAnalyticLoaderState
|
||||||
|
>;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
DashboardAnalytic dashboardAnalytic,
|
||||||
|
Option<AnalyticFailure> failureOption,
|
||||||
|
bool isFetching,
|
||||||
|
});
|
||||||
|
|
||||||
|
$DashboardAnalyticCopyWith<$Res> get dashboardAnalytic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$DashboardAnalyticLoaderStateCopyWithImpl<
|
||||||
|
$Res,
|
||||||
|
$Val extends DashboardAnalyticLoaderState
|
||||||
|
>
|
||||||
|
implements $DashboardAnalyticLoaderStateCopyWith<$Res> {
|
||||||
|
_$DashboardAnalyticLoaderStateCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of DashboardAnalyticLoaderState
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? dashboardAnalytic = null,
|
||||||
|
Object? failureOption = null,
|
||||||
|
Object? isFetching = null,
|
||||||
|
}) {
|
||||||
|
return _then(
|
||||||
|
_value.copyWith(
|
||||||
|
dashboardAnalytic: null == dashboardAnalytic
|
||||||
|
? _value.dashboardAnalytic
|
||||||
|
: dashboardAnalytic // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DashboardAnalytic,
|
||||||
|
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 DashboardAnalyticLoaderState
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$DashboardAnalyticCopyWith<$Res> get dashboardAnalytic {
|
||||||
|
return $DashboardAnalyticCopyWith<$Res>(_value.dashboardAnalytic, (value) {
|
||||||
|
return _then(_value.copyWith(dashboardAnalytic: value) as $Val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$DashboardAnalyticLoaderStateImplCopyWith<$Res>
|
||||||
|
implements $DashboardAnalyticLoaderStateCopyWith<$Res> {
|
||||||
|
factory _$$DashboardAnalyticLoaderStateImplCopyWith(
|
||||||
|
_$DashboardAnalyticLoaderStateImpl value,
|
||||||
|
$Res Function(_$DashboardAnalyticLoaderStateImpl) then,
|
||||||
|
) = __$$DashboardAnalyticLoaderStateImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
DashboardAnalytic dashboardAnalytic,
|
||||||
|
Option<AnalyticFailure> failureOption,
|
||||||
|
bool isFetching,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
$DashboardAnalyticCopyWith<$Res> get dashboardAnalytic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$DashboardAnalyticLoaderStateImplCopyWithImpl<$Res>
|
||||||
|
extends
|
||||||
|
_$DashboardAnalyticLoaderStateCopyWithImpl<
|
||||||
|
$Res,
|
||||||
|
_$DashboardAnalyticLoaderStateImpl
|
||||||
|
>
|
||||||
|
implements _$$DashboardAnalyticLoaderStateImplCopyWith<$Res> {
|
||||||
|
__$$DashboardAnalyticLoaderStateImplCopyWithImpl(
|
||||||
|
_$DashboardAnalyticLoaderStateImpl _value,
|
||||||
|
$Res Function(_$DashboardAnalyticLoaderStateImpl) _then,
|
||||||
|
) : super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of DashboardAnalyticLoaderState
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? dashboardAnalytic = null,
|
||||||
|
Object? failureOption = null,
|
||||||
|
Object? isFetching = null,
|
||||||
|
}) {
|
||||||
|
return _then(
|
||||||
|
_$DashboardAnalyticLoaderStateImpl(
|
||||||
|
dashboardAnalytic: null == dashboardAnalytic
|
||||||
|
? _value.dashboardAnalytic
|
||||||
|
: dashboardAnalytic // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DashboardAnalytic,
|
||||||
|
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 _$DashboardAnalyticLoaderStateImpl
|
||||||
|
implements _DashboardAnalyticLoaderState {
|
||||||
|
_$DashboardAnalyticLoaderStateImpl({
|
||||||
|
required this.dashboardAnalytic,
|
||||||
|
required this.failureOption,
|
||||||
|
this.isFetching = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
final DashboardAnalytic dashboardAnalytic;
|
||||||
|
@override
|
||||||
|
final Option<AnalyticFailure> failureOption;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final bool isFetching;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'DashboardAnalyticLoaderState(dashboardAnalytic: $dashboardAnalytic, failureOption: $failureOption, isFetching: $isFetching)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$DashboardAnalyticLoaderStateImpl &&
|
||||||
|
(identical(other.dashboardAnalytic, dashboardAnalytic) ||
|
||||||
|
other.dashboardAnalytic == dashboardAnalytic) &&
|
||||||
|
(identical(other.failureOption, failureOption) ||
|
||||||
|
other.failureOption == failureOption) &&
|
||||||
|
(identical(other.isFetching, isFetching) ||
|
||||||
|
other.isFetching == isFetching));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
Object.hash(runtimeType, dashboardAnalytic, failureOption, isFetching);
|
||||||
|
|
||||||
|
/// Create a copy of DashboardAnalyticLoaderState
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$DashboardAnalyticLoaderStateImplCopyWith<
|
||||||
|
_$DashboardAnalyticLoaderStateImpl
|
||||||
|
>
|
||||||
|
get copyWith =>
|
||||||
|
__$$DashboardAnalyticLoaderStateImplCopyWithImpl<
|
||||||
|
_$DashboardAnalyticLoaderStateImpl
|
||||||
|
>(this, _$identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _DashboardAnalyticLoaderState
|
||||||
|
implements DashboardAnalyticLoaderState {
|
||||||
|
factory _DashboardAnalyticLoaderState({
|
||||||
|
required final DashboardAnalytic dashboardAnalytic,
|
||||||
|
required final Option<AnalyticFailure> failureOption,
|
||||||
|
final bool isFetching,
|
||||||
|
}) = _$DashboardAnalyticLoaderStateImpl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
DashboardAnalytic get dashboardAnalytic;
|
||||||
|
@override
|
||||||
|
Option<AnalyticFailure> get failureOption;
|
||||||
|
@override
|
||||||
|
bool get isFetching;
|
||||||
|
|
||||||
|
/// Create a copy of DashboardAnalyticLoaderState
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$DashboardAnalyticLoaderStateImplCopyWith<
|
||||||
|
_$DashboardAnalyticLoaderStateImpl
|
||||||
|
>
|
||||||
|
get copyWith => throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
part of 'dashboard_analytic_loader_bloc.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class DashboardAnalyticLoaderEvent with _$DashboardAnalyticLoaderEvent {
|
||||||
|
const factory DashboardAnalyticLoaderEvent.fetched({
|
||||||
|
required DateTime startDate,
|
||||||
|
required DateTime endDate,
|
||||||
|
}) = _Fetched;
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
part of 'dashboard_analytic_loader_bloc.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class DashboardAnalyticLoaderState with _$DashboardAnalyticLoaderState {
|
||||||
|
factory DashboardAnalyticLoaderState({
|
||||||
|
required DashboardAnalytic dashboardAnalytic,
|
||||||
|
required Option<AnalyticFailure> failureOption,
|
||||||
|
@Default(false) bool isFetching,
|
||||||
|
}) = _DashboardAnalyticLoaderState;
|
||||||
|
|
||||||
|
factory DashboardAnalyticLoaderState.initial() =>
|
||||||
|
DashboardAnalyticLoaderState(
|
||||||
|
dashboardAnalytic: DashboardAnalytic.empty(),
|
||||||
|
failureOption: none(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -12,9 +12,9 @@ class ReportState with _$ReportState {
|
|||||||
|
|
||||||
factory ReportState.initial() => ReportState(
|
factory ReportState.initial() => ReportState(
|
||||||
title: 'Ringkasan Laporan Penjualan',
|
title: 'Ringkasan Laporan Penjualan',
|
||||||
startDate: DateTime.now(),
|
startDate: DateTime.now().subtract(const Duration(days: 30)),
|
||||||
endDate: DateTime.now(),
|
endDate: DateTime.now(),
|
||||||
rangeDateFormatted:
|
rangeDateFormatted:
|
||||||
'${DateTime.now().toFormattedDate()} - ${DateTime.now().toFormattedDate()}',
|
'${DateTime.now().subtract(const Duration(days: 30)).toFormattedDate()} - ${DateTime.now().toFormattedDate()}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,4 +62,12 @@ extension DateTimeExt on DateTime {
|
|||||||
|
|
||||||
return '$day $month $year, $hour:$minute:$second';
|
return '$day $month $year, $hour:$minute:$second';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String toServerDate() {
|
||||||
|
return DateFormat('dd-MM-yyyy').format(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toSimpleMonthDate() {
|
||||||
|
return DateFormat('dd MMM yyyy').format(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,4 +8,5 @@ class ApiPath {
|
|||||||
static const String paymentMethods = '/api/v1/payment-methods';
|
static const String paymentMethods = '/api/v1/payment-methods';
|
||||||
static const String orders = '/api/v1/orders';
|
static const String orders = '/api/v1/orders';
|
||||||
static const String payments = '/api/v1/payments';
|
static const String payments = '/api/v1/payments';
|
||||||
|
static const String analyticDashboard = '/api/v1/analytics/dashboard';
|
||||||
}
|
}
|
||||||
|
|||||||
10
lib/domain/analytic/analytic.dart
Normal file
10
lib/domain/analytic/analytic.dart
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
import '../../common/api/api_failure.dart';
|
||||||
|
|
||||||
|
part 'analytic.freezed.dart';
|
||||||
|
|
||||||
|
part 'entities/dashboard_entity.dart';
|
||||||
|
part 'failures/analytic_failure.dart';
|
||||||
|
part 'repositories/i_analytic_repository.dart';
|
||||||
1984
lib/domain/analytic/analytic.freezed.dart
Normal file
1984
lib/domain/analytic/analytic.freezed.dart
Normal file
File diff suppressed because it is too large
Load Diff
118
lib/domain/analytic/entities/dashboard_entity.dart
Normal file
118
lib/domain/analytic/entities/dashboard_entity.dart
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
part of '../analytic.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class DashboardAnalytic with _$DashboardAnalytic {
|
||||||
|
const factory DashboardAnalytic({
|
||||||
|
required String organizationId,
|
||||||
|
required String outletId,
|
||||||
|
required String dateFrom,
|
||||||
|
required String dateTo,
|
||||||
|
required DashboardOverview overview,
|
||||||
|
required List<DashboardTopProduct> topProducts,
|
||||||
|
required List<DashboardPaymentMethod> paymentMethods,
|
||||||
|
required List<DashboardRecentSale> recentSales,
|
||||||
|
}) = _DashboardAnalytic;
|
||||||
|
|
||||||
|
factory DashboardAnalytic.empty() => DashboardAnalytic(
|
||||||
|
organizationId: '',
|
||||||
|
outletId: '',
|
||||||
|
dateFrom: '',
|
||||||
|
dateTo: '',
|
||||||
|
overview: DashboardOverview.empty(),
|
||||||
|
topProducts: const [],
|
||||||
|
paymentMethods: const [],
|
||||||
|
recentSales: const [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class DashboardOverview with _$DashboardOverview {
|
||||||
|
const factory DashboardOverview({
|
||||||
|
required int totalSales,
|
||||||
|
required int totalOrders,
|
||||||
|
required double averageOrderValue,
|
||||||
|
required int totalCustomers,
|
||||||
|
required int voidedOrders,
|
||||||
|
required int refundedOrders,
|
||||||
|
}) = _DashboardOverview;
|
||||||
|
|
||||||
|
factory DashboardOverview.empty() => const DashboardOverview(
|
||||||
|
totalSales: 0,
|
||||||
|
totalOrders: 0,
|
||||||
|
averageOrderValue: 0.0,
|
||||||
|
totalCustomers: 0,
|
||||||
|
voidedOrders: 0,
|
||||||
|
refundedOrders: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class DashboardTopProduct with _$DashboardTopProduct {
|
||||||
|
const factory DashboardTopProduct({
|
||||||
|
required String productId,
|
||||||
|
required String productName,
|
||||||
|
required String categoryId,
|
||||||
|
required String categoryName,
|
||||||
|
required int quantitySold,
|
||||||
|
required int revenue,
|
||||||
|
required double averagePrice,
|
||||||
|
required int orderCount,
|
||||||
|
}) = _DashboardTopProduct;
|
||||||
|
|
||||||
|
factory DashboardTopProduct.empty() => const DashboardTopProduct(
|
||||||
|
productId: '',
|
||||||
|
productName: '',
|
||||||
|
categoryId: '',
|
||||||
|
categoryName: '',
|
||||||
|
quantitySold: 0,
|
||||||
|
revenue: 0,
|
||||||
|
averagePrice: 0.0,
|
||||||
|
orderCount: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class DashboardPaymentMethod with _$DashboardPaymentMethod {
|
||||||
|
const factory DashboardPaymentMethod({
|
||||||
|
required String paymentMethodId,
|
||||||
|
required String paymentMethodName,
|
||||||
|
required String paymentMethodType,
|
||||||
|
required int totalAmount,
|
||||||
|
required int orderCount,
|
||||||
|
required int paymentCount,
|
||||||
|
required double percentage,
|
||||||
|
}) = _DashboardPaymentMethod;
|
||||||
|
|
||||||
|
factory DashboardPaymentMethod.empty() => const DashboardPaymentMethod(
|
||||||
|
paymentMethodId: '',
|
||||||
|
paymentMethodName: '',
|
||||||
|
paymentMethodType: '',
|
||||||
|
totalAmount: 0,
|
||||||
|
orderCount: 0,
|
||||||
|
paymentCount: 0,
|
||||||
|
percentage: 0.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class DashboardRecentSale with _$DashboardRecentSale {
|
||||||
|
const factory DashboardRecentSale({
|
||||||
|
required String date,
|
||||||
|
required int sales,
|
||||||
|
required int orders,
|
||||||
|
required int items,
|
||||||
|
required int tax,
|
||||||
|
required int discount,
|
||||||
|
required int netSales,
|
||||||
|
}) = _DashboardRecentSale;
|
||||||
|
|
||||||
|
factory DashboardRecentSale.empty() => const DashboardRecentSale(
|
||||||
|
date: '',
|
||||||
|
sales: 0,
|
||||||
|
orders: 0,
|
||||||
|
items: 0,
|
||||||
|
tax: 0,
|
||||||
|
discount: 0,
|
||||||
|
netSales: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
9
lib/domain/analytic/failures/analytic_failure.dart
Normal file
9
lib/domain/analytic/failures/analytic_failure.dart
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
part of '../analytic.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class AnalyticFailure with _$AnalyticFailure {
|
||||||
|
const factory AnalyticFailure.serverError(ApiFailure failure) = _ServerError;
|
||||||
|
const factory AnalyticFailure.unexpectedError() = _UnexpectedError;
|
||||||
|
const factory AnalyticFailure.dynamicErrorMessage(String erroMessage) =
|
||||||
|
_DynamicErrorMessage;
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
part of '../analytic.dart';
|
||||||
|
|
||||||
|
abstract class IAnalyticRepository {
|
||||||
|
Future<Either<AnalyticFailure, DashboardAnalytic>> getDashboard({
|
||||||
|
required DateTime dateFrom,
|
||||||
|
required DateTime dateTo,
|
||||||
|
});
|
||||||
|
}
|
||||||
8
lib/infrastructure/analytic/analytic_dtos.dart
Normal file
8
lib/infrastructure/analytic/analytic_dtos.dart
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
import '../../domain/analytic/analytic.dart';
|
||||||
|
|
||||||
|
part 'analytic_dtos.freezed.dart';
|
||||||
|
part 'analytic_dtos.g.dart';
|
||||||
|
|
||||||
|
part 'dtos/dashboard_dto.dart';
|
||||||
1747
lib/infrastructure/analytic/analytic_dtos.freezed.dart
Normal file
1747
lib/infrastructure/analytic/analytic_dtos.freezed.dart
Normal file
File diff suppressed because it is too large
Load Diff
139
lib/infrastructure/analytic/analytic_dtos.g.dart
Normal file
139
lib/infrastructure/analytic/analytic_dtos.g.dart
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'analytic_dtos.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$DashboardAnalyticDtoImpl _$$DashboardAnalyticDtoImplFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => _$DashboardAnalyticDtoImpl(
|
||||||
|
organizationId: json['organization_id'] as String?,
|
||||||
|
outletId: json['outlet_id'] as String?,
|
||||||
|
dateFrom: json['date_from'] as String?,
|
||||||
|
dateTo: json['date_to'] as String?,
|
||||||
|
overview: json['overview'] == null
|
||||||
|
? null
|
||||||
|
: DashboardOverviewDto.fromJson(json['overview'] as Map<String, dynamic>),
|
||||||
|
topProducts: (json['top_products'] as List<dynamic>?)
|
||||||
|
?.map((e) => DashboardTopProductDto.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
paymentMethods: (json['payment_methods'] as List<dynamic>?)
|
||||||
|
?.map(
|
||||||
|
(e) => DashboardPaymentMethodDto.fromJson(e as Map<String, dynamic>),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
recentSales: (json['recent_sales'] as List<dynamic>?)
|
||||||
|
?.map((e) => DashboardRecentSaleDto.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$DashboardAnalyticDtoImplToJson(
|
||||||
|
_$DashboardAnalyticDtoImpl instance,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'organization_id': instance.organizationId,
|
||||||
|
'outlet_id': instance.outletId,
|
||||||
|
'date_from': instance.dateFrom,
|
||||||
|
'date_to': instance.dateTo,
|
||||||
|
'overview': instance.overview,
|
||||||
|
'top_products': instance.topProducts,
|
||||||
|
'payment_methods': instance.paymentMethods,
|
||||||
|
'recent_sales': instance.recentSales,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$DashboardOverviewDtoImpl _$$DashboardOverviewDtoImplFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => _$DashboardOverviewDtoImpl(
|
||||||
|
totalSales: (json['total_sales'] as num?)?.toInt(),
|
||||||
|
totalOrders: (json['total_orders'] as num?)?.toInt(),
|
||||||
|
averageOrderValue: (json['average_order_value'] as num?)?.toDouble(),
|
||||||
|
totalCustomers: (json['total_customers'] as num?)?.toInt(),
|
||||||
|
voidedOrders: (json['voided_orders'] as num?)?.toInt(),
|
||||||
|
refundedOrders: (json['refunded_orders'] as num?)?.toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$DashboardOverviewDtoImplToJson(
|
||||||
|
_$DashboardOverviewDtoImpl instance,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'total_sales': instance.totalSales,
|
||||||
|
'total_orders': instance.totalOrders,
|
||||||
|
'average_order_value': instance.averageOrderValue,
|
||||||
|
'total_customers': instance.totalCustomers,
|
||||||
|
'voided_orders': instance.voidedOrders,
|
||||||
|
'refunded_orders': instance.refundedOrders,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$DashboardTopProductDtoImpl _$$DashboardTopProductDtoImplFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => _$DashboardTopProductDtoImpl(
|
||||||
|
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(),
|
||||||
|
averagePrice: (json['average_price'] as num?)?.toDouble(),
|
||||||
|
orderCount: (json['order_count'] as num?)?.toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$DashboardTopProductDtoImplToJson(
|
||||||
|
_$DashboardTopProductDtoImpl 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,
|
||||||
|
'average_price': instance.averagePrice,
|
||||||
|
'order_count': instance.orderCount,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$DashboardPaymentMethodDtoImpl _$$DashboardPaymentMethodDtoImplFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => _$DashboardPaymentMethodDtoImpl(
|
||||||
|
paymentMethodId: json['payment_method_id'] as String?,
|
||||||
|
paymentMethodName: json['payment_method_name'] as String?,
|
||||||
|
paymentMethodType: json['payment_method_type'] as String?,
|
||||||
|
totalAmount: (json['total_amount'] as num?)?.toInt(),
|
||||||
|
orderCount: (json['order_count'] as num?)?.toInt(),
|
||||||
|
paymentCount: (json['payment_count'] as num?)?.toInt(),
|
||||||
|
percentage: (json['percentage'] as num?)?.toDouble(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$DashboardPaymentMethodDtoImplToJson(
|
||||||
|
_$DashboardPaymentMethodDtoImpl instance,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'payment_method_id': instance.paymentMethodId,
|
||||||
|
'payment_method_name': instance.paymentMethodName,
|
||||||
|
'payment_method_type': instance.paymentMethodType,
|
||||||
|
'total_amount': instance.totalAmount,
|
||||||
|
'order_count': instance.orderCount,
|
||||||
|
'payment_count': instance.paymentCount,
|
||||||
|
'percentage': instance.percentage,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$DashboardRecentSaleDtoImpl _$$DashboardRecentSaleDtoImplFromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) => _$DashboardRecentSaleDtoImpl(
|
||||||
|
date: json['date'] as String?,
|
||||||
|
sales: (json['sales'] as num?)?.toInt(),
|
||||||
|
orders: (json['orders'] as num?)?.toInt(),
|
||||||
|
items: (json['items'] as num?)?.toInt(),
|
||||||
|
tax: (json['tax'] as num?)?.toInt(),
|
||||||
|
discount: (json['discount'] as num?)?.toInt(),
|
||||||
|
netSales: (json['net_sales'] as num?)?.toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$DashboardRecentSaleDtoImplToJson(
|
||||||
|
_$DashboardRecentSaleDtoImpl instance,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'date': instance.date,
|
||||||
|
'sales': instance.sales,
|
||||||
|
'orders': instance.orders,
|
||||||
|
'items': instance.items,
|
||||||
|
'tax': instance.tax,
|
||||||
|
'discount': instance.discount,
|
||||||
|
'net_sales': instance.netSales,
|
||||||
|
};
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:data_channel/data_channel.dart';
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
|
||||||
|
import '../../../common/api/api_client.dart';
|
||||||
|
import '../../../common/api/api_failure.dart';
|
||||||
|
import '../../../common/extension/extension.dart';
|
||||||
|
import '../../../common/function/app_function.dart';
|
||||||
|
import '../../../common/url/api_path.dart';
|
||||||
|
import '../../../domain/analytic/analytic.dart';
|
||||||
|
import '../analytic_dtos.dart';
|
||||||
|
|
||||||
|
@injectable
|
||||||
|
class AnalyticRemoteDataProvider {
|
||||||
|
final ApiClient _apiClient;
|
||||||
|
|
||||||
|
final _logName = 'AnalyticRemoteDataProvider';
|
||||||
|
|
||||||
|
AnalyticRemoteDataProvider(this._apiClient);
|
||||||
|
|
||||||
|
Future<DC<AnalyticFailure, DashboardAnalyticDto>> fetchDashboard({
|
||||||
|
required DateTime dateFrom,
|
||||||
|
required DateTime dateTo,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final response = await _apiClient.get(
|
||||||
|
ApiPath.analyticDashboard,
|
||||||
|
params: {
|
||||||
|
'date_from': dateFrom.toServerDate(),
|
||||||
|
'date_to': dateTo.toServerDate(),
|
||||||
|
},
|
||||||
|
headers: getAuthorizationHeader(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data['success'] == false) {
|
||||||
|
return DC.error(AnalyticFailure.unexpectedError());
|
||||||
|
}
|
||||||
|
|
||||||
|
final dashboard = DashboardAnalyticDto.fromJson(
|
||||||
|
response.data['data'] as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
|
||||||
|
return DC.data(dashboard);
|
||||||
|
} on ApiFailure catch (e, s) {
|
||||||
|
log('fetchDashboard', name: _logName, error: e, stackTrace: s);
|
||||||
|
return DC.error(AnalyticFailure.serverError(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
149
lib/infrastructure/analytic/dtos/dashboard_dto.dart
Normal file
149
lib/infrastructure/analytic/dtos/dashboard_dto.dart
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
part of '../analytic_dtos.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class DashboardAnalyticDto with _$DashboardAnalyticDto {
|
||||||
|
const DashboardAnalyticDto._();
|
||||||
|
|
||||||
|
const factory DashboardAnalyticDto({
|
||||||
|
@JsonKey(name: "organization_id") String? organizationId,
|
||||||
|
@JsonKey(name: "outlet_id") String? outletId,
|
||||||
|
@JsonKey(name: "date_from") String? dateFrom,
|
||||||
|
@JsonKey(name: "date_to") String? dateTo,
|
||||||
|
@JsonKey(name: "overview") DashboardOverviewDto? overview,
|
||||||
|
@JsonKey(name: "top_products") List<DashboardTopProductDto>? topProducts,
|
||||||
|
@JsonKey(name: "payment_methods")
|
||||||
|
List<DashboardPaymentMethodDto>? paymentMethods,
|
||||||
|
@JsonKey(name: "recent_sales") List<DashboardRecentSaleDto>? recentSales,
|
||||||
|
}) = _DashboardAnalyticDto;
|
||||||
|
|
||||||
|
factory DashboardAnalyticDto.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$DashboardAnalyticDtoFromJson(json);
|
||||||
|
|
||||||
|
// Optional mapping ke domain
|
||||||
|
DashboardAnalytic toDomain() => DashboardAnalytic(
|
||||||
|
organizationId: organizationId ?? '',
|
||||||
|
outletId: outletId ?? '',
|
||||||
|
dateFrom: dateFrom ?? '',
|
||||||
|
dateTo: dateTo ?? '',
|
||||||
|
overview: overview?.toDomain() ?? DashboardOverview.empty(),
|
||||||
|
topProducts: topProducts?.map((e) => e.toDomain()).toList() ?? [],
|
||||||
|
paymentMethods: paymentMethods?.map((e) => e.toDomain()).toList() ?? [],
|
||||||
|
recentSales: recentSales?.map((e) => e.toDomain()).toList() ?? [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class DashboardOverviewDto with _$DashboardOverviewDto {
|
||||||
|
const DashboardOverviewDto._();
|
||||||
|
|
||||||
|
const factory DashboardOverviewDto({
|
||||||
|
@JsonKey(name: "total_sales") int? totalSales,
|
||||||
|
@JsonKey(name: "total_orders") int? totalOrders,
|
||||||
|
@JsonKey(name: "average_order_value") double? averageOrderValue,
|
||||||
|
@JsonKey(name: "total_customers") int? totalCustomers,
|
||||||
|
@JsonKey(name: "voided_orders") int? voidedOrders,
|
||||||
|
@JsonKey(name: "refunded_orders") int? refundedOrders,
|
||||||
|
}) = _DashboardOverviewDto;
|
||||||
|
|
||||||
|
factory DashboardOverviewDto.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$DashboardOverviewDtoFromJson(json);
|
||||||
|
|
||||||
|
// Optional mapping ke domain
|
||||||
|
DashboardOverview toDomain() => DashboardOverview(
|
||||||
|
totalSales: totalSales ?? 0,
|
||||||
|
totalOrders: totalOrders ?? 0,
|
||||||
|
averageOrderValue: averageOrderValue ?? 0.0,
|
||||||
|
totalCustomers: totalCustomers ?? 0,
|
||||||
|
voidedOrders: voidedOrders ?? 0,
|
||||||
|
refundedOrders: refundedOrders ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class DashboardTopProductDto with _$DashboardTopProductDto {
|
||||||
|
const DashboardTopProductDto._();
|
||||||
|
|
||||||
|
const factory DashboardTopProductDto({
|
||||||
|
@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: "average_price") double? averagePrice,
|
||||||
|
@JsonKey(name: "order_count") int? orderCount,
|
||||||
|
}) = _DashboardTopProductDto;
|
||||||
|
|
||||||
|
factory DashboardTopProductDto.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$DashboardTopProductDtoFromJson(json);
|
||||||
|
|
||||||
|
// Optional mapping ke domain
|
||||||
|
DashboardTopProduct toDomain() => DashboardTopProduct(
|
||||||
|
productId: productId ?? '',
|
||||||
|
productName: productName ?? '',
|
||||||
|
categoryId: categoryId ?? '',
|
||||||
|
categoryName: categoryName ?? '',
|
||||||
|
quantitySold: quantitySold ?? 0,
|
||||||
|
revenue: revenue ?? 0,
|
||||||
|
averagePrice: averagePrice ?? 0.0,
|
||||||
|
orderCount: orderCount ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class DashboardPaymentMethodDto with _$DashboardPaymentMethodDto {
|
||||||
|
const DashboardPaymentMethodDto._();
|
||||||
|
|
||||||
|
const factory DashboardPaymentMethodDto({
|
||||||
|
@JsonKey(name: "payment_method_id") String? paymentMethodId,
|
||||||
|
@JsonKey(name: "payment_method_name") String? paymentMethodName,
|
||||||
|
@JsonKey(name: "payment_method_type") String? paymentMethodType,
|
||||||
|
@JsonKey(name: "total_amount") int? totalAmount,
|
||||||
|
@JsonKey(name: "order_count") int? orderCount,
|
||||||
|
@JsonKey(name: "payment_count") int? paymentCount,
|
||||||
|
@JsonKey(name: "percentage") double? percentage,
|
||||||
|
}) = _DashboardPaymentMethodDto;
|
||||||
|
|
||||||
|
factory DashboardPaymentMethodDto.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$DashboardPaymentMethodDtoFromJson(json);
|
||||||
|
|
||||||
|
// Optional mapping ke domain
|
||||||
|
DashboardPaymentMethod toDomain() => DashboardPaymentMethod(
|
||||||
|
paymentMethodId: paymentMethodId ?? '',
|
||||||
|
paymentMethodName: paymentMethodName ?? '',
|
||||||
|
paymentMethodType: paymentMethodType ?? '',
|
||||||
|
totalAmount: totalAmount ?? 0,
|
||||||
|
orderCount: orderCount ?? 0,
|
||||||
|
paymentCount: paymentCount ?? 0,
|
||||||
|
percentage: percentage ?? 0.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class DashboardRecentSaleDto with _$DashboardRecentSaleDto {
|
||||||
|
const DashboardRecentSaleDto._();
|
||||||
|
|
||||||
|
const factory DashboardRecentSaleDto({
|
||||||
|
@JsonKey(name: "date") String? date,
|
||||||
|
@JsonKey(name: "sales") int? sales,
|
||||||
|
@JsonKey(name: "orders") int? orders,
|
||||||
|
@JsonKey(name: "items") int? items,
|
||||||
|
@JsonKey(name: "tax") int? tax,
|
||||||
|
@JsonKey(name: "discount") int? discount,
|
||||||
|
@JsonKey(name: "net_sales") int? netSales,
|
||||||
|
}) = _DashboardRecentSaleDto;
|
||||||
|
|
||||||
|
factory DashboardRecentSaleDto.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$DashboardRecentSaleDtoFromJson(json);
|
||||||
|
|
||||||
|
// Optional mapping ke domain
|
||||||
|
DashboardRecentSale toDomain() => DashboardRecentSale(
|
||||||
|
date: date ?? '',
|
||||||
|
sales: sales ?? 0,
|
||||||
|
orders: orders ?? 0,
|
||||||
|
items: items ?? 0,
|
||||||
|
tax: tax ?? 0,
|
||||||
|
discount: discount ?? 0,
|
||||||
|
netSales: netSales ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
|
||||||
|
import '../../../domain/analytic/analytic.dart';
|
||||||
|
import '../datasources/remote_data_provider.dart';
|
||||||
|
|
||||||
|
@Injectable(as: IAnalyticRepository)
|
||||||
|
class AnalyticRepository implements IAnalyticRepository {
|
||||||
|
final AnalyticRemoteDataProvider _dataProvider;
|
||||||
|
|
||||||
|
final _logName = 'AnalyticRepository';
|
||||||
|
|
||||||
|
AnalyticRepository(this._dataProvider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<AnalyticFailure, DashboardAnalytic>> getDashboard({
|
||||||
|
required DateTime dateFrom,
|
||||||
|
required DateTime dateTo,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final result = await _dataProvider.fetchDashboard(
|
||||||
|
dateFrom: dateFrom,
|
||||||
|
dateTo: dateTo,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.hasError) {
|
||||||
|
return left(result.error!);
|
||||||
|
}
|
||||||
|
|
||||||
|
final dashboard = result.data!.toDomain();
|
||||||
|
|
||||||
|
return right(dashboard);
|
||||||
|
} catch (e) {
|
||||||
|
log('getDashboardError', name: _logName, error: e);
|
||||||
|
return left(const AnalyticFailure.unexpectedError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,6 +9,8 @@
|
|||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
|
|
||||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||||
|
import 'package:apskel_pos_flutter_v2/application/analytic/dashboard_analytic_loader/dashboard_analytic_loader_bloc.dart'
|
||||||
|
as _i80;
|
||||||
import 'package:apskel_pos_flutter_v2/application/auth/auth_bloc.dart' as _i343;
|
import 'package:apskel_pos_flutter_v2/application/auth/auth_bloc.dart' as _i343;
|
||||||
import 'package:apskel_pos_flutter_v2/application/auth/login_form/login_form_bloc.dart'
|
import 'package:apskel_pos_flutter_v2/application/auth/login_form/login_form_bloc.dart'
|
||||||
as _i46;
|
as _i46;
|
||||||
@ -54,6 +56,7 @@ import 'package:apskel_pos_flutter_v2/common/di/di_shared_preferences.dart'
|
|||||||
as _i135;
|
as _i135;
|
||||||
import 'package:apskel_pos_flutter_v2/common/network/network_client.dart'
|
import 'package:apskel_pos_flutter_v2/common/network/network_client.dart'
|
||||||
as _i171;
|
as _i171;
|
||||||
|
import 'package:apskel_pos_flutter_v2/domain/analytic/analytic.dart' as _i346;
|
||||||
import 'package:apskel_pos_flutter_v2/domain/auth/auth.dart' as _i776;
|
import 'package:apskel_pos_flutter_v2/domain/auth/auth.dart' as _i776;
|
||||||
import 'package:apskel_pos_flutter_v2/domain/category/category.dart' as _i502;
|
import 'package:apskel_pos_flutter_v2/domain/category/category.dart' as _i502;
|
||||||
import 'package:apskel_pos_flutter_v2/domain/customer/customer.dart' as _i143;
|
import 'package:apskel_pos_flutter_v2/domain/customer/customer.dart' as _i143;
|
||||||
@ -64,6 +67,10 @@ import 'package:apskel_pos_flutter_v2/domain/payment_method/payment_method.dart'
|
|||||||
import 'package:apskel_pos_flutter_v2/domain/product/product.dart' as _i44;
|
import 'package:apskel_pos_flutter_v2/domain/product/product.dart' as _i44;
|
||||||
import 'package:apskel_pos_flutter_v2/domain/table/table.dart' as _i983;
|
import 'package:apskel_pos_flutter_v2/domain/table/table.dart' as _i983;
|
||||||
import 'package:apskel_pos_flutter_v2/env.dart' as _i923;
|
import 'package:apskel_pos_flutter_v2/env.dart' as _i923;
|
||||||
|
import 'package:apskel_pos_flutter_v2/infrastructure/analytic/datasources/remote_data_provider.dart'
|
||||||
|
as _i708;
|
||||||
|
import 'package:apskel_pos_flutter_v2/infrastructure/analytic/repositories/analytic_repository.dart'
|
||||||
|
as _i288;
|
||||||
import 'package:apskel_pos_flutter_v2/infrastructure/auth/datasources/local_data_provider.dart'
|
import 'package:apskel_pos_flutter_v2/infrastructure/auth/datasources/local_data_provider.dart'
|
||||||
as _i204;
|
as _i204;
|
||||||
import 'package:apskel_pos_flutter_v2/infrastructure/auth/datasources/remote_data_provider.dart'
|
import 'package:apskel_pos_flutter_v2/infrastructure/auth/datasources/remote_data_provider.dart'
|
||||||
@ -182,6 +189,9 @@ extension GetItInjectableX on _i174.GetIt {
|
|||||||
gh.factory<_i841.CustomerRemoteDataProvider>(
|
gh.factory<_i841.CustomerRemoteDataProvider>(
|
||||||
() => _i841.CustomerRemoteDataProvider(gh<_i457.ApiClient>()),
|
() => _i841.CustomerRemoteDataProvider(gh<_i457.ApiClient>()),
|
||||||
);
|
);
|
||||||
|
gh.factory<_i708.AnalyticRemoteDataProvider>(
|
||||||
|
() => _i708.AnalyticRemoteDataProvider(gh<_i457.ApiClient>()),
|
||||||
|
);
|
||||||
gh.factory<_i776.IAuthRepository>(
|
gh.factory<_i776.IAuthRepository>(
|
||||||
() => _i941.AuthRepository(
|
() => _i941.AuthRepository(
|
||||||
gh<_i370.AuthRemoteDataProvider>(),
|
gh<_i370.AuthRemoteDataProvider>(),
|
||||||
@ -256,6 +266,9 @@ extension GetItInjectableX on _i174.GetIt {
|
|||||||
gh.factory<_i683.CustomerLoaderBloc>(
|
gh.factory<_i683.CustomerLoaderBloc>(
|
||||||
() => _i683.CustomerLoaderBloc(gh<_i143.ICustomerRepository>()),
|
() => _i683.CustomerLoaderBloc(gh<_i143.ICustomerRepository>()),
|
||||||
);
|
);
|
||||||
|
gh.factory<_i346.IAnalyticRepository>(
|
||||||
|
() => _i288.AnalyticRepository(gh<_i708.AnalyticRemoteDataProvider>()),
|
||||||
|
);
|
||||||
gh.factory<_i952.PaymentMethodLoaderBloc>(
|
gh.factory<_i952.PaymentMethodLoaderBloc>(
|
||||||
() => _i952.PaymentMethodLoaderBloc(gh<_i297.IPaymentMethodRepository>()),
|
() => _i952.PaymentMethodLoaderBloc(gh<_i297.IPaymentMethodRepository>()),
|
||||||
);
|
);
|
||||||
@ -277,6 +290,9 @@ extension GetItInjectableX on _i174.GetIt {
|
|||||||
gh<_i502.ICategoryRepository>(),
|
gh<_i502.ICategoryRepository>(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
gh.factory<_i80.DashboardAnalyticLoaderBloc>(
|
||||||
|
() => _i80.DashboardAnalyticLoaderBloc(gh<_i346.IAnalyticRepository>()),
|
||||||
|
);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,43 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../../../common/data/report_menu.dart';
|
||||||
|
import '../../../domain/analytic/analytic.dart';
|
||||||
|
import '../card/error_card.dart';
|
||||||
|
|
||||||
|
class AnalyticErrorStateWidget extends StatelessWidget {
|
||||||
|
final AnalyticFailure failure;
|
||||||
|
final ReportMenu menu;
|
||||||
|
final Function() onRefresh;
|
||||||
|
const AnalyticErrorStateWidget({
|
||||||
|
super.key,
|
||||||
|
required this.failure,
|
||||||
|
required this.menu,
|
||||||
|
required this.onRefresh,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return failure.maybeMap(
|
||||||
|
orElse: () => ErrorCard(
|
||||||
|
title: menu.title,
|
||||||
|
message: 'Terjadi kesalahan, Silakan coba lagi!',
|
||||||
|
onTap: onRefresh,
|
||||||
|
),
|
||||||
|
dynamicErrorMessage: (value) => ErrorCard(
|
||||||
|
title: menu.title,
|
||||||
|
message: value.erroMessage,
|
||||||
|
onTap: onRefresh,
|
||||||
|
),
|
||||||
|
serverError: (value) => ErrorCard(
|
||||||
|
title: menu.title,
|
||||||
|
message: 'Server Error, Silakan coba lagi!',
|
||||||
|
onTap: onRefresh,
|
||||||
|
),
|
||||||
|
unexpectedError: (value) => ErrorCard(
|
||||||
|
title: menu.title,
|
||||||
|
message: 'Terjadi kesalahan, Silakan coba lagi!',
|
||||||
|
onTap: onRefresh,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import '../../../../common/data/report_menu.dart';
|
||||||
|
import '../../../../common/extension/extension.dart';
|
||||||
|
import '../../../../common/theme/theme.dart';
|
||||||
|
import '../../spaces/space.dart';
|
||||||
|
|
||||||
|
class ReportHeader extends StatelessWidget {
|
||||||
|
final ReportMenu menu;
|
||||||
|
final DateTime endDate;
|
||||||
|
final DateTime startDate;
|
||||||
|
const ReportHeader({
|
||||||
|
super.key,
|
||||||
|
required this.menu,
|
||||||
|
required this.endDate,
|
||||||
|
required this.startDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
menu.title,
|
||||||
|
style: AppStyle.xl.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColor.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SpaceHeight(4),
|
||||||
|
Text(
|
||||||
|
menu.subtitle,
|
||||||
|
style: AppStyle.md.copyWith(color: AppColor.textSecondary),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.primary,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.calendar_today, color: Colors.white, size: 16),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
startDate.toSimpleMonthDate() == endDate.toSimpleMonthDate()
|
||||||
|
? startDate.toSimpleMonthDate()
|
||||||
|
: '${startDate.toSimpleMonthDate()} - ${endDate.toSimpleMonthDate()}',
|
||||||
|
style: AppStyle.sm.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../../../common/theme/theme.dart';
|
||||||
|
import '../../spaces/space.dart';
|
||||||
|
|
||||||
|
class ReportSummaryCard extends StatelessWidget {
|
||||||
|
final Color color;
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
final String value;
|
||||||
|
const ReportSummaryCard({
|
||||||
|
super.key,
|
||||||
|
required this.color,
|
||||||
|
required this.icon,
|
||||||
|
required this.title,
|
||||||
|
required this.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(18),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.white,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
border: Border.all(color: Colors.grey[200]!),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withOpacity(0.12),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: color, size: 22),
|
||||||
|
),
|
||||||
|
SpaceWidth(12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: AppStyle.xxl.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColor.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SpaceHeight(4),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: AppStyle.sm.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColor.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
import '../../../../../application/analytic/dashboard_analytic_loader/dashboard_analytic_loader_bloc.dart';
|
||||||
import '../../../../../application/report/report_bloc.dart';
|
import '../../../../../application/report/report_bloc.dart';
|
||||||
import '../../../../../common/data/report_menu.dart';
|
import '../../../../../common/data/report_menu.dart';
|
||||||
import '../../../../../common/theme/theme.dart';
|
import '../../../../../common/theme/theme.dart';
|
||||||
@ -11,6 +12,7 @@ import '../../../../components/page/page_title.dart';
|
|||||||
import '../../../../components/picker/date_range_picker.dart';
|
import '../../../../components/picker/date_range_picker.dart';
|
||||||
import '../../../../components/spaces/space.dart';
|
import '../../../../components/spaces/space.dart';
|
||||||
import '../../../../router/app_router.gr.dart';
|
import '../../../../router/app_router.gr.dart';
|
||||||
|
import 'sections/report_dashboard_section.dart';
|
||||||
import 'widgets/report_menu_card.dart';
|
import 'widgets/report_menu_card.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
@ -27,6 +29,7 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
|
|||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
PageTitle(
|
PageTitle(
|
||||||
|
isBack: false,
|
||||||
title: 'Report',
|
title: 'Report',
|
||||||
subtitle: state.rangeDateFormatted,
|
subtitle: state.rangeDateFormatted,
|
||||||
actionWidget: [
|
actionWidget: [
|
||||||
@ -51,6 +54,11 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
|
|||||||
),
|
),
|
||||||
SpaceWidth(8),
|
SpaceWidth(8),
|
||||||
AppIconButton(icons: Icons.download_outlined, onTap: () {}),
|
AppIconButton(icons: Icons.download_outlined, onTap: () {}),
|
||||||
|
SpaceWidth(8),
|
||||||
|
AppIconButton(
|
||||||
|
icons: Icons.refresh_outlined,
|
||||||
|
onTap: () => onFetched(context, state),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -81,6 +89,8 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
|
|||||||
title: reportMenus[index].title,
|
title: reportMenus[index].title,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
onFetched(context, state);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -94,7 +104,20 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
|
|||||||
child: Material(
|
child: Material(
|
||||||
color: AppColor.background,
|
color: AppColor.background,
|
||||||
child: switch (state.selectedMenu) {
|
child: switch (state.selectedMenu) {
|
||||||
0 => Text(state.title),
|
0 =>
|
||||||
|
BlocBuilder<
|
||||||
|
DashboardAnalyticLoaderBloc,
|
||||||
|
DashboardAnalyticLoaderState
|
||||||
|
>(
|
||||||
|
builder: (context, dashboard) {
|
||||||
|
return ReportDashboardSection(
|
||||||
|
menu: reportMenus[state.selectedMenu],
|
||||||
|
state: dashboard,
|
||||||
|
startDate: state.startDate,
|
||||||
|
endDate: state.endDate,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
1 => Text(state.title),
|
1 => Text(state.title),
|
||||||
2 => Text(state.title),
|
2 => Text(state.title),
|
||||||
3 => Text(state.title),
|
3 => Text(state.title),
|
||||||
@ -117,7 +140,41 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void onFetched(BuildContext context, ReportState state) {
|
||||||
Widget wrappedRoute(BuildContext context) =>
|
switch (state.selectedMenu) {
|
||||||
BlocProvider(create: (context) => getIt<ReportBloc>(), child: this);
|
case 0:
|
||||||
|
return context.read<DashboardAnalyticLoaderBloc>().add(
|
||||||
|
DashboardAnalyticLoaderEvent.fetched(
|
||||||
|
startDate: state.startDate,
|
||||||
|
endDate: state.endDate,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case 1:
|
||||||
|
case 2:
|
||||||
|
case 3:
|
||||||
|
case 4:
|
||||||
|
case 5:
|
||||||
|
case 6:
|
||||||
|
case 7:
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget wrappedRoute(BuildContext context) => MultiBlocProvider(
|
||||||
|
providers: [
|
||||||
|
BlocProvider(create: (context) => getIt<ReportBloc>()),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => getIt<DashboardAnalyticLoaderBloc>()
|
||||||
|
..add(
|
||||||
|
DashboardAnalyticLoaderEvent.fetched(
|
||||||
|
startDate: DateTime.now().subtract(const Duration(days: 30)),
|
||||||
|
endDate: DateTime.now(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: this,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,184 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
import '../../../../../../application/analytic/dashboard_analytic_loader/dashboard_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/dashboard/report_dashboard_order.dart';
|
||||||
|
import '../widgets/dashboard/report_dashboard_product_chart.dart';
|
||||||
|
import '../widgets/dashboard/report_dashboard_sales_chart.dart';
|
||||||
|
import '../widgets/dashboard/report_dashboard_top_product.dart';
|
||||||
|
|
||||||
|
class ReportDashboardSection extends StatelessWidget {
|
||||||
|
final ReportMenu menu;
|
||||||
|
final DashboardAnalyticLoaderState state;
|
||||||
|
final DateTime startDate;
|
||||||
|
final DateTime endDate;
|
||||||
|
|
||||||
|
const ReportDashboardSection({
|
||||||
|
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(
|
||||||
|
backgroundColor: AppColor.white,
|
||||||
|
color: AppColor.primary,
|
||||||
|
onRefresh: () {
|
||||||
|
context.read<DashboardAnalyticLoaderBloc>().add(
|
||||||
|
DashboardAnalyticLoaderEvent.fetched(
|
||||||
|
startDate: startDate,
|
||||||
|
endDate: endDate,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Future.value();
|
||||||
|
},
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: [
|
||||||
|
ReportHeader(menu: menu, startDate: startDate, endDate: endDate),
|
||||||
|
_buildSummary(),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: ReportDashboardSalesChart(
|
||||||
|
sales: List.of(state.dashboardAnalytic.recentSales)
|
||||||
|
..sort(
|
||||||
|
(a, b) => DateTime.parse(
|
||||||
|
a.date,
|
||||||
|
).compareTo(DateTime.parse(b.date)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SpaceWidth(12),
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: ReportDashboardProductChart(
|
||||||
|
data: state.dashboardAnalytic.topProducts,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: ReportDashboardTopProduct(
|
||||||
|
data: state.dashboardAnalytic.topProducts,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SpaceWidth(12),
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: ReportDashboardOrder(data: state.dashboardAnalytic),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(f) => AnalyticErrorStateWidget(
|
||||||
|
failure: f,
|
||||||
|
menu: menu,
|
||||||
|
onRefresh: () => context.read<DashboardAnalyticLoaderBloc>().add(
|
||||||
|
DashboardAnalyticLoaderEvent.fetched(
|
||||||
|
startDate: startDate,
|
||||||
|
endDate: endDate,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Padding _buildSummary() {
|
||||||
|
final successfulOrders =
|
||||||
|
state.dashboardAnalytic.overview.totalOrders -
|
||||||
|
state.dashboardAnalytic.overview.voidedOrders -
|
||||||
|
state.dashboardAnalytic.overview.refundedOrders;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ReportSummaryCard(
|
||||||
|
color: AppColor.success,
|
||||||
|
icon: Icons.trending_up,
|
||||||
|
title: 'Total Penjualan',
|
||||||
|
value: state
|
||||||
|
.dashboardAnalytic
|
||||||
|
.overview
|
||||||
|
.totalSales
|
||||||
|
.currencyFormatRpV2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SpaceWidth(12),
|
||||||
|
Expanded(
|
||||||
|
child: ReportSummaryCard(
|
||||||
|
color: AppColor.info,
|
||||||
|
icon: Icons.shopping_cart_outlined,
|
||||||
|
title: 'Total Pesanan',
|
||||||
|
value: state.dashboardAnalytic.overview.totalOrders
|
||||||
|
.toString(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SpaceHeight(12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ReportSummaryCard(
|
||||||
|
color: AppColor.warning,
|
||||||
|
icon: Icons.attach_money,
|
||||||
|
title: 'Rata-rata Pesanan',
|
||||||
|
value: state
|
||||||
|
.dashboardAnalytic
|
||||||
|
.overview
|
||||||
|
.averageOrderValue
|
||||||
|
.currencyFormatRpV2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SpaceWidth(12),
|
||||||
|
Expanded(
|
||||||
|
child: ReportSummaryCard(
|
||||||
|
color: AppColor.primary,
|
||||||
|
icon: Icons.check_circle_outline,
|
||||||
|
title: 'Pesanan Sukses',
|
||||||
|
value: successfulOrders.toString(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,138 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../../../../../../common/theme/theme.dart';
|
||||||
|
import '../../../../../../../domain/analytic/analytic.dart';
|
||||||
|
import '../../../../../../components/spaces/space.dart';
|
||||||
|
|
||||||
|
class ReportDashboardOrder extends StatelessWidget {
|
||||||
|
final DashboardAnalytic data;
|
||||||
|
const ReportDashboardOrder({super.key, required this.data});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final successfulOrders =
|
||||||
|
data.overview.totalOrders -
|
||||||
|
data.overview.voidedOrders -
|
||||||
|
data.overview.refundedOrders;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Ringkasan Pesanan',
|
||||||
|
style: AppStyle.xl.copyWith(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColor.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SpaceHeight(20),
|
||||||
|
_buildSummaryItem(
|
||||||
|
'Total Pesanan',
|
||||||
|
'${data.overview.totalOrders}',
|
||||||
|
Icons.shopping_cart,
|
||||||
|
AppColor.info,
|
||||||
|
),
|
||||||
|
_buildSummaryItem(
|
||||||
|
'Pesanan Sukses',
|
||||||
|
'$successfulOrders',
|
||||||
|
Icons.check_circle,
|
||||||
|
AppColor.success,
|
||||||
|
),
|
||||||
|
_buildSummaryItem(
|
||||||
|
'Pesanan Dibatalkan',
|
||||||
|
'${data.overview.voidedOrders}',
|
||||||
|
Icons.cancel,
|
||||||
|
AppColor.error,
|
||||||
|
),
|
||||||
|
_buildSummaryItem(
|
||||||
|
'Pesanan Refund',
|
||||||
|
'${data.overview.refundedOrders}',
|
||||||
|
Icons.refresh,
|
||||||
|
AppColor.warning,
|
||||||
|
),
|
||||||
|
const SpaceHeight(20),
|
||||||
|
// Payment Methods
|
||||||
|
if (data.paymentMethods.isNotEmpty)
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.primary.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Metode Pembayaran',
|
||||||
|
style: AppStyle.sm.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: AppColor.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SpaceHeight(8),
|
||||||
|
...data.paymentMethods.map(
|
||||||
|
(method) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
method.paymentMethodType == 'cash'
|
||||||
|
? Icons.payments
|
||||||
|
: Icons.credit_card,
|
||||||
|
color: AppColor.primary,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
const SpaceWidth(8),
|
||||||
|
Text(
|
||||||
|
method.paymentMethodName,
|
||||||
|
style: AppStyle.md.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColor.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSummaryItem(
|
||||||
|
String title,
|
||||||
|
String value,
|
||||||
|
IconData icon,
|
||||||
|
Color color,
|
||||||
|
) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: color, size: 16),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(child: Text(title, style: const TextStyle(fontSize: 12))),
|
||||||
|
Text(value, style: AppStyle.md.copyWith(fontWeight: FontWeight.w600)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../../../../../../common/theme/theme.dart';
|
||||||
|
import '../../../../../../../domain/analytic/analytic.dart';
|
||||||
|
import '../../../../../../components/spaces/space.dart';
|
||||||
|
|
||||||
|
class ReportDashboardProductChart extends StatelessWidget {
|
||||||
|
final List<DashboardTopProduct> data;
|
||||||
|
const ReportDashboardProductChart({super.key, required this.data});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colors = [
|
||||||
|
AppColor.primary,
|
||||||
|
AppColor.secondary,
|
||||||
|
AppColor.info,
|
||||||
|
AppColor.warning,
|
||||||
|
AppColor.success,
|
||||||
|
];
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Distribusi Produk',
|
||||||
|
style: AppStyle.xl.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColor.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SpaceHeight(12),
|
||||||
|
SizedBox(
|
||||||
|
height: 160,
|
||||||
|
child: PieChart(
|
||||||
|
PieChartData(
|
||||||
|
sectionsSpace: 2,
|
||||||
|
centerSpaceRadius: 40,
|
||||||
|
sections: data.asMap().entries.map((entry) {
|
||||||
|
return PieChartSectionData(
|
||||||
|
color: colors[entry.key % colors.length],
|
||||||
|
value: entry.value.quantitySold.toDouble(),
|
||||||
|
title: '${entry.value.quantitySold}',
|
||||||
|
radius: 40,
|
||||||
|
titleStyle: AppStyle.sm.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SpaceHeight(16),
|
||||||
|
Column(
|
||||||
|
children: data.take(3).map((product) {
|
||||||
|
final index = data.indexOf(product);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors[index % colors.length],
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(product.productName, style: AppStyle.sm),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${product.quantitySold}',
|
||||||
|
style: AppStyle.sm.copyWith(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,170 @@
|
|||||||
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
import '../../../../../../../common/extension/extension.dart';
|
||||||
|
import '../../../../../../../common/theme/theme.dart';
|
||||||
|
import '../../../../../../../domain/analytic/analytic.dart';
|
||||||
|
import '../../../../../../components/spaces/space.dart';
|
||||||
|
|
||||||
|
class ReportDashboardSalesChart extends StatelessWidget {
|
||||||
|
final List<DashboardRecentSale> sales;
|
||||||
|
const ReportDashboardSalesChart({super.key, required this.sales});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (sales.isEmpty) {
|
||||||
|
return const Center(child: Text('Tidak ada data penjualan'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort agar tanggal berurutan
|
||||||
|
final sortedSales = List.of(
|
||||||
|
sales,
|
||||||
|
)..sort((a, b) => DateTime.parse(a.date).compareTo(DateTime.parse(b.date)));
|
||||||
|
|
||||||
|
// Convert ke FlSpot berdasarkan tanggal
|
||||||
|
final spots = sortedSales.map((sale) {
|
||||||
|
final date = DateTime.parse(sale.date);
|
||||||
|
return FlSpot(
|
||||||
|
date.millisecondsSinceEpoch.toDouble(),
|
||||||
|
sale.sales.toDouble(),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
// Batas min & max untuk X axis
|
||||||
|
final minX = spots.first.x;
|
||||||
|
final maxX = spots.last.x;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Tren Penjualan Harian',
|
||||||
|
style: AppStyle.xl.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColor.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SpaceHeight(12),
|
||||||
|
SizedBox(
|
||||||
|
height: 220,
|
||||||
|
child: LineChart(
|
||||||
|
LineChartData(
|
||||||
|
minX: minX,
|
||||||
|
maxX: maxX,
|
||||||
|
lineTouchData: LineTouchData(
|
||||||
|
touchTooltipData: LineTouchTooltipData(
|
||||||
|
fitInsideHorizontally: true,
|
||||||
|
fitInsideVertically: true,
|
||||||
|
getTooltipColor: (touchedSpot) => AppColor.primary,
|
||||||
|
getTooltipItems: (touchedSpots) {
|
||||||
|
return touchedSpots.map((spot) {
|
||||||
|
final date = DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
spot.x.toInt(),
|
||||||
|
);
|
||||||
|
final sale = spot.y.toInt();
|
||||||
|
|
||||||
|
return LineTooltipItem(
|
||||||
|
'${date.toFormattedDate()}\n ${sale.currencyFormatRpV2}',
|
||||||
|
AppStyle.xs.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
gridData: FlGridData(
|
||||||
|
show: true,
|
||||||
|
drawHorizontalLine: true,
|
||||||
|
drawVerticalLine: false,
|
||||||
|
horizontalInterval: 200000,
|
||||||
|
getDrawingHorizontalLine: (value) {
|
||||||
|
return FlLine(color: Colors.grey[200]!, strokeWidth: 1);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
titlesData: FlTitlesData(
|
||||||
|
leftTitles: AxisTitles(
|
||||||
|
sideTitles: SideTitles(
|
||||||
|
showTitles: true,
|
||||||
|
reservedSize: 60,
|
||||||
|
getTitlesWidget: (value, meta) {
|
||||||
|
return Text(
|
||||||
|
'${(value / 1000).toInt()}K',
|
||||||
|
style: AppStyle.xs.copyWith(
|
||||||
|
color: AppColor.textSecondary,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomTitles: AxisTitles(
|
||||||
|
sideTitles: SideTitles(
|
||||||
|
showTitles: true,
|
||||||
|
interval:
|
||||||
|
(maxX - minX) /
|
||||||
|
(sortedSales.length > 6 ? 6 : sortedSales.length),
|
||||||
|
getTitlesWidget: (value, meta) {
|
||||||
|
final date = DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
value.toInt(),
|
||||||
|
);
|
||||||
|
final formatter = DateFormat('dd MMM');
|
||||||
|
|
||||||
|
// tampilkan hanya jika tanggal cocok dengan titik data sebenarnya
|
||||||
|
final match = sortedSales.any((s) {
|
||||||
|
final d = DateTime.parse(s.date);
|
||||||
|
return d.difference(date).inDays.abs() < 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!match) return const SizedBox();
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: Text(
|
||||||
|
formatter.format(date),
|
||||||
|
style: AppStyle.xs.copyWith(
|
||||||
|
color: AppColor.textSecondary,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
rightTitles: const AxisTitles(
|
||||||
|
sideTitles: SideTitles(showTitles: false),
|
||||||
|
),
|
||||||
|
topTitles: const AxisTitles(
|
||||||
|
sideTitles: SideTitles(showTitles: false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
borderData: FlBorderData(show: false),
|
||||||
|
|
||||||
|
lineBarsData: [
|
||||||
|
LineChartBarData(
|
||||||
|
spots: spots,
|
||||||
|
isCurved: true,
|
||||||
|
color: AppColor.primary,
|
||||||
|
dotData: const FlDotData(show: true),
|
||||||
|
belowBarData: BarAreaData(
|
||||||
|
show: true,
|
||||||
|
color: AppColor.primary.withOpacity(0.1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../../../../../../common/extension/extension.dart';
|
||||||
|
import '../../../../../../../common/theme/theme.dart';
|
||||||
|
import '../../../../../../../domain/analytic/analytic.dart';
|
||||||
|
import '../../../../../../components/spaces/space.dart';
|
||||||
|
|
||||||
|
class ReportDashboardTopProduct extends StatelessWidget {
|
||||||
|
final List<DashboardTopProduct> data;
|
||||||
|
const ReportDashboardTopProduct({super.key, required this.data});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Produk Terlaris',
|
||||||
|
style: AppStyle.xl.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColor.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SpaceHeight(16),
|
||||||
|
Column(
|
||||||
|
children: data.map((product) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF8FAFC),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: AppColor.border),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
product.productName,
|
||||||
|
style: AppStyle.md.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SpaceHeight(2),
|
||||||
|
Text(
|
||||||
|
product.categoryName,
|
||||||
|
style: AppStyle.sm.copyWith(
|
||||||
|
fontSize: 12,
|
||||||
|
color: AppColor.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'${product.quantitySold} unit',
|
||||||
|
style: AppStyle.md.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SpaceHeight(2),
|
||||||
|
Text(
|
||||||
|
product.revenue.currencyFormatRpV2,
|
||||||
|
style: AppStyle.sm.copyWith(
|
||||||
|
fontSize: 12,
|
||||||
|
color: AppColor.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
pubspec.lock
16
pubspec.lock
@ -353,6 +353,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.6"
|
version: "5.0.6"
|
||||||
|
equatable:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: equatable
|
||||||
|
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.7"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -425,6 +433,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
|
fl_chart:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: fl_chart
|
||||||
|
sha256: "7ca9a40f4eb85949190e54087be8b4d6ac09dc4c54238d782a34cf1f7c011de9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|||||||
@ -38,6 +38,7 @@ dependencies:
|
|||||||
shimmer: ^3.0.0
|
shimmer: ^3.0.0
|
||||||
dropdown_search: ^5.0.6
|
dropdown_search: ^5.0.6
|
||||||
syncfusion_flutter_datepicker: ^31.2.3
|
syncfusion_flutter_datepicker: ^31.2.3
|
||||||
|
fl_chart: ^1.1.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user