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/constants/variables.dart';
import 'package:enaklo_pos/core/network/dio_client.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/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/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:enaklo_pos/data/models/response/sales_analytic_response_model.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@ -79,4 +81,72 @@ class AnalyticRemoteDatasource {
return left('Unexpected error occurred'); 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/order_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/product_local_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/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/payment_methods_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/settings_local_datasource.dart'; import 'package:enaklo_pos/data/datasources/settings_local_datasource.dart';
import 'package:enaklo_pos/presentation/auth/bloc/logout/logout_bloc.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()), create: (context) => GetCategoriesBloc(CategoryRemoteDatasource()),
), ),
BlocProvider( BlocProvider(
create: (context) => SummaryBloc(OrderRemoteDatasource()), create: (context) => SummaryBloc(AnalyticRemoteDatasource()),
), ),
BlocProvider( BlocProvider(
create: (context) => ProductSalesBloc(OrderItemRemoteDatasource()), create: (context) => ProductSalesBloc(AnalyticRemoteDatasource()),
), ),
BlocProvider( BlocProvider(
create: (context) => ItemSalesReportBloc(AnalyticRemoteDatasource()), 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: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'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'product_sales_event.dart'; part 'product_sales_event.dart';
@ -8,14 +8,16 @@ part 'product_sales_state.dart';
part 'product_sales_bloc.freezed.dart'; part 'product_sales_bloc.freezed.dart';
class ProductSalesBloc extends Bloc<ProductSalesEvent, ProductSalesState> { class ProductSalesBloc extends Bloc<ProductSalesEvent, ProductSalesState> {
final OrderItemRemoteDatasource datasource; final AnalyticRemoteDatasource datasource;
ProductSalesBloc( ProductSalesBloc(
this.datasource, this.datasource,
) : super(const _Initial()) { ) : super(const _Initial()) {
on<_GetProductSales>((event, emit) async { on<_GetProductSales>((event, emit) async {
emit(const _Loading()); emit(const _Loading());
final result = await datasource.getProductSalesByRangeDate( final result = await datasource.getProduct(
event.startDate, event.endDate); dateFrom: event.startDate,
dateTo: event.endDate,
);
result.fold((l) => emit(_Error(l)), (r) => emit(_Success(r.data!))); result.fold((l) => emit(_Error(l)), (r) => emit(_Success(r.data!)));
}); });
} }

View File

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

View File

@ -4,7 +4,7 @@ part of 'product_sales_bloc.dart';
class ProductSalesEvent with _$ProductSalesEvent { class ProductSalesEvent with _$ProductSalesEvent {
const factory ProductSalesEvent.started() = _Started; const factory ProductSalesEvent.started() = _Started;
const factory ProductSalesEvent.getProductSales( const factory ProductSalesEvent.getProductSales(
String startDate, DateTime startDate,
String endDate, DateTime endDate,
) = _GetProductSales; ) = _GetProductSales;
} }

View File

@ -6,7 +6,7 @@ class ProductSalesState with _$ProductSalesState {
const factory ProductSalesState.loading() = _Loading; const factory ProductSalesState.loading() = _Loading;
const factory ProductSalesState.success(List<ProductSales> productSales) = const factory ProductSalesState.success(ProductAnalyticData product) =
_Success; _Success;
const factory ProductSalesState.error(String message) = _Error; 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: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'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'summary_event.dart'; part 'summary_event.dart';
@ -8,15 +8,17 @@ part 'summary_state.dart';
part 'summary_bloc.freezed.dart'; part 'summary_bloc.freezed.dart';
class SummaryBloc extends Bloc<SummaryEvent, SummaryState> { class SummaryBloc extends Bloc<SummaryEvent, SummaryState> {
final OrderRemoteDatasource datasource; final AnalyticRemoteDatasource datasource;
SummaryBloc( SummaryBloc(
this.datasource, this.datasource,
) : super(const _Initial()) { ) : super(const _Initial()) {
on<_GetSummary>((event, emit) async { on<_GetSummary>((event, emit) async {
emit(const _Loading()); emit(const _Loading());
final result = await datasource.getSummaryByRangeDate( final result = await datasource.getDashboard(
event.startDate, event.endDate); dateFrom: event.startDate,
result.fold((l) => emit(_Error(l)), (r) => emit(_Success(r.data!))); dateTo: event.endDate,
);
result.fold((l) => emit(_Error(l)), (r) => emit(_Success(r.data)));
}); });
} }
} }

View File

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

View File

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

View File

@ -4,6 +4,6 @@ part of 'summary_bloc.dart';
class SummaryState with _$SummaryState { class SummaryState with _$SummaryState {
const factory SummaryState.initial() = _Initial; const factory SummaryState.initial() = _Initial;
const factory SummaryState.loading() = _Loading; 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; const factory SummaryState.error(String message) = _Error;
} }

View File

@ -1,6 +1,7 @@
import 'dart:developer'; import 'dart:developer';
import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; 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:enaklo_pos/presentation/sales/pages/sales_page.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:enaklo_pos/core/components/custom_date_picker.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/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/item_sales_report_widget.dart';
import 'package:enaklo_pos/presentation/report/widgets/payment_method_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_menu.dart';
import 'package:enaklo_pos/presentation/report/widgets/report_title.dart'; import 'package:enaklo_pos/presentation/report/widgets/report_title.dart';
import 'package:flutter/material.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 'package:enaklo_pos/presentation/report/widgets/transaction_report_widget.dart';
import '../../../core/components/spaces.dart'; import '../../../core/components/spaces.dart';
@ -32,7 +32,7 @@ class ReportPage extends StatefulWidget {
class _ReportPageState extends State<ReportPage> { class _ReportPageState extends State<ReportPage> {
int selectedMenu = 1; int selectedMenu = 1;
String title = 'Transaction Report'; String title = 'Laporan Penjualan Item';
DateTime fromDate = DateTime.now().subtract(const Duration(days: 30)); DateTime fromDate = DateTime.now().subtract(const Duration(days: 30));
DateTime toDate = DateTime.now(); DateTime toDate = DateTime.now();
@ -139,19 +139,19 @@ class _ReportPageState extends State<ReportPage> {
isActive: selectedMenu == 1, isActive: selectedMenu == 1,
), ),
ReportMenu( ReportMenu(
label: 'Chart Penjualan Produk', label: 'Laporan Penjualan Produk',
subtitle: subtitle:
'Grafik visual penjualan produk untuk analisa performa penjualan.', 'Laporan penjualan berdasarkan masing-masing produk.',
icon: Icons.bar_chart_outlined, icon: Icons.bar_chart_outlined,
onPressed: () { onPressed: () {
selectedMenu = 2; selectedMenu = 2;
title = 'Chart Penjualan Produk'; title = 'Laporan Penjualan Produk';
setState(() {}); setState(() {});
context.read<ProductSalesBloc>().add( context.read<ProductSalesBloc>().add(
ProductSalesEvent.getProductSales( ProductSalesEvent.getProductSales(
DateFormatter.formatDateTime( fromDate,
fromDate), toDate,
DateFormatter.formatDateTime(toDate)), ),
); );
}, },
isActive: selectedMenu == 2, isActive: selectedMenu == 2,
@ -166,10 +166,7 @@ class _ReportPageState extends State<ReportPage> {
title = 'Ringkasan Laporan Penjualan'; title = 'Ringkasan Laporan Penjualan';
setState(() {}); setState(() {});
context.read<SummaryBloc>().add( context.read<SummaryBloc>().add(
SummaryEvent.getSummary( SummaryEvent.getSummary(fromDate, toDate),
DateFormatter.formatDateTime(
fromDate),
DateFormatter.formatDateTime(toDate)),
); );
log("Date ${DateFormatter.formatDateTime(fromDate)}"); log("Date ${DateFormatter.formatDateTime(fromDate)}");
@ -262,12 +259,12 @@ class _ReportPageState extends State<ReportPage> {
error: (message) { error: (message) {
return Text(message); return Text(message);
}, },
success: (productSales) { success: (products) {
return ProductSalesChartWidgets( return ProductAnalyticsWidget(
title: title, title: title,
searchDateFormatted: searchDateFormatted:
searchDateFormatted, searchDateFormatted,
productSales: productSales, productData: products,
); );
}, },
); );
@ -284,9 +281,9 @@ class _ReportPageState extends State<ReportPage> {
error: (message) { error: (message) {
return Text(message); return Text(message);
}, },
success: (summary) { success: (data) {
return SummaryReportWidget( return DashboardAnalyticWidget(
summary: summary, data: data,
title: title, title: title,
searchDateFormatted: searchDateFormatted:
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 // Daily Performance Section
Text( Text(
'Daily Performance', 'Kinerja Harian',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -124,7 +124,7 @@ class ItemSalesReportWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Sales Analytics', title,
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
@ -182,9 +182,9 @@ class ItemSalesReportWidget extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: _buildMetricCard( child: _buildMetricCard(
title: 'Total Sales', title: 'Jumlah Penjualan',
value: summary.totalSales.currencyFormatRpV2, value: summary.totalSales.currencyFormatRpV2,
subtitle: 'Net Sales', subtitle: 'Penjualan Bersih',
color: const Color(0xFF3B82F6), color: const Color(0xFF3B82F6),
backgroundColor: const Color(0xFFEFF6FF), backgroundColor: const Color(0xFFEFF6FF),
), ),
@ -192,9 +192,9 @@ class ItemSalesReportWidget extends StatelessWidget {
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: _buildMetricCard( child: _buildMetricCard(
title: 'Total Orders', title: 'Jumlah Pesanan',
value: '${summary.totalOrders}', value: '${summary.totalOrders}',
subtitle: '${summary.totalItems} Items', subtitle: '${summary.totalItems} Item',
color: const Color(0xFF8B5CF6), color: const Color(0xFF8B5CF6),
backgroundColor: const Color(0xFFF3E8FF), backgroundColor: const Color(0xFFF3E8FF),
), ),
@ -203,9 +203,9 @@ class ItemSalesReportWidget extends StatelessWidget {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildFullWidthMetricCard( _buildFullWidthMetricCard(
title: 'Average Order Value', title: 'Nilai Pesanan Rata-rata',
value: summary.averageOrderValue.round().currencyFormatRpV2, value: summary.averageOrderValue.round().currencyFormatRpV2,
subtitle: 'Per transaction', subtitle: 'Per transaksi',
color: const Color(0xFFEF4444), color: const Color(0xFFEF4444),
backgroundColor: const Color(0xFFFEF2F2), backgroundColor: const Color(0xFFFEF2F2),
), ),
@ -245,7 +245,7 @@ class ItemSalesReportWidget extends StatelessWidget {
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Text( 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( style: TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@ -445,7 +445,7 @@ class ItemSalesReportWidget extends StatelessWidget {
), ),
), ),
Text( Text(
'Revenue', 'Pendapatan',
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
@ -461,7 +461,7 @@ class ItemSalesReportWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text( Text(
'$orders orders', '$orders pesanan',
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@ -469,7 +469,7 @@ class ItemSalesReportWidget extends StatelessWidget {
), ),
), ),
Text( Text(
'$items items', '$items item',
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
fontWeight: FontWeight.w400, 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" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.6" 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: esc_pos_utils_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@ -446,6 +454,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" 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: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -1548,4 +1564,4 @@ packages:
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.7.0-0 <4.0.0" 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 awesome_dio_interceptor: ^1.3.0
another_flushbar: ^1.12.30 another_flushbar: ^1.12.30
dropdown_search: ^5.0.6 dropdown_search: ^5.0.6
fl_chart: ^1.0.0
# imin_printer: ^0.6.10 # imin_printer: ^0.6.10
dev_dependencies: dev_dependencies: