feat: transaction report
This commit is contained in:
@@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user