dev #1

Merged
aefril merged 128 commits from dev into main 2025-08-13 17:19:48 +00:00
11 changed files with 998 additions and 67 deletions
Showing only changes of commit bdb2c0ba52 - Show all commits

View File

@ -5,6 +5,7 @@ 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/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';
@ -114,4 +115,38 @@ class AnalyticRemoteDatasource {
return left('Unexpected error occurred'); 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

@ -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,7 +189,7 @@ 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(AnalyticRemoteDatasource()), create: (context) => ProductSalesBloc(AnalyticRemoteDatasource()),

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';
@ -18,7 +19,6 @@ import 'package:enaklo_pos/presentation/report/widgets/product_analytic_widget.d
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';
@ -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)}");
@ -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

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