Compare commits

..

3 Commits

Author SHA1 Message Date
efrilm
bdb2c0ba52 feat: Dashboard Analytic 2025-08-06 13:05:58 +07:00
efrilm
91335ad8db feat: product analytic 2025-08-06 12:32:53 +07:00
efrilm
648a4f5eb4 fix: sales analytic 2025-08-06 12:10:56 +07:00
19 changed files with 1760 additions and 284 deletions

View File

@ -5,7 +5,9 @@ import 'package:dio/dio.dart';
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/dashboard_analytic_response_model.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 +81,72 @@ 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');
}
}
Future<Either<String, DashboardAnalyticResponseModel>> getDashboard({
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/dashboard',
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(DashboardAnalyticResponseModel.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');
}
}
}

View File

@ -0,0 +1,242 @@
class DashboardAnalyticResponseModel {
final bool success;
final DashboardAnalyticData data;
final dynamic errors;
DashboardAnalyticResponseModel({
required this.success,
required this.data,
this.errors,
});
/// Khusus untuk JSON string
factory DashboardAnalyticResponseModel.fromJson(Map<String, dynamic> json) =>
DashboardAnalyticResponseModel.fromMap(json);
/// Untuk menerima Map biasa (bukan dari JSON string)
factory DashboardAnalyticResponseModel.fromMap(Map<String, dynamic> map) {
return DashboardAnalyticResponseModel(
success: map['success'] ?? false,
data: DashboardAnalyticData.fromMap(map['data'] ?? {}),
errors: map['errors'],
);
}
Map<String, dynamic> toJson() => toMap();
Map<String, dynamic> toMap() => {
'success': success,
'data': data.toMap(),
'errors': errors,
};
}
class DashboardAnalyticData {
final String organizationId;
final String outletId;
final String dateFrom;
final String dateTo;
final DashboardOverview overview;
final List<TopProduct> topProducts;
final List<PaymentMethodAnalytic> paymentMethods;
final List<RecentSale> recentSales;
DashboardAnalyticData({
required this.organizationId,
required this.outletId,
required this.dateFrom,
required this.dateTo,
required this.overview,
required this.topProducts,
required this.paymentMethods,
required this.recentSales,
});
factory DashboardAnalyticData.fromMap(Map<String, dynamic> map) =>
DashboardAnalyticData(
organizationId: map['organization_id'],
outletId: map['outlet_id'],
dateFrom: map['date_from'],
dateTo: map['date_to'],
overview: DashboardOverview.fromMap(map['overview']),
topProducts: List<TopProduct>.from(
map['top_products']?.map((x) => TopProduct.fromMap(x))),
paymentMethods: List<PaymentMethodAnalytic>.from(map['payment_methods']
?.map((x) => PaymentMethodAnalytic.fromMap(x))),
recentSales: List<RecentSale>.from(
map['recent_sales']?.map((x) => RecentSale.fromMap(x))),
);
Map<String, dynamic> toMap() => {
'organization_id': organizationId,
'outlet_id': outletId,
'date_from': dateFrom,
'date_to': dateTo,
'overview': overview.toMap(),
'top_products': topProducts.map((x) => x.toMap()).toList(),
'payment_methods': paymentMethods.map((x) => x.toMap()).toList(),
'recent_sales': recentSales.map((x) => x.toMap()).toList(),
};
}
class DashboardOverview {
final int totalSales;
final int totalOrders;
final double averageOrderValue;
final int totalCustomers;
final int voidedOrders;
final int refundedOrders;
DashboardOverview({
required this.totalSales,
required this.totalOrders,
required this.averageOrderValue,
required this.totalCustomers,
required this.voidedOrders,
required this.refundedOrders,
});
factory DashboardOverview.fromMap(Map<String, dynamic> map) =>
DashboardOverview(
totalSales: map['total_sales'],
totalOrders: map['total_orders'],
averageOrderValue: map['average_order_value']?.toDouble() ?? 0.0,
totalCustomers: map['total_customers'],
voidedOrders: map['voided_orders'],
refundedOrders: map['refunded_orders'],
);
Map<String, dynamic> toMap() => {
'total_sales': totalSales,
'total_orders': totalOrders,
'average_order_value': averageOrderValue,
'total_customers': totalCustomers,
'voided_orders': voidedOrders,
'refunded_orders': refundedOrders,
};
}
class TopProduct {
final String productId;
final String productName;
final String categoryId;
final String categoryName;
final int quantitySold;
final int revenue;
final double averagePrice;
final int orderCount;
TopProduct({
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 TopProduct.fromMap(Map<String, dynamic> map) => TopProduct(
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']?.toDouble() ?? 0.0,
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 PaymentMethodAnalytic {
final String paymentMethodId;
final String paymentMethodName;
final String paymentMethodType;
final int totalAmount;
final int orderCount;
final int paymentCount;
final double percentage;
PaymentMethodAnalytic({
required this.paymentMethodId,
required this.paymentMethodName,
required this.paymentMethodType,
required this.totalAmount,
required this.orderCount,
required this.paymentCount,
required this.percentage,
});
factory PaymentMethodAnalytic.fromMap(Map<String, dynamic> map) =>
PaymentMethodAnalytic(
paymentMethodId: map['payment_method_id'],
paymentMethodName: map['payment_method_name'],
paymentMethodType: map['payment_method_type'],
totalAmount: map['total_amount'],
orderCount: map['order_count'],
paymentCount: map['payment_count'],
percentage: map['percentage']?.toDouble() ?? 0.0,
);
Map<String, dynamic> toMap() => {
'payment_method_id': paymentMethodId,
'payment_method_name': paymentMethodName,
'payment_method_type': paymentMethodType,
'total_amount': totalAmount,
'order_count': orderCount,
'payment_count': paymentCount,
'percentage': percentage,
};
}
class RecentSale {
final String date;
final int sales;
final int orders;
final int items;
final int tax;
final int discount;
final int netSales;
RecentSale({
required this.date,
required this.sales,
required this.orders,
required this.items,
required this.tax,
required this.discount,
required this.netSales,
});
factory RecentSale.fromMap(Map<String, dynamic> map) => RecentSale(
date: map['date'],
sales: map['sales'],
orders: map['orders'],
items: map['items'],
tax: map['tax'],
discount: map['discount'],
netSales: map['net_sales'],
);
Map<String, dynamic> toMap() => {
'date': date,
'sales': sales,
'orders': orders,
'items': items,
'tax': tax,
'discount': discount,
'net_sales': netSales,
};
}

View 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,
});
}

View File

@ -27,7 +27,6 @@ import 'package:enaklo_pos/data/datasources/midtrans_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/order_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/product_local_datasource.dart';
import 'package:enaklo_pos/data/datasources/product_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/order_item_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/payment_methods_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/settings_local_datasource.dart';
import 'package:enaklo_pos/presentation/auth/bloc/logout/logout_bloc.dart';
@ -190,10 +189,10 @@ class _MyAppState extends State<MyApp> {
create: (context) => GetCategoriesBloc(CategoryRemoteDatasource()),
),
BlocProvider(
create: (context) => SummaryBloc(OrderRemoteDatasource()),
create: (context) => SummaryBloc(AnalyticRemoteDatasource()),
),
BlocProvider(
create: (context) => ProductSalesBloc(OrderItemRemoteDatasource()),
create: (context) => ProductSalesBloc(AnalyticRemoteDatasource()),
),
BlocProvider(
create: (context) => ItemSalesReportBloc(AnalyticRemoteDatasource()),

View File

@ -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!)));
});
}

View File

@ -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(),
}) {

View File

@ -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;
}

View File

@ -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;

View File

@ -1,6 +1,6 @@
import 'package:enaklo_pos/data/datasources/analytic_remote_datasource.dart';
import 'package:enaklo_pos/data/models/response/dashboard_analytic_response_model.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:enaklo_pos/data/datasources/order_remote_datasource.dart';
import 'package:enaklo_pos/data/models/response/summary_response_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'summary_event.dart';
@ -8,15 +8,17 @@ part 'summary_state.dart';
part 'summary_bloc.freezed.dart';
class SummaryBloc extends Bloc<SummaryEvent, SummaryState> {
final OrderRemoteDatasource datasource;
final AnalyticRemoteDatasource datasource;
SummaryBloc(
this.datasource,
) : super(const _Initial()) {
on<_GetSummary>((event, emit) async {
emit(const _Loading());
final result = await datasource.getSummaryByRangeDate(
event.startDate, event.endDate);
result.fold((l) => emit(_Error(l)), (r) => emit(_Success(r.data!)));
final result = await datasource.getDashboard(
dateFrom: event.startDate,
dateTo: event.endDate,
);
result.fold((l) => emit(_Error(l)), (r) => emit(_Success(r.data)));
});
}
}

View File

@ -19,19 +19,19 @@ mixin _$SummaryEvent {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() started,
required TResult Function(String startDate, String endDate) getSummary,
required TResult Function(DateTime startDate, DateTime endDate) getSummary,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started,
TResult? Function(String startDate, String endDate)? getSummary,
TResult? Function(DateTime startDate, DateTime endDate)? getSummary,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? started,
TResult Function(String startDate, String endDate)? getSummary,
TResult Function(DateTime startDate, DateTime endDate)? getSummary,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
@ -119,7 +119,7 @@ class _$StartedImpl implements _Started {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() started,
required TResult Function(String startDate, String endDate) getSummary,
required TResult Function(DateTime startDate, DateTime endDate) getSummary,
}) {
return started();
}
@ -128,7 +128,7 @@ class _$StartedImpl implements _Started {
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started,
TResult? Function(String startDate, String endDate)? getSummary,
TResult? Function(DateTime startDate, DateTime endDate)? getSummary,
}) {
return started?.call();
}
@ -137,7 +137,7 @@ class _$StartedImpl implements _Started {
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? started,
TResult Function(String startDate, String endDate)? getSummary,
TResult Function(DateTime startDate, DateTime endDate)? getSummary,
required TResult orElse(),
}) {
if (started != null) {
@ -188,7 +188,7 @@ abstract class _$$GetSummaryImplCopyWith<$Res> {
_$GetSummaryImpl value, $Res Function(_$GetSummaryImpl) then) =
__$$GetSummaryImplCopyWithImpl<$Res>;
@useResult
$Res call({String startDate, String endDate});
$Res call({DateTime startDate, DateTime endDate});
}
/// @nodoc
@ -211,11 +211,11 @@ class __$$GetSummaryImplCopyWithImpl<$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 +226,9 @@ class _$GetSummaryImpl implements _GetSummary {
const _$GetSummaryImpl(this.startDate, this.endDate);
@override
final String startDate;
final DateTime startDate;
@override
final String endDate;
final DateTime endDate;
@override
String toString() {
@ -260,7 +260,7 @@ class _$GetSummaryImpl implements _GetSummary {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() started,
required TResult Function(String startDate, String endDate) getSummary,
required TResult Function(DateTime startDate, DateTime endDate) getSummary,
}) {
return getSummary(startDate, endDate);
}
@ -269,7 +269,7 @@ class _$GetSummaryImpl implements _GetSummary {
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started,
TResult? Function(String startDate, String endDate)? getSummary,
TResult? Function(DateTime startDate, DateTime endDate)? getSummary,
}) {
return getSummary?.call(startDate, endDate);
}
@ -278,7 +278,7 @@ class _$GetSummaryImpl implements _GetSummary {
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? started,
TResult Function(String startDate, String endDate)? getSummary,
TResult Function(DateTime startDate, DateTime endDate)? getSummary,
required TResult orElse(),
}) {
if (getSummary != null) {
@ -320,11 +320,11 @@ class _$GetSummaryImpl implements _GetSummary {
}
abstract class _GetSummary implements SummaryEvent {
const factory _GetSummary(final String startDate, final String endDate) =
const factory _GetSummary(final DateTime startDate, final DateTime endDate) =
_$GetSummaryImpl;
String get startDate;
String get endDate;
DateTime get startDate;
DateTime get endDate;
/// Create a copy of SummaryEvent
/// with the given fields replaced by the non-null parameter values.
@ -339,7 +339,7 @@ mixin _$SummaryState {
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(SummaryModel summary) success,
required TResult Function(DashboardAnalyticData data) success,
required TResult Function(String message) error,
}) =>
throw _privateConstructorUsedError;
@ -347,7 +347,7 @@ mixin _$SummaryState {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(SummaryModel summary)? success,
TResult? Function(DashboardAnalyticData data)? success,
TResult? Function(String message)? error,
}) =>
throw _privateConstructorUsedError;
@ -355,7 +355,7 @@ mixin _$SummaryState {
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(SummaryModel summary)? success,
TResult Function(DashboardAnalyticData data)? success,
TResult Function(String message)? error,
required TResult orElse(),
}) =>
@ -451,7 +451,7 @@ class _$InitialImpl implements _Initial {
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(SummaryModel summary) success,
required TResult Function(DashboardAnalyticData data) success,
required TResult Function(String message) error,
}) {
return initial();
@ -462,7 +462,7 @@ class _$InitialImpl implements _Initial {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(SummaryModel summary)? success,
TResult? Function(DashboardAnalyticData data)? success,
TResult? Function(String message)? error,
}) {
return initial?.call();
@ -473,7 +473,7 @@ class _$InitialImpl implements _Initial {
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(SummaryModel summary)? success,
TResult Function(DashboardAnalyticData data)? success,
TResult Function(String message)? error,
required TResult orElse(),
}) {
@ -568,7 +568,7 @@ class _$LoadingImpl implements _Loading {
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(SummaryModel summary) success,
required TResult Function(DashboardAnalyticData data) success,
required TResult Function(String message) error,
}) {
return loading();
@ -579,7 +579,7 @@ class _$LoadingImpl implements _Loading {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(SummaryModel summary)? success,
TResult? Function(DashboardAnalyticData data)? success,
TResult? Function(String message)? error,
}) {
return loading?.call();
@ -590,7 +590,7 @@ class _$LoadingImpl implements _Loading {
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(SummaryModel summary)? success,
TResult Function(DashboardAnalyticData data)? success,
TResult Function(String message)? error,
required TResult orElse(),
}) {
@ -648,7 +648,7 @@ abstract class _$$SuccessImplCopyWith<$Res> {
_$SuccessImpl value, $Res Function(_$SuccessImpl) then) =
__$$SuccessImplCopyWithImpl<$Res>;
@useResult
$Res call({SummaryModel summary});
$Res call({DashboardAnalyticData data});
}
/// @nodoc
@ -664,13 +664,13 @@ class __$$SuccessImplCopyWithImpl<$Res>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? summary = null,
Object? data = null,
}) {
return _then(_$SuccessImpl(
null == summary
? _value.summary
: summary // ignore: cast_nullable_to_non_nullable
as SummaryModel,
null == data
? _value.data
: data // ignore: cast_nullable_to_non_nullable
as DashboardAnalyticData,
));
}
}
@ -678,14 +678,14 @@ class __$$SuccessImplCopyWithImpl<$Res>
/// @nodoc
class _$SuccessImpl implements _Success {
const _$SuccessImpl(this.summary);
const _$SuccessImpl(this.data);
@override
final SummaryModel summary;
final DashboardAnalyticData data;
@override
String toString() {
return 'SummaryState.success(summary: $summary)';
return 'SummaryState.success(data: $data)';
}
@override
@ -693,11 +693,11 @@ class _$SuccessImpl implements _Success {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SuccessImpl &&
(identical(other.summary, summary) || other.summary == summary));
(identical(other.data, data) || other.data == data));
}
@override
int get hashCode => Object.hash(runtimeType, summary);
int get hashCode => Object.hash(runtimeType, data);
/// Create a copy of SummaryState
/// with the given fields replaced by the non-null parameter values.
@ -712,10 +712,10 @@ class _$SuccessImpl implements _Success {
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(SummaryModel summary) success,
required TResult Function(DashboardAnalyticData data) success,
required TResult Function(String message) error,
}) {
return success(summary);
return success(data);
}
@override
@ -723,10 +723,10 @@ class _$SuccessImpl implements _Success {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(SummaryModel summary)? success,
TResult? Function(DashboardAnalyticData data)? success,
TResult? Function(String message)? error,
}) {
return success?.call(summary);
return success?.call(data);
}
@override
@ -734,12 +734,12 @@ class _$SuccessImpl implements _Success {
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(SummaryModel summary)? success,
TResult Function(DashboardAnalyticData data)? success,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (success != null) {
return success(summary);
return success(data);
}
return orElse();
}
@ -783,9 +783,9 @@ class _$SuccessImpl implements _Success {
}
abstract class _Success implements SummaryState {
const factory _Success(final SummaryModel summary) = _$SuccessImpl;
const factory _Success(final DashboardAnalyticData data) = _$SuccessImpl;
SummaryModel get summary;
DashboardAnalyticData get data;
/// Create a copy of SummaryState
/// with the given fields replaced by the non-null parameter values.
@ -864,7 +864,7 @@ class _$ErrorImpl implements _Error {
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(SummaryModel summary) success,
required TResult Function(DashboardAnalyticData data) success,
required TResult Function(String message) error,
}) {
return error(message);
@ -875,7 +875,7 @@ class _$ErrorImpl implements _Error {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(SummaryModel summary)? success,
TResult? Function(DashboardAnalyticData data)? success,
TResult? Function(String message)? error,
}) {
return error?.call(message);
@ -886,7 +886,7 @@ class _$ErrorImpl implements _Error {
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(SummaryModel summary)? success,
TResult Function(DashboardAnalyticData data)? success,
TResult Function(String message)? error,
required TResult orElse(),
}) {

View File

@ -3,6 +3,6 @@ part of 'summary_bloc.dart';
@freezed
class SummaryEvent with _$SummaryEvent {
const factory SummaryEvent.started() = _Started;
const factory SummaryEvent.getSummary(String startDate, String endDate) =
const factory SummaryEvent.getSummary(DateTime startDate, DateTime endDate) =
_GetSummary;
}

View File

@ -4,6 +4,6 @@ part of 'summary_bloc.dart';
class SummaryState with _$SummaryState {
const factory SummaryState.initial() = _Initial;
const factory SummaryState.loading() = _Loading;
const factory SummaryState.success(SummaryModel summary) = _Success;
const factory SummaryState.success(DashboardAnalyticData data) = _Success;
const factory SummaryState.error(String message) = _Error;
}

View File

@ -1,6 +1,7 @@
import 'dart:developer';
import 'package:enaklo_pos/core/extensions/build_context_ext.dart';
import 'package:enaklo_pos/presentation/report/widgets/dashboard_analytic_widget.dart';
import 'package:enaklo_pos/presentation/sales/pages/sales_page.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:enaklo_pos/core/components/custom_date_picker.dart';
@ -14,11 +15,10 @@ 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';
import 'package:enaklo_pos/presentation/report/widgets/summary_report_widget.dart';
import 'package:enaklo_pos/presentation/report/widgets/transaction_report_widget.dart';
import '../../../core/components/spaces.dart';
@ -32,7 +32,7 @@ class ReportPage extends StatefulWidget {
class _ReportPageState extends State<ReportPage> {
int selectedMenu = 1;
String title = 'Transaction Report';
String title = 'Laporan Penjualan Item';
DateTime fromDate = DateTime.now().subtract(const Duration(days: 30));
DateTime toDate = DateTime.now();
@ -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,
@ -166,10 +166,7 @@ class _ReportPageState extends State<ReportPage> {
title = 'Ringkasan Laporan Penjualan';
setState(() {});
context.read<SummaryBloc>().add(
SummaryEvent.getSummary(
DateFormatter.formatDateTime(
fromDate),
DateFormatter.formatDateTime(toDate)),
SummaryEvent.getSummary(fromDate, toDate),
);
log("Date ${DateFormatter.formatDateTime(fromDate)}");
@ -262,12 +259,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,
);
},
);
@ -284,9 +281,9 @@ class _ReportPageState extends State<ReportPage> {
error: (message) {
return Text(message);
},
success: (summary) {
return SummaryReportWidget(
summary: summary,
success: (data) {
return DashboardAnalyticWidget(
data: data,
title: title,
searchDateFormatted:
searchDateFormatted,

View File

@ -0,0 +1,639 @@
import 'package:enaklo_pos/core/constants/colors.dart';
import 'package:enaklo_pos/data/models/response/dashboard_analytic_response_model.dart';
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:intl/intl.dart';
// App Colors
class AppColorDashboard {
static const secondary = Color(0xff7c3aed);
static const success = Color(0xff10b981);
static const warning = Color(0xfff59e0b);
static const danger = Color(0xffef4444);
static const info = Color(0xff3b82f6);
}
class DashboardAnalyticWidget extends StatelessWidget {
final String title;
final String searchDateFormatted;
final DashboardAnalyticData data;
const DashboardAnalyticWidget({
super.key,
required this.data,
required this.title,
required this.searchDateFormatted,
});
@override
Widget build(BuildContext context) {
return Container(
color: const Color(0xFFF8FAFC),
padding: const EdgeInsets.all(24.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 24),
_buildKPICards(),
const SizedBox(height: 24),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(flex: 2, child: _buildSalesChart()),
const SizedBox(width: 16),
Expanded(flex: 1, child: _buildProductChart()),
],
),
const SizedBox(height: 24),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(flex: 2, child: _buildTopProductsList()),
const SizedBox(width: 16),
Expanded(flex: 1, child: _buildOrderSummary()),
],
),
],
),
),
);
}
Widget _buildHeader() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 4),
Text(
'Analisis performa penjualan outlet',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: AppColors.primary,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.calendar_today, color: Colors.white, size: 16),
const SizedBox(width: 8),
Text(
searchDateFormatted,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
),
],
);
}
Widget _buildKPICards() {
final successfulOrders = data.overview.totalOrders -
data.overview.voidedOrders -
data.overview.refundedOrders;
final kpiData = [
{
'title': 'Total Penjualan',
'value': _formatCurrency(data.overview.totalSales),
'icon': Icons.trending_up,
'color': AppColorDashboard.success,
'bgColor': AppColorDashboard.success.withOpacity(0.1),
},
{
'title': 'Total Pesanan',
'value': '${data.overview.totalOrders}',
'icon': Icons.shopping_cart,
'color': AppColorDashboard.info,
'bgColor': AppColorDashboard.info.withOpacity(0.1),
},
{
'title': 'Rata-rata Pesanan',
'value': _formatCurrency(data.overview.averageOrderValue.toInt()),
'icon': Icons.attach_money,
'color': AppColorDashboard.warning,
'bgColor': AppColorDashboard.warning.withOpacity(0.1),
},
{
'title': 'Pesanan Sukses',
'value': '$successfulOrders',
'icon': Icons.check_circle,
'color': AppColors.primary,
'bgColor': AppColors.primary.withOpacity(0.1),
},
];
return Row(
children: kpiData.map((kpi) {
return Expanded(
child: Container(
margin: const EdgeInsets.only(right: 16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[200]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: kpi['bgColor'] as Color,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
kpi['icon'] as IconData,
color: kpi['color'] as Color,
size: 20,
),
),
Icon(
Icons.trending_up,
color: Colors.grey[400],
size: 16,
),
],
),
const SizedBox(height: 16),
Text(
kpi['value'] as String,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 4),
Text(
kpi['title'] as String,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}).toList(),
);
}
Widget _buildSalesChart() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[200]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Tren Penjualan Harian',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 20),
SizedBox(
height: 200,
child: LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawHorizontalLine: true,
drawVerticalLine: false,
horizontalInterval: 200000,
getDrawingHorizontalLine: (value) {
return FlLine(
color: Colors.grey[200]!,
strokeWidth: 1,
);
},
),
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 60,
getTitlesWidget: (value, meta) {
return Text(
'${(value / 1000).toInt()}K',
style: TextStyle(
color: Colors.grey[600],
fontSize: 10,
),
);
},
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
final index = value.toInt();
if (index >= 0 && index < data.recentSales.length) {
final date =
DateTime.parse(data.recentSales[index].date);
final formatter = DateFormat('dd MMM');
return Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
formatter.format(date),
style: TextStyle(
color: Colors.grey[600],
fontSize: 10,
),
),
);
}
return const SizedBox();
},
),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false)),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false)),
),
borderData: FlBorderData(show: false),
lineBarsData: [
LineChartBarData(
spots: data.recentSales.asMap().entries.map((entry) {
return FlSpot(
entry.key.toDouble(), entry.value.sales.toDouble());
}).toList(),
isCurved: true,
color: AppColors.primary,
// strokeWidth: 3,
dotData: const FlDotData(show: true),
belowBarData: BarAreaData(
show: true,
color: AppColors.primary.withOpacity(0.1),
),
),
],
),
),
),
],
),
);
}
Widget _buildProductChart() {
final colors = [
AppColors.primary,
AppColorDashboard.secondary,
AppColorDashboard.info,
AppColorDashboard.warning,
AppColorDashboard.success,
];
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[200]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Distribusi Produk',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 20),
SizedBox(
height: 160,
child: PieChart(
PieChartData(
sectionsSpace: 2,
centerSpaceRadius: 40,
sections: data.topProducts.asMap().entries.map((entry) {
return PieChartSectionData(
color: colors[entry.key % colors.length],
value: entry.value.quantitySold.toDouble(),
title: '${entry.value.quantitySold}',
radius: 60,
titleStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
);
}).toList(),
),
),
),
const SizedBox(height: 16),
Column(
children: data.topProducts.take(3).map((product) {
final index = data.topProducts.indexOf(product);
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: colors[index % colors.length],
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
product.productName,
style: const TextStyle(fontSize: 12),
),
),
Text(
'${product.quantitySold}',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
);
}).toList(),
),
],
),
);
}
Widget _buildTopProductsList() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[200]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Produk Terlaris',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 16),
Column(
children: data.topProducts.map((product) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF8FAFC),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[200]!),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.local_cafe,
color: AppColors.primary,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.productName,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
const SizedBox(height: 2),
Text(
product.categoryName,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${product.quantitySold} unit',
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
const SizedBox(height: 2),
Text(
_formatCurrency(product.revenue),
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
],
),
);
}).toList(),
),
],
),
);
}
Widget _buildOrderSummary() {
final successfulOrders = data.overview.totalOrders -
data.overview.voidedOrders -
data.overview.refundedOrders;
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[200]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Ringkasan Pesanan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF1F2937),
),
),
const SizedBox(height: 20),
_buildSummaryItem('Total Pesanan', '${data.overview.totalOrders}',
Icons.shopping_cart, AppColorDashboard.info),
_buildSummaryItem('Pesanan Sukses', '$successfulOrders',
Icons.check_circle, AppColorDashboard.success),
_buildSummaryItem(
'Pesanan Dibatalkan',
'${data.overview.voidedOrders}',
Icons.cancel,
AppColorDashboard.danger),
_buildSummaryItem('Pesanan Refund', '${data.overview.refundedOrders}',
Icons.refresh, AppColorDashboard.warning),
const SizedBox(height: 20),
// Payment Methods
if (data.paymentMethods.isNotEmpty)
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
const Text(
'Metode Pembayaran',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF374151),
),
),
const SizedBox(height: 8),
...data.paymentMethods
.map((method) => Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
method.paymentMethodType == 'cash'
? Icons.payments
: Icons.credit_card,
color: AppColors.primary,
size: 16,
),
const SizedBox(width: 8),
Text(
method.paymentMethodName,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
],
),
))
.toList(),
],
),
),
],
),
);
}
Widget _buildSummaryItem(
String title, String value, IconData icon, Color color) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 16),
),
const SizedBox(width: 12),
Expanded(
child: Text(
title,
style: const TextStyle(fontSize: 12),
),
),
Text(
value,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
],
),
);
}
String _formatCurrency(int amount) {
final formatter = NumberFormat.currency(
locale: 'id_ID',
symbol: 'Rp ',
decimalDigits: 0,
);
return formatter.format(amount);
}
}

View File

@ -48,7 +48,7 @@ class ItemSalesReportWidget extends StatelessWidget {
// Daily Performance Section
Text(
'Daily Performance',
'Kinerja Harian',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
@ -124,7 +124,7 @@ class ItemSalesReportWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Sales Analytics',
title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
@ -182,9 +182,9 @@ class ItemSalesReportWidget extends StatelessWidget {
children: [
Expanded(
child: _buildMetricCard(
title: 'Total Sales',
title: 'Jumlah Penjualan',
value: summary.totalSales.currencyFormatRpV2,
subtitle: 'Net Sales',
subtitle: 'Penjualan Bersih',
color: const Color(0xFF3B82F6),
backgroundColor: const Color(0xFFEFF6FF),
),
@ -192,9 +192,9 @@ class ItemSalesReportWidget extends StatelessWidget {
const SizedBox(width: 16),
Expanded(
child: _buildMetricCard(
title: 'Total Orders',
title: 'Jumlah Pesanan',
value: '${summary.totalOrders}',
subtitle: '${summary.totalItems} Items',
subtitle: '${summary.totalItems} Item',
color: const Color(0xFF8B5CF6),
backgroundColor: const Color(0xFFF3E8FF),
),
@ -203,9 +203,9 @@ class ItemSalesReportWidget extends StatelessWidget {
),
const SizedBox(height: 16),
_buildFullWidthMetricCard(
title: 'Average Order Value',
title: 'Nilai Pesanan Rata-rata',
value: summary.averageOrderValue.round().currencyFormatRpV2,
subtitle: 'Per transaction',
subtitle: 'Per transaksi',
color: const Color(0xFFEF4444),
backgroundColor: const Color(0xFFFEF2F2),
),
@ -245,7 +245,7 @@ class ItemSalesReportWidget extends StatelessWidget {
const SizedBox(width: 12),
Expanded(
child: Text(
'Peak performance on $formattedDate with ${_formatCurrency(highestDay.sales)} revenue (${highestDay.orders} orders)',
'Kinerja puncak pada $formattedDate dengan pendapatan ${_formatCurrency(highestDay.sales)} (pesanan ${highestDay.orders})',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
@ -445,7 +445,7 @@ class ItemSalesReportWidget extends StatelessWidget {
),
),
Text(
'Revenue',
'Pendapatan',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w400,
@ -461,7 +461,7 @@ class ItemSalesReportWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'$orders orders',
'$orders pesanan',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
@ -469,7 +469,7 @@ class ItemSalesReportWidget extends StatelessWidget {
),
),
Text(
'$items items',
'$items item',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w400,

View 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)}';
}
}
}

View File

@ -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---
),
],
),
),
),
)
],
);
}
}

View File

@ -374,6 +374,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.0.6"
equatable:
dependency: transitive
description:
name: equatable
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
url: "https://pub.dev"
source: hosted
version: "2.0.7"
esc_pos_utils_plus:
dependency: "direct main"
description:
@ -446,6 +454,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
fl_chart:
dependency: "direct main"
description:
name: fl_chart
sha256: "577aeac8ca414c25333334d7c4bb246775234c0e44b38b10a82b559dd4d764e7"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
flutter:
dependency: "direct main"
description: flutter
@ -1548,4 +1564,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.7.0-0 <4.0.0"
flutter: ">=3.24.0"
flutter: ">=3.27.4"

View File

@ -63,6 +63,7 @@ dependencies:
awesome_dio_interceptor: ^1.3.0
another_flushbar: ^1.12.30
dropdown_search: ^5.0.6
fl_chart: ^1.0.0
# imin_printer: ^0.6.10
dev_dependencies: