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 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( 0, (sum, item) => sum + (item.revenue), ), 'totalCost': profitLossData.productData.fold( 0, (sum, item) => sum + (item.cost), ), 'totalGrossProfit': profitLossData.productData.fold( 0, (sum, item) => sum + (item.grossProfit), ), 'totalQuantity': profitLossData.productData.fold( 0, (sum, item) => sum + (item.quantitySold), ), }; final categorySummary = { 'totalRevenue': categoryAnalyticData.data.fold( 0, (sum, item) => sum + (item.totalRevenue), ), 'orderCount': categoryAnalyticData.data.fold( 0, (sum, item) => sum + (item.orderCount), ), 'productCount': categoryAnalyticData.data.fold( 0, (sum, item) => sum + (item.productCount), ), 'totalQuantity': categoryAnalyticData.data.fold( 0, (sum, item) => sum + (item.totalQuantity), ), }; final productItemSummary = { 'totalRevenue': productAnalyticData.data.fold( 0, (sum, item) => sum + (item.revenue), ), 'orderCount': productAnalyticData.data.fold( 0, (sum, item) => sum + (item.orderCount), ), 'totalQuantitySold': productAnalyticData.data.fold( 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), ], ); } }