diff --git a/lib/core/components/pdf/pdf_header.dart b/lib/core/components/pdf/pdf_header.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/core/utils/transaction_report.dart b/lib/core/utils/transaction_report.dart new file mode 100644 index 0000000..4567cc8 --- /dev/null +++ b/lib/core/utils/transaction_report.dart @@ -0,0 +1,912 @@ +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 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), + ], + ); + } +} diff --git a/lib/data/datasources/analytic_remote_datasource.dart b/lib/data/datasources/analytic_remote_datasource.dart index b538034..226a74d 100644 --- a/lib/data/datasources/analytic_remote_datasource.dart +++ b/lib/data/datasources/analytic_remote_datasource.dart @@ -5,6 +5,7 @@ import 'package:dio/dio.dart'; import 'package:enaklo_pos/core/constants/variables.dart'; import 'package:enaklo_pos/core/network/dio_client.dart'; import 'package:enaklo_pos/data/datasources/auth_local_datasource.dart'; +import 'package:enaklo_pos/data/models/response/category_analytic_response_model.dart'; import 'package:enaklo_pos/data/models/response/dashboard_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'; @@ -184,4 +185,38 @@ class AnalyticRemoteDatasource { return left('Unexpected error occurred'); } } + + Future> getCategory({ + required DateTime dateFrom, + required DateTime dateTo, + }) async { + final authData = await AuthLocalDataSource().getAuthData(); + final headers = { + 'Authorization': 'Bearer ${authData.token}', + 'Accept': 'application/json', + }; + + try { + final response = await dio.get( + '${Variables.baseUrl}/api/v1/analytics/categories', + queryParameters: { + 'date_from': DateFormat('dd-MM-yyyy').format(dateFrom), + 'date_to': DateFormat('dd-MM-yyyy').format(dateTo), + }, + options: Options(headers: headers), + ); + + if (response.statusCode == 200) { + return right(CategoryAnalyticResponseModel.fromMap(response.data)); + } else { + return left('Terjadi Kesalahan, Coba lagi nanti.'); + } + } on DioException catch (e) { + log('Dio error: ${e.message}'); + return left(e.response?.data.toString() ?? e.message ?? 'Unknown error'); + } catch (e) { + log('Unexpected error: $e'); + return left('Unexpected error occurred'); + } + } } diff --git a/lib/data/models/response/category_analytic_response_model.dart b/lib/data/models/response/category_analytic_response_model.dart new file mode 100644 index 0000000..da4c877 --- /dev/null +++ b/lib/data/models/response/category_analytic_response_model.dart @@ -0,0 +1,129 @@ +class CategoryAnalyticResponseModel { + final bool success; + final CategoryAnalyticData? data; + final dynamic errors; + + CategoryAnalyticResponseModel({ + required this.success, + required this.data, + this.errors, + }); + + // Dari JSON String ke Model + factory CategoryAnalyticResponseModel.fromJson(Map json) { + return CategoryAnalyticResponseModel( + success: json['success'], + data: json['data'] == null + ? null + : CategoryAnalyticData.fromMap(json['data']), + errors: json['errors'], + ); + } + + // Dari Model ke JSON String + Map toJson() { + return { + 'success': success, + 'data': data?.toMap(), + 'errors': errors, + }; + } + + // Dari Map ke Model + factory CategoryAnalyticResponseModel.fromMap(Map map) { + return CategoryAnalyticResponseModel( + success: map['success'], + data: CategoryAnalyticData.fromMap(map['data']), + errors: map['errors'], + ); + } + + // Dari Model ke Map + Map toMap() { + return { + 'success': success, + 'data': data?.toMap(), + 'errors': errors, + }; + } +} + +class CategoryAnalyticData { + final String organizationId; + final String outletId; + final DateTime dateFrom; + final DateTime dateTo; + final List data; + + CategoryAnalyticData({ + required this.organizationId, + required this.outletId, + required this.dateFrom, + required this.dateTo, + required this.data, + }); + + factory CategoryAnalyticData.fromMap(Map map) { + return CategoryAnalyticData( + organizationId: map['organization_id'], + outletId: map['outlet_id'], + dateFrom: DateTime.parse(map['date_from']), + dateTo: DateTime.parse(map['date_to']), + data: map['data'] == null + ? [] + : List.from( + map['data']?.map((x) => CategoryAnalyticItem.fromMap(x)) ?? [], + ), + ); + } + + Map toMap() { + return { + 'organization_id': organizationId, + 'outlet_id': outletId, + 'date_from': dateFrom.toIso8601String(), + 'date_to': dateTo.toIso8601String(), + 'data': data.map((x) => x.toMap()).toList(), + }; + } +} + +class CategoryAnalyticItem { + final String categoryId; + final String categoryName; + final int totalRevenue; + final int totalQuantity; + final int productCount; + final int orderCount; + + CategoryAnalyticItem({ + required this.categoryId, + required this.categoryName, + required this.totalRevenue, + required this.totalQuantity, + required this.productCount, + required this.orderCount, + }); + + factory CategoryAnalyticItem.fromMap(Map map) { + return CategoryAnalyticItem( + categoryId: map['category_id'], + categoryName: map['category_name'], + totalRevenue: map['total_revenue'] ?? 0, + totalQuantity: map['total_quantity'] ?? 0, + productCount: map['product_count'] ?? 0, + orderCount: map['order_count'] ?? 0, + ); + } + + Map toMap() { + return { + 'category_id': categoryId, + 'category_name': categoryName, + 'total_revenue': totalRevenue, + 'total_quantity': totalQuantity, + 'product_count': productCount, + 'order_count': orderCount, + }; + } +} diff --git a/lib/data/models/response/payment_method_analytic_response_model.dart b/lib/data/models/response/payment_method_analytic_response_model.dart index 6796cae..e5d5bd4 100644 --- a/lib/data/models/response/payment_method_analytic_response_model.dart +++ b/lib/data/models/response/payment_method_analytic_response_model.dart @@ -1,6 +1,6 @@ class PaymentMethodAnalyticResponseModel { final bool success; - final PaymentMethodAnalyticData data; + final PaymentMethodAnalyticData? data; final dynamic errors; PaymentMethodAnalyticResponseModel({ @@ -18,7 +18,9 @@ class PaymentMethodAnalyticResponseModel { factory PaymentMethodAnalyticResponseModel.fromMap(Map map) { return PaymentMethodAnalyticResponseModel( success: map['success'], - data: PaymentMethodAnalyticData.fromMap(map['data']), + data: map['data'] == null + ? null + : PaymentMethodAnalyticData.fromMap(map['data']), errors: map['errors'], ); } @@ -26,7 +28,7 @@ class PaymentMethodAnalyticResponseModel { Map toMap() { return { 'success': success, - 'data': data.toMap(), + 'data': data?.toMap(), 'errors': errors, }; } diff --git a/lib/data/models/response/product_analytic_response_model.dart b/lib/data/models/response/product_analytic_response_model.dart index 35a9f4f..513cd55 100644 --- a/lib/data/models/response/product_analytic_response_model.dart +++ b/lib/data/models/response/product_analytic_response_model.dart @@ -1,6 +1,6 @@ class ProductAnalyticResponseModel { final bool success; - final ProductAnalyticData data; + final ProductAnalyticData? data; final dynamic errors; ProductAnalyticResponseModel({ @@ -17,7 +17,8 @@ class ProductAnalyticResponseModel { factory ProductAnalyticResponseModel.fromMap(Map map) { return ProductAnalyticResponseModel( success: map['success'] ?? false, - data: ProductAnalyticData.fromMap(map['data']), + data: + map['data'] == null ? null : ProductAnalyticData.fromMap(map['data']), errors: map['errors'], ); } @@ -25,7 +26,7 @@ class ProductAnalyticResponseModel { Map toMap() { return { 'success': success, - 'data': data.toMap(), + 'data': data?.toMap(), 'errors': errors, }; } diff --git a/lib/data/models/response/profit_loss_response_model.dart b/lib/data/models/response/profit_loss_response_model.dart index baabbb9..102abf7 100644 --- a/lib/data/models/response/profit_loss_response_model.dart +++ b/lib/data/models/response/profit_loss_response_model.dart @@ -1,6 +1,6 @@ class ProfitLossResponseModel { final bool success; - final ProfitLossData data; + final ProfitLossData? data; final dynamic errors; ProfitLossResponseModel({ @@ -13,7 +13,7 @@ class ProfitLossResponseModel { factory ProfitLossResponseModel.fromJson(Map json) { return ProfitLossResponseModel( success: json['success'], - data: ProfitLossData.fromMap(json['data']), + data: json['data'] == null ? null : ProfitLossData.fromMap(json['data']), errors: json['errors'], ); } @@ -22,7 +22,7 @@ class ProfitLossResponseModel { Map toJson() { return { 'success': success, - 'data': data.toMap(), + 'data': data?.toMap(), 'errors': errors, }; } @@ -40,7 +40,7 @@ class ProfitLossResponseModel { Map toMap() { return { 'success': success, - 'data': data.toMap(), + 'data': data?.toMap(), 'errors': errors, }; } diff --git a/lib/main.dart b/lib/main.dart index e6acfe8..2185456 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,6 +17,7 @@ import 'package:enaklo_pos/presentation/home/bloc/product_loader/product_loader_ import 'package:enaklo_pos/presentation/home/bloc/user_update_outlet/user_update_outlet_bloc.dart'; import 'package:enaklo_pos/presentation/refund/bloc/refund_bloc.dart'; import 'package:enaklo_pos/presentation/report/blocs/profit_loss/profit_loss_bloc.dart'; +import 'package:enaklo_pos/presentation/report/blocs/report/report_bloc.dart'; import 'package:enaklo_pos/presentation/sales/blocs/order_loader/order_loader_bloc.dart'; import 'package:enaklo_pos/presentation/sales/blocs/payment_form/payment_form_bloc.dart'; import 'package:enaklo_pos/presentation/setting/bloc/get_printer_ticket/get_printer_ticket_bloc.dart'; @@ -288,6 +289,9 @@ class _MyAppState extends State { BlocProvider( create: (context) => TransferTableBloc(TableRemoteDataSource()), ), + BlocProvider( + create: (context) => ReportBloc(AnalyticRemoteDatasource()), + ), ], child: MaterialApp( navigatorKey: AuthInterceptor.navigatorKey, diff --git a/lib/presentation/report/blocs/payment_method_report/payment_method_report_bloc.dart b/lib/presentation/report/blocs/payment_method_report/payment_method_report_bloc.dart index ddf6ff7..e250e7d 100644 --- a/lib/presentation/report/blocs/payment_method_report/payment_method_report_bloc.dart +++ b/lib/presentation/report/blocs/payment_method_report/payment_method_report_bloc.dart @@ -22,7 +22,7 @@ class PaymentMethodReportBloc (l) => emit(_Error(l)), (r) => emit( _Loaded( - r.data, + r.data!, ), ), ); diff --git a/lib/presentation/report/blocs/profit_loss/profit_loss_bloc.dart b/lib/presentation/report/blocs/profit_loss/profit_loss_bloc.dart index 5b5c4c4..6500659 100644 --- a/lib/presentation/report/blocs/profit_loss/profit_loss_bloc.dart +++ b/lib/presentation/report/blocs/profit_loss/profit_loss_bloc.dart @@ -16,7 +16,7 @@ class ProfitLossBloc extends Bloc { dateFrom: event.startDate, dateTo: event.endDate, ); - result.fold((l) => emit(_Error(l)), (r) => emit(_Success(r.data))); + result.fold((l) => emit(_Error(l)), (r) => emit(_Success(r.data!))); }); } } diff --git a/lib/presentation/report/blocs/report/report_bloc.dart b/lib/presentation/report/blocs/report/report_bloc.dart new file mode 100644 index 0000000..f8b8e2b --- /dev/null +++ b/lib/presentation/report/blocs/report/report_bloc.dart @@ -0,0 +1,63 @@ +import 'package:bloc/bloc.dart'; +import 'package:enaklo_pos/data/datasources/analytic_remote_datasource.dart'; +import 'package:enaklo_pos/data/datasources/outlet_local_datasource.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:freezed_annotation/freezed_annotation.dart'; + +part 'report_event.dart'; +part 'report_state.dart'; +part 'report_bloc.freezed.dart'; + +class ReportBloc extends Bloc { + final AnalyticRemoteDatasource _datasource; + ReportBloc(this._datasource) : super(ReportState.initial()) { + on<_Get>((event, emit) async { + emit(_Loading()); + + final outlet = await OutletLocalDatasource().get(); + + final category = await _datasource.getCategory( + dateFrom: event.startDate, dateTo: event.endDate); + + final product = await _datasource.getProduct( + dateFrom: event.startDate, dateTo: event.endDate); + + final paymentMethod = await _datasource.getPaymentMethod( + dateFrom: event.startDate, dateTo: event.endDate); + + final profitLoss = await _datasource.getProfitLoss( + dateFrom: event.startDate, dateTo: event.endDate); + + if (category.isLeft() || + product.isLeft() || + paymentMethod.isLeft() || + profitLoss.isLeft()) { + emit(_Error()); + } + + emit(_Loaded( + outlet, + category + .getOrElse( + () => CategoryAnalyticResponseModel(success: false, data: null)) + .data!, + profitLoss + .getOrElse( + () => ProfitLossResponseModel(success: false, data: null)) + .data, + paymentMethod + .getOrElse(() => + PaymentMethodAnalyticResponseModel(success: false, data: null)) + .data, + product + .getOrElse( + () => ProductAnalyticResponseModel(success: false, data: null)) + .data, + )); + }); + } +} diff --git a/lib/presentation/report/blocs/report/report_bloc.freezed.dart b/lib/presentation/report/blocs/report/report_bloc.freezed.dart new file mode 100644 index 0000000..13c0c41 --- /dev/null +++ b/lib/presentation/report/blocs/report/report_bloc.freezed.dart @@ -0,0 +1,975 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'report_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$ReportEvent { + DateTime get startDate => throw _privateConstructorUsedError; + DateTime get endDate => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function(DateTime startDate, DateTime endDate) get, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(DateTime startDate, DateTime endDate)? get, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(DateTime startDate, DateTime endDate)? get, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Get value) get, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Get value)? get, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Get value)? get, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + /// Create a copy of ReportEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ReportEventCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ReportEventCopyWith<$Res> { + factory $ReportEventCopyWith( + ReportEvent value, $Res Function(ReportEvent) then) = + _$ReportEventCopyWithImpl<$Res, ReportEvent>; + @useResult + $Res call({DateTime startDate, DateTime endDate}); +} + +/// @nodoc +class _$ReportEventCopyWithImpl<$Res, $Val extends ReportEvent> + implements $ReportEventCopyWith<$Res> { + _$ReportEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ReportEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? startDate = null, + Object? endDate = null, + }) { + return _then(_value.copyWith( + startDate: null == startDate + ? _value.startDate + : startDate // ignore: cast_nullable_to_non_nullable + as DateTime, + endDate: null == endDate + ? _value.endDate + : endDate // ignore: cast_nullable_to_non_nullable + as DateTime, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$GetImplCopyWith<$Res> implements $ReportEventCopyWith<$Res> { + factory _$$GetImplCopyWith(_$GetImpl value, $Res Function(_$GetImpl) then) = + __$$GetImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({DateTime startDate, DateTime endDate}); +} + +/// @nodoc +class __$$GetImplCopyWithImpl<$Res> + extends _$ReportEventCopyWithImpl<$Res, _$GetImpl> + implements _$$GetImplCopyWith<$Res> { + __$$GetImplCopyWithImpl(_$GetImpl _value, $Res Function(_$GetImpl) _then) + : super(_value, _then); + + /// Create a copy of ReportEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? startDate = null, + Object? endDate = null, + }) { + return _then(_$GetImpl( + startDate: null == startDate + ? _value.startDate + : startDate // ignore: cast_nullable_to_non_nullable + as DateTime, + endDate: null == endDate + ? _value.endDate + : endDate // ignore: cast_nullable_to_non_nullable + as DateTime, + )); + } +} + +/// @nodoc + +class _$GetImpl implements _Get { + const _$GetImpl({required this.startDate, required this.endDate}); + + @override + final DateTime startDate; + @override + final DateTime endDate; + + @override + String toString() { + return 'ReportEvent.get(startDate: $startDate, endDate: $endDate)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$GetImpl && + (identical(other.startDate, startDate) || + other.startDate == startDate) && + (identical(other.endDate, endDate) || other.endDate == endDate)); + } + + @override + int get hashCode => Object.hash(runtimeType, startDate, endDate); + + /// Create a copy of ReportEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$GetImplCopyWith<_$GetImpl> get copyWith => + __$$GetImplCopyWithImpl<_$GetImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(DateTime startDate, DateTime endDate) get, + }) { + return get(startDate, endDate); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(DateTime startDate, DateTime endDate)? get, + }) { + return get?.call(startDate, endDate); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(DateTime startDate, DateTime endDate)? get, + required TResult orElse(), + }) { + if (get != null) { + return get(startDate, endDate); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Get value) get, + }) { + return get(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Get value)? get, + }) { + return get?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Get value)? get, + required TResult orElse(), + }) { + if (get != null) { + return get(this); + } + return orElse(); + } +} + +abstract class _Get implements ReportEvent { + const factory _Get( + {required final DateTime startDate, + required final DateTime endDate}) = _$GetImpl; + + @override + DateTime get startDate; + @override + DateTime get endDate; + + /// Create a copy of ReportEvent + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$GetImplCopyWith<_$GetImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$ReportState { + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function( + Outlet outlet, + CategoryAnalyticData? categoryAnalyticData, + ProfitLossData? profitLossData, + PaymentMethodAnalyticData? paymentMethodAnalyticData, + ProductAnalyticData? productAnalyticData) + loaded, + required TResult Function() error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function( + Outlet outlet, + CategoryAnalyticData? categoryAnalyticData, + ProfitLossData? profitLossData, + PaymentMethodAnalyticData? paymentMethodAnalyticData, + ProductAnalyticData? productAnalyticData)? + loaded, + TResult? Function()? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function( + Outlet outlet, + CategoryAnalyticData? categoryAnalyticData, + ProfitLossData? profitLossData, + PaymentMethodAnalyticData? paymentMethodAnalyticData, + ProductAnalyticData? productAnalyticData)? + loaded, + TResult Function()? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_Error value) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_Error value)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_Error value)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ReportStateCopyWith<$Res> { + factory $ReportStateCopyWith( + ReportState value, $Res Function(ReportState) then) = + _$ReportStateCopyWithImpl<$Res, ReportState>; +} + +/// @nodoc +class _$ReportStateCopyWithImpl<$Res, $Val extends ReportState> + implements $ReportStateCopyWith<$Res> { + _$ReportStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ReportState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc +abstract class _$$InitialImplCopyWith<$Res> { + factory _$$InitialImplCopyWith( + _$InitialImpl value, $Res Function(_$InitialImpl) then) = + __$$InitialImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$InitialImplCopyWithImpl<$Res> + extends _$ReportStateCopyWithImpl<$Res, _$InitialImpl> + implements _$$InitialImplCopyWith<$Res> { + __$$InitialImplCopyWithImpl( + _$InitialImpl _value, $Res Function(_$InitialImpl) _then) + : super(_value, _then); + + /// Create a copy of ReportState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$InitialImpl implements _Initial { + const _$InitialImpl(); + + @override + String toString() { + return 'ReportState.initial()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$InitialImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function( + Outlet outlet, + CategoryAnalyticData? categoryAnalyticData, + ProfitLossData? profitLossData, + PaymentMethodAnalyticData? paymentMethodAnalyticData, + ProductAnalyticData? productAnalyticData) + loaded, + required TResult Function() error, + }) { + return initial(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function( + Outlet outlet, + CategoryAnalyticData? categoryAnalyticData, + ProfitLossData? profitLossData, + PaymentMethodAnalyticData? paymentMethodAnalyticData, + ProductAnalyticData? productAnalyticData)? + loaded, + TResult? Function()? error, + }) { + return initial?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function( + Outlet outlet, + CategoryAnalyticData? categoryAnalyticData, + ProfitLossData? profitLossData, + PaymentMethodAnalyticData? paymentMethodAnalyticData, + ProductAnalyticData? productAnalyticData)? + loaded, + TResult Function()? error, + required TResult orElse(), + }) { + if (initial != null) { + return initial(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_Error value) error, + }) { + return initial(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_Error value)? error, + }) { + return initial?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_Error value)? error, + required TResult orElse(), + }) { + if (initial != null) { + return initial(this); + } + return orElse(); + } +} + +abstract class _Initial implements ReportState { + const factory _Initial() = _$InitialImpl; +} + +/// @nodoc +abstract class _$$LoadingImplCopyWith<$Res> { + factory _$$LoadingImplCopyWith( + _$LoadingImpl value, $Res Function(_$LoadingImpl) then) = + __$$LoadingImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$LoadingImplCopyWithImpl<$Res> + extends _$ReportStateCopyWithImpl<$Res, _$LoadingImpl> + implements _$$LoadingImplCopyWith<$Res> { + __$$LoadingImplCopyWithImpl( + _$LoadingImpl _value, $Res Function(_$LoadingImpl) _then) + : super(_value, _then); + + /// Create a copy of ReportState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$LoadingImpl implements _Loading { + const _$LoadingImpl(); + + @override + String toString() { + return 'ReportState.loading()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$LoadingImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function( + Outlet outlet, + CategoryAnalyticData? categoryAnalyticData, + ProfitLossData? profitLossData, + PaymentMethodAnalyticData? paymentMethodAnalyticData, + ProductAnalyticData? productAnalyticData) + loaded, + required TResult Function() error, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function( + Outlet outlet, + CategoryAnalyticData? categoryAnalyticData, + ProfitLossData? profitLossData, + PaymentMethodAnalyticData? paymentMethodAnalyticData, + ProductAnalyticData? productAnalyticData)? + loaded, + TResult? Function()? error, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function( + Outlet outlet, + CategoryAnalyticData? categoryAnalyticData, + ProfitLossData? profitLossData, + PaymentMethodAnalyticData? paymentMethodAnalyticData, + ProductAnalyticData? productAnalyticData)? + loaded, + TResult Function()? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_Error value) error, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_Error value)? error, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_Error value)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class _Loading implements ReportState { + const factory _Loading() = _$LoadingImpl; +} + +/// @nodoc +abstract class _$$LoadedImplCopyWith<$Res> { + factory _$$LoadedImplCopyWith( + _$LoadedImpl value, $Res Function(_$LoadedImpl) then) = + __$$LoadedImplCopyWithImpl<$Res>; + @useResult + $Res call( + {Outlet outlet, + CategoryAnalyticData? categoryAnalyticData, + ProfitLossData? profitLossData, + PaymentMethodAnalyticData? paymentMethodAnalyticData, + ProductAnalyticData? productAnalyticData}); +} + +/// @nodoc +class __$$LoadedImplCopyWithImpl<$Res> + extends _$ReportStateCopyWithImpl<$Res, _$LoadedImpl> + implements _$$LoadedImplCopyWith<$Res> { + __$$LoadedImplCopyWithImpl( + _$LoadedImpl _value, $Res Function(_$LoadedImpl) _then) + : super(_value, _then); + + /// Create a copy of ReportState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? outlet = null, + Object? categoryAnalyticData = freezed, + Object? profitLossData = freezed, + Object? paymentMethodAnalyticData = freezed, + Object? productAnalyticData = freezed, + }) { + return _then(_$LoadedImpl( + null == outlet + ? _value.outlet + : outlet // ignore: cast_nullable_to_non_nullable + as Outlet, + freezed == categoryAnalyticData + ? _value.categoryAnalyticData + : categoryAnalyticData // ignore: cast_nullable_to_non_nullable + as CategoryAnalyticData?, + freezed == profitLossData + ? _value.profitLossData + : profitLossData // ignore: cast_nullable_to_non_nullable + as ProfitLossData?, + freezed == paymentMethodAnalyticData + ? _value.paymentMethodAnalyticData + : paymentMethodAnalyticData // ignore: cast_nullable_to_non_nullable + as PaymentMethodAnalyticData?, + freezed == productAnalyticData + ? _value.productAnalyticData + : productAnalyticData // ignore: cast_nullable_to_non_nullable + as ProductAnalyticData?, + )); + } +} + +/// @nodoc + +class _$LoadedImpl implements _Loaded { + const _$LoadedImpl( + this.outlet, + this.categoryAnalyticData, + this.profitLossData, + this.paymentMethodAnalyticData, + this.productAnalyticData); + + @override + final Outlet outlet; + @override + final CategoryAnalyticData? categoryAnalyticData; + @override + final ProfitLossData? profitLossData; + @override + final PaymentMethodAnalyticData? paymentMethodAnalyticData; + @override + final ProductAnalyticData? productAnalyticData; + + @override + String toString() { + return 'ReportState.loaded(outlet: $outlet, categoryAnalyticData: $categoryAnalyticData, profitLossData: $profitLossData, paymentMethodAnalyticData: $paymentMethodAnalyticData, productAnalyticData: $productAnalyticData)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LoadedImpl && + (identical(other.outlet, outlet) || other.outlet == outlet) && + (identical(other.categoryAnalyticData, categoryAnalyticData) || + other.categoryAnalyticData == categoryAnalyticData) && + (identical(other.profitLossData, profitLossData) || + other.profitLossData == profitLossData) && + (identical(other.paymentMethodAnalyticData, + paymentMethodAnalyticData) || + other.paymentMethodAnalyticData == paymentMethodAnalyticData) && + (identical(other.productAnalyticData, productAnalyticData) || + other.productAnalyticData == productAnalyticData)); + } + + @override + int get hashCode => Object.hash(runtimeType, outlet, categoryAnalyticData, + profitLossData, paymentMethodAnalyticData, productAnalyticData); + + /// Create a copy of ReportState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$LoadedImplCopyWith<_$LoadedImpl> get copyWith => + __$$LoadedImplCopyWithImpl<_$LoadedImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function( + Outlet outlet, + CategoryAnalyticData? categoryAnalyticData, + ProfitLossData? profitLossData, + PaymentMethodAnalyticData? paymentMethodAnalyticData, + ProductAnalyticData? productAnalyticData) + loaded, + required TResult Function() error, + }) { + return loaded(outlet, categoryAnalyticData, profitLossData, + paymentMethodAnalyticData, productAnalyticData); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function( + Outlet outlet, + CategoryAnalyticData? categoryAnalyticData, + ProfitLossData? profitLossData, + PaymentMethodAnalyticData? paymentMethodAnalyticData, + ProductAnalyticData? productAnalyticData)? + loaded, + TResult? Function()? error, + }) { + return loaded?.call(outlet, categoryAnalyticData, profitLossData, + paymentMethodAnalyticData, productAnalyticData); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function( + Outlet outlet, + CategoryAnalyticData? categoryAnalyticData, + ProfitLossData? profitLossData, + PaymentMethodAnalyticData? paymentMethodAnalyticData, + ProductAnalyticData? productAnalyticData)? + loaded, + TResult Function()? error, + required TResult orElse(), + }) { + if (loaded != null) { + return loaded(outlet, categoryAnalyticData, profitLossData, + paymentMethodAnalyticData, productAnalyticData); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_Error value) error, + }) { + return loaded(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_Error value)? error, + }) { + return loaded?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_Error value)? error, + required TResult orElse(), + }) { + if (loaded != null) { + return loaded(this); + } + return orElse(); + } +} + +abstract class _Loaded implements ReportState { + const factory _Loaded( + final Outlet outlet, + final CategoryAnalyticData? categoryAnalyticData, + final ProfitLossData? profitLossData, + final PaymentMethodAnalyticData? paymentMethodAnalyticData, + final ProductAnalyticData? productAnalyticData) = _$LoadedImpl; + + Outlet get outlet; + CategoryAnalyticData? get categoryAnalyticData; + ProfitLossData? get profitLossData; + PaymentMethodAnalyticData? get paymentMethodAnalyticData; + ProductAnalyticData? get productAnalyticData; + + /// Create a copy of ReportState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$LoadedImplCopyWith<_$LoadedImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$ErrorImplCopyWith<$Res> { + factory _$$ErrorImplCopyWith( + _$ErrorImpl value, $Res Function(_$ErrorImpl) then) = + __$$ErrorImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$ErrorImplCopyWithImpl<$Res> + extends _$ReportStateCopyWithImpl<$Res, _$ErrorImpl> + implements _$$ErrorImplCopyWith<$Res> { + __$$ErrorImplCopyWithImpl( + _$ErrorImpl _value, $Res Function(_$ErrorImpl) _then) + : super(_value, _then); + + /// Create a copy of ReportState + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$ErrorImpl implements _Error { + const _$ErrorImpl(); + + @override + String toString() { + return 'ReportState.error()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$ErrorImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function( + Outlet outlet, + CategoryAnalyticData? categoryAnalyticData, + ProfitLossData? profitLossData, + PaymentMethodAnalyticData? paymentMethodAnalyticData, + ProductAnalyticData? productAnalyticData) + loaded, + required TResult Function() error, + }) { + return error(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function( + Outlet outlet, + CategoryAnalyticData? categoryAnalyticData, + ProfitLossData? profitLossData, + PaymentMethodAnalyticData? paymentMethodAnalyticData, + ProductAnalyticData? productAnalyticData)? + loaded, + TResult? Function()? error, + }) { + return error?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function( + Outlet outlet, + CategoryAnalyticData? categoryAnalyticData, + ProfitLossData? profitLossData, + PaymentMethodAnalyticData? paymentMethodAnalyticData, + ProductAnalyticData? productAnalyticData)? + loaded, + TResult Function()? error, + required TResult orElse(), + }) { + if (error != null) { + return error(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_Error value) error, + }) { + return error(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_Error value)? error, + }) { + return error?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_Error value)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(this); + } + return orElse(); + } +} + +abstract class _Error implements ReportState { + const factory _Error() = _$ErrorImpl; +} diff --git a/lib/presentation/report/blocs/report/report_event.dart b/lib/presentation/report/blocs/report/report_event.dart new file mode 100644 index 0000000..d60c94d --- /dev/null +++ b/lib/presentation/report/blocs/report/report_event.dart @@ -0,0 +1,9 @@ +part of 'report_bloc.dart'; + +@freezed +class ReportEvent with _$ReportEvent { + const factory ReportEvent.get({ + required DateTime startDate, + required DateTime endDate, + }) = _Get; +} diff --git a/lib/presentation/report/blocs/report/report_state.dart b/lib/presentation/report/blocs/report/report_state.dart new file mode 100644 index 0000000..526162f --- /dev/null +++ b/lib/presentation/report/blocs/report/report_state.dart @@ -0,0 +1,15 @@ +part of 'report_bloc.dart'; + +@freezed +class ReportState with _$ReportState { + const factory ReportState.initial() = _Initial; + const factory ReportState.loading() = _Loading; + const factory ReportState.loaded( + Outlet outlet, + CategoryAnalyticData? categoryAnalyticData, + ProfitLossData? profitLossData, + PaymentMethodAnalyticData? paymentMethodAnalyticData, + ProductAnalyticData? productAnalyticData, + ) = _Loaded; + const factory ReportState.error() = _Error; +} diff --git a/lib/presentation/report/pages/report_page.dart b/lib/presentation/report/pages/report_page.dart index 7b50923..fd94ad5 100644 --- a/lib/presentation/report/pages/report_page.dart +++ b/lib/presentation/report/pages/report_page.dart @@ -1,7 +1,11 @@ import 'dart:developer'; import 'package:enaklo_pos/core/extensions/build_context_ext.dart'; +import 'package:enaklo_pos/core/utils/helper_pdf_service.dart'; +import 'package:enaklo_pos/core/utils/permession_handler.dart'; +import 'package:enaklo_pos/core/utils/transaction_report.dart'; import 'package:enaklo_pos/presentation/report/blocs/profit_loss/profit_loss_bloc.dart'; +import 'package:enaklo_pos/presentation/report/blocs/report/report_bloc.dart'; import 'package:enaklo_pos/presentation/report/widgets/dashboard_analytic_widget.dart'; import 'package:enaklo_pos/presentation/report/widgets/profit_loss_widget.dart'; import 'package:enaklo_pos/presentation/sales/pages/sales_page.dart'; @@ -44,6 +48,9 @@ class _ReportPageState extends State { context.read().add( SummaryEvent.getSummary(fromDate, toDate), ); + context.read().add( + ReportEvent.get(startDate: fromDate, endDate: toDate), + ); } @override @@ -64,12 +71,14 @@ class _ReportPageState extends State { initialDate: fromDate, onDateSelected: (selectedDate) { fromDate = selectedDate; - + context.read().add( + ReportEvent.get(startDate: fromDate, endDate: toDate), + ); setState(() {}); }, ), ), - const SpaceWidth(24.0), + const SpaceWidth(12.0), SizedBox( width: 300, child: CustomDatePicker( @@ -77,6 +86,9 @@ class _ReportPageState extends State { initialDate: toDate, onDateSelected: (selectedDate) { toDate = selectedDate; + context.read().add( + ReportEvent.get(startDate: fromDate, endDate: toDate), + ); setState(() {}); // context.read().add( // TransactionReportEvent.getReport( @@ -97,6 +109,72 @@ class _ReportPageState extends State { }, ), ), + const SpaceWidth(12.0), + BlocBuilder( + builder: (context, state) { + return state.maybeWhen( + orElse: () => SizedBox.shrink(), + loading: () => SizedBox( + height: 24, + width: 24, + child: const CircularProgressIndicator(), + ), + loaded: (outlet, categoryAnalyticData, profitLossData, + paymentMethodAnalyticData, productAnalyticData) => + InkWell( + onTap: () async { + try { + final status = + await PermessionHelper().checkPermission(); + if (status) { + final pdfFile = await TransactionReport.previewPdf( + outlet: outlet, + searchDateFormatted: searchDateFormatted, + categoryAnalyticData: categoryAnalyticData, + profitLossData: profitLossData, + paymentMethodAnalyticData: + paymentMethodAnalyticData, + productAnalyticData: productAnalyticData, + ); + 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, + ), + ); + } + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 12.0, vertical: 8.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6.0), + border: Border.all( + color: AppColors.stroke, + )), + child: Icon( + Icons.download, + color: AppColors.primary, + size: 28, + ), + ), + ), + ); + }, + ), ], ), Expanded( diff --git a/lib/presentation/report/widgets/report_title.dart b/lib/presentation/report/widgets/report_title.dart index 7268dfe..ea5ef72 100644 --- a/lib/presentation/report/widgets/report_title.dart +++ b/lib/presentation/report/widgets/report_title.dart @@ -51,6 +51,8 @@ class ReportTitle extends StatelessWidget { ), if (actionWidget != null) Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, children: actionWidget!, ), ],