From 07c0bdb3affa1c083f3bbd49d571941a885a17a1 Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 25 Sep 2025 22:52:09 +0700 Subject: [PATCH] Sales Report PDF --- .../apps/report/sales/sales-report/page.tsx | 18 + .../export/pdf/PDFExportSalesService.ts | 686 ++++++++++++++++++ src/views/apps/report/ReportSalesList.tsx | 5 + .../sales/sales-report/ReportSalesContent.tsx | 389 ++++++++++ 4 files changed, 1098 insertions(+) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/report/sales/sales-report/page.tsx create mode 100644 src/services/export/pdf/PDFExportSalesService.ts create mode 100644 src/views/apps/report/sales/sales-report/ReportSalesContent.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/report/sales/sales-report/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/report/sales/sales-report/page.tsx new file mode 100644 index 0000000..229116c --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/report/sales/sales-report/page.tsx @@ -0,0 +1,18 @@ +import ReportTitle from '@/components/report/ReportTitle' +import ReportSalesContent from '@/views/apps/report/sales/sales-report/ReportSalesContent' +import Grid from '@mui/material/Grid2' + +const SalesReportPage = () => { + return ( + + + + + + + + + ) +} + +export default SalesReportPage diff --git a/src/services/export/pdf/PDFExportSalesService.ts b/src/services/export/pdf/PDFExportSalesService.ts new file mode 100644 index 0000000..c9f15e7 --- /dev/null +++ b/src/services/export/pdf/PDFExportSalesService.ts @@ -0,0 +1,686 @@ +import { CategoryReport, PaymentReport, ProductSalesReport, ProfitLossReport } from '@/types/services/analytic' + +export interface SalesReportData { + profitLoss: ProfitLossReport + paymentAnalytics: PaymentReport + categoryAnalytics: CategoryReport + productAnalytics: ProductSalesReport +} + +export class PDFExportSalesService { + /** + * Export Sales Report to PDF + */ + static async exportSalesReportToPDF(salesData: SalesReportData, filename?: string) { + try { + // Dynamic import untuk jsPDF + const jsPDFModule = await import('jspdf') + const jsPDF = jsPDFModule.default + + // Create new PDF document - PORTRAIT A4 + const pdf = new jsPDF('p', 'mm', 'a4') + + // Add content + await this.addSalesReportContent(pdf, salesData) + + // Generate filename + const exportFilename = filename || this.generateFilename('Laporan_Transaksi', 'pdf') + + // Save PDF + pdf.save(exportFilename) + + return { success: true, filename: exportFilename } + } catch (error) { + console.error('Error exporting sales report to PDF:', error) + return { success: false, error: `PDF export failed: ${(error as Error).message}` } + } + } + + /** + * Add sales report content to PDF + */ + private static async addSalesReportContent(pdf: any, salesData: SalesReportData) { + let yPos = 20 + const pageWidth = pdf.internal.pageSize.getWidth() + const pageHeight = pdf.internal.pageSize.getHeight() + const marginLeft = 20 + const marginRight = 20 + const marginBottom = 15 + + // Helper function to check page break + const checkPageBreak = (neededSpace: number) => { + if (yPos + neededSpace > pageHeight - marginBottom) { + pdf.addPage() + yPos = 20 + return true + } + return false + } + + // Title + yPos = this.addReportTitle(pdf, salesData.profitLoss, yPos, pageWidth, marginLeft, marginRight) + + // Section 1: Ringkasan + checkPageBreak(60) + yPos = this.addRingkasanSection(pdf, salesData.profitLoss, yPos, pageWidth, marginLeft, marginRight, checkPageBreak) + + // Section 2: Invoice Summary + checkPageBreak(50) + yPos = this.addInvoiceSection(pdf, salesData.profitLoss, yPos, pageWidth, marginLeft, marginRight, checkPageBreak) + + // Section 3: Payment Methods + checkPageBreak(120) + yPos = this.addPaymentMethodsSection( + pdf, + salesData.paymentAnalytics, + yPos, + pageWidth, + marginLeft, + marginRight, + checkPageBreak + ) + + // Section 4: Category Summary + checkPageBreak(120) + yPos = this.addCategorySection( + pdf, + salesData.categoryAnalytics, + yPos, + pageWidth, + marginLeft, + marginRight, + checkPageBreak + ) + + // Section 5: Product Summary + checkPageBreak(150) + yPos = this.addProductSection( + pdf, + salesData.productAnalytics, + yPos, + pageWidth, + marginLeft, + marginRight, + checkPageBreak + ) + } + + /** + * Add report title + */ + private static addReportTitle( + pdf: any, + profitLoss: ProfitLossReport, + startY: number, + pageWidth: number, + marginLeft: number, + marginRight: number + ): number { + let yPos = startY + + // Title + pdf.setFontSize(20) + pdf.setFont('helvetica', 'bold') + pdf.setTextColor(0, 0, 0) + pdf.text('Laporan Transaksi', pageWidth / 2, yPos, { align: 'center' }) + yPos += 10 + + // Period + pdf.setFontSize(12) + pdf.setFont('helvetica', 'normal') + const periodText = `${profitLoss.date_from.split('T')[0]} - ${profitLoss.date_to.split('T')[0]}` + pdf.text(periodText, pageWidth / 2, yPos, { align: 'center' }) + yPos += 10 + + // Purple line separator + pdf.setDrawColor(102, 45, 145) + pdf.setLineWidth(2) + pdf.line(marginLeft, yPos, pageWidth - marginRight, yPos) + yPos += 15 + + return yPos + } + + /** + * Add Ringkasan section + */ + private static addRingkasanSection( + pdf: any, + profitLoss: ProfitLossReport, + startY: number, + pageWidth: number, + marginLeft: number, + marginRight: number, + checkPageBreak: (space: number) => boolean + ): number { + let yPos = startY + + // Section title + pdf.setFontSize(16) + pdf.setFont('helvetica', 'bold') + pdf.setTextColor(102, 45, 145) + pdf.text('Ringkasan', marginLeft, yPos) + yPos += 10 + + // Reset text color + pdf.setTextColor(0, 0, 0) + pdf.setFontSize(11) + + const ringkasanItems = [ + { label: 'Total Penjualan', value: this.formatCurrency(profitLoss.summary.total_revenue), bold: false }, + { label: 'Total Diskon', value: this.formatCurrency(profitLoss.summary.total_discount), bold: false }, + { label: 'Total Pajak', value: this.formatCurrency(profitLoss.summary.total_tax), bold: false }, + { label: 'Total', value: this.formatCurrency(profitLoss.summary.total_revenue), bold: true } + ] + + ringkasanItems.forEach((item, index) => { + if (checkPageBreak(15)) yPos = 20 + + // Set font weight + pdf.setFont('helvetica', item.bold ? 'bold' : 'normal') + + pdf.text(item.label, marginLeft, yPos) + pdf.text(item.value, pageWidth - marginRight, yPos, { align: 'right' }) + + // Light separator line (except for total row) + if (!item.bold) { + pdf.setDrawColor(230, 230, 230) + pdf.setLineWidth(0.5) + pdf.line(marginLeft, yPos + 2, pageWidth - marginRight, yPos + 2) + } + + yPos += 8 + }) + + return yPos + 10 + } + + /** + * Add Invoice section + */ + private static addInvoiceSection( + pdf: any, + profitLoss: ProfitLossReport, + startY: number, + pageWidth: number, + marginLeft: number, + marginRight: number, + checkPageBreak: (space: number) => boolean + ): number { + let yPos = startY + + // Section title + pdf.setFontSize(16) + pdf.setFont('helvetica', 'bold') + pdf.setTextColor(102, 45, 145) + pdf.text('Invoice', marginLeft, yPos) + yPos += 10 + + // Reset formatting + pdf.setTextColor(0, 0, 0) + pdf.setFontSize(11) + + const invoiceItems = [ + { label: 'Total Invoice', value: profitLoss.summary.total_orders.toString() }, + { label: 'Rata-rata Tagihan Per Invoice', value: this.formatCurrency(profitLoss.summary.average_profit) } + ] + + invoiceItems.forEach((item, index) => { + if (checkPageBreak(15)) yPos = 20 + + pdf.setFont('helvetica', 'normal') + pdf.text(item.label, marginLeft, yPos) + pdf.text(item.value, pageWidth - marginRight, yPos, { align: 'right' }) + + // Light separator line + if (index < invoiceItems.length - 1) { + pdf.setDrawColor(230, 230, 230) + pdf.setLineWidth(0.5) + pdf.line(marginLeft, yPos + 2, pageWidth - marginRight, yPos + 2) + } + + yPos += 8 + }) + + return yPos + 15 + } + + /** + * Add Payment Methods section + */ + private static addPaymentMethodsSection( + pdf: any, + paymentAnalytics: PaymentReport, + startY: number, + pageWidth: number, + marginLeft: number, + marginRight: number, + checkPageBreak: (space: number) => boolean + ): number { + let yPos = startY + + // Section title + pdf.setFontSize(14) + pdf.setFont('helvetica', 'bold') + pdf.setTextColor(102, 45, 145) + pdf.text('Ringkasan Metode Pembayaran', marginLeft, yPos) + yPos += 12 + + // Reset formatting + pdf.setTextColor(0, 0, 0) + + // Table setup + const tableWidth = pageWidth - marginLeft - marginRight + const colWidths = [50, 25, 30, 35, 25] // Method, Type, Order, Amount, % + let currentX = marginLeft + + // Table header + pdf.setFillColor(240, 240, 240) + pdf.rect(marginLeft, yPos, tableWidth, 10, 'F') + + pdf.setFont('helvetica', 'bold') + pdf.setFontSize(9) + + const headers = ['Metode Pembayaran', 'Tipe', 'Jumlah Order', 'Total Amount', 'Persentase'] + currentX = marginLeft + + headers.forEach((header, index) => { + if (index === 0) { + pdf.text(header, currentX + 2, yPos + 6) + } else { + pdf.text(header, currentX + colWidths[index] / 2, yPos + 6, { align: 'center' }) + } + currentX += colWidths[index] + }) + + yPos += 12 + + // Table rows + pdf.setFont('helvetica', 'normal') + pdf.setFontSize(9) + + paymentAnalytics.data?.forEach((payment, index) => { + if (checkPageBreak(10)) yPos = 20 + + currentX = marginLeft + pdf.setFont('helvetica', 'normal') + pdf.setFontSize(9) + pdf.setTextColor(0, 0, 0) + + // Method name + pdf.text(payment.payment_method_name, currentX + 2, yPos + 5) + currentX += colWidths[0] + + // Type with simple color coding + const typeText = payment.payment_method_type.toUpperCase() + if (payment.payment_method_type === 'cash') { + pdf.setTextColor(0, 120, 0) // Green + } else if (payment.payment_method_type === 'card') { + pdf.setTextColor(0, 80, 200) // Blue + } else { + pdf.setTextColor(200, 100, 0) // Orange + } + pdf.text(typeText, currentX + colWidths[1] / 2, yPos + 5, { align: 'center' }) + pdf.setTextColor(0, 0, 0) // Reset color + currentX += colWidths[1] + + // Order count + pdf.text(payment.order_count.toString(), currentX + colWidths[2] / 2, yPos + 5, { align: 'center' }) + currentX += colWidths[2] + + // Amount + pdf.text(this.formatCurrency(payment.total_amount), currentX + colWidths[3] - 2, yPos + 5, { align: 'right' }) + currentX += colWidths[3] + + // Percentage + pdf.text(`${(payment.percentage ?? 0).toFixed(1)}%`, currentX + colWidths[4] / 2, yPos + 5, { align: 'center' }) + + // Draw bottom border line + pdf.setDrawColor(230, 230, 230) + pdf.setLineWidth(0.3) + pdf.line(marginLeft, yPos + 8, marginLeft + tableWidth, yPos + 8) + + yPos += 10 + }) + + // Table footer (Total) - directly after last row + pdf.setFillColor(245, 245, 245) // Lighter gray + pdf.rect(marginLeft, yPos, tableWidth, 10, 'F') + + pdf.setFont('helvetica', 'bold') + pdf.setFontSize(9) + + currentX = marginLeft + pdf.text('TOTAL', currentX + 2, yPos + 6) + currentX += colWidths[0] + colWidths[1] + + pdf.text((paymentAnalytics.summary?.total_orders ?? 0).toString(), currentX + colWidths[2] / 2, yPos + 6, { + align: 'center' + }) + currentX += colWidths[2] + + pdf.text(this.formatCurrency(paymentAnalytics.summary?.total_amount ?? 0), currentX + colWidths[3] - 2, yPos + 6, { + align: 'right' + }) + + return yPos + 25 + } + + /** + * Add Category section + */ + private static addCategorySection( + pdf: any, + categoryAnalytics: CategoryReport, + startY: number, + pageWidth: number, + marginLeft: number, + marginRight: number, + checkPageBreak: (space: number) => boolean + ): number { + let yPos = startY + + // Section title + pdf.setFontSize(16) + pdf.setFont('helvetica', 'bold') + pdf.setTextColor(102, 45, 145) + pdf.text('Ringkasan Kategori', marginLeft, yPos) + yPos += 15 + + // Reset formatting + pdf.setTextColor(0, 0, 0) + + // Table setup + const tableWidth = pageWidth - marginLeft - marginRight + const colWidths = [50, 30, 25, 35] // Name, Products, Qty, Revenue + let currentX = marginLeft + + // Table header + pdf.setFillColor(240, 240, 240) + pdf.rect(marginLeft, yPos - 3, tableWidth, 12, 'F') + + pdf.setFont('helvetica', 'bold') + pdf.setFontSize(10) + + const headers = ['Nama', 'Total Produk', 'Qty', 'Pendapatan'] + currentX = marginLeft + + headers.forEach((header, index) => { + if (index === 0) { + pdf.text(header, currentX + 2, yPos + 3) + } else { + pdf.text(header, currentX + colWidths[index] - 2, yPos + 3, { align: 'right' }) + } + currentX += colWidths[index] + }) + + yPos += 15 + + // Calculate summaries + const categorySummary = { + totalRevenue: categoryAnalytics.data?.reduce((sum, item) => sum + (item?.total_revenue || 0), 0) || 0, + productCount: categoryAnalytics.data?.reduce((sum, item) => sum + (item?.product_count || 0), 0) || 0, + totalQuantity: categoryAnalytics.data?.reduce((sum, item) => sum + (item?.total_quantity || 0), 0) || 0 + } + + // Table rows + pdf.setFont('helvetica', 'normal') + pdf.setFontSize(9) + + categoryAnalytics.data?.forEach((category, index) => { + if (checkPageBreak(12)) yPos = 20 + + // Row background (alternating) + if (index % 2 === 1) { + pdf.setFillColor(250, 250, 250) + pdf.rect(marginLeft, yPos - 3, tableWidth, 10, 'F') + } + + currentX = marginLeft + + // Category name + pdf.text(category.category_name, currentX + 2, yPos + 2) + currentX += colWidths[0] + + // Product count + pdf.text(category.product_count.toString(), currentX + colWidths[1] - 2, yPos + 2, { align: 'right' }) + currentX += colWidths[1] + + // Quantity + pdf.text(category.total_quantity.toString(), currentX + colWidths[2] - 2, yPos + 2, { align: 'right' }) + currentX += colWidths[2] + + // Revenue + pdf.text(this.formatCurrency(category.total_revenue), currentX + colWidths[3] - 2, yPos + 2, { align: 'right' }) + + yPos += 10 + }) + + // Table footer (Total) + if (checkPageBreak(12)) yPos = 20 + + pdf.setFillColor(220, 220, 220) + pdf.rect(marginLeft, yPos - 3, tableWidth, 12, 'F') + + pdf.setFont('helvetica', 'bold') + pdf.setFontSize(10) + + currentX = marginLeft + pdf.text('TOTAL', currentX + 2, yPos + 3) + currentX += colWidths[0] + + pdf.text(categorySummary.productCount.toString(), currentX + colWidths[1] - 2, yPos + 3, { align: 'right' }) + currentX += colWidths[1] + + pdf.text(categorySummary.totalQuantity.toString(), currentX + colWidths[2] - 2, yPos + 3, { align: 'right' }) + currentX += colWidths[2] + + pdf.text(this.formatCurrency(categorySummary.totalRevenue), currentX + colWidths[3] - 2, yPos + 3, { + align: 'right' + }) + + return yPos + 25 + } + + /** + * Add Product section with category grouping + */ + private static addProductSection( + pdf: any, + productAnalytics: ProductSalesReport, + startY: number, + pageWidth: number, + marginLeft: number, + marginRight: number, + checkPageBreak: (space: number) => boolean + ): number { + let yPos = startY + + // Section title + pdf.setFontSize(16) + pdf.setFont('helvetica', 'bold') + pdf.setTextColor(102, 45, 145) + pdf.text('Ringkasan Item', marginLeft, yPos) + yPos += 15 + + // Reset formatting + pdf.setTextColor(0, 0, 0) + + // Table setup + const tableWidth = pageWidth - marginLeft - marginRight + const colWidths = [60, 20, 20, 30, 30] // Product, Qty, Order, Revenue, Average + let currentX = marginLeft + + // Table header + pdf.setFillColor(240, 240, 240) + pdf.rect(marginLeft, yPos - 3, tableWidth, 12, 'F') + + pdf.setFont('helvetica', 'bold') + pdf.setFontSize(10) + + const headers = ['Produk', 'Qty', 'Order', 'Pendapatan', 'Rata Rata'] + currentX = marginLeft + + headers.forEach((header, index) => { + if (index === 0) { + pdf.text(header, currentX + 2, yPos + 3) + } else { + pdf.text(header, currentX + colWidths[index] - 2, yPos + 3, { align: 'right' }) + } + currentX += colWidths[index] + }) + + yPos += 15 + + // Group products by category + const groupedProducts = + productAnalytics.data?.reduce( + (acc, item) => { + const categoryName = item.category_name || 'Tidak Berkategori' + if (!acc[categoryName]) { + acc[categoryName] = [] + } + acc[categoryName].push(item) + return acc + }, + {} as Record + ) || {} + + // Calculate product summary + const productSummary = { + totalQuantitySold: productAnalytics.data?.reduce((sum, item) => sum + (item?.quantity_sold || 0), 0) || 0, + totalRevenue: productAnalytics.data?.reduce((sum, item) => sum + (item?.revenue || 0), 0) || 0, + totalOrders: productAnalytics.data?.reduce((sum, item) => sum + (item?.order_count || 0), 0) || 0 + } + + pdf.setFontSize(9) + + // Render grouped products + Object.keys(groupedProducts) + .sort() + .forEach(categoryName => { + const categoryProducts = groupedProducts[categoryName] + + // Check page break for category header + if (checkPageBreak(15)) yPos = 20 + + // Category header + pdf.setFillColor(230, 230, 230) + pdf.rect(marginLeft, yPos - 3, tableWidth, 12, 'F') + pdf.setFont('helvetica', 'bold') + pdf.setTextColor(102, 45, 145) + pdf.text(categoryName.toUpperCase(), marginLeft + 2, yPos + 3) + pdf.setTextColor(0, 0, 0) + yPos += 12 + + // Category products + categoryProducts.forEach((item, index) => { + if (checkPageBreak(10)) yPos = 20 + + // Row background (alternating within category) + if (index % 2 === 1) { + pdf.setFillColor(250, 250, 250) + pdf.rect(marginLeft, yPos - 2, tableWidth, 8, 'F') + } + + pdf.setFont('helvetica', 'normal') + currentX = marginLeft + + // Product name (indented and truncated if needed) + const productName = + item.product_name.length > 45 ? item.product_name.substring(0, 42) + '...' : item.product_name + + pdf.text(productName, currentX + 5, yPos + 2) // Indented for products + currentX += colWidths[0] + + // Quantity + pdf.text(item.quantity_sold.toString(), currentX + colWidths[1] - 2, yPos + 2, { align: 'right' }) + currentX += colWidths[1] + + // Order count + pdf.text((item.order_count || 0).toString(), currentX + colWidths[2] - 2, yPos + 2, { align: 'right' }) + currentX += colWidths[2] + + // Revenue + pdf.text(this.formatCurrency(item.revenue), currentX + colWidths[3] - 2, yPos + 2, { align: 'right' }) + currentX += colWidths[3] + + // Average price + pdf.text(this.formatCurrency(item.average_price), currentX + colWidths[4] - 2, yPos + 2, { align: 'right' }) + + yPos += 8 + }) + + // Category subtotal + if (checkPageBreak(10)) yPos = 20 + + const categoryTotalQty = categoryProducts.reduce((sum, item) => sum + (item.quantity_sold || 0), 0) + const categoryTotalOrders = categoryProducts.reduce((sum, item) => sum + (item.order_count || 0), 0) + const categoryTotalRevenue = categoryProducts.reduce((sum, item) => sum + (item.revenue || 0), 0) + + pdf.setFillColor(200, 200, 200) + pdf.rect(marginLeft, yPos - 2, tableWidth, 10, 'F') + pdf.setFont('helvetica', 'bold') + + currentX = marginLeft + pdf.text(`Subtotal ${categoryName}`, currentX + 5, yPos + 3) + currentX += colWidths[0] + + pdf.text(categoryTotalQty.toString(), currentX + colWidths[1] - 2, yPos + 3, { align: 'right' }) + currentX += colWidths[1] + + pdf.text(categoryTotalOrders.toString(), currentX + colWidths[2] - 2, yPos + 3, { align: 'right' }) + currentX += colWidths[2] + + pdf.text(this.formatCurrency(categoryTotalRevenue), currentX + colWidths[3] - 2, yPos + 3, { align: 'right' }) + + yPos += 15 + }) + + // Grand total + if (checkPageBreak(12)) yPos = 20 + + pdf.setFillColor(180, 180, 180) + pdf.rect(marginLeft, yPos - 3, tableWidth, 12, 'F') + + pdf.setFont('helvetica', 'bold') + pdf.setFontSize(10) + + currentX = marginLeft + pdf.text('TOTAL KESELURUHAN', currentX + 2, yPos + 3) + currentX += colWidths[0] + + pdf.text(productSummary.totalQuantitySold.toString(), currentX + colWidths[1] - 2, yPos + 3, { align: 'right' }) + currentX += colWidths[1] + + pdf.text(productSummary.totalOrders.toString(), currentX + colWidths[2] - 2, yPos + 3, { align: 'right' }) + currentX += colWidths[2] + + pdf.text(this.formatCurrency(productSummary.totalRevenue), currentX + colWidths[3] - 2, yPos + 3, { + align: 'right' + }) + + return yPos + 20 + } + + /** + * Format currency for display + */ + private static formatCurrency(amount: number): string { + return `Rp ${amount.toLocaleString('id-ID')}` + } + + /** + * Generate filename with timestamp + */ + private static generateFilename(prefix: string, extension: string): string { + const now = new Date() + const year = now.getFullYear() + const month = (now.getMonth() + 1).toString().padStart(2, '0') + const day = now.getDate().toString().padStart(2, '0') + const hour = now.getHours().toString().padStart(2, '0') + const minute = now.getMinutes().toString().padStart(2, '0') + + return `${prefix}_${year}_${month}_${day}_${hour}${minute}.${extension}` + } +} diff --git a/src/views/apps/report/ReportSalesList.tsx b/src/views/apps/report/ReportSalesList.tsx index cdd78cb..36349da 100644 --- a/src/views/apps/report/ReportSalesList.tsx +++ b/src/views/apps/report/ReportSalesList.tsx @@ -11,6 +11,11 @@ const ReportSalesList: React.FC = () => { const { lang: locale } = useParams() const salesReports = [ + { + title: 'Penjualan', + iconClass: 'tabler-receipt-2', + link: getLocalizedUrl(`/apps/report/sales/sales-report`, locale as Locale) + }, { title: 'Detail Penjualan', iconClass: 'tabler-receipt-2', diff --git a/src/views/apps/report/sales/sales-report/ReportSalesContent.tsx b/src/views/apps/report/sales/sales-report/ReportSalesContent.tsx new file mode 100644 index 0000000..6441362 --- /dev/null +++ b/src/views/apps/report/sales/sales-report/ReportSalesContent.tsx @@ -0,0 +1,389 @@ +'use client' + +import DateRangePicker from '@/components/RangeDatePicker' +import { ReportItem, ReportItemFooter, ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem' +import { PDFExportSalesService } from '@/services/export/pdf/PDFExportSalesService' +import { + useCategoryAnalytics, + usePaymentAnalytics, + useProductSalesAnalytics, + useProfitLossAnalytics +} from '@/services/queries/analytics' +import { formatCurrency, formatDateDDMMYYYY } from '@/utils/transform' +import { Button, Card, CardContent, Paper } from '@mui/material' +import { useState } from 'react' + +const ReportSalesContent = () => { + const [startDate, setStartDate] = useState(new Date()) + const [endDate, setEndDate] = useState(new Date()) + + const { data: profitLoss } = useProfitLossAnalytics({ + date_from: formatDateDDMMYYYY(startDate!), + date_to: formatDateDDMMYYYY(endDate!) + }) + + const { data: paymentAnalytics } = usePaymentAnalytics({ + date_from: formatDateDDMMYYYY(startDate!), + date_to: formatDateDDMMYYYY(endDate!) + }) + + const { data: category } = useCategoryAnalytics({ + date_from: formatDateDDMMYYYY(startDate!), + date_to: formatDateDDMMYYYY(endDate!) + }) + + const { data: products } = useProductSalesAnalytics({ + date_from: formatDateDDMMYYYY(startDate!), + date_to: formatDateDDMMYYYY(endDate!) + }) + + const categorySummary = { + totalRevenue: category?.data?.reduce((sum, item) => sum + (item?.total_revenue || 0), 0) || 0, + orderCount: category?.data?.reduce((sum, item) => sum + (item?.order_count || 0), 0) || 0, + productCount: category?.data?.reduce((sum, item) => sum + (item?.product_count || 0), 0) || 0, + totalQuantity: category?.data?.reduce((sum, item) => sum + (item?.total_quantity || 0), 0) || 0 + } + + const productSummary = { + totalQuantitySold: products?.data?.reduce((sum, item) => sum + (item?.quantity_sold || 0), 0) || 0, + totalRevenue: products?.data?.reduce((sum, item) => sum + (item?.revenue || 0), 0) || 0, + totalOrders: products?.data?.reduce((sum, item) => sum + (item?.order_count || 0), 0) || 0 + } + + const handleExportPDF = async () => { + try { + const salesData = { + profitLoss: profitLoss!, + paymentAnalytics: paymentAnalytics!, + categoryAnalytics: category!, + productAnalytics: products! + } + + const result = await PDFExportSalesService.exportSalesReportToPDF(salesData) + + if (result.success) { + console.log('PDF export successful:', result.filename) + // Optional: Show success notification + } else { + console.error('PDF export failed:', result.error) + alert('Export PDF gagal. Silakan coba lagi.') + } + } catch (error) { + console.error('PDF export error:', error) + alert('Terjadi kesalahan saat export PDF.') + } + } + + return ( + +
+
+ + +
+
+ + + {}} + /> + {}} + /> + {}} + /> + + + + {}} + /> + {}} + /> + + +
+ + + + + + + + + + + + {paymentAnalytics?.data?.map((payment, index) => ( + + + + + + + + )) || []} + + + + + + + + + + +
Metode PembayaranTipeJumlah OrderTotal AmountPersentase
{payment.payment_method_name} + + {payment.payment_method_type.toUpperCase()} + + {payment.order_count}{formatCurrency(payment.total_amount)} + {(payment.percentage ?? 0).toFixed(1)}% +
TOTAL{paymentAnalytics?.summary.total_orders ?? 0} + {formatCurrency(paymentAnalytics?.summary.total_amount ?? 0)} +
+
+ + +
+ + + + + + + + + + + {category?.data?.map((c, index) => ( + + + + + + + )) || []} + + + + + + + + + +
NamaTotal ProdukQtyPendapatan
{c.category_name}{c.product_count}{c.total_quantity} + {formatCurrency(c.total_revenue)} +
TOTAL{categorySummary?.productCount ?? 0}{categorySummary?.totalQuantity ?? 0}{formatCurrency(categorySummary?.totalRevenue ?? 0)}
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + {(() => { + // Group products by category + const groupedProducts = + products?.data?.reduce( + (acc, item) => { + const categoryName = item.category_name || 'Tidak Berkategori' + if (!acc[categoryName]) { + acc[categoryName] = [] + } + acc[categoryName].push(item) + return acc + }, + {} as Record + ) || {} + + const rows: JSX.Element[] = [] + let globalIndex = 0 + + // Sort categories alphabetically + Object.keys(groupedProducts) + .sort() + .forEach(categoryName => { + const categoryProducts = groupedProducts[categoryName] + + // Category header row + rows.push( + + + + + + + + ) + + // Product rows for this category + categoryProducts.forEach((item, index) => { + globalIndex++ + rows.push( + + + + + + + + ) + }) + + // Category subtotal row + const categoryTotalQty = categoryProducts.reduce( + (sum, item) => sum + (item.quantity_sold || 0), + 0 + ) + const categoryTotalOrders = categoryProducts.reduce( + (sum, item) => sum + (item.order_count || 0), + 0 + ) + const categoryTotalRevenue = categoryProducts.reduce((sum, item) => sum + (item.revenue || 0), 0) + + rows.push( + + + + + + + + ) + }) + + return rows + })()} + + + + + + + + + + +
ProdukQtyOrderPendapatanRata Rata
+ {categoryName.toUpperCase()} +
+ {item.product_name} + + {item.quantity_sold} + + {item.order_count ?? 0} + + {formatCurrency(item.revenue)} + + {formatCurrency(item.average_price)} +
+ Subtotal {categoryName} + + {categoryTotalQty} + + {categoryTotalOrders} + + {formatCurrency(categoryTotalRevenue)} +
TOTAL KESELURUHAN + {productSummary.totalQuantitySold ?? 0} + + {productSummary.totalOrders ?? 0} + + {formatCurrency(productSummary.totalRevenue ?? 0)} +
+
+
+ +
+
+ ) +} + +export default ReportSalesContent