feat: item sales report
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:enaklo_pos/data/models/response/item_sales_response_model.dart';
|
||||
import 'package:enaklo_pos/data/datasources/order_item_remote_datasource.dart';
|
||||
import 'package:enaklo_pos/data/datasources/analytic_remote_datasource.dart';
|
||||
import 'package:enaklo_pos/data/models/response/sales_analytic_response_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'item_sales_report_event.dart';
|
||||
@@ -9,12 +9,14 @@ part 'item_sales_report_bloc.freezed.dart';
|
||||
|
||||
class ItemSalesReportBloc
|
||||
extends Bloc<ItemSalesReportEvent, ItemSalesReportState> {
|
||||
final OrderItemRemoteDatasource datasource;
|
||||
final AnalyticRemoteDatasource datasource;
|
||||
ItemSalesReportBloc(this.datasource) : super(const _Initial()) {
|
||||
on<_GetItemSales>((event, emit) async {
|
||||
emit(const _Loading());
|
||||
final result = await datasource.getItemSalesByRangeDate(
|
||||
event.startDate, event.endDate);
|
||||
final result = await datasource.getSales(
|
||||
dateFrom: event.startDate,
|
||||
dateTo: event.endDate,
|
||||
);
|
||||
result.fold((l) => emit(_Error(l)), (r) => emit(_Loaded(r.data!)));
|
||||
});
|
||||
}
|
||||
|
||||
+46
-49
@@ -19,19 +19,20 @@ mixin _$ItemSalesReportEvent {
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() started,
|
||||
required TResult Function(String startDate, String endDate) getItemSales,
|
||||
required TResult Function(DateTime startDate, DateTime endDate)
|
||||
getItemSales,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? started,
|
||||
TResult? Function(String startDate, String endDate)? getItemSales,
|
||||
TResult? Function(DateTime startDate, DateTime endDate)? getItemSales,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? started,
|
||||
TResult Function(String startDate, String endDate)? getItemSales,
|
||||
TResult Function(DateTime startDate, DateTime endDate)? getItemSales,
|
||||
required TResult orElse(),
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@@ -120,7 +121,8 @@ class _$StartedImpl implements _Started {
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() started,
|
||||
required TResult Function(String startDate, String endDate) getItemSales,
|
||||
required TResult Function(DateTime startDate, DateTime endDate)
|
||||
getItemSales,
|
||||
}) {
|
||||
return started();
|
||||
}
|
||||
@@ -129,7 +131,7 @@ class _$StartedImpl implements _Started {
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? started,
|
||||
TResult? Function(String startDate, String endDate)? getItemSales,
|
||||
TResult? Function(DateTime startDate, DateTime endDate)? getItemSales,
|
||||
}) {
|
||||
return started?.call();
|
||||
}
|
||||
@@ -138,7 +140,7 @@ class _$StartedImpl implements _Started {
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? started,
|
||||
TResult Function(String startDate, String endDate)? getItemSales,
|
||||
TResult Function(DateTime startDate, DateTime endDate)? getItemSales,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (started != null) {
|
||||
@@ -189,7 +191,7 @@ abstract class _$$GetItemSalesImplCopyWith<$Res> {
|
||||
_$GetItemSalesImpl value, $Res Function(_$GetItemSalesImpl) then) =
|
||||
__$$GetItemSalesImplCopyWithImpl<$Res>;
|
||||
@useResult
|
||||
$Res call({String startDate, String endDate});
|
||||
$Res call({DateTime startDate, DateTime endDate});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -212,11 +214,11 @@ class __$$GetItemSalesImplCopyWithImpl<$Res>
|
||||
startDate: null == startDate
|
||||
? _value.startDate
|
||||
: startDate // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
as DateTime,
|
||||
endDate: null == endDate
|
||||
? _value.endDate
|
||||
: 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});
|
||||
|
||||
@override
|
||||
final String startDate;
|
||||
final DateTime startDate;
|
||||
@override
|
||||
final String endDate;
|
||||
final DateTime endDate;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -261,7 +263,8 @@ class _$GetItemSalesImpl implements _GetItemSales {
|
||||
@optionalTypeArgs
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() started,
|
||||
required TResult Function(String startDate, String endDate) getItemSales,
|
||||
required TResult Function(DateTime startDate, DateTime endDate)
|
||||
getItemSales,
|
||||
}) {
|
||||
return getItemSales(startDate, endDate);
|
||||
}
|
||||
@@ -270,7 +273,7 @@ class _$GetItemSalesImpl implements _GetItemSales {
|
||||
@optionalTypeArgs
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? started,
|
||||
TResult? Function(String startDate, String endDate)? getItemSales,
|
||||
TResult? Function(DateTime startDate, DateTime endDate)? getItemSales,
|
||||
}) {
|
||||
return getItemSales?.call(startDate, endDate);
|
||||
}
|
||||
@@ -279,7 +282,7 @@ class _$GetItemSalesImpl implements _GetItemSales {
|
||||
@optionalTypeArgs
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? started,
|
||||
TResult Function(String startDate, String endDate)? getItemSales,
|
||||
TResult Function(DateTime startDate, DateTime endDate)? getItemSales,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
if (getItemSales != null) {
|
||||
@@ -322,11 +325,11 @@ class _$GetItemSalesImpl implements _GetItemSales {
|
||||
|
||||
abstract class _GetItemSales implements ItemSalesReportEvent {
|
||||
const factory _GetItemSales(
|
||||
{required final String startDate,
|
||||
required final String endDate}) = _$GetItemSalesImpl;
|
||||
{required final DateTime startDate,
|
||||
required final DateTime endDate}) = _$GetItemSalesImpl;
|
||||
|
||||
String get startDate;
|
||||
String get endDate;
|
||||
DateTime get startDate;
|
||||
DateTime get endDate;
|
||||
|
||||
/// Create a copy of ItemSalesReportEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -341,7 +344,7 @@ mixin _$ItemSalesReportState {
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() initial,
|
||||
required TResult Function() loading,
|
||||
required TResult Function(List<ItemSales> itemSales) loaded,
|
||||
required TResult Function(SalesAnalyticData itemSales) loaded,
|
||||
required TResult Function(String message) error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@@ -349,7 +352,7 @@ mixin _$ItemSalesReportState {
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? initial,
|
||||
TResult? Function()? loading,
|
||||
TResult? Function(List<ItemSales> itemSales)? loaded,
|
||||
TResult? Function(SalesAnalyticData itemSales)? loaded,
|
||||
TResult? Function(String message)? error,
|
||||
}) =>
|
||||
throw _privateConstructorUsedError;
|
||||
@@ -357,7 +360,7 @@ mixin _$ItemSalesReportState {
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? initial,
|
||||
TResult Function()? loading,
|
||||
TResult Function(List<ItemSales> itemSales)? loaded,
|
||||
TResult Function(SalesAnalyticData itemSales)? loaded,
|
||||
TResult Function(String message)? error,
|
||||
required TResult orElse(),
|
||||
}) =>
|
||||
@@ -454,7 +457,7 @@ class _$InitialImpl implements _Initial {
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() initial,
|
||||
required TResult Function() loading,
|
||||
required TResult Function(List<ItemSales> itemSales) loaded,
|
||||
required TResult Function(SalesAnalyticData itemSales) loaded,
|
||||
required TResult Function(String message) error,
|
||||
}) {
|
||||
return initial();
|
||||
@@ -465,7 +468,7 @@ class _$InitialImpl implements _Initial {
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? initial,
|
||||
TResult? Function()? loading,
|
||||
TResult? Function(List<ItemSales> itemSales)? loaded,
|
||||
TResult? Function(SalesAnalyticData itemSales)? loaded,
|
||||
TResult? Function(String message)? error,
|
||||
}) {
|
||||
return initial?.call();
|
||||
@@ -476,7 +479,7 @@ class _$InitialImpl implements _Initial {
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? initial,
|
||||
TResult Function()? loading,
|
||||
TResult Function(List<ItemSales> itemSales)? loaded,
|
||||
TResult Function(SalesAnalyticData itemSales)? loaded,
|
||||
TResult Function(String message)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
@@ -571,7 +574,7 @@ class _$LoadingImpl implements _Loading {
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() initial,
|
||||
required TResult Function() loading,
|
||||
required TResult Function(List<ItemSales> itemSales) loaded,
|
||||
required TResult Function(SalesAnalyticData itemSales) loaded,
|
||||
required TResult Function(String message) error,
|
||||
}) {
|
||||
return loading();
|
||||
@@ -582,7 +585,7 @@ class _$LoadingImpl implements _Loading {
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? initial,
|
||||
TResult? Function()? loading,
|
||||
TResult? Function(List<ItemSales> itemSales)? loaded,
|
||||
TResult? Function(SalesAnalyticData itemSales)? loaded,
|
||||
TResult? Function(String message)? error,
|
||||
}) {
|
||||
return loading?.call();
|
||||
@@ -593,7 +596,7 @@ class _$LoadingImpl implements _Loading {
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? initial,
|
||||
TResult Function()? loading,
|
||||
TResult Function(List<ItemSales> itemSales)? loaded,
|
||||
TResult Function(SalesAnalyticData itemSales)? loaded,
|
||||
TResult Function(String message)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
@@ -651,7 +654,7 @@ abstract class _$$LoadedImplCopyWith<$Res> {
|
||||
_$LoadedImpl value, $Res Function(_$LoadedImpl) then) =
|
||||
__$$LoadedImplCopyWithImpl<$Res>;
|
||||
@useResult
|
||||
$Res call({List<ItemSales> itemSales});
|
||||
$Res call({SalesAnalyticData itemSales});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@@ -671,9 +674,9 @@ class __$$LoadedImplCopyWithImpl<$Res>
|
||||
}) {
|
||||
return _then(_$LoadedImpl(
|
||||
null == itemSales
|
||||
? _value._itemSales
|
||||
? _value.itemSales
|
||||
: itemSales // ignore: cast_nullable_to_non_nullable
|
||||
as List<ItemSales>,
|
||||
as SalesAnalyticData,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -681,15 +684,10 @@ class __$$LoadedImplCopyWithImpl<$Res>
|
||||
/// @nodoc
|
||||
|
||||
class _$LoadedImpl implements _Loaded {
|
||||
const _$LoadedImpl(final List<ItemSales> itemSales) : _itemSales = itemSales;
|
||||
const _$LoadedImpl(this.itemSales);
|
||||
|
||||
final List<ItemSales> _itemSales;
|
||||
@override
|
||||
List<ItemSales> get itemSales {
|
||||
if (_itemSales is EqualUnmodifiableListView) return _itemSales;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_itemSales);
|
||||
}
|
||||
final SalesAnalyticData itemSales;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -701,13 +699,12 @@ class _$LoadedImpl implements _Loaded {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$LoadedImpl &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._itemSales, _itemSales));
|
||||
(identical(other.itemSales, itemSales) ||
|
||||
other.itemSales == itemSales));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, const DeepCollectionEquality().hash(_itemSales));
|
||||
int get hashCode => Object.hash(runtimeType, itemSales);
|
||||
|
||||
/// Create a copy of ItemSalesReportState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -722,7 +719,7 @@ class _$LoadedImpl implements _Loaded {
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() initial,
|
||||
required TResult Function() loading,
|
||||
required TResult Function(List<ItemSales> itemSales) loaded,
|
||||
required TResult Function(SalesAnalyticData itemSales) loaded,
|
||||
required TResult Function(String message) error,
|
||||
}) {
|
||||
return loaded(itemSales);
|
||||
@@ -733,7 +730,7 @@ class _$LoadedImpl implements _Loaded {
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? initial,
|
||||
TResult? Function()? loading,
|
||||
TResult? Function(List<ItemSales> itemSales)? loaded,
|
||||
TResult? Function(SalesAnalyticData itemSales)? loaded,
|
||||
TResult? Function(String message)? error,
|
||||
}) {
|
||||
return loaded?.call(itemSales);
|
||||
@@ -744,7 +741,7 @@ class _$LoadedImpl implements _Loaded {
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? initial,
|
||||
TResult Function()? loading,
|
||||
TResult Function(List<ItemSales> itemSales)? loaded,
|
||||
TResult Function(SalesAnalyticData itemSales)? loaded,
|
||||
TResult Function(String message)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
@@ -793,9 +790,9 @@ class _$LoadedImpl implements _Loaded {
|
||||
}
|
||||
|
||||
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
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -874,7 +871,7 @@ class _$ErrorImpl implements _Error {
|
||||
TResult when<TResult extends Object?>({
|
||||
required TResult Function() initial,
|
||||
required TResult Function() loading,
|
||||
required TResult Function(List<ItemSales> itemSales) loaded,
|
||||
required TResult Function(SalesAnalyticData itemSales) loaded,
|
||||
required TResult Function(String message) error,
|
||||
}) {
|
||||
return error(message);
|
||||
@@ -885,7 +882,7 @@ class _$ErrorImpl implements _Error {
|
||||
TResult? whenOrNull<TResult extends Object?>({
|
||||
TResult? Function()? initial,
|
||||
TResult? Function()? loading,
|
||||
TResult? Function(List<ItemSales> itemSales)? loaded,
|
||||
TResult? Function(SalesAnalyticData itemSales)? loaded,
|
||||
TResult? Function(String message)? error,
|
||||
}) {
|
||||
return error?.call(message);
|
||||
@@ -896,7 +893,7 @@ class _$ErrorImpl implements _Error {
|
||||
TResult maybeWhen<TResult extends Object?>({
|
||||
TResult Function()? initial,
|
||||
TResult Function()? loading,
|
||||
TResult Function(List<ItemSales> itemSales)? loaded,
|
||||
TResult Function(SalesAnalyticData itemSales)? loaded,
|
||||
TResult Function(String message)? error,
|
||||
required TResult orElse(),
|
||||
}) {
|
||||
|
||||
@@ -4,7 +4,7 @@ part of 'item_sales_report_bloc.dart';
|
||||
class ItemSalesReportEvent with _$ItemSalesReportEvent {
|
||||
const factory ItemSalesReportEvent.started() = _Started;
|
||||
const factory ItemSalesReportEvent.getItemSales({
|
||||
required String startDate,
|
||||
required String endDate,
|
||||
required DateTime startDate,
|
||||
required DateTime endDate,
|
||||
}) = _GetItemSales;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ part of 'item_sales_report_bloc.dart';
|
||||
class ItemSalesReportState with _$ItemSalesReportState {
|
||||
const factory ItemSalesReportState.initial() = _Initial;
|
||||
const factory ItemSalesReportState.loading() = _Loading;
|
||||
const factory ItemSalesReportState.loaded(List<ItemSales> itemSales) =
|
||||
const factory ItemSalesReportState.loaded(SalesAnalyticData itemSales) =
|
||||
_Loaded;
|
||||
const factory ItemSalesReportState.error(String message) = _Error;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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:enaklo_pos/core/components/custom_date_picker.dart';
|
||||
import 'package:enaklo_pos/core/constants/colors.dart';
|
||||
@@ -29,7 +31,7 @@ class ReportPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ReportPageState extends State<ReportPage> {
|
||||
int selectedMenu = 0;
|
||||
int selectedMenu = 1;
|
||||
String title = 'Transaction Report';
|
||||
DateTime fromDate = DateTime.now().subtract(const Duration(days: 30));
|
||||
DateTime toDate = DateTime.now();
|
||||
@@ -37,10 +39,9 @@ class _ReportPageState extends State<ReportPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<TransactionReportBloc>().add(
|
||||
TransactionReportEvent.getReport(
|
||||
startDate: DateFormatter.formatDateTime(fromDate),
|
||||
endDate: DateFormatter.formatDateTime(toDate)),
|
||||
context.read<ItemSalesReportBloc>().add(
|
||||
ItemSalesReportEvent.getItemSales(
|
||||
startDate: fromDate, endDate: toDate),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,18 +118,7 @@ class _ReportPageState extends State<ReportPage> {
|
||||
'Menampilkan riwayat lengkap semua transaksi yang telah dilakukan.',
|
||||
icon: Icons.receipt_long_outlined,
|
||||
onPressed: () {
|
||||
selectedMenu = 0;
|
||||
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)),
|
||||
);
|
||||
context.push(SalesPage(status: 'completed'));
|
||||
},
|
||||
isActive: selectedMenu == 0,
|
||||
),
|
||||
@@ -143,11 +133,7 @@ class _ReportPageState extends State<ReportPage> {
|
||||
setState(() {});
|
||||
context.read<ItemSalesReportBloc>().add(
|
||||
ItemSalesReportEvent.getItemSales(
|
||||
startDate:
|
||||
DateFormatter.formatDateTime(
|
||||
fromDate),
|
||||
endDate: DateFormatter.formatDateTime(
|
||||
toDate)),
|
||||
startDate: fromDate, endDate: toDate),
|
||||
);
|
||||
},
|
||||
isActive: selectedMenu == 1,
|
||||
@@ -254,7 +240,7 @@ class _ReportPageState extends State<ReportPage> {
|
||||
},
|
||||
loaded: (itemSales) {
|
||||
return ItemSalesReportWidget(
|
||||
itemSales: itemSales,
|
||||
sales: itemSales,
|
||||
title: title,
|
||||
searchDateFormatted:
|
||||
searchDateFormatted,
|
||||
|
||||
@@ -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/utils/helper_pdf_service.dart';
|
||||
import 'package:enaklo_pos/presentation/report/widgets/report_page_title.dart';
|
||||
import 'package:enaklo_pos/data/models/response/sales_analytic_response_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:enaklo_pos/core/utils/item_sales_invoice.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';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class ItemSalesReportWidget extends StatelessWidget {
|
||||
final String title;
|
||||
final String searchDateFormatted;
|
||||
final List<ItemSales> itemSales;
|
||||
final SalesAnalyticData sales;
|
||||
final List<Widget>? headerWidgets;
|
||||
const ItemSalesReportWidget({
|
||||
super.key,
|
||||
required this.itemSales,
|
||||
required this.sales,
|
||||
required this.title,
|
||||
required this.searchDateFormatted,
|
||||
required this.headerWidgets,
|
||||
@@ -26,123 +18,522 @@ class ItemSalesReportWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
// 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,
|
||||
),
|
||||
),
|
||||
const SpaceHeight(16.0),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: HorizontalDataTable(
|
||||
leftHandSideColumnWidth: 80,
|
||||
rightHandSideColumnWidth: 670,
|
||||
isFixedHeader: true,
|
||||
headerWidgets: headerWidgets,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
_buildHeader(),
|
||||
|
||||
// isFixedFooter: true,
|
||||
// footerWidgets: _getTitleWidget(),
|
||||
leftSideItemBuilder: (context, index) {
|
||||
return Container(
|
||||
width: 80,
|
||||
height: 52,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Center(child: Text(itemSales[index].id.toString())),
|
||||
);
|
||||
},
|
||||
rightSideItemBuilder: (context, index) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 100,
|
||||
height: 52,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Center(
|
||||
child: Text(itemSales[index].orderId.toString())),
|
||||
),
|
||||
Container(
|
||||
width: 200,
|
||||
height: 52,
|
||||
alignment: Alignment.centerLeft,
|
||||
child:
|
||||
Center(child: Text(itemSales[index].productName!)),
|
||||
),
|
||||
Container(
|
||||
width: 60,
|
||||
height: 52,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Center(
|
||||
child: Text(itemSales[index].quantity.toString())),
|
||||
),
|
||||
Container(
|
||||
width: 150,
|
||||
height: 52,
|
||||
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Center(
|
||||
child: Text(
|
||||
itemSales[index].price!.currencyFormatRp,
|
||||
)),
|
||||
),
|
||||
Container(
|
||||
width: 160,
|
||||
height: 52,
|
||||
padding: const EdgeInsets.fromLTRB(5, 0, 0, 0),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Center(
|
||||
child: Text(
|
||||
(itemSales[index].price! * itemSales[index].quantity!)
|
||||
.currencyFormatRp,
|
||||
)),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
itemCount: itemSales.length,
|
||||
rowSeparatorWidget: const Divider(
|
||||
color: Colors.black38,
|
||||
height: 1.0,
|
||||
thickness: 0.0,
|
||||
),
|
||||
leftHandSideColBackgroundColor: AppColors.white,
|
||||
rightHandSideColBackgroundColor: AppColors.white,
|
||||
const SizedBox(height: 24),
|
||||
|
||||
itemExtent: 55,
|
||||
// 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(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildMetricCard(
|
||||
title: 'Total Sales',
|
||||
value: summary.totalSales.currencyFormatRpV2,
|
||||
subtitle: 'Net Sales',
|
||||
color: const Color(0xFF3B82F6),
|
||||
backgroundColor: const Color(0xFFEFF6FF),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildMetricCard(
|
||||
title: 'Total Orders',
|
||||
value: '${summary.totalOrders}',
|
||||
subtitle: '${summary.totalItems} Items',
|
||||
color: const Color(0xFF8B5CF6),
|
||||
backgroundColor: const Color(0xFFF3E8FF),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSummaryFooter(SalesAnalyticItem? highestDay) {
|
||||
if (highestDay == null) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
final dateFormat = DateFormat('dd MMM');
|
||||
final formattedDate = dateFormat.format(highestDay.date);
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
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(
|
||||
width: 60,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
date,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF111827),
|
||||
),
|
||||
),
|
||||
if (isHighest)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 4),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF10B981),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'TOP',
|
||||
style: TextStyle(
|
||||
fontSize: 8,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
Reference in New Issue
Block a user