import 'dart:io'; import 'dart:typed_data'; import 'package:enaklo_pos/core/extensions/string_ext.dart'; import 'package:enaklo_pos/core/utils/helper_pdf_service.dart'; import 'package:enaklo_pos/data/models/response/category_analytic_response_model.dart'; import 'package:enaklo_pos/data/models/response/payment_method_analytic_response_model.dart'; import 'package:enaklo_pos/data/models/response/product_analytic_response_model.dart'; import 'package:enaklo_pos/data/models/response/profit_loss_response_model.dart'; import 'package:enaklo_pos/presentation/home/models/outlet_model.dart'; import 'package:flutter/services.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; class TransactionReport { static final primaryColor = PdfColor.fromHex("36175e"); static Future previewPdf({ required Outlet outlet, required String searchDateFormatted, required CategoryAnalyticData? categoryAnalyticData, required ProfitLossData? profitLossData, required PaymentMethodAnalyticData? paymentMethodAnalyticData, required ProductAnalyticData? productAnalyticData, }) async { final pdf = pw.Document(); final ByteData dataImage = await rootBundle.load('assets/logo/logo.png'); final Uint8List bytes = dataImage.buffer.asUint8List(); final profitLossProductSummary = { 'totalRevenue': profitLossData?.productData .fold(0, (sum, item) => sum + (item.revenue)) ?? 0, 'totalCost': profitLossData?.productData .fold(0, (sum, item) => sum + (item.cost)) ?? 0, 'totalGrossProfit': profitLossData?.productData .fold(0, (sum, item) => sum + (item.grossProfit)) ?? 0, 'totalQuantity': profitLossData?.productData .fold(0, (sum, item) => sum + (item.quantitySold)) ?? 0, }; final categorySummary = { 'totalRevenue': categoryAnalyticData?.data .fold(0, (sum, item) => sum + (item.totalRevenue)) ?? 0, 'orderCount': categoryAnalyticData?.data .fold(0, (sum, item) => sum + (item.orderCount)) ?? 0, 'productCount': categoryAnalyticData?.data .fold(0, (sum, item) => sum + (item.productCount)) ?? 0, 'totalQuantity': categoryAnalyticData?.data .fold(0, (sum, item) => sum + (item.totalQuantity)) ?? 0, }; final productItemSummary = { 'totalRevenue': productAnalyticData?.data .fold(0, (sum, item) => sum + (item.revenue)) ?? 0, 'orderCount': productAnalyticData?.data .fold(0, (sum, item) => sum + (item.orderCount)) ?? 0, 'totalQuantitySold': productAnalyticData?.data .fold(0, (sum, item) => sum + (item.quantitySold)) ?? 0, }; // 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 ?? 0) .toString() .currencyFormatRpV2, ), _buildSummaryItem( 'Total Terjual', (profitLossData?.summary.totalOrders ?? 0) .toString(), ), _buildSummaryItem( 'HPP', '${(profitLossData?.summary.totalCost ?? 0).toString().currencyFormatRpV2} | ${(((profitLossData?.summary.totalCost ?? 0) / (profitLossData?.summary.totalRevenue ?? 1)) * 100).round()}%', ), _buildSummaryItem( 'Laba Kotor', '${(profitLossData?.summary.grossProfit ?? 0).toString().currencyFormatRpV2} | ${(profitLossData?.summary.grossProfitMargin ?? 0).round()}%', 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', '${(profitLossData?.summary.totalTax ?? 0).toString().currencyFormatRpV2} | ${(((profitLossData?.summary.totalTax ?? 0) / (profitLossData?.summary.totalRevenue ?? 1)) * 100).round()}%', ), _buildSummaryItem( 'Laba/Rugi', '${(profitLossData?.summary.netProfit ?? 0).toString().currencyFormatRpV2} | ${(profitLossData?.summary.netProfitMargin ?? 0).round()}%', 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: '${profitLoss.grossProfitMargin.round()}%', 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: '${payment.percentage.round()}%', 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 ?? 0) .toString()), _buildTotalCell((paymentMethodAnalyticData ?.summary.totalAmount ?? 0) .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: item.averagePrice .round() .toString() .currencyFormatRpV2, 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 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), ], ); } }