feat: transaction report

This commit is contained in:
efrilm
2025-08-19 17:39:00 +07:00
parent 1aa65d1732
commit f07d07b3a8
8 changed files with 1927 additions and 38 deletions
@@ -0,0 +1,956 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import '../../../common/extension/extension.dart';
import '../../../common/utils/pdf_service.dart';
import '../../../domain/analytic/analytic.dart';
import '../../../domain/outlet/outlet.dart';
class TransactionReport {
static final primaryColor = PdfColor.fromHex("36175e");
static Future<File> previewPdf({
required Outlet outlet,
required String searchDateFormatted,
required CategoryAnalytic categoryAnalyticData,
required ProfitLossAnalytic profitLossData,
required PaymentMethodAnalytic paymentMethodAnalyticData,
required ProductAnalytic productAnalyticData,
}) async {
final pdf = pw.Document();
final ByteData dataImage = await rootBundle.load('assets/images/logo.png');
final Uint8List bytes = dataImage.buffer.asUint8List();
final profitLossProductSummary = {
'totalRevenue': profitLossData.productData.fold<num>(
0,
(sum, item) => sum + (item.revenue),
),
'totalCost': profitLossData.productData.fold<num>(
0,
(sum, item) => sum + (item.cost),
),
'totalGrossProfit': profitLossData.productData.fold<num>(
0,
(sum, item) => sum + (item.grossProfit),
),
'totalQuantity': profitLossData.productData.fold<num>(
0,
(sum, item) => sum + (item.quantitySold),
),
};
final categorySummary = {
'totalRevenue': categoryAnalyticData.data.fold<num>(
0,
(sum, item) => sum + (item.totalRevenue),
),
'orderCount': categoryAnalyticData.data.fold<num>(
0,
(sum, item) => sum + (item.orderCount),
),
'productCount': categoryAnalyticData.data.fold<num>(
0,
(sum, item) => sum + (item.productCount),
),
'totalQuantity': categoryAnalyticData.data.fold<num>(
0,
(sum, item) => sum + (item.totalQuantity),
),
};
final productItemSummary = {
'totalRevenue': productAnalyticData.data.fold<num>(
0,
(sum, item) => sum + (item.revenue),
),
'orderCount': productAnalyticData.data.fold<num>(
0,
(sum, item) => sum + (item.orderCount),
),
'totalQuantitySold': productAnalyticData.data.fold<num>(
0,
(sum, item) => sum + (item.quantitySold),
),
};
// Membuat objek Image dari gambar
final image = pw.MemoryImage(bytes);
pdf.addPage(
pw.MultiPage(
pageFormat: PdfPageFormat.a4,
margin: pw.EdgeInsets.zero,
build: (pw.Context context) {
return [
pw.Container(
padding: pw.EdgeInsets.all(20),
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
// Bagian kiri - Logo dan Info Perusahaan
pw.Row(
crossAxisAlignment: pw.CrossAxisAlignment.center,
children: [
// Icon/Logo placeholder (bisa diganti dengan gambar logo)
pw.Container(
width: 40,
height: 40,
child: pw.Image(image),
),
pw.SizedBox(width: 15),
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
'Apskel',
style: pw.TextStyle(
fontSize: 28,
fontWeight: pw.FontWeight.bold,
color: primaryColor,
),
),
pw.SizedBox(height: 4),
pw.Text(
outlet.name,
style: pw.TextStyle(
fontSize: 16,
color: PdfColors.grey700,
),
),
pw.SizedBox(height: 2),
pw.Text(
outlet.address,
style: pw.TextStyle(
fontSize: 12,
color: PdfColors.grey600,
),
),
],
),
],
),
// Bagian kanan - Info Laporan
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
pw.Text(
'Laporan Transaksi',
style: pw.TextStyle(
fontSize: 24,
fontWeight: pw.FontWeight.bold,
color: PdfColors.grey800,
),
),
pw.SizedBox(height: 8),
pw.Text(
searchDateFormatted,
style: pw.TextStyle(
fontSize: 14,
color: PdfColors.grey600,
),
),
pw.SizedBox(height: 4),
pw.Text(
'Laporan',
style: pw.TextStyle(
fontSize: 12,
color: PdfColors.grey500,
),
),
],
),
],
),
),
pw.Container(
width: double.infinity,
height: 3,
color: primaryColor,
),
// Summary
pw.Container(
padding: pw.EdgeInsets.all(20),
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_buildSectionWidget('1. Ringkasan'),
pw.SizedBox(height: 30),
pw.Row(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Expanded(
flex: 1,
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_buildSummaryItem(
'Total Penjualan (termasuk rasik)',
(profitLossData.summary.totalRevenue)
.toString()
.currencyFormatRpV2,
),
_buildSummaryItem(
'Total Terjual',
(profitLossData.summary.totalOrders).toString(),
),
_buildSummaryItem(
'HPP',
'${safeCurrency(profitLossData.summary.totalCost)} | ${safePercentage(profitLossData.summary.totalCost, profitLossData.summary.totalRevenue)}',
),
_buildSummaryItem(
'Laba Kotor',
'${safeCurrency(profitLossData.summary.grossProfit)} | ${safeRound(profitLossData.summary.grossProfitMargin)}%',
valueStyle: pw.TextStyle(
color: PdfColors.green800,
fontWeight: pw.FontWeight.bold,
fontSize: 16,
),
labelStyle: pw.TextStyle(
color: PdfColors.green800,
fontWeight: pw.FontWeight.bold,
fontSize: 16,
),
),
],
),
),
pw.SizedBox(width: 20),
pw.Expanded(
flex: 1,
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_buildSummaryItem(
'Biaya Lain lain',
'${safeCurrency(profitLossData.summary.totalTax)} | ${safePercentage(profitLossData.summary.totalTax, profitLossData.summary.totalRevenue)}',
),
_buildSummaryItem(
'Laba/Rugi',
'${safeCurrency(profitLossData.summary.netProfit)} | ${safeRound(profitLossData.summary.netProfitMargin)}%',
valueStyle: pw.TextStyle(
color: PdfColors.blue800,
fontWeight: pw.FontWeight.bold,
fontSize: 16,
),
labelStyle: pw.TextStyle(
color: PdfColors.blue800,
fontWeight: pw.FontWeight.bold,
fontSize: 16,
),
),
],
),
),
],
),
pw.SizedBox(height: 16),
pw.Text(
"Laba Rugi Perproduk",
style: pw.TextStyle(
fontSize: 16,
fontWeight: pw.FontWeight.bold,
color: primaryColor,
),
),
pw.SizedBox(height: 20),
pw.Column(
children: [
pw.Container(
decoration: pw.BoxDecoration(
color: primaryColor, // Purple color
borderRadius: pw.BorderRadius.only(
topLeft: pw.Radius.circular(8),
topRight: pw.Radius.circular(8),
),
),
child: pw.Table(
columnWidths: const {
0: pw.FlexColumnWidth(2.5), // Produk
1: pw.FlexColumnWidth(1), // Qty
2: pw.FlexColumnWidth(2.5), // Pendapatan
3: pw.FlexColumnWidth(2), // HPP
4: pw.FlexColumnWidth(2), // Laba Kotor
5: pw.FlexColumnWidth(2), // Margin (%)
},
children: [
pw.TableRow(
children: [
_buildHeaderCell('Produk'),
_buildHeaderCell('Qty'),
_buildHeaderCell('Pendapatan'),
_buildHeaderCell('HPP'),
_buildHeaderCell('Laba Kotor'),
_buildHeaderCell('Margin (%)'),
],
),
],
),
),
pw.Container(
decoration: pw.BoxDecoration(color: PdfColors.white),
child: pw.Table(
columnWidths: {
0: pw.FlexColumnWidth(2.5), // Produk
1: pw.FlexColumnWidth(1), // Qty
2: pw.FlexColumnWidth(2.5), // Pendapatan
3: pw.FlexColumnWidth(2), // HPP
4: pw.FlexColumnWidth(2), // Laba Kotor
5: pw.FlexColumnWidth(2), // Margin (%)
},
children: profitLossData.productData
.map(
(profitLoss) => _buildPerProductDataRow(
product: profitLoss.productName,
qty: profitLoss.quantitySold.toString(),
pendapatan: profitLoss.revenue
.toString()
.currencyFormatRpV2,
hpp: profitLoss.cost
.toString()
.currencyFormatRpV2,
labaKotor: profitLoss.grossProfit
.toString()
.currencyFormatRpV2,
margin:
'${safeRound(profitLoss.grossProfitMargin)}%',
isEven:
profitLossData.productData.indexOf(
profitLoss,
) %
2 ==
0,
),
)
.toList(),
),
),
pw.Container(
decoration: pw.BoxDecoration(
color: primaryColor, // Purple color
borderRadius: pw.BorderRadius.only(
bottomLeft: pw.Radius.circular(8),
bottomRight: pw.Radius.circular(8),
),
),
child: pw.Table(
columnWidths: const {
0: pw.FlexColumnWidth(2.5), // Produk
1: pw.FlexColumnWidth(1), // Qty
2: pw.FlexColumnWidth(2.5), // Pendapatan
3: pw.FlexColumnWidth(2), // HPP
4: pw.FlexColumnWidth(2), // Laba Kotor
5: pw.FlexColumnWidth(2), // Margin (%)
},
children: [
pw.TableRow(
children: [
_buildTotalCell('TOTAL'),
_buildTotalCell(
profitLossProductSummary['totalQuantity']
.toString(),
),
_buildTotalCell(
profitLossProductSummary['totalRevenue']
.toString()
.currencyFormatRpV2,
),
_buildTotalCell(
profitLossProductSummary['totalCost']
.toString()
.currencyFormatRpV2,
),
_buildTotalCell(
profitLossProductSummary['totalGrossProfit']
.toString()
.currencyFormatRpV2,
),
_buildTotalCell(''),
],
),
],
),
),
],
),
],
),
),
// Summary Payment Method
pw.Container(
padding: pw.EdgeInsets.all(20),
child: pw.Column(
children: [
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_buildSectionWidget('2. Ringkasan Metode Pembayaran'),
pw.SizedBox(height: 30),
pw.Container(
decoration: pw.BoxDecoration(
color: primaryColor, // Purple color
borderRadius: pw.BorderRadius.only(
topLeft: pw.Radius.circular(8),
topRight: pw.Radius.circular(8),
),
),
child: pw.Table(
columnWidths: const {
0: pw.FlexColumnWidth(2.5), // Nama
1: pw.FlexColumnWidth(1), // Tipe
2: pw.FlexColumnWidth(2.5), // Jumlah Order
3: pw.FlexColumnWidth(2), // Total Amount
4: pw.FlexColumnWidth(2), // Presentase
},
children: [
pw.TableRow(
children: [
_buildHeaderCell('Nama'),
_buildHeaderCell('Tipe'),
_buildHeaderCell('Jumlah Order'),
_buildHeaderCell('Total Amount'),
_buildHeaderCell('Presentase'),
],
),
],
),
),
pw.Container(
decoration: pw.BoxDecoration(color: PdfColors.white),
child: pw.Table(
columnWidths: {
0: pw.FlexColumnWidth(2.5), // Nama
1: pw.FlexColumnWidth(1), // Tipe
2: pw.FlexColumnWidth(2.5), // Jumlah Order
3: pw.FlexColumnWidth(2), // Total Amount
4: pw.FlexColumnWidth(2), // Presentase
},
children: paymentMethodAnalyticData.data
.map(
(payment) => _buildPaymentMethodDataRow(
name: payment.paymentMethodName,
tipe: payment.paymentMethodType.toTitleCase,
jumlahOrder: payment.orderCount.toString(),
totalAmount: payment.totalAmount
.toString()
.currencyFormatRpV2,
presentase:
'${safeRound(payment.percentage)}%',
isEven:
paymentMethodAnalyticData.data.indexOf(
payment,
) %
2 ==
0,
),
)
.toList(),
),
),
pw.Container(
decoration: pw.BoxDecoration(
color: primaryColor, // Purple color
borderRadius: pw.BorderRadius.only(
bottomLeft: pw.Radius.circular(8),
bottomRight: pw.Radius.circular(8),
),
),
child: pw.Table(
columnWidths: const {
0: pw.FlexColumnWidth(2.5), // Produk
1: pw.FlexColumnWidth(1), // Qty
2: pw.FlexColumnWidth(2.5), // Pendapatan
3: pw.FlexColumnWidth(2), // HPP
4: pw.FlexColumnWidth(2), // Laba Kotor
5: pw.FlexColumnWidth(2), // Margin (%)
},
children: [
pw.TableRow(
children: [
_buildTotalCell('TOTAL'),
_buildTotalCell(''),
_buildTotalCell(
(paymentMethodAnalyticData
.summary
.totalOrders)
.toString(),
),
_buildTotalCell(
(paymentMethodAnalyticData
.summary
.totalAmount)
.toString()
.currencyFormatRpV2,
),
_buildTotalCell(''),
],
),
],
),
),
],
),
],
),
),
// Summary Category
pw.Container(
padding: pw.EdgeInsets.all(20),
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_buildSectionWidget('3. Ringkasan Kategori'),
pw.SizedBox(height: 30),
pw.Column(
children: [
pw.Container(
decoration: pw.BoxDecoration(
color: primaryColor, // Purple color
borderRadius: pw.BorderRadius.only(
topLeft: pw.Radius.circular(8),
topRight: pw.Radius.circular(8),
),
),
child: pw.Table(
columnWidths: const {
0: pw.FlexColumnWidth(2.5), // Nama
1: pw.FlexColumnWidth(2), // Total Product
2: pw.FlexColumnWidth(1), // qty
3: pw.FlexColumnWidth(2), // Jumlah Order
4: pw.FlexColumnWidth(2.5), // Presentase
},
children: [
pw.TableRow(
children: [
_buildHeaderCell('Nama'),
_buildHeaderCell('Total Produk'),
_buildHeaderCell('Qty'),
_buildHeaderCell('Jumlah Order'),
_buildHeaderCell('Pendapatan'),
],
),
],
),
),
pw.Container(
decoration: pw.BoxDecoration(color: PdfColors.white),
child: pw.Table(
columnWidths: {
0: pw.FlexColumnWidth(2.5), // Nama
1: pw.FlexColumnWidth(2), // Total Product
2: pw.FlexColumnWidth(1), // qty
3: pw.FlexColumnWidth(2), // Jumlah Order
4: pw.FlexColumnWidth(2.5), // Presentase
},
children: categoryAnalyticData.data
.map(
(category) => _buildCategoryDataRow(
name: category.categoryName,
totalProduct: category.productCount
.toString(),
qty: category.totalQuantity.toString(),
jumlahOrder: category.orderCount.toString(),
pendapatan: category.totalRevenue
.toString()
.currencyFormatRpV2,
isEven:
categoryAnalyticData.data.indexOf(
category,
) %
2 ==
0,
),
)
.toList(),
),
),
pw.Container(
decoration: pw.BoxDecoration(
color: primaryColor, // Purple color
borderRadius: pw.BorderRadius.only(
bottomLeft: pw.Radius.circular(8),
bottomRight: pw.Radius.circular(8),
),
),
child: pw.Table(
columnWidths: const {
0: pw.FlexColumnWidth(2.5), // Nama
1: pw.FlexColumnWidth(2), // Total Product
2: pw.FlexColumnWidth(1), // qty
3: pw.FlexColumnWidth(2), // Jumlah Order
4: pw.FlexColumnWidth(2.5), // Presentase
},
children: [
pw.TableRow(
children: [
_buildTotalCell('TOTAL'),
_buildTotalCell(
categorySummary['productCount'].toString(),
),
_buildTotalCell(
categorySummary['totalQuantity'].toString(),
),
_buildTotalCell(
categorySummary['orderCount'].toString(),
),
_buildTotalCell(
categorySummary['totalRevenue']
.toString()
.currencyFormatRpV2,
),
],
),
],
),
),
],
),
],
),
),
// Summary Item
pw.Container(
padding: pw.EdgeInsets.all(20),
child: pw.Column(
children: [
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
_buildSectionWidget('4. Ringkasan Item'),
pw.SizedBox(height: 30),
pw.Container(
decoration: pw.BoxDecoration(
color: primaryColor, // Purple color
borderRadius: pw.BorderRadius.only(
topLeft: pw.Radius.circular(8),
topRight: pw.Radius.circular(8),
),
),
child: pw.Table(
columnWidths: const {
0: pw.FlexColumnWidth(2.5), // Produk
1: pw.FlexColumnWidth(2), // Kategori
2: pw.FlexColumnWidth(1), // qty
3: pw.FlexColumnWidth(2), // Order
4: pw.FlexColumnWidth(2), // Pendapatan
5: pw.FlexColumnWidth(2), // Average
},
children: [
pw.TableRow(
children: [
_buildHeaderCell('Produk'),
_buildHeaderCell('Kategori'),
_buildHeaderCell('Qty'),
_buildHeaderCell('Order'),
_buildHeaderCell('Pendapatan'),
_buildHeaderCell('Rata Rata'),
],
),
],
),
),
pw.Container(
decoration: pw.BoxDecoration(color: PdfColors.white),
child: pw.Table(
columnWidths: {
0: pw.FlexColumnWidth(2.5), // Produk
1: pw.FlexColumnWidth(2), // Kategori
2: pw.FlexColumnWidth(1), // qty
3: pw.FlexColumnWidth(2), // Order
4: pw.FlexColumnWidth(2), // Pendapatan
5: pw.FlexColumnWidth(2), // Average
},
children: productAnalyticData.data
.map(
(item) => _buildItemDataRow(
product: item.productName,
category: item.categoryName,
qty: item.quantitySold.toString(),
order: item.orderCount.toString(),
pendapatan: item.revenue
.toString()
.currencyFormatRpV2,
average: safeCurrency(
item.averagePrice.round(),
),
isEven:
productAnalyticData.data.indexOf(item) %
2 ==
0,
),
)
.toList(),
),
),
pw.Container(
decoration: pw.BoxDecoration(
color: primaryColor, // Purple color
borderRadius: pw.BorderRadius.only(
bottomLeft: pw.Radius.circular(8),
bottomRight: pw.Radius.circular(8),
),
),
child: pw.Table(
columnWidths: const {
0: pw.FlexColumnWidth(2.5), // Produk
1: pw.FlexColumnWidth(2), // Kategori
2: pw.FlexColumnWidth(1), // qty
3: pw.FlexColumnWidth(2), // Order
4: pw.FlexColumnWidth(2), // Pendapatan
5: pw.FlexColumnWidth(2), // Average
},
children: [
pw.TableRow(
children: [
_buildTotalCell('TOTAL'),
_buildTotalCell(''),
_buildTotalCell(
productItemSummary['totalQuantitySold']
.toString(),
),
_buildTotalCell(
productItemSummary['orderCount'].toString(),
),
_buildTotalCell(
productItemSummary['totalRevenue']
.toString()
.currencyFormatRpV2,
),
_buildTotalCell(''),
],
),
],
),
),
],
),
],
),
),
];
},
),
);
return HelperPdfService.saveDocument(
name: 'Laporan Transaksi | $searchDateFormatted.pdf',
pdf: pdf,
);
}
static String safePercentage(num numerator, num denominator) {
if (denominator == 0 ||
numerator.isInfinite ||
numerator.isNaN ||
denominator.isInfinite ||
denominator.isNaN) {
return '0%';
}
final result = (numerator / denominator) * 100;
if (result.isInfinite || result.isNaN) {
return '0%';
}
return '${result.round()}%';
}
static String safeRound(num value) {
if (value.isInfinite || value.isNaN) {
return '0';
}
return value.round().toString();
}
static String safeCurrency(num value) {
if (value.isInfinite || value.isNaN) {
return '0'.currencyFormatRpV2;
}
return value.toString().currencyFormatRpV2;
}
static pw.Widget _buildSectionWidget(String title) {
return pw.Text(
title,
style: pw.TextStyle(
fontSize: 20,
fontWeight: pw.FontWeight.bold,
color: primaryColor,
),
);
}
static pw.Widget _buildSummaryItem(
String label,
String value, {
pw.TextStyle? valueStyle,
pw.TextStyle? labelStyle,
}) {
return pw.Container(
padding: pw.EdgeInsets.only(bottom: 8),
margin: pw.EdgeInsets.only(bottom: 16),
decoration: pw.BoxDecoration(
border: pw.Border(bottom: pw.BorderSide(color: PdfColors.grey300)),
),
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text(label, style: labelStyle),
pw.Text(
value,
style: valueStyle ?? pw.TextStyle(fontWeight: pw.FontWeight.bold),
),
],
),
);
}
static pw.Widget _buildHeaderCell(String text) {
return pw.Container(
padding: pw.EdgeInsets.symmetric(horizontal: 12, vertical: 16),
child: pw.Text(
text,
style: pw.TextStyle(
color: PdfColors.white,
fontWeight: pw.FontWeight.bold,
fontSize: 12,
),
textAlign: pw.TextAlign.center,
),
);
}
static pw.Widget _buildDataCell(
String text, {
pw.Alignment alignment = pw.Alignment.center,
PdfColor? textColor,
}) {
return pw.Container(
padding: pw.EdgeInsets.symmetric(horizontal: 12, vertical: 16),
alignment: alignment,
child: pw.Text(
text,
style: pw.TextStyle(
fontSize: 12,
color: textColor ?? PdfColors.black,
fontWeight: pw.FontWeight.normal,
),
textAlign: alignment == pw.Alignment.centerLeft
? pw.TextAlign.left
: pw.TextAlign.center,
),
);
}
static pw.Widget _buildTotalCell(String text) {
return pw.Container(
padding: pw.EdgeInsets.symmetric(horizontal: 12, vertical: 16),
child: pw.Text(
text,
style: pw.TextStyle(
color: PdfColors.white,
fontWeight: pw.FontWeight.bold,
fontSize: 12,
),
textAlign: pw.TextAlign.center,
),
);
}
static pw.TableRow _buildPerProductDataRow({
required String product,
required String qty,
required String pendapatan,
required String hpp,
required String labaKotor,
required String margin,
required bool isEven,
}) {
return pw.TableRow(
decoration: pw.BoxDecoration(
color: isEven ? PdfColors.grey50 : PdfColors.white,
),
children: [
_buildDataCell(product, alignment: pw.Alignment.centerLeft),
_buildDataCell(qty),
_buildDataCell(pendapatan),
_buildDataCell(hpp, textColor: PdfColors.red600),
_buildDataCell(labaKotor, textColor: PdfColors.green600),
_buildDataCell(margin),
],
);
}
static pw.TableRow _buildPaymentMethodDataRow({
required String name,
required String tipe,
required String jumlahOrder,
required String totalAmount,
required String presentase,
required bool isEven,
}) {
return pw.TableRow(
decoration: pw.BoxDecoration(
color: isEven ? PdfColors.grey50 : PdfColors.white,
),
children: [
_buildDataCell(name, alignment: pw.Alignment.centerLeft),
_buildDataCell(tipe),
_buildDataCell(jumlahOrder),
_buildDataCell(totalAmount),
_buildDataCell(presentase),
],
);
}
static pw.TableRow _buildCategoryDataRow({
required String name,
required String totalProduct,
required String qty,
required String jumlahOrder,
required String pendapatan,
required bool isEven,
}) {
return pw.TableRow(
decoration: pw.BoxDecoration(
color: isEven ? PdfColors.grey50 : PdfColors.white,
),
children: [
_buildDataCell(name, alignment: pw.Alignment.centerLeft),
_buildDataCell(totalProduct),
_buildDataCell(qty),
_buildDataCell(jumlahOrder),
_buildDataCell(pendapatan),
],
);
}
static pw.TableRow _buildItemDataRow({
required String product,
required String category,
required String qty,
required String order,
required String pendapatan,
required String average,
required bool isEven,
}) {
return pw.TableRow(
decoration: pw.BoxDecoration(
color: isEven ? PdfColors.grey50 : PdfColors.white,
),
children: [
_buildDataCell(product, alignment: pw.Alignment.centerLeft),
_buildDataCell(category, alignment: pw.Alignment.centerLeft),
_buildDataCell(qty),
_buildDataCell(order),
_buildDataCell(pendapatan),
_buildDataCell(average),
],
);
}
}
@@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import '../../../application/report/inventory_report/inventory_report_bloc.dart';
import '../../../application/report/transaction_report/transaction_report_bloc.dart';
import '../../../common/extension/extension.dart';
import '../../../common/theme/theme.dart';
import '../../../common/utils/pdf_service.dart';
@@ -14,6 +15,7 @@ import '../../../injection.dart';
import '../../components/appbar/appbar.dart';
import '../../components/field/date_range_picker_field.dart';
import '../../components/report/inventory_report.dart';
import '../../components/report/transaction_report.dart';
import '../../components/toast/flushbar.dart';
@RoutePage()
@@ -24,9 +26,19 @@ class DownloadReportPage extends StatefulWidget implements AutoRouteWrapper {
State<DownloadReportPage> createState() => _DownloadReportPageState();
@override
Widget wrappedRoute(BuildContext context) => BlocProvider(
create: (context) =>
getIt<InventoryReportBloc>()..add(InventoryReportEvent.fetchedOutlet()),
Widget wrappedRoute(BuildContext context) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) =>
getIt<InventoryReportBloc>()
..add(InventoryReportEvent.fetchedOutlet()),
),
BlocProvider(
create: (context) =>
getIt<TransactionReportBloc>()
..add(TransactionReportEvent.fetchedOutlet()),
),
],
child: this,
);
}
@@ -88,17 +100,6 @@ class _DownloadReportPageState extends State<DownloadReportPage>
super.dispose();
}
void _downloadReport(
String reportType,
DateTime? startDate,
DateTime? endDate,
) {
if (startDate == null || endDate == null) {
AppFlushbar.showError(context, 'Please select both start and end dates');
return;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -127,30 +128,75 @@ class _DownloadReportPageState extends State<DownloadReportPage>
child: Column(
children: [
// Transaction Report Card
_ReportOptionCard(
title: 'Transaction Report',
subtitle:
'Export all transaction data with detailed analytics',
icon: Icons.receipt_long_outlined,
gradient: const [
AppColor.primary,
AppColor.primaryLight,
],
startDate: _transactionStartDate,
endDate: _transactionEndDate,
isLoading: false,
onDateRangeChanged: (start, end) {
setState(() {
_transactionStartDate = start;
_transactionEndDate = end;
});
BlocBuilder<
TransactionReportBloc,
TransactionReportState
>(
builder: (context, state) {
return _ReportOptionCard(
title: 'Transaction Report',
subtitle:
'Export all transaction data with detailed analytics',
icon: Icons.receipt_long_outlined,
gradient: const [
AppColor.primary,
AppColor.primaryLight,
],
startDate: _transactionStartDate,
endDate: _transactionEndDate,
isLoading: state.isFetching,
onDateRangeChanged: (start, end) {
setState(() {
_transactionStartDate = start;
_transactionEndDate = end;
});
if (start != null || end != null) {
context.read<TransactionReportBloc>().add(
TransactionReportEvent.fetchedTransaction(
start!,
end!,
),
);
}
},
onDownload: () async {
try {
final status = await PermessionHelper()
.checkPermission();
if (status) {
final pdfFile =
await TransactionReport.previewPdf(
searchDateFormatted:
"${_transactionStartDate?.toServerDate} - ${_transactionEndDate?.toServerDate}",
outlet: state.outlet,
categoryAnalyticData:
state.categoryAnalytic,
profitLossData:
state.profitLossAnalytic,
paymentMethodAnalyticData:
state.paymentMethodAnalytic,
productAnalyticData:
state.productAnalytic,
);
log("pdfFile: $pdfFile");
await HelperPdfService.openFile(pdfFile);
} else {
AppFlushbar.showError(
context,
'Storage permission is required to save PDF',
);
}
} catch (e) {
log("Error generating PDF: $e");
AppFlushbar.showError(
context,
'Failed to generate PDF: $e',
);
}
},
delay: 200,
);
},
onDownload: () => _downloadReport(
'Transaction Report',
_transactionStartDate,
_transactionEndDate,
),
delay: 200,
),
const SizedBox(height: 20),
+1 -1
View File
@@ -139,7 +139,7 @@ class DownloadReportRoute extends _i26.PageRouteInfo<void> {
static _i26.PageInfo page = _i26.PageInfo(
name,
builder: (data) {
return const _i5.DownloadReportPage();
return _i26.WrappedRoute(child: const _i5.DownloadReportPage());
},
);
}