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(
|
||||
title: 'Ringkasan Laporan Penjualan',
|
||||
startDate: DateTime.now(),
|
||||
startDate: DateTime.now().subtract(const Duration(days: 30)),
|
||||
endDate: DateTime.now(),
|
||||
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';
|
||||
}
|
||||
|
||||
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 orders = '/api/v1/orders';
|
||||
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
|
||||
|
||||
// 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/login_form/login_form_bloc.dart'
|
||||
as _i46;
|
||||
@ -54,6 +56,7 @@ import 'package:apskel_pos_flutter_v2/common/di/di_shared_preferences.dart'
|
||||
as _i135;
|
||||
import 'package:apskel_pos_flutter_v2/common/network/network_client.dart'
|
||||
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/category/category.dart' as _i502;
|
||||
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/table/table.dart' as _i983;
|
||||
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'
|
||||
as _i204;
|
||||
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>(
|
||||
() => _i841.CustomerRemoteDataProvider(gh<_i457.ApiClient>()),
|
||||
);
|
||||
gh.factory<_i708.AnalyticRemoteDataProvider>(
|
||||
() => _i708.AnalyticRemoteDataProvider(gh<_i457.ApiClient>()),
|
||||
);
|
||||
gh.factory<_i776.IAuthRepository>(
|
||||
() => _i941.AuthRepository(
|
||||
gh<_i370.AuthRemoteDataProvider>(),
|
||||
@ -256,6 +266,9 @@ extension GetItInjectableX on _i174.GetIt {
|
||||
gh.factory<_i683.CustomerLoaderBloc>(
|
||||
() => _i683.CustomerLoaderBloc(gh<_i143.ICustomerRepository>()),
|
||||
);
|
||||
gh.factory<_i346.IAnalyticRepository>(
|
||||
() => _i288.AnalyticRepository(gh<_i708.AnalyticRemoteDataProvider>()),
|
||||
);
|
||||
gh.factory<_i952.PaymentMethodLoaderBloc>(
|
||||
() => _i952.PaymentMethodLoaderBloc(gh<_i297.IPaymentMethodRepository>()),
|
||||
);
|
||||
@ -277,6 +290,9 @@ extension GetItInjectableX on _i174.GetIt {
|
||||
gh<_i502.ICategoryRepository>(),
|
||||
),
|
||||
);
|
||||
gh.factory<_i80.DashboardAnalyticLoaderBloc>(
|
||||
() => _i80.DashboardAnalyticLoaderBloc(gh<_i346.IAnalyticRepository>()),
|
||||
);
|
||||
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_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../application/analytic/dashboard_analytic_loader/dashboard_analytic_loader_bloc.dart';
|
||||
import '../../../../../application/report/report_bloc.dart';
|
||||
import '../../../../../common/data/report_menu.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/spaces/space.dart';
|
||||
import '../../../../router/app_router.gr.dart';
|
||||
import 'sections/report_dashboard_section.dart';
|
||||
import 'widgets/report_menu_card.dart';
|
||||
|
||||
@RoutePage()
|
||||
@ -27,6 +29,7 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
|
||||
return Column(
|
||||
children: [
|
||||
PageTitle(
|
||||
isBack: false,
|
||||
title: 'Report',
|
||||
subtitle: state.rangeDateFormatted,
|
||||
actionWidget: [
|
||||
@ -51,6 +54,11 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
|
||||
),
|
||||
SpaceWidth(8),
|
||||
AppIconButton(icons: Icons.download_outlined, onTap: () {}),
|
||||
SpaceWidth(8),
|
||||
AppIconButton(
|
||||
icons: Icons.refresh_outlined,
|
||||
onTap: () => onFetched(context, state),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
@ -81,6 +89,8 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
|
||||
title: reportMenus[index].title,
|
||||
),
|
||||
);
|
||||
|
||||
onFetched(context, state);
|
||||
}
|
||||
},
|
||||
),
|
||||
@ -94,7 +104,20 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
|
||||
child: Material(
|
||||
color: AppColor.background,
|
||||
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),
|
||||
2 => Text(state.title),
|
||||
3 => Text(state.title),
|
||||
@ -117,7 +140,41 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper {
|
||||
);
|
||||
}
|
||||
|
||||
void onFetched(BuildContext context, ReportState state) {
|
||||
switch (state.selectedMenu) {
|
||||
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) =>
|
||||
BlocProvider(create: (context) => getIt<ReportBloc>(), child: this);
|
||||
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"
|
||||
source: hosted
|
||||
version: "5.0.6"
|
||||
equatable:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: equatable
|
||||
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -425,6 +433,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
||||
@ -38,6 +38,7 @@ dependencies:
|
||||
shimmer: ^3.0.0
|
||||
dropdown_search: ^5.0.6
|
||||
syncfusion_flutter_datepicker: ^31.2.3
|
||||
fl_chart: ^1.1.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user