From 2a44ce023e36f4c49af0242115caa5d2529ebd23 Mon Sep 17 00:00:00 2001 From: efrilm Date: Mon, 3 Nov 2025 23:24:16 +0700 Subject: [PATCH] report category --- .../category_analytic_loader_bloc.dart | 52 ++ ...category_analytic_loader_bloc.freezed.dart | 476 ++++++++++++++ .../category_analytic_loader_event.dart | 9 + .../category_analytic_loader_state.dart | 15 + lib/common/url/api_path.dart | 1 + lib/domain/analytic/analytic.dart | 1 + lib/domain/analytic/analytic.freezed.dart | 509 +++++++++++++++ .../entities/category_analytic_entity.dart | 50 ++ .../repositories/i_analytic_repository.dart | 4 + .../analytic/analytic_dtos.dart | 1 + .../analytic/analytic_dtos.freezed.dart | 585 ++++++++++++++++++ .../analytic/analytic_dtos.g.dart | 48 ++ .../datasources/remote_data_provider.dart | 29 + .../analytic/dtos/category_analytic_dto.dart | 53 ++ .../repositories/analytic_repository.dart | 24 + lib/injection.config.dart | 5 + .../pages/main/pages/report/report_page.dart | 33 +- .../sections/report_category_section.dart | 269 ++++++++ 18 files changed, 2163 insertions(+), 1 deletion(-) create mode 100644 lib/application/analytic/category_analytic_loader/category_analytic_loader_bloc.dart create mode 100644 lib/application/analytic/category_analytic_loader/category_analytic_loader_bloc.freezed.dart create mode 100644 lib/application/analytic/category_analytic_loader/category_analytic_loader_event.dart create mode 100644 lib/application/analytic/category_analytic_loader/category_analytic_loader_state.dart create mode 100644 lib/domain/analytic/entities/category_analytic_entity.dart create mode 100644 lib/infrastructure/analytic/dtos/category_analytic_dto.dart create mode 100644 lib/presentation/pages/main/pages/report/sections/report_category_section.dart diff --git a/lib/application/analytic/category_analytic_loader/category_analytic_loader_bloc.dart b/lib/application/analytic/category_analytic_loader/category_analytic_loader_bloc.dart new file mode 100644 index 0000000..1188faa --- /dev/null +++ b/lib/application/analytic/category_analytic_loader/category_analytic_loader_bloc.dart @@ -0,0 +1,52 @@ +import 'package:bloc/bloc.dart'; +import 'package:dartz/dartz.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:injectable/injectable.dart'; + +import '../../../domain/analytic/analytic.dart'; + +part 'category_analytic_loader_event.dart'; +part 'category_analytic_loader_state.dart'; +part 'category_analytic_loader_bloc.freezed.dart'; + +@injectable +class CategoryAnalyticLoaderBloc + extends Bloc { + final IAnalyticRepository _analyticRepository; + CategoryAnalyticLoaderBloc(this._analyticRepository) + : super(CategoryAnalyticLoaderState.initial()) { + on(_onCategoryAnalyticLoaderEvent); + } + + Future _onCategoryAnalyticLoaderEvent( + CategoryAnalyticLoaderEvent event, + Emitter emit, + ) { + return event.map( + fetched: (e) async { + emit(state.copyWith(isFetching: true, failureOption: none())); + + final result = await _analyticRepository.getCategories( + dateFrom: e.startDate, + dateTo: e.endDate, + ); + + await result.fold( + (failure) async { + emit( + state.copyWith( + isFetching: false, + failureOption: optionOf(failure), + ), + ); + }, + (categories) async { + emit( + state.copyWith(isFetching: false, categoryAnalytic: categories), + ); + }, + ); + }, + ); + } +} diff --git a/lib/application/analytic/category_analytic_loader/category_analytic_loader_bloc.freezed.dart b/lib/application/analytic/category_analytic_loader/category_analytic_loader_bloc.freezed.dart new file mode 100644 index 0000000..87d3b8e --- /dev/null +++ b/lib/application/analytic/category_analytic_loader/category_analytic_loader_bloc.freezed.dart @@ -0,0 +1,476 @@ +// 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 'category_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 _$CategoryAnalyticLoaderEvent { + 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 CategoryAnalyticLoaderEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CategoryAnalyticLoaderEventCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CategoryAnalyticLoaderEventCopyWith<$Res> { + factory $CategoryAnalyticLoaderEventCopyWith( + CategoryAnalyticLoaderEvent value, + $Res Function(CategoryAnalyticLoaderEvent) then, + ) = + _$CategoryAnalyticLoaderEventCopyWithImpl< + $Res, + CategoryAnalyticLoaderEvent + >; + @useResult + $Res call({DateTime startDate, DateTime endDate}); +} + +/// @nodoc +class _$CategoryAnalyticLoaderEventCopyWithImpl< + $Res, + $Val extends CategoryAnalyticLoaderEvent +> + implements $CategoryAnalyticLoaderEventCopyWith<$Res> { + _$CategoryAnalyticLoaderEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CategoryAnalyticLoaderEvent + /// 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 $CategoryAnalyticLoaderEventCopyWith<$Res> { + factory _$$FetchedImplCopyWith( + _$FetchedImpl value, + $Res Function(_$FetchedImpl) then, + ) = __$$FetchedImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({DateTime startDate, DateTime endDate}); +} + +/// @nodoc +class __$$FetchedImplCopyWithImpl<$Res> + extends _$CategoryAnalyticLoaderEventCopyWithImpl<$Res, _$FetchedImpl> + implements _$$FetchedImplCopyWith<$Res> { + __$$FetchedImplCopyWithImpl( + _$FetchedImpl _value, + $Res Function(_$FetchedImpl) _then, + ) : super(_value, _then); + + /// Create a copy of CategoryAnalyticLoaderEvent + /// 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 'CategoryAnalyticLoaderEvent.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 CategoryAnalyticLoaderEvent + /// 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 CategoryAnalyticLoaderEvent { + const factory _Fetched({ + required final DateTime startDate, + required final DateTime endDate, + }) = _$FetchedImpl; + + @override + DateTime get startDate; + @override + DateTime get endDate; + + /// Create a copy of CategoryAnalyticLoaderEvent + /// 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 _$CategoryAnalyticLoaderState { + CategoryAnalytic get categoryAnalytic => throw _privateConstructorUsedError; + Option get failureOption => + throw _privateConstructorUsedError; + bool get isFetching => throw _privateConstructorUsedError; + + /// Create a copy of CategoryAnalyticLoaderState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CategoryAnalyticLoaderStateCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CategoryAnalyticLoaderStateCopyWith<$Res> { + factory $CategoryAnalyticLoaderStateCopyWith( + CategoryAnalyticLoaderState value, + $Res Function(CategoryAnalyticLoaderState) then, + ) = + _$CategoryAnalyticLoaderStateCopyWithImpl< + $Res, + CategoryAnalyticLoaderState + >; + @useResult + $Res call({ + CategoryAnalytic categoryAnalytic, + Option failureOption, + bool isFetching, + }); + + $CategoryAnalyticCopyWith<$Res> get categoryAnalytic; +} + +/// @nodoc +class _$CategoryAnalyticLoaderStateCopyWithImpl< + $Res, + $Val extends CategoryAnalyticLoaderState +> + implements $CategoryAnalyticLoaderStateCopyWith<$Res> { + _$CategoryAnalyticLoaderStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CategoryAnalyticLoaderState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? categoryAnalytic = null, + Object? failureOption = null, + Object? isFetching = null, + }) { + return _then( + _value.copyWith( + categoryAnalytic: null == categoryAnalytic + ? _value.categoryAnalytic + : categoryAnalytic // ignore: cast_nullable_to_non_nullable + as CategoryAnalytic, + 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 CategoryAnalyticLoaderState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $CategoryAnalyticCopyWith<$Res> get categoryAnalytic { + return $CategoryAnalyticCopyWith<$Res>(_value.categoryAnalytic, (value) { + return _then(_value.copyWith(categoryAnalytic: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$CategoryAnalyticLoaderStateImplCopyWith<$Res> + implements $CategoryAnalyticLoaderStateCopyWith<$Res> { + factory _$$CategoryAnalyticLoaderStateImplCopyWith( + _$CategoryAnalyticLoaderStateImpl value, + $Res Function(_$CategoryAnalyticLoaderStateImpl) then, + ) = __$$CategoryAnalyticLoaderStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + CategoryAnalytic categoryAnalytic, + Option failureOption, + bool isFetching, + }); + + @override + $CategoryAnalyticCopyWith<$Res> get categoryAnalytic; +} + +/// @nodoc +class __$$CategoryAnalyticLoaderStateImplCopyWithImpl<$Res> + extends + _$CategoryAnalyticLoaderStateCopyWithImpl< + $Res, + _$CategoryAnalyticLoaderStateImpl + > + implements _$$CategoryAnalyticLoaderStateImplCopyWith<$Res> { + __$$CategoryAnalyticLoaderStateImplCopyWithImpl( + _$CategoryAnalyticLoaderStateImpl _value, + $Res Function(_$CategoryAnalyticLoaderStateImpl) _then, + ) : super(_value, _then); + + /// Create a copy of CategoryAnalyticLoaderState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? categoryAnalytic = null, + Object? failureOption = null, + Object? isFetching = null, + }) { + return _then( + _$CategoryAnalyticLoaderStateImpl( + categoryAnalytic: null == categoryAnalytic + ? _value.categoryAnalytic + : categoryAnalytic // ignore: cast_nullable_to_non_nullable + as CategoryAnalytic, + 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 _$CategoryAnalyticLoaderStateImpl + implements _CategoryAnalyticLoaderState { + _$CategoryAnalyticLoaderStateImpl({ + required this.categoryAnalytic, + required this.failureOption, + this.isFetching = false, + }); + + @override + final CategoryAnalytic categoryAnalytic; + @override + final Option failureOption; + @override + @JsonKey() + final bool isFetching; + + @override + String toString() { + return 'CategoryAnalyticLoaderState(categoryAnalytic: $categoryAnalytic, failureOption: $failureOption, isFetching: $isFetching)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CategoryAnalyticLoaderStateImpl && + (identical(other.categoryAnalytic, categoryAnalytic) || + other.categoryAnalytic == categoryAnalytic) && + (identical(other.failureOption, failureOption) || + other.failureOption == failureOption) && + (identical(other.isFetching, isFetching) || + other.isFetching == isFetching)); + } + + @override + int get hashCode => + Object.hash(runtimeType, categoryAnalytic, failureOption, isFetching); + + /// Create a copy of CategoryAnalyticLoaderState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CategoryAnalyticLoaderStateImplCopyWith<_$CategoryAnalyticLoaderStateImpl> + get copyWith => + __$$CategoryAnalyticLoaderStateImplCopyWithImpl< + _$CategoryAnalyticLoaderStateImpl + >(this, _$identity); +} + +abstract class _CategoryAnalyticLoaderState + implements CategoryAnalyticLoaderState { + factory _CategoryAnalyticLoaderState({ + required final CategoryAnalytic categoryAnalytic, + required final Option failureOption, + final bool isFetching, + }) = _$CategoryAnalyticLoaderStateImpl; + + @override + CategoryAnalytic get categoryAnalytic; + @override + Option get failureOption; + @override + bool get isFetching; + + /// Create a copy of CategoryAnalyticLoaderState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CategoryAnalyticLoaderStateImplCopyWith<_$CategoryAnalyticLoaderStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/application/analytic/category_analytic_loader/category_analytic_loader_event.dart b/lib/application/analytic/category_analytic_loader/category_analytic_loader_event.dart new file mode 100644 index 0000000..6dd1635 --- /dev/null +++ b/lib/application/analytic/category_analytic_loader/category_analytic_loader_event.dart @@ -0,0 +1,9 @@ +part of 'category_analytic_loader_bloc.dart'; + +@freezed +class CategoryAnalyticLoaderEvent with _$CategoryAnalyticLoaderEvent { + const factory CategoryAnalyticLoaderEvent.fetched({ + required DateTime startDate, + required DateTime endDate, + }) = _Fetched; +} diff --git a/lib/application/analytic/category_analytic_loader/category_analytic_loader_state.dart b/lib/application/analytic/category_analytic_loader/category_analytic_loader_state.dart new file mode 100644 index 0000000..824f59e --- /dev/null +++ b/lib/application/analytic/category_analytic_loader/category_analytic_loader_state.dart @@ -0,0 +1,15 @@ +part of 'category_analytic_loader_bloc.dart'; + +@freezed +class CategoryAnalyticLoaderState with _$CategoryAnalyticLoaderState { + factory CategoryAnalyticLoaderState({ + required CategoryAnalytic categoryAnalytic, + required Option failureOption, + @Default(false) bool isFetching, + }) = _CategoryAnalyticLoaderState; + + factory CategoryAnalyticLoaderState.initial() => CategoryAnalyticLoaderState( + categoryAnalytic: CategoryAnalytic.empty(), + failureOption: none(), + ); +} diff --git a/lib/common/url/api_path.dart b/lib/common/url/api_path.dart index 1f56b0a..cde8c8b 100644 --- a/lib/common/url/api_path.dart +++ b/lib/common/url/api_path.dart @@ -14,4 +14,5 @@ class ApiPath { static const String analyticPaymentMethods = '/api/v1/analytics/payment-methods'; static const String analyticProfitLoss = '/api/v1/analytics/profit-loss'; + static const String analyticCategories = '/api/v1/analytics/categories'; } diff --git a/lib/domain/analytic/analytic.dart b/lib/domain/analytic/analytic.dart index 3653bc9..32f0f10 100644 --- a/lib/domain/analytic/analytic.dart +++ b/lib/domain/analytic/analytic.dart @@ -10,5 +10,6 @@ part 'entities/sales_entity.dart'; part 'entities/product_analytic_entity.dart'; part 'entities/payment_method_analytic_entity.dart'; part 'entities/profit_loss_analytic_entity.dart'; +part 'entities/category_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 00b2356..9ac8695 100644 --- a/lib/domain/analytic/analytic.freezed.dart +++ b/lib/domain/analytic/analytic.freezed.dart @@ -5357,6 +5357,515 @@ abstract class _ProfitLossAnalyticProduct implements ProfitLossAnalyticProduct { get copyWith => throw _privateConstructorUsedError; } +/// @nodoc +mixin _$CategoryAnalytic { + 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 CategoryAnalytic + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CategoryAnalyticCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CategoryAnalyticCopyWith<$Res> { + factory $CategoryAnalyticCopyWith( + CategoryAnalytic value, + $Res Function(CategoryAnalytic) then, + ) = _$CategoryAnalyticCopyWithImpl<$Res, CategoryAnalytic>; + @useResult + $Res call({ + String organizationId, + String outletId, + DateTime dateFrom, + DateTime dateTo, + List data, + }); +} + +/// @nodoc +class _$CategoryAnalyticCopyWithImpl<$Res, $Val extends CategoryAnalytic> + implements $CategoryAnalyticCopyWith<$Res> { + _$CategoryAnalyticCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CategoryAnalytic + /// 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 _$$CategoryAnalyticImplCopyWith<$Res> + implements $CategoryAnalyticCopyWith<$Res> { + factory _$$CategoryAnalyticImplCopyWith( + _$CategoryAnalyticImpl value, + $Res Function(_$CategoryAnalyticImpl) then, + ) = __$$CategoryAnalyticImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String organizationId, + String outletId, + DateTime dateFrom, + DateTime dateTo, + List data, + }); +} + +/// @nodoc +class __$$CategoryAnalyticImplCopyWithImpl<$Res> + extends _$CategoryAnalyticCopyWithImpl<$Res, _$CategoryAnalyticImpl> + implements _$$CategoryAnalyticImplCopyWith<$Res> { + __$$CategoryAnalyticImplCopyWithImpl( + _$CategoryAnalyticImpl _value, + $Res Function(_$CategoryAnalyticImpl) _then, + ) : super(_value, _then); + + /// Create a copy of CategoryAnalytic + /// 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( + _$CategoryAnalyticImpl( + 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 _$CategoryAnalyticImpl extends _CategoryAnalytic { + const _$CategoryAnalyticImpl({ + 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 'CategoryAnalytic(organizationId: $organizationId, outletId: $outletId, dateFrom: $dateFrom, dateTo: $dateTo, data: $data)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CategoryAnalyticImpl && + (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 CategoryAnalytic + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CategoryAnalyticImplCopyWith<_$CategoryAnalyticImpl> get copyWith => + __$$CategoryAnalyticImplCopyWithImpl<_$CategoryAnalyticImpl>( + this, + _$identity, + ); +} + +abstract class _CategoryAnalytic extends CategoryAnalytic { + const factory _CategoryAnalytic({ + required final String organizationId, + required final String outletId, + required final DateTime dateFrom, + required final DateTime dateTo, + required final List data, + }) = _$CategoryAnalyticImpl; + const _CategoryAnalytic._() : 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 CategoryAnalytic + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CategoryAnalyticImplCopyWith<_$CategoryAnalyticImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$CategoryAnalyticItem { + String get categoryId => throw _privateConstructorUsedError; + String get categoryName => throw _privateConstructorUsedError; + int get totalRevenue => throw _privateConstructorUsedError; + int get totalQuantity => throw _privateConstructorUsedError; + int get productCount => throw _privateConstructorUsedError; + int get orderCount => throw _privateConstructorUsedError; + + /// Create a copy of CategoryAnalyticItem + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CategoryAnalyticItemCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CategoryAnalyticItemCopyWith<$Res> { + factory $CategoryAnalyticItemCopyWith( + CategoryAnalyticItem value, + $Res Function(CategoryAnalyticItem) then, + ) = _$CategoryAnalyticItemCopyWithImpl<$Res, CategoryAnalyticItem>; + @useResult + $Res call({ + String categoryId, + String categoryName, + int totalRevenue, + int totalQuantity, + int productCount, + int orderCount, + }); +} + +/// @nodoc +class _$CategoryAnalyticItemCopyWithImpl< + $Res, + $Val extends CategoryAnalyticItem +> + implements $CategoryAnalyticItemCopyWith<$Res> { + _$CategoryAnalyticItemCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CategoryAnalyticItem + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? categoryId = null, + Object? categoryName = null, + Object? totalRevenue = null, + Object? totalQuantity = null, + Object? productCount = null, + Object? orderCount = null, + }) { + return _then( + _value.copyWith( + 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, + totalRevenue: null == totalRevenue + ? _value.totalRevenue + : totalRevenue // ignore: cast_nullable_to_non_nullable + as int, + totalQuantity: null == totalQuantity + ? _value.totalQuantity + : totalQuantity // ignore: cast_nullable_to_non_nullable + as int, + productCount: null == productCount + ? _value.productCount + : productCount // ignore: cast_nullable_to_non_nullable + as int, + orderCount: null == orderCount + ? _value.orderCount + : orderCount // ignore: cast_nullable_to_non_nullable + as int, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$CategoryAnalyticItemImplCopyWith<$Res> + implements $CategoryAnalyticItemCopyWith<$Res> { + factory _$$CategoryAnalyticItemImplCopyWith( + _$CategoryAnalyticItemImpl value, + $Res Function(_$CategoryAnalyticItemImpl) then, + ) = __$$CategoryAnalyticItemImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String categoryId, + String categoryName, + int totalRevenue, + int totalQuantity, + int productCount, + int orderCount, + }); +} + +/// @nodoc +class __$$CategoryAnalyticItemImplCopyWithImpl<$Res> + extends _$CategoryAnalyticItemCopyWithImpl<$Res, _$CategoryAnalyticItemImpl> + implements _$$CategoryAnalyticItemImplCopyWith<$Res> { + __$$CategoryAnalyticItemImplCopyWithImpl( + _$CategoryAnalyticItemImpl _value, + $Res Function(_$CategoryAnalyticItemImpl) _then, + ) : super(_value, _then); + + /// Create a copy of CategoryAnalyticItem + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? categoryId = null, + Object? categoryName = null, + Object? totalRevenue = null, + Object? totalQuantity = null, + Object? productCount = null, + Object? orderCount = null, + }) { + return _then( + _$CategoryAnalyticItemImpl( + 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, + totalRevenue: null == totalRevenue + ? _value.totalRevenue + : totalRevenue // ignore: cast_nullable_to_non_nullable + as int, + totalQuantity: null == totalQuantity + ? _value.totalQuantity + : totalQuantity // ignore: cast_nullable_to_non_nullable + as int, + productCount: null == productCount + ? _value.productCount + : productCount // ignore: cast_nullable_to_non_nullable + as int, + orderCount: null == orderCount + ? _value.orderCount + : orderCount // ignore: cast_nullable_to_non_nullable + as int, + ), + ); + } +} + +/// @nodoc + +class _$CategoryAnalyticItemImpl implements _CategoryAnalyticItem { + const _$CategoryAnalyticItemImpl({ + required this.categoryId, + required this.categoryName, + required this.totalRevenue, + required this.totalQuantity, + required this.productCount, + required this.orderCount, + }); + + @override + final String categoryId; + @override + final String categoryName; + @override + final int totalRevenue; + @override + final int totalQuantity; + @override + final int productCount; + @override + final int orderCount; + + @override + String toString() { + return 'CategoryAnalyticItem(categoryId: $categoryId, categoryName: $categoryName, totalRevenue: $totalRevenue, totalQuantity: $totalQuantity, productCount: $productCount, orderCount: $orderCount)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CategoryAnalyticItemImpl && + (identical(other.categoryId, categoryId) || + other.categoryId == categoryId) && + (identical(other.categoryName, categoryName) || + other.categoryName == categoryName) && + (identical(other.totalRevenue, totalRevenue) || + other.totalRevenue == totalRevenue) && + (identical(other.totalQuantity, totalQuantity) || + other.totalQuantity == totalQuantity) && + (identical(other.productCount, productCount) || + other.productCount == productCount) && + (identical(other.orderCount, orderCount) || + other.orderCount == orderCount)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + categoryId, + categoryName, + totalRevenue, + totalQuantity, + productCount, + orderCount, + ); + + /// Create a copy of CategoryAnalyticItem + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CategoryAnalyticItemImplCopyWith<_$CategoryAnalyticItemImpl> + get copyWith => + __$$CategoryAnalyticItemImplCopyWithImpl<_$CategoryAnalyticItemImpl>( + this, + _$identity, + ); +} + +abstract class _CategoryAnalyticItem implements CategoryAnalyticItem { + const factory _CategoryAnalyticItem({ + required final String categoryId, + required final String categoryName, + required final int totalRevenue, + required final int totalQuantity, + required final int productCount, + required final int orderCount, + }) = _$CategoryAnalyticItemImpl; + + @override + String get categoryId; + @override + String get categoryName; + @override + int get totalRevenue; + @override + int get totalQuantity; + @override + int get productCount; + @override + int get orderCount; + + /// Create a copy of CategoryAnalyticItem + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CategoryAnalyticItemImplCopyWith<_$CategoryAnalyticItemImpl> + get copyWith => throw _privateConstructorUsedError; +} + /// @nodoc mixin _$AnalyticFailure { @optionalTypeArgs diff --git a/lib/domain/analytic/entities/category_analytic_entity.dart b/lib/domain/analytic/entities/category_analytic_entity.dart new file mode 100644 index 0000000..f975e9b --- /dev/null +++ b/lib/domain/analytic/entities/category_analytic_entity.dart @@ -0,0 +1,50 @@ +part of '../analytic.dart'; + +@freezed +class CategoryAnalytic with _$CategoryAnalytic { + const CategoryAnalytic._(); + + const factory CategoryAnalytic({ + required String organizationId, + required String outletId, + required DateTime dateFrom, + required DateTime dateTo, + required List data, + }) = _CategoryAnalytic; + + factory CategoryAnalytic.empty() => CategoryAnalytic( + organizationId: '', + outletId: '', + dateFrom: DateTime.now(), + dateTo: DateTime.now(), + data: const [], + ); + + int get totalRevenue => data.fold(0, (sum, item) => sum + item.totalRevenue); + + int get totalQuantity => + data.fold(0, (sum, item) => sum + item.totalQuantity); + + int get totalOrders => data.fold(0, (sum, item) => sum + item.orderCount); +} + +@freezed +class CategoryAnalyticItem with _$CategoryAnalyticItem { + const factory CategoryAnalyticItem({ + required String categoryId, + required String categoryName, + required int totalRevenue, + required int totalQuantity, + required int productCount, + required int orderCount, + }) = _CategoryAnalyticItem; + + factory CategoryAnalyticItem.empty() => const CategoryAnalyticItem( + categoryId: '', + categoryName: '', + totalRevenue: 0, + totalQuantity: 0, + productCount: 0, + orderCount: 0, + ); +} diff --git a/lib/domain/analytic/repositories/i_analytic_repository.dart b/lib/domain/analytic/repositories/i_analytic_repository.dart index 3bbd636..7fe14c1 100644 --- a/lib/domain/analytic/repositories/i_analytic_repository.dart +++ b/lib/domain/analytic/repositories/i_analytic_repository.dart @@ -21,4 +21,8 @@ abstract class IAnalyticRepository { required DateTime dateFrom, required DateTime dateTo, }); + Future> getCategories({ + required DateTime dateFrom, + required DateTime dateTo, + }); } diff --git a/lib/infrastructure/analytic/analytic_dtos.dart b/lib/infrastructure/analytic/analytic_dtos.dart index fdb6476..8f3fe63 100644 --- a/lib/infrastructure/analytic/analytic_dtos.dart +++ b/lib/infrastructure/analytic/analytic_dtos.dart @@ -10,3 +10,4 @@ part 'dtos/sales_dto.dart'; part 'dtos/product_analytic_dto.dart'; part 'dtos/payment_method_analytic_dto.dart'; part 'dtos/profit_loss_analytic_dto.dart'; +part 'dtos/category_analytic_dto.dart'; diff --git a/lib/infrastructure/analytic/analytic_dtos.freezed.dart b/lib/infrastructure/analytic/analytic_dtos.freezed.dart index 373381b..c3f90ea 100644 --- a/lib/infrastructure/analytic/analytic_dtos.freezed.dart +++ b/lib/infrastructure/analytic/analytic_dtos.freezed.dart @@ -6032,3 +6032,588 @@ abstract class _ProfitLossAnalyticProductDto > get copyWith => throw _privateConstructorUsedError; } + +CategoryAnalyticDto _$CategoryAnalyticDtoFromJson(Map json) { + return _CategoryAnalyticDto.fromJson(json); +} + +/// @nodoc +mixin _$CategoryAnalyticDto { + @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; + List? get data => throw _privateConstructorUsedError; + + /// Serializes this CategoryAnalyticDto to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of CategoryAnalyticDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CategoryAnalyticDtoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CategoryAnalyticDtoCopyWith<$Res> { + factory $CategoryAnalyticDtoCopyWith( + CategoryAnalyticDto value, + $Res Function(CategoryAnalyticDto) then, + ) = _$CategoryAnalyticDtoCopyWithImpl<$Res, CategoryAnalyticDto>; + @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, + List? data, + }); +} + +/// @nodoc +class _$CategoryAnalyticDtoCopyWithImpl<$Res, $Val extends CategoryAnalyticDto> + implements $CategoryAnalyticDtoCopyWith<$Res> { + _$CategoryAnalyticDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CategoryAnalyticDto + /// 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 _$$CategoryAnalyticDtoImplCopyWith<$Res> + implements $CategoryAnalyticDtoCopyWith<$Res> { + factory _$$CategoryAnalyticDtoImplCopyWith( + _$CategoryAnalyticDtoImpl value, + $Res Function(_$CategoryAnalyticDtoImpl) then, + ) = __$$CategoryAnalyticDtoImplCopyWithImpl<$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, + List? data, + }); +} + +/// @nodoc +class __$$CategoryAnalyticDtoImplCopyWithImpl<$Res> + extends _$CategoryAnalyticDtoCopyWithImpl<$Res, _$CategoryAnalyticDtoImpl> + implements _$$CategoryAnalyticDtoImplCopyWith<$Res> { + __$$CategoryAnalyticDtoImplCopyWithImpl( + _$CategoryAnalyticDtoImpl _value, + $Res Function(_$CategoryAnalyticDtoImpl) _then, + ) : super(_value, _then); + + /// Create a copy of CategoryAnalyticDto + /// 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( + _$CategoryAnalyticDtoImpl( + 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 _$CategoryAnalyticDtoImpl extends _CategoryAnalyticDto { + const _$CategoryAnalyticDtoImpl({ + @JsonKey(name: "organization_id") this.organizationId, + @JsonKey(name: "outlet_id") this.outletId, + @JsonKey(name: "date_from") this.dateFrom, + @JsonKey(name: "date_to") this.dateTo, + final List? data, + }) : _data = data, + super._(); + + factory _$CategoryAnalyticDtoImpl.fromJson(Map json) => + _$$CategoryAnalyticDtoImplFromJson(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 + 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 'CategoryAnalyticDto(organizationId: $organizationId, outletId: $outletId, dateFrom: $dateFrom, dateTo: $dateTo, data: $data)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CategoryAnalyticDtoImpl && + (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 CategoryAnalyticDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CategoryAnalyticDtoImplCopyWith<_$CategoryAnalyticDtoImpl> get copyWith => + __$$CategoryAnalyticDtoImplCopyWithImpl<_$CategoryAnalyticDtoImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$CategoryAnalyticDtoImplToJson(this); + } +} + +abstract class _CategoryAnalyticDto extends CategoryAnalyticDto { + const factory _CategoryAnalyticDto({ + @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, + final List? data, + }) = _$CategoryAnalyticDtoImpl; + const _CategoryAnalyticDto._() : super._(); + + factory _CategoryAnalyticDto.fromJson(Map json) = + _$CategoryAnalyticDtoImpl.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 + List? get data; + + /// Create a copy of CategoryAnalyticDto + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CategoryAnalyticDtoImplCopyWith<_$CategoryAnalyticDtoImpl> get copyWith => + throw _privateConstructorUsedError; +} + +CategoryAnalyticItemDto _$CategoryAnalyticItemDtoFromJson( + Map json, +) { + return _CategoryAnalyticItemDto.fromJson(json); +} + +/// @nodoc +mixin _$CategoryAnalyticItemDto { + @JsonKey(name: "category_id") + String? get categoryId => throw _privateConstructorUsedError; + @JsonKey(name: "category_name") + String? get categoryName => throw _privateConstructorUsedError; + @JsonKey(name: "total_revenue") + int? get totalRevenue => throw _privateConstructorUsedError; + @JsonKey(name: "total_quantity") + int? get totalQuantity => throw _privateConstructorUsedError; + @JsonKey(name: "product_count") + int? get productCount => throw _privateConstructorUsedError; + @JsonKey(name: "order_count") + int? get orderCount => throw _privateConstructorUsedError; + + /// Serializes this CategoryAnalyticItemDto to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of CategoryAnalyticItemDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CategoryAnalyticItemDtoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CategoryAnalyticItemDtoCopyWith<$Res> { + factory $CategoryAnalyticItemDtoCopyWith( + CategoryAnalyticItemDto value, + $Res Function(CategoryAnalyticItemDto) then, + ) = _$CategoryAnalyticItemDtoCopyWithImpl<$Res, CategoryAnalyticItemDto>; + @useResult + $Res call({ + @JsonKey(name: "category_id") String? categoryId, + @JsonKey(name: "category_name") String? categoryName, + @JsonKey(name: "total_revenue") int? totalRevenue, + @JsonKey(name: "total_quantity") int? totalQuantity, + @JsonKey(name: "product_count") int? productCount, + @JsonKey(name: "order_count") int? orderCount, + }); +} + +/// @nodoc +class _$CategoryAnalyticItemDtoCopyWithImpl< + $Res, + $Val extends CategoryAnalyticItemDto +> + implements $CategoryAnalyticItemDtoCopyWith<$Res> { + _$CategoryAnalyticItemDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CategoryAnalyticItemDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? categoryId = freezed, + Object? categoryName = freezed, + Object? totalRevenue = freezed, + Object? totalQuantity = freezed, + Object? productCount = freezed, + Object? orderCount = freezed, + }) { + return _then( + _value.copyWith( + 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?, + totalRevenue: freezed == totalRevenue + ? _value.totalRevenue + : totalRevenue // ignore: cast_nullable_to_non_nullable + as int?, + totalQuantity: freezed == totalQuantity + ? _value.totalQuantity + : totalQuantity // ignore: cast_nullable_to_non_nullable + as int?, + productCount: freezed == productCount + ? _value.productCount + : productCount // ignore: cast_nullable_to_non_nullable + as int?, + orderCount: freezed == orderCount + ? _value.orderCount + : orderCount // ignore: cast_nullable_to_non_nullable + as int?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$CategoryAnalyticItemDtoImplCopyWith<$Res> + implements $CategoryAnalyticItemDtoCopyWith<$Res> { + factory _$$CategoryAnalyticItemDtoImplCopyWith( + _$CategoryAnalyticItemDtoImpl value, + $Res Function(_$CategoryAnalyticItemDtoImpl) then, + ) = __$$CategoryAnalyticItemDtoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: "category_id") String? categoryId, + @JsonKey(name: "category_name") String? categoryName, + @JsonKey(name: "total_revenue") int? totalRevenue, + @JsonKey(name: "total_quantity") int? totalQuantity, + @JsonKey(name: "product_count") int? productCount, + @JsonKey(name: "order_count") int? orderCount, + }); +} + +/// @nodoc +class __$$CategoryAnalyticItemDtoImplCopyWithImpl<$Res> + extends + _$CategoryAnalyticItemDtoCopyWithImpl< + $Res, + _$CategoryAnalyticItemDtoImpl + > + implements _$$CategoryAnalyticItemDtoImplCopyWith<$Res> { + __$$CategoryAnalyticItemDtoImplCopyWithImpl( + _$CategoryAnalyticItemDtoImpl _value, + $Res Function(_$CategoryAnalyticItemDtoImpl) _then, + ) : super(_value, _then); + + /// Create a copy of CategoryAnalyticItemDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? categoryId = freezed, + Object? categoryName = freezed, + Object? totalRevenue = freezed, + Object? totalQuantity = freezed, + Object? productCount = freezed, + Object? orderCount = freezed, + }) { + return _then( + _$CategoryAnalyticItemDtoImpl( + 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?, + totalRevenue: freezed == totalRevenue + ? _value.totalRevenue + : totalRevenue // ignore: cast_nullable_to_non_nullable + as int?, + totalQuantity: freezed == totalQuantity + ? _value.totalQuantity + : totalQuantity // ignore: cast_nullable_to_non_nullable + as int?, + productCount: freezed == productCount + ? _value.productCount + : productCount // ignore: cast_nullable_to_non_nullable + as int?, + orderCount: freezed == orderCount + ? _value.orderCount + : orderCount // ignore: cast_nullable_to_non_nullable + as int?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$CategoryAnalyticItemDtoImpl extends _CategoryAnalyticItemDto { + const _$CategoryAnalyticItemDtoImpl({ + @JsonKey(name: "category_id") this.categoryId, + @JsonKey(name: "category_name") this.categoryName, + @JsonKey(name: "total_revenue") this.totalRevenue, + @JsonKey(name: "total_quantity") this.totalQuantity, + @JsonKey(name: "product_count") this.productCount, + @JsonKey(name: "order_count") this.orderCount, + }) : super._(); + + factory _$CategoryAnalyticItemDtoImpl.fromJson(Map json) => + _$$CategoryAnalyticItemDtoImplFromJson(json); + + @override + @JsonKey(name: "category_id") + final String? categoryId; + @override + @JsonKey(name: "category_name") + final String? categoryName; + @override + @JsonKey(name: "total_revenue") + final int? totalRevenue; + @override + @JsonKey(name: "total_quantity") + final int? totalQuantity; + @override + @JsonKey(name: "product_count") + final int? productCount; + @override + @JsonKey(name: "order_count") + final int? orderCount; + + @override + String toString() { + return 'CategoryAnalyticItemDto(categoryId: $categoryId, categoryName: $categoryName, totalRevenue: $totalRevenue, totalQuantity: $totalQuantity, productCount: $productCount, orderCount: $orderCount)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CategoryAnalyticItemDtoImpl && + (identical(other.categoryId, categoryId) || + other.categoryId == categoryId) && + (identical(other.categoryName, categoryName) || + other.categoryName == categoryName) && + (identical(other.totalRevenue, totalRevenue) || + other.totalRevenue == totalRevenue) && + (identical(other.totalQuantity, totalQuantity) || + other.totalQuantity == totalQuantity) && + (identical(other.productCount, productCount) || + other.productCount == productCount) && + (identical(other.orderCount, orderCount) || + other.orderCount == orderCount)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + categoryId, + categoryName, + totalRevenue, + totalQuantity, + productCount, + orderCount, + ); + + /// Create a copy of CategoryAnalyticItemDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CategoryAnalyticItemDtoImplCopyWith<_$CategoryAnalyticItemDtoImpl> + get copyWith => + __$$CategoryAnalyticItemDtoImplCopyWithImpl< + _$CategoryAnalyticItemDtoImpl + >(this, _$identity); + + @override + Map toJson() { + return _$$CategoryAnalyticItemDtoImplToJson(this); + } +} + +abstract class _CategoryAnalyticItemDto extends CategoryAnalyticItemDto { + const factory _CategoryAnalyticItemDto({ + @JsonKey(name: "category_id") final String? categoryId, + @JsonKey(name: "category_name") final String? categoryName, + @JsonKey(name: "total_revenue") final int? totalRevenue, + @JsonKey(name: "total_quantity") final int? totalQuantity, + @JsonKey(name: "product_count") final int? productCount, + @JsonKey(name: "order_count") final int? orderCount, + }) = _$CategoryAnalyticItemDtoImpl; + const _CategoryAnalyticItemDto._() : super._(); + + factory _CategoryAnalyticItemDto.fromJson(Map json) = + _$CategoryAnalyticItemDtoImpl.fromJson; + + @override + @JsonKey(name: "category_id") + String? get categoryId; + @override + @JsonKey(name: "category_name") + String? get categoryName; + @override + @JsonKey(name: "total_revenue") + int? get totalRevenue; + @override + @JsonKey(name: "total_quantity") + int? get totalQuantity; + @override + @JsonKey(name: "product_count") + int? get productCount; + @override + @JsonKey(name: "order_count") + int? get orderCount; + + /// Create a copy of CategoryAnalyticItemDto + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CategoryAnalyticItemDtoImplCopyWith<_$CategoryAnalyticItemDtoImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/infrastructure/analytic/analytic_dtos.g.dart b/lib/infrastructure/analytic/analytic_dtos.g.dart index 398b4e5..feac7c7 100644 --- a/lib/infrastructure/analytic/analytic_dtos.g.dart +++ b/lib/infrastructure/analytic/analytic_dtos.g.dart @@ -479,3 +479,51 @@ Map _$$ProfitLossAnalyticProductDtoImplToJson( 'average_cost': instance.averageCost, 'profit_per_unit': instance.profitPerUnit, }; + +_$CategoryAnalyticDtoImpl _$$CategoryAnalyticDtoImplFromJson( + Map json, +) => _$CategoryAnalyticDtoImpl( + 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) => CategoryAnalyticItemDto.fromJson(e as Map)) + .toList(), +); + +Map _$$CategoryAnalyticDtoImplToJson( + _$CategoryAnalyticDtoImpl instance, +) => { + 'organization_id': instance.organizationId, + 'outlet_id': instance.outletId, + 'date_from': instance.dateFrom?.toIso8601String(), + 'date_to': instance.dateTo?.toIso8601String(), + 'data': instance.data, +}; + +_$CategoryAnalyticItemDtoImpl _$$CategoryAnalyticItemDtoImplFromJson( + Map json, +) => _$CategoryAnalyticItemDtoImpl( + categoryId: json['category_id'] as String?, + categoryName: json['category_name'] as String?, + totalRevenue: (json['total_revenue'] as num?)?.toInt(), + totalQuantity: (json['total_quantity'] as num?)?.toInt(), + productCount: (json['product_count'] as num?)?.toInt(), + orderCount: (json['order_count'] as num?)?.toInt(), +); + +Map _$$CategoryAnalyticItemDtoImplToJson( + _$CategoryAnalyticItemDtoImpl instance, +) => { + 'category_id': instance.categoryId, + 'category_name': instance.categoryName, + 'total_revenue': instance.totalRevenue, + 'total_quantity': instance.totalQuantity, + 'product_count': instance.productCount, + '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 c775827..3a1028a 100644 --- a/lib/infrastructure/analytic/datasources/remote_data_provider.dart +++ b/lib/infrastructure/analytic/datasources/remote_data_provider.dart @@ -163,4 +163,33 @@ class AnalyticRemoteDataProvider { return DC.error(AnalyticFailure.serverError(e)); } } + + Future> fetchCategories({ + required DateTime dateFrom, + required DateTime dateTo, + }) async { + try { + final response = await _apiClient.get( + ApiPath.analyticCategories, + params: { + 'date_from': dateFrom.toServerDate(), + 'date_to': dateTo.toServerDate(), + }, + headers: getAuthorizationHeader(), + ); + + if (response.data['success'] == false) { + return DC.error(AnalyticFailure.unexpectedError()); + } + + final categories = CategoryAnalyticDto.fromJson( + response.data['data'] as Map, + ); + + return DC.data(categories); + } on ApiFailure catch (e, s) { + log('fetchCategories', name: _logName, error: e, stackTrace: s); + return DC.error(AnalyticFailure.serverError(e)); + } + } } diff --git a/lib/infrastructure/analytic/dtos/category_analytic_dto.dart b/lib/infrastructure/analytic/dtos/category_analytic_dto.dart new file mode 100644 index 0000000..63ae69d --- /dev/null +++ b/lib/infrastructure/analytic/dtos/category_analytic_dto.dart @@ -0,0 +1,53 @@ +part of '../analytic_dtos.dart'; + +@freezed +class CategoryAnalyticDto with _$CategoryAnalyticDto { + const CategoryAnalyticDto._(); + + const factory CategoryAnalyticDto({ + @JsonKey(name: "organization_id") String? organizationId, + @JsonKey(name: "outlet_id") String? outletId, + @JsonKey(name: "date_from") DateTime? dateFrom, + @JsonKey(name: "date_to") DateTime? dateTo, + List? data, + }) = _CategoryAnalyticDto; + + factory CategoryAnalyticDto.fromJson(Map json) => + _$CategoryAnalyticDtoFromJson(json); + + // Optional mapper ke entity + CategoryAnalytic toDomain() => CategoryAnalytic( + organizationId: organizationId ?? '', + outletId: outletId ?? '', + dateFrom: dateFrom ?? DateTime.now(), + dateTo: dateTo ?? DateTime.now(), + data: data?.map((e) => e.toDomain()).toList() ?? [], + ); +} + +@freezed +class CategoryAnalyticItemDto with _$CategoryAnalyticItemDto { + const CategoryAnalyticItemDto._(); + + const factory CategoryAnalyticItemDto({ + @JsonKey(name: "category_id") String? categoryId, + @JsonKey(name: "category_name") String? categoryName, + @JsonKey(name: "total_revenue") int? totalRevenue, + @JsonKey(name: "total_quantity") int? totalQuantity, + @JsonKey(name: "product_count") int? productCount, + @JsonKey(name: "order_count") int? orderCount, + }) = _CategoryAnalyticItemDto; + + factory CategoryAnalyticItemDto.fromJson(Map json) => + _$CategoryAnalyticItemDtoFromJson(json); + + // Optional mapper ke entity + CategoryAnalyticItem toDomain() => CategoryAnalyticItem( + categoryId: categoryId ?? '', + categoryName: categoryName ?? '', + totalRevenue: totalRevenue ?? 0, + totalQuantity: totalQuantity ?? 0, + productCount: productCount ?? 0, + orderCount: orderCount ?? 0, + ); +} diff --git a/lib/infrastructure/analytic/repositories/analytic_repository.dart b/lib/infrastructure/analytic/repositories/analytic_repository.dart index 365ed72..eba12d1 100644 --- a/lib/infrastructure/analytic/repositories/analytic_repository.dart +++ b/lib/infrastructure/analytic/repositories/analytic_repository.dart @@ -133,4 +133,28 @@ class AnalyticRepository implements IAnalyticRepository { return left(const AnalyticFailure.unexpectedError()); } } + + @override + Future> getCategories({ + required DateTime dateFrom, + required DateTime dateTo, + }) async { + try { + final result = await _dataProvider.fetchCategories( + dateFrom: dateFrom, + dateTo: dateTo, + ); + + if (result.hasError) { + return left(result.error!); + } + + final categories = result.data!.toDomain(); + + return right(categories); + } catch (e) { + log('getCategoriesError', name: _logName, error: e); + return left(const AnalyticFailure.unexpectedError()); + } + } } diff --git a/lib/injection.config.dart b/lib/injection.config.dart index aef938e..1fa6ce2 100644 --- a/lib/injection.config.dart +++ b/lib/injection.config.dart @@ -9,6 +9,8 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:apskel_pos_flutter_v2/application/analytic/category_analytic_loader/category_analytic_loader_bloc.dart' + as _i911; 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/payment_method_analytic_loader/payment_method_analytic_loader_bloc.dart' @@ -315,6 +317,9 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i741.ProfitLossAnalyticLoaderBloc>( () => _i741.ProfitLossAnalyticLoaderBloc(gh<_i346.IAnalyticRepository>()), ); + gh.factory<_i911.CategoryAnalyticLoaderBloc>( + () => _i911.CategoryAnalyticLoaderBloc(gh<_i346.IAnalyticRepository>()), + ); return this; } } diff --git a/lib/presentation/pages/main/pages/report/report_page.dart b/lib/presentation/pages/main/pages/report/report_page.dart index 5bd1939..8f81b7d 100644 --- a/lib/presentation/pages/main/pages/report/report_page.dart +++ b/lib/presentation/pages/main/pages/report/report_page.dart @@ -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/category_analytic_loader/category_analytic_loader_bloc.dart'; import '../../../../../application/analytic/dashboard_analytic_loader/dashboard_analytic_loader_bloc.dart'; import '../../../../../application/analytic/payment_method_analytic_loader/payment_method_analytic_loader_bloc.dart'; import '../../../../../application/analytic/product_analytic_loader/product_analytic_loader_bloc.dart'; @@ -16,6 +17,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_category_section.dart'; import 'sections/report_dashboard_section.dart'; import 'sections/report_payment_method_section.dart'; import 'sections/report_product_section.dart'; @@ -184,7 +186,20 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper { }, ), 6 => Text(state.title), - 7 => Text(state.title), + 7 => + BlocBuilder< + CategoryAnalyticLoaderBloc, + CategoryAnalyticLoaderState + >( + builder: (context, category) { + return ReportCategorySection( + menu: reportMenus[state.selectedMenu], + state: category, + startDate: state.startDate, + endDate: state.endDate, + ); + }, + ), _ => Container(), }, ), @@ -241,6 +256,12 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper { ); case 6: case 7: + return context.read().add( + CategoryAnalyticLoaderEvent.fetched( + startDate: state.startDate, + endDate: state.endDate, + ), + ); default: return; } @@ -295,6 +316,16 @@ class ReportPage extends StatelessWidget implements AutoRouteWrapper { ), ), ), + + BlocProvider( + create: (context) => getIt() + ..add( + CategoryAnalyticLoaderEvent.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_category_section.dart b/lib/presentation/pages/main/pages/report/sections/report_category_section.dart new file mode 100644 index 0000000..7103ebb --- /dev/null +++ b/lib/presentation/pages/main/pages/report/sections/report_category_section.dart @@ -0,0 +1,269 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../../../application/analytic/category_analytic_loader/category_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/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'; + +class ReportCategorySection extends StatelessWidget { + final ReportMenu menu; + final CategoryAnalyticLoaderState state; + final DateTime startDate; + final DateTime endDate; + const ReportCategorySection({ + 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( + () => + BlocBuilder( + builder: (context, state) { + return ListView( + padding: const EdgeInsets.all(16), + children: [ + ReportHeader( + menu: menu, + endDate: endDate, + startDate: startDate, + ), + _buildSummary(state), + Container( + margin: EdgeInsets.only(top: 16), + decoration: BoxDecoration( + color: AppColor.white, + borderRadius: BorderRadius.circular(14), + ), + child: Column( + children: [ + Container( + decoration: BoxDecoration( + color: AppColor.disabled.withOpacity(0.8), + borderRadius: BorderRadius.vertical( + top: Radius.circular(14), + ), + ), + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + child: Row( + children: [ + Expanded( + flex: 3, + child: Text( + 'Kategori', + style: AppStyle.lg.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textPrimary, + ), + ), + ), + Expanded( + flex: 2, + child: Text( + 'Pendapatan', + textAlign: TextAlign.center, + style: AppStyle.lg.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textPrimary, + ), + ), + ), + Expanded( + flex: 1, + child: Text( + 'Qty', + textAlign: TextAlign.center, + style: AppStyle.lg.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textPrimary, + ), + ), + ), + Expanded( + flex: 1, + child: Text( + 'Order', + textAlign: TextAlign.center, + style: AppStyle.lg.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textPrimary, + ), + ), + ), + ], + ), + ), + Container( + decoration: BoxDecoration( + color: AppColor.white, + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(14), + ), + ), + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: state.categoryAnalytic.data.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return _buildCategoryItem( + state.categoryAnalytic.data[index], + index, + state.categoryAnalytic.data.length, + ); + }, + ), + ), + ], + ), + ), + ], + ); + }, + ), + (f) => AnalyticErrorStateWidget( + failure: f, + menu: menu, + onRefresh: () { + context.read().add( + CategoryAnalyticLoaderEvent.fetched( + startDate: startDate, + endDate: endDate, + ), + ); + }, + ), + ); + } + + Widget _buildCategoryItem( + CategoryAnalyticItem category, + int index, + int totalCount, + ) { + final isEven = index % 2 == 0; + final isLast = index == totalCount - 1; + + return Container( + decoration: BoxDecoration( + color: isEven ? AppColor.white : AppColor.disabled.withOpacity(0.3), + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(isLast ? 14 : 0), + ), + ), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Row( + children: [ + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + category.categoryName, + style: AppStyle.lg.copyWith( + fontWeight: FontWeight.w500, + color: AppColor.textPrimary, + ), + ), + const SizedBox(height: 4), + Text( + '${category.productCount} produk', + style: AppStyle.sm.copyWith(color: AppColor.textSecondary), + ), + ], + ), + ), + Expanded( + flex: 2, + child: Column( + children: [ + Text( + category.totalRevenue.currencyFormatRpV2, + textAlign: TextAlign.center, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w500, + color: AppColor.success, + ), + ), + ], + ), + ), + Expanded( + flex: 1, + child: Text( + '${category.totalQuantity}', + textAlign: TextAlign.center, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w500, + color: AppColor.textPrimary, + ), + ), + ), + Expanded( + flex: 1, + child: Text( + '${category.orderCount}', + textAlign: TextAlign.center, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w500, + color: AppColor.textPrimary, + ), + ), + ), + ], + ), + ); + } + + Row _buildSummary(CategoryAnalyticLoaderState state) { + return Row( + children: [ + Expanded( + child: ReportSummaryCard( + color: AppColor.success, + icon: Icons.trending_up, + title: 'Total Pendapatan', + value: state.categoryAnalytic.totalRevenue.currencyFormatRpV2, + ), + ), + SpaceWidth(12), + Expanded( + child: ReportSummaryCard( + color: AppColor.info, + icon: Icons.shopping_cart_outlined, + title: 'Total Item Terjual', + value: "${state.categoryAnalytic.totalQuantity} pcs", + ), + ), + SpaceWidth(12), + Expanded( + child: ReportSummaryCard( + color: AppColor.primary, + icon: Icons.receipt_outlined, + title: 'Total Pesanan', + value: state.categoryAnalytic.totalOrders.toString(), + ), + ), + ], + ); + } +}