From 7007f397669cb707534fcfdcc78d105b9064dacd Mon Sep 17 00:00:00 2001 From: efrilm Date: Fri, 15 Aug 2025 17:07:29 +0700 Subject: [PATCH] feat: category report --- lib/main.dart | 4 + .../category_report/category_report_bloc.dart | 28 + .../category_report_bloc.freezed.dart | 861 ++++++++++++++++++ .../category_report_event.dart | 9 + .../category_report_state.dart | 9 + .../report/pages/report_page.dart | 59 +- .../widgets/category_report_widget.dart | 292 ++++++ 7 files changed, 1261 insertions(+), 1 deletion(-) create mode 100644 lib/presentation/report/blocs/category_report/category_report_bloc.dart create mode 100644 lib/presentation/report/blocs/category_report/category_report_bloc.freezed.dart create mode 100644 lib/presentation/report/blocs/category_report/category_report_event.dart create mode 100644 lib/presentation/report/blocs/category_report/category_report_state.dart create mode 100644 lib/presentation/report/widgets/category_report_widget.dart diff --git a/lib/main.dart b/lib/main.dart index d0ff9c6..f090eea 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,6 +16,7 @@ import 'package:enaklo_pos/presentation/home/bloc/outlet_loader/outlet_loader_bl import 'package:enaklo_pos/presentation/home/bloc/product_loader/product_loader_bloc.dart'; import 'package:enaklo_pos/presentation/home/bloc/user_update_outlet/user_update_outlet_bloc.dart'; import 'package:enaklo_pos/presentation/refund/bloc/refund_bloc.dart'; +import 'package:enaklo_pos/presentation/report/blocs/category_report/category_report_bloc.dart'; import 'package:enaklo_pos/presentation/report/blocs/inventory_report/inventory_report_bloc.dart'; import 'package:enaklo_pos/presentation/report/blocs/profit_loss/profit_loss_bloc.dart'; import 'package:enaklo_pos/presentation/report/blocs/report/report_bloc.dart'; @@ -296,6 +297,9 @@ class _MyAppState extends State { BlocProvider( create: (context) => InventoryReportBloc(AnalyticRemoteDatasource()), ), + BlocProvider( + create: (context) => CategoryReportBloc(AnalyticRemoteDatasource()), + ), ], child: MaterialApp( navigatorKey: AuthInterceptor.navigatorKey, diff --git a/lib/presentation/report/blocs/category_report/category_report_bloc.dart b/lib/presentation/report/blocs/category_report/category_report_bloc.dart new file mode 100644 index 0000000..47e1734 --- /dev/null +++ b/lib/presentation/report/blocs/category_report/category_report_bloc.dart @@ -0,0 +1,28 @@ +import 'package:bloc/bloc.dart'; +import 'package:enaklo_pos/data/datasources/analytic_remote_datasource.dart'; +import 'package:enaklo_pos/data/models/response/category_analytic_response_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'category_report_event.dart'; +part 'category_report_state.dart'; +part 'category_report_bloc.freezed.dart'; + +class CategoryReportBloc + extends Bloc { + final AnalyticRemoteDatasource _datasource; + CategoryReportBloc(this._datasource) : super(CategoryReportState.initial()) { + on<_Get>((event, emit) async { + emit(_Loading()); + + final result = await _datasource.getCategory( + dateFrom: event.startDate, + dateTo: event.endDate, + ); + + result.fold( + (l) => emit(_Error(l)), + (r) => emit(_Loaded(r.data!)), + ); + }); + } +} diff --git a/lib/presentation/report/blocs/category_report/category_report_bloc.freezed.dart b/lib/presentation/report/blocs/category_report/category_report_bloc.freezed.dart new file mode 100644 index 0000000..aa0752b --- /dev/null +++ b/lib/presentation/report/blocs/category_report/category_report_bloc.freezed.dart @@ -0,0 +1,861 @@ +// 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_report_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 _$CategoryReportEvent { + DateTime get startDate => throw _privateConstructorUsedError; + DateTime get endDate => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function(DateTime startDate, DateTime endDate) get, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(DateTime startDate, DateTime endDate)? get, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(DateTime startDate, DateTime endDate)? get, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Get value) get, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Get value)? get, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Get value)? get, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + /// Create a copy of CategoryReportEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CategoryReportEventCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CategoryReportEventCopyWith<$Res> { + factory $CategoryReportEventCopyWith( + CategoryReportEvent value, $Res Function(CategoryReportEvent) then) = + _$CategoryReportEventCopyWithImpl<$Res, CategoryReportEvent>; + @useResult + $Res call({DateTime startDate, DateTime endDate}); +} + +/// @nodoc +class _$CategoryReportEventCopyWithImpl<$Res, $Val extends CategoryReportEvent> + implements $CategoryReportEventCopyWith<$Res> { + _$CategoryReportEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CategoryReportEvent + /// 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 _$$GetImplCopyWith<$Res> + implements $CategoryReportEventCopyWith<$Res> { + factory _$$GetImplCopyWith(_$GetImpl value, $Res Function(_$GetImpl) then) = + __$$GetImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({DateTime startDate, DateTime endDate}); +} + +/// @nodoc +class __$$GetImplCopyWithImpl<$Res> + extends _$CategoryReportEventCopyWithImpl<$Res, _$GetImpl> + implements _$$GetImplCopyWith<$Res> { + __$$GetImplCopyWithImpl(_$GetImpl _value, $Res Function(_$GetImpl) _then) + : super(_value, _then); + + /// Create a copy of CategoryReportEvent + /// 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(_$GetImpl( + 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 _$GetImpl implements _Get { + const _$GetImpl({required this.startDate, required this.endDate}); + + @override + final DateTime startDate; + @override + final DateTime endDate; + + @override + String toString() { + return 'CategoryReportEvent.get(startDate: $startDate, endDate: $endDate)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$GetImpl && + (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 CategoryReportEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$GetImplCopyWith<_$GetImpl> get copyWith => + __$$GetImplCopyWithImpl<_$GetImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(DateTime startDate, DateTime endDate) get, + }) { + return get(startDate, endDate); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(DateTime startDate, DateTime endDate)? get, + }) { + return get?.call(startDate, endDate); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(DateTime startDate, DateTime endDate)? get, + required TResult orElse(), + }) { + if (get != null) { + return get(startDate, endDate); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Get value) get, + }) { + return get(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Get value)? get, + }) { + return get?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Get value)? get, + required TResult orElse(), + }) { + if (get != null) { + return get(this); + } + return orElse(); + } +} + +abstract class _Get implements CategoryReportEvent { + const factory _Get( + {required final DateTime startDate, + required final DateTime endDate}) = _$GetImpl; + + @override + DateTime get startDate; + @override + DateTime get endDate; + + /// Create a copy of CategoryReportEvent + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$GetImplCopyWith<_$GetImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$CategoryReportState { + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(CategoryAnalyticData data) loaded, + required TResult Function(String message) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(CategoryAnalyticData data)? loaded, + TResult? Function(String message)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(CategoryAnalyticData data)? loaded, + TResult Function(String message)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_Error value) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_Error value)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_Error value)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CategoryReportStateCopyWith<$Res> { + factory $CategoryReportStateCopyWith( + CategoryReportState value, $Res Function(CategoryReportState) then) = + _$CategoryReportStateCopyWithImpl<$Res, CategoryReportState>; +} + +/// @nodoc +class _$CategoryReportStateCopyWithImpl<$Res, $Val extends CategoryReportState> + implements $CategoryReportStateCopyWith<$Res> { + _$CategoryReportStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CategoryReportState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc +abstract class _$$InitialImplCopyWith<$Res> { + factory _$$InitialImplCopyWith( + _$InitialImpl value, $Res Function(_$InitialImpl) then) = + __$$InitialImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$InitialImplCopyWithImpl<$Res> + extends _$CategoryReportStateCopyWithImpl<$Res, _$InitialImpl> + implements _$$InitialImplCopyWith<$Res> { + __$$InitialImplCopyWithImpl( + _$InitialImpl _value, $Res Function(_$InitialImpl) _then) + : super(_value, _then); + + /// Create a copy of CategoryReportState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$InitialImpl implements _Initial { + const _$InitialImpl(); + + @override + String toString() { + return 'CategoryReportState.initial()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$InitialImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(CategoryAnalyticData data) loaded, + required TResult Function(String message) error, + }) { + return initial(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(CategoryAnalyticData data)? loaded, + TResult? Function(String message)? error, + }) { + return initial?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(CategoryAnalyticData data)? loaded, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if (initial != null) { + return initial(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_Error value) error, + }) { + return initial(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_Error value)? error, + }) { + return initial?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_Error value)? error, + required TResult orElse(), + }) { + if (initial != null) { + return initial(this); + } + return orElse(); + } +} + +abstract class _Initial implements CategoryReportState { + const factory _Initial() = _$InitialImpl; +} + +/// @nodoc +abstract class _$$LoadingImplCopyWith<$Res> { + factory _$$LoadingImplCopyWith( + _$LoadingImpl value, $Res Function(_$LoadingImpl) then) = + __$$LoadingImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$LoadingImplCopyWithImpl<$Res> + extends _$CategoryReportStateCopyWithImpl<$Res, _$LoadingImpl> + implements _$$LoadingImplCopyWith<$Res> { + __$$LoadingImplCopyWithImpl( + _$LoadingImpl _value, $Res Function(_$LoadingImpl) _then) + : super(_value, _then); + + /// Create a copy of CategoryReportState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$LoadingImpl implements _Loading { + const _$LoadingImpl(); + + @override + String toString() { + return 'CategoryReportState.loading()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$LoadingImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(CategoryAnalyticData data) loaded, + required TResult Function(String message) error, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(CategoryAnalyticData data)? loaded, + TResult? Function(String message)? error, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(CategoryAnalyticData data)? loaded, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_Error value) error, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_Error value)? error, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_Error value)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class _Loading implements CategoryReportState { + const factory _Loading() = _$LoadingImpl; +} + +/// @nodoc +abstract class _$$LoadedImplCopyWith<$Res> { + factory _$$LoadedImplCopyWith( + _$LoadedImpl value, $Res Function(_$LoadedImpl) then) = + __$$LoadedImplCopyWithImpl<$Res>; + @useResult + $Res call({CategoryAnalyticData data}); +} + +/// @nodoc +class __$$LoadedImplCopyWithImpl<$Res> + extends _$CategoryReportStateCopyWithImpl<$Res, _$LoadedImpl> + implements _$$LoadedImplCopyWith<$Res> { + __$$LoadedImplCopyWithImpl( + _$LoadedImpl _value, $Res Function(_$LoadedImpl) _then) + : super(_value, _then); + + /// Create a copy of CategoryReportState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? data = null, + }) { + return _then(_$LoadedImpl( + null == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as CategoryAnalyticData, + )); + } +} + +/// @nodoc + +class _$LoadedImpl implements _Loaded { + const _$LoadedImpl(this.data); + + @override + final CategoryAnalyticData data; + + @override + String toString() { + return 'CategoryReportState.loaded(data: $data)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LoadedImpl && + (identical(other.data, data) || other.data == data)); + } + + @override + int get hashCode => Object.hash(runtimeType, data); + + /// Create a copy of CategoryReportState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$LoadedImplCopyWith<_$LoadedImpl> get copyWith => + __$$LoadedImplCopyWithImpl<_$LoadedImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(CategoryAnalyticData data) loaded, + required TResult Function(String message) error, + }) { + return loaded(data); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(CategoryAnalyticData data)? loaded, + TResult? Function(String message)? error, + }) { + return loaded?.call(data); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(CategoryAnalyticData data)? loaded, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if (loaded != null) { + return loaded(data); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_Error value) error, + }) { + return loaded(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_Error value)? error, + }) { + return loaded?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_Error value)? error, + required TResult orElse(), + }) { + if (loaded != null) { + return loaded(this); + } + return orElse(); + } +} + +abstract class _Loaded implements CategoryReportState { + const factory _Loaded(final CategoryAnalyticData data) = _$LoadedImpl; + + CategoryAnalyticData get data; + + /// Create a copy of CategoryReportState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$LoadedImplCopyWith<_$LoadedImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$ErrorImplCopyWith<$Res> { + factory _$$ErrorImplCopyWith( + _$ErrorImpl value, $Res Function(_$ErrorImpl) then) = + __$$ErrorImplCopyWithImpl<$Res>; + @useResult + $Res call({String message}); +} + +/// @nodoc +class __$$ErrorImplCopyWithImpl<$Res> + extends _$CategoryReportStateCopyWithImpl<$Res, _$ErrorImpl> + implements _$$ErrorImplCopyWith<$Res> { + __$$ErrorImplCopyWithImpl( + _$ErrorImpl _value, $Res Function(_$ErrorImpl) _then) + : super(_value, _then); + + /// Create a copy of CategoryReportState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? message = null, + }) { + return _then(_$ErrorImpl( + null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$ErrorImpl implements _Error { + const _$ErrorImpl(this.message); + + @override + final String message; + + @override + String toString() { + return 'CategoryReportState.error(message: $message)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ErrorImpl && + (identical(other.message, message) || other.message == message)); + } + + @override + int get hashCode => Object.hash(runtimeType, message); + + /// Create a copy of CategoryReportState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ErrorImplCopyWith<_$ErrorImpl> get copyWith => + __$$ErrorImplCopyWithImpl<_$ErrorImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(CategoryAnalyticData data) loaded, + required TResult Function(String message) error, + }) { + return error(message); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(CategoryAnalyticData data)? loaded, + TResult? Function(String message)? error, + }) { + return error?.call(message); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(CategoryAnalyticData data)? loaded, + TResult Function(String message)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(message); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_Error value) error, + }) { + return error(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_Error value)? error, + }) { + return error?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_Error value)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(this); + } + return orElse(); + } +} + +abstract class _Error implements CategoryReportState { + const factory _Error(final String message) = _$ErrorImpl; + + String get message; + + /// Create a copy of CategoryReportState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ErrorImplCopyWith<_$ErrorImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/presentation/report/blocs/category_report/category_report_event.dart b/lib/presentation/report/blocs/category_report/category_report_event.dart new file mode 100644 index 0000000..15b819d --- /dev/null +++ b/lib/presentation/report/blocs/category_report/category_report_event.dart @@ -0,0 +1,9 @@ +part of 'category_report_bloc.dart'; + +@freezed +class CategoryReportEvent with _$CategoryReportEvent { + const factory CategoryReportEvent.get({ + required DateTime startDate, + required DateTime endDate, + }) = _Get; +} diff --git a/lib/presentation/report/blocs/category_report/category_report_state.dart b/lib/presentation/report/blocs/category_report/category_report_state.dart new file mode 100644 index 0000000..0c9a80b --- /dev/null +++ b/lib/presentation/report/blocs/category_report/category_report_state.dart @@ -0,0 +1,9 @@ +part of 'category_report_bloc.dart'; + +@freezed +class CategoryReportState with _$CategoryReportState { + const factory CategoryReportState.initial() = _Initial; + const factory CategoryReportState.loading() = _Loading; + const factory CategoryReportState.loaded(CategoryAnalyticData data) = _Loaded; + const factory CategoryReportState.error(String message) = _Error; +} diff --git a/lib/presentation/report/pages/report_page.dart b/lib/presentation/report/pages/report_page.dart index 879a125..3e4151b 100644 --- a/lib/presentation/report/pages/report_page.dart +++ b/lib/presentation/report/pages/report_page.dart @@ -5,9 +5,11 @@ import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; import 'package:enaklo_pos/core/utils/helper_pdf_service.dart'; import 'package:enaklo_pos/core/utils/permession_handler.dart'; import 'package:enaklo_pos/core/utils/transaction_report.dart'; +import 'package:enaklo_pos/presentation/report/blocs/category_report/category_report_bloc.dart'; import 'package:enaklo_pos/presentation/report/blocs/inventory_report/inventory_report_bloc.dart'; import 'package:enaklo_pos/presentation/report/blocs/profit_loss/profit_loss_bloc.dart'; import 'package:enaklo_pos/presentation/report/blocs/report/report_bloc.dart'; +import 'package:enaklo_pos/presentation/report/widgets/category_report_widget.dart'; import 'package:enaklo_pos/presentation/report/widgets/dashboard_analytic_widget.dart'; import 'package:enaklo_pos/presentation/report/widgets/inventory_report_widget.dart'; import 'package:enaklo_pos/presentation/report/widgets/profit_loss_widget.dart'; @@ -111,6 +113,15 @@ class _ReportPageState extends State { ), ); } + + if (selectedMenu == 7) { + context.read().add( + CategoryReportEvent.get( + startDate: fromDate, + endDate: toDate, + ), + ); + } } @override @@ -346,6 +357,23 @@ class _ReportPageState extends State { }, isActive: selectedMenu == 6, ), + ReportMenu( + label: 'Laporan Kategori', + subtitle: 'Laporan kategori produk', + icon: Icons.archive_outlined, + onPressed: () { + selectedMenu = 7; + title = 'Laporan Kategori'; + setState(() {}); + context.read().add( + CategoryReportEvent.get( + startDate: fromDate, + endDate: toDate, + ), + ); + }, + isActive: selectedMenu == 7, + ), ], ), ), @@ -521,7 +549,36 @@ class _ReportPageState extends State { ); }, ) - : const SizedBox.shrink()), + : selectedMenu == 7 + ? BlocBuilder< + CategoryReportBloc, + CategoryReportState>( + builder: + (context, state) { + return state + .maybeWhen( + orElse: () => + const Center( + child: + CircularProgressIndicator(), + ), + error: (message) { + return Text( + message); + }, + loaded: (data) { + return CategoryReportWidget( + title: title, + searchDateFormatted: + searchDateFormatted, + categoryAnalyticData: + data, + ); + }, + ); + }, + ) + : const SizedBox.shrink()), ], ), ), diff --git a/lib/presentation/report/widgets/category_report_widget.dart b/lib/presentation/report/widgets/category_report_widget.dart new file mode 100644 index 0000000..0c83090 --- /dev/null +++ b/lib/presentation/report/widgets/category_report_widget.dart @@ -0,0 +1,292 @@ +import 'package:enaklo_pos/core/constants/colors.dart'; +import 'package:enaklo_pos/data/models/response/category_analytic_response_model.dart'; +import 'package:flutter/material.dart'; + +class CategoryReportWidget extends StatelessWidget { + final String title; + final String searchDateFormatted; + final CategoryAnalyticData categoryAnalyticData; + const CategoryReportWidget({ + super.key, + required this.title, + required this.searchDateFormatted, + required this.categoryAnalyticData, + }); + + String formatCurrency(int amount) { + return 'Rp ${amount.toString().replaceAllMapped( + RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), + (Match m) => '${m[1]}.', + )}'; + } + + int getTotalRevenue() { + return categoryAnalyticData.data + .fold(0, (sum, item) => sum + item.totalRevenue); + } + + int getTotalQuantity() { + return categoryAnalyticData.data + .fold(0, (sum, item) => sum + item.totalQuantity); + } + + int getTotalOrders() { + return categoryAnalyticData.data + .fold(0, (sum, item) => sum + item.orderCount); + } + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + color: AppColors.background, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Section + Container( + width: double.infinity, + color: AppColors.primary, + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: AppColors.whiteText, + ), + ), + const SizedBox(height: 8), + Text( + 'Periode: $searchDateFormatted', + style: TextStyle( + fontSize: 14, + color: AppColors.whiteText.withOpacity(0.8), + ), + ), + ], + ), + ), + + // Summary Cards Section + Container( + color: AppColors.white, + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Expanded( + child: _buildSummaryCard( + 'Total Pendapatan', + formatCurrency(getTotalRevenue()), + AppColors.green, + ), + ), + const SizedBox(width: 16), + Expanded( + child: _buildSummaryCard( + 'Total Item Terjual', + '${getTotalQuantity()} pcs', + AppColors.subtitle, + ), + ), + const SizedBox(width: 16), + Expanded( + child: _buildSummaryCard( + 'Total Pesanan', + '${getTotalOrders()}', + AppColors.primary, + ), + ), + ], + ), + ), + + // Categories List Header + Container( + color: AppColors.stroke, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Row( + children: [ + Expanded( + flex: 3, + child: Text( + 'Kategori', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.black, + ), + ), + ), + Expanded( + flex: 2, + child: Text( + 'Pendapatan', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.black, + ), + ), + ), + Expanded( + flex: 1, + child: Text( + 'Qty', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.black, + ), + ), + ), + Expanded( + flex: 1, + child: Text( + 'Order', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.black, + ), + ), + ), + ], + ), + ), + + // Categories List + Expanded( + child: Container( + color: AppColors.white, + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: categoryAnalyticData.data.length, + itemBuilder: (context, index) { + return _buildCategoryItem( + categoryAnalyticData.data[index], index); + }, + ), + ), + ), + ], + ), + ); + } + + Widget _buildSummaryCard(String title, String value, Color color) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.light, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.stroke, width: 1), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 12, + color: AppColors.subtitle, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Text( + value, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: color, + ), + ), + ], + ), + ); + } + + Widget _buildCategoryItem(CategoryAnalyticItem category, int index) { + final isEven = index % 2 == 0; + + return Container( + color: isEven ? AppColors.white : AppColors.light.withOpacity(0.3), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Row( + children: [ + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + category.categoryName, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.black, + ), + ), + const SizedBox(height: 4), + Text( + '${category.productCount} produk', + style: TextStyle( + fontSize: 12, + color: AppColors.subtitle, + ), + ), + ], + ), + ), + Expanded( + flex: 2, + child: Column( + children: [ + Text( + formatCurrency(category.totalRevenue), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.green, + ), + ), + ], + ), + ), + Expanded( + flex: 1, + child: Text( + '${category.totalQuantity}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.black, + ), + ), + ), + Expanded( + flex: 1, + child: Text( + '${category.orderCount}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.black, + ), + ), + ), + ], + ), + ); + } +}