Compare commits

..

No commits in common. "bdb2c0ba521ed123f322058e73e891851a89142a" and "2a457ea5f67bfe5fdc97d0136a947eaec4b95f0b" have entirely different histories.

19 changed files with 284 additions and 1760 deletions

View File

@ -5,9 +5,7 @@ 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';
@ -81,72 +79,4 @@ 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

@ -1,242 +0,0 @@
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

@ -1,143 +0,0 @@
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,6 +27,7 @@ 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';
@ -189,10 +190,10 @@ class _MyAppState extends State<MyApp> {
create: (context) => GetCategoriesBloc(CategoryRemoteDatasource()),
),
BlocProvider(
create: (context) => SummaryBloc(AnalyticRemoteDatasource()),
create: (context) => SummaryBloc(OrderRemoteDatasource()),
),
BlocProvider(
create: (context) => ProductSalesBloc(AnalyticRemoteDatasource()),
create: (context) => ProductSalesBloc(OrderItemRemoteDatasource()),
),
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,16 +8,14 @@ part 'product_sales_state.dart';
part 'product_sales_bloc.freezed.dart';
class ProductSalesBloc extends Bloc<ProductSalesEvent, ProductSalesState> {
final AnalyticRemoteDatasource datasource;
final OrderItemRemoteDatasource datasource;
ProductSalesBloc(
this.datasource,
) : super(const _Initial()) {
on<_GetProductSales>((event, emit) async {
emit(const _Loading());
final result = await datasource.getProduct(
dateFrom: event.startDate,
dateTo: event.endDate,
);
final result = await datasource.getProductSalesByRangeDate(
event.startDate, event.endDate);
result.fold((l) => emit(_Error(l)), (r) => emit(_Success(r.data!)));
});
}

View File

@ -19,20 +19,19 @@ mixin _$ProductSalesEvent {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() started,
required TResult Function(DateTime startDate, DateTime endDate)
getProductSales,
required TResult Function(String startDate, String endDate) getProductSales,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started,
TResult? Function(DateTime startDate, DateTime endDate)? getProductSales,
TResult? Function(String startDate, String endDate)? getProductSales,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? started,
TResult Function(DateTime startDate, DateTime endDate)? getProductSales,
TResult Function(String startDate, String endDate)? getProductSales,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
@ -120,8 +119,7 @@ class _$StartedImpl implements _Started {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() started,
required TResult Function(DateTime startDate, DateTime endDate)
getProductSales,
required TResult Function(String startDate, String endDate) getProductSales,
}) {
return started();
}
@ -130,7 +128,7 @@ class _$StartedImpl implements _Started {
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started,
TResult? Function(DateTime startDate, DateTime endDate)? getProductSales,
TResult? Function(String startDate, String endDate)? getProductSales,
}) {
return started?.call();
}
@ -139,7 +137,7 @@ class _$StartedImpl implements _Started {
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? started,
TResult Function(DateTime startDate, DateTime endDate)? getProductSales,
TResult Function(String startDate, String endDate)? getProductSales,
required TResult orElse(),
}) {
if (started != null) {
@ -190,7 +188,7 @@ abstract class _$$GetProductSalesImplCopyWith<$Res> {
$Res Function(_$GetProductSalesImpl) then) =
__$$GetProductSalesImplCopyWithImpl<$Res>;
@useResult
$Res call({DateTime startDate, DateTime endDate});
$Res call({String startDate, String endDate});
}
/// @nodoc
@ -213,11 +211,11 @@ class __$$GetProductSalesImplCopyWithImpl<$Res>
null == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime,
as String,
null == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime,
as String,
));
}
}
@ -228,9 +226,9 @@ class _$GetProductSalesImpl implements _GetProductSales {
const _$GetProductSalesImpl(this.startDate, this.endDate);
@override
final DateTime startDate;
final String startDate;
@override
final DateTime endDate;
final String endDate;
@override
String toString() {
@ -263,8 +261,7 @@ class _$GetProductSalesImpl implements _GetProductSales {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() started,
required TResult Function(DateTime startDate, DateTime endDate)
getProductSales,
required TResult Function(String startDate, String endDate) getProductSales,
}) {
return getProductSales(startDate, endDate);
}
@ -273,7 +270,7 @@ class _$GetProductSalesImpl implements _GetProductSales {
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started,
TResult? Function(DateTime startDate, DateTime endDate)? getProductSales,
TResult? Function(String startDate, String endDate)? getProductSales,
}) {
return getProductSales?.call(startDate, endDate);
}
@ -282,7 +279,7 @@ class _$GetProductSalesImpl implements _GetProductSales {
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? started,
TResult Function(DateTime startDate, DateTime endDate)? getProductSales,
TResult Function(String startDate, String endDate)? getProductSales,
required TResult orElse(),
}) {
if (getProductSales != null) {
@ -324,11 +321,11 @@ class _$GetProductSalesImpl implements _GetProductSales {
}
abstract class _GetProductSales implements ProductSalesEvent {
const factory _GetProductSales(
final DateTime startDate, final DateTime endDate) = _$GetProductSalesImpl;
const factory _GetProductSales(final String startDate, final String endDate) =
_$GetProductSalesImpl;
DateTime get startDate;
DateTime get endDate;
String get startDate;
String get endDate;
/// Create a copy of ProductSalesEvent
/// with the given fields replaced by the non-null parameter values.
@ -343,7 +340,7 @@ mixin _$ProductSalesState {
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(ProductAnalyticData product) success,
required TResult Function(List<ProductSales> productSales) success,
required TResult Function(String message) error,
}) =>
throw _privateConstructorUsedError;
@ -351,7 +348,7 @@ mixin _$ProductSalesState {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(ProductAnalyticData product)? success,
TResult? Function(List<ProductSales> productSales)? success,
TResult? Function(String message)? error,
}) =>
throw _privateConstructorUsedError;
@ -359,7 +356,7 @@ mixin _$ProductSalesState {
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(ProductAnalyticData product)? success,
TResult Function(List<ProductSales> productSales)? success,
TResult Function(String message)? error,
required TResult orElse(),
}) =>
@ -455,7 +452,7 @@ class _$InitialImpl implements _Initial {
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(ProductAnalyticData product) success,
required TResult Function(List<ProductSales> productSales) success,
required TResult Function(String message) error,
}) {
return initial();
@ -466,7 +463,7 @@ class _$InitialImpl implements _Initial {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(ProductAnalyticData product)? success,
TResult? Function(List<ProductSales> productSales)? success,
TResult? Function(String message)? error,
}) {
return initial?.call();
@ -477,7 +474,7 @@ class _$InitialImpl implements _Initial {
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(ProductAnalyticData product)? success,
TResult Function(List<ProductSales> productSales)? success,
TResult Function(String message)? error,
required TResult orElse(),
}) {
@ -572,7 +569,7 @@ class _$LoadingImpl implements _Loading {
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(ProductAnalyticData product) success,
required TResult Function(List<ProductSales> productSales) success,
required TResult Function(String message) error,
}) {
return loading();
@ -583,7 +580,7 @@ class _$LoadingImpl implements _Loading {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(ProductAnalyticData product)? success,
TResult? Function(List<ProductSales> productSales)? success,
TResult? Function(String message)? error,
}) {
return loading?.call();
@ -594,7 +591,7 @@ class _$LoadingImpl implements _Loading {
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(ProductAnalyticData product)? success,
TResult Function(List<ProductSales> productSales)? success,
TResult Function(String message)? error,
required TResult orElse(),
}) {
@ -652,7 +649,7 @@ abstract class _$$SuccessImplCopyWith<$Res> {
_$SuccessImpl value, $Res Function(_$SuccessImpl) then) =
__$$SuccessImplCopyWithImpl<$Res>;
@useResult
$Res call({ProductAnalyticData product});
$Res call({List<ProductSales> productSales});
}
/// @nodoc
@ -668,13 +665,13 @@ class __$$SuccessImplCopyWithImpl<$Res>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? product = null,
Object? productSales = null,
}) {
return _then(_$SuccessImpl(
null == product
? _value.product
: product // ignore: cast_nullable_to_non_nullable
as ProductAnalyticData,
null == productSales
? _value._productSales
: productSales // ignore: cast_nullable_to_non_nullable
as List<ProductSales>,
));
}
}
@ -682,14 +679,20 @@ class __$$SuccessImplCopyWithImpl<$Res>
/// @nodoc
class _$SuccessImpl implements _Success {
const _$SuccessImpl(this.product);
const _$SuccessImpl(final List<ProductSales> productSales)
: _productSales = productSales;
final List<ProductSales> _productSales;
@override
final ProductAnalyticData product;
List<ProductSales> get productSales {
if (_productSales is EqualUnmodifiableListView) return _productSales;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_productSales);
}
@override
String toString() {
return 'ProductSalesState.success(product: $product)';
return 'ProductSalesState.success(productSales: $productSales)';
}
@override
@ -697,11 +700,13 @@ class _$SuccessImpl implements _Success {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SuccessImpl &&
(identical(other.product, product) || other.product == product));
const DeepCollectionEquality()
.equals(other._productSales, _productSales));
}
@override
int get hashCode => Object.hash(runtimeType, product);
int get hashCode => Object.hash(
runtimeType, const DeepCollectionEquality().hash(_productSales));
/// Create a copy of ProductSalesState
/// with the given fields replaced by the non-null parameter values.
@ -716,10 +721,10 @@ class _$SuccessImpl implements _Success {
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(ProductAnalyticData product) success,
required TResult Function(List<ProductSales> productSales) success,
required TResult Function(String message) error,
}) {
return success(product);
return success(productSales);
}
@override
@ -727,10 +732,10 @@ class _$SuccessImpl implements _Success {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(ProductAnalyticData product)? success,
TResult? Function(List<ProductSales> productSales)? success,
TResult? Function(String message)? error,
}) {
return success?.call(product);
return success?.call(productSales);
}
@override
@ -738,12 +743,12 @@ class _$SuccessImpl implements _Success {
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(ProductAnalyticData product)? success,
TResult Function(List<ProductSales> productSales)? success,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (success != null) {
return success(product);
return success(productSales);
}
return orElse();
}
@ -787,9 +792,9 @@ class _$SuccessImpl implements _Success {
}
abstract class _Success implements ProductSalesState {
const factory _Success(final ProductAnalyticData product) = _$SuccessImpl;
const factory _Success(final List<ProductSales> productSales) = _$SuccessImpl;
ProductAnalyticData get product;
List<ProductSales> get productSales;
/// Create a copy of ProductSalesState
/// with the given fields replaced by the non-null parameter values.
@ -868,7 +873,7 @@ class _$ErrorImpl implements _Error {
TResult when<TResult extends Object?>({
required TResult Function() initial,
required TResult Function() loading,
required TResult Function(ProductAnalyticData product) success,
required TResult Function(List<ProductSales> productSales) success,
required TResult Function(String message) error,
}) {
return error(message);
@ -879,7 +884,7 @@ class _$ErrorImpl implements _Error {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(ProductAnalyticData product)? success,
TResult? Function(List<ProductSales> productSales)? success,
TResult? Function(String message)? error,
}) {
return error?.call(message);
@ -890,7 +895,7 @@ class _$ErrorImpl implements _Error {
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(ProductAnalyticData product)? success,
TResult Function(List<ProductSales> productSales)? 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(
DateTime startDate,
DateTime endDate,
String startDate,
String endDate,
) = _GetProductSales;
}

View File

@ -6,7 +6,7 @@ class ProductSalesState with _$ProductSalesState {
const factory ProductSalesState.loading() = _Loading;
const factory ProductSalesState.success(ProductAnalyticData product) =
const factory ProductSalesState.success(List<ProductSales> productSales) =
_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,17 +8,15 @@ part 'summary_state.dart';
part 'summary_bloc.freezed.dart';
class SummaryBloc extends Bloc<SummaryEvent, SummaryState> {
final AnalyticRemoteDatasource datasource;
final OrderRemoteDatasource datasource;
SummaryBloc(
this.datasource,
) : super(const _Initial()) {
on<_GetSummary>((event, emit) async {
emit(const _Loading());
final result = await datasource.getDashboard(
dateFrom: event.startDate,
dateTo: event.endDate,
);
result.fold((l) => emit(_Error(l)), (r) => emit(_Success(r.data)));
final result = await datasource.getSummaryByRangeDate(
event.startDate, 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(DateTime startDate, DateTime endDate) getSummary,
required TResult Function(String startDate, String endDate) getSummary,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started,
TResult? Function(DateTime startDate, DateTime endDate)? getSummary,
TResult? Function(String startDate, String endDate)? getSummary,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? started,
TResult Function(DateTime startDate, DateTime endDate)? getSummary,
TResult Function(String startDate, String 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(DateTime startDate, DateTime endDate) getSummary,
required TResult Function(String startDate, String endDate) getSummary,
}) {
return started();
}
@ -128,7 +128,7 @@ class _$StartedImpl implements _Started {
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started,
TResult? Function(DateTime startDate, DateTime endDate)? getSummary,
TResult? Function(String startDate, String endDate)? getSummary,
}) {
return started?.call();
}
@ -137,7 +137,7 @@ class _$StartedImpl implements _Started {
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? started,
TResult Function(DateTime startDate, DateTime endDate)? getSummary,
TResult Function(String startDate, String 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({DateTime startDate, DateTime endDate});
$Res call({String startDate, String endDate});
}
/// @nodoc
@ -211,11 +211,11 @@ class __$$GetSummaryImplCopyWithImpl<$Res>
null == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime,
as String,
null == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime,
as String,
));
}
}
@ -226,9 +226,9 @@ class _$GetSummaryImpl implements _GetSummary {
const _$GetSummaryImpl(this.startDate, this.endDate);
@override
final DateTime startDate;
final String startDate;
@override
final DateTime endDate;
final String 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(DateTime startDate, DateTime endDate) getSummary,
required TResult Function(String startDate, String 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(DateTime startDate, DateTime endDate)? getSummary,
TResult? Function(String startDate, String 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(DateTime startDate, DateTime endDate)? getSummary,
TResult Function(String startDate, String 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 DateTime startDate, final DateTime endDate) =
const factory _GetSummary(final String startDate, final String endDate) =
_$GetSummaryImpl;
DateTime get startDate;
DateTime get endDate;
String get startDate;
String 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(DashboardAnalyticData data) success,
required TResult Function(SummaryModel summary) 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(DashboardAnalyticData data)? success,
TResult? Function(SummaryModel summary)? 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(DashboardAnalyticData data)? success,
TResult Function(SummaryModel summary)? 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(DashboardAnalyticData data) success,
required TResult Function(SummaryModel summary) 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(DashboardAnalyticData data)? success,
TResult? Function(SummaryModel summary)? 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(DashboardAnalyticData data)? success,
TResult Function(SummaryModel summary)? 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(DashboardAnalyticData data) success,
required TResult Function(SummaryModel summary) 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(DashboardAnalyticData data)? success,
TResult? Function(SummaryModel summary)? 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(DashboardAnalyticData data)? success,
TResult Function(SummaryModel summary)? 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({DashboardAnalyticData data});
$Res call({SummaryModel summary});
}
/// @nodoc
@ -664,13 +664,13 @@ class __$$SuccessImplCopyWithImpl<$Res>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? data = null,
Object? summary = null,
}) {
return _then(_$SuccessImpl(
null == data
? _value.data
: data // ignore: cast_nullable_to_non_nullable
as DashboardAnalyticData,
null == summary
? _value.summary
: summary // ignore: cast_nullable_to_non_nullable
as SummaryModel,
));
}
}
@ -678,14 +678,14 @@ class __$$SuccessImplCopyWithImpl<$Res>
/// @nodoc
class _$SuccessImpl implements _Success {
const _$SuccessImpl(this.data);
const _$SuccessImpl(this.summary);
@override
final DashboardAnalyticData data;
final SummaryModel summary;
@override
String toString() {
return 'SummaryState.success(data: $data)';
return 'SummaryState.success(summary: $summary)';
}
@override
@ -693,11 +693,11 @@ class _$SuccessImpl implements _Success {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SuccessImpl &&
(identical(other.data, data) || other.data == data));
(identical(other.summary, summary) || other.summary == summary));
}
@override
int get hashCode => Object.hash(runtimeType, data);
int get hashCode => Object.hash(runtimeType, summary);
/// 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(DashboardAnalyticData data) success,
required TResult Function(SummaryModel summary) success,
required TResult Function(String message) error,
}) {
return success(data);
return success(summary);
}
@override
@ -723,10 +723,10 @@ class _$SuccessImpl implements _Success {
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? initial,
TResult? Function()? loading,
TResult? Function(DashboardAnalyticData data)? success,
TResult? Function(SummaryModel summary)? success,
TResult? Function(String message)? error,
}) {
return success?.call(data);
return success?.call(summary);
}
@override
@ -734,12 +734,12 @@ class _$SuccessImpl implements _Success {
TResult maybeWhen<TResult extends Object?>({
TResult Function()? initial,
TResult Function()? loading,
TResult Function(DashboardAnalyticData data)? success,
TResult Function(SummaryModel summary)? success,
TResult Function(String message)? error,
required TResult orElse(),
}) {
if (success != null) {
return success(data);
return success(summary);
}
return orElse();
}
@ -783,9 +783,9 @@ class _$SuccessImpl implements _Success {
}
abstract class _Success implements SummaryState {
const factory _Success(final DashboardAnalyticData data) = _$SuccessImpl;
const factory _Success(final SummaryModel summary) = _$SuccessImpl;
DashboardAnalyticData get data;
SummaryModel get summary;
/// 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(DashboardAnalyticData data) success,
required TResult Function(SummaryModel summary) 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(DashboardAnalyticData data)? success,
TResult? Function(SummaryModel summary)? 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(DashboardAnalyticData data)? success,
TResult Function(SummaryModel summary)? 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(DateTime startDate, DateTime endDate) =
const factory SummaryEvent.getSummary(String startDate, String 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(DashboardAnalyticData data) = _Success;
const factory SummaryState.success(SummaryModel summary) = _Success;
const factory SummaryState.error(String message) = _Error;
}

View File

@ -1,7 +1,6 @@
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';
@ -15,10 +14,11 @@ 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_analytic_widget.dart';
import 'package:enaklo_pos/presentation/report/widgets/product_sales_chart_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 = 'Laporan Penjualan Item';
String title = 'Transaction Report';
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: 'Laporan Penjualan Produk',
label: 'Chart Penjualan Produk',
subtitle:
'Laporan penjualan berdasarkan masing-masing produk.',
'Grafik visual penjualan produk untuk analisa performa penjualan.',
icon: Icons.bar_chart_outlined,
onPressed: () {
selectedMenu = 2;
title = 'Laporan Penjualan Produk';
title = 'Chart Penjualan Produk';
setState(() {});
context.read<ProductSalesBloc>().add(
ProductSalesEvent.getProductSales(
fromDate,
toDate,
),
DateFormatter.formatDateTime(
fromDate),
DateFormatter.formatDateTime(toDate)),
);
},
isActive: selectedMenu == 2,
@ -166,7 +166,10 @@ class _ReportPageState extends State<ReportPage> {
title = 'Ringkasan Laporan Penjualan';
setState(() {});
context.read<SummaryBloc>().add(
SummaryEvent.getSummary(fromDate, toDate),
SummaryEvent.getSummary(
DateFormatter.formatDateTime(
fromDate),
DateFormatter.formatDateTime(toDate)),
);
log("Date ${DateFormatter.formatDateTime(fromDate)}");
@ -259,12 +262,12 @@ class _ReportPageState extends State<ReportPage> {
error: (message) {
return Text(message);
},
success: (products) {
return ProductAnalyticsWidget(
success: (productSales) {
return ProductSalesChartWidgets(
title: title,
searchDateFormatted:
searchDateFormatted,
productData: products,
productSales: productSales,
);
},
);
@ -281,9 +284,9 @@ class _ReportPageState extends State<ReportPage> {
error: (message) {
return Text(message);
},
success: (data) {
return DashboardAnalyticWidget(
data: data,
success: (summary) {
return SummaryReportWidget(
summary: summary,
title: title,
searchDateFormatted:
searchDateFormatted,

View File

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

View File

@ -1,498 +0,0 @@
// 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

@ -0,0 +1,128 @@
// 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,14 +374,6 @@ 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:
@ -454,14 +446,6 @@ 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
@ -1564,4 +1548,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.7.0-0 <4.0.0"
flutter: ">=3.27.4"
flutter: ">=3.24.0"

View File

@ -63,7 +63,6 @@ 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: