feat: product analytic
This commit is contained in:
parent
648a4f5eb4
commit
91335ad8db
@ -6,6 +6,7 @@ import 'package:enaklo_pos/core/constants/variables.dart';
|
||||
import 'package:enaklo_pos/core/network/dio_client.dart';
|
||||
import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart';
|
||||
import 'package:enaklo_pos/data/models/response/payment_method_analytic_response_model.dart';
|
||||
import 'package:enaklo_pos/data/models/response/product_analytic_response_model.dart';
|
||||
import 'package:enaklo_pos/data/models/response/sales_analytic_response_model.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
@ -79,4 +80,38 @@ class AnalyticRemoteDatasource {
|
||||
return left('Unexpected error occurred');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Either<String, ProductAnalyticResponseModel>> getProduct({
|
||||
required DateTime dateFrom,
|
||||
required DateTime dateTo,
|
||||
}) async {
|
||||
final authData = await AuthLocalDataSource().getAuthData();
|
||||
final headers = {
|
||||
'Authorization': 'Bearer ${authData.token}',
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await dio.get(
|
||||
'${Variables.baseUrl}/api/v1/analytics/products',
|
||||
queryParameters: {
|
||||
'date_from': DateFormat('dd-MM-yyyy').format(dateFrom),
|
||||
'date_to': DateFormat('dd-MM-yyyy').format(dateTo),
|
||||
},
|
||||
options: Options(headers: headers),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return right(ProductAnalyticResponseModel.fromMap(response.data));
|
||||
} else {
|
||||
return left('Terjadi Kesalahan, Coba lagi nanti.');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
log('Dio error: ${e.message}');
|
||||
return left(e.response?.data.toString() ?? e.message ?? 'Unknown error');
|
||||
} catch (e) {
|
||||
log('Unexpected error: $e');
|
||||
return left('Unexpected error occurred');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
143
lib/data/models/response/product_analytic_response_model.dart
Normal file
143
lib/data/models/response/product_analytic_response_model.dart
Normal file
@ -0,0 +1,143 @@
|
||||
class ProductAnalyticResponseModel {
|
||||
final bool success;
|
||||
final ProductAnalyticData data;
|
||||
final dynamic errors;
|
||||
|
||||
ProductAnalyticResponseModel({
|
||||
required this.success,
|
||||
required this.data,
|
||||
this.errors,
|
||||
});
|
||||
|
||||
factory ProductAnalyticResponseModel.fromJson(Map<String, dynamic> json) =>
|
||||
ProductAnalyticResponseModel.fromMap(json);
|
||||
|
||||
Map<String, dynamic> toJson() => toMap();
|
||||
|
||||
factory ProductAnalyticResponseModel.fromMap(Map<String, dynamic> map) {
|
||||
return ProductAnalyticResponseModel(
|
||||
success: map['success'] ?? false,
|
||||
data: ProductAnalyticData.fromMap(map['data']),
|
||||
errors: map['errors'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'success': success,
|
||||
'data': data.toMap(),
|
||||
'errors': errors,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ProductAnalyticData {
|
||||
final String organizationId;
|
||||
final String outletId;
|
||||
final DateTime dateFrom;
|
||||
final DateTime dateTo;
|
||||
final List<ProductAnalyticItem> data;
|
||||
|
||||
ProductAnalyticData({
|
||||
required this.organizationId,
|
||||
required this.outletId,
|
||||
required this.dateFrom,
|
||||
required this.dateTo,
|
||||
required this.data,
|
||||
});
|
||||
|
||||
factory ProductAnalyticData.fromMap(Map<String, dynamic> map) =>
|
||||
ProductAnalyticData(
|
||||
organizationId: map['organization_id'],
|
||||
outletId: map['outlet_id'],
|
||||
dateFrom: DateTime.parse(map['date_from']),
|
||||
dateTo: DateTime.parse(map['date_to']),
|
||||
data: List<ProductAnalyticItem>.from(
|
||||
map['data'].map((x) => ProductAnalyticItem.fromMap(x)),
|
||||
),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'organization_id': organizationId,
|
||||
'outlet_id': outletId,
|
||||
'date_from': dateFrom.toIso8601String(),
|
||||
'date_to': dateTo.toIso8601String(),
|
||||
'data': data.map((x) => x.toMap()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
class ProductAnalyticItem {
|
||||
final String productId;
|
||||
final String productName;
|
||||
final String categoryId;
|
||||
final String categoryName;
|
||||
final int quantitySold;
|
||||
final int revenue;
|
||||
final double averagePrice;
|
||||
final int orderCount;
|
||||
|
||||
ProductAnalyticItem({
|
||||
required this.productId,
|
||||
required this.productName,
|
||||
required this.categoryId,
|
||||
required this.categoryName,
|
||||
required this.quantitySold,
|
||||
required this.revenue,
|
||||
required this.averagePrice,
|
||||
required this.orderCount,
|
||||
});
|
||||
|
||||
factory ProductAnalyticItem.fromMap(Map<String, dynamic> map) =>
|
||||
ProductAnalyticItem(
|
||||
productId: map['product_id'],
|
||||
productName: map['product_name'],
|
||||
categoryId: map['category_id'],
|
||||
categoryName: map['category_name'],
|
||||
quantitySold: map['quantity_sold'],
|
||||
revenue: map['revenue'],
|
||||
averagePrice: (map['average_price'] as num).toDouble(),
|
||||
orderCount: map['order_count'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
'product_id': productId,
|
||||
'product_name': productName,
|
||||
'category_id': categoryId,
|
||||
'category_name': categoryName,
|
||||
'quantity_sold': quantitySold,
|
||||
'revenue': revenue,
|
||||
'average_price': averagePrice,
|
||||
'order_count': orderCount,
|
||||
};
|
||||
}
|
||||
|
||||
class ProductInsights {
|
||||
final List<ProductAnalyticItem> topProducts;
|
||||
final ProductAnalyticItem? bestProduct;
|
||||
final List<CategorySummary> categorySummary;
|
||||
final int totalProducts;
|
||||
final int totalRevenue;
|
||||
final int totalQuantitySold;
|
||||
|
||||
ProductInsights({
|
||||
required this.topProducts,
|
||||
required this.bestProduct,
|
||||
required this.categorySummary,
|
||||
required this.totalProducts,
|
||||
required this.totalRevenue,
|
||||
required this.totalQuantitySold,
|
||||
});
|
||||
}
|
||||
|
||||
// Category summary class
|
||||
class CategorySummary {
|
||||
final String categoryName;
|
||||
int productCount;
|
||||
int totalRevenue;
|
||||
|
||||
CategorySummary({
|
||||
required this.categoryName,
|
||||
required this.productCount,
|
||||
required this.totalRevenue,
|
||||
});
|
||||
}
|
||||
@ -193,7 +193,7 @@ class _MyAppState extends State<MyApp> {
|
||||
create: (context) => SummaryBloc(OrderRemoteDatasource()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => ProductSalesBloc(OrderItemRemoteDatasource()),
|
||||
create: (context) => ProductSalesBloc(AnalyticRemoteDatasource()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => ItemSalesReportBloc(AnalyticRemoteDatasource()),
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import 'package:enaklo_pos/data/datasources/analytic_remote_datasource.dart';
|
||||
import 'package:enaklo_pos/data/models/response/product_analytic_response_model.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:enaklo_pos/data/datasources/order_item_remote_datasource.dart';
|
||||
import 'package:enaklo_pos/data/models/response/product_sales_response_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'product_sales_event.dart';
|
||||
@ -8,14 +8,16 @@ part 'product_sales_state.dart';
|
||||
part 'product_sales_bloc.freezed.dart';
|
||||
|
||||
class ProductSalesBloc extends Bloc<ProductSalesEvent, ProductSalesState> {
|
||||
final OrderItemRemoteDatasource datasource;
|
||||
final AnalyticRemoteDatasource datasource;
|
||||
ProductSalesBloc(
|
||||
this.datasource,
|
||||
) : super(const _Initial()) {
|
||||
on<_GetProductSales>((event, emit) async {
|
||||
emit(const _Loading());
|
||||
final result = await datasource.getProductSalesByRangeDate(
|
||||
event.startDate, event.endDate);
|
||||
final result = await datasource.getProduct(
|
||||
dateFrom: event.startDate,
|
||||
dateTo: event.endDate,
|
||||
);
|
||||
result.fold((l) => emit(_Error(l)), (r) => emit(_Success(r.data!)));
|
||||
});
|
||||
}
|
||||
|
||||
@ -19,19 +19,20 @@ mixin _$ProductSalesEvent {
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() started,
|
||||
required TResult Function(String startDate, String endDate) getProductSales,
|
||||
required TResult Function(DateTime startDate, DateTime endDate)
|
||||
getProductSales,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? started,
|
||||
TResult? Function(String startDate, String endDate)? getProductSales,
|
||||
TResult? Function(DateTime startDate, DateTime endDate)? getProductSales,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? started,
|
||||
TResult Function(String startDate, String endDate)? getProductSales,
|
||||
TResult Function(DateTime startDate, DateTime endDate)? getProductSales,
|
||||
required TResult orElse(),
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@ -119,7 +120,8 @@ class _$StartedImpl implements _Started {
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() started,
|
||||
required TResult Function(String startDate, String endDate) getProductSales,
|
||||
required TResult Function(DateTime startDate, DateTime endDate)
|
||||
getProductSales,
|
||||
}) {
|
||||
return started();
|
||||
}
|
||||
@ -128,7 +130,7 @@ class _$StartedImpl implements _Started {
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? started,
|
||||
TResult? Function(String startDate, String endDate)? getProductSales,
|
||||
TResult? Function(DateTime startDate, DateTime endDate)? getProductSales,
|
||||
}) {
|
||||
return started?.call();
|
||||
}
|
||||
@ -137,7 +139,7 @@ class _$StartedImpl implements _Started {
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? started,
|
||||
TResult Function(String startDate, String endDate)? getProductSales,
|
||||
TResult Function(DateTime startDate, DateTime endDate)? getProductSales,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (started != null) {
|
||||
@ -188,7 +190,7 @@ abstract class _$$GetProductSalesImplCopyWith<$Res> {
|
||||
$Res Function(_$GetProductSalesImpl) then) =
|
||||
__$$GetProductSalesImplCopyWithImpl<$Res>;
|
||||
@useResult
|
||||
$Res call({String startDate, String endDate});
|
||||
$Res call({DateTime startDate, DateTime endDate});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -211,11 +213,11 @@ class __$$GetProductSalesImplCopyWithImpl<$Res>
|
||||
null == startDate
|
||||
? _value.startDate
|
||||
: startDate // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
as DateTime,
|
||||
null == endDate
|
||||
? _value.endDate
|
||||
: endDate // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
as DateTime,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -226,9 +228,9 @@ class _$GetProductSalesImpl implements _GetProductSales {
|
||||
const _$GetProductSalesImpl(this.startDate, this.endDate);
|
||||
|
||||
@override
|
||||
final String startDate;
|
||||
final DateTime startDate;
|
||||
@override
|
||||
final String endDate;
|
||||
final DateTime endDate;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@ -261,7 +263,8 @@ class _$GetProductSalesImpl implements _GetProductSales {
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() started,
|
||||
required TResult Function(String startDate, String endDate) getProductSales,
|
||||
required TResult Function(DateTime startDate, DateTime endDate)
|
||||
getProductSales,
|
||||
}) {
|
||||
return getProductSales(startDate, endDate);
|
||||
}
|
||||
@ -270,7 +273,7 @@ class _$GetProductSalesImpl implements _GetProductSales {
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? started,
|
||||
TResult? Function(String startDate, String endDate)? getProductSales,
|
||||
TResult? Function(DateTime startDate, DateTime endDate)? getProductSales,
|
||||
}) {
|
||||
return getProductSales?.call(startDate, endDate);
|
||||
}
|
||||
@ -279,7 +282,7 @@ class _$GetProductSalesImpl implements _GetProductSales {
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? started,
|
||||
TResult Function(String startDate, String endDate)? getProductSales,
|
||||
TResult Function(DateTime startDate, DateTime endDate)? getProductSales,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (getProductSales != null) {
|
||||
@ -321,11 +324,11 @@ class _$GetProductSalesImpl implements _GetProductSales {
|
||||
}
|
||||
|
||||
abstract class _GetProductSales implements ProductSalesEvent {
|
||||
const factory _GetProductSales(final String startDate, final String endDate) =
|
||||
_$GetProductSalesImpl;
|
||||
const factory _GetProductSales(
|
||||
final DateTime startDate, final DateTime endDate) = _$GetProductSalesImpl;
|
||||
|
||||
String get startDate;
|
||||
String get endDate;
|
||||
DateTime get startDate;
|
||||
DateTime get endDate;
|
||||
|
||||
/// Create a copy of ProductSalesEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@ -340,7 +343,7 @@ mixin _$ProductSalesState {
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() initial,
|
||||
required TResult Function() loading,
|
||||
required TResult Function(List<ProductSales> productSales) success,
|
||||
required TResult Function(ProductAnalyticData product) success,
|
||||
required TResult Function(String message) error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@ -348,7 +351,7 @@ mixin _$ProductSalesState {
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? initial,
|
||||
TResult? Function()? loading,
|
||||
TResult? Function(List<ProductSales> productSales)? success,
|
||||
TResult? Function(ProductAnalyticData product)? success,
|
||||
TResult? Function(String message)? error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@ -356,7 +359,7 @@ mixin _$ProductSalesState {
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? initial,
|
||||
TResult Function()? loading,
|
||||
TResult Function(List<ProductSales> productSales)? success,
|
||||
TResult Function(ProductAnalyticData product)? success,
|
||||
TResult Function(String message)? error,
|
||||
required TResult orElse(),
|
||||
}) =>
|
||||
@ -452,7 +455,7 @@ class _$InitialImpl implements _Initial {
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() initial,
|
||||
required TResult Function() loading,
|
||||
required TResult Function(List<ProductSales> productSales) success,
|
||||
required TResult Function(ProductAnalyticData product) success,
|
||||
required TResult Function(String message) error,
|
||||
}) {
|
||||
return initial();
|
||||
@ -463,7 +466,7 @@ class _$InitialImpl implements _Initial {
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? initial,
|
||||
TResult? Function()? loading,
|
||||
TResult? Function(List<ProductSales> productSales)? success,
|
||||
TResult? Function(ProductAnalyticData product)? success,
|
||||
TResult? Function(String message)? error,
|
||||
}) {
|
||||
return initial?.call();
|
||||
@ -474,7 +477,7 @@ class _$InitialImpl implements _Initial {
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? initial,
|
||||
TResult Function()? loading,
|
||||
TResult Function(List<ProductSales> productSales)? success,
|
||||
TResult Function(ProductAnalyticData product)? success,
|
||||
TResult Function(String message)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
@ -569,7 +572,7 @@ class _$LoadingImpl implements _Loading {
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() initial,
|
||||
required TResult Function() loading,
|
||||
required TResult Function(List<ProductSales> productSales) success,
|
||||
required TResult Function(ProductAnalyticData product) success,
|
||||
required TResult Function(String message) error,
|
||||
}) {
|
||||
return loading();
|
||||
@ -580,7 +583,7 @@ class _$LoadingImpl implements _Loading {
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? initial,
|
||||
TResult? Function()? loading,
|
||||
TResult? Function(List<ProductSales> productSales)? success,
|
||||
TResult? Function(ProductAnalyticData product)? success,
|
||||
TResult? Function(String message)? error,
|
||||
}) {
|
||||
return loading?.call();
|
||||
@ -591,7 +594,7 @@ class _$LoadingImpl implements _Loading {
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? initial,
|
||||
TResult Function()? loading,
|
||||
TResult Function(List<ProductSales> productSales)? success,
|
||||
TResult Function(ProductAnalyticData product)? success,
|
||||
TResult Function(String message)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
@ -649,7 +652,7 @@ abstract class _$$SuccessImplCopyWith<$Res> {
|
||||
_$SuccessImpl value, $Res Function(_$SuccessImpl) then) =
|
||||
__$$SuccessImplCopyWithImpl<$Res>;
|
||||
@useResult
|
||||
$Res call({List<ProductSales> productSales});
|
||||
$Res call({ProductAnalyticData product});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -665,13 +668,13 @@ class __$$SuccessImplCopyWithImpl<$Res>
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? productSales = null,
|
||||
Object? product = null,
|
||||
}) {
|
||||
return _then(_$SuccessImpl(
|
||||
null == productSales
|
||||
? _value._productSales
|
||||
: productSales // ignore: cast_nullable_to_non_nullable
|
||||
as List<ProductSales>,
|
||||
null == product
|
||||
? _value.product
|
||||
: product // ignore: cast_nullable_to_non_nullable
|
||||
as ProductAnalyticData,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -679,20 +682,14 @@ class __$$SuccessImplCopyWithImpl<$Res>
|
||||
/// @nodoc
|
||||
|
||||
class _$SuccessImpl implements _Success {
|
||||
const _$SuccessImpl(final List<ProductSales> productSales)
|
||||
: _productSales = productSales;
|
||||
const _$SuccessImpl(this.product);
|
||||
|
||||
final List<ProductSales> _productSales;
|
||||
@override
|
||||
List<ProductSales> get productSales {
|
||||
if (_productSales is EqualUnmodifiableListView) return _productSales;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_productSales);
|
||||
}
|
||||
final ProductAnalyticData product;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ProductSalesState.success(productSales: $productSales)';
|
||||
return 'ProductSalesState.success(product: $product)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -700,13 +697,11 @@ class _$SuccessImpl implements _Success {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SuccessImpl &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._productSales, _productSales));
|
||||
(identical(other.product, product) || other.product == product));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType, const DeepCollectionEquality().hash(_productSales));
|
||||
int get hashCode => Object.hash(runtimeType, product);
|
||||
|
||||
/// Create a copy of ProductSalesState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@ -721,10 +716,10 @@ class _$SuccessImpl implements _Success {
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() initial,
|
||||
required TResult Function() loading,
|
||||
required TResult Function(List<ProductSales> productSales) success,
|
||||
required TResult Function(ProductAnalyticData product) success,
|
||||
required TResult Function(String message) error,
|
||||
}) {
|
||||
return success(productSales);
|
||||
return success(product);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -732,10 +727,10 @@ class _$SuccessImpl implements _Success {
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? initial,
|
||||
TResult? Function()? loading,
|
||||
TResult? Function(List<ProductSales> productSales)? success,
|
||||
TResult? Function(ProductAnalyticData product)? success,
|
||||
TResult? Function(String message)? error,
|
||||
}) {
|
||||
return success?.call(productSales);
|
||||
return success?.call(product);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -743,12 +738,12 @@ class _$SuccessImpl implements _Success {
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? initial,
|
||||
TResult Function()? loading,
|
||||
TResult Function(List<ProductSales> productSales)? success,
|
||||
TResult Function(ProductAnalyticData product)? success,
|
||||
TResult Function(String message)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (success != null) {
|
||||
return success(productSales);
|
||||
return success(product);
|
||||
}
|
||||
return orElse();
|
||||
}
|
||||
@ -792,9 +787,9 @@ class _$SuccessImpl implements _Success {
|
||||
}
|
||||
|
||||
abstract class _Success implements ProductSalesState {
|
||||
const factory _Success(final List<ProductSales> productSales) = _$SuccessImpl;
|
||||
const factory _Success(final ProductAnalyticData product) = _$SuccessImpl;
|
||||
|
||||
List<ProductSales> get productSales;
|
||||
ProductAnalyticData get product;
|
||||
|
||||
/// Create a copy of ProductSalesState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@ -873,7 +868,7 @@ class _$ErrorImpl implements _Error {
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() initial,
|
||||
required TResult Function() loading,
|
||||
required TResult Function(List<ProductSales> productSales) success,
|
||||
required TResult Function(ProductAnalyticData product) success,
|
||||
required TResult Function(String message) error,
|
||||
}) {
|
||||
return error(message);
|
||||
@ -884,7 +879,7 @@ class _$ErrorImpl implements _Error {
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? initial,
|
||||
TResult? Function()? loading,
|
||||
TResult? Function(List<ProductSales> productSales)? success,
|
||||
TResult? Function(ProductAnalyticData product)? success,
|
||||
TResult? Function(String message)? error,
|
||||
}) {
|
||||
return error?.call(message);
|
||||
@ -895,7 +890,7 @@ class _$ErrorImpl implements _Error {
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? initial,
|
||||
TResult Function()? loading,
|
||||
TResult Function(List<ProductSales> productSales)? success,
|
||||
TResult Function(ProductAnalyticData product)? success,
|
||||
TResult Function(String message)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
|
||||
@ -4,7 +4,7 @@ part of 'product_sales_bloc.dart';
|
||||
class ProductSalesEvent with _$ProductSalesEvent {
|
||||
const factory ProductSalesEvent.started() = _Started;
|
||||
const factory ProductSalesEvent.getProductSales(
|
||||
String startDate,
|
||||
String endDate,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
) = _GetProductSales;
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ class ProductSalesState with _$ProductSalesState {
|
||||
|
||||
const factory ProductSalesState.loading() = _Loading;
|
||||
|
||||
const factory ProductSalesState.success(List<ProductSales> productSales) =
|
||||
const factory ProductSalesState.success(ProductAnalyticData product) =
|
||||
_Success;
|
||||
|
||||
const factory ProductSalesState.error(String message) = _Error;
|
||||
|
||||
@ -14,7 +14,7 @@ import 'package:enaklo_pos/presentation/report/blocs/summary/summary_bloc.dart';
|
||||
import 'package:enaklo_pos/presentation/report/blocs/transaction_report/transaction_report_bloc.dart';
|
||||
import 'package:enaklo_pos/presentation/report/widgets/item_sales_report_widget.dart';
|
||||
import 'package:enaklo_pos/presentation/report/widgets/payment_method_report_widget.dart';
|
||||
import 'package:enaklo_pos/presentation/report/widgets/product_sales_chart_widget.dart';
|
||||
import 'package:enaklo_pos/presentation/report/widgets/product_analytic_widget.dart';
|
||||
import 'package:enaklo_pos/presentation/report/widgets/report_menu.dart';
|
||||
import 'package:enaklo_pos/presentation/report/widgets/report_title.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -139,19 +139,19 @@ class _ReportPageState extends State<ReportPage> {
|
||||
isActive: selectedMenu == 1,
|
||||
),
|
||||
ReportMenu(
|
||||
label: 'Chart Penjualan Produk',
|
||||
label: 'Laporan Penjualan Produk',
|
||||
subtitle:
|
||||
'Grafik visual penjualan produk untuk analisa performa penjualan.',
|
||||
'Laporan penjualan berdasarkan masing-masing produk.',
|
||||
icon: Icons.bar_chart_outlined,
|
||||
onPressed: () {
|
||||
selectedMenu = 2;
|
||||
title = 'Chart Penjualan Produk';
|
||||
title = 'Laporan Penjualan Produk';
|
||||
setState(() {});
|
||||
context.read<ProductSalesBloc>().add(
|
||||
ProductSalesEvent.getProductSales(
|
||||
DateFormatter.formatDateTime(
|
||||
fromDate),
|
||||
DateFormatter.formatDateTime(toDate)),
|
||||
fromDate,
|
||||
toDate,
|
||||
),
|
||||
);
|
||||
},
|
||||
isActive: selectedMenu == 2,
|
||||
@ -262,12 +262,12 @@ class _ReportPageState extends State<ReportPage> {
|
||||
error: (message) {
|
||||
return Text(message);
|
||||
},
|
||||
success: (productSales) {
|
||||
return ProductSalesChartWidgets(
|
||||
success: (products) {
|
||||
return ProductAnalyticsWidget(
|
||||
title: title,
|
||||
searchDateFormatted:
|
||||
searchDateFormatted,
|
||||
productSales: productSales,
|
||||
productData: products,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
498
lib/presentation/report/widgets/product_analytic_widget.dart
Normal file
498
lib/presentation/report/widgets/product_analytic_widget.dart
Normal file
@ -0,0 +1,498 @@
|
||||
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||
|
||||
import 'package:enaklo_pos/data/models/response/product_analytic_response_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class ProductAnalyticsWidget extends StatelessWidget {
|
||||
final ProductAnalyticData productData;
|
||||
final String title;
|
||||
final String searchDateFormatted;
|
||||
|
||||
const ProductAnalyticsWidget(
|
||||
{super.key,
|
||||
required this.productData,
|
||||
required this.title,
|
||||
required this.searchDateFormatted});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Proses data untuk mendapatkan insights
|
||||
final insights = _processProductData(productData);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: const Color(0xFFD1D5DB),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header Section dengan Icon dan Stats
|
||||
_buildHeader(insights),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Category Summary Cards (Horizontal Scroll)
|
||||
SizedBox(
|
||||
height: 80,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: insights.categorySummary.length,
|
||||
itemBuilder: (context, index) {
|
||||
final category = insights.categorySummary[index];
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: index == insights.categorySummary.length - 1
|
||||
? 0
|
||||
: 12),
|
||||
child: _buildCategorySummaryCard(
|
||||
categoryName: category.categoryName,
|
||||
productCount: category.productCount,
|
||||
totalRevenue: category.totalRevenue,
|
||||
color: _getCategoryColor(category.categoryName),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Top Products Section
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Produk Berkinerja Terbaik',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF111827),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF3F4F6),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFD1D5DB),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Berdasarkan Pendapatan',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: const Color(0xFF6B7280),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Product List dengan data dinamis
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: insights.topProducts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final product = insights.topProducts[index];
|
||||
return _buildProductItem(
|
||||
rank: index + 1,
|
||||
product: product,
|
||||
isTopPerformer: product == insights.bestProduct,
|
||||
categoryColor: _getCategoryColor(product.categoryName),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Bottom Summary dengan insights dinamis
|
||||
_buildBottomSummary(insights.bestProduct),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Method untuk memproses data dan mendapatkan insights
|
||||
ProductInsights _processProductData(ProductAnalyticData data) {
|
||||
// Sort products by revenue (descending) untuk ranking
|
||||
List<ProductAnalyticItem> sortedProducts = List.from(data.data);
|
||||
sortedProducts.sort((a, b) => b.revenue.compareTo(a.revenue));
|
||||
|
||||
// Best product adalah yang revenue tertinggi
|
||||
ProductAnalyticItem? bestProduct;
|
||||
if (sortedProducts.isNotEmpty) {
|
||||
bestProduct = sortedProducts.first;
|
||||
}
|
||||
|
||||
// Group by category untuk summary
|
||||
Map<String, CategorySummary> categoryMap = {};
|
||||
|
||||
for (var product in data.data) {
|
||||
if (categoryMap.containsKey(product.categoryName)) {
|
||||
categoryMap[product.categoryName]!.productCount++;
|
||||
categoryMap[product.categoryName]!.totalRevenue += product.revenue;
|
||||
} else {
|
||||
categoryMap[product.categoryName] = CategorySummary(
|
||||
categoryName: product.categoryName,
|
||||
productCount: 1,
|
||||
totalRevenue: product.revenue,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert map to list dan sort by revenue
|
||||
List<CategorySummary> categorySummary = categoryMap.values.toList();
|
||||
categorySummary.sort((a, b) => b.totalRevenue.compareTo(a.totalRevenue));
|
||||
|
||||
// Calculate total metrics
|
||||
int totalProducts = data.data.length;
|
||||
int totalRevenue = data.data.fold(0, (sum, item) => sum + item.revenue);
|
||||
int totalQuantitySold =
|
||||
data.data.fold(0, (sum, item) => sum + item.quantitySold);
|
||||
|
||||
return ProductInsights(
|
||||
topProducts: sortedProducts,
|
||||
bestProduct: bestProduct,
|
||||
categorySummary: categorySummary,
|
||||
totalProducts: totalProducts,
|
||||
totalRevenue: totalRevenue,
|
||||
totalQuantitySold: totalQuantitySold,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(ProductInsights insights) {
|
||||
return Row(
|
||||
children: [
|
||||
// Icon Container
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF3B82F6),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.inventory_2,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Title and Period
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: const Color(0xFF111827),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
searchDateFormatted,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: const Color(0xFF6B7280),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Total Products Badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF059669),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
'${insights.totalProducts} Produk',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomSummary(ProductAnalyticItem? bestProduct) {
|
||||
if (bestProduct == null) return Container();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFEF3C7),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFD97706),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.star,
|
||||
color: const Color(0xFFD97706),
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${bestProduct.productName} memimpin dengan ${bestProduct.quantitySold} unit terjual dan pendapatan ${_formatCurrency(bestProduct.revenue)}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: const Color(0xff92400E),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Helper method untuk category color
|
||||
Color _getCategoryColor(String categoryName) {
|
||||
switch (categoryName.toLowerCase()) {
|
||||
case 'minuman':
|
||||
return const Color(0xFF06B6D4);
|
||||
case 'makanan':
|
||||
return const Color(0xFFEF4444);
|
||||
case 'snack':
|
||||
return const Color(0xFF8B5CF6);
|
||||
default:
|
||||
return const Color(0xFF6B7280);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildCategorySummaryCard({
|
||||
required String categoryName,
|
||||
required int productCount,
|
||||
required int totalRevenue,
|
||||
required Color color,
|
||||
}) {
|
||||
return Container(
|
||||
width: 140,
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: color.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
categoryName,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$productCount items',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: const Color(0xFF6B7280),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_formatCurrency(totalRevenue),
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: const Color(0xFF111827),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProductItem({
|
||||
required int rank,
|
||||
required ProductAnalyticItem product,
|
||||
required bool isTopPerformer,
|
||||
required Color categoryColor,
|
||||
}) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isTopPerformer ? const Color(0xFFF0F9FF) : const Color(0xFFF9FAFB),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isTopPerformer
|
||||
? const Color(0xFF3B82F6)
|
||||
: const Color(0xFFE5E7EB),
|
||||
width: isTopPerformer ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Rank Badge
|
||||
Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: isTopPerformer
|
||||
? const Color(0xFF3B82F6)
|
||||
: const Color(0xFF6B7280),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'$rank',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Product Info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
product.productName,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF111827),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
if (isTopPerformer)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6, vertical: 2),
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF10B981),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
'BEST',
|
||||
style: TextStyle(
|
||||
fontSize: 8,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_formatCurrency(product.revenue),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: const Color(0xFF111827),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
// Category Badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: categoryColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
product.categoryName,
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: categoryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// Stats
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${product.quantitySold} units • ${product.orderCount} orders • Avg ${_formatCurrency(product.averagePrice.round())}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: const Color(0xFF6B7280),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Helper method untuk format currency
|
||||
String _formatCurrency(int amount) {
|
||||
if (amount >= 1000000) {
|
||||
return 'Rp ${(amount / 1000000).toStringAsFixed(1)}M';
|
||||
} else if (amount >= 1000) {
|
||||
return 'Rp ${(amount / 1000).toStringAsFixed(0)}K';
|
||||
} else {
|
||||
return 'Rp ${NumberFormat('#,###').format(amount)}';
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,128 +0,0 @@
|
||||
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||
|
||||
import 'package:enaklo_pos/core/components/spaces.dart';
|
||||
import 'package:enaklo_pos/presentation/report/widgets/report_page_title.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pie_chart/pie_chart.dart';
|
||||
|
||||
import 'package:enaklo_pos/data/models/response/product_sales_response_model.dart';
|
||||
|
||||
class ProductSalesChartWidgets extends StatefulWidget {
|
||||
final String title;
|
||||
final String searchDateFormatted;
|
||||
final List<ProductSales> productSales;
|
||||
const ProductSalesChartWidgets({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.searchDateFormatted,
|
||||
required this.productSales,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ProductSalesChartWidgets> createState() =>
|
||||
_ProductSalesChartWidgetsState();
|
||||
}
|
||||
|
||||
class _ProductSalesChartWidgetsState extends State<ProductSalesChartWidgets> {
|
||||
Map<String, double> dataMap2 = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
loadData();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
loadData() {
|
||||
for (var data in widget.productSales) {
|
||||
dataMap2[data.productName ?? 'Unknown'] =
|
||||
double.parse(data.totalQuantity!);
|
||||
}
|
||||
}
|
||||
|
||||
final colorList = <Color>[
|
||||
const Color(0xfffdcb6e),
|
||||
const Color(0xff0984e3),
|
||||
const Color(0xfffd79a8),
|
||||
const Color(0xffe17055),
|
||||
const Color(0xff6c5ce7),
|
||||
const Color(0xfff0932b),
|
||||
const Color(0xff6ab04c),
|
||||
const Color(0xfff8a5c2),
|
||||
const Color(0xffe84393),
|
||||
const Color(0xfffd79a8),
|
||||
const Color(0xffa29bfe),
|
||||
const Color(0xff00b894),
|
||||
const Color(0xffe17055),
|
||||
const Color(0xffd63031),
|
||||
const Color(0xffa29bfe),
|
||||
const Color(0xff6c5ce7),
|
||||
const Color(0xff00cec9),
|
||||
const Color(0xfffad390),
|
||||
const Color(0xff686de0),
|
||||
const Color(0xfffdcb6e),
|
||||
const Color(0xff0984e3),
|
||||
const Color(0xfffd79a8),
|
||||
const Color(0xffe17055),
|
||||
const Color(0xff6c5ce7),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
ReportPageTitle(
|
||||
title: widget.title,
|
||||
searchDateFormatted: widget.searchDateFormatted,
|
||||
onExport: () async {},
|
||||
isExport: false, // Set to false if export is not needed
|
||||
),
|
||||
const SpaceHeight(16.0),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
margin: const EdgeInsets.all(12.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
PieChart(
|
||||
dataMap: dataMap2,
|
||||
animationDuration: Duration(milliseconds: 800),
|
||||
chartLegendSpacing: 32,
|
||||
chartRadius: MediaQuery.of(context).size.width / 3.2,
|
||||
colorList: colorList,
|
||||
initialAngleInDegree: 0,
|
||||
chartType: ChartType.disc,
|
||||
ringStrokeWidth: 32,
|
||||
// centerText: "HYBRID",
|
||||
legendOptions: LegendOptions(
|
||||
showLegendsInRow: false,
|
||||
legendPosition: LegendPosition.right,
|
||||
showLegends: true,
|
||||
legendShape: BoxShape.circle,
|
||||
legendTextStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
chartValuesOptions: ChartValuesOptions(
|
||||
showChartValueBackground: true,
|
||||
showChartValues: true,
|
||||
showChartValuesInPercentage: false,
|
||||
showChartValuesOutside: false,
|
||||
decimalPlaces: 0,
|
||||
),
|
||||
// gradientList: ---To add gradient colors---
|
||||
// emptyColorGradient: ---Empty Color gradient---
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user