diff --git a/lib/application/analytic/product_analytic_loader/product_analytic_loader_bloc.dart b/lib/application/analytic/product_analytic_loader/product_analytic_loader_bloc.dart new file mode 100644 index 0000000..9753838 --- /dev/null +++ b/lib/application/analytic/product_analytic_loader/product_analytic_loader_bloc.dart @@ -0,0 +1,50 @@ +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 'product_analytic_loader_event.dart'; +part 'product_analytic_loader_state.dart'; +part 'product_analytic_loader_bloc.freezed.dart'; + +@injectable +class ProductAnalyticLoaderBloc + extends Bloc { + final IAnalyticRepository _analyticRepository; + ProductAnalyticLoaderBloc(this._analyticRepository) + : super(ProductAnalyticLoaderState.initial()) { + on(_onProductAnalyticLoaderEvent); + } + + Future _onProductAnalyticLoaderEvent( + ProductAnalyticLoaderEvent event, + Emitter emit, + ) { + return event.map( + fetched: (e) async { + emit(state.copyWith(isFetching: true, failureOption: none())); + + final result = await _analyticRepository.getProducts( + dateFrom: e.startDate, + dateTo: e.endDate, + ); + + await result.fold( + (failure) async { + emit( + state.copyWith( + isFetching: false, + failureOption: optionOf(failure), + ), + ); + }, + (products) async { + emit(state.copyWith(isFetching: false, productAnalytic: products)); + }, + ); + }, + ); + } +} diff --git a/lib/application/analytic/product_analytic_loader/product_analytic_loader_bloc.freezed.dart b/lib/application/analytic/product_analytic_loader/product_analytic_loader_bloc.freezed.dart new file mode 100644 index 0000000..313cf99 --- /dev/null +++ b/lib/application/analytic/product_analytic_loader/product_analytic_loader_bloc.freezed.dart @@ -0,0 +1,475 @@ +// 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 'product_analytic_loader_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +/// @nodoc +mixin _$ProductAnalyticLoaderEvent { + DateTime get startDate => throw _privateConstructorUsedError; + DateTime get endDate => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function(DateTime startDate, DateTime endDate) fetched, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(DateTime startDate, DateTime endDate)? fetched, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(DateTime startDate, DateTime endDate)? fetched, + required TResult orElse(), + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Fetched value) fetched, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Fetched value)? fetched, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Fetched value)? fetched, + required TResult orElse(), + }) => throw _privateConstructorUsedError; + + /// Create a copy of ProductAnalyticLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ProductAnalyticLoaderEventCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProductAnalyticLoaderEventCopyWith<$Res> { + factory $ProductAnalyticLoaderEventCopyWith( + ProductAnalyticLoaderEvent value, + $Res Function(ProductAnalyticLoaderEvent) then, + ) = + _$ProductAnalyticLoaderEventCopyWithImpl< + $Res, + ProductAnalyticLoaderEvent + >; + @useResult + $Res call({DateTime startDate, DateTime endDate}); +} + +/// @nodoc +class _$ProductAnalyticLoaderEventCopyWithImpl< + $Res, + $Val extends ProductAnalyticLoaderEvent +> + implements $ProductAnalyticLoaderEventCopyWith<$Res> { + _$ProductAnalyticLoaderEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ProductAnalyticLoaderEvent + /// 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 $ProductAnalyticLoaderEventCopyWith<$Res> { + factory _$$FetchedImplCopyWith( + _$FetchedImpl value, + $Res Function(_$FetchedImpl) then, + ) = __$$FetchedImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({DateTime startDate, DateTime endDate}); +} + +/// @nodoc +class __$$FetchedImplCopyWithImpl<$Res> + extends _$ProductAnalyticLoaderEventCopyWithImpl<$Res, _$FetchedImpl> + implements _$$FetchedImplCopyWith<$Res> { + __$$FetchedImplCopyWithImpl( + _$FetchedImpl _value, + $Res Function(_$FetchedImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ProductAnalyticLoaderEvent + /// 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 'ProductAnalyticLoaderEvent.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 ProductAnalyticLoaderEvent + /// 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({ + required TResult Function(DateTime startDate, DateTime endDate) fetched, + }) { + return fetched(startDate, endDate); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(DateTime startDate, DateTime endDate)? fetched, + }) { + return fetched?.call(startDate, endDate); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(DateTime startDate, DateTime endDate)? fetched, + required TResult orElse(), + }) { + if (fetched != null) { + return fetched(startDate, endDate); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Fetched value) fetched, + }) { + return fetched(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Fetched value)? fetched, + }) { + return fetched?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Fetched value)? fetched, + required TResult orElse(), + }) { + if (fetched != null) { + return fetched(this); + } + return orElse(); + } +} + +abstract class _Fetched implements ProductAnalyticLoaderEvent { + const factory _Fetched({ + required final DateTime startDate, + required final DateTime endDate, + }) = _$FetchedImpl; + + @override + DateTime get startDate; + @override + DateTime get endDate; + + /// Create a copy of ProductAnalyticLoaderEvent + /// 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 _$ProductAnalyticLoaderState { + ProductAnalytic get productAnalytic => throw _privateConstructorUsedError; + Option get failureOption => + throw _privateConstructorUsedError; + bool get isFetching => throw _privateConstructorUsedError; + + /// Create a copy of ProductAnalyticLoaderState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ProductAnalyticLoaderStateCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProductAnalyticLoaderStateCopyWith<$Res> { + factory $ProductAnalyticLoaderStateCopyWith( + ProductAnalyticLoaderState value, + $Res Function(ProductAnalyticLoaderState) then, + ) = + _$ProductAnalyticLoaderStateCopyWithImpl< + $Res, + ProductAnalyticLoaderState + >; + @useResult + $Res call({ + ProductAnalytic productAnalytic, + Option failureOption, + bool isFetching, + }); + + $ProductAnalyticCopyWith<$Res> get productAnalytic; +} + +/// @nodoc +class _$ProductAnalyticLoaderStateCopyWithImpl< + $Res, + $Val extends ProductAnalyticLoaderState +> + implements $ProductAnalyticLoaderStateCopyWith<$Res> { + _$ProductAnalyticLoaderStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ProductAnalyticLoaderState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? productAnalytic = null, + Object? failureOption = null, + Object? isFetching = null, + }) { + return _then( + _value.copyWith( + productAnalytic: null == productAnalytic + ? _value.productAnalytic + : productAnalytic // ignore: cast_nullable_to_non_nullable + as ProductAnalytic, + failureOption: null == failureOption + ? _value.failureOption + : failureOption // ignore: cast_nullable_to_non_nullable + as Option, + isFetching: null == isFetching + ? _value.isFetching + : isFetching // ignore: cast_nullable_to_non_nullable + as bool, + ) + as $Val, + ); + } + + /// Create a copy of ProductAnalyticLoaderState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ProductAnalyticCopyWith<$Res> get productAnalytic { + return $ProductAnalyticCopyWith<$Res>(_value.productAnalytic, (value) { + return _then(_value.copyWith(productAnalytic: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$ProductAnalyticLoaderStateImplCopyWith<$Res> + implements $ProductAnalyticLoaderStateCopyWith<$Res> { + factory _$$ProductAnalyticLoaderStateImplCopyWith( + _$ProductAnalyticLoaderStateImpl value, + $Res Function(_$ProductAnalyticLoaderStateImpl) then, + ) = __$$ProductAnalyticLoaderStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + ProductAnalytic productAnalytic, + Option failureOption, + bool isFetching, + }); + + @override + $ProductAnalyticCopyWith<$Res> get productAnalytic; +} + +/// @nodoc +class __$$ProductAnalyticLoaderStateImplCopyWithImpl<$Res> + extends + _$ProductAnalyticLoaderStateCopyWithImpl< + $Res, + _$ProductAnalyticLoaderStateImpl + > + implements _$$ProductAnalyticLoaderStateImplCopyWith<$Res> { + __$$ProductAnalyticLoaderStateImplCopyWithImpl( + _$ProductAnalyticLoaderStateImpl _value, + $Res Function(_$ProductAnalyticLoaderStateImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ProductAnalyticLoaderState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? productAnalytic = null, + Object? failureOption = null, + Object? isFetching = null, + }) { + return _then( + _$ProductAnalyticLoaderStateImpl( + productAnalytic: null == productAnalytic + ? _value.productAnalytic + : productAnalytic // ignore: cast_nullable_to_non_nullable + as ProductAnalytic, + failureOption: null == failureOption + ? _value.failureOption + : failureOption // ignore: cast_nullable_to_non_nullable + as Option, + isFetching: null == isFetching + ? _value.isFetching + : isFetching // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} + +/// @nodoc + +class _$ProductAnalyticLoaderStateImpl implements _ProductAnalyticLoaderState { + _$ProductAnalyticLoaderStateImpl({ + required this.productAnalytic, + required this.failureOption, + this.isFetching = false, + }); + + @override + final ProductAnalytic productAnalytic; + @override + final Option failureOption; + @override + @JsonKey() + final bool isFetching; + + @override + String toString() { + return 'ProductAnalyticLoaderState(productAnalytic: $productAnalytic, failureOption: $failureOption, isFetching: $isFetching)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ProductAnalyticLoaderStateImpl && + (identical(other.productAnalytic, productAnalytic) || + other.productAnalytic == productAnalytic) && + (identical(other.failureOption, failureOption) || + other.failureOption == failureOption) && + (identical(other.isFetching, isFetching) || + other.isFetching == isFetching)); + } + + @override + int get hashCode => + Object.hash(runtimeType, productAnalytic, failureOption, isFetching); + + /// Create a copy of ProductAnalyticLoaderState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ProductAnalyticLoaderStateImplCopyWith<_$ProductAnalyticLoaderStateImpl> + get copyWith => + __$$ProductAnalyticLoaderStateImplCopyWithImpl< + _$ProductAnalyticLoaderStateImpl + >(this, _$identity); +} + +abstract class _ProductAnalyticLoaderState + implements ProductAnalyticLoaderState { + factory _ProductAnalyticLoaderState({ + required final ProductAnalytic productAnalytic, + required final Option failureOption, + final bool isFetching, + }) = _$ProductAnalyticLoaderStateImpl; + + @override + ProductAnalytic get productAnalytic; + @override + Option get failureOption; + @override + bool get isFetching; + + /// Create a copy of ProductAnalyticLoaderState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ProductAnalyticLoaderStateImplCopyWith<_$ProductAnalyticLoaderStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/application/analytic/product_analytic_loader/product_analytic_loader_event.dart b/lib/application/analytic/product_analytic_loader/product_analytic_loader_event.dart new file mode 100644 index 0000000..8a953cd --- /dev/null +++ b/lib/application/analytic/product_analytic_loader/product_analytic_loader_event.dart @@ -0,0 +1,9 @@ +part of 'product_analytic_loader_bloc.dart'; + +@freezed +class ProductAnalyticLoaderEvent with _$ProductAnalyticLoaderEvent { + const factory ProductAnalyticLoaderEvent.fetched({ + required DateTime startDate, + required DateTime endDate, + }) = _Fetched; +} diff --git a/lib/application/analytic/product_analytic_loader/product_analytic_loader_state.dart b/lib/application/analytic/product_analytic_loader/product_analytic_loader_state.dart new file mode 100644 index 0000000..4271d50 --- /dev/null +++ b/lib/application/analytic/product_analytic_loader/product_analytic_loader_state.dart @@ -0,0 +1,15 @@ +part of 'product_analytic_loader_bloc.dart'; + +@freezed +class ProductAnalyticLoaderState with _$ProductAnalyticLoaderState { + factory ProductAnalyticLoaderState({ + required ProductAnalytic productAnalytic, + required Option failureOption, + @Default(false) bool isFetching, + }) = _ProductAnalyticLoaderState; + + factory ProductAnalyticLoaderState.initial() => ProductAnalyticLoaderState( + productAnalytic: ProductAnalytic.empty(), + failureOption: none(), + ); +} diff --git a/lib/common/url/api_path.dart b/lib/common/url/api_path.dart index 69523bd..172e640 100644 --- a/lib/common/url/api_path.dart +++ b/lib/common/url/api_path.dart @@ -10,4 +10,5 @@ class ApiPath { static const String payments = '/api/v1/payments'; static const String analyticDashboard = '/api/v1/analytics/dashboard'; static const String analyticSales = '/api/v1/analytics/sales'; + static const String analyticProducts = '/api/v1/analytics/products'; } diff --git a/lib/domain/analytic/analytic.dart b/lib/domain/analytic/analytic.dart index 1955b90..80e02e0 100644 --- a/lib/domain/analytic/analytic.dart +++ b/lib/domain/analytic/analytic.dart @@ -7,5 +7,6 @@ part 'analytic.freezed.dart'; part 'entities/dashboard_entity.dart'; part 'entities/sales_entity.dart'; +part 'entities/product_analytic_entity.dart'; part 'failures/analytic_failure.dart'; part 'repositories/i_analytic_repository.dart'; diff --git a/lib/domain/analytic/analytic.freezed.dart b/lib/domain/analytic/analytic.freezed.dart index 4598ae0..4c34010 100644 --- a/lib/domain/analytic/analytic.freezed.dart +++ b/lib/domain/analytic/analytic.freezed.dart @@ -2349,6 +2349,750 @@ abstract class _SalesAnalyticItem implements SalesAnalyticItem { throw _privateConstructorUsedError; } +/// @nodoc +mixin _$ProductAnalytic { + String get organizationId => throw _privateConstructorUsedError; + String get outletId => throw _privateConstructorUsedError; + DateTime get dateFrom => throw _privateConstructorUsedError; + DateTime get dateTo => throw _privateConstructorUsedError; + List get data => throw _privateConstructorUsedError; + + /// Create a copy of ProductAnalytic + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ProductAnalyticCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProductAnalyticCopyWith<$Res> { + factory $ProductAnalyticCopyWith( + ProductAnalytic value, + $Res Function(ProductAnalytic) then, + ) = _$ProductAnalyticCopyWithImpl<$Res, ProductAnalytic>; + @useResult + $Res call({ + String organizationId, + String outletId, + DateTime dateFrom, + DateTime dateTo, + List data, + }); +} + +/// @nodoc +class _$ProductAnalyticCopyWithImpl<$Res, $Val extends ProductAnalytic> + implements $ProductAnalyticCopyWith<$Res> { + _$ProductAnalyticCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ProductAnalytic + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? organizationId = null, + Object? outletId = null, + Object? dateFrom = null, + Object? dateTo = null, + Object? data = null, + }) { + return _then( + _value.copyWith( + organizationId: null == organizationId + ? _value.organizationId + : organizationId // ignore: cast_nullable_to_non_nullable + as String, + outletId: null == outletId + ? _value.outletId + : outletId // ignore: cast_nullable_to_non_nullable + as String, + dateFrom: null == dateFrom + ? _value.dateFrom + : dateFrom // ignore: cast_nullable_to_non_nullable + as DateTime, + dateTo: null == dateTo + ? _value.dateTo + : dateTo // ignore: cast_nullable_to_non_nullable + as DateTime, + data: null == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as List, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ProductAnalyticImplCopyWith<$Res> + implements $ProductAnalyticCopyWith<$Res> { + factory _$$ProductAnalyticImplCopyWith( + _$ProductAnalyticImpl value, + $Res Function(_$ProductAnalyticImpl) then, + ) = __$$ProductAnalyticImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String organizationId, + String outletId, + DateTime dateFrom, + DateTime dateTo, + List data, + }); +} + +/// @nodoc +class __$$ProductAnalyticImplCopyWithImpl<$Res> + extends _$ProductAnalyticCopyWithImpl<$Res, _$ProductAnalyticImpl> + implements _$$ProductAnalyticImplCopyWith<$Res> { + __$$ProductAnalyticImplCopyWithImpl( + _$ProductAnalyticImpl _value, + $Res Function(_$ProductAnalyticImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ProductAnalytic + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? organizationId = null, + Object? outletId = null, + Object? dateFrom = null, + Object? dateTo = null, + Object? data = null, + }) { + return _then( + _$ProductAnalyticImpl( + organizationId: null == organizationId + ? _value.organizationId + : organizationId // ignore: cast_nullable_to_non_nullable + as String, + outletId: null == outletId + ? _value.outletId + : outletId // ignore: cast_nullable_to_non_nullable + as String, + dateFrom: null == dateFrom + ? _value.dateFrom + : dateFrom // ignore: cast_nullable_to_non_nullable + as DateTime, + dateTo: null == dateTo + ? _value.dateTo + : dateTo // ignore: cast_nullable_to_non_nullable + as DateTime, + data: null == data + ? _value._data + : data // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} + +/// @nodoc + +class _$ProductAnalyticImpl extends _ProductAnalytic { + const _$ProductAnalyticImpl({ + required this.organizationId, + required this.outletId, + required this.dateFrom, + required this.dateTo, + required final List data, + }) : _data = data, + super._(); + + @override + final String organizationId; + @override + final String outletId; + @override + final DateTime dateFrom; + @override + final DateTime dateTo; + final List _data; + @override + List get data { + if (_data is EqualUnmodifiableListView) return _data; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_data); + } + + @override + String toString() { + return 'ProductAnalytic(organizationId: $organizationId, outletId: $outletId, dateFrom: $dateFrom, dateTo: $dateTo, data: $data)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ProductAnalyticImpl && + (identical(other.organizationId, organizationId) || + other.organizationId == organizationId) && + (identical(other.outletId, outletId) || + other.outletId == outletId) && + (identical(other.dateFrom, dateFrom) || + other.dateFrom == dateFrom) && + (identical(other.dateTo, dateTo) || other.dateTo == dateTo) && + const DeepCollectionEquality().equals(other._data, _data)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + organizationId, + outletId, + dateFrom, + dateTo, + const DeepCollectionEquality().hash(_data), + ); + + /// Create a copy of ProductAnalytic + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ProductAnalyticImplCopyWith<_$ProductAnalyticImpl> get copyWith => + __$$ProductAnalyticImplCopyWithImpl<_$ProductAnalyticImpl>( + this, + _$identity, + ); +} + +abstract class _ProductAnalytic extends ProductAnalytic { + const factory _ProductAnalytic({ + required final String organizationId, + required final String outletId, + required final DateTime dateFrom, + required final DateTime dateTo, + required final List data, + }) = _$ProductAnalyticImpl; + const _ProductAnalytic._() : super._(); + + @override + String get organizationId; + @override + String get outletId; + @override + DateTime get dateFrom; + @override + DateTime get dateTo; + @override + List get data; + + /// Create a copy of ProductAnalytic + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ProductAnalyticImplCopyWith<_$ProductAnalyticImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$ProductAnalyticItem { + String get productId => throw _privateConstructorUsedError; + String get productName => throw _privateConstructorUsedError; + String get categoryId => throw _privateConstructorUsedError; + String get categoryName => throw _privateConstructorUsedError; + int get quantitySold => throw _privateConstructorUsedError; + int get revenue => throw _privateConstructorUsedError; + double get averagePrice => throw _privateConstructorUsedError; + int get orderCount => throw _privateConstructorUsedError; + + /// Create a copy of ProductAnalyticItem + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ProductAnalyticItemCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProductAnalyticItemCopyWith<$Res> { + factory $ProductAnalyticItemCopyWith( + ProductAnalyticItem value, + $Res Function(ProductAnalyticItem) then, + ) = _$ProductAnalyticItemCopyWithImpl<$Res, ProductAnalyticItem>; + @useResult + $Res call({ + String productId, + String productName, + String categoryId, + String categoryName, + int quantitySold, + int revenue, + double averagePrice, + int orderCount, + }); +} + +/// @nodoc +class _$ProductAnalyticItemCopyWithImpl<$Res, $Val extends ProductAnalyticItem> + implements $ProductAnalyticItemCopyWith<$Res> { + _$ProductAnalyticItemCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ProductAnalyticItem + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? productId = null, + Object? productName = null, + Object? categoryId = null, + Object? categoryName = null, + Object? quantitySold = null, + Object? revenue = null, + Object? averagePrice = null, + Object? orderCount = null, + }) { + return _then( + _value.copyWith( + productId: null == productId + ? _value.productId + : productId // ignore: cast_nullable_to_non_nullable + as String, + productName: null == productName + ? _value.productName + : productName // ignore: cast_nullable_to_non_nullable + as String, + categoryId: null == categoryId + ? _value.categoryId + : categoryId // ignore: cast_nullable_to_non_nullable + as String, + categoryName: null == categoryName + ? _value.categoryName + : categoryName // ignore: cast_nullable_to_non_nullable + as String, + quantitySold: null == quantitySold + ? _value.quantitySold + : quantitySold // ignore: cast_nullable_to_non_nullable + as int, + revenue: null == revenue + ? _value.revenue + : revenue // ignore: cast_nullable_to_non_nullable + as int, + averagePrice: null == averagePrice + ? _value.averagePrice + : averagePrice // ignore: cast_nullable_to_non_nullable + as double, + orderCount: null == orderCount + ? _value.orderCount + : orderCount // ignore: cast_nullable_to_non_nullable + as int, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ProductAnalyticItemImplCopyWith<$Res> + implements $ProductAnalyticItemCopyWith<$Res> { + factory _$$ProductAnalyticItemImplCopyWith( + _$ProductAnalyticItemImpl value, + $Res Function(_$ProductAnalyticItemImpl) then, + ) = __$$ProductAnalyticItemImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String productId, + String productName, + String categoryId, + String categoryName, + int quantitySold, + int revenue, + double averagePrice, + int orderCount, + }); +} + +/// @nodoc +class __$$ProductAnalyticItemImplCopyWithImpl<$Res> + extends _$ProductAnalyticItemCopyWithImpl<$Res, _$ProductAnalyticItemImpl> + implements _$$ProductAnalyticItemImplCopyWith<$Res> { + __$$ProductAnalyticItemImplCopyWithImpl( + _$ProductAnalyticItemImpl _value, + $Res Function(_$ProductAnalyticItemImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ProductAnalyticItem + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? productId = null, + Object? productName = null, + Object? categoryId = null, + Object? categoryName = null, + Object? quantitySold = null, + Object? revenue = null, + Object? averagePrice = null, + Object? orderCount = null, + }) { + return _then( + _$ProductAnalyticItemImpl( + productId: null == productId + ? _value.productId + : productId // ignore: cast_nullable_to_non_nullable + as String, + productName: null == productName + ? _value.productName + : productName // ignore: cast_nullable_to_non_nullable + as String, + categoryId: null == categoryId + ? _value.categoryId + : categoryId // ignore: cast_nullable_to_non_nullable + as String, + categoryName: null == categoryName + ? _value.categoryName + : categoryName // ignore: cast_nullable_to_non_nullable + as String, + quantitySold: null == quantitySold + ? _value.quantitySold + : quantitySold // ignore: cast_nullable_to_non_nullable + as int, + revenue: null == revenue + ? _value.revenue + : revenue // ignore: cast_nullable_to_non_nullable + as int, + averagePrice: null == averagePrice + ? _value.averagePrice + : averagePrice // ignore: cast_nullable_to_non_nullable + as double, + orderCount: null == orderCount + ? _value.orderCount + : orderCount // ignore: cast_nullable_to_non_nullable + as int, + ), + ); + } +} + +/// @nodoc + +class _$ProductAnalyticItemImpl implements _ProductAnalyticItem { + const _$ProductAnalyticItemImpl({ + required this.productId, + required this.productName, + required this.categoryId, + required this.categoryName, + required this.quantitySold, + required this.revenue, + required this.averagePrice, + required this.orderCount, + }); + + @override + final String productId; + @override + final String productName; + @override + final String categoryId; + @override + final String categoryName; + @override + final int quantitySold; + @override + final int revenue; + @override + final double averagePrice; + @override + final int orderCount; + + @override + String toString() { + return 'ProductAnalyticItem(productId: $productId, productName: $productName, categoryId: $categoryId, categoryName: $categoryName, quantitySold: $quantitySold, revenue: $revenue, averagePrice: $averagePrice, orderCount: $orderCount)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ProductAnalyticItemImpl && + (identical(other.productId, productId) || + other.productId == productId) && + (identical(other.productName, productName) || + other.productName == productName) && + (identical(other.categoryId, categoryId) || + other.categoryId == categoryId) && + (identical(other.categoryName, categoryName) || + other.categoryName == categoryName) && + (identical(other.quantitySold, quantitySold) || + other.quantitySold == quantitySold) && + (identical(other.revenue, revenue) || other.revenue == revenue) && + (identical(other.averagePrice, averagePrice) || + other.averagePrice == averagePrice) && + (identical(other.orderCount, orderCount) || + other.orderCount == orderCount)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + productId, + productName, + categoryId, + categoryName, + quantitySold, + revenue, + averagePrice, + orderCount, + ); + + /// Create a copy of ProductAnalyticItem + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ProductAnalyticItemImplCopyWith<_$ProductAnalyticItemImpl> get copyWith => + __$$ProductAnalyticItemImplCopyWithImpl<_$ProductAnalyticItemImpl>( + this, + _$identity, + ); +} + +abstract class _ProductAnalyticItem implements ProductAnalyticItem { + const factory _ProductAnalyticItem({ + required final String productId, + required final String productName, + required final String categoryId, + required final String categoryName, + required final int quantitySold, + required final int revenue, + required final double averagePrice, + required final int orderCount, + }) = _$ProductAnalyticItemImpl; + + @override + String get productId; + @override + String get productName; + @override + String get categoryId; + @override + String get categoryName; + @override + int get quantitySold; + @override + int get revenue; + @override + double get averagePrice; + @override + int get orderCount; + + /// Create a copy of ProductAnalyticItem + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ProductAnalyticItemImplCopyWith<_$ProductAnalyticItemImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$ProductAnalyticCategoryItem { + String get categoryName => throw _privateConstructorUsedError; + int get productCount => throw _privateConstructorUsedError; + int get totalRevenue => throw _privateConstructorUsedError; + + /// Create a copy of ProductAnalyticCategoryItem + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ProductAnalyticCategoryItemCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProductAnalyticCategoryItemCopyWith<$Res> { + factory $ProductAnalyticCategoryItemCopyWith( + ProductAnalyticCategoryItem value, + $Res Function(ProductAnalyticCategoryItem) then, + ) = + _$ProductAnalyticCategoryItemCopyWithImpl< + $Res, + ProductAnalyticCategoryItem + >; + @useResult + $Res call({String categoryName, int productCount, int totalRevenue}); +} + +/// @nodoc +class _$ProductAnalyticCategoryItemCopyWithImpl< + $Res, + $Val extends ProductAnalyticCategoryItem +> + implements $ProductAnalyticCategoryItemCopyWith<$Res> { + _$ProductAnalyticCategoryItemCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ProductAnalyticCategoryItem + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? categoryName = null, + Object? productCount = null, + Object? totalRevenue = null, + }) { + return _then( + _value.copyWith( + categoryName: null == categoryName + ? _value.categoryName + : categoryName // ignore: cast_nullable_to_non_nullable + as String, + productCount: null == productCount + ? _value.productCount + : productCount // ignore: cast_nullable_to_non_nullable + as int, + totalRevenue: null == totalRevenue + ? _value.totalRevenue + : totalRevenue // ignore: cast_nullable_to_non_nullable + as int, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ProductAnalyticCategoryItemImplCopyWith<$Res> + implements $ProductAnalyticCategoryItemCopyWith<$Res> { + factory _$$ProductAnalyticCategoryItemImplCopyWith( + _$ProductAnalyticCategoryItemImpl value, + $Res Function(_$ProductAnalyticCategoryItemImpl) then, + ) = __$$ProductAnalyticCategoryItemImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String categoryName, int productCount, int totalRevenue}); +} + +/// @nodoc +class __$$ProductAnalyticCategoryItemImplCopyWithImpl<$Res> + extends + _$ProductAnalyticCategoryItemCopyWithImpl< + $Res, + _$ProductAnalyticCategoryItemImpl + > + implements _$$ProductAnalyticCategoryItemImplCopyWith<$Res> { + __$$ProductAnalyticCategoryItemImplCopyWithImpl( + _$ProductAnalyticCategoryItemImpl _value, + $Res Function(_$ProductAnalyticCategoryItemImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ProductAnalyticCategoryItem + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? categoryName = null, + Object? productCount = null, + Object? totalRevenue = null, + }) { + return _then( + _$ProductAnalyticCategoryItemImpl( + categoryName: null == categoryName + ? _value.categoryName + : categoryName // ignore: cast_nullable_to_non_nullable + as String, + productCount: null == productCount + ? _value.productCount + : productCount // ignore: cast_nullable_to_non_nullable + as int, + totalRevenue: null == totalRevenue + ? _value.totalRevenue + : totalRevenue // ignore: cast_nullable_to_non_nullable + as int, + ), + ); + } +} + +/// @nodoc + +class _$ProductAnalyticCategoryItemImpl + implements _ProductAnalyticCategoryItem { + const _$ProductAnalyticCategoryItemImpl({ + required this.categoryName, + required this.productCount, + required this.totalRevenue, + }); + + @override + final String categoryName; + @override + final int productCount; + @override + final int totalRevenue; + + @override + String toString() { + return 'ProductAnalyticCategoryItem(categoryName: $categoryName, productCount: $productCount, totalRevenue: $totalRevenue)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ProductAnalyticCategoryItemImpl && + (identical(other.categoryName, categoryName) || + other.categoryName == categoryName) && + (identical(other.productCount, productCount) || + other.productCount == productCount) && + (identical(other.totalRevenue, totalRevenue) || + other.totalRevenue == totalRevenue)); + } + + @override + int get hashCode => + Object.hash(runtimeType, categoryName, productCount, totalRevenue); + + /// Create a copy of ProductAnalyticCategoryItem + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ProductAnalyticCategoryItemImplCopyWith<_$ProductAnalyticCategoryItemImpl> + get copyWith => + __$$ProductAnalyticCategoryItemImplCopyWithImpl< + _$ProductAnalyticCategoryItemImpl + >(this, _$identity); +} + +abstract class _ProductAnalyticCategoryItem + implements ProductAnalyticCategoryItem { + const factory _ProductAnalyticCategoryItem({ + required final String categoryName, + required final int productCount, + required final int totalRevenue, + }) = _$ProductAnalyticCategoryItemImpl; + + @override + String get categoryName; + @override + int get productCount; + @override + int get totalRevenue; + + /// Create a copy of ProductAnalyticCategoryItem + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ProductAnalyticCategoryItemImplCopyWith<_$ProductAnalyticCategoryItemImpl> + get copyWith => throw _privateConstructorUsedError; +} + /// @nodoc mixin _$AnalyticFailure { @optionalTypeArgs diff --git a/lib/domain/analytic/entities/product_analytic_entity.dart b/lib/domain/analytic/entities/product_analytic_entity.dart new file mode 100644 index 0000000..107bb6c --- /dev/null +++ b/lib/domain/analytic/entities/product_analytic_entity.dart @@ -0,0 +1,97 @@ +part of '../analytic.dart'; + +@freezed +class ProductAnalytic with _$ProductAnalytic { + const ProductAnalytic._(); + + const factory ProductAnalytic({ + required String organizationId, + required String outletId, + required DateTime dateFrom, + required DateTime dateTo, + required List data, + }) = _ProductAnalytic; + + factory ProductAnalytic.empty() => ProductAnalytic( + organizationId: '', + outletId: '', + dateFrom: DateTime.now(), + dateTo: DateTime.now(), + data: const [], + ); + + ProductAnalyticItem get bestProduct { + if (data.isEmpty) { + return ProductAnalyticItem.empty(); + } + + return data.first; + } + + List get categories { + final Map categoryMap = {}; + + for (final product in data) { + final key = product.categoryName; + + if (categoryMap.containsKey(key)) { + final existing = categoryMap[key]!; + categoryMap[key] = existing.copyWith( + productCount: existing.productCount + 1, + totalRevenue: existing.totalRevenue + product.revenue, + ); + } else { + categoryMap[key] = ProductAnalyticCategoryItem( + categoryName: product.categoryName, + productCount: 1, + totalRevenue: product.revenue, + ); + } + } + + final categorySummary = categoryMap.values.toList() + ..sort((a, b) => b.totalRevenue.compareTo(a.totalRevenue)); + + return categorySummary; + } +} + +@freezed +class ProductAnalyticItem with _$ProductAnalyticItem { + const factory ProductAnalyticItem({ + required String productId, + required String productName, + required String categoryId, + required String categoryName, + required int quantitySold, + required int revenue, + required double averagePrice, + required int orderCount, + }) = _ProductAnalyticItem; + + factory ProductAnalyticItem.empty() => const ProductAnalyticItem( + productId: '', + productName: '', + categoryId: '', + categoryName: '', + quantitySold: 0, + revenue: 0, + averagePrice: 0.0, + orderCount: 0, + ); +} + +@freezed +class ProductAnalyticCategoryItem with _$ProductAnalyticCategoryItem { + const factory ProductAnalyticCategoryItem({ + required String categoryName, + required int productCount, + required int totalRevenue, + }) = _ProductAnalyticCategoryItem; + + factory ProductAnalyticCategoryItem.empty() => ProductAnalyticCategoryItem( + categoryName: '', + productCount: 0, + totalRevenue: 0, + ); +} diff --git a/lib/domain/analytic/repositories/i_analytic_repository.dart b/lib/domain/analytic/repositories/i_analytic_repository.dart index f66003c..b75cdf6 100644 --- a/lib/domain/analytic/repositories/i_analytic_repository.dart +++ b/lib/domain/analytic/repositories/i_analytic_repository.dart @@ -9,4 +9,8 @@ abstract class IAnalyticRepository { required DateTime dateFrom, required DateTime dateTo, }); + Future> getProducts({ + required DateTime dateFrom, + required DateTime dateTo, + }); } diff --git a/lib/infrastructure/analytic/analytic_dtos.dart b/lib/infrastructure/analytic/analytic_dtos.dart index 2b3b7a4..925a625 100644 --- a/lib/infrastructure/analytic/analytic_dtos.dart +++ b/lib/infrastructure/analytic/analytic_dtos.dart @@ -7,3 +7,4 @@ part 'analytic_dtos.g.dart'; part 'dtos/dashboard_dto.dart'; part 'dtos/sales_dto.dart'; +part 'dtos/product_analytic_dto.dart'; diff --git a/lib/infrastructure/analytic/analytic_dtos.freezed.dart b/lib/infrastructure/analytic/analytic_dtos.freezed.dart index e1a8825..d19cd9f 100644 --- a/lib/infrastructure/analytic/analytic_dtos.freezed.dart +++ b/lib/infrastructure/analytic/analytic_dtos.freezed.dart @@ -2739,3 +2739,638 @@ abstract class _SalesAnalyticItemDto extends SalesAnalyticItemDto { _$$SalesAnalyticItemDtoImplCopyWith<_$SalesAnalyticItemDtoImpl> get copyWith => throw _privateConstructorUsedError; } + +ProductAnalyticDto _$ProductAnalyticDtoFromJson(Map json) { + return _ProductAnalyticDto.fromJson(json); +} + +/// @nodoc +mixin _$ProductAnalyticDto { + @JsonKey(name: "organization_id") + String? get organizationId => throw _privateConstructorUsedError; + @JsonKey(name: "outlet_id") + String? get outletId => throw _privateConstructorUsedError; + @JsonKey(name: "date_from") + DateTime? get dateFrom => throw _privateConstructorUsedError; + @JsonKey(name: "date_to") + DateTime? get dateTo => throw _privateConstructorUsedError; + @JsonKey(name: "data") + List? get data => throw _privateConstructorUsedError; + + /// Serializes this ProductAnalyticDto to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ProductAnalyticDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ProductAnalyticDtoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProductAnalyticDtoCopyWith<$Res> { + factory $ProductAnalyticDtoCopyWith( + ProductAnalyticDto value, + $Res Function(ProductAnalyticDto) then, + ) = _$ProductAnalyticDtoCopyWithImpl<$Res, ProductAnalyticDto>; + @useResult + $Res call({ + @JsonKey(name: "organization_id") String? organizationId, + @JsonKey(name: "outlet_id") String? outletId, + @JsonKey(name: "date_from") DateTime? dateFrom, + @JsonKey(name: "date_to") DateTime? dateTo, + @JsonKey(name: "data") List? data, + }); +} + +/// @nodoc +class _$ProductAnalyticDtoCopyWithImpl<$Res, $Val extends ProductAnalyticDto> + implements $ProductAnalyticDtoCopyWith<$Res> { + _$ProductAnalyticDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ProductAnalyticDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? organizationId = freezed, + Object? outletId = freezed, + Object? dateFrom = freezed, + Object? dateTo = freezed, + Object? data = freezed, + }) { + return _then( + _value.copyWith( + organizationId: freezed == organizationId + ? _value.organizationId + : organizationId // ignore: cast_nullable_to_non_nullable + as String?, + outletId: freezed == outletId + ? _value.outletId + : outletId // ignore: cast_nullable_to_non_nullable + as String?, + dateFrom: freezed == dateFrom + ? _value.dateFrom + : dateFrom // ignore: cast_nullable_to_non_nullable + as DateTime?, + dateTo: freezed == dateTo + ? _value.dateTo + : dateTo // ignore: cast_nullable_to_non_nullable + as DateTime?, + data: freezed == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as List?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ProductAnalyticDtoImplCopyWith<$Res> + implements $ProductAnalyticDtoCopyWith<$Res> { + factory _$$ProductAnalyticDtoImplCopyWith( + _$ProductAnalyticDtoImpl value, + $Res Function(_$ProductAnalyticDtoImpl) then, + ) = __$$ProductAnalyticDtoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: "organization_id") String? organizationId, + @JsonKey(name: "outlet_id") String? outletId, + @JsonKey(name: "date_from") DateTime? dateFrom, + @JsonKey(name: "date_to") DateTime? dateTo, + @JsonKey(name: "data") List? data, + }); +} + +/// @nodoc +class __$$ProductAnalyticDtoImplCopyWithImpl<$Res> + extends _$ProductAnalyticDtoCopyWithImpl<$Res, _$ProductAnalyticDtoImpl> + implements _$$ProductAnalyticDtoImplCopyWith<$Res> { + __$$ProductAnalyticDtoImplCopyWithImpl( + _$ProductAnalyticDtoImpl _value, + $Res Function(_$ProductAnalyticDtoImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ProductAnalyticDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? organizationId = freezed, + Object? outletId = freezed, + Object? dateFrom = freezed, + Object? dateTo = freezed, + Object? data = freezed, + }) { + return _then( + _$ProductAnalyticDtoImpl( + organizationId: freezed == organizationId + ? _value.organizationId + : organizationId // ignore: cast_nullable_to_non_nullable + as String?, + outletId: freezed == outletId + ? _value.outletId + : outletId // ignore: cast_nullable_to_non_nullable + as String?, + dateFrom: freezed == dateFrom + ? _value.dateFrom + : dateFrom // ignore: cast_nullable_to_non_nullable + as DateTime?, + dateTo: freezed == dateTo + ? _value.dateTo + : dateTo // ignore: cast_nullable_to_non_nullable + as DateTime?, + data: freezed == data + ? _value._data + : data // ignore: cast_nullable_to_non_nullable + as List?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$ProductAnalyticDtoImpl extends _ProductAnalyticDto { + const _$ProductAnalyticDtoImpl({ + @JsonKey(name: "organization_id") this.organizationId, + @JsonKey(name: "outlet_id") this.outletId, + @JsonKey(name: "date_from") this.dateFrom, + @JsonKey(name: "date_to") this.dateTo, + @JsonKey(name: "data") final List? data, + }) : _data = data, + super._(); + + factory _$ProductAnalyticDtoImpl.fromJson(Map json) => + _$$ProductAnalyticDtoImplFromJson(json); + + @override + @JsonKey(name: "organization_id") + final String? organizationId; + @override + @JsonKey(name: "outlet_id") + final String? outletId; + @override + @JsonKey(name: "date_from") + final DateTime? dateFrom; + @override + @JsonKey(name: "date_to") + final DateTime? dateTo; + final List? _data; + @override + @JsonKey(name: "data") + List? get data { + final value = _data; + if (value == null) return null; + if (_data is EqualUnmodifiableListView) return _data; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + String toString() { + return 'ProductAnalyticDto(organizationId: $organizationId, outletId: $outletId, dateFrom: $dateFrom, dateTo: $dateTo, data: $data)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ProductAnalyticDtoImpl && + (identical(other.organizationId, organizationId) || + other.organizationId == organizationId) && + (identical(other.outletId, outletId) || + other.outletId == outletId) && + (identical(other.dateFrom, dateFrom) || + other.dateFrom == dateFrom) && + (identical(other.dateTo, dateTo) || other.dateTo == dateTo) && + const DeepCollectionEquality().equals(other._data, _data)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + organizationId, + outletId, + dateFrom, + dateTo, + const DeepCollectionEquality().hash(_data), + ); + + /// Create a copy of ProductAnalyticDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ProductAnalyticDtoImplCopyWith<_$ProductAnalyticDtoImpl> get copyWith => + __$$ProductAnalyticDtoImplCopyWithImpl<_$ProductAnalyticDtoImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$ProductAnalyticDtoImplToJson(this); + } +} + +abstract class _ProductAnalyticDto extends ProductAnalyticDto { + const factory _ProductAnalyticDto({ + @JsonKey(name: "organization_id") final String? organizationId, + @JsonKey(name: "outlet_id") final String? outletId, + @JsonKey(name: "date_from") final DateTime? dateFrom, + @JsonKey(name: "date_to") final DateTime? dateTo, + @JsonKey(name: "data") final List? data, + }) = _$ProductAnalyticDtoImpl; + const _ProductAnalyticDto._() : super._(); + + factory _ProductAnalyticDto.fromJson(Map json) = + _$ProductAnalyticDtoImpl.fromJson; + + @override + @JsonKey(name: "organization_id") + String? get organizationId; + @override + @JsonKey(name: "outlet_id") + String? get outletId; + @override + @JsonKey(name: "date_from") + DateTime? get dateFrom; + @override + @JsonKey(name: "date_to") + DateTime? get dateTo; + @override + @JsonKey(name: "data") + List? get data; + + /// Create a copy of ProductAnalyticDto + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ProductAnalyticDtoImplCopyWith<_$ProductAnalyticDtoImpl> get copyWith => + throw _privateConstructorUsedError; +} + +ProductAnalyticItemDto _$ProductAnalyticItemDtoFromJson( + Map json, +) { + return _ProductAnalyticItemDto.fromJson(json); +} + +/// @nodoc +mixin _$ProductAnalyticItemDto { + @JsonKey(name: "product_id") + String? get productId => throw _privateConstructorUsedError; + @JsonKey(name: "product_name") + String? get productName => throw _privateConstructorUsedError; + @JsonKey(name: "category_id") + String? get categoryId => throw _privateConstructorUsedError; + @JsonKey(name: "category_name") + String? get categoryName => throw _privateConstructorUsedError; + @JsonKey(name: "quantity_sold") + int? get quantitySold => throw _privateConstructorUsedError; + @JsonKey(name: "revenue") + int? get revenue => throw _privateConstructorUsedError; + @JsonKey(name: "average_price") + double? get averagePrice => throw _privateConstructorUsedError; + @JsonKey(name: "order_count") + int? get orderCount => throw _privateConstructorUsedError; + + /// Serializes this ProductAnalyticItemDto to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ProductAnalyticItemDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ProductAnalyticItemDtoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ProductAnalyticItemDtoCopyWith<$Res> { + factory $ProductAnalyticItemDtoCopyWith( + ProductAnalyticItemDto value, + $Res Function(ProductAnalyticItemDto) then, + ) = _$ProductAnalyticItemDtoCopyWithImpl<$Res, ProductAnalyticItemDto>; + @useResult + $Res call({ + @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, + }); +} + +/// @nodoc +class _$ProductAnalyticItemDtoCopyWithImpl< + $Res, + $Val extends ProductAnalyticItemDto +> + implements $ProductAnalyticItemDtoCopyWith<$Res> { + _$ProductAnalyticItemDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ProductAnalyticItemDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? productId = freezed, + Object? productName = freezed, + Object? categoryId = freezed, + Object? categoryName = freezed, + Object? quantitySold = freezed, + Object? revenue = freezed, + Object? averagePrice = freezed, + Object? orderCount = freezed, + }) { + return _then( + _value.copyWith( + productId: freezed == productId + ? _value.productId + : productId // ignore: cast_nullable_to_non_nullable + as String?, + productName: freezed == productName + ? _value.productName + : productName // ignore: cast_nullable_to_non_nullable + as String?, + categoryId: freezed == categoryId + ? _value.categoryId + : categoryId // ignore: cast_nullable_to_non_nullable + as String?, + categoryName: freezed == categoryName + ? _value.categoryName + : categoryName // ignore: cast_nullable_to_non_nullable + as String?, + quantitySold: freezed == quantitySold + ? _value.quantitySold + : quantitySold // ignore: cast_nullable_to_non_nullable + as int?, + revenue: freezed == revenue + ? _value.revenue + : revenue // ignore: cast_nullable_to_non_nullable + as int?, + averagePrice: freezed == averagePrice + ? _value.averagePrice + : averagePrice // ignore: cast_nullable_to_non_nullable + as double?, + orderCount: freezed == orderCount + ? _value.orderCount + : orderCount // ignore: cast_nullable_to_non_nullable + as int?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ProductAnalyticItemDtoImplCopyWith<$Res> + implements $ProductAnalyticItemDtoCopyWith<$Res> { + factory _$$ProductAnalyticItemDtoImplCopyWith( + _$ProductAnalyticItemDtoImpl value, + $Res Function(_$ProductAnalyticItemDtoImpl) then, + ) = __$$ProductAnalyticItemDtoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @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, + }); +} + +/// @nodoc +class __$$ProductAnalyticItemDtoImplCopyWithImpl<$Res> + extends + _$ProductAnalyticItemDtoCopyWithImpl<$Res, _$ProductAnalyticItemDtoImpl> + implements _$$ProductAnalyticItemDtoImplCopyWith<$Res> { + __$$ProductAnalyticItemDtoImplCopyWithImpl( + _$ProductAnalyticItemDtoImpl _value, + $Res Function(_$ProductAnalyticItemDtoImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ProductAnalyticItemDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? productId = freezed, + Object? productName = freezed, + Object? categoryId = freezed, + Object? categoryName = freezed, + Object? quantitySold = freezed, + Object? revenue = freezed, + Object? averagePrice = freezed, + Object? orderCount = freezed, + }) { + return _then( + _$ProductAnalyticItemDtoImpl( + productId: freezed == productId + ? _value.productId + : productId // ignore: cast_nullable_to_non_nullable + as String?, + productName: freezed == productName + ? _value.productName + : productName // ignore: cast_nullable_to_non_nullable + as String?, + categoryId: freezed == categoryId + ? _value.categoryId + : categoryId // ignore: cast_nullable_to_non_nullable + as String?, + categoryName: freezed == categoryName + ? _value.categoryName + : categoryName // ignore: cast_nullable_to_non_nullable + as String?, + quantitySold: freezed == quantitySold + ? _value.quantitySold + : quantitySold // ignore: cast_nullable_to_non_nullable + as int?, + revenue: freezed == revenue + ? _value.revenue + : revenue // ignore: cast_nullable_to_non_nullable + as int?, + averagePrice: freezed == averagePrice + ? _value.averagePrice + : averagePrice // ignore: cast_nullable_to_non_nullable + as double?, + orderCount: freezed == orderCount + ? _value.orderCount + : orderCount // ignore: cast_nullable_to_non_nullable + as int?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$ProductAnalyticItemDtoImpl extends _ProductAnalyticItemDto { + const _$ProductAnalyticItemDtoImpl({ + @JsonKey(name: "product_id") this.productId, + @JsonKey(name: "product_name") this.productName, + @JsonKey(name: "category_id") this.categoryId, + @JsonKey(name: "category_name") this.categoryName, + @JsonKey(name: "quantity_sold") this.quantitySold, + @JsonKey(name: "revenue") this.revenue, + @JsonKey(name: "average_price") this.averagePrice, + @JsonKey(name: "order_count") this.orderCount, + }) : super._(); + + factory _$ProductAnalyticItemDtoImpl.fromJson(Map json) => + _$$ProductAnalyticItemDtoImplFromJson(json); + + @override + @JsonKey(name: "product_id") + final String? productId; + @override + @JsonKey(name: "product_name") + final String? productName; + @override + @JsonKey(name: "category_id") + final String? categoryId; + @override + @JsonKey(name: "category_name") + final String? categoryName; + @override + @JsonKey(name: "quantity_sold") + final int? quantitySold; + @override + @JsonKey(name: "revenue") + final int? revenue; + @override + @JsonKey(name: "average_price") + final double? averagePrice; + @override + @JsonKey(name: "order_count") + final int? orderCount; + + @override + String toString() { + return 'ProductAnalyticItemDto(productId: $productId, productName: $productName, categoryId: $categoryId, categoryName: $categoryName, quantitySold: $quantitySold, revenue: $revenue, averagePrice: $averagePrice, orderCount: $orderCount)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ProductAnalyticItemDtoImpl && + (identical(other.productId, productId) || + other.productId == productId) && + (identical(other.productName, productName) || + other.productName == productName) && + (identical(other.categoryId, categoryId) || + other.categoryId == categoryId) && + (identical(other.categoryName, categoryName) || + other.categoryName == categoryName) && + (identical(other.quantitySold, quantitySold) || + other.quantitySold == quantitySold) && + (identical(other.revenue, revenue) || other.revenue == revenue) && + (identical(other.averagePrice, averagePrice) || + other.averagePrice == averagePrice) && + (identical(other.orderCount, orderCount) || + other.orderCount == orderCount)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + productId, + productName, + categoryId, + categoryName, + quantitySold, + revenue, + averagePrice, + orderCount, + ); + + /// Create a copy of ProductAnalyticItemDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ProductAnalyticItemDtoImplCopyWith<_$ProductAnalyticItemDtoImpl> + get copyWith => + __$$ProductAnalyticItemDtoImplCopyWithImpl<_$ProductAnalyticItemDtoImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$ProductAnalyticItemDtoImplToJson(this); + } +} + +abstract class _ProductAnalyticItemDto extends ProductAnalyticItemDto { + const factory _ProductAnalyticItemDto({ + @JsonKey(name: "product_id") final String? productId, + @JsonKey(name: "product_name") final String? productName, + @JsonKey(name: "category_id") final String? categoryId, + @JsonKey(name: "category_name") final String? categoryName, + @JsonKey(name: "quantity_sold") final int? quantitySold, + @JsonKey(name: "revenue") final int? revenue, + @JsonKey(name: "average_price") final double? averagePrice, + @JsonKey(name: "order_count") final int? orderCount, + }) = _$ProductAnalyticItemDtoImpl; + const _ProductAnalyticItemDto._() : super._(); + + factory _ProductAnalyticItemDto.fromJson(Map json) = + _$ProductAnalyticItemDtoImpl.fromJson; + + @override + @JsonKey(name: "product_id") + String? get productId; + @override + @JsonKey(name: "product_name") + String? get productName; + @override + @JsonKey(name: "category_id") + String? get categoryId; + @override + @JsonKey(name: "category_name") + String? get categoryName; + @override + @JsonKey(name: "quantity_sold") + int? get quantitySold; + @override + @JsonKey(name: "revenue") + int? get revenue; + @override + @JsonKey(name: "average_price") + double? get averagePrice; + @override + @JsonKey(name: "order_count") + int? get orderCount; + + /// Create a copy of ProductAnalyticItemDto + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ProductAnalyticItemDtoImplCopyWith<_$ProductAnalyticItemDtoImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/infrastructure/analytic/analytic_dtos.g.dart b/lib/infrastructure/analytic/analytic_dtos.g.dart index 47a962a..ad65b56 100644 --- a/lib/infrastructure/analytic/analytic_dtos.g.dart +++ b/lib/infrastructure/analytic/analytic_dtos.g.dart @@ -217,3 +217,55 @@ Map _$$SalesAnalyticItemDtoImplToJson( 'discount': instance.discount, 'net_sales': instance.netSales, }; + +_$ProductAnalyticDtoImpl _$$ProductAnalyticDtoImplFromJson( + Map json, +) => _$ProductAnalyticDtoImpl( + organizationId: json['organization_id'] as String?, + outletId: json['outlet_id'] as String?, + dateFrom: json['date_from'] == null + ? null + : DateTime.parse(json['date_from'] as String), + dateTo: json['date_to'] == null + ? null + : DateTime.parse(json['date_to'] as String), + data: (json['data'] as List?) + ?.map((e) => ProductAnalyticItemDto.fromJson(e as Map)) + .toList(), +); + +Map _$$ProductAnalyticDtoImplToJson( + _$ProductAnalyticDtoImpl instance, +) => { + 'organization_id': instance.organizationId, + 'outlet_id': instance.outletId, + 'date_from': instance.dateFrom?.toIso8601String(), + 'date_to': instance.dateTo?.toIso8601String(), + 'data': instance.data, +}; + +_$ProductAnalyticItemDtoImpl _$$ProductAnalyticItemDtoImplFromJson( + Map json, +) => _$ProductAnalyticItemDtoImpl( + 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 _$$ProductAnalyticItemDtoImplToJson( + _$ProductAnalyticItemDtoImpl instance, +) => { + '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, +}; diff --git a/lib/infrastructure/analytic/datasources/remote_data_provider.dart b/lib/infrastructure/analytic/datasources/remote_data_provider.dart index 1dd0d12..6d15524 100644 --- a/lib/infrastructure/analytic/datasources/remote_data_provider.dart +++ b/lib/infrastructure/analytic/datasources/remote_data_provider.dart @@ -76,4 +76,33 @@ class AnalyticRemoteDataProvider { return DC.error(AnalyticFailure.serverError(e)); } } + + Future> fetchProducts({ + required DateTime dateFrom, + required DateTime dateTo, + }) async { + try { + final response = await _apiClient.get( + ApiPath.analyticProducts, + params: { + 'date_from': dateFrom.toServerDate(), + 'date_to': dateTo.toServerDate(), + }, + headers: getAuthorizationHeader(), + ); + + if (response.data['success'] == false) { + return DC.error(AnalyticFailure.unexpectedError()); + } + + final products = ProductAnalyticDto.fromJson( + response.data['data'] as Map, + ); + + return DC.data(products); + } on ApiFailure catch (e, s) { + log('fetchProducts', name: _logName, error: e, stackTrace: s); + return DC.error(AnalyticFailure.serverError(e)); + } + } } diff --git a/lib/infrastructure/analytic/dtos/product_analytic_dto.dart b/lib/infrastructure/analytic/dtos/product_analytic_dto.dart new file mode 100644 index 0000000..62c1770 --- /dev/null +++ b/lib/infrastructure/analytic/dtos/product_analytic_dto.dart @@ -0,0 +1,57 @@ +part of '../analytic_dtos.dart'; + +@freezed +class ProductAnalyticDto with _$ProductAnalyticDto { + const ProductAnalyticDto._(); + + const factory ProductAnalyticDto({ + @JsonKey(name: "organization_id") String? organizationId, + @JsonKey(name: "outlet_id") String? outletId, + @JsonKey(name: "date_from") DateTime? dateFrom, + @JsonKey(name: "date_to") DateTime? dateTo, + @JsonKey(name: "data") List? data, + }) = _ProductAnalyticDto; + + factory ProductAnalyticDto.fromJson(Map json) => + _$ProductAnalyticDtoFromJson(json); + + // Optional mapping ke domain entity + ProductAnalytic toDomain() => ProductAnalytic( + organizationId: organizationId ?? '', + outletId: outletId ?? '', + dateFrom: dateFrom ?? DateTime.now(), + dateTo: dateTo ?? DateTime.now(), + data: data?.map((e) => e.toDomain()).toList() ?? [], + ); +} + +@freezed +class ProductAnalyticItemDto with _$ProductAnalyticItemDto { + const ProductAnalyticItemDto._(); + + const factory ProductAnalyticItemDto({ + @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, + }) = _ProductAnalyticItemDto; + + factory ProductAnalyticItemDto.fromJson(Map json) => + _$ProductAnalyticItemDtoFromJson(json); + + // Optional mapping ke domain entity + ProductAnalyticItem toDomain() => ProductAnalyticItem( + productId: productId ?? '', + productName: productName ?? '', + categoryId: categoryId ?? '', + categoryName: categoryName ?? '', + quantitySold: quantitySold ?? 0, + revenue: revenue ?? 0, + averagePrice: averagePrice ?? 0.0, + orderCount: orderCount ?? 0, + ); +} diff --git a/lib/infrastructure/analytic/repositories/analytic_repository.dart b/lib/infrastructure/analytic/repositories/analytic_repository.dart index 7deb4a6..de2f2c5 100644 --- a/lib/infrastructure/analytic/repositories/analytic_repository.dart +++ b/lib/infrastructure/analytic/repositories/analytic_repository.dart @@ -61,4 +61,28 @@ class AnalyticRepository implements IAnalyticRepository { return left(const AnalyticFailure.unexpectedError()); } } + + @override + Future> getProducts({ + required DateTime dateFrom, + required DateTime dateTo, + }) async { + try { + final result = await _dataProvider.fetchProducts( + dateFrom: dateFrom, + dateTo: dateTo, + ); + + if (result.hasError) { + return left(result.error!); + } + + final products = result.data!.toDomain(); + + return right(products); + } catch (e) { + log('getProductsError', name: _logName, error: e); + return left(const AnalyticFailure.unexpectedError()); + } + } } diff --git a/lib/injection.config.dart b/lib/injection.config.dart index 668f16a..67681aa 100644 --- a/lib/injection.config.dart +++ b/lib/injection.config.dart @@ -11,6 +11,8 @@ // 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/analytic/product_analytic_loader/product_analytic_loader_bloc.dart' + as _i268; import 'package:apskel_pos_flutter_v2/application/analytic/sales_analytic_loader/sales_analytic_loader_bloc.dart' as _i413; import 'package:apskel_pos_flutter_v2/application/auth/auth_bloc.dart' as _i343; @@ -298,6 +300,9 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i413.SalesAnalyticLoaderBloc>( () => _i413.SalesAnalyticLoaderBloc(gh<_i346.IAnalyticRepository>()), ); + gh.factory<_i268.ProductAnalyticLoaderBloc>( + () => _i268.ProductAnalyticLoaderBloc(gh<_i346.IAnalyticRepository>()), + ); return this; } } diff --git a/lib/presentation/components/widgets/report/report_summary_card.dart b/lib/presentation/components/widgets/report/report_summary_card.dart index 1b20a38..03324a2 100644 --- a/lib/presentation/components/widgets/report/report_summary_card.dart +++ b/lib/presentation/components/widgets/report/report_summary_card.dart @@ -8,18 +8,20 @@ class ReportSummaryCard extends StatelessWidget { final IconData icon; final String title; final String value; + final String? subtitle; const ReportSummaryCard({ super.key, required this.color, required this.icon, required this.title, required this.value, + this.subtitle, }); @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.all(18), + padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppColor.white, borderRadius: BorderRadius.circular(14), @@ -55,6 +57,16 @@ class ReportSummaryCard extends StatelessWidget { color: AppColor.textPrimary, ), ), + if (subtitle != null) ...[ + SpaceHeight(2), + Text( + subtitle!, + style: AppStyle.sm.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textSecondary, + ), + ), + ], ], ), ), diff --git a/lib/presentation/pages/main/pages/report/report_page.dart b/lib/presentation/pages/main/pages/report/report_page.dart index bb16fba..f89c41a 100644 --- a/lib/presentation/pages/main/pages/report/report_page.dart +++ b/lib/presentation/pages/main/pages/report/report_page.dart @@ -3,6 +3,7 @@ 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/analytic/product_analytic_loader/product_analytic_loader_bloc.dart'; import '../../../../../application/analytic/sales_analytic_loader/sales_analytic_loader_bloc.dart'; import '../../../../../application/report/report_bloc.dart'; import '../../../../../common/data/report_menu.dart'; @@ -14,6 +15,7 @@ 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 'sections/report_product_section.dart'; import 'sections/report_sales_section.dart'; import 'widgets/report_menu_card.dart'; @@ -135,7 +137,20 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper { ); }, ), - 3 => Text(state.title), + 3 => + BlocBuilder< + ProductAnalyticLoaderBloc, + ProductAnalyticLoaderState + >( + builder: (context, product) { + return ReportProductSection( + state: product, + menu: reportMenus[state.selectedMenu], + startDate: state.startDate, + endDate: state.endDate, + ); + }, + ), 4 => Text(state.title), 5 => Text(state.title), 6 => Text(state.title), @@ -173,6 +188,12 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper { ), ); case 3: + return context.read().add( + ProductAnalyticLoaderEvent.fetched( + startDate: state.startDate, + endDate: state.endDate, + ), + ); case 4: case 5: case 6: @@ -204,6 +225,15 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper { ), ), ), + BlocProvider( + create: (context) => getIt() + ..add( + ProductAnalyticLoaderEvent.fetched( + startDate: DateTime.now().subtract(const Duration(days: 30)), + endDate: DateTime.now(), + ), + ), + ), ], child: this, ); diff --git a/lib/presentation/pages/main/pages/report/sections/report_product_section.dart b/lib/presentation/pages/main/pages/report/sections/report_product_section.dart new file mode 100644 index 0000000..212f5a5 --- /dev/null +++ b/lib/presentation/pages/main/pages/report/sections/report_product_section.dart @@ -0,0 +1,314 @@ +import 'package:flutter/material.dart'; + +import '../../../../../../application/analytic/product_analytic_loader/product_analytic_loader_bloc.dart'; +import '../../../../../../common/data/report_menu.dart'; +import '../../../../../../common/extension/extension.dart'; +import '../../../../../../common/theme/theme.dart'; +import '../../../../../../domain/analytic/analytic.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'; + +class ReportProductSection extends StatelessWidget { + final ReportMenu menu; + final ProductAnalyticLoaderState state; + final DateTime startDate; + final DateTime endDate; + const ReportProductSection({ + super.key, + required this.state, + required this.menu, + required this.startDate, + required this.endDate, + }); + + @override + Widget build(BuildContext context) { + if (state.isFetching) { + return const Center(child: LoaderWithText()); + } + + return Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.all(16), + child: Column( + children: [ + ReportHeader( + menu: menu, + endDate: endDate, + startDate: startDate, + ), + Container( + height: 90, + margin: EdgeInsets.only(top: 16), + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: state.productAnalytic.categories.length, + itemBuilder: (context, index) { + final category = state.productAnalytic.categories[index]; + return Container( + width: 200, + padding: EdgeInsets.only( + right: + index == + state.productAnalytic.categories.length - 1 + ? 0 + : 12, + ), + child: ReportSummaryCard( + color: AppColor.primary, + icon: Icons.category_outlined, + title: category.categoryName, + value: category.totalRevenue.currencyFormatRpV2, + subtitle: "${category.productCount} Item", + ), + ); + }, + ), + ), + Container( + margin: EdgeInsets.only(top: 16), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColor.white, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Produk Berkinerja Terbaik', + style: AppStyle.lg.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textPrimary, + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: AppColor.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: AppColor.primary, + width: 1, + ), + ), + child: Text( + 'Berdasarkan Pendapatan', + style: AppStyle.xs.copyWith( + fontWeight: FontWeight.w500, + color: AppColor.primary, + fontSize: 10, + ), + ), + ), + ], + ), + SpaceHeight(12), + ListView.builder( + itemCount: state.productAnalytic.data.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + final product = state.productAnalytic.data[index]; + return _buildProductItem( + rank: index + 1, + product: product, + isTopPerformer: + product == state.productAnalytic.bestProduct, + categoryColor: AppColor.primary, + ); + }, + ), + ], + ), + ), + ], + ), + ), + ), + _buildBottomSummary(), + ], + ); + } + + Widget _buildBottomSummary() { + return Container( + padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16), + decoration: BoxDecoration(color: AppColor.white), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: AppColor.success.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: AppColor.success, width: 1), + ), + child: Row( + children: [ + Icon(Icons.star, color: AppColor.success, size: 16), + const SizedBox(width: 8), + Expanded( + child: Text( + '${state.productAnalytic.bestProduct.productName} memimpin dengan ${state.productAnalytic.bestProduct.quantitySold} unit terjual dan pendapatan ${state.productAnalytic.bestProduct.revenue.currencyFormatRpV2}', + style: AppStyle.sm.copyWith( + fontWeight: FontWeight.bold, + color: AppColor.success, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildProductItem({ + required int rank, + required ProductAnalyticItem product, + required bool isTopPerformer, + required Color categoryColor, + }) { + return Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: isTopPerformer + ? AppColor.primary.withOpacity(0.1) + : AppColor.white, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isTopPerformer ? AppColor.primary : AppColor.border, + width: isTopPerformer ? 2 : 1, + ), + ), + child: Row( + children: [ + // Rank Badge + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: isTopPerformer ? AppColor.primary : AppColor.textSecondary, + borderRadius: BorderRadius.circular(14), + ), + child: Center( + child: Text( + '$rank', + style: AppStyle.sm.copyWith( + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + ), + ), + + SpaceWidth(12), + + // Product Info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + product.productName, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textPrimary, + ), + ), + ), + Row( + children: [ + if (isTopPerformer) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + color: AppColor.success, + borderRadius: BorderRadius.circular(3), + ), + child: Text( + 'BEST', + style: AppStyle.xs.copyWith( + fontSize: 8, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + ), + ), + Text( + product.revenue.currencyFormatRpV2, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w700, + color: AppColor.primary, + ), + ), + ], + ), + ], + ), + SpaceHeight(6), + Row( + children: [ + // Category Badge + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: categoryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + product.categoryName, + style: AppStyle.xs.copyWith( + fontSize: 9, + fontWeight: FontWeight.w500, + color: categoryColor, + ), + ), + ), + + const SizedBox(width: 8), + + // Stats + Expanded( + child: Text( + '${product.quantitySold} units • ${product.orderCount} orders • Avg ${product.averagePrice.round()}', + style: AppStyle.xs.copyWith( + fontSize: 11, + fontWeight: FontWeight.w400, + color: AppColor.textSecondary, + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +}