feat: item sales report

This commit is contained in:
efrilm 2025-08-06 12:06:34 +07:00
parent fd254c22fd
commit 2a457ea5f6
9 changed files with 809 additions and 203 deletions

View File

@ -6,6 +6,7 @@ import 'package:enaklo_pos/core/constants/variables.dart';
import 'package:enaklo_pos/core/network/dio_client.dart'; import 'package:enaklo_pos/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/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/sales_analytic_response_model.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class AnalyticRemoteDatasource { class AnalyticRemoteDatasource {
@ -34,7 +35,41 @@ class AnalyticRemoteDatasource {
if (response.statusCode == 200) { if (response.statusCode == 200) {
return right(PaymentMethodAnalyticResponseModel.fromMap(response.data)); return right(PaymentMethodAnalyticResponseModel.fromMap(response.data));
} else { } else {
return left(response.data.toString()); 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, SalesAnalyticResponseModel>> getSales({
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/sales',
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(SalesAnalyticResponseModel.fromMap(response.data));
} else {
return left('Terjadi Kesalahan, Coba lagi nanti.');
} }
} on DioException catch (e) { } on DioException catch (e) {
log('Dio error: ${e.message}'); log('Dio error: ${e.message}');

View File

@ -0,0 +1,195 @@
class SalesAnalyticResponseModel {
final bool success;
final SalesAnalyticData data;
final dynamic errors;
SalesAnalyticResponseModel({
required this.success,
required this.data,
this.errors,
});
factory SalesAnalyticResponseModel.fromJson(Map<String, dynamic> json) =>
SalesAnalyticResponseModel.fromMap(json);
Map<String, dynamic> toJson() => toMap();
factory SalesAnalyticResponseModel.fromMap(Map<String, dynamic> map) {
return SalesAnalyticResponseModel(
success: map['success'],
data: SalesAnalyticData.fromMap(map['data']),
errors: map['errors'],
);
}
Map<String, dynamic> toMap() {
return {
'success': success,
'data': data.toMap(),
'errors': errors,
};
}
}
class SalesAnalyticData {
final String organizationId;
final String outletId;
final DateTime dateFrom;
final DateTime dateTo;
final String groupBy;
final SalesSummary summary;
final List<SalesAnalyticItem> data;
SalesAnalyticData({
required this.organizationId,
required this.outletId,
required this.dateFrom,
required this.dateTo,
required this.groupBy,
required this.summary,
required this.data,
});
factory SalesAnalyticData.fromJson(Map<String, dynamic> json) =>
SalesAnalyticData.fromMap(json);
Map<String, dynamic> toJson() => toMap();
factory SalesAnalyticData.fromMap(Map<String, dynamic> map) {
return SalesAnalyticData(
organizationId: map['organization_id'],
outletId: map['outlet_id'],
dateFrom: DateTime.parse(map['date_from']),
dateTo: DateTime.parse(map['date_to']),
groupBy: map['group_by'],
summary: SalesSummary.fromMap(map['summary']),
data: List<SalesAnalyticItem>.from(
map['data']?.map((x) => SalesAnalyticItem.fromMap(x)) ?? [],
),
);
}
Map<String, dynamic> toMap() {
return {
'organization_id': organizationId,
'outlet_id': outletId,
'date_from': dateFrom.toIso8601String(),
'date_to': dateTo.toIso8601String(),
'group_by': groupBy,
'summary': summary.toMap(),
'data': data.map((x) => x.toMap()).toList(),
};
}
}
class SalesSummary {
final int totalSales;
final int totalOrders;
final int totalItems;
final double averageOrderValue;
final int totalTax;
final int totalDiscount;
final int netSales;
SalesSummary({
required this.totalSales,
required this.totalOrders,
required this.totalItems,
required this.averageOrderValue,
required this.totalTax,
required this.totalDiscount,
required this.netSales,
});
factory SalesSummary.fromJson(Map<String, dynamic> json) =>
SalesSummary.fromMap(json);
Map<String, dynamic> toJson() => toMap();
factory SalesSummary.fromMap(Map<String, dynamic> map) {
return SalesSummary(
totalSales: map['total_sales'],
totalOrders: map['total_orders'],
totalItems: map['total_items'],
averageOrderValue: (map['average_order_value'] as num).toDouble(),
totalTax: map['total_tax'],
totalDiscount: map['total_discount'],
netSales: map['net_sales'],
);
}
Map<String, dynamic> toMap() {
return {
'total_sales': totalSales,
'total_orders': totalOrders,
'total_items': totalItems,
'average_order_value': averageOrderValue,
'total_tax': totalTax,
'total_discount': totalDiscount,
'net_sales': netSales,
};
}
}
class SalesAnalyticItem {
final DateTime date;
final int sales;
final int orders;
final int items;
final int tax;
final int discount;
final int netSales;
SalesAnalyticItem({
required this.date,
required this.sales,
required this.orders,
required this.items,
required this.tax,
required this.discount,
required this.netSales,
});
factory SalesAnalyticItem.fromJson(Map<String, dynamic> json) =>
SalesAnalyticItem.fromMap(json);
Map<String, dynamic> toJson() => toMap();
factory SalesAnalyticItem.fromMap(Map<String, dynamic> map) {
return SalesAnalyticItem(
date: DateTime.parse(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() {
return {
'date': date.toIso8601String(),
'sales': sales,
'orders': orders,
'items': items,
'tax': tax,
'discount': discount,
'net_sales': netSales,
};
}
}
class SalesInsights {
final List<SalesAnalyticItem> originalData;
final List<SalesAnalyticItem> sortedDailyData;
final SalesAnalyticItem? highestRevenueDay;
final SalesSummary summary;
SalesInsights({
required this.originalData,
required this.sortedDailyData,
required this.highestRevenueDay,
required this.summary,
});
}

View File

@ -196,7 +196,7 @@ class _MyAppState extends State<MyApp> {
create: (context) => ProductSalesBloc(OrderItemRemoteDatasource()), create: (context) => ProductSalesBloc(OrderItemRemoteDatasource()),
), ),
BlocProvider( BlocProvider(
create: (context) => ItemSalesReportBloc(OrderItemRemoteDatasource()), create: (context) => ItemSalesReportBloc(AnalyticRemoteDatasource()),
), ),
BlocProvider( BlocProvider(
create: (context) => create: (context) =>

View File

@ -1,6 +1,6 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:enaklo_pos/data/models/response/item_sales_response_model.dart'; import 'package:enaklo_pos/data/datasources/analytic_remote_datasource.dart';
import 'package:enaklo_pos/data/datasources/order_item_remote_datasource.dart'; import 'package:enaklo_pos/data/models/response/sales_analytic_response_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'item_sales_report_event.dart'; part 'item_sales_report_event.dart';
@ -9,12 +9,14 @@ part 'item_sales_report_bloc.freezed.dart';
class ItemSalesReportBloc class ItemSalesReportBloc
extends Bloc<ItemSalesReportEvent, ItemSalesReportState> { extends Bloc<ItemSalesReportEvent, ItemSalesReportState> {
final OrderItemRemoteDatasource datasource; final AnalyticRemoteDatasource datasource;
ItemSalesReportBloc(this.datasource) : super(const _Initial()) { ItemSalesReportBloc(this.datasource) : super(const _Initial()) {
on<_GetItemSales>((event, emit) async { on<_GetItemSales>((event, emit) async {
emit(const _Loading()); emit(const _Loading());
final result = await datasource.getItemSalesByRangeDate( final result = await datasource.getSales(
event.startDate, event.endDate); dateFrom: event.startDate,
dateTo: event.endDate,
);
result.fold((l) => emit(_Error(l)), (r) => emit(_Loaded(r.data!))); result.fold((l) => emit(_Error(l)), (r) => emit(_Loaded(r.data!)));
}); });
} }

View File

@ -19,19 +19,20 @@ mixin _$ItemSalesReportEvent {
@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) getItemSales, required TResult Function(DateTime startDate, DateTime endDate)
getItemSales,
}) => }) =>
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)? getItemSales, TResult? Function(DateTime startDate, DateTime endDate)? getItemSales,
}) => }) =>
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)? getItemSales, TResult Function(DateTime startDate, DateTime endDate)? getItemSales,
required TResult orElse(), required TResult orElse(),
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@ -120,7 +121,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) getItemSales, required TResult Function(DateTime startDate, DateTime endDate)
getItemSales,
}) { }) {
return started(); return started();
} }
@ -129,7 +131,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)? getItemSales, TResult? Function(DateTime startDate, DateTime endDate)? getItemSales,
}) { }) {
return started?.call(); return started?.call();
} }
@ -138,7 +140,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)? getItemSales, TResult Function(DateTime startDate, DateTime endDate)? getItemSales,
required TResult orElse(), required TResult orElse(),
}) { }) {
if (started != null) { if (started != null) {
@ -189,7 +191,7 @@ abstract class _$$GetItemSalesImplCopyWith<$Res> {
_$GetItemSalesImpl value, $Res Function(_$GetItemSalesImpl) then) = _$GetItemSalesImpl value, $Res Function(_$GetItemSalesImpl) then) =
__$$GetItemSalesImplCopyWithImpl<$Res>; __$$GetItemSalesImplCopyWithImpl<$Res>;
@useResult @useResult
$Res call({String startDate, String endDate}); $Res call({DateTime startDate, DateTime endDate});
} }
/// @nodoc /// @nodoc
@ -212,11 +214,11 @@ class __$$GetItemSalesImplCopyWithImpl<$Res>
startDate: null == startDate 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,
endDate: null == endDate 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,
)); ));
} }
} }
@ -227,9 +229,9 @@ class _$GetItemSalesImpl implements _GetItemSales {
const _$GetItemSalesImpl({required this.startDate, required this.endDate}); const _$GetItemSalesImpl({required this.startDate, required 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 _$GetItemSalesImpl implements _GetItemSales {
@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) getItemSales, required TResult Function(DateTime startDate, DateTime endDate)
getItemSales,
}) { }) {
return getItemSales(startDate, endDate); return getItemSales(startDate, endDate);
} }
@ -270,7 +273,7 @@ class _$GetItemSalesImpl implements _GetItemSales {
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({ TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? started, TResult? Function()? started,
TResult? Function(String startDate, String endDate)? getItemSales, TResult? Function(DateTime startDate, DateTime endDate)? getItemSales,
}) { }) {
return getItemSales?.call(startDate, endDate); return getItemSales?.call(startDate, endDate);
} }
@ -279,7 +282,7 @@ class _$GetItemSalesImpl implements _GetItemSales {
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? started, TResult Function()? started,
TResult Function(String startDate, String endDate)? getItemSales, TResult Function(DateTime startDate, DateTime endDate)? getItemSales,
required TResult orElse(), required TResult orElse(),
}) { }) {
if (getItemSales != null) { if (getItemSales != null) {
@ -322,11 +325,11 @@ class _$GetItemSalesImpl implements _GetItemSales {
abstract class _GetItemSales implements ItemSalesReportEvent { abstract class _GetItemSales implements ItemSalesReportEvent {
const factory _GetItemSales( const factory _GetItemSales(
{required final String startDate, {required final DateTime startDate,
required final String endDate}) = _$GetItemSalesImpl; required final DateTime endDate}) = _$GetItemSalesImpl;
String get startDate; DateTime get startDate;
String get endDate; DateTime get endDate;
/// Create a copy of ItemSalesReportEvent /// Create a copy of ItemSalesReportEvent
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -341,7 +344,7 @@ mixin _$ItemSalesReportState {
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<ItemSales> itemSales) loaded, required TResult Function(SalesAnalyticData itemSales) loaded,
required TResult Function(String message) error, required TResult Function(String message) error,
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@ -349,7 +352,7 @@ mixin _$ItemSalesReportState {
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<ItemSales> itemSales)? loaded, TResult? Function(SalesAnalyticData itemSales)? loaded,
TResult? Function(String message)? error, TResult? Function(String message)? error,
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@ -357,7 +360,7 @@ mixin _$ItemSalesReportState {
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<ItemSales> itemSales)? loaded, TResult Function(SalesAnalyticData itemSales)? loaded,
TResult Function(String message)? error, TResult Function(String message)? error,
required TResult orElse(), required TResult orElse(),
}) => }) =>
@ -454,7 +457,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<ItemSales> itemSales) loaded, required TResult Function(SalesAnalyticData itemSales) loaded,
required TResult Function(String message) error, required TResult Function(String message) error,
}) { }) {
return initial(); return initial();
@ -465,7 +468,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<ItemSales> itemSales)? loaded, TResult? Function(SalesAnalyticData itemSales)? loaded,
TResult? Function(String message)? error, TResult? Function(String message)? error,
}) { }) {
return initial?.call(); return initial?.call();
@ -476,7 +479,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<ItemSales> itemSales)? loaded, TResult Function(SalesAnalyticData itemSales)? loaded,
TResult Function(String message)? error, TResult Function(String message)? error,
required TResult orElse(), required TResult orElse(),
}) { }) {
@ -571,7 +574,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<ItemSales> itemSales) loaded, required TResult Function(SalesAnalyticData itemSales) loaded,
required TResult Function(String message) error, required TResult Function(String message) error,
}) { }) {
return loading(); return loading();
@ -582,7 +585,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<ItemSales> itemSales)? loaded, TResult? Function(SalesAnalyticData itemSales)? loaded,
TResult? Function(String message)? error, TResult? Function(String message)? error,
}) { }) {
return loading?.call(); return loading?.call();
@ -593,7 +596,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<ItemSales> itemSales)? loaded, TResult Function(SalesAnalyticData itemSales)? loaded,
TResult Function(String message)? error, TResult Function(String message)? error,
required TResult orElse(), required TResult orElse(),
}) { }) {
@ -651,7 +654,7 @@ abstract class _$$LoadedImplCopyWith<$Res> {
_$LoadedImpl value, $Res Function(_$LoadedImpl) then) = _$LoadedImpl value, $Res Function(_$LoadedImpl) then) =
__$$LoadedImplCopyWithImpl<$Res>; __$$LoadedImplCopyWithImpl<$Res>;
@useResult @useResult
$Res call({List<ItemSales> itemSales}); $Res call({SalesAnalyticData itemSales});
} }
/// @nodoc /// @nodoc
@ -671,9 +674,9 @@ class __$$LoadedImplCopyWithImpl<$Res>
}) { }) {
return _then(_$LoadedImpl( return _then(_$LoadedImpl(
null == itemSales null == itemSales
? _value._itemSales ? _value.itemSales
: itemSales // ignore: cast_nullable_to_non_nullable : itemSales // ignore: cast_nullable_to_non_nullable
as List<ItemSales>, as SalesAnalyticData,
)); ));
} }
} }
@ -681,15 +684,10 @@ class __$$LoadedImplCopyWithImpl<$Res>
/// @nodoc /// @nodoc
class _$LoadedImpl implements _Loaded { class _$LoadedImpl implements _Loaded {
const _$LoadedImpl(final List<ItemSales> itemSales) : _itemSales = itemSales; const _$LoadedImpl(this.itemSales);
final List<ItemSales> _itemSales;
@override @override
List<ItemSales> get itemSales { final SalesAnalyticData itemSales;
if (_itemSales is EqualUnmodifiableListView) return _itemSales;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_itemSales);
}
@override @override
String toString() { String toString() {
@ -701,13 +699,12 @@ class _$LoadedImpl implements _Loaded {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _$LoadedImpl && other is _$LoadedImpl &&
const DeepCollectionEquality() (identical(other.itemSales, itemSales) ||
.equals(other._itemSales, _itemSales)); other.itemSales == itemSales));
} }
@override @override
int get hashCode => int get hashCode => Object.hash(runtimeType, itemSales);
Object.hash(runtimeType, const DeepCollectionEquality().hash(_itemSales));
/// Create a copy of ItemSalesReportState /// Create a copy of ItemSalesReportState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -722,7 +719,7 @@ class _$LoadedImpl implements _Loaded {
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<ItemSales> itemSales) loaded, required TResult Function(SalesAnalyticData itemSales) loaded,
required TResult Function(String message) error, required TResult Function(String message) error,
}) { }) {
return loaded(itemSales); return loaded(itemSales);
@ -733,7 +730,7 @@ class _$LoadedImpl implements _Loaded {
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<ItemSales> itemSales)? loaded, TResult? Function(SalesAnalyticData itemSales)? loaded,
TResult? Function(String message)? error, TResult? Function(String message)? error,
}) { }) {
return loaded?.call(itemSales); return loaded?.call(itemSales);
@ -744,7 +741,7 @@ class _$LoadedImpl implements _Loaded {
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<ItemSales> itemSales)? loaded, TResult Function(SalesAnalyticData itemSales)? loaded,
TResult Function(String message)? error, TResult Function(String message)? error,
required TResult orElse(), required TResult orElse(),
}) { }) {
@ -793,9 +790,9 @@ class _$LoadedImpl implements _Loaded {
} }
abstract class _Loaded implements ItemSalesReportState { abstract class _Loaded implements ItemSalesReportState {
const factory _Loaded(final List<ItemSales> itemSales) = _$LoadedImpl; const factory _Loaded(final SalesAnalyticData itemSales) = _$LoadedImpl;
List<ItemSales> get itemSales; SalesAnalyticData get itemSales;
/// Create a copy of ItemSalesReportState /// Create a copy of ItemSalesReportState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -874,7 +871,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<ItemSales> itemSales) loaded, required TResult Function(SalesAnalyticData itemSales) loaded,
required TResult Function(String message) error, required TResult Function(String message) error,
}) { }) {
return error(message); return error(message);
@ -885,7 +882,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<ItemSales> itemSales)? loaded, TResult? Function(SalesAnalyticData itemSales)? loaded,
TResult? Function(String message)? error, TResult? Function(String message)? error,
}) { }) {
return error?.call(message); return error?.call(message);
@ -896,7 +893,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<ItemSales> itemSales)? loaded, TResult Function(SalesAnalyticData itemSales)? loaded,
TResult Function(String message)? error, TResult Function(String message)? error,
required TResult orElse(), required TResult orElse(),
}) { }) {

View File

@ -4,7 +4,7 @@ part of 'item_sales_report_bloc.dart';
class ItemSalesReportEvent with _$ItemSalesReportEvent { class ItemSalesReportEvent with _$ItemSalesReportEvent {
const factory ItemSalesReportEvent.started() = _Started; const factory ItemSalesReportEvent.started() = _Started;
const factory ItemSalesReportEvent.getItemSales({ const factory ItemSalesReportEvent.getItemSales({
required String startDate, required DateTime startDate,
required String endDate, required DateTime endDate,
}) = _GetItemSales; }) = _GetItemSales;
} }

View File

@ -4,7 +4,7 @@ part of 'item_sales_report_bloc.dart';
class ItemSalesReportState with _$ItemSalesReportState { class ItemSalesReportState with _$ItemSalesReportState {
const factory ItemSalesReportState.initial() = _Initial; const factory ItemSalesReportState.initial() = _Initial;
const factory ItemSalesReportState.loading() = _Loading; const factory ItemSalesReportState.loading() = _Loading;
const factory ItemSalesReportState.loaded(List<ItemSales> itemSales) = const factory ItemSalesReportState.loaded(SalesAnalyticData itemSales) =
_Loaded; _Loaded;
const factory ItemSalesReportState.error(String message) = _Error; const factory ItemSalesReportState.error(String message) = _Error;
} }

View File

@ -1,5 +1,7 @@
import 'dart:developer'; import 'dart:developer';
import 'package:enaklo_pos/core/extensions/build_context_ext.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';
import 'package:enaklo_pos/core/constants/colors.dart'; import 'package:enaklo_pos/core/constants/colors.dart';
@ -29,7 +31,7 @@ class ReportPage extends StatefulWidget {
} }
class _ReportPageState extends State<ReportPage> { class _ReportPageState extends State<ReportPage> {
int selectedMenu = 0; int selectedMenu = 1;
String title = 'Transaction Report'; String title = 'Transaction Report';
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();
@ -37,10 +39,9 @@ class _ReportPageState extends State<ReportPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
context.read<TransactionReportBloc>().add( context.read<ItemSalesReportBloc>().add(
TransactionReportEvent.getReport( ItemSalesReportEvent.getItemSales(
startDate: DateFormatter.formatDateTime(fromDate), startDate: fromDate, endDate: toDate),
endDate: DateFormatter.formatDateTime(toDate)),
); );
} }
@ -117,18 +118,7 @@ class _ReportPageState extends State<ReportPage> {
'Menampilkan riwayat lengkap semua transaksi yang telah dilakukan.', 'Menampilkan riwayat lengkap semua transaksi yang telah dilakukan.',
icon: Icons.receipt_long_outlined, icon: Icons.receipt_long_outlined,
onPressed: () { onPressed: () {
selectedMenu = 0; context.push(SalesPage(status: 'completed'));
title = 'Laporan Transaksi';
setState(() {});
//enddate is 1 month before the current date
context.read<TransactionReportBloc>().add(
TransactionReportEvent.getReport(
startDate:
DateFormatter.formatDateTime(
fromDate),
endDate: DateFormatter.formatDateTime(
toDate)),
);
}, },
isActive: selectedMenu == 0, isActive: selectedMenu == 0,
), ),
@ -143,11 +133,7 @@ class _ReportPageState extends State<ReportPage> {
setState(() {}); setState(() {});
context.read<ItemSalesReportBloc>().add( context.read<ItemSalesReportBloc>().add(
ItemSalesReportEvent.getItemSales( ItemSalesReportEvent.getItemSales(
startDate: startDate: fromDate, endDate: toDate),
DateFormatter.formatDateTime(
fromDate),
endDate: DateFormatter.formatDateTime(
toDate)),
); );
}, },
isActive: selectedMenu == 1, isActive: selectedMenu == 1,
@ -254,7 +240,7 @@ class _ReportPageState extends State<ReportPage> {
}, },
loaded: (itemSales) { loaded: (itemSales) {
return ItemSalesReportWidget( return ItemSalesReportWidget(
itemSales: itemSales, sales: itemSales,
title: title, title: title,
searchDateFormatted: searchDateFormatted:
searchDateFormatted, searchDateFormatted,

View File

@ -1,24 +1,16 @@
import 'dart:developer';
import 'package:enaklo_pos/core/components/spaces.dart';
import 'package:enaklo_pos/core/constants/colors.dart';
import 'package:enaklo_pos/core/extensions/int_ext.dart'; import 'package:enaklo_pos/core/extensions/int_ext.dart';
import 'package:enaklo_pos/core/utils/helper_pdf_service.dart'; import 'package:enaklo_pos/data/models/response/sales_analytic_response_model.dart';
import 'package:enaklo_pos/presentation/report/widgets/report_page_title.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:enaklo_pos/core/utils/item_sales_invoice.dart'; import 'package:intl/intl.dart';
import 'package:enaklo_pos/core/utils/permession_handler.dart';
import 'package:enaklo_pos/data/models/response/item_sales_response_model.dart';
import 'package:horizontal_data_table/horizontal_data_table.dart';
class ItemSalesReportWidget extends StatelessWidget { class ItemSalesReportWidget extends StatelessWidget {
final String title; final String title;
final String searchDateFormatted; final String searchDateFormatted;
final List<ItemSales> itemSales; final SalesAnalyticData sales;
final List<Widget>? headerWidgets; final List<Widget>? headerWidgets;
const ItemSalesReportWidget({ const ItemSalesReportWidget({
super.key, super.key,
required this.itemSales, required this.sales,
required this.title, required this.title,
required this.searchDateFormatted, required this.searchDateFormatted,
required this.headerWidgets, required this.headerWidgets,
@ -26,123 +18,522 @@ class ItemSalesReportWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Proses data untuk mendapatkan insights
final insights = _processSalesData(sales);
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
border: Border(
left: BorderSide(
color: const Color(0xFFE5E7EB),
width: 1,
),
),
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
_buildHeader(),
const SizedBox(height: 24),
// Metrics menggunakan data dari summary
_buildMetrics(),
const SizedBox(height: 24),
// Daily Performance Section
Text(
'Daily Performance',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: const Color(0xFF111827),
),
),
const SizedBox(height: 16),
// Daily Performance List dengan data dinamis
ListView.builder(
itemCount: insights.sortedDailyData.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
final dayData = insights.sortedDailyData[index];
return _buildDailyPerformanceItem(
date: dayData.formattedDate,
sales: dayData.formattedSales,
orders: dayData.orders,
items: dayData.items,
isHighest: dayData == insights.highestRevenueDay,
);
},
),
const SizedBox(height: 16),
// Summary Footer dengan data dinamis
_buildSummaryFooter(insights.highestRevenueDay),
],
),
),
);
}
// Method untuk memproses data dan mendapatkan insights
SalesInsights _processSalesData(SalesAnalyticData data) {
// Sort data by sales (descending) untuk ranking
List<SalesAnalyticItem> sortedData = List.from(data.data);
sortedData.sort((a, b) => b.sales.compareTo(a.sales));
// Find highest revenue day
SalesAnalyticItem? highestRevenueDay;
if (sortedData.isNotEmpty) {
highestRevenueDay = sortedData.first;
}
return SalesInsights(
originalData: data.data,
sortedDailyData: sortedData,
highestRevenueDay: highestRevenueDay,
summary: data.summary,
);
}
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: const Color(0xFFE5E7EB),
width: 1,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Sales Analytics',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: const Color(0xFF111827),
),
),
const SizedBox(height: 4),
Text(
searchDateFormatted,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: const Color(0xFF6B7280),
),
),
],
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFF10B981),
borderRadius: BorderRadius.circular(24),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.trending_up,
size: 16,
color: Colors.white,
),
const SizedBox(width: 6),
Text(
'Growing',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
),
],
),
);
}
Widget _buildMetrics() {
final summary = sales.summary;
return Column( return Column(
children: [ children: [
ReportPageTitle( Row(
title: title, children: [
searchDateFormatted: searchDateFormatted,
onExport: () async {
try {
final status = await PermessionHelper().checkPermission();
if (status) {
final pdfFile = await ItemSalesInvoice.generate(
itemSales, searchDateFormatted);
log("pdfFile: $pdfFile");
await HelperPdfService.openFile(pdfFile);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Storage permission is required to save PDF'),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
log("Error generating PDF: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to generate PDF: $e'),
backgroundColor: Colors.red,
),
);
}
},
),
const SpaceHeight(16.0),
Expanded( Expanded(
child: Padding( child: _buildMetricCard(
padding: const EdgeInsets.all(12), title: 'Total Sales',
child: ClipRRect( value: summary.totalSales.currencyFormatRpV2,
borderRadius: BorderRadius.circular(12), subtitle: 'Net Sales',
child: HorizontalDataTable( color: const Color(0xFF3B82F6),
leftHandSideColumnWidth: 80, backgroundColor: const Color(0xFFEFF6FF),
rightHandSideColumnWidth: 670, ),
isFixedHeader: true, ),
headerWidgets: headerWidgets, const SizedBox(width: 16),
Expanded(
// isFixedFooter: true, child: _buildMetricCard(
// footerWidgets: _getTitleWidget(), title: 'Total Orders',
leftSideItemBuilder: (context, index) { value: '${summary.totalOrders}',
return Container( subtitle: '${summary.totalItems} Items',
width: 80, color: const Color(0xFF8B5CF6),
height: 52, backgroundColor: const Color(0xFFF3E8FF),
alignment: Alignment.centerLeft, ),
child: Center(child: Text(itemSales[index].id.toString())), ),
],
),
const SizedBox(height: 16),
_buildFullWidthMetricCard(
title: 'Average Order Value',
value: summary.averageOrderValue.round().currencyFormatRpV2,
subtitle: 'Per transaction',
color: const Color(0xFFEF4444),
backgroundColor: const Color(0xFFFEF2F2),
),
],
); );
}, }
rightSideItemBuilder: (context, index) {
return Row( Widget _buildSummaryFooter(SalesAnalyticItem? highestDay) {
children: <Widget>[ if (highestDay == null) {
Container( return Container();
width: 100, }
height: 52,
alignment: Alignment.centerLeft, final dateFormat = DateFormat('dd MMM');
child: Center( final formattedDate = dateFormat.format(highestDay.date);
child: Text(itemSales[index].orderId.toString())),
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF9FAFB),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFFE5E7EB),
width: 1,
), ),
Container(
width: 200,
height: 52,
alignment: Alignment.centerLeft,
child:
Center(child: Text(itemSales[index].productName!)),
), ),
child: Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: const Color(0xFF10B981),
shape: BoxShape.circle,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
'Peak performance on $formattedDate with ${_formatCurrency(highestDay.sales)} revenue (${highestDay.orders} orders)',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: const Color(0xFF374151),
),
),
),
],
),
);
}
// Helper method untuk format currency
String _formatCurrency(int amount) {
if (amount >= 1000000) {
return 'Rp ${(amount / 1000000).toStringAsFixed(1)}JT';
} else if (amount >= 1000) {
return 'Rp ${(amount / 1000).toStringAsFixed(0)}RB';
} else {
return 'Rp ${NumberFormat('#,###').format(amount)}';
}
}
Widget _buildMetricCard({
required String title,
required String value,
required String subtitle,
required Color color,
required Color backgroundColor,
}) {
return Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: color.withOpacity(0.2),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: color,
),
),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: const Color(0xFF111827),
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w400,
color: const Color(0xFF6B7280),
),
),
],
),
);
}
Widget _buildFullWidthMetricCard({
required String title,
required String value,
required String subtitle,
required Color color,
required Color backgroundColor,
}) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: color.withOpacity(0.2),
width: 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: color,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w400,
color: const Color(0xFF6B7280),
),
),
],
),
Text(
value,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: const Color(0xFF111827),
),
),
],
),
);
}
Widget _buildDailyPerformanceItem({
required String date,
required String sales,
required int orders,
required int items,
required bool isHighest,
}) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isHighest ? const Color(0xFF10B981) : const Color(0xFFE5E7EB),
width: isHighest ? 2 : 1,
),
),
child: Row(
children: [
Container( Container(
width: 60, width: 60,
height: 52, child: Column(
alignment: Alignment.centerLeft, crossAxisAlignment: CrossAxisAlignment.start,
child: Center( children: [
child: Text(itemSales[index].quantity.toString())), Text(
date,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: const Color(0xFF111827),
), ),
Container(
width: 150,
height: 52,
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
alignment: Alignment.centerLeft,
child: Center(
child: Text(
itemSales[index].price!.currencyFormatRp,
)),
), ),
if (isHighest)
Container( Container(
width: 160, margin: const EdgeInsets.only(top: 4),
height: 52, padding:
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0), const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
alignment: Alignment.centerLeft, decoration: BoxDecoration(
child: Center( color: const Color(0xFF10B981),
borderRadius: BorderRadius.circular(4),
),
child: Text( child: Text(
(itemSales[index].price! * itemSales[index].quantity!) 'TOP',
.currencyFormatRp, style: TextStyle(
)), fontSize: 8,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
), ),
], ],
);
},
itemCount: itemSales.length,
rowSeparatorWidget: const Divider(
color: Colors.black38,
height: 1.0,
thickness: 0.0,
), ),
leftHandSideColBackgroundColor: AppColors.white, ),
rightHandSideColBackgroundColor: AppColors.white, const SizedBox(width: 16),
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
sales,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: const Color(0xFF111827),
),
),
Text(
'Revenue',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w400,
color: const Color(0xFF6B7280),
),
),
],
),
),
Expanded(
flex: 1,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'$orders orders',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: const Color(0xFF374151),
),
),
Text(
'$items items',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w400,
color: const Color(0xFF6B7280),
),
),
],
),
),
],
),
);
}
}
itemExtent: 55, extension SalesAnalyticItemExtension on SalesAnalyticItem {
), String get formattedDate {
), final dateFormat = DateFormat('dd MMM');
), return dateFormat.format(date);
), }
],
); String get formattedSales {
if (sales >= 1000000) {
return 'Rp ${(sales / 1000000).toStringAsFixed(1)}JT';
} else if (sales >= 1000) {
return 'Rp ${(sales / 1000).toStringAsFixed(0)}RB';
} else {
return 'Rp ${NumberFormat('#,###').format(sales)}';
} }
} }
double get averageOrderValue {
return orders > 0 ? sales / orders : 0.0;
}
}
// ReportPageTitle(
// title: title,
// searchDateFormatted: searchDateFormatted,
// onExport: () async {
// try {
// final status = await PermessionHelper().checkPermission();
// if (status) {
// final pdfFile = await ItemSalesInvoice.generate(
// itemSales, searchDateFormatted);
// log("pdfFile: $pdfFile");
// await HelperPdfService.openFile(pdfFile);
// } else {
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(
// content:
// Text('Storage permission is required to save PDF'),
// backgroundColor: Colors.red,
// ),
// );
// }
// } catch (e) {
// log("Error generating PDF: $e");
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(
// content: Text('Failed to generate PDF: $e'),
// backgroundColor: Colors.red,
// ),
// );
// }
// },
// ),