From 80763acc53b8096b44d64fdd5e0c19877b73c2b8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 11:27:43 +0700 Subject: [PATCH 01/16] refactor(FE): Add utility for PDF badge styles and integrate into reports --- src/components/helper/pdf/utils/pdf-badge.ts | 65 +++++++++++++++++++ .../export/CustomerPaymentExportPDF.tsx | 44 ++++++++----- .../export/CustomerPaymentExportXLSX.tsx | 10 +-- .../finance/export/DebtSupllierExportPDF.tsx | 61 ++++------------- 4 files changed, 111 insertions(+), 69 deletions(-) create mode 100644 src/components/helper/pdf/utils/pdf-badge.ts diff --git a/src/components/helper/pdf/utils/pdf-badge.ts b/src/components/helper/pdf/utils/pdf-badge.ts new file mode 100644 index 00000000..4b26b4eb --- /dev/null +++ b/src/components/helper/pdf/utils/pdf-badge.ts @@ -0,0 +1,65 @@ +export type StatusColor = { + bg: string; + text: string; + border: string; +}; + +// Due status colors (for debt supplier reports) +export const dueStatusColors: Record = { + 'SUDAH JATUH TEMPO': { + bg: '#FEE2E2', + text: '#991B1B', + border: '#F87171', + }, // error/red + 'BELUM JATUH TEMPO': { + bg: '#D1FAE5', + text: '#065F46', + border: '#34D399', + }, // success/green + 'MENDEKATI JATUH TEMPO': { + bg: '#FEF3C7', + text: '#92400E', + border: '#FBBF24', + }, // warning/yellow +}; + +// Payment status colors (for customer payment & debt supplier reports) +export const paymentStatusColors: Record = { + 'BELUM LUNAS': { + bg: '#FEF3C7', + text: '#92400E', + border: '#FBBF24', + }, // warning/yellow + LUNAS: { + bg: '#DBEAFE', + text: '#1E40AF', + border: '#60A5FA', + }, // primary/blue + 'PEMBAYARAN SEBAGIAN': { bg: '#D1FAE5', text: '#065F46', border: '#34D399' }, // success/green + PEMBAYARAN: { + bg: '#D1FAE5', + text: '#065F46', + border: '#34D399', + }, // success/green +}; + +// Fallback color for unknown statuses +export const fallbackStatusColor: StatusColor = { + bg: '#F3F4F6', + text: '#374151', + border: '#D1D5DB', +}; // neutral + +export const getPDFBadgeStyle = ( + statusText: string, + type: 'due' | 'payment' = 'payment' +): StatusColor => { + const normalizedStatus = statusText.toUpperCase().trim(); + + const colors = + type === 'due' + ? dueStatusColors[normalizedStatus] + : paymentStatusColors[normalizedStatus]; + + return colors || fallbackStatusColor; +}; diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx index 61e8792a..1c374e4b 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -9,7 +9,12 @@ import { pdf, } from '@react-pdf/renderer'; -import { formatDate, formatCurrency, formatNumber } from '@/lib/helper'; +import { + formatDate, + formatCurrency, + formatNumber, + formatTitleCase, +} from '@/lib/helper'; import { CustomerPaymentReport } from '@/types/api/report/customer-payment'; import { PdfTable, @@ -20,6 +25,7 @@ import { import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge'; import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; +import { getPDFBadgeStyle } from '@/components/helper/pdf/utils/pdf-badge'; Font.register({ family: 'Helvetica', @@ -70,11 +76,21 @@ const getTableColumns = (): PdfColumn[] => [ { key: 'qty', header: 'Qty', flex: 0.8, align: 'right' }, { key: 'weight', header: 'Berat', flex: 1, align: 'right' }, { key: 'average_weight', header: 'Rata-Rata', flex: 0.8, align: 'right' }, - { key: 'unit_price', header: 'Harga/Unit', flex: 1.2, align: 'right' }, - { key: 'final_price', header: 'Harga Akhir', flex: 1.2, align: 'right' }, - { key: 'total_price', header: 'Total', flex: 1.2, align: 'right' }, - { key: 'payment_amount', header: 'Pembayaran', flex: 1.2, align: 'right' }, - { key: 'accounts_receivable', header: 'Saldo', flex: 1.2, align: 'right' }, + { key: 'unit_price', header: 'Harga/Unit (Rp)', flex: 1.2, align: 'right' }, + { key: 'final_price', header: 'Harga Akhir (Rp)', flex: 1.2, align: 'right' }, + { key: 'total_price', header: 'Total (Rp)', flex: 1.2, align: 'right' }, + { + key: 'payment_amount', + header: 'Pembayaran (Rp)', + flex: 1.2, + align: 'right', + }, + { + key: 'accounts_receivable', + header: 'Saldo (Rp)', + flex: 1.2, + align: 'right', + }, { key: 'status', header: 'Keterangan', flex: 1.5, align: 'center' }, { key: 'pickup_info', header: 'Pengambilan', flex: 1, align: 'left' }, { key: 'sales_person', header: 'Sales', flex: 1.5, align: 'left' }, @@ -139,18 +155,18 @@ const getTableData = ( key: 'accounts_receivable', value: formatCurrency(item.accounts_receivable), align: 'right', - color: item.accounts_receivable < 0 ? '#DC2626' : undefined, + color: item.accounts_receivable < 0 ? 'red' : undefined, }, { key: 'status', value: item.status ? ( - {item.status === 'LUNAS' ? 'Lunas' : 'Belum Lunas'} + {formatTitleCase(item.status)} ) : ( @@ -204,8 +220,7 @@ const getTableFooter = ( key: 'accounts_receivable', value: formatCurrency(summary?.total_accounts_receivable || 0), align: 'right', - color: - (summary?.total_accounts_receivable || 0) < 0 ? '#DC2626' : undefined, + color: (summary?.total_accounts_receivable || 0) < 0 ? 'red' : undefined, }, { key: 'status', value: '' }, { key: 'pickup_info', value: '' }, @@ -273,8 +288,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { valueKey: 'accounts_receivable', value: customerReport.initial_balance, align: 'right', - color: - customerReport.initial_balance < 0 ? '#DC2626' : 'black', + color: customerReport.initial_balance < 0 ? 'red' : 'black', } : undefined } diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx index 3238d46e..e8bfda5e 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx @@ -27,11 +27,11 @@ export const generateCustomerPaymentExcel = async ( { header: 'Ekor/Qty', key: 'qty', width: 10 }, { header: 'Berat (Kg)', key: 'weight', width: 12 }, { header: 'AVG', key: 'avgWeight', width: 10 }, - { header: 'Harga/Unit', key: 'unitPrice', width: 15 }, - { header: 'Harga Akhir', key: 'finalPrice', width: 15 }, - { header: 'Total', key: 'totalPrice', width: 15 }, - { header: 'Pembayaran', key: 'paymentAmount', width: 15 }, - { header: 'Saldo Piutang', key: 'accountsReceivable', width: 15 }, + { header: 'Harga/Unit (Rp)', key: 'unitPrice', width: 15 }, + { header: 'Harga Akhir (Rp)', key: 'finalPrice', width: 15 }, + { header: 'Total (Rp)', key: 'totalPrice', width: 15 }, + { header: 'Pembayaran (Rp)', key: 'paymentAmount', width: 15 }, + { header: 'Saldo Piutang (Rp)', key: 'accountsReceivable', width: 15 }, { header: 'Keterangan', key: 'status', width: 20 }, { header: 'Pengambilan', key: 'pickupInfo', width: 15 }, { header: 'Sales/Marketing', key: 'salesPerson', width: 20 }, diff --git a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx index b44c8060..b5c26525 100644 --- a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx +++ b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx @@ -20,53 +20,13 @@ import { PdfTbodyCell, PdfTfootCell, } from '@/components/helper/pdf/table'; +import { getPDFBadgeStyle } from '@/components/helper/pdf/utils/pdf-badge'; Font.register({ family: 'Helvetica', src: 'helvetica', }); -// Status color mappings (same as in DebtSupplierTab) -const dueStatusColors: Record< - string, - { bg: string; text: string; border: string } -> = { - 'Sudah Jatuh Tempo': { bg: '#FEE2E2', text: '#991B1B', border: '#F87171' }, // error/red - 'Belum Jatuh Tempo': { bg: '#D1FAE5', text: '#065F46', border: '#34D399' }, // success/green - 'Mendekati Jatuh Tempo': { - bg: '#FEF3C7', - text: '#92400E', - border: '#FBBF24', - }, // warning/yellow -}; - -const paymentStatusColors: Record< - string, - { bg: string; text: string; border: string } -> = { - 'Belum Lunas': { bg: '#FEF3C7', text: '#92400E', border: '#FBBF24' }, // warning/yellow - Lunas: { bg: '#DBEAFE', text: '#1E40AF', border: '#60A5FA' }, // primary/blue - Pembayaran: { bg: '#D1FAE5', text: '#065F46', border: '#34D399' }, // success/green -}; - -/** - * Get badge style for PDF rendering - * @param statusText - The status text - * @param type - Type of status: 'due' or 'payment' - * @returns Style object with background and text colors - */ -const getPDFBadgeStyle = ( - statusText: string, - type: 'due' | 'payment' = 'payment' -) => { - const colors = - type === 'due' - ? dueStatusColors[statusText] - : paymentStatusColors[statusText]; - - return colors || { bg: '#F3F4F6', text: '#374151', border: '#D1D5DB' }; // neutral fallback -}; - const pdfStyles = StyleSheet.create({ page: { fontSize: 10, @@ -99,7 +59,7 @@ const getTableColumns = (): PdfColumn[] => [ { key: 'area', header: 'Area', flex: 1, align: 'left' }, { key: 'warehouse', header: 'Gudang', flex: 1, align: 'left' }, { key: 'due_date', header: 'Jatuh Tempo', flex: 1, align: 'center' }, - { key: 'due_status', header: 'Status Jatuh Tempo', flex: 2, align: 'left' }, + { key: 'due_status', header: 'Status Jatuh Tempo', flex: 2, align: 'center' }, { key: 'total_price', header: 'Nominal Pembelian (Rp)', @@ -151,13 +111,15 @@ const getTableData = (rows: DebtSupplier['rows']): PdfTbodyCell[][] => { key: 'due_status', value: item.due_status && item.due_status !== '-' ? ( - - {item.due_status} - + + + {item.due_status} + + ) : ( '-' ), @@ -226,6 +188,7 @@ const getTableFooter = (total: DebtSupplier['total']): PdfTfootCell[] => [ key: 'balance', value: formatCurrency(total?.debt_price || 0), align: 'right', + color: (total?.debt_price || 0) < 0 ? 'red' : undefined, }, { key: 'status', value: '' }, { key: 'travel_number', value: '' }, From 4f9401ed340758270188c9711f2a2f5cb8447b00 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 11:36:27 +0700 Subject: [PATCH 02/16] refactor(FE): Refactor export logic for PurchasesPerSupplier report --- ....tsx => PurchasesPerSupplierExportPDF.tsx} | 0 .../export/PurchasesPerSupplierExportXLSX.tsx | 101 ++++++++++++++++++ .../tab/PurchasesPerSupplierTab.tsx | 92 +--------------- 3 files changed, 105 insertions(+), 88 deletions(-) rename src/components/pages/report/logistic-stock/export/{PurchasesPerSupplierExport.tsx => PurchasesPerSupplierExportPDF.tsx} (100%) create mode 100644 src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX.tsx diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx similarity index 100% rename from src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx rename to src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX.tsx new file mode 100644 index 00000000..110bd65e --- /dev/null +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX.tsx @@ -0,0 +1,101 @@ +'use client'; + +import ExcelJS from 'exceljs'; +import { formatDate, formatCurrency, formatNumber } from '@/lib/helper'; +import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; + +interface PurchasesPerSupplierExportExcelParams { + data: LogisticPurchasePerSupplierReport[]; +} + +export const generatePurchasesPerSupplierExcel = async ( + params: PurchasesPerSupplierExportExcelParams +): Promise => { + if (!params.data || params.data.length === 0) { + return; + } + + const workbook = new ExcelJS.Workbook(); + + const columns = [ + { header: 'No', key: 'no', width: 5 }, + { header: 'Tanggal Terima', key: 'receiveDate', width: 15 }, + { header: 'Tanggal PO', key: 'poDate', width: 15 }, + { header: 'No. Referensi', key: 'poNumber', width: 15 }, + { header: 'Nama Produk', key: 'productName', width: 30 }, + { header: 'Tujuan', key: 'warehouse', width: 20 }, + { header: 'QTY', key: 'qty', width: 10 }, + { header: 'Harga Beli (Rp)', key: 'unitPrice', width: 18 }, + { header: 'Value Harga Beli (Rp)', key: 'purchaseValue', width: 20 }, + { header: 'Transport (Rp)', key: 'transportUnitPrice', width: 15 }, + { header: 'Value Transport (Rp)', key: 'transportValue', width: 20 }, + { header: 'Jumlah (Rp)', key: 'totalAmount', width: 18 }, + { header: 'Ekspedisi', key: 'expedition', width: 15 }, + { header: 'Surat Jalan', key: 'deliveryNumber', width: 15 }, + ]; + + for (const supplierReport of params.data) { + const supplierData = supplierReport.rows; + const supplierName = supplierReport.supplier?.name || 'Unknown Supplier'; + + const worksheet = workbook.addWorksheet(supplierName.substring(0, 31)); + worksheet.columns = columns; + + supplierData.forEach((item, index) => { + worksheet.addRow({ + no: index + 1, + receiveDate: item.receive_date + ? formatDate(item.receive_date, 'DD MMM YYYY') + : '', + poDate: item.po_date ? formatDate(item.po_date, 'DD MMM YYYY') : '', + poNumber: item.po_number || '', + productName: item.product?.name || '', + warehouse: item.warehouse?.name || '', + qty: formatNumber(item.qty || 0), + unitPrice: formatCurrency(item.unit_price || 0), + purchaseValue: formatCurrency(item.purchase_value || 0), + transportUnitPrice: formatCurrency(item.transport_unit_price || 0), + transportValue: formatCurrency(item.transport_value || 0), + totalAmount: formatCurrency(item.total_amount || 0), + expedition: item.expedition || '', + deliveryNumber: item.delivery_number || '', + }); + }); + + if (supplierReport.summary) { + worksheet.addRow({ + no: 'Total', + receiveDate: '', + poDate: '', + poNumber: '', + productName: '', + warehouse: '', + qty: formatNumber(supplierReport.summary.total_qty || 0), + unitPrice: '', + purchaseValue: formatCurrency( + supplierReport.summary.total_purchase_value || 0 + ), + transportUnitPrice: '', + transportValue: formatCurrency( + supplierReport.summary.total_transport_value || 0 + ), + totalAmount: formatCurrency(supplierReport.summary.total_amount || 0), + expedition: '', + deliveryNumber: '', + }); + } + } + + const filename = `laporan-pembelian-per-supplier-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; + + const buffer = await workbook.xlsx.writeBuffer(); + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + window.URL.revokeObjectURL(url); +}; diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 5366f3cd..1c5a4f2d 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -26,9 +26,9 @@ import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; import MenuItem from '@/components/menu/MenuItem'; import Menu from '@/components/menu/Menu'; -import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport'; +import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF'; +import { generatePurchasesPerSupplierExcel } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX'; import toast from 'react-hot-toast'; -import * as XLSX from 'xlsx'; import { Icon } from '@iconify/react'; const PurchasesPerSupplierTab = () => { @@ -355,98 +355,14 @@ const PurchasesPerSupplierTab = () => { return; } - const workbook = XLSX.utils.book_new(); - - allDataForExport.forEach((supplierReport) => { - const supplierData = supplierReport.rows; - const supplierName = - supplierReport.supplier?.name || 'Unknown Supplier'; - - const excelData: { [key: string]: string | number }[] = - supplierData.map((item, index) => ({ - No: index + 1, - 'Tanggal Terima': item.receive_date - ? formatDate(item.receive_date, 'DD MMM YYYY') - : '', - 'Tanggal PO': item.po_date - ? formatDate(item.po_date, 'DD MMM YYYY') - : '', - 'No. Referensi': item.po_number || '', - 'Nama Produk': item.product?.name || '', - Tujuan: item.warehouse?.name || '', - QTY: item.qty || 0, - 'Harga Beli (Rp)': item.unit_price || 0, - 'Value Harga Beli (Rp)': item.purchase_value || 0, - 'Transport (Rp)': item.transport_unit_price || 0, - 'Value Transport (Rp)': item.transport_value || 0, - 'Jumlah (Rp)': item.total_amount || 0, - Ekspedisi: item.expedition || '', - 'Surat Jalan': item.delivery_number || '', - })); - - if (supplierReport.summary) { - excelData.push({ - No: 'Total', - 'Tanggal Terima': '', - 'Tanggal PO': '', - 'No. Referensi': '', - 'Nama Produk': '', - Tujuan: '', - QTY: supplierReport.summary.total_qty || 0, - 'Harga Beli (Rp)': '', - 'Value Harga Beli (Rp)': - supplierReport.summary.total_purchase_value || 0, - 'Transport (Rp)': '', - 'Value Transport (Rp)': - supplierReport.summary.total_transport_value || 0, - 'Jumlah (Rp)': supplierReport.summary.total_amount || 0, - Ekspedisi: '', - 'Surat Jalan': '', - }); - } - - const worksheet = XLSX.utils.json_to_sheet(excelData); - - const colWidths = [ - { wch: 5 }, // No - { wch: 15 }, // Tanggal Terima - { wch: 15 }, // Tanggal PO - { wch: 15 }, // No. Referensi - { wch: 30 }, // Nama Produk - { wch: 20 }, // Tujuan - { wch: 10 }, // QTY - { wch: 18 }, // Harga Beli - { wch: 20 }, // Value Harga Beli - { wch: 15 }, // Transport - { wch: 20 }, // Value Transport - { wch: 18 }, // Jumlah - { wch: 15 }, // Ekspedisi - { wch: 15 }, // Surat Jalan - ]; - worksheet['!cols'] = colWidths; - - const sheetName = - supplierName.length > 31 - ? supplierName.substring(0, 31) - : supplierName; - XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); - }); - - const filename = `laporan-pembelian-per-supplier-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; - - XLSX.writeFile(workbook, filename); + await generatePurchasesPerSupplierExcel({ data: allDataForExport }); toast.success('Excel berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat Excel. Silakan coba lagi.'); } finally { setIsExcelExportLoading(false); } - }, [ - logisticPurchasePerSupplierExport, - tableFilterState, - areaOptions, - supplierOptions, - ]); + }, [logisticPurchasePerSupplierExport]); const handleExportPdf = useCallback(async () => { setIsPdfExportLoading(true); From def894e5f4a40cf6f67e782615dc69c0f66457f4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 11:43:23 +0700 Subject: [PATCH 03/16] refactor(FE): Refactor PDF generation for purchases per supplier --- .../export/PurchasesPerSupplierExportPDF.tsx | 309 +++++++++--------- .../tab/PurchasesPerSupplierTab.tsx | 5 +- 2 files changed, 162 insertions(+), 152 deletions(-) diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx index bd6f301a..cc2b7976 100644 --- a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx @@ -2,7 +2,6 @@ import { Page, - Text, View, Document, StyleSheet, @@ -15,7 +14,11 @@ import { PdfTable, PdfColumn, PdfTbodyCell, + PdfTfootCell, } from '@/components/helper/pdf/table'; +import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; +import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge'; +import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; Font.register({ family: 'Helvetica', @@ -32,53 +35,16 @@ const pdfStyles = StyleSheet.create({ titleSection: { marginBottom: 10, }, - mainTitle: { - fontSize: 14, - fontWeight: 'bold', - marginBottom: 5, - color: '#1f74bf', - }, - supplierTitle: { - fontSize: 12, - fontWeight: 'bold', - marginBottom: 8, - color: '#1f74bf', - }, - badge: { - backgroundColor: '#1f74bf', - color: '#FFFFFF', - padding: 2, - borderRadius: 2, - fontSize: 7, - fontWeight: 'bold', - alignSelf: 'center', - marginRight: 4, - }, - parameterBadge: { - backgroundColor: '#F5F5F5', - color: '#333333', - padding: 4, - borderRadius: 4, - fontSize: 8, - marginRight: 8, - marginBottom: 4, - }, parameterContainer: { flexDirection: 'row', flexWrap: 'wrap', marginBottom: 8, }, - supplierSection: { - marginBottom: 10, - }, - supplierSectionBreak: { - marginBottom: 15, - }, }); interface PurchasesPerSupplierExportParams { data: LogisticPurchasePerSupplierReport[]; - params: { + params?: { area_name?: string; supplier_name?: string; product_name?: string; @@ -92,73 +58,57 @@ interface PurchasesPerSupplierExportParams { }; } -const getParameterText = ( - params: PurchasesPerSupplierExportParams['params'] -) => { - const paramsText = []; - - if (params.supplier_name) { - paramsText.push(`Supplier: ${params.supplier_name}`); - } else { - paramsText.push('Semua Supplier'); - } - - if (params.start_date && params.end_date) { - const startDate = formatDate(params.start_date, 'DD MMM YYYY'); - const endDate = formatDate(params.end_date, 'DD MMM YYYY'); - paramsText.push(`Periode: ${startDate} - ${endDate}`); - } else if (params.start_date) { - const startDate = formatDate(params.start_date, 'DD MMM YYYY'); - paramsText.push(`Tanggal: ${startDate}`); - } - - const currentDate = formatDate(new Date(), 'DD MMM YYYY HH:mm'); - paramsText.push(`Dicetak: ${currentDate}`); - - return paramsText; -}; - -// Helper functions for PdfTable const getTableColumns = (): PdfColumn[] => [ { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'receive_date', header: 'Tanggal Terima', flex: 1, align: 'center' }, - { key: 'po_date', header: 'Tanggal PO', flex: 1, align: 'center' }, - { key: 'po_number', header: 'Referensi', flex: 1, align: 'left' }, - { key: 'product', header: 'Produk', flex: 1, align: 'left' }, - { key: 'warehouse', header: 'Tujuan', flex: 1, align: 'left' }, - { key: 'qty', header: 'Qty', flex: 0.8, align: 'right' }, - { key: 'unit_price', header: 'Harga Beli', flex: 1.2, align: 'right' }, + { + key: 'receive_date', + header: 'Tanggal Terima', + flex: 1.2, + align: 'center', + }, + { key: 'po_date', header: 'Tanggal PO', flex: 1.2, align: 'center' }, + { key: 'po_number', header: 'No. Referensi', flex: 1.5, align: 'left' }, + { key: 'product', header: 'Nama Produk', flex: 2, align: 'left' }, + { key: 'warehouse', header: 'Tujuan', flex: 1.5, align: 'left' }, + { key: 'qty', header: 'QTY', flex: 0.8, align: 'right' }, + { key: 'unit_price', header: 'Harga Beli (Rp)', flex: 1.5, align: 'right' }, { key: 'purchase_value', - header: 'Nilai Pembelian', - flex: 1.5, + header: 'Value Harga Beli (Rp)', + flex: 1.8, align: 'right', }, { - key: 'transport_price', - header: 'Biaya Transport', - flex: 1.2, + key: 'transport_unit_price', + header: 'Transport (Rp)', + flex: 1.3, align: 'right', }, - { key: 'total_amount', header: 'Total', flex: 1.5, align: 'right' }, - { key: 'expedition', header: 'Armada', flex: 1.2, align: 'center' }, - { key: 'delivery_number', header: 'Surat Jalan', flex: 1, align: 'left' }, + { + key: 'transport_value', + header: 'Value Transport (Rp)', + flex: 1.8, + align: 'right', + }, + { key: 'total_amount', header: 'Jumlah (Rp)', flex: 1.5, align: 'right' }, + { key: 'expedition', header: 'Ekspedisi', flex: 1.2, align: 'center' }, + { key: 'delivery_number', header: 'Surat Jalan', flex: 1.2, align: 'left' }, ]; const getTableData = ( rows: LogisticPurchasePerSupplierReport['rows'] ): PdfTbodyCell[][] => { return rows.map((item, index) => [ - { key: 'no', value: index + 1, align: 'center' }, + { key: 'no', value: index + 1 }, { key: 'receive_date', - value: formatDate(item.receive_date, 'DD-MMM-YYYY'), - align: 'center', + value: item.receive_date + ? formatDate(item.receive_date, 'DD MMM YY') + : '-', }, { key: 'po_date', - value: formatDate(item.po_date, 'DD-MMM-YYYY'), - align: 'center', + value: item.po_date ? formatDate(item.po_date, 'DD MMM YY') : '-', }, { key: 'po_number', value: item.po_number || '-' }, { key: 'product', value: item.product?.name || '-' }, @@ -175,10 +125,15 @@ const getTableData = ( align: 'right', }, { - key: 'transport_price', + key: 'transport_unit_price', value: formatCurrency(item.transport_unit_price || 0), align: 'right', }, + { + key: 'transport_value', + value: formatCurrency(item.transport_value || 0), + align: 'right', + }, { key: 'total_amount', value: formatCurrency(item.total_amount || 0), @@ -186,82 +141,134 @@ const getTableData = ( }, { key: 'expedition', - value: ( - - {item.expedition || '-'} + value: item.expedition ? ( + + + {item.expedition} + + ) : ( + '-' ), - align: 'center', }, { key: 'delivery_number', value: item.delivery_number || '-' }, ]); }; -const createPDFDocument = ( - supplierReports: LogisticPurchasePerSupplierReport[], - params: PurchasesPerSupplierExportParams['params'] -) => ( - - - {/* Title and Parameters */} - - - Laporan > Rekapitulasi Pembelian Per Supplier - - - - - Jenis Tanggal:{' '} - {params.filter_by === 'received_date' - ? 'Tanggal Terima' - : 'Tanggal PO'} - +const getTableFooter = ( + summary: LogisticPurchasePerSupplierReport['summary'] +): PdfTfootCell[] => [ + { key: 'no', value: 'Total' }, + { key: 'receive_date', value: '' }, + { key: 'po_date', value: '' }, + { key: 'po_number', value: '' }, + { key: 'product', value: '' }, + { key: 'warehouse', value: '' }, + { + key: 'qty', + value: formatNumber(summary?.total_qty || 0), + align: 'right', + }, + { key: 'unit_price', value: '' }, + { + key: 'purchase_value', + value: formatCurrency(summary?.total_purchase_value || 0), + align: 'right', + }, + { key: 'transport_unit_price', value: '' }, + { + key: 'transport_value', + value: formatCurrency(summary?.total_transport_value || 0), + align: 'right', + }, + { + key: 'total_amount', + value: formatCurrency(summary?.total_amount || 0), + align: 'right', + }, + { key: 'expedition', value: '' }, + { key: 'delivery_number', value: '' }, +]; + +const createPDFDocument = (params: PurchasesPerSupplierExportParams) => { + return ( + + {params.data.map((supplierReport, supplierIndex) => ( + + {/* Title and Parameters */} + + + Laporan > Rekapitulasi Pembelian Per Supplier + + + + Jenis Tanggal:{' '} + {params.params?.filter_by === 'received_date' + ? 'Tanggal Terima' + : 'Tanggal PO'} + + + Periode:{' '} + {params.params?.start_date + ? formatDate(params.params.start_date, 'DD MMM YYYY') + : '-'}{' '} + s.d{' '} + {params.params?.end_date + ? formatDate(params.params.end_date, 'DD MMM YYYY') + : '-'} + + + Supplier: {params.params?.supplier_name || 'Semua Supplier'} + + + Area: {params.params?.area_name || 'Semua Area'} + + + Produk: {params.params?.product_name || 'Semua Produk'} + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + + + + {supplierReport.supplier.name} + + {supplierReport.supplier.address && ( + + Alamat: {supplierReport.supplier.address} + + )} - {getParameterText(params).map((param, index) => ( - - {param} - - ))} - - - {/* Supplier Sections */} - {supplierReports.map( - ( - supplierReport: LogisticPurchasePerSupplierReport, - supplierIndex: number - ) => { - return ( - - - {supplierReport.supplier.name} - - - - - ); - } - )} - - -); + {/* Table */} + + + ))} + + ); +}; export const generatePurchasesPerSupplierPDF = async ( - data: LogisticPurchasePerSupplierReport[], - params: PurchasesPerSupplierExportParams['params'] + params: PurchasesPerSupplierExportParams ): Promise => { - const PDFDocument = createPDFDocument(data, params); + const PDFDocument = createPDFDocument(params); try { const blob = await pdf(PDFDocument).toBlob(); diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 1c5a4f2d..e1659470 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -433,7 +433,10 @@ const PurchasesPerSupplierTab = () => { end_date: tableFilterState.end_date || '', }; - await generatePurchasesPerSupplierPDF(allDataForExport, exportParams); + await generatePurchasesPerSupplierPDF({ + data: allDataForExport, + params: exportParams, + }); toast.success('PDF berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat PDF. Silakan coba lagi.'); From d0dea834c1c18b87b72b9b156c4794b0c8f013e3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 11:54:15 +0700 Subject: [PATCH 04/16] refactor(FE): Refactor HppPerKandang export logic to use ExcelJS --- ...gExport.tsx => HppPerkandangExportPDF.tsx} | 0 .../sale/export/HppPerkandangExportXLSX.tsx | 135 ++++++++++++++++++ .../report/sale/tab/HppPerKandangTab.tsx | 132 +---------------- 3 files changed, 142 insertions(+), 125 deletions(-) rename src/components/pages/report/sale/export/{HppPerkandangExport.tsx => HppPerkandangExportPDF.tsx} (100%) create mode 100644 src/components/pages/report/sale/export/HppPerkandangExportXLSX.tsx diff --git a/src/components/pages/report/sale/export/HppPerkandangExport.tsx b/src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx similarity index 100% rename from src/components/pages/report/sale/export/HppPerkandangExport.tsx rename to src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx diff --git a/src/components/pages/report/sale/export/HppPerkandangExportXLSX.tsx b/src/components/pages/report/sale/export/HppPerkandangExportXLSX.tsx new file mode 100644 index 00000000..20faaa13 --- /dev/null +++ b/src/components/pages/report/sale/export/HppPerkandangExportXLSX.tsx @@ -0,0 +1,135 @@ +'use client'; + +import ExcelJS from 'exceljs'; +import { formatCurrency, formatNumber } from '@/lib/helper'; +import { + HppPerKandangReport, + HppPerKandangRow, + HppPerKandangPerWeightRange, +} from '@/types/api/report/hpp-per-kandang'; + +interface HppPerKandangExportExcelParams { + data: HppPerKandangReport; + allFeedSuppliers: string; + allDocSuppliers: string; +} + +const formatSuppliers = ( + suppliers: { alias?: string; name: string }[] | null +): string => { + if (!suppliers || suppliers.length === 0) return ''; + return suppliers.map((s) => s.alias || s.name).join(' | '); +}; + +export const generateHppPerKandangExcel = async ( + params: HppPerKandangExportExcelParams +): Promise => { + if (!params.data || !params.data.rows || params.data.rows.length === 0) { + return; + } + + const workbook = new ExcelJS.Workbook(); + + // ===== REKAPITULASI WORKSHEET ===== + const rekapitulasiColumns = [ + { header: 'No', key: 'no', width: 5 }, + { header: 'Rentang BW', key: 'weightRange', width: 15 }, + { header: 'Sisa Butir', key: 'eggPieces', width: 15 }, + { header: 'Sisa Kg', key: 'eggKg', width: 12 }, + { header: 'Rata-Rata Bobot (Kg)', key: 'avgWeight', width: 18 }, + { header: 'Feed (Supplier)', key: 'feedSuppliers', width: 20 }, + { header: 'DOC (Supplier)', key: 'docSuppliers', width: 20 }, + { header: 'Rata-Rata Harga DOC', key: 'avgDocPrice', width: 20 }, + { header: 'HPP Telur (Rp/Kg)', key: 'eggHpp', width: 18 }, + { header: 'Nominal Sisa', key: 'eggValue', width: 25 }, + ]; + + const rekapitulasiWorksheet = workbook.addWorksheet('Rekapitulasi'); + rekapitulasiWorksheet.columns = rekapitulasiColumns; + + const perWeightRangeSummary = params.data.summary.per_weight_range || []; + + perWeightRangeSummary.forEach( + (item: HppPerKandangPerWeightRange, index: number) => { + rekapitulasiWorksheet.addRow({ + no: index + 1, + weightRange: item.label || '', + eggPieces: formatNumber(item.egg_production_pieces || 0), + eggKg: formatNumber(item.egg_production_kg || 0), + avgWeight: formatNumber(item.avg_weight_kg || 0), + feedSuppliers: formatSuppliers(item.feed_suppliers), + docSuppliers: formatSuppliers(item.doc_suppliers), + avgDocPrice: formatCurrency(item.average_doc_price_rp || 0), + eggHpp: formatCurrency(item.egg_hpp_rp_per_kg || 0), + eggValue: formatCurrency(item.egg_value_rp || 0), + }); + } + ); + + // ===== DETAIL PER KANDANG WORKSHEET ===== + const detailColumns = [ + { header: 'No', key: 'no', width: 5 }, + { header: 'Kandang', key: 'kandang', width: 30 }, + { header: 'Rentang Bobot', key: 'weightRange', width: 15 }, + { header: 'Rata-Rata Bobot (KG)', key: 'avgWeightKg', width: 18 }, + { header: 'Sisa Telur (Butir)', key: 'eggPieces', width: 15 }, + { header: 'Sisa Telur (KG)', key: 'eggKg', width: 15 }, + { header: 'Feed (Supplier)', key: 'feedSuppliers', width: 20 }, + { header: 'DOC (Supplier)', key: 'docSuppliers', width: 20 }, + { header: 'Rata-Rata Harga DOC (Rp)', key: 'avgDocPrice', width: 20 }, + { header: 'HPP Telur (Rp/Kg)', key: 'eggHpp', width: 18 }, + { header: 'Nilai Nominal Sisa Telur (Rp)', key: 'eggValue', width: 25 }, + ]; + + const detailWorksheet = workbook.addWorksheet('Detail Per Kandang'); + detailWorksheet.columns = detailColumns; + + const allExportData = params.data.rows; + + allExportData.forEach((item: HppPerKandangRow, index: number) => { + detailWorksheet.addRow({ + no: index + 1, + kandang: item.kandang?.name || '', + weightRange: item.weight_range + ? `${formatNumber(item.weight_range.weight_min)} - ${formatNumber(item.weight_range.weight_max)}` + : '', + avgWeightKg: formatNumber(item.avg_weight_kg || 0), + eggPieces: formatNumber(item.egg_production_pieces || 0), + eggKg: formatNumber(item.egg_production_kg || 0), + feedSuppliers: formatSuppliers(item.feed_suppliers), + docSuppliers: formatSuppliers(item.doc_suppliers), + avgDocPrice: formatCurrency(item.average_doc_price_rp || 0), + eggHpp: formatCurrency(item.egg_hpp_rp_per_kg || 0), + eggValue: formatCurrency(item.egg_value_rp || 0), + }); + }); + + // Add TOTAL row + const summaryTotal = params.data.summary.total; + detailWorksheet.addRow({ + no: 'TOTAL', + kandang: 'ALL', + weightRange: '-', + avgWeightKg: formatNumber(summaryTotal?.average_weight_kg || 0), + eggPieces: formatNumber(summaryTotal?.total_egg_production_pieces || 0), + eggKg: formatNumber(summaryTotal?.total_egg_production_kg || 0), + feedSuppliers: params.allFeedSuppliers, + docSuppliers: params.allDocSuppliers, + avgDocPrice: formatCurrency(summaryTotal?.total_average_doc_price_rp || 0), + eggHpp: formatCurrency(summaryTotal?.average_egg_hpp_rp_per_kg || 0), + eggValue: formatCurrency(summaryTotal?.total_egg_value_rp || 0), + }); + + const filename = `laporan-hpp-harian-kandang-periode-${params.data.period}.xlsx`; + + const buffer = await workbook.xlsx.writeBuffer(); + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + window.URL.revokeObjectURL(url); +}; diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 7bd774f3..9e4d4004 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -26,9 +26,9 @@ import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; import MenuItem from '@/components/menu/MenuItem'; import Menu from '@/components/menu/Menu'; -import { generateHppPerKandangPDF } from '../export/HppPerkandangExport'; +import { generateHppPerKandangPDF } from '../export/HppPerkandangExportPDF'; +import { generateHppPerKandangExcel } from '../export/HppPerkandangExportXLSX'; import toast from 'react-hot-toast'; -import * as XLSX from 'xlsx'; import { Icon } from '@iconify/react'; const HppPerKandangTab = () => { @@ -346,136 +346,18 @@ const HppPerKandangTab = () => { return; } - const allExportData = - allDataForExport.rows as HppPerKandangReport['rows']; - - const perWeightRangeSummary = - allDataForExport.summary.per_weight_range || []; - - const summaryTotal = allDataForExport.summary.total; - - const rekapitulasiData: { [key: string]: string | number }[] = - perWeightRangeSummary.map( - (item: HppPerKandangPerWeightRange, index: number) => ({ - No: index + 1, - 'Rentang BW': item.label || '', - 'Sisa Butir': item.egg_production_pieces || 0, - 'Sisa Kg': item.egg_production_kg || 0, - 'Rata-Rata Bobot (Kg)': item.avg_weight_kg || 0, - 'Feed (Supplier)': - item.feed_suppliers - ?.map( - (s: { alias?: string; name: string }) => s.alias || s.name - ) - .join(' | ') || '', - 'DOC (Supplier)': - item.doc_suppliers - ?.map( - (s: { alias?: string; name: string }) => s.alias || s.name - ) - .join(' | ') || '', - 'Rata-Rata Harga DOC': item.average_doc_price_rp || 0, - 'HPP Telur (RP/KG)': item.egg_hpp_rp_per_kg || 0, - 'Nominal Sisa': item.egg_value_rp || 0, - }) - ); - - const rekapitulasiWorksheet = XLSX.utils.json_to_sheet(rekapitulasiData); - - const rekapitulasiColWidths = [ - { wch: 5 }, // No - { wch: 15 }, // Rentang BW - { wch: 15 }, // Sisa Butir - { wch: 12 }, // Sisa Kg - { wch: 18 }, // Rata-Rata Bobot (Kg) - { wch: 20 }, // Feed (Supplier) - { wch: 20 }, // DOC (Supplier) - { wch: 20 }, // Rata-Rata Harga DOC - { wch: 18 }, // HPP Telur (RP/KG) - { wch: 25 }, // Nominal Sisa - ]; - rekapitulasiWorksheet['!cols'] = rekapitulasiColWidths; - - const excelData: { [key: string]: string | number }[] = allExportData.map( - (item: HppPerKandangRow, index: number) => ({ - No: index + 1, - Kandang: item.kandang?.name || '', - 'Rentang Bobot': item.weight_range - ? `${formatNumber(item.weight_range.weight_min)} - ${formatNumber(item.weight_range.weight_max)}` - : '', - 'Rata-Rata Bobot (KG)': item.avg_weight_kg || 0, - 'Sisa Telur (Butir)': item.egg_production_pieces || 0, - 'Sisa Telur (KG)': item.egg_production_kg || 0, - 'Feed (Supplier)': - item.feed_suppliers - ?.map((s: { alias?: string; name: string }) => s.alias || s.name) - .join(' | ') || '', - 'DOC (Supplier)': - item.doc_suppliers - ?.map((s: { alias?: string; name: string }) => s.alias || s.name) - .join(' | ') || '', - 'Rata-Rata Harga DOC (RP)': item.average_doc_price_rp || 0, - 'HPP Telur (RP/KG)': item.egg_hpp_rp_per_kg || 0, - 'Nilai Nominal Sisa Telur (RP)': item.egg_value_rp || 0, - }) - ); - - excelData.push({ - No: 'TOTAL', - Kandang: 'ALL', - 'Rentang Bobot': '-', - 'Rata-Rata Bobot (KG)': summaryTotal?.average_weight_kg || 0, - 'Sisa Telur (Butir)': summaryTotal?.total_egg_production_pieces || 0, - 'Sisa Telur (KG)': summaryTotal?.total_egg_production_kg || 0, - 'Feed (Supplier)': allFeedSuppliers, - 'DOC (Supplier)': allDocSuppliers, - 'Rata-Rata Harga DOC (RP)': - summaryTotal?.total_average_doc_price_rp || 0, - 'HPP Telur (RP/KG)': summaryTotal?.average_egg_hpp_rp_per_kg || 0, - 'Nilai Nominal Sisa Telur (RP)': summaryTotal?.total_egg_value_rp || 0, + await generateHppPerKandangExcel({ + data: allDataForExport, + allFeedSuppliers, + allDocSuppliers, }); - - const worksheet = XLSX.utils.json_to_sheet(excelData); - - const colWidths = [ - { wch: 5 }, // No - { wch: 30 }, // Kandang - { wch: 15 }, // Rentang Bobot - { wch: 18 }, // Rata-Rata Bobot (KG) - { wch: 15 }, // Sisa Telur (Butir) - { wch: 15 }, // Sisa Telur (KG) - { wch: 20 }, // Feed (Supplier) - { wch: 20 }, // DOC (Supplier) - { wch: 20 }, // Rata-Rata Harga DOC (RP) - { wch: 18 }, // HPP Telur (RP/KG) - { wch: 25 }, // Nilai Nominal Sisa Telur (RP) - ]; - worksheet['!cols'] = colWidths; - - const workbook = XLSX.utils.book_new(); - XLSX.utils.book_append_sheet( - workbook, - rekapitulasiWorksheet, - 'Rekapitulasi' - ); - XLSX.utils.book_append_sheet(workbook, worksheet, 'Detail Per Kandang'); - - const filename = `laporan-hpp-harian-kandang-periode-${tableFilterState.period}.xlsx`; - - XLSX.writeFile(workbook, filename); toast.success('Excel berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat Excel. Silakan coba lagi.'); } finally { setIsExcelExportLoading(false); } - }, [ - hppPerKandangExport, - tableFilterState, - areaOptions, - locationOptions, - kandangOptions, - ]); + }, [hppPerKandangExport, allFeedSuppliers, allDocSuppliers]); const handleExportPDF = useCallback(async () => { setIsPdfExportLoading(true); From 4775c1e115adcd72c77699f709f12f643d11436f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 12:01:51 +0700 Subject: [PATCH 05/16] refactor(FE): Refactor HppPerKandang PDF generation logic and UI renderer --- .../sale/export/HppPerkandangExportPDF.tsx | 290 ++++++++++-------- .../report/sale/tab/HppPerKandangTab.tsx | 29 +- 2 files changed, 174 insertions(+), 145 deletions(-) diff --git a/src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx b/src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx index 3a76d8f4..94c24e93 100644 --- a/src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx +++ b/src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx @@ -2,7 +2,6 @@ import { Page, - Text, View, Document, StyleSheet, @@ -21,6 +20,8 @@ import { PdfTbodyCell, PdfTfootCell, } from '@/components/helper/pdf/table'; +import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; +import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; Font.register({ family: 'Helvetica', @@ -37,27 +38,6 @@ const pdfStyles = StyleSheet.create({ titleSection: { marginBottom: 10, }, - mainTitle: { - fontSize: 14, - fontWeight: 'bold', - marginBottom: 5, - color: '#1f74bf', - }, - supplierTitle: { - fontSize: 12, - fontWeight: 'bold', - marginBottom: 8, - color: '#1f74bf', - }, - parameterBadge: { - backgroundColor: '#F5F5F5', - color: '#333333', - padding: 4, - borderRadius: 4, - fontSize: 8, - marginRight: 8, - marginBottom: 4, - }, parameterContainer: { flexDirection: 'row', flexWrap: 'wrap', @@ -70,7 +50,7 @@ const pdfStyles = StyleSheet.create({ interface HppPerKandangExportParams { data: HppPerKandangReport; - params: { + params?: { area_name?: string; location_name?: string; kandang_name?: string; @@ -82,44 +62,11 @@ interface HppPerKandangExportParams { }; } -const getParameterText = (params: HppPerKandangExportParams['params']) => { - const paramsText = []; - - if (params.area_name && params.area_name !== 'Semua Area') { - paramsText.push(`Area: ${params.area_name}`); - } - - if (params.location_name && params.location_name !== 'Semua Lokasi') { - paramsText.push(`Lokasi: ${params.location_name}`); - } - - if (params.kandang_name && params.kandang_name !== 'Semua Kandang') { - paramsText.push(`Kandang: ${params.kandang_name}`); - } - - if (params.period) { - const formattedDate = formatDate(params.period, 'DD MMM YYYY'); - paramsText.push(`Tanggal: ${formattedDate}`); - } - - if (params.weight_min || params.weight_max) { - const weightRange = - params.weight_min && params.weight_max - ? `${params.weight_min} - ${params.weight_max} kg` - : params.weight_min - ? `≥ ${params.weight_min} kg` - : `≤ ${params.weight_max} kg`; - paramsText.push(`Rentang Bobot: ${weightRange}`); - } - - if (params.show_unrecorded === 'true') { - paramsText.push('Tampilkan: Tanpa Recording'); - } - - const currentDate = formatDate(new Date().toISOString(), 'DD MMM YYYY HH:mm'); - paramsText.push(`Dicetak: ${currentDate}`); - - return paramsText; +const formatSuppliers = ( + suppliers: { alias?: string; name: string }[] | null | undefined +): string => { + if (!suppliers || suppliers.length === 0) return '-'; + return suppliers.map((s) => s.alias || s.name).join(' | '); }; // Helper functions for PdfTable - Rekapitulasi @@ -133,65 +80,79 @@ const getRekapitulasiColumns = (): PdfColumn[] => [ flex: 1.2, align: 'right', }, - { key: 'feed_supplier', header: 'Feed (Supplier)', flex: 1.5, align: 'left' }, - { key: 'doc_supplier', header: 'DOC (Supplier)', flex: 1.2, align: 'left' }, + { + key: 'feed_supplier', + header: 'Feed (Supplier)', + flex: 1.5, + align: 'left', + }, + { + key: 'doc_supplier', + header: 'DOC (Supplier)', + flex: 1.2, + align: 'left', + }, { key: 'rata_harga_doc', header: 'Rata-Rata Harga DOC', flex: 1.2, align: 'right', }, - { key: 'hpp_telur', header: 'HPP Telur (RP/KG)', flex: 1.2, align: 'right' }, - { key: 'nominal_sisa', header: 'Nominal Sisa', flex: 1.2, align: 'right' }, + { + key: 'hpp_telur', + header: 'HPP Telur (Rp/Kg)', + flex: 1.2, + align: 'right', + }, + { + key: 'nominal_sisa', + header: 'Nominal Sisa', + flex: 1.2, + align: 'right', + }, ]; const getRekapitulasiData = ( perWeightRange: HppPerKandangPerWeightRange[] ): PdfTbodyCell[][] => { return perWeightRange.map((group) => [ - { key: 'rentang_bw', value: group.label, align: 'center' }, + { key: 'rentang_bw', value: group.label || '-' }, { key: 'sisa_butir', - value: formatNumber(group.egg_production_pieces), + value: formatNumber(group.egg_production_pieces || 0), align: 'right', }, { key: 'sisa_kg', - value: formatNumber(group.egg_production_kg), + value: formatNumber(group.egg_production_kg || 0), align: 'right', }, { key: 'rata_rata_bobot', - value: formatNumber(group.avg_weight_kg), + value: formatNumber(group.avg_weight_kg || 0), align: 'right', }, { key: 'feed_supplier', - value: - group.feed_suppliers - ?.map((s: { alias?: string; name: string }) => s.alias || s.name) - .join(' | ') || '-', + value: formatSuppliers(group.feed_suppliers), }, { key: 'doc_supplier', - value: - group.doc_suppliers - ?.map((s: { alias?: string; name: string }) => s.alias || s.name) - .join(' | ') || '-', + value: formatSuppliers(group.doc_suppliers), }, { key: 'rata_harga_doc', - value: formatCurrency(group.average_doc_price_rp), + value: formatCurrency(group.average_doc_price_rp || 0), align: 'right', }, { key: 'hpp_telur', - value: formatCurrency(group.egg_hpp_rp_per_kg), + value: formatCurrency(group.egg_hpp_rp_per_kg || 0), align: 'right', }, { key: 'nominal_sisa', - value: formatCurrency(group.egg_value_rp), + value: formatCurrency(group.egg_value_rp || 0), align: 'right', }, ]); @@ -210,21 +171,45 @@ const getDetailColumns = (): PdfColumn[] => [ }, { key: 'sisa_butir', header: 'Sisa Butir', flex: 0.8, align: 'right' }, { key: 'sisa_kg', header: 'Sisa Kg (Telur)', flex: 0.8, align: 'right' }, - { key: 'feed_supplier', header: 'Feed (Supplier)', flex: 1.2, align: 'left' }, - { key: 'doc_supplier', header: 'DOC (Supplier)', flex: 1, align: 'left' }, + { + key: 'feed_supplier', + header: 'Feed (Supplier)', + flex: 1.2, + align: 'left', + }, + { + key: 'doc_supplier', + header: 'DOC (Supplier)', + flex: 1, + align: 'left', + }, { key: 'rata_harga_doc', header: 'Rata-Rata Harga DOC', flex: 1.2, align: 'right', }, - { key: 'hpp_telur', header: 'HPP Telur (RP/KG)', flex: 1, align: 'right' }, - { key: 'nominal_sisa', header: 'Nominal Sisa', flex: 1.2, align: 'right' }, + { + key: 'hpp_telur', + header: 'HPP Telur (Rp/Kg)', + flex: 1, + align: 'right', + }, + { + key: 'nominal_sisa', + header: 'Nominal Sisa', + flex: 1.2, + align: 'right', + }, ]; -const getDetailData = (rows: HppPerKandangRow[]): PdfTbodyCell[][] => { +const getDetailData = ( + rows: HppPerKandangRow[], + allFeedSuppliers: string, + allDocSuppliers: string +): PdfTbodyCell[][] => { return rows.map((item, index) => [ - { key: 'no', value: index + 1, align: 'center' }, + { key: 'no', value: index + 1 }, { key: 'kandang', value: item.kandang?.name || '-' }, { key: 'rentang_bw', @@ -232,131 +217,146 @@ const getDetailData = (rows: HppPerKandangRow[]): PdfTbodyCell[][] => { }, { key: 'rata_rata_bobot', - value: formatNumber(item.avg_weight_kg), + value: formatNumber(item.avg_weight_kg || 0), align: 'right', }, { key: 'sisa_butir', - value: formatNumber(item.egg_production_pieces), + value: formatNumber(item.egg_production_pieces || 0), align: 'right', }, { key: 'sisa_kg', - value: formatNumber(item.egg_production_kg), + value: formatNumber(item.egg_production_kg || 0), align: 'right', }, { key: 'feed_supplier', - value: - item.feed_suppliers - ?.map((s: { alias?: string; name: string }) => s.alias || s.name) - .join(' | ') || '-', + value: formatSuppliers(item.feed_suppliers), }, { key: 'doc_supplier', - value: - item.doc_suppliers - ?.map((s: { alias?: string; name: string }) => s.alias || s.name) - .join(' | ') || '-', + value: formatSuppliers(item.doc_suppliers), }, { key: 'rata_harga_doc', - value: formatCurrency(item.average_doc_price_rp), + value: formatCurrency(item.average_doc_price_rp || 0), align: 'right', }, { key: 'hpp_telur', - value: formatCurrency(item.egg_hpp_rp_per_kg), + value: formatCurrency(item.egg_hpp_rp_per_kg || 0), align: 'right', }, { key: 'nominal_sisa', - value: formatCurrency(item.egg_value_rp), + value: formatCurrency(item.egg_value_rp || 0), align: 'right', }, ]); }; const getDetailFooter = ( - summary: HppPerKandangReport['summary'] + summary: HppPerKandangReport['summary'], + allFeedSuppliers: string, + allDocSuppliers: string ): PdfTfootCell[] => { if (!summary?.total) return []; - const allFeedSuppliers = - summary.total.feed_suppliers - ?.map((s: { alias?: string; name: string }) => s.alias || s.name) - .join(' | ') || '-'; - - const allDocSuppliers = - summary.total.doc_suppliers - ?.map((s: { alias?: string; name: string }) => s.alias || s.name) - .join(' | ') || '-'; - return [ { key: 'no', value: 'TOTAL' }, { key: 'kandang', value: 'ALL' }, { key: 'rentang_bw', value: '-' }, { key: 'rata_rata_bobot', - value: formatNumber(summary.total.average_weight_kg), + value: formatNumber(summary.total.average_weight_kg || 0), align: 'right', }, { key: 'sisa_butir', - value: formatNumber(summary.total.total_egg_production_pieces), + value: formatNumber(summary.total.total_egg_production_pieces || 0), align: 'right', }, { key: 'sisa_kg', - value: formatNumber(summary.total.total_egg_production_kg), + value: formatNumber(summary.total.total_egg_production_kg || 0), align: 'right', }, { key: 'feed_supplier', value: allFeedSuppliers }, { key: 'doc_supplier', value: allDocSuppliers }, { key: 'rata_harga_doc', - value: formatCurrency(summary.total.total_average_doc_price_rp), + value: formatCurrency(summary.total.total_average_doc_price_rp || 0), align: 'right', }, { key: 'hpp_telur', - value: formatCurrency(summary.total.average_egg_hpp_rp_per_kg), + value: formatCurrency(summary.total.average_egg_hpp_rp_per_kg || 0), align: 'right', }, { key: 'nominal_sisa', - value: formatCurrency(summary.total.total_egg_value_rp), + value: formatCurrency(summary.total.total_egg_value_rp || 0), align: 'right', }, ]; }; const createPDFDocument = ( - data: HppPerKandangExportParams['data'], - params: HppPerKandangExportParams['params'] + params: HppPerKandangExportParams, + allFeedSuppliers: string, + allDocSuppliers: string ) => { - const rekapitulasiByWeightRange = data.summary?.per_weight_range || []; + const rekapitulasiByWeightRange = params.data.summary?.per_weight_range || []; + + const weightRangeText = + params.params?.weight_min || params.params?.weight_max + ? params.params.weight_min && params.params.weight_max + ? `${params.params.weight_min} - ${params.params.weight_max} kg` + : params.params.weight_min + ? `≥ ${params.params.weight_min} kg` + : `≤ ${params.params.weight_max} kg` + : '-'; return ( {/* Title and Parameters */} - + Laporan > HPP Harian Kandang - + - {getParameterText(params).map((param, index) => ( - - {param} - - ))} + + Area: {params.params?.area_name || 'Semua Area'} + + + Lokasi: {params.params?.location_name || 'Semua Lokasi'} + + + Kandang: {params.params?.kandang_name || 'Semua Kandang'} + + + Periode:{' '} + {params.params?.period + ? formatDate(params.params.period, 'DD MMM YYYY') + : '-'} + + Rentang Bobot: {weightRangeText} + {params.params?.show_unrecorded === 'true' && ( + Tampilkan: Tanpa Recording + )} + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + {/* Rekapitulasi Section */} - Rekapitulasi + + Rekapitulasi + - Detail Per Kandang + + Detail Per Kandang + @@ -379,10 +393,15 @@ const createPDFDocument = ( }; export const generateHppPerKandangPDF = async ( - data: HppPerKandangExportParams['data'], - params: HppPerKandangExportParams['params'] + params: HppPerKandangExportParams, + allFeedSuppliers: string, + allDocSuppliers: string ): Promise => { - const PDFDocument = createPDFDocument(data, params); + const PDFDocument = createPDFDocument( + params, + allFeedSuppliers, + allDocSuppliers + ); try { const blob = await pdf(PDFDocument).toBlob(); @@ -390,7 +409,8 @@ export const generateHppPerKandangPDF = async ( const link = document.createElement('a'); link.href = url; - const period = params.period || formatDate(new Date(), 'YYYY-MM-DD'); + const period = + params.params?.period || formatDate(new Date(), 'YYYY-MM-DD'); link.download = `laporan-hpp-harian-kandang-periode-${period}.pdf`; document.body.appendChild(link); diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 9e4d4004..d9690792 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -406,16 +406,23 @@ const HppPerKandangTab = () => { .join(', ') || 'Semua Kandang' : 'Semua Kandang'; - await generateHppPerKandangPDF(allDataForExport, { - area_name: areaName, - location_name: locationName, - kandang_name: kandangName, - period: tableFilterState.period, - weight_min: tableFilterState.weight_min, - weight_max: tableFilterState.weight_max, - show_unrecorded: tableFilterState.show_unrecorded.toString(), - sort_by: tableFilterState.sort_by, - }); + await generateHppPerKandangPDF( + { + data: allDataForExport, + params: { + area_name: areaName, + location_name: locationName, + kandang_name: kandangName, + period: tableFilterState.period, + weight_min: tableFilterState.weight_min, + weight_max: tableFilterState.weight_max, + show_unrecorded: tableFilterState.show_unrecorded.toString(), + sort_by: tableFilterState.sort_by, + }, + }, + allFeedSuppliers, + allDocSuppliers + ); toast.success('PDF berhasil dibuat dan diunduh.'); } catch { @@ -429,6 +436,8 @@ const HppPerKandangTab = () => { areaOptions, locationOptions, kandangOptions, + allFeedSuppliers, + allDocSuppliers, ]); const getTableColumns = (): ColumnDef[] => { From 2af83bed8a90a125fb379e4f705922075d311d45 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 13:37:34 +0700 Subject: [PATCH 06/16] refactor(FE): Refactor getDetailData to remove unused parameters --- .../report/sale/export/HppPerkandangExportPDF.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx b/src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx index 94c24e93..d9f33f27 100644 --- a/src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx +++ b/src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx @@ -203,11 +203,7 @@ const getDetailColumns = (): PdfColumn[] => [ }, ]; -const getDetailData = ( - rows: HppPerKandangRow[], - allFeedSuppliers: string, - allDocSuppliers: string -): PdfTbodyCell[][] => { +const getDetailData = (rows: HppPerKandangRow[]): PdfTbodyCell[][] => { return rows.map((item, index) => [ { key: 'no', value: index + 1 }, { key: 'kandang', value: item.kandang?.name || '-' }, @@ -370,11 +366,7 @@ const createPDFDocument = ( Date: Tue, 10 Feb 2026 14:00:28 +0700 Subject: [PATCH 07/16] refactor(FE): Refactor PDF components to support custom styles --- .../helper/pdf/badge/PdfParamBadge.tsx | 6 ++-- .../helper/pdf/badge/PdfStatusBadge.tsx | 35 +++++++++++-------- .../helper/pdf/typography/PdfTypography.tsx | 11 +++--- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/components/helper/pdf/badge/PdfParamBadge.tsx b/src/components/helper/pdf/badge/PdfParamBadge.tsx index 01fc1a63..fbc2cf2a 100644 --- a/src/components/helper/pdf/badge/PdfParamBadge.tsx +++ b/src/components/helper/pdf/badge/PdfParamBadge.tsx @@ -1,7 +1,9 @@ import { Text, View, StyleSheet } from '@react-pdf/renderer'; +import type { Style } from '@react-pdf/types'; type PdfParamBadgeProps = { children: React.ReactNode; + style?: Style; }; const styles = StyleSheet.create({ @@ -16,9 +18,9 @@ const styles = StyleSheet.create({ }, }); -export const PdfParamBadge = ({ children }: PdfParamBadgeProps) => { +export const PdfParamBadge = ({ children, style }: PdfParamBadgeProps) => { return ( - + {children} ); diff --git a/src/components/helper/pdf/badge/PdfStatusBadge.tsx b/src/components/helper/pdf/badge/PdfStatusBadge.tsx index e0444284..23c9c5e9 100644 --- a/src/components/helper/pdf/badge/PdfStatusBadge.tsx +++ b/src/components/helper/pdf/badge/PdfStatusBadge.tsx @@ -1,10 +1,9 @@ import { Text, View, StyleSheet } from '@react-pdf/renderer'; +import type { Style } from '@react-pdf/types'; type PdfStatusBadgeProps = { children: React.ReactNode; - backgroundColor?: string; - textColor?: string; - borderColor?: string; + style?: Style; }; const styles = StyleSheet.create({ @@ -16,30 +15,38 @@ const styles = StyleSheet.create({ fontWeight: 'bold', borderWidth: 1, borderStyle: 'solid', + backgroundColor: '#F5F5F5', + borderColor: '#E5E7EB', }, statusBadgeText: { fontSize: 7, fontWeight: 'bold', + color: '#333333', }, }); -export const PdfStatusBadge = ({ - children, - backgroundColor = '#F5F5F5', - textColor = '#333333', - borderColor = '#E5E7EB', -}: PdfStatusBadgeProps) => { +export const PdfStatusBadge = ({ children, style }: PdfStatusBadgeProps) => { + const styleRecord = style as Record; + const color = styleRecord?.color as string | undefined; + + const viewStyle = Object.entries(styleRecord || {}).reduce( + (acc, [key, value]) => { + if (key !== 'color') { + acc[key] = value; + } + return acc; + }, + {} as Record + ); + return ( 0 ? [viewStyle as Style] : []), ]} > - + {children} diff --git a/src/components/helper/pdf/typography/PdfTypography.tsx b/src/components/helper/pdf/typography/PdfTypography.tsx index 31efe449..43aac19a 100644 --- a/src/components/helper/pdf/typography/PdfTypography.tsx +++ b/src/components/helper/pdf/typography/PdfTypography.tsx @@ -1,5 +1,6 @@ import { Color } from '@/types/theme'; import { Text, StyleSheet } from '@react-pdf/renderer'; +import type { Style } from '@react-pdf/types'; type TypographySize = 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'small' | 'label'; @@ -10,7 +11,7 @@ type PdfTypographyProps = { size?: TypographySize; variant?: TypographyVariant; color?: string; - marginBottom?: number; + style?: Style; }; const styles = StyleSheet.create({ @@ -66,17 +67,13 @@ export const PdfTypography = ({ size = 'p', variant = 'default', color, - marginBottom, + style, }: PdfTypographyProps) => { const sizeStyle = styles[size]; const textColor = color || variantColors[variant]; - const customStyle = { - ...(marginBottom !== undefined && { marginBottom }), - }; - return ( - + {children} ); From 5cc51c52d9b1aa1e7d528d6d7c24cebc551075a2 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 14:02:53 +0700 Subject: [PATCH 08/16] feat(FE): Add PdfPageNumber component for rendering page numbers --- .../helper/pdf/layout/PdfPageNumber.tsx | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/components/helper/pdf/layout/PdfPageNumber.tsx diff --git a/src/components/helper/pdf/layout/PdfPageNumber.tsx b/src/components/helper/pdf/layout/PdfPageNumber.tsx new file mode 100644 index 00000000..977cac89 --- /dev/null +++ b/src/components/helper/pdf/layout/PdfPageNumber.tsx @@ -0,0 +1,48 @@ +import { Text, View, StyleSheet } from '@react-pdf/renderer'; +import type { Style } from '@react-pdf/types'; + +type PdfPageNumberProps = { + style?: Style; + /** + * Format template for page number. + * Use {pageNumber} and {totalPages} as placeholders. + * Default: "{pageNumber} / {totalPages}" + */ + format?: string; +}; + +const styles = StyleSheet.create({ + footer: { + width: '100%', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 16, + position: 'absolute', + fontSize: 8, + bottom: 30, + left: 0, + right: 0, + textAlign: 'center', + color: 'grey', + }, +}); + +export const PdfPageNumber = ({ + style, + format = '{pageNumber} / {totalPages}', +}: PdfPageNumberProps) => { + return ( + + + format + .replace('{pageNumber}', String(pageNumber)) + .replace('{totalPages}', String(totalPages)) + } + fixed + /> + + ); +}; From 4c6ac6e8e10df7e9fddb14feed4554556c86014a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 14:04:44 +0700 Subject: [PATCH 09/16] refactor(FE): Refactor PdfStatusBadge to use a single style prop --- .../finance/export/CustomerPaymentExportPDF.tsx | 8 +++++--- .../finance/export/DebtSupllierExportPDF.tsx | 16 ++++++++++------ .../export/PurchasesPerSupplierExportPDF.tsx | 8 +++++--- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx index 1c374e4b..b0cb5c8d 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -162,9 +162,11 @@ const getTableData = ( value: item.status ? ( {formatTitleCase(item.status)} diff --git a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx index b5c26525..849ce4ef 100644 --- a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx +++ b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx @@ -113,9 +113,11 @@ const getTableData = (rows: DebtSupplier['rows']): PdfTbodyCell[][] => { item.due_status && item.due_status !== '-' ? ( {item.due_status} @@ -148,9 +150,11 @@ const getTableData = (rows: DebtSupplier['rows']): PdfTbodyCell[][] => { item.status && item.status !== '-' ? ( {item.status} diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx index cc2b7976..3423ca69 100644 --- a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx @@ -144,9 +144,11 @@ const getTableData = ( value: item.expedition ? ( {item.expedition} From be7b2a0f938c8d6e1864c62f37dbdb6255001449 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 16:06:11 +0700 Subject: [PATCH 10/16] refactor(FE): Refactor DailyMarketingReportPDF component for cleaner structure --- .../pages/report/DailyMarketingReportPDF.tsx | 762 ++++++------------ 1 file changed, 228 insertions(+), 534 deletions(-) diff --git a/src/components/pages/report/DailyMarketingReportPDF.tsx b/src/components/pages/report/DailyMarketingReportPDF.tsx index 86ee29bc..53b36b80 100644 --- a/src/components/pages/report/DailyMarketingReportPDF.tsx +++ b/src/components/pages/report/DailyMarketingReportPDF.tsx @@ -1,275 +1,226 @@ 'use client'; -import { - Document, - Image, - Page, - StyleSheet, - Text, - View, -} from '@react-pdf/renderer'; - +import { Page, View, Document, StyleSheet, Font } from '@react-pdf/renderer'; import { DailyMarketingReport, SalesSummary, } from '@/types/api/report/marketing'; -import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { + formatCurrency, + formatDate, + formatNumber, + formatTitleCase, +} from '@/lib/helper'; +import { + PdfTable, + PdfColumn, + PdfTbodyCell, + PdfTfootCell, +} from '@/components/helper/pdf/table'; +import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; +import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge'; +import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; +import { PdfPageNumber } from '@/components/helper/pdf/layout/PdfPageNumber'; + +Font.register({ + family: 'Helvetica', + src: 'helvetica', +}); + +const pdfStyles = StyleSheet.create({ + page: { + fontSize: 10, + fontFamily: 'Helvetica', + padding: 20, + backgroundColor: '#FFFFFF', + }, + titleSection: { + marginBottom: 10, + }, + parameterContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + marginBottom: 8, + }, +}); interface DailyMarketingReportPDFProps { data?: DailyMarketingReport; total?: SalesSummary; } -const DailyMarketingReportPDFStyle = StyleSheet.create({ - page: { - paddingTop: 24, - paddingBottom: 64, - paddingHorizontal: 16, // Reduce padding to fit more columns - orientation: 'landscape', +const getTableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'so_date', header: 'Tanggal Sales Order', flex: 1.3, align: 'center' }, + { + key: 'do_date', + header: 'Tanggal Delivery Order', + flex: 1.3, + align: 'center', }, + { key: 'aging', header: 'Aging (Hari)', flex: 0.7, align: 'center' }, + { key: 'warehouse', header: 'Gudang', flex: 1.2, align: 'left' }, + { key: 'customer', header: 'Pelanggan', flex: 1.5, align: 'left' }, + { key: 'sales', header: 'Sales', flex: 1, align: 'left' }, + { key: 'product', header: 'Produk', flex: 1.3, align: 'left' }, + { key: 'do_number', header: 'Nomor DO', flex: 1.2, align: 'left' }, + { key: 'vehicle', header: 'Nomor Polisi', flex: 1, align: 'left' }, + { key: 'marketing_type', header: 'Tipe Marketing', flex: 1, align: 'center' }, + { key: 'qty', header: 'Quantity', flex: 0.7, align: 'right' }, + { key: 'avg_weight', header: 'Rata-Rata (Kg)', flex: 0.8, align: 'right' }, + { + key: 'total_weight', + header: 'Total Berat (Kg)', + flex: 0.9, + align: 'right', + }, + { key: 'sales_price', header: 'Harga Jual (Rp)', flex: 0.9, align: 'right' }, + { key: 'hpp_price', header: 'HPP (Rp)', flex: 1.3, align: 'right' }, + { key: 'sales_amount', header: 'Total Jual (Rp)', flex: 1, align: 'right' }, + { key: 'hpp_amount', header: 'Total HPP (Rp)', flex: 1.3, align: 'right' }, +]; - companyInfoHeader: { - width: '100%', - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - marginBottom: 8, - }, - companyLogo: { - width: 64, - height: 'auto', - }, - companyInfoHeaderDate: { - paddingTop: 8, - fontSize: 10, - }, - companyName: { - fontSize: 12, - fontWeight: 'bold', - marginBottom: 4, - }, - companyAddress: { - fontSize: 8, - maxWidth: 400, - marginBottom: 10, - }, +const getTableData = (rows: DailyMarketingReport): PdfTbodyCell[][] => { + return rows.map((row, index) => [ + { key: 'no', value: index + 1 }, + { + key: 'so_date', + value: row.so_date ? formatDate(row.so_date, 'DD MMM YY') : '-', + }, + { + key: 'do_date', + value: row.realization_date + ? formatDate(row.realization_date, 'DD MMM YY') + : '-', + }, + { key: 'aging', value: row.aging_days ?? '-' }, + { key: 'warehouse', value: row.warehouse?.name ?? '-' }, + { key: 'customer', value: row.customer?.name ?? '-' }, + { key: 'sales', value: row.sales?.name ?? '-' }, + { key: 'product', value: row.product?.name ?? '-' }, + { key: 'do_number', value: row.do_number ?? '-' }, + { key: 'vehicle', value: row.vehicle_number ?? '-' }, + { + key: 'marketing_type', + value: row.marketing_type ? ( + + + {formatTitleCase(row.marketing_type)} + + + ) : ( + '-' + ), + }, + { key: 'qty', value: formatNumber(row.qty ?? 0), align: 'right' }, + { + key: 'avg_weight', + value: formatNumber(row.average_weight_kg ?? 0), + align: 'right', + }, + { + key: 'total_weight', + value: formatNumber(row.total_weight_kg ?? 0), + align: 'right', + }, + { + key: 'sales_price', + value: formatCurrency(row.sales_price_per_kg ?? 0), + align: 'right', + }, + { + key: 'hpp_price', + value: formatCurrency(row.hpp_price_per_kg ?? 0), + align: 'right', + }, + { + key: 'sales_amount', + value: formatCurrency(row.sales_amount ?? 0), + align: 'right', + }, + { + key: 'hpp_amount', + value: formatCurrency(row.hpp_amount ?? 0), + align: 'right', + }, + ]); +}; - title: { - marginTop: 16, - fontSize: 14, - lineHeight: '150%', - textAlign: 'center', - fontFamily: 'Times-Roman', - fontWeight: 'bold', - }, +const getTableFooter = (summary?: SalesSummary): PdfTfootCell[] => { + if (!summary) return []; - footer: { - width: '100%', - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingHorizontal: 16, - - position: 'absolute', - fontSize: 8, - bottom: 30, - left: 0, - right: 0, - textAlign: 'center', - color: 'grey', - }, - - // Table Styles - table: { - width: '100%', - marginTop: 16, - borderWidth: 1, - borderColor: '#000000', - borderBottomWidth: 0, - fontSize: 7, // Smaller font for report - }, - tableRow: { - flexDirection: 'row', - borderBottomWidth: 1, - borderBottomColor: '#000000', - alignItems: 'center', - minHeight: 20, - }, - tableHeader: { - backgroundColor: '#f0f0f0', - fontWeight: 'bold', - }, - - // Columns definition (Total 100%) - colNo: { - width: '3%', - padding: 2, - textAlign: 'center', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colSoDate: { - width: '6%', - padding: 2, - textAlign: 'left', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colDoDate: { - width: '6%', - padding: 2, - textAlign: 'left', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colAging: { - width: '3%', - padding: 2, - textAlign: 'center', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colWarehouse: { - width: '7%', - padding: 2, - textAlign: 'left', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colCustomer: { - width: '9%', - padding: 2, - textAlign: 'left', - borderRightWidth: 1, - borderRightColor: '#000000', - }, // Reduced slightly - colSales: { - width: '6%', - padding: 2, - textAlign: 'left', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colProduct: { - width: '8%', - padding: 2, - textAlign: 'left', - borderRightWidth: 1, - borderRightColor: '#000000', - }, // Reduced slightly - colDoNumber: { - width: '7%', - padding: 2, - textAlign: 'left', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colVehicle: { - width: '5%', - padding: 2, - textAlign: 'left', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colMarketingType: { - width: '5%', - padding: 2, - textAlign: 'left', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colQty: { - width: '4%', - padding: 2, - textAlign: 'right', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colAvgWeight: { - width: '4%', - padding: 2, - textAlign: 'right', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colTotalWeight: { - width: '5%', - padding: 2, - textAlign: 'right', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colSalesPrice: { - width: '5%', - padding: 2, - textAlign: 'right', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colHppPrice: { - width: '5%', - padding: 2, - textAlign: 'right', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colSalesAmount: { - width: '6%', - padding: 2, - textAlign: 'right', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colHppAmount: { width: '6%', padding: 2, textAlign: 'right' }, // Last column - - // Text inside columns - cellText: { - fontSize: 6, - }, - headerText: { - fontSize: 7, - fontWeight: 'bold', - textAlign: 'center', - }, - - // Utils - doubleDivider: { - width: '100%', - height: 6, - borderTop: '2px solid black', - borderBottom: '2px solid black', - }, - - // Summary - summaryContainer: { - marginTop: 12, - flexDirection: 'row', - justifyContent: 'flex-end', - width: '100%', - }, - summaryTable: { - width: '30%', - borderWidth: 1, - borderColor: '#000000', - fontSize: 8, - }, - summaryRow: { - flexDirection: 'row', - padding: 2, - borderBottomWidth: 1, - borderBottomColor: '#eee', - }, - summaryLabel: { - width: '50%', - fontWeight: 'bold', - }, - summaryValue: { - width: '50%', - textAlign: 'right', - }, -}); + return [ + { key: 'no', value: 'TOTAL' }, + { key: 'so_date', value: '' }, + { key: 'do_date', value: '' }, + { key: 'aging', value: '' }, + { key: 'warehouse', value: '' }, + { key: 'customer', value: '' }, + { key: 'sales', value: '' }, + { key: 'product', value: '' }, + { key: 'do_number', value: '' }, + { key: 'vehicle', value: '' }, + { key: 'marketing_type', value: '' }, + { + key: 'qty', + value: formatNumber(summary.total_qty ?? 0), + align: 'right', + }, + { + key: 'avg_weight', + value: formatNumber(summary.total_weight_kg ?? 0), + align: 'right', + }, + { + key: 'total_weight', + value: formatNumber(summary.total_weight_kg ?? 0), + align: 'right', + }, + { key: 'sales_price', value: '' }, + { + key: 'hpp_price', + value: formatCurrency(summary.total_hpp_price_per_kg ?? 0), + align: 'right', + }, + { + key: 'sales_amount', + value: formatCurrency(summary.total_sales_amount ?? 0), + align: 'right', + }, + { + key: 'hpp_amount', + value: formatCurrency(summary.total_hpp_amount ?? 0), + align: 'right', + }, + ]; +}; const DailyMarketingReportPDF = ({ data, @@ -280,288 +231,31 @@ const DailyMarketingReportPDF = ({ return ( - - - - - - - {formatDate(Date.now(), 'DD MMMM YYYY')} - - - - - - PT LUMBUNG TELUR INDONESIA - - - SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. - Cipedes, Kec. Sukajadi, Kota Bandung 40162 - - - + + {/* Title and Parameters */} + + + Laporan > Penjualan Harian + + + + Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')} + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + - - Laporan Penjualan Harian - + {/* Table */} + - {/* Data Table */} - - {/* Header */} - - - No - - - - Tgl SO - - - - - Tgl DO - - - - Aging - - - - Gudang - - - - - Pelanggan - - - - Sales - - - - Produk - - - - No DO - - - - Plat No - - - - Tipe - - - Qty - - - - Rerata - - - - Berat - - - - Hrg Jual - - - - - HPP/kg - - - - - Total Jual - - - - - Total HPP - - - - - {/* Rows */} - {rows.map((row, index) => ( - - - - {index + 1} - - - - - {formatDate(row.so_date, 'DD/MM/YYYY')} - - - - - {formatDate(row.realization_date, 'DD/MM/YYYY')} - - - - - {row.aging_days} - - - - - {row.warehouse?.name} - - - - - {row.customer?.name} - - - - - {row.sales.name} - - - - - {row.product?.name} - - - - - {row.do_number} - - - - - {row.vehicle_number} - - - - - {row.marketing_type} - - - - - {formatNumber(row.qty)} - - - - - {formatNumber(row.average_weight_kg)} - - - - - {formatNumber(row.total_weight_kg)} - - - - - {formatCurrency(row.sales_price_per_kg)} - - - - - {formatCurrency(row.hpp_price_per_kg)} - - - - - {formatCurrency(row.sales_amount)} - - - - - {formatCurrency(row.hpp_amount)} - - - - ))} - - - {/* Summary */} - - - - - Total Qty: - - - {formatNumber(summary?.total_qty ?? 0)} - - - - - Total Berat (kg): - - - {formatNumber(summary?.total_weight_kg ?? 0)} - - - - - Total Penjualan: - - - {formatCurrency(summary?.total_sales_amount ?? 0)} - - - - - Total HPP Per KG: - - - {formatCurrency(summary?.total_hpp_price_per_kg ?? 0)} - - - - - Total HPP: - - - {formatCurrency(summary?.total_hpp_amount ?? 0)} - - - - - - - - `${pageNumber} / ${totalPages}` - } - fixed - /> - + ); From 5593463eab2ee11713cf82af08e9a6686a4a7c74 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 16:20:19 +0700 Subject: [PATCH 11/16] refactor(FE): Refactor marketing report components into a dedicated folder --- src/app/report/marketing/page.tsx | 2 +- .../{ => marketing}/DailyMarketingsTable.tsx | 0 .../MarketingReportContent.tsx | 4 +- .../export}/DailyMarketingReportPDF.tsx | 0 .../export/HppPerkandangExportPDF.tsx | 0 .../export/HppPerkandangExportXLSX.tsx | 0 .../tab}/DailyMarketingReportContent.tsx | 4 +- .../tab/HppPerKandangTab.tsx | 4 +- .../pages/report/sale/SaleReportTabs.tsx | 37 ------------------- 9 files changed, 7 insertions(+), 44 deletions(-) rename src/components/pages/report/{ => marketing}/DailyMarketingsTable.tsx (100%) rename src/components/pages/report/{ => marketing}/MarketingReportContent.tsx (88%) rename src/components/pages/report/{ => marketing/export}/DailyMarketingReportPDF.tsx (100%) rename src/components/pages/report/{sale => marketing}/export/HppPerkandangExportPDF.tsx (100%) rename src/components/pages/report/{sale => marketing}/export/HppPerkandangExportXLSX.tsx (100%) rename src/components/pages/report/{ => marketing/tab}/DailyMarketingReportContent.tsx (98%) rename src/components/pages/report/{sale => marketing}/tab/HppPerKandangTab.tsx (99%) delete mode 100644 src/components/pages/report/sale/SaleReportTabs.tsx diff --git a/src/app/report/marketing/page.tsx b/src/app/report/marketing/page.tsx index 52a3d4dd..87ed7a1a 100644 --- a/src/app/report/marketing/page.tsx +++ b/src/app/report/marketing/page.tsx @@ -1,4 +1,4 @@ -import MarketingReportContent from '@/components/pages/report/MarketingReportContent'; +import MarketingReportContent from '@/components/pages/report/marketing/MarketingReportContent'; const MarketingReportPage = () => { return ( diff --git a/src/components/pages/report/DailyMarketingsTable.tsx b/src/components/pages/report/marketing/DailyMarketingsTable.tsx similarity index 100% rename from src/components/pages/report/DailyMarketingsTable.tsx rename to src/components/pages/report/marketing/DailyMarketingsTable.tsx diff --git a/src/components/pages/report/MarketingReportContent.tsx b/src/components/pages/report/marketing/MarketingReportContent.tsx similarity index 88% rename from src/components/pages/report/MarketingReportContent.tsx rename to src/components/pages/report/marketing/MarketingReportContent.tsx index 3ebacecb..e38a39d4 100644 --- a/src/components/pages/report/MarketingReportContent.tsx +++ b/src/components/pages/report/marketing/MarketingReportContent.tsx @@ -3,8 +3,8 @@ import { JSX, useState } from 'react'; import Tabs from '@/components/Tabs'; -import DailyMarketingReportContent from '@/components/pages/report/DailyMarketingReportContent'; -import HppPerKandangTab from './sale/tab/HppPerKandangTab'; +import DailyMarketingReportContent from '@/components/pages/report/marketing/tab/DailyMarketingReportContent'; +import HppPerKandangTab from '@/components/pages/report/marketing/tab/HppPerKandangTab'; type MarketingReportTabType = | 'daily' diff --git a/src/components/pages/report/DailyMarketingReportPDF.tsx b/src/components/pages/report/marketing/export/DailyMarketingReportPDF.tsx similarity index 100% rename from src/components/pages/report/DailyMarketingReportPDF.tsx rename to src/components/pages/report/marketing/export/DailyMarketingReportPDF.tsx diff --git a/src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx b/src/components/pages/report/marketing/export/HppPerkandangExportPDF.tsx similarity index 100% rename from src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx rename to src/components/pages/report/marketing/export/HppPerkandangExportPDF.tsx diff --git a/src/components/pages/report/sale/export/HppPerkandangExportXLSX.tsx b/src/components/pages/report/marketing/export/HppPerkandangExportXLSX.tsx similarity index 100% rename from src/components/pages/report/sale/export/HppPerkandangExportXLSX.tsx rename to src/components/pages/report/marketing/export/HppPerkandangExportXLSX.tsx diff --git a/src/components/pages/report/DailyMarketingReportContent.tsx b/src/components/pages/report/marketing/tab/DailyMarketingReportContent.tsx similarity index 98% rename from src/components/pages/report/DailyMarketingReportContent.tsx rename to src/components/pages/report/marketing/tab/DailyMarketingReportContent.tsx index 01c360d0..ca5ec12f 100644 --- a/src/components/pages/report/DailyMarketingReportContent.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingReportContent.tsx @@ -14,9 +14,9 @@ import SelectInput, { } from '@/components/input/SelectInput'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; -import DailyMarketingsTable from '@/components/pages/report/DailyMarketingsTable'; +import DailyMarketingsTable from '@/components/pages/report/marketing/DailyMarketingsTable'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import DailyMarketingReportPDF from '@/components/pages/report/DailyMarketingReportPDF'; +import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingReportPDF'; import { Area } from '@/types/api/master-data/area'; import { diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx similarity index 99% rename from src/components/pages/report/sale/tab/HppPerKandangTab.tsx rename to src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index d9690792..c0371abf 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -26,8 +26,8 @@ import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; import MenuItem from '@/components/menu/MenuItem'; import Menu from '@/components/menu/Menu'; -import { generateHppPerKandangPDF } from '../export/HppPerkandangExportPDF'; -import { generateHppPerKandangExcel } from '../export/HppPerkandangExportXLSX'; +import { generateHppPerKandangPDF } from '@/components/pages/report/marketing/export/HppPerkandangExportPDF'; +import { generateHppPerKandangExcel } from '@/components/pages/report/marketing/export/HppPerkandangExportXLSX'; import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; diff --git a/src/components/pages/report/sale/SaleReportTabs.tsx b/src/components/pages/report/sale/SaleReportTabs.tsx deleted file mode 100644 index 988c16b2..00000000 --- a/src/components/pages/report/sale/SaleReportTabs.tsx +++ /dev/null @@ -1,37 +0,0 @@ -'use client'; - -import Tabs from '@/components/Tabs'; -import HppPerKandangTab from '@/components/pages/report/sale/tab/HppPerKandangTab'; - -const SaleReportTabs = () => { - const tabs = [ - // { - // id: '1', - // label: 'Penjualan Harian', - // content: 'Penjualan Harian Tab', - // }, - // { - // id: '2', - // label: 'Transaksi Penjualan DO', - // content: 'Transaksi Penjualan DO Tab', - // }, - // { - // id: '3', - // label: 'Perbandingan HPP Per Rentang BW', - // content: 'Perbandingan HPP Per Rentang BW Tab', - // }, - { - id: '4', - label: 'HPP Harian Kandang', - content: , - }, - ]; - - return ( -
- -
- ); -}; - -export default SaleReportTabs; From 1227b7639ff87983518152f403346b07776ca9f4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 16:52:52 +0700 Subject: [PATCH 12/16] refactor(FE): Refactor ProductionResultReportPDF to use reusable PDF components --- .../ProductionResultReportPDF.tsx | 679 ++++++++++-------- 1 file changed, 376 insertions(+), 303 deletions(-) diff --git a/src/components/pages/report/production-result/ProductionResultReportPDF.tsx b/src/components/pages/report/production-result/ProductionResultReportPDF.tsx index 9bc27c4b..139f4640 100644 --- a/src/components/pages/report/production-result/ProductionResultReportPDF.tsx +++ b/src/components/pages/report/production-result/ProductionResultReportPDF.tsx @@ -1,18 +1,19 @@ 'use client'; import React from 'react'; -import { - Document, - Page, - StyleSheet, - Text, - View, - Image, -} from '@react-pdf/renderer'; +import { Document, Page, StyleSheet, View, Text } from '@react-pdf/renderer'; import { formatDate, formatNumber } from '@/lib/helper'; import { BaseProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; import { ProductionResult } from '@/types/api/report/production-result'; +import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; +import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; +import { PdfPageNumber } from '@/components/helper/pdf/layout/PdfPageNumber'; +import { + PdfTable, + PdfColumn, + PdfTbodyCell, +} from '@/components/helper/pdf/table'; type MappedProductionResultsItem = { projectFlockKandang: BaseProjectFlockKandang; @@ -25,132 +26,28 @@ interface ProductionResultReportPDFProps { const styles = StyleSheet.create({ page: { - paddingTop: 24, - paddingBottom: 52, - paddingHorizontal: 16, - }, - - companyInfoHeader: { - width: '100%', - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - marginBottom: 8, - }, - companyLogo: { - width: 64, - height: 'auto', - }, - companyInfoHeaderDate: { - paddingTop: 8, fontSize: 10, + fontFamily: 'Helvetica', + padding: 20, + backgroundColor: '#FFFFFF', }, - companyName: { - fontSize: 12, - fontWeight: 'bold', - marginBottom: 4, - }, - companyAddress: { - fontSize: 8, - maxWidth: 420, + titleSection: { marginBottom: 10, }, - doubleDivider: { - width: '100%', - height: 6, - borderTopWidth: 2, - borderTopColor: '#000', - borderBottomWidth: 2, - borderBottomColor: '#000', - }, - - title: { - marginTop: 14, - fontSize: 14, - lineHeight: '150%', - textAlign: 'center', - fontFamily: 'Times-Roman', - fontWeight: 'bold', - }, - - footer: { - width: '100%', - display: 'flex', + parameterContainer: { flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingHorizontal: 16, - position: 'absolute', - fontSize: 8, - bottom: 22, - left: 0, - right: 0, - textAlign: 'center', - color: 'grey', + flexWrap: 'wrap', + marginBottom: 8, }, - - section: { - marginTop: 12, - borderWidth: 1, - borderColor: '#000', - padding: 8, + tableSection: { + marginBottom: 12, }, - - sectionHeader: { - marginBottom: 6, - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'baseline', - }, - sectionTitle: { + tableTitle: { fontSize: 10, fontWeight: 'bold', + marginBottom: 6, + color: '#333', }, - sectionSubtitle: { - fontSize: 8, - color: '#444', - }, - - // Simple grid table (label/value pairs) - grid: { - width: '100%', - borderWidth: 1, - borderColor: '#000', - }, - gridRow: { - flexDirection: 'row', - borderBottomWidth: 1, - borderBottomColor: '#000', - }, - gridRowLast: { - borderBottomWidth: 0, - }, - gridCellLabel: { - width: '40%', - paddingVertical: 3, - paddingHorizontal: 6, - fontSize: 8, - borderRightWidth: 1, - borderRightColor: '#000', - fontWeight: 'bold', - }, - gridCellValue: { - width: '60%', - paddingVertical: 3, - paddingHorizontal: 6, - fontSize: 8, - textAlign: 'right', - }, - - // Subsection headings - groupTitle: { - marginTop: 8, - marginBottom: 4, - fontSize: 9, - fontWeight: 'bold', - }, - emptyText: { fontSize: 8, color: '#666', @@ -169,125 +66,243 @@ function valueText(v: unknown) { return String(v); } -/** - * Render label/value table for one ProductionResult. - * Uses a compact grid to keep page readable. - */ -function ProductionResultGrid({ pr }: { pr: ProductionResult }) { - const rows: Array<[string, string]> = [ - ['WOA', valueText(pr.woa)], +// ======================================== +// TABLE 1: WOA & BW +// ======================================== +const getBwTableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'woa', header: 'WOA', flex: 0.8, align: 'center' }, + { key: 'bw', header: 'BW', flex: 1, align: 'right' }, + { key: 'std_bw', header: 'Std BW', flex: 1, align: 'right' }, + { key: 'uniformity', header: 'Uniformity', flex: 1.2, align: 'right' }, + { + key: 'std_uniformity', + header: 'Std Uniformity', + flex: 1.3, + align: 'right', + }, +]; - // BW - ['BW', valueText(pr.bw)], - ['Std BW', valueText(pr.std_bw)], - ['Uniformity', valueText(pr.uniformity)], - ['Std Uniformity', valueText(pr.std_uniformity)], +const getBwTableData = ( + productionResults: ProductionResult[] +): PdfTbodyCell[][] => { + return productionResults.map((pr, index) => { + return [ + { key: 'no', value: index + 1 }, + { key: 'woa', value: valueText(pr.woa) }, + { key: 'bw', value: valueText(pr.bw), align: 'right' }, + { key: 'std_bw', value: valueText(pr.std_bw), align: 'right' }, + { key: 'uniformity', value: valueText(pr.uniformity), align: 'right' }, + { + key: 'std_uniformity', + value: valueText(pr.std_uniformity), + align: 'right', + }, + ]; + }); +}; - // Dep - ['Dep Kum', valueText(pr.dep_kum)], - ['Dep Std', valueText(pr.dep_std)], +// ======================================== +// TABLE 2: DEPLESI +// ======================================== +const getDepTableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'dep_kum', header: 'Dep Kum', flex: 1.5, align: 'right' }, + { key: 'dep_std', header: 'Dep Std', flex: 1.5, align: 'right' }, +]; - // Butiran - ['Butiran Utuh', valueText(pr.butiran_utuh)], - ['Butiran Putih', valueText(pr.butiran_putih)], - ['Butiran Retak', valueText(pr.butiran_retak)], - ['Butiran Pecah', valueText(pr.butiran_pecah)], - ['Butiran Jumlah', valueText(pr.butiran_jumlah)], - ['Total Butir', valueText(pr.total_butir)], +const getDepTableData = ( + productionResults: ProductionResult[] +): PdfTbodyCell[][] => { + return productionResults.map((pr, index) => { + return [ + { key: 'no', value: index + 1 }, + { key: 'dep_kum', value: valueText(pr.dep_kum), align: 'right' }, + { key: 'dep_std', value: valueText(pr.dep_std), align: 'right' }, + ]; + }); +}; - // Kg - ['Kg Utuh', valueText(pr.kg_utuh)], - ['Kg Putih', valueText(pr.kg_putih)], - ['Kg Retak', valueText(pr.kg_retak)], - ['Kg Pecah', valueText(pr.kg_pecah)], - ['Kg Jumlah', valueText(pr.kg_jumlah)], - ['Total Kg', valueText(pr.total_kg)], +// ======================================== +// TABLE 3: BUTIRAN +// ======================================== +const getButiranTableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'butiran_utuh', header: 'Utuh', flex: 1.2, align: 'right' }, + { key: 'butiran_putih', header: 'Putih', flex: 1.2, align: 'right' }, + { key: 'butiran_retak', header: 'Retak', flex: 1.2, align: 'right' }, + { key: 'butiran_pecah', header: 'Pecah', flex: 1.2, align: 'right' }, + { key: 'butiran_jumlah', header: 'Jumlah', flex: 1.2, align: 'right' }, + { key: 'total_butir', header: 'Total Butir', flex: 1.3, align: 'right' }, +]; - // % - ['% Utuh', valueText(pr.persen_utuh)], - ['% Putih', valueText(pr.persen_putih)], - ['% Retak', valueText(pr.persen_retak)], - ['% Pecah', valueText(pr.persen_pecah)], +const getButiranTableData = ( + productionResults: ProductionResult[] +): PdfTbodyCell[][] => { + return productionResults.map((pr, index) => { + return [ + { key: 'no', value: index + 1 }, + { + key: 'butiran_utuh', + value: valueText(pr.butiran_utuh), + align: 'right', + }, + { + key: 'butiran_putih', + value: valueText(pr.butiran_putih), + align: 'right', + }, + { + key: 'butiran_retak', + value: valueText(pr.butiran_retak), + align: 'right', + }, + { + key: 'butiran_pecah', + value: valueText(pr.butiran_pecah), + align: 'right', + }, + { + key: 'butiran_jumlah', + value: valueText(pr.butiran_jumlah), + align: 'right', + }, + { + key: 'total_butir', + value: valueText(pr.total_butir), + align: 'right', + }, + ]; + }); +}; - // Produksi - ['HD', valueText(pr.hd)], - ['HD Std', valueText(pr.hd_std)], - ['FI', valueText(pr.fi)], - ['FI Std', valueText(pr.fi_std)], - ['EM', valueText(pr.em)], - ['EM Std', valueText(pr.em_std)], - ['EW', valueText(pr.ew)], - ['EW Std', valueText(pr.ew_std)], - ['FCR', valueText(pr.fcr)], - ['FCR Std', valueText(pr.fcr_std)], - ['HH', valueText(pr.hh)], - ['HH Std', valueText(pr.hh_std)], - ]; +// ======================================== +// TABLE 4: BERAT (KG) +// ======================================== +const getKgTableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'kg_utuh', header: 'Utuh (Kg)', flex: 1.2, align: 'right' }, + { key: 'kg_putih', header: 'Putih (Kg)', flex: 1.2, align: 'right' }, + { key: 'kg_retak', header: 'Retak (Kg)', flex: 1.2, align: 'right' }, + { key: 'kg_pecah', header: 'Pecah (Kg)', flex: 1.2, align: 'right' }, + { key: 'kg_jumlah', header: 'Jumlah (Kg)', flex: 1.3, align: 'right' }, + { key: 'total_kg', header: 'Total (Kg)', flex: 1.3, align: 'right' }, +]; - return ( - - {rows.map(([label, value], idx) => { - const isLast = idx === rows.length - 1; - return ( - - {label} - {value} - - ); - })} - - ); -} +const getKgTableData = ( + productionResults: ProductionResult[] +): PdfTbodyCell[][] => { + return productionResults.map((pr, index) => { + return [ + { key: 'no', value: index + 1 }, + { key: 'kg_utuh', value: valueText(pr.kg_utuh), align: 'right' }, + { key: 'kg_putih', value: valueText(pr.kg_putih), align: 'right' }, + { key: 'kg_retak', value: valueText(pr.kg_retak), align: 'right' }, + { key: 'kg_pecah', value: valueText(pr.kg_pecah), align: 'right' }, + { key: 'kg_jumlah', value: valueText(pr.kg_jumlah), align: 'right' }, + { key: 'total_kg', value: valueText(pr.total_kg), align: 'right' }, + ]; + }); +}; -/** - * If there are multiple ProductionResult entries for a kandang, - * we show them sequentially with a small header per result. - * - * You can later change this to render only the latest WOA, or group by week. - */ -function ProductionResultList({ - productionResults, -}: { - productionResults: ProductionResult[]; -}) { - return ( - - {productionResults.map((pr, idx) => { - const kandangName = - pr.project_flock?.kandang?.name || - pr.project_flock?.kandang?.id?.toString() || - ''; +// ======================================== +// TABLE 5: PERSENTASE +// ======================================== +const getPersenTableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'persen_utuh', header: '% Utuh', flex: 1.5, align: 'right' }, + { key: 'persen_putih', header: '% Putih', flex: 1.5, align: 'right' }, + { key: 'persen_retak', header: '% Retak', flex: 1.5, align: 'right' }, + { key: 'persen_pecah', header: '% Pecah', flex: 1.5, align: 'right' }, +]; - // Optional: show a compact subheader - const headerLeft = `Data #${idx + 1}`; - const headerRight = - kandangName && pr.woa !== undefined - ? `${kandangName} • WOA ${safeNum(pr.woa)}` - : pr.woa !== undefined - ? `WOA ${safeNum(pr.woa)}` - : ''; +const getPersenTableData = ( + productionResults: ProductionResult[] +): PdfTbodyCell[][] => { + return productionResults.map((pr, index) => { + return [ + { key: 'no', value: index + 1 }, + { + key: 'persen_utuh', + value: valueText(pr.persen_utuh), + align: 'right', + }, + { + key: 'persen_putih', + value: valueText(pr.persen_putih), + align: 'right', + }, + { + key: 'persen_retak', + value: valueText(pr.persen_retak), + align: 'right', + }, + { + key: 'persen_pecah', + value: valueText(pr.persen_pecah), + align: 'right', + }, + ]; + }); +}; - return ( - - - {headerLeft} - {headerRight} - +// ======================================== +// TABLE 6: PRODUKSI (HD, FI, EM, EW) +// ======================================== +const getProduksi1TableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'hd', header: 'HD', flex: 0.8, align: 'right' }, + { key: 'hd_std', header: 'HD Std', flex: 1, align: 'right' }, + { key: 'fi', header: 'FI', flex: 0.8, align: 'right' }, + { key: 'fi_std', header: 'FI Std', flex: 1, align: 'right' }, + { key: 'em', header: 'EM', flex: 0.8, align: 'right' }, + { key: 'em_std', header: 'EM Std', flex: 1, align: 'right' }, + { key: 'ew', header: 'EW', flex: 0.8, align: 'right' }, + { key: 'ew_std', header: 'EW Std', flex: 1, align: 'right' }, +]; - - - ); - })} - - ); -} +const getProduksi1TableData = ( + productionResults: ProductionResult[] +): PdfTbodyCell[][] => { + return productionResults.map((pr, index) => { + return [ + { key: 'no', value: index + 1 }, + { key: 'hd', value: valueText(pr.hd), align: 'right' }, + { key: 'hd_std', value: valueText(pr.hd_std), align: 'right' }, + { key: 'fi', value: valueText(pr.fi), align: 'right' }, + { key: 'fi_std', value: valueText(pr.fi_std), align: 'right' }, + { key: 'em', value: valueText(pr.em), align: 'right' }, + { key: 'em_std', value: valueText(pr.em_std), align: 'right' }, + { key: 'ew', value: valueText(pr.ew), align: 'right' }, + { key: 'ew_std', value: valueText(pr.ew_std), align: 'right' }, + ]; + }); +}; + +// ======================================== +// TABLE 7: PRODUKSI (FCR, HH) +// ======================================== +const getProduksi2TableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'fcr', header: 'FCR', flex: 1, align: 'right' }, + { key: 'fcr_std', header: 'FCR Std', flex: 1.2, align: 'right' }, + { key: 'hh', header: 'HH', flex: 1, align: 'right' }, + { key: 'hh_std', header: 'HH Std', flex: 1.2, align: 'right' }, +]; + +const getProduksi2TableData = ( + productionResults: ProductionResult[] +): PdfTbodyCell[][] => { + return productionResults.map((pr, index) => { + return [ + { key: 'no', value: index + 1 }, + { key: 'fcr', value: valueText(pr.fcr), align: 'right' }, + { key: 'fcr_std', value: valueText(pr.fcr_std), align: 'right' }, + { key: 'hh', value: valueText(pr.hh), align: 'right' }, + { key: 'hh_std', value: valueText(pr.hh_std), align: 'right' }, + ]; + }); +}; /** * ✅ Main PDF Component @@ -297,90 +312,148 @@ const ProductionResultReportPDF = ({ }: ProductionResultReportPDFProps) => { return ( - - {/* Header */} - - - - - {formatDate(Date.now(), 'DD MMMM YYYY')} - + {mappedProductionResults.length === 0 ? ( + + {/* Title and Parameters */} + + + Laporan > Production Result + + + + Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')} + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + + - - PT LUMBUNG TELUR INDONESIA - - SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. - Cipedes, Kec. Sukajadi, Kota Bandung 40162 - - - - - - - Laporan Production Result - - {/* Sections per ProjectFlockKandang */} - {mappedProductionResults.length === 0 ? ( Tidak ada data. - ) : ( - mappedProductionResults.map((item, idx) => { - const pfk = item.projectFlockKandang; - // Try to display meaningful identifiers. - // Adjust these fields based on your real BaseProjectFlockKandang structure. - const kandangName = - pfk?.kandang?.name ?? `Kandang #${pfk?.kandang_id ?? idx + 1}`; + + + ) : ( + mappedProductionResults.map((item, idx) => { + const pfk = item.projectFlockKandang; - const projectName = pfk?.project_flock?.name ?? ''; + const kandangName = + pfk?.kandang?.name ?? `Kandang #${pfk?.kandang_id ?? idx + 1}`; - const locationName = pfk?.project_flock?.location?.name ?? ''; + const projectName = pfk?.project_flock?.name ?? ''; - const areaName = pfk?.project_flock?.area?.name ?? ''; + const locationName = pfk?.project_flock?.location?.name ?? ''; - return ( - 0} // each kandang starts on a new page for clarity - > - - - {projectName - ? `${projectName} • ${kandangName}` - : kandangName} - - - {[areaName, locationName].filter(Boolean).join(' • ')} - + const areaName = pfk?.project_flock?.area?.name ?? ''; + + const hasData = + item.productionResult && item.productionResult.length > 0; + + return ( + + {/* Title and Parameters */} + + + Laporan > Production Result + + + + Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')} + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + - - {item.productionResult && item.productionResult.length > 0 ? ( - - ) : ( - - Tidak ada production result untuk kandang ini. - - )} + + {projectName + ? `${projectName} • ${kandangName}` + : kandangName} + + + {[areaName, locationName].filter(Boolean).join(' • ')} + - ); - }) - )} - {/* Footer */} - - - `${pageNumber} / ${totalPages}` - } - fixed - /> - - + {hasData ? ( + <> + {/* Table 1: WOA & BW */} + + 1. WOA & Body Weight + + + + {/* Table 2: Deplesi */} + + 2. Deplesi + + + + {/* Table 3: Butiran */} + + 3. Butiran + + + + {/* Table 4: Berat (Kg) */} + + 4. Berat (Kg) + + + + {/* Table 5: Persentase */} + + 5. Persentase + + + + {/* Table 6: Produksi (HD, FI, EM, EW) */} + + + 6. Produksi (HD, FI, EM, EW) + + + + + {/* Table 7: Produksi (FCR, HH) */} + + 7. Produksi (FCR, HH) + + + + ) : ( + + Tidak ada production result untuk kandang ini. + + )} + + + + ); + }) + )} ); }; From 02d13efc2548bdcd5d3f176906f4504ecc2fe494 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 11 Feb 2026 09:28:24 +0700 Subject: [PATCH 13/16] refactor(FE): Refactor table column headers for clarity and consistency --- .../ProductionResultReportPDF.tsx | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/components/pages/report/production-result/ProductionResultReportPDF.tsx b/src/components/pages/report/production-result/ProductionResultReportPDF.tsx index 139f4640..6f7b8313 100644 --- a/src/components/pages/report/production-result/ProductionResultReportPDF.tsx +++ b/src/components/pages/report/production-result/ProductionResultReportPDF.tsx @@ -55,11 +55,6 @@ const styles = StyleSheet.create({ }, }); -function safeNum(v: unknown): number { - const n = typeof v === 'number' ? v : Number(v); - return Number.isFinite(n) ? n : 0; -} - function valueText(v: unknown) { if (v === null || v === undefined) return '-'; if (typeof v === 'number') return formatNumber(v); @@ -71,9 +66,9 @@ function valueText(v: unknown) { // ======================================== const getBwTableColumns = (): PdfColumn[] => [ { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'woa', header: 'WOA', flex: 0.8, align: 'center' }, - { key: 'bw', header: 'BW', flex: 1, align: 'right' }, - { key: 'std_bw', header: 'Std BW', flex: 1, align: 'right' }, + { key: 'woa', header: 'Week of Age', flex: 0.8, align: 'center' }, + { key: 'bw', header: 'Body Weight', flex: 1, align: 'right' }, + { key: 'std_bw', header: 'Std Body Weight', flex: 1, align: 'right' }, { key: 'uniformity', header: 'Uniformity', flex: 1.2, align: 'right' }, { key: 'std_uniformity', @@ -107,8 +102,13 @@ const getBwTableData = ( // ======================================== const getDepTableColumns = (): PdfColumn[] => [ { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'dep_kum', header: 'Dep Kum', flex: 1.5, align: 'right' }, - { key: 'dep_std', header: 'Dep Std', flex: 1.5, align: 'right' }, + { + key: 'dep_kum', + header: 'Depletion Cummulative', + flex: 1.5, + align: 'right', + }, + { key: 'dep_std', header: 'Depletion Std', flex: 1.5, align: 'right' }, ]; const getDepTableData = ( @@ -210,10 +210,10 @@ const getKgTableData = ( // ======================================== const getPersenTableColumns = (): PdfColumn[] => [ { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'persen_utuh', header: '% Utuh', flex: 1.5, align: 'right' }, - { key: 'persen_putih', header: '% Putih', flex: 1.5, align: 'right' }, - { key: 'persen_retak', header: '% Retak', flex: 1.5, align: 'right' }, - { key: 'persen_pecah', header: '% Pecah', flex: 1.5, align: 'right' }, + { key: 'persen_utuh', header: 'Utuh (%)', flex: 1.5, align: 'right' }, + { key: 'persen_putih', header: 'Putih (%)', flex: 1.5, align: 'right' }, + { key: 'persen_retak', header: '% Retak (%)', flex: 1.5, align: 'right' }, + { key: 'persen_pecah', header: '% Pecah (%)', flex: 1.5, align: 'right' }, ]; const getPersenTableData = ( @@ -251,14 +251,14 @@ const getPersenTableData = ( // ======================================== const getProduksi1TableColumns = (): PdfColumn[] => [ { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'hd', header: 'HD', flex: 0.8, align: 'right' }, - { key: 'hd_std', header: 'HD Std', flex: 1, align: 'right' }, - { key: 'fi', header: 'FI', flex: 0.8, align: 'right' }, - { key: 'fi_std', header: 'FI Std', flex: 1, align: 'right' }, - { key: 'em', header: 'EM', flex: 0.8, align: 'right' }, - { key: 'em_std', header: 'EM Std', flex: 1, align: 'right' }, - { key: 'ew', header: 'EW', flex: 0.8, align: 'right' }, - { key: 'ew_std', header: 'EW Std', flex: 1, align: 'right' }, + { key: 'hd', header: 'Hen Day', flex: 0.8, align: 'right' }, + { key: 'hd_std', header: 'Hen Day Std', flex: 1, align: 'right' }, + { key: 'fi', header: 'Feed Intake', flex: 0.8, align: 'right' }, + { key: 'fi_std', header: 'Feed Intake Std', flex: 1, align: 'right' }, + { key: 'em', header: 'Egg Mass', flex: 0.8, align: 'right' }, + { key: 'em_std', header: 'Egg Mass Std', flex: 1, align: 'right' }, + { key: 'ew', header: 'Egg Weight', flex: 0.8, align: 'right' }, + { key: 'ew_std', header: 'Egg Weight Std', flex: 1, align: 'right' }, ]; const getProduksi1TableData = ( @@ -286,8 +286,8 @@ const getProduksi2TableColumns = (): PdfColumn[] => [ { key: 'no', header: 'No', flex: 0.5, align: 'center' }, { key: 'fcr', header: 'FCR', flex: 1, align: 'right' }, { key: 'fcr_std', header: 'FCR Std', flex: 1.2, align: 'right' }, - { key: 'hh', header: 'HH', flex: 1, align: 'right' }, - { key: 'hh_std', header: 'HH Std', flex: 1.2, align: 'right' }, + { key: 'hh', header: 'Hen House', flex: 1, align: 'right' }, + { key: 'hh_std', header: 'Hen House Std', flex: 1.2, align: 'right' }, ]; const getProduksi2TableData = ( From 70b63f7773e58141727929103f8bcad87a5b1905 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 11 Feb 2026 10:38:51 +0700 Subject: [PATCH 14/16] refactor(FE): Refactor PdfTable components to support generic data types --- src/components/helper/pdf/table/PdfTable.tsx | 35 ++++--- src/components/helper/pdf/table/PdfTbody.tsx | 98 +++++++++----------- src/components/helper/pdf/table/PdfTfoot.tsx | 86 +++++++++-------- src/components/helper/pdf/table/PdfThead.tsx | 43 ++++++--- src/components/helper/pdf/table/index.ts | 4 +- src/components/helper/pdf/table/types.ts | 24 +++++ 6 files changed, 167 insertions(+), 123 deletions(-) create mode 100644 src/components/helper/pdf/table/types.ts diff --git a/src/components/helper/pdf/table/PdfTable.tsx b/src/components/helper/pdf/table/PdfTable.tsx index 27369db5..86f4ed77 100644 --- a/src/components/helper/pdf/table/PdfTable.tsx +++ b/src/components/helper/pdf/table/PdfTable.tsx @@ -1,9 +1,10 @@ 'use client'; import { View, StyleSheet } from '@react-pdf/renderer'; -import { PdfThead, PdfColumn } from './PdfThead'; -import { PdfTbody, PdfTbodyCell } from './PdfTbody'; -import { PdfTfoot, PdfTfootCell } from './PdfTfoot'; +import type { PdfColumn } from './types'; +import { PdfThead } from './PdfThead'; +import { PdfTbody } from './PdfTbody'; +import { PdfTfoot } from './PdfTfoot'; const styles = StyleSheet.create({ table: { @@ -13,10 +14,10 @@ const styles = StyleSheet.create({ }, }); -interface PdfTableProps { - columns: PdfColumn[]; - data: PdfTbodyCell[][]; - footer?: PdfTfootCell[]; +interface PdfTableProps> { + columns: PdfColumn[]; + data: TData[]; + showFooter?: boolean; footerLabel?: string; firstRow?: { valueKey: string; @@ -26,20 +27,26 @@ interface PdfTableProps { }; } -export const PdfTable = ({ +export const PdfTable = ,>({ columns, data, - footer, + showFooter = false, footerLabel = 'Total', firstRow, -}: PdfTableProps) => { +}: PdfTableProps) => { + // Check if any column has footer defined + const hasFooter = + showFooter || columns.some((col) => col.footer !== undefined); + return ( - - - {footer && footer.length > 0 && ( - + + + {hasFooter && data.length > 0 && ( + )} ); }; + +export type { PdfColumn }; diff --git a/src/components/helper/pdf/table/PdfTbody.tsx b/src/components/helper/pdf/table/PdfTbody.tsx index fee79726..cc9fe41d 100644 --- a/src/components/helper/pdf/table/PdfTbody.tsx +++ b/src/components/helper/pdf/table/PdfTbody.tsx @@ -1,22 +1,8 @@ 'use client'; import { Text, View, StyleSheet } from '@react-pdf/renderer'; - -export interface PdfColumn { - key: string; - header: string; - flex: number; - align?: 'left' | 'center' | 'right'; -} - -export interface PdfTbodyCell { - key: string; - value: string | number | React.ReactNode; - align?: 'left' | 'center' | 'right'; - color?: string; - formatAs?: 'text' | 'date' | 'currency' | 'number'; - formatDate?: string; -} +import { ReactNode } from 'react'; +import type { PdfColumn } from './types'; const styles = StyleSheet.create({ tableRow: { @@ -71,21 +57,22 @@ const styles = StyleSheet.create({ }, }); -interface PdfTbodyProps { - columns: PdfColumn[]; - rows: PdfTbodyCell[][]; +interface PdfTbodyProps> { + columns: PdfColumn[]; + data: TData[]; firstRow?: { valueKey: string; value: number; align?: 'right'; color?: string; }; - formatDate?: (date: string, format: string) => string; - formatNumber?: (num: number) => string; - formatCurrency?: (num: number) => string; } -export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { +export const PdfTbody = ,>({ + columns, + data, + firstRow, +}: PdfTbodyProps) => { return ( <> {/* First Row */} @@ -93,17 +80,17 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { {columns.map((column, index) => { const isLastColumn = index === columns.length - 1; - const isfirstRowColumn = column.key === firstRow.valueKey; - const align = column.align || 'center'; + const isFirstRowColumn = column.key === firstRow.valueKey; + const align = column.align || 'left'; const cellStyle = column.key === 'no' - ? [styles.tableCellNo, { flex: column.flex }] - : isfirstRowColumn + ? [styles.tableCellNo, { flex: column.flex || 1 }] + : isFirstRowColumn ? [ styles.tableCellRight, { - flex: column.flex, + flex: column.flex || 1, color: firstRow.color || 'black', borderRightWidth: isLastColumn ? 0 : 1, }, @@ -112,7 +99,7 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { ? [ styles.tableCellRight, { - flex: column.flex, + flex: column.flex || 1, borderRightWidth: isLastColumn ? 0 : 1, }, ] @@ -120,7 +107,7 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { ? [ styles.tableCellCenter, { - flex: column.flex, + flex: column.flex || 1, borderRightWidth: isLastColumn ? 0 : 1, }, ] @@ -128,15 +115,15 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { ? [ styles.tableCellLast, { - flex: column.flex, + flex: column.flex || 1, borderRightWidth: 0, }, ] - : [styles.tableCell, { flex: column.flex }]; + : [styles.tableCell, { flex: column.flex || 1 }]; return ( - {isfirstRowColumn ? firstRow.value : ''} + {isFirstRowColumn ? firstRow.value : ''} ); })} @@ -144,8 +131,8 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { )} {/* Data Rows */} - {rows.map((row, rowIndex) => { - const isLastRow = rowIndex === rows.length - 1; + {data.map((row, rowIndex) => { + const isLastRow = rowIndex === data.length - 1; return ( { ]} > {columns.map((column, colIndex) => { - const cell = row.find((c) => c.key === column.key); const isLastColumn = colIndex === columns.length - 1; - const align = cell?.align || column.align || 'center'; + const align = column.align || 'left'; + + // Get cell content from column.cell function or fallback to row value + let cellContent: ReactNode; + if (column.cell) { + cellContent = column.cell({ row, index: rowIndex }); + } else { + cellContent = + ((row as Record)[column.key] as ReactNode) ?? + '-'; + } const cellStyle = column.key === 'no' - ? [styles.tableCellNo, { flex: column.flex }] + ? [styles.tableCellNo, { flex: column.flex || 1 }] : align === 'right' ? [ styles.tableCellRight, { - flex: column.flex, - color: cell?.color || 'black', + flex: column.flex || 1, borderRightWidth: isLastColumn ? 0 : 1, }, ] @@ -176,37 +171,30 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { ? [ styles.tableCellCenter, { - flex: column.flex, - color: cell?.color || 'black', + flex: column.flex || 1, borderRightWidth: isLastColumn ? 0 : 1, }, ] : isLastColumn ? [ styles.tableCellLast, - { flex: column.flex, borderRightWidth: 0 }, + { flex: column.flex || 1, borderRightWidth: 0 }, ] : [ styles.tableCell, { - flex: column.flex, - color: cell?.color || 'black', + flex: column.flex || 1, borderRightWidth: isLastColumn ? 0 : 1, }, ]; return ( - {cell?.value !== undefined && - cell?.value !== null && - cell?.value !== '' ? ( - typeof cell.value === 'object' ? ( - cell.value - ) : ( - {String(cell.value)} - ) + {typeof cellContent === 'string' || + typeof cellContent === 'number' ? ( + {String(cellContent)} ) : ( - - + cellContent )} ); @@ -217,3 +205,5 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { ); }; + +export type { PdfColumn }; diff --git a/src/components/helper/pdf/table/PdfTfoot.tsx b/src/components/helper/pdf/table/PdfTfoot.tsx index a9f209b1..9d974f38 100644 --- a/src/components/helper/pdf/table/PdfTfoot.tsx +++ b/src/components/helper/pdf/table/PdfTfoot.tsx @@ -1,21 +1,8 @@ 'use client'; import { Text, View, StyleSheet } from '@react-pdf/renderer'; - -export interface PdfColumn { - key: string; - header: string; - flex: number; - align?: 'left' | 'center' | 'right'; -} - -export interface PdfTfootCell { - key: string; - value: string | number; - align?: 'left' | 'center' | 'right'; - flex?: number; - color?: string; -} +import { ReactNode } from 'react'; +import type { PdfColumn } from './types'; const styles = StyleSheet.create({ tableRow: { @@ -69,63 +56,86 @@ const styles = StyleSheet.create({ }, }); -interface PdfTfootProps { - columns: PdfColumn[]; - cells: PdfTfootCell[]; +interface PdfTfootProps> { + columns: PdfColumn[]; + data: TData[]; label?: string; } -export const PdfTfoot = ({ +export const PdfTfoot = ,>({ columns, - cells, + data, label = 'Total', -}: PdfTfootProps) => { +}: PdfTfootProps) => { return ( {columns.map((column, index) => { const isLastColumn = index === columns.length - 1; - const cellData = cells.find((c) => c.key === column.key); + + // Get footer content from column definition + let footerContent: ReactNode; + if (typeof column.footer === 'function') { + footerContent = column.footer(data); + } else { + footerContent = column.footer; + } + + // Use label for first column (usually 'no' column) + const displayContent = column.key === 'no' ? label : footerContent; + + // Determine alignment + const align = column.footerAlign || column.align || 'left'; + const color = column.footerColor || 'black'; const cellStyle = column.key === 'no' ? [ styles.tableCellNo, - { flex: column.flex, borderRightWidth: isLastColumn ? 0 : 1 }, + { + flex: column.flex || 1, + borderRightWidth: isLastColumn ? 0 : 1, + color, + }, ] - : cellData?.align === 'right' + : align === 'right' ? [ styles.tableCellRight, { - flex: column.flex, - color: cellData?.color || 'black', + flex: column.flex || 1, + color, borderRightWidth: isLastColumn ? 0 : 1, }, ] - : cellData?.align === 'center' + : align === 'center' ? [ styles.tableCellCenter, { - flex: column.flex, - color: cellData?.color || 'black', + flex: column.flex || 1, + color, borderRightWidth: isLastColumn ? 0 : 1, }, ] : isLastColumn - ? [styles.tableCellLast, { flex: column.flex }] - : [ - styles.tableCell, - { - flex: column.flex, - color: cellData?.color || 'black', - }, - ]; + ? [styles.tableCellLast, { flex: column.flex || 1, color }] + : [styles.tableCell, { flex: column.flex || 1, color }]; return ( - {column.key === 'no' ? label : cellData?.value || ''} + {displayContent !== undefined && displayContent !== null ? ( + typeof displayContent === 'string' || + typeof displayContent === 'number' ? ( + {String(displayContent)} + ) : ( + displayContent + ) + ) : ( + - + )} ); })} ); }; + +export type { PdfColumn }; diff --git a/src/components/helper/pdf/table/PdfThead.tsx b/src/components/helper/pdf/table/PdfThead.tsx index 89037216..889f9f34 100644 --- a/src/components/helper/pdf/table/PdfThead.tsx +++ b/src/components/helper/pdf/table/PdfThead.tsx @@ -1,13 +1,8 @@ 'use client'; import { Text, View, StyleSheet } from '@react-pdf/renderer'; - -export interface PdfColumn { - key: string; - header: string; - flex: number; - align?: 'left' | 'center' | 'right'; -} +import { ReactNode } from 'react'; +import type { PdfColumn } from './types'; const styles = StyleSheet.create({ tableRow: { @@ -48,23 +43,37 @@ const styles = StyleSheet.create({ }, }); -interface PdfTheadProps { - columns: PdfColumn[]; +interface PdfTheadProps> { + columns: PdfColumn[]; + data?: TData[]; } -export const PdfThead = ({ columns }: PdfTheadProps) => { +export const PdfThead = ,>({ + columns, + data, +}: PdfTheadProps) => { return ( {columns.map((column, index) => { - const align = column.align || 'center'; const isLastColumn = index === columns.length - 1; + // Get header content from column definition + let headerContent: ReactNode; + if (typeof column.header === 'function') { + headerContent = column.header(data || []); + } else { + headerContent = column.header || column.key; + } + + // Determine alignment - columns align right by default for numeric data + const align = column.align || 'left'; + const cellStyle = align === 'right' ? [ styles.tableCellHeaderRight, { - flex: column.flex, + flex: column.flex || 1, textAlign: 'right' as const, borderRightWidth: isLastColumn ? 0 : 1, }, @@ -72,7 +81,7 @@ export const PdfThead = ({ columns }: PdfTheadProps) => { : [ styles.tableCellHeader, { - flex: column.flex, + flex: column.flex || 1, textAlign: align as 'left' | 'center' | 'right', borderRightWidth: isLastColumn ? 0 : 1, }, @@ -80,10 +89,16 @@ export const PdfThead = ({ columns }: PdfTheadProps) => { return ( - {column.header} + {typeof headerContent === 'string' ? ( + {headerContent} + ) : ( + headerContent + )} ); })} ); }; + +export type { PdfColumn }; diff --git a/src/components/helper/pdf/table/index.ts b/src/components/helper/pdf/table/index.ts index 35839f17..3c780688 100644 --- a/src/components/helper/pdf/table/index.ts +++ b/src/components/helper/pdf/table/index.ts @@ -2,6 +2,4 @@ export { PdfTable } from './PdfTable'; export { PdfThead } from './PdfThead'; export { PdfTbody } from './PdfTbody'; export { PdfTfoot } from './PdfTfoot'; -export type { PdfColumn } from './PdfThead'; -export type { PdfTbodyCell } from './PdfTbody'; -export type { PdfTfootCell } from './PdfTfoot'; +export type { PdfColumn } from './types'; diff --git a/src/components/helper/pdf/table/types.ts b/src/components/helper/pdf/table/types.ts new file mode 100644 index 00000000..c2437f13 --- /dev/null +++ b/src/components/helper/pdf/table/types.ts @@ -0,0 +1,24 @@ +import { ReactNode } from 'react'; + +/** + * PdfColumn - Mirip dengan ColumnDef di TanStack Table + * Mengatur header (thead), body (tbody), dan footer (tfoot) dalam satu definisi + */ +export interface PdfColumn> { + key: string; + flex?: number; + + // Header configuration (thead) + header?: string | ((data: TData[]) => ReactNode); + + // Body configuration (tbody) + align?: 'left' | 'center' | 'right'; + cell?: (props: { row: TData; index: number }) => ReactNode | string | number; + + // Footer configuration (tfoot) + footer?: string | number | ((data: TData[]) => ReactNode | string | number); + footerAlign?: 'left' | 'center' | 'right'; + footerColor?: string; +} + +export type { PdfColumn as default }; From 0f1d2ce4773d6dd3f4ed7faccedbaf75abc32d87 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 11 Feb 2026 10:39:37 +0700 Subject: [PATCH 15/16] refactor(FE): Refactor PDF table components to simplify imports --- .../export/CustomerPaymentExportPDF.tsx | 295 +++++----- .../finance/export/DebtSupllierExportPDF.tsx | 285 ++++++---- .../export/PurchasesPerSupplierExportPDF.tsx | 209 ++++---- .../export/DailyMarketingReportPDF.tsx | 284 +++++----- .../export/HppPerkandangExportPDF.tsx | 280 ++++------ .../ProductionResultReportPDF.tsx | 505 ++++++++++-------- 6 files changed, 973 insertions(+), 885 deletions(-) diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx index b0cb5c8d..4a6f0238 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -7,6 +7,7 @@ import { StyleSheet, Font, pdf, + Text, } from '@react-pdf/renderer'; import { @@ -16,12 +17,7 @@ import { formatTitleCase, } from '@/lib/helper'; import { CustomerPaymentReport } from '@/types/api/report/customer-payment'; -import { - PdfTable, - PdfColumn, - PdfTbodyCell, - PdfTfootCell, -} from '@/components/helper/pdf/table'; +import { PdfTable, PdfColumn } from '@/components/helper/pdf/table'; import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge'; import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; @@ -61,172 +57,183 @@ interface CustomerPaymentExportPDFParams { }; } -const getTableColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'trans_date', header: 'Tanggal DO', flex: 1.2, align: 'center' }, +const getTableColumns = ( + summary?: CustomerPaymentReport['summary'] +): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + footer: 'Total', + }, + { + key: 'trans_date', + header: 'Tanggal DO', + flex: 1.2, + align: 'center', + cell: ({ row }) => + row.trans_date ? formatDate(row.trans_date, 'DD MMM YY') : '-', + footer: '', + }, { key: 'delivery_date', header: 'Tanggal Realisasi', flex: 1.2, align: 'center', + cell: ({ row }) => + row.delivery_date ? formatDate(row.delivery_date, 'DD MMM YY') : '-', + footer: '', + }, + { + key: 'aging', + header: 'Aging', + flex: 0.8, + align: 'center', + cell: ({ row }) => + row.aging_day != null ? `${formatNumber(row.aging_day)} hari` : '-', + footer: '', + }, + { + key: 'reference', + header: 'Referensi', + flex: 1.5, + align: 'left', + cell: ({ row }) => row.reference || '-', + footer: '', + }, + { + key: 'vehicle_numbers', + header: 'No Polisi', + flex: 1.2, + align: 'left', + cell: ({ row }) => + Array.isArray(row.vehicle_numbers) && row.vehicle_numbers.length > 0 + ? row.vehicle_numbers.join(', ') + : '-', + footer: '', + }, + { + key: 'qty', + header: 'Qty', + flex: 0.8, + align: 'right', + cell: ({ row }) => formatNumber(row.qty), + footer: summary ? formatNumber(summary.total_qty || 0) : '', + footerAlign: 'right', + }, + { + key: 'weight', + header: 'Berat', + flex: 1, + align: 'right', + cell: ({ row }) => formatNumber(row.weight), + footer: summary ? formatNumber(summary.total_weight || 0) : '', + footerAlign: 'right', + }, + { + key: 'average_weight', + header: 'Rata-Rata', + flex: 0.8, + align: 'right', + cell: ({ row }) => formatNumber(row.average_weight), + footer: '', + }, + { + key: 'unit_price', + header: 'Harga/Unit (Rp)', + flex: 1.2, + align: 'right', + cell: ({ row }) => formatCurrency(row.unit_price), + footer: '', + }, + { + key: 'final_price', + header: 'Harga Akhir (Rp)', + flex: 1.2, + align: 'right', + cell: ({ row }) => formatCurrency(row.final_price), + footer: summary ? formatCurrency(summary.total_final_amount || 0) : '', + footerAlign: 'right', + }, + { + key: 'total_price', + header: 'Total (Rp)', + flex: 1.2, + align: 'right', + cell: ({ row }) => formatCurrency(row.total_price), + footer: summary ? formatCurrency(summary.total_grand_amount || 0) : '', + footerAlign: 'right', }, - { key: 'aging', header: 'Aging', flex: 0.8, align: 'center' }, - { key: 'reference', header: 'Referensi', flex: 1.5, align: 'left' }, - { key: 'vehicle_numbers', header: 'No Polisi', flex: 1.2, align: 'left' }, - { key: 'qty', header: 'Qty', flex: 0.8, align: 'right' }, - { key: 'weight', header: 'Berat', flex: 1, align: 'right' }, - { key: 'average_weight', header: 'Rata-Rata', flex: 0.8, align: 'right' }, - { key: 'unit_price', header: 'Harga/Unit (Rp)', flex: 1.2, align: 'right' }, - { key: 'final_price', header: 'Harga Akhir (Rp)', flex: 1.2, align: 'right' }, - { key: 'total_price', header: 'Total (Rp)', flex: 1.2, align: 'right' }, { key: 'payment_amount', header: 'Pembayaran (Rp)', flex: 1.2, align: 'right', + cell: ({ row }) => formatCurrency(row.payment_amount), + footer: summary ? formatCurrency(summary.total_payment || 0) : '', + footerAlign: 'right', }, { key: 'accounts_receivable', header: 'Saldo (Rp)', flex: 1.2, align: 'right', + cell: ({ row }) => ( + + {formatCurrency(row.accounts_receivable)} + + ), + footer: summary + ? formatCurrency(summary.total_accounts_receivable || 0) + : '', + footerAlign: 'right', + footerColor: + (summary?.total_accounts_receivable || 0) < 0 ? 'red' : undefined, }, - { key: 'status', header: 'Keterangan', flex: 1.5, align: 'center' }, - { key: 'pickup_info', header: 'Pengambilan', flex: 1, align: 'left' }, - { key: 'sales_person', header: 'Sales', flex: 1.5, align: 'left' }, -]; - -const getTableData = ( - rows: CustomerPaymentReport['rows'] -): PdfTbodyCell[][] => { - return rows.map((item, index) => [ - { key: 'no', value: index + 1 }, - { - key: 'trans_date', - value: item.trans_date ? formatDate(item.trans_date, 'DD MMM YY') : '-', - }, - { - key: 'delivery_date', - value: item.delivery_date - ? formatDate(item.delivery_date, 'DD MMM YY') - : '-', - }, - { - key: 'aging', - value: - item.aging_day != null ? `${formatNumber(item.aging_day)} hari` : '-', - }, - { key: 'reference', value: item.reference || '-' }, - { - key: 'vehicle_numbers', - value: - Array.isArray(item.vehicle_numbers) && item.vehicle_numbers.length > 0 - ? item.vehicle_numbers.join(', ') - : '-', - }, - { key: 'qty', value: formatNumber(item.qty), align: 'right' }, - { key: 'weight', value: formatNumber(item.weight), align: 'right' }, - { - key: 'average_weight', - value: formatNumber(item.average_weight), - align: 'right', - }, - { - key: 'unit_price', - value: formatCurrency(item.unit_price), - align: 'right', - }, - { - key: 'final_price', - value: formatCurrency(item.final_price), - align: 'right', - }, - { - key: 'total_price', - value: formatCurrency(item.total_price), - align: 'right', - }, - { - key: 'payment_amount', - value: formatCurrency(item.payment_amount), - align: 'right', - }, - { - key: 'accounts_receivable', - value: formatCurrency(item.accounts_receivable), - align: 'right', - color: item.accounts_receivable < 0 ? 'red' : undefined, - }, - { - key: 'status', - value: item.status ? ( + { + key: 'status', + header: 'Keterangan', + flex: 1.5, + align: 'center', + cell: ({ row }) => + row.status ? ( - {formatTitleCase(item.status)} + {formatTitleCase(row.status)} ) : ( '-' ), - }, - { - key: 'pickup_info', - value: - Array.isArray(item.pickup_info) && item.pickup_info.length > 0 - ? item.pickup_info.join(', ') - : '-', - }, - { key: 'sales_person', value: item.sales_person || '-' }, - ]); -}; - -const getTableFooter = ( - summary: CustomerPaymentReport['summary'] -): PdfTfootCell[] => [ - { key: 'no', value: 'Total' }, - { key: 'trans_date', value: '' }, - { key: 'delivery_date', value: '' }, - { key: 'aging', value: '' }, - { key: 'reference', value: '' }, - { key: 'vehicle_numbers', value: '' }, - { key: 'qty', value: formatNumber(summary?.total_qty || 0), align: 'right' }, - { - key: 'weight', - value: formatNumber(summary?.total_weight || 0), - align: 'right', - }, - { key: 'average_weight', value: '' }, - { key: 'unit_price', value: '' }, - { - key: 'final_price', - value: formatCurrency(summary?.total_final_amount || 0), - align: 'right', + footer: '', }, { - key: 'total_price', - value: formatCurrency(summary?.total_grand_amount || 0), - align: 'right', + key: 'pickup_info', + header: 'Pengambilan', + flex: 1, + align: 'left', + cell: ({ row }) => + Array.isArray(row.pickup_info) && row.pickup_info.length > 0 + ? row.pickup_info.join(', ') + : '-', + footer: '', }, { - key: 'payment_amount', - value: formatCurrency(summary?.total_payment || 0), - align: 'right', + key: 'sales_person', + header: 'Sales', + flex: 1.5, + align: 'left', + cell: ({ row }) => row.sales_person || '-', + footer: '', }, - { - key: 'accounts_receivable', - value: formatCurrency(summary?.total_accounts_receivable || 0), - align: 'right', - color: (summary?.total_accounts_receivable || 0) < 0 ? 'red' : undefined, - }, - { key: 'status', value: '' }, - { key: 'pickup_info', value: '' }, - { key: 'sales_person', value: '' }, ]; const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { @@ -276,13 +283,9 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { {/* Table */} [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'pr_number', header: 'No. PR', flex: 1, align: 'left' }, - { key: 'po_number', header: 'No. PO', flex: 1, align: 'left' }, - { - key: 'received_date', - header: 'Tgl Terima/Bayar', - flex: 0.7, - align: 'center', - }, - { key: 'po_date', header: 'Tgl PO', flex: 0.7, align: 'center' }, - { key: 'aging', header: 'Aging', flex: 0.6, align: 'center' }, - { key: 'area', header: 'Area', flex: 1, align: 'left' }, - { key: 'warehouse', header: 'Gudang', flex: 1, align: 'left' }, - { key: 'due_date', header: 'Jatuh Tempo', flex: 1, align: 'center' }, - { key: 'due_status', header: 'Status Jatuh Tempo', flex: 2, align: 'center' }, - { - key: 'total_price', - header: 'Nominal Pembelian (Rp)', - flex: 1.5, - align: 'right', - }, - { - key: 'payment_price', - header: 'Pembayaran (Rp)', - flex: 1.5, - align: 'right', - }, - { - key: 'balance', - header: 'Sisa Saldo Hutang (Rp)', - flex: 1.5, - align: 'right', - }, - { key: 'status', header: 'Status', flex: 1.2, align: 'center' }, - { key: 'travel_number', header: 'No. Perjalanan', flex: 1, align: 'left' }, -]; +const getTableColumns = (total?: DebtSupplier['total']): PdfColumn[] => { + type DebtRow = DebtSupplier['rows'][number]; -const getTableData = (rows: DebtSupplier['rows']): PdfTbodyCell[][] => { - return rows.map((item, index) => [ - { key: 'no', value: index + 1 }, - { key: 'pr_number', value: item.pr_number || '-' }, - { key: 'po_number', value: item.po_number || '-' }, + return [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + footer: 'Total', + }, + { + key: 'pr_number', + header: 'No. PR', + flex: 1, + align: 'left', + cell: ({ row }) => (row as unknown as DebtRow).pr_number || '-', + footer: '', + }, + { + key: 'po_number', + header: 'No. PO', + flex: 1, + align: 'left', + cell: ({ row }) => (row as unknown as DebtRow).po_number || '-', + footer: '', + }, { key: 'received_date', - value: item.received_date - ? formatDate(item.received_date, 'DD MMM YY') - : '-', + header: 'Tgl Terima/Bayar', + flex: 0.7, + align: 'center', + cell: ({ row }) => + (row as unknown as DebtRow).received_date + ? formatDate((row as unknown as DebtRow).received_date, 'DD MMM YY') + : '-', + footer: '', }, { key: 'po_date', - value: item.po_date ? formatDate(item.po_date, 'DD MMM YY') : '-', + header: 'Tgl PO', + flex: 0.7, + align: 'center', + cell: ({ row }) => + (row as unknown as DebtRow).po_date + ? formatDate((row as unknown as DebtRow).po_date, 'DD MMM YY') + : '-', + footer: '', }, { key: 'aging', - value: item.aging != null ? `${formatNumber(item.aging)}` : '-', + header: 'Aging', + flex: 0.6, + align: 'center', + cell: ({ row }) => + (row as unknown as DebtRow).aging != null + ? `${formatNumber((row as unknown as DebtRow).aging)}` + : '-', + footer: total ? formatNumber(total.aging || 0) + ' Hari' : '', + }, + { + key: 'area', + header: 'Area', + flex: 1, + align: 'left', + cell: ({ row }) => (row as unknown as DebtRow).area?.name || '-', + footer: '', + }, + { + key: 'warehouse', + header: 'Gudang', + flex: 1, + align: 'left', + cell: ({ row }) => (row as unknown as DebtRow).warehouse?.name || '-', + footer: '', }, - { key: 'area', value: item.area?.name || '-' }, - { key: 'warehouse', value: item.warehouse?.name || '-' }, { key: 'due_date', - value: item.due_date ? formatDate(item.due_date, 'DD MMM YY') : '-', + header: 'Jatuh Tempo', + flex: 1, + align: 'center', + cell: ({ row }) => + (row as unknown as DebtRow).due_date + ? formatDate((row as unknown as DebtRow).due_date, 'DD MMM YY') + : '-', + footer: '', }, { key: 'due_status', - value: - item.due_status && item.due_status !== '-' ? ( + header: 'Status Jatuh Tempo', + flex: 2, + align: 'center', + cell: ({ row }) => + (row as unknown as DebtRow).due_status && + (row as unknown as DebtRow).due_status !== '-' ? ( - {item.due_status} + {(row as unknown as DebtRow).due_status} ) : ( '-' ), + footer: '', }, { key: 'total_price', - value: formatCurrency(item.total_price), + header: 'Nominal Pembelian (Rp)', + flex: 1.5, align: 'right', - color: item.total_price < 0 ? 'red' : undefined, + cell: ({ row }) => ( + + {formatCurrency((row as unknown as DebtRow).total_price)} + + ), + footer: total ? formatCurrency(total.total_price || 0) : '', + footerAlign: 'right', }, { key: 'payment_price', - value: formatCurrency(item.payment_price), + header: 'Pembayaran (Rp)', + flex: 1.5, align: 'right', - color: item.payment_price < 0 ? 'red' : undefined, + cell: ({ row }) => ( + + {formatCurrency((row as unknown as DebtRow).payment_price)} + + ), + footer: total ? formatCurrency(total.payment_price || 0) : '', + footerAlign: 'right', }, { key: 'balance', - value: formatCurrency(item.balance), + header: 'Sisa Saldo Hutang (Rp)', + flex: 1.5, align: 'right', - color: item.balance < 0 ? 'red' : undefined, + cell: ({ row }) => ( + + {formatCurrency((row as unknown as DebtRow).balance)} + + ), + footer: total ? formatCurrency(total.debt_price || 0) : '', + footerAlign: 'right', + footerColor: (total?.debt_price || 0) < 0 ? 'red' : undefined, }, { key: 'status', - value: - item.status && item.status !== '-' ? ( + header: 'Status', + flex: 1.2, + align: 'center', + cell: ({ row }) => + (row as unknown as DebtRow).status && + (row as unknown as DebtRow).status !== '-' ? ( - {item.status} + {(row as unknown as DebtRow).status} ) : ( '-' ), + footer: '', }, - { key: 'travel_number', value: item.travel_number || '-' }, - ]); + { + key: 'travel_number', + header: 'No. Perjalanan', + flex: 1, + align: 'left', + cell: ({ row }) => (row as unknown as DebtRow).travel_number || '-', + footer: '', + }, + ]; }; -const getTableFooter = (total: DebtSupplier['total']): PdfTfootCell[] => [ - { key: 'no', value: 'Total' }, - { key: 'pr_number', value: '' }, - { key: 'po_number', value: '' }, - { key: 'received_date', value: '' }, - { key: 'po_date', value: '' }, - { key: 'aging', value: formatNumber(total?.aging || 0) + ' Hari' }, - { key: 'area', value: '' }, - { key: 'warehouse', value: '' }, - { key: 'due_date', value: '' }, - { key: 'due_status', value: '' }, - { - key: 'total_price', - value: formatCurrency(total?.total_price || 0), - align: 'right', - }, - { - key: 'payment_price', - value: formatCurrency(total?.payment_price || 0), - align: 'right', - }, - { - key: 'balance', - value: formatCurrency(total?.debt_price || 0), - align: 'right', - color: (total?.debt_price || 0) < 0 ? 'red' : undefined, - }, - { key: 'status', value: '' }, - { key: 'travel_number', value: '' }, -]; - interface DebtSupplierExportPDFParams { data: DebtSupplier[]; params?: { @@ -263,13 +324,9 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { {/* Table */} []} + showFooter={!!supplierReport.total} firstRow={ typeof supplierReport.initial_balance === 'number' && supplierReport.initial_balance !== 0 diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx index 3423ca69..d265d08f 100644 --- a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx @@ -10,12 +10,7 @@ import { } from '@react-pdf/renderer'; import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; -import { - PdfTable, - PdfColumn, - PdfTbodyCell, - PdfTfootCell, -} from '@/components/helper/pdf/table'; +import { PdfTable, PdfColumn } from '@/components/helper/pdf/table'; import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge'; import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; @@ -58,90 +53,118 @@ interface PurchasesPerSupplierExportParams { }; } -const getTableColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, +const getTableColumns = ( + summary?: LogisticPurchasePerSupplierReport['summary'] +): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + footer: 'Total', + }, { key: 'receive_date', header: 'Tanggal Terima', flex: 1.2, align: 'center', + cell: ({ row }) => + row.receive_date ? formatDate(row.receive_date, 'DD MMM YY') : '-', + footer: '', + }, + { + key: 'po_date', + header: 'Tanggal PO', + flex: 1.2, + align: 'center', + cell: ({ row }) => + row.po_date ? formatDate(row.po_date, 'DD MMM YY') : '-', + footer: '', + }, + { + key: 'po_number', + header: 'No. Referensi', + flex: 1.5, + align: 'left', + cell: ({ row }) => row.po_number || '-', + footer: '', + }, + { + key: 'product', + header: 'Nama Produk', + flex: 2, + align: 'left', + cell: ({ row }) => row.product?.name || '-', + footer: '', + }, + { + key: 'warehouse', + header: 'Tujuan', + flex: 1.5, + align: 'left', + cell: ({ row }) => row.warehouse?.name || '-', + footer: '', + }, + { + key: 'qty', + header: 'QTY', + flex: 0.8, + align: 'right', + cell: ({ row }) => formatNumber(row.qty || 0), + footer: summary ? formatNumber(summary.total_qty || 0) : '', + footerAlign: 'right', + }, + { + key: 'unit_price', + header: 'Harga Beli (Rp)', + flex: 1.5, + align: 'right', + cell: ({ row }) => formatCurrency(row.unit_price || 0), + footer: '', }, - { key: 'po_date', header: 'Tanggal PO', flex: 1.2, align: 'center' }, - { key: 'po_number', header: 'No. Referensi', flex: 1.5, align: 'left' }, - { key: 'product', header: 'Nama Produk', flex: 2, align: 'left' }, - { key: 'warehouse', header: 'Tujuan', flex: 1.5, align: 'left' }, - { key: 'qty', header: 'QTY', flex: 0.8, align: 'right' }, - { key: 'unit_price', header: 'Harga Beli (Rp)', flex: 1.5, align: 'right' }, { key: 'purchase_value', header: 'Value Harga Beli (Rp)', flex: 1.8, align: 'right', + cell: ({ row }) => formatCurrency(row.purchase_value || 0), + footer: summary ? formatCurrency(summary.total_purchase_value || 0) : '', + footerAlign: 'right', }, { key: 'transport_unit_price', header: 'Transport (Rp)', flex: 1.3, align: 'right', + cell: ({ row }) => formatCurrency(row.transport_unit_price || 0), + footer: '', }, { key: 'transport_value', header: 'Value Transport (Rp)', flex: 1.8, align: 'right', + cell: ({ row }) => formatCurrency(row.transport_value || 0), + footer: summary ? formatCurrency(summary.total_transport_value || 0) : '', + footerAlign: 'right', }, - { key: 'total_amount', header: 'Jumlah (Rp)', flex: 1.5, align: 'right' }, - { key: 'expedition', header: 'Ekspedisi', flex: 1.2, align: 'center' }, - { key: 'delivery_number', header: 'Surat Jalan', flex: 1.2, align: 'left' }, -]; - -const getTableData = ( - rows: LogisticPurchasePerSupplierReport['rows'] -): PdfTbodyCell[][] => { - return rows.map((item, index) => [ - { key: 'no', value: index + 1 }, - { - key: 'receive_date', - value: item.receive_date - ? formatDate(item.receive_date, 'DD MMM YY') - : '-', - }, - { - key: 'po_date', - value: item.po_date ? formatDate(item.po_date, 'DD MMM YY') : '-', - }, - { key: 'po_number', value: item.po_number || '-' }, - { key: 'product', value: item.product?.name || '-' }, - { key: 'warehouse', value: item.warehouse?.name || '-' }, - { key: 'qty', value: formatNumber(item.qty || 0), align: 'right' }, - { - key: 'unit_price', - value: formatCurrency(item.unit_price || 0), - align: 'right', - }, - { - key: 'purchase_value', - value: formatCurrency(item.purchase_value || 0), - align: 'right', - }, - { - key: 'transport_unit_price', - value: formatCurrency(item.transport_unit_price || 0), - align: 'right', - }, - { - key: 'transport_value', - value: formatCurrency(item.transport_value || 0), - align: 'right', - }, - { - key: 'total_amount', - value: formatCurrency(item.total_amount || 0), - align: 'right', - }, - { - key: 'expedition', - value: item.expedition ? ( + { + key: 'total_amount', + header: 'Jumlah (Rp)', + flex: 1.5, + align: 'right', + cell: ({ row }) => formatCurrency(row.total_amount || 0), + footer: summary ? formatCurrency(summary.total_amount || 0) : '', + footerAlign: 'right', + }, + { + key: 'expedition', + header: 'Ekspedisi', + flex: 1.2, + align: 'center', + cell: ({ row }) => + row.expedition ? ( - {item.expedition} + {row.expedition} ) : ( '-' ), - }, - { key: 'delivery_number', value: item.delivery_number || '-' }, - ]); -}; - -const getTableFooter = ( - summary: LogisticPurchasePerSupplierReport['summary'] -): PdfTfootCell[] => [ - { key: 'no', value: 'Total' }, - { key: 'receive_date', value: '' }, - { key: 'po_date', value: '' }, - { key: 'po_number', value: '' }, - { key: 'product', value: '' }, - { key: 'warehouse', value: '' }, - { - key: 'qty', - value: formatNumber(summary?.total_qty || 0), - align: 'right', - }, - { key: 'unit_price', value: '' }, - { - key: 'purchase_value', - value: formatCurrency(summary?.total_purchase_value || 0), - align: 'right', - }, - { key: 'transport_unit_price', value: '' }, - { - key: 'transport_value', - value: formatCurrency(summary?.total_transport_value || 0), - align: 'right', + footer: '', }, { - key: 'total_amount', - value: formatCurrency(summary?.total_amount || 0), - align: 'right', + key: 'delivery_number', + header: 'Surat Jalan', + flex: 1.2, + align: 'left', + cell: ({ row }) => row.delivery_number || '-', + footer: '', }, - { key: 'expedition', value: '' }, - { key: 'delivery_number', value: '' }, ]; const createPDFDocument = (params: PurchasesPerSupplierExportParams) => { @@ -253,13 +248,9 @@ const createPDFDocument = (params: PurchasesPerSupplierExportParams) => { {/* Table */}
))} diff --git a/src/components/pages/report/marketing/export/DailyMarketingReportPDF.tsx b/src/components/pages/report/marketing/export/DailyMarketingReportPDF.tsx index 53b36b80..29c1d619 100644 --- a/src/components/pages/report/marketing/export/DailyMarketingReportPDF.tsx +++ b/src/components/pages/report/marketing/export/DailyMarketingReportPDF.tsx @@ -11,12 +11,7 @@ import { formatNumber, formatTitleCase, } from '@/lib/helper'; -import { - PdfTable, - PdfColumn, - PdfTbodyCell, - PdfTfootCell, -} from '@/components/helper/pdf/table'; +import { PdfTable, PdfColumn } from '@/components/helper/pdf/table'; import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge'; import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; @@ -49,60 +44,90 @@ interface DailyMarketingReportPDFProps { total?: SalesSummary; } -const getTableColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'so_date', header: 'Tanggal Sales Order', flex: 1.3, align: 'center' }, +const getTableColumns = ( + summary?: SalesSummary +): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + }, + { + key: 'so_date', + header: 'Tanggal Sales Order', + flex: 1.3, + align: 'center', + cell: ({ row }) => + row.so_date ? formatDate(row.so_date, 'DD MMM YY') : '-', + }, { key: 'do_date', header: 'Tanggal Delivery Order', flex: 1.3, align: 'center', - }, - { key: 'aging', header: 'Aging (Hari)', flex: 0.7, align: 'center' }, - { key: 'warehouse', header: 'Gudang', flex: 1.2, align: 'left' }, - { key: 'customer', header: 'Pelanggan', flex: 1.5, align: 'left' }, - { key: 'sales', header: 'Sales', flex: 1, align: 'left' }, - { key: 'product', header: 'Produk', flex: 1.3, align: 'left' }, - { key: 'do_number', header: 'Nomor DO', flex: 1.2, align: 'left' }, - { key: 'vehicle', header: 'Nomor Polisi', flex: 1, align: 'left' }, - { key: 'marketing_type', header: 'Tipe Marketing', flex: 1, align: 'center' }, - { key: 'qty', header: 'Quantity', flex: 0.7, align: 'right' }, - { key: 'avg_weight', header: 'Rata-Rata (Kg)', flex: 0.8, align: 'right' }, - { - key: 'total_weight', - header: 'Total Berat (Kg)', - flex: 0.9, - align: 'right', - }, - { key: 'sales_price', header: 'Harga Jual (Rp)', flex: 0.9, align: 'right' }, - { key: 'hpp_price', header: 'HPP (Rp)', flex: 1.3, align: 'right' }, - { key: 'sales_amount', header: 'Total Jual (Rp)', flex: 1, align: 'right' }, - { key: 'hpp_amount', header: 'Total HPP (Rp)', flex: 1.3, align: 'right' }, -]; - -const getTableData = (rows: DailyMarketingReport): PdfTbodyCell[][] => { - return rows.map((row, index) => [ - { key: 'no', value: index + 1 }, - { - key: 'so_date', - value: row.so_date ? formatDate(row.so_date, 'DD MMM YY') : '-', - }, - { - key: 'do_date', - value: row.realization_date + cell: ({ row }) => + row.realization_date ? formatDate(row.realization_date, 'DD MMM YY') : '-', - }, - { key: 'aging', value: row.aging_days ?? '-' }, - { key: 'warehouse', value: row.warehouse?.name ?? '-' }, - { key: 'customer', value: row.customer?.name ?? '-' }, - { key: 'sales', value: row.sales?.name ?? '-' }, - { key: 'product', value: row.product?.name ?? '-' }, - { key: 'do_number', value: row.do_number ?? '-' }, - { key: 'vehicle', value: row.vehicle_number ?? '-' }, - { - key: 'marketing_type', - value: row.marketing_type ? ( + }, + { + key: 'aging', + header: 'Aging (Hari)', + flex: 0.7, + align: 'center', + cell: ({ row }) => row.aging_days ?? '-', + }, + { + key: 'warehouse', + header: 'Gudang', + flex: 1.2, + align: 'left', + cell: ({ row }) => row.warehouse?.name ?? '-', + }, + { + key: 'customer', + header: 'Pelanggan', + flex: 1.5, + align: 'left', + cell: ({ row }) => row.customer?.name ?? '-', + }, + { + key: 'sales', + header: 'Sales', + flex: 1, + align: 'left', + cell: ({ row }) => row.sales?.name ?? '-', + }, + { + key: 'product', + header: 'Produk', + flex: 1.3, + align: 'left', + cell: ({ row }) => row.product?.name ?? '-', + }, + { + key: 'do_number', + header: 'Nomor DO', + flex: 1.2, + align: 'left', + cell: ({ row }) => row.do_number ?? '-', + }, + { + key: 'vehicle', + header: 'Nomor Polisi', + flex: 1, + align: 'left', + cell: ({ row }) => row.vehicle_number ?? '-', + }, + { + key: 'marketing_type', + header: 'Tipe Marketing', + flex: 1, + align: 'center', + cell: ({ row }) => + row.marketing_type ? ( { ) : ( '-' ), - }, - { key: 'qty', value: formatNumber(row.qty ?? 0), align: 'right' }, - { - key: 'avg_weight', - value: formatNumber(row.average_weight_kg ?? 0), - align: 'right', - }, - { - key: 'total_weight', - value: formatNumber(row.total_weight_kg ?? 0), - align: 'right', - }, - { - key: 'sales_price', - value: formatCurrency(row.sales_price_per_kg ?? 0), - align: 'right', - }, - { - key: 'hpp_price', - value: formatCurrency(row.hpp_price_per_kg ?? 0), - align: 'right', - }, - { - key: 'sales_amount', - value: formatCurrency(row.sales_amount ?? 0), - align: 'right', - }, - { - key: 'hpp_amount', - value: formatCurrency(row.hpp_amount ?? 0), - align: 'right', - }, - ]); -}; - -const getTableFooter = (summary?: SalesSummary): PdfTfootCell[] => { - if (!summary) return []; - - return [ - { key: 'no', value: 'TOTAL' }, - { key: 'so_date', value: '' }, - { key: 'do_date', value: '' }, - { key: 'aging', value: '' }, - { key: 'warehouse', value: '' }, - { key: 'customer', value: '' }, - { key: 'sales', value: '' }, - { key: 'product', value: '' }, - { key: 'do_number', value: '' }, - { key: 'vehicle', value: '' }, - { key: 'marketing_type', value: '' }, - { - key: 'qty', - value: formatNumber(summary.total_qty ?? 0), - align: 'right', - }, - { - key: 'avg_weight', - value: formatNumber(summary.total_weight_kg ?? 0), - align: 'right', - }, - { - key: 'total_weight', - value: formatNumber(summary.total_weight_kg ?? 0), - align: 'right', - }, - { key: 'sales_price', value: '' }, - { - key: 'hpp_price', - value: formatCurrency(summary.total_hpp_price_per_kg ?? 0), - align: 'right', - }, - { - key: 'sales_amount', - value: formatCurrency(summary.total_sales_amount ?? 0), - align: 'right', - }, - { - key: 'hpp_amount', - value: formatCurrency(summary.total_hpp_amount ?? 0), - align: 'right', - }, - ]; -}; + }, + { + key: 'qty', + header: 'Quantity', + flex: 0.7, + align: 'right', + cell: ({ row }) => formatNumber(row.qty ?? 0), + footer: summary ? formatNumber(summary.total_qty ?? 0) : '', + footerAlign: 'right', + }, + { + key: 'avg_weight', + header: 'Rata-Rata (Kg)', + flex: 0.8, + align: 'right', + cell: ({ row }) => formatNumber(row.average_weight_kg ?? 0), + footer: summary ? formatNumber(summary.total_weight_kg ?? 0) : '', + footerAlign: 'right', + }, + { + key: 'total_weight', + header: 'Total Berat (Kg)', + flex: 0.9, + align: 'right', + cell: ({ row }) => formatNumber(row.total_weight_kg ?? 0), + footer: summary ? formatNumber(summary.total_weight_kg ?? 0) : '', + footerAlign: 'right', + }, + { + key: 'sales_price', + header: 'Harga Jual (Rp)', + flex: 0.9, + align: 'right', + cell: ({ row }) => formatCurrency(row.sales_price_per_kg ?? 0), + footer: '', + }, + { + key: 'hpp_price', + header: 'HPP (Rp)', + flex: 1.3, + align: 'right', + cell: ({ row }) => formatCurrency(row.hpp_price_per_kg ?? 0), + footer: summary ? formatCurrency(summary.total_hpp_price_per_kg ?? 0) : '', + footerAlign: 'right', + }, + { + key: 'sales_amount', + header: 'Total Jual (Rp)', + flex: 1, + align: 'right', + cell: ({ row }) => formatCurrency(row.sales_amount ?? 0), + footer: summary ? formatCurrency(summary.total_sales_amount ?? 0) : '', + footerAlign: 'right', + }, + { + key: 'hpp_amount', + header: 'Total HPP (Rp)', + flex: 1.3, + align: 'right', + cell: ({ row }) => formatCurrency(row.hpp_amount ?? 0), + footer: summary ? formatCurrency(summary.total_hpp_amount ?? 0) : '', + footerAlign: 'right', + }, +]; const DailyMarketingReportPDF = ({ data, @@ -249,9 +255,9 @@ const DailyMarketingReportPDF = ({ {/* Table */} diff --git a/src/components/pages/report/marketing/export/HppPerkandangExportPDF.tsx b/src/components/pages/report/marketing/export/HppPerkandangExportPDF.tsx index d9f33f27..f2a3c835 100644 --- a/src/components/pages/report/marketing/export/HppPerkandangExportPDF.tsx +++ b/src/components/pages/report/marketing/export/HppPerkandangExportPDF.tsx @@ -14,12 +14,7 @@ import { HppPerKandangPerWeightRange, } from '@/types/api/report/hpp-per-kandang'; import { formatDate, formatNumber, formatCurrency } from '@/lib/helper'; -import { - PdfTable, - PdfColumn, - PdfTbodyCell, - PdfTfootCell, -} from '@/components/helper/pdf/table'; +import { PdfTable, PdfColumn } from '@/components/helper/pdf/table'; import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; @@ -70,234 +65,185 @@ const formatSuppliers = ( }; // Helper functions for PdfTable - Rekapitulasi -const getRekapitulasiColumns = (): PdfColumn[] => [ - { key: 'rentang_bw', header: 'Rentang BW', flex: 1.2, align: 'center' }, - { key: 'sisa_butir', header: 'Sisa Butir', flex: 1, align: 'right' }, - { key: 'sisa_kg', header: 'Sisa Kg', flex: 1, align: 'right' }, +const getRekapitulasiColumns = (): PdfColumn[] => [ + { + key: 'rentang_bw', + header: 'Rentang BW', + flex: 1.2, + align: 'center', + cell: ({ row }) => row.label || '-', + }, + { + key: 'sisa_butir', + header: 'Sisa Butir', + flex: 1, + align: 'right', + cell: ({ row }) => formatNumber(row.egg_production_pieces || 0), + }, + { + key: 'sisa_kg', + header: 'Sisa Kg', + flex: 1, + align: 'right', + cell: ({ row }) => formatNumber(row.egg_production_kg || 0), + }, { key: 'rata_rata_bobot', header: 'Rata-Rata Bobot (Kg)', flex: 1.2, align: 'right', + cell: ({ row }) => formatNumber(row.avg_weight_kg || 0), }, { key: 'feed_supplier', header: 'Feed (Supplier)', flex: 1.5, align: 'left', + cell: ({ row }) => formatSuppliers(row.feed_suppliers), }, { key: 'doc_supplier', header: 'DOC (Supplier)', flex: 1.2, align: 'left', + cell: ({ row }) => formatSuppliers(row.doc_suppliers), }, { key: 'rata_harga_doc', header: 'Rata-Rata Harga DOC', flex: 1.2, align: 'right', + cell: ({ row }) => formatCurrency(row.average_doc_price_rp || 0), }, { key: 'hpp_telur', header: 'HPP Telur (Rp/Kg)', flex: 1.2, align: 'right', + cell: ({ row }) => formatCurrency(row.egg_hpp_rp_per_kg || 0), }, { key: 'nominal_sisa', header: 'Nominal Sisa', flex: 1.2, align: 'right', + cell: ({ row }) => formatCurrency(row.egg_value_rp || 0), }, ]; -const getRekapitulasiData = ( - perWeightRange: HppPerKandangPerWeightRange[] -): PdfTbodyCell[][] => { - return perWeightRange.map((group) => [ - { key: 'rentang_bw', value: group.label || '-' }, - { - key: 'sisa_butir', - value: formatNumber(group.egg_production_pieces || 0), - align: 'right', - }, - { - key: 'sisa_kg', - value: formatNumber(group.egg_production_kg || 0), - align: 'right', - }, - { - key: 'rata_rata_bobot', - value: formatNumber(group.avg_weight_kg || 0), - align: 'right', - }, - { - key: 'feed_supplier', - value: formatSuppliers(group.feed_suppliers), - }, - { - key: 'doc_supplier', - value: formatSuppliers(group.doc_suppliers), - }, - { - key: 'rata_harga_doc', - value: formatCurrency(group.average_doc_price_rp || 0), - align: 'right', - }, - { - key: 'hpp_telur', - value: formatCurrency(group.egg_hpp_rp_per_kg || 0), - align: 'right', - }, - { - key: 'nominal_sisa', - value: formatCurrency(group.egg_value_rp || 0), - align: 'right', - }, - ]); -}; - // Helper functions for PdfTable - Detail Per Kandang -const getDetailColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'kandang', header: 'Kandang', flex: 1.5, align: 'left' }, - { key: 'rentang_bw', header: 'Rentang BW', flex: 1, align: 'left' }, +const getDetailColumns = ( + summary?: HppPerKandangReport['summary'], + allFeedSuppliers?: string, + allDocSuppliers?: string +): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + footer: 'TOTAL', + }, + { + key: 'kandang', + header: 'Kandang', + flex: 1.5, + align: 'left', + cell: ({ row }) => row.kandang?.name || '-', + footer: 'ALL', + }, + { + key: 'rentang_bw', + header: 'Rentang BW', + flex: 1, + align: 'left', + cell: ({ row }) => + `${row.weight_range.weight_min.toFixed(2)} - ${row.weight_range.weight_max.toFixed(2)}`, + footer: '-', + }, { key: 'rata_rata_bobot', header: 'Rata-Rata Bobot (Kg)', flex: 1, align: 'right', + cell: ({ row }) => formatNumber(row.avg_weight_kg || 0), + footer: summary ? formatNumber(summary.total.average_weight_kg || 0) : '', + footerAlign: 'right', + }, + { + key: 'sisa_butir', + header: 'Sisa Butir', + flex: 0.8, + align: 'right', + cell: ({ row }) => formatNumber(row.egg_production_pieces || 0), + footer: summary + ? formatNumber(summary.total.total_egg_production_pieces || 0) + : '', + footerAlign: 'right', + }, + { + key: 'sisa_kg', + header: 'Sisa Kg (Telur)', + flex: 0.8, + align: 'right', + cell: ({ row }) => formatNumber(row.egg_production_kg || 0), + footer: summary + ? formatNumber(summary.total.total_egg_production_kg || 0) + : '', + footerAlign: 'right', }, - { key: 'sisa_butir', header: 'Sisa Butir', flex: 0.8, align: 'right' }, - { key: 'sisa_kg', header: 'Sisa Kg (Telur)', flex: 0.8, align: 'right' }, { key: 'feed_supplier', header: 'Feed (Supplier)', flex: 1.2, align: 'left', + cell: ({ row }) => formatSuppliers(row.feed_suppliers), + footer: allFeedSuppliers || '-', }, { key: 'doc_supplier', header: 'DOC (Supplier)', flex: 1, align: 'left', + cell: ({ row }) => formatSuppliers(row.doc_suppliers), + footer: allDocSuppliers || '-', }, { key: 'rata_harga_doc', header: 'Rata-Rata Harga DOC', flex: 1.2, align: 'right', + cell: ({ row }) => formatCurrency(row.average_doc_price_rp || 0), + footer: summary + ? formatCurrency(summary.total.total_average_doc_price_rp || 0) + : '', + footerAlign: 'right', }, { key: 'hpp_telur', header: 'HPP Telur (Rp/Kg)', flex: 1, align: 'right', + cell: ({ row }) => formatCurrency(row.egg_hpp_rp_per_kg || 0), + footer: summary + ? formatCurrency(summary.total.average_egg_hpp_rp_per_kg || 0) + : '', + footerAlign: 'right', }, { key: 'nominal_sisa', header: 'Nominal Sisa', flex: 1.2, align: 'right', + cell: ({ row }) => formatCurrency(row.egg_value_rp || 0), + footer: summary + ? formatCurrency(summary.total.total_egg_value_rp || 0) + : '', + footerAlign: 'right', }, ]; -const getDetailData = (rows: HppPerKandangRow[]): PdfTbodyCell[][] => { - return rows.map((item, index) => [ - { key: 'no', value: index + 1 }, - { key: 'kandang', value: item.kandang?.name || '-' }, - { - key: 'rentang_bw', - value: `${item.weight_range.weight_min.toFixed(2)} - ${item.weight_range.weight_max.toFixed(2)}`, - }, - { - key: 'rata_rata_bobot', - value: formatNumber(item.avg_weight_kg || 0), - align: 'right', - }, - { - key: 'sisa_butir', - value: formatNumber(item.egg_production_pieces || 0), - align: 'right', - }, - { - key: 'sisa_kg', - value: formatNumber(item.egg_production_kg || 0), - align: 'right', - }, - { - key: 'feed_supplier', - value: formatSuppliers(item.feed_suppliers), - }, - { - key: 'doc_supplier', - value: formatSuppliers(item.doc_suppliers), - }, - { - key: 'rata_harga_doc', - value: formatCurrency(item.average_doc_price_rp || 0), - align: 'right', - }, - { - key: 'hpp_telur', - value: formatCurrency(item.egg_hpp_rp_per_kg || 0), - align: 'right', - }, - { - key: 'nominal_sisa', - value: formatCurrency(item.egg_value_rp || 0), - align: 'right', - }, - ]); -}; - -const getDetailFooter = ( - summary: HppPerKandangReport['summary'], - allFeedSuppliers: string, - allDocSuppliers: string -): PdfTfootCell[] => { - if (!summary?.total) return []; - - return [ - { key: 'no', value: 'TOTAL' }, - { key: 'kandang', value: 'ALL' }, - { key: 'rentang_bw', value: '-' }, - { - key: 'rata_rata_bobot', - value: formatNumber(summary.total.average_weight_kg || 0), - align: 'right', - }, - { - key: 'sisa_butir', - value: formatNumber(summary.total.total_egg_production_pieces || 0), - align: 'right', - }, - { - key: 'sisa_kg', - value: formatNumber(summary.total.total_egg_production_kg || 0), - align: 'right', - }, - { key: 'feed_supplier', value: allFeedSuppliers }, - { key: 'doc_supplier', value: allDocSuppliers }, - { - key: 'rata_harga_doc', - value: formatCurrency(summary.total.total_average_doc_price_rp || 0), - align: 'right', - }, - { - key: 'hpp_telur', - value: formatCurrency(summary.total.average_egg_hpp_rp_per_kg || 0), - align: 'right', - }, - { - key: 'nominal_sisa', - value: formatCurrency(summary.total.total_egg_value_rp || 0), - align: 'right', - }, - ]; -}; - const createPDFDocument = ( params: HppPerKandangExportParams, allFeedSuppliers: string, @@ -355,7 +301,7 @@ const createPDFDocument = ( @@ -365,17 +311,13 @@ const createPDFDocument = ( Detail Per Kandang
diff --git a/src/components/pages/report/production-result/ProductionResultReportPDF.tsx b/src/components/pages/report/production-result/ProductionResultReportPDF.tsx index 6f7b8313..eabb03bf 100644 --- a/src/components/pages/report/production-result/ProductionResultReportPDF.tsx +++ b/src/components/pages/report/production-result/ProductionResultReportPDF.tsx @@ -9,11 +9,7 @@ import { ProductionResult } from '@/types/api/report/production-result'; import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; import { PdfPageNumber } from '@/components/helper/pdf/layout/PdfPageNumber'; -import { - PdfTable, - PdfColumn, - PdfTbodyCell, -} from '@/components/helper/pdf/table'; +import { PdfTable, PdfColumn } from '@/components/helper/pdf/table'; type MappedProductionResultsItem = { projectFlockKandang: BaseProjectFlockKandang; @@ -64,246 +60,339 @@ function valueText(v: unknown) { // ======================================== // TABLE 1: WOA & BW // ======================================== -const getBwTableColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'woa', header: 'Week of Age', flex: 0.8, align: 'center' }, - { key: 'bw', header: 'Body Weight', flex: 1, align: 'right' }, - { key: 'std_bw', header: 'Std Body Weight', flex: 1, align: 'right' }, - { key: 'uniformity', header: 'Uniformity', flex: 1.2, align: 'right' }, +const getBwTableColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + }, + { + key: 'woa', + header: 'Week of Age', + flex: 0.8, + align: 'center', + cell: ({ row }) => valueText(row.woa), + }, + { + key: 'bw', + header: 'Body Weight', + flex: 1, + align: 'right', + cell: ({ row }) => valueText(row.bw), + }, + { + key: 'std_bw', + header: 'Std Body Weight', + flex: 1, + align: 'right', + cell: ({ row }) => valueText(row.std_bw), + }, + { + key: 'uniformity', + header: 'Uniformity', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.uniformity), + }, { key: 'std_uniformity', header: 'Std Uniformity', flex: 1.3, align: 'right', + cell: ({ row }) => valueText(row.std_uniformity), }, ]; -const getBwTableData = ( - productionResults: ProductionResult[] -): PdfTbodyCell[][] => { - return productionResults.map((pr, index) => { - return [ - { key: 'no', value: index + 1 }, - { key: 'woa', value: valueText(pr.woa) }, - { key: 'bw', value: valueText(pr.bw), align: 'right' }, - { key: 'std_bw', value: valueText(pr.std_bw), align: 'right' }, - { key: 'uniformity', value: valueText(pr.uniformity), align: 'right' }, - { - key: 'std_uniformity', - value: valueText(pr.std_uniformity), - align: 'right', - }, - ]; - }); -}; - // ======================================== // TABLE 2: DEPLESI // ======================================== -const getDepTableColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, +const getDepTableColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + }, { key: 'dep_kum', header: 'Depletion Cummulative', flex: 1.5, align: 'right', + cell: ({ row }) => valueText(row.dep_kum), + }, + { + key: 'dep_std', + header: 'Depletion Std', + flex: 1.5, + align: 'right', + cell: ({ row }) => valueText(row.dep_std), }, - { key: 'dep_std', header: 'Depletion Std', flex: 1.5, align: 'right' }, ]; -const getDepTableData = ( - productionResults: ProductionResult[] -): PdfTbodyCell[][] => { - return productionResults.map((pr, index) => { - return [ - { key: 'no', value: index + 1 }, - { key: 'dep_kum', value: valueText(pr.dep_kum), align: 'right' }, - { key: 'dep_std', value: valueText(pr.dep_std), align: 'right' }, - ]; - }); -}; - // ======================================== // TABLE 3: BUTIRAN // ======================================== -const getButiranTableColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'butiran_utuh', header: 'Utuh', flex: 1.2, align: 'right' }, - { key: 'butiran_putih', header: 'Putih', flex: 1.2, align: 'right' }, - { key: 'butiran_retak', header: 'Retak', flex: 1.2, align: 'right' }, - { key: 'butiran_pecah', header: 'Pecah', flex: 1.2, align: 'right' }, - { key: 'butiran_jumlah', header: 'Jumlah', flex: 1.2, align: 'right' }, - { key: 'total_butir', header: 'Total Butir', flex: 1.3, align: 'right' }, +const getButiranTableColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + }, + { + key: 'butiran_utuh', + header: 'Utuh', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.butiran_utuh), + }, + { + key: 'butiran_putih', + header: 'Putih', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.butiran_putih), + }, + { + key: 'butiran_retak', + header: 'Retak', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.butiran_retak), + }, + { + key: 'butiran_pecah', + header: 'Pecah', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.butiran_pecah), + }, + { + key: 'butiran_jumlah', + header: 'Jumlah', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.butiran_jumlah), + }, + { + key: 'total_butir', + header: 'Total Butir', + flex: 1.3, + align: 'right', + cell: ({ row }) => valueText(row.total_butir), + }, ]; -const getButiranTableData = ( - productionResults: ProductionResult[] -): PdfTbodyCell[][] => { - return productionResults.map((pr, index) => { - return [ - { key: 'no', value: index + 1 }, - { - key: 'butiran_utuh', - value: valueText(pr.butiran_utuh), - align: 'right', - }, - { - key: 'butiran_putih', - value: valueText(pr.butiran_putih), - align: 'right', - }, - { - key: 'butiran_retak', - value: valueText(pr.butiran_retak), - align: 'right', - }, - { - key: 'butiran_pecah', - value: valueText(pr.butiran_pecah), - align: 'right', - }, - { - key: 'butiran_jumlah', - value: valueText(pr.butiran_jumlah), - align: 'right', - }, - { - key: 'total_butir', - value: valueText(pr.total_butir), - align: 'right', - }, - ]; - }); -}; - // ======================================== // TABLE 4: BERAT (KG) // ======================================== -const getKgTableColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'kg_utuh', header: 'Utuh (Kg)', flex: 1.2, align: 'right' }, - { key: 'kg_putih', header: 'Putih (Kg)', flex: 1.2, align: 'right' }, - { key: 'kg_retak', header: 'Retak (Kg)', flex: 1.2, align: 'right' }, - { key: 'kg_pecah', header: 'Pecah (Kg)', flex: 1.2, align: 'right' }, - { key: 'kg_jumlah', header: 'Jumlah (Kg)', flex: 1.3, align: 'right' }, - { key: 'total_kg', header: 'Total (Kg)', flex: 1.3, align: 'right' }, +const getKgTableColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + }, + { + key: 'kg_utuh', + header: 'Utuh (Kg)', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.kg_utuh), + }, + { + key: 'kg_putih', + header: 'Putih (Kg)', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.kg_putih), + }, + { + key: 'kg_retak', + header: 'Retak (Kg)', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.kg_retak), + }, + { + key: 'kg_pecah', + header: 'Pecah (Kg)', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.kg_pecah), + }, + { + key: 'kg_jumlah', + header: 'Jumlah (Kg)', + flex: 1.3, + align: 'right', + cell: ({ row }) => valueText(row.kg_jumlah), + }, + { + key: 'total_kg', + header: 'Total (Kg)', + flex: 1.3, + align: 'right', + cell: ({ row }) => valueText(row.total_kg), + }, ]; -const getKgTableData = ( - productionResults: ProductionResult[] -): PdfTbodyCell[][] => { - return productionResults.map((pr, index) => { - return [ - { key: 'no', value: index + 1 }, - { key: 'kg_utuh', value: valueText(pr.kg_utuh), align: 'right' }, - { key: 'kg_putih', value: valueText(pr.kg_putih), align: 'right' }, - { key: 'kg_retak', value: valueText(pr.kg_retak), align: 'right' }, - { key: 'kg_pecah', value: valueText(pr.kg_pecah), align: 'right' }, - { key: 'kg_jumlah', value: valueText(pr.kg_jumlah), align: 'right' }, - { key: 'total_kg', value: valueText(pr.total_kg), align: 'right' }, - ]; - }); -}; - // ======================================== // TABLE 5: PERSENTASE // ======================================== -const getPersenTableColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'persen_utuh', header: 'Utuh (%)', flex: 1.5, align: 'right' }, - { key: 'persen_putih', header: 'Putih (%)', flex: 1.5, align: 'right' }, - { key: 'persen_retak', header: '% Retak (%)', flex: 1.5, align: 'right' }, - { key: 'persen_pecah', header: '% Pecah (%)', flex: 1.5, align: 'right' }, +const getPersenTableColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + }, + { + key: 'persen_utuh', + header: 'Utuh (%)', + flex: 1.5, + align: 'right', + cell: ({ row }) => valueText(row.persen_utuh), + }, + { + key: 'persen_putih', + header: 'Putih (%)', + flex: 1.5, + align: 'right', + cell: ({ row }) => valueText(row.persen_putih), + }, + { + key: 'persen_retak', + header: '% Retak (%)', + flex: 1.5, + align: 'right', + cell: ({ row }) => valueText(row.persen_retak), + }, + { + key: 'persen_pecah', + header: '% Pecah (%)', + flex: 1.5, + align: 'right', + cell: ({ row }) => valueText(row.persen_pecah), + }, ]; -const getPersenTableData = ( - productionResults: ProductionResult[] -): PdfTbodyCell[][] => { - return productionResults.map((pr, index) => { - return [ - { key: 'no', value: index + 1 }, - { - key: 'persen_utuh', - value: valueText(pr.persen_utuh), - align: 'right', - }, - { - key: 'persen_putih', - value: valueText(pr.persen_putih), - align: 'right', - }, - { - key: 'persen_retak', - value: valueText(pr.persen_retak), - align: 'right', - }, - { - key: 'persen_pecah', - value: valueText(pr.persen_pecah), - align: 'right', - }, - ]; - }); -}; - // ======================================== // TABLE 6: PRODUKSI (HD, FI, EM, EW) // ======================================== -const getProduksi1TableColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'hd', header: 'Hen Day', flex: 0.8, align: 'right' }, - { key: 'hd_std', header: 'Hen Day Std', flex: 1, align: 'right' }, - { key: 'fi', header: 'Feed Intake', flex: 0.8, align: 'right' }, - { key: 'fi_std', header: 'Feed Intake Std', flex: 1, align: 'right' }, - { key: 'em', header: 'Egg Mass', flex: 0.8, align: 'right' }, - { key: 'em_std', header: 'Egg Mass Std', flex: 1, align: 'right' }, - { key: 'ew', header: 'Egg Weight', flex: 0.8, align: 'right' }, - { key: 'ew_std', header: 'Egg Weight Std', flex: 1, align: 'right' }, +const getProduksi1TableColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + }, + { + key: 'hd', + header: 'Hen Day', + flex: 0.8, + align: 'right', + cell: ({ row }) => valueText(row.hd), + }, + { + key: 'hd_std', + header: 'Hen Day Std', + flex: 1, + align: 'right', + cell: ({ row }) => valueText(row.hd_std), + }, + { + key: 'fi', + header: 'Feed Intake', + flex: 0.8, + align: 'right', + cell: ({ row }) => valueText(row.fi), + }, + { + key: 'fi_std', + header: 'Feed Intake Std', + flex: 1, + align: 'right', + cell: ({ row }) => valueText(row.fi_std), + }, + { + key: 'em', + header: 'Egg Mass', + flex: 0.8, + align: 'right', + cell: ({ row }) => valueText(row.em), + }, + { + key: 'em_std', + header: 'Egg Mass Std', + flex: 1, + align: 'right', + cell: ({ row }) => valueText(row.em_std), + }, + { + key: 'ew', + header: 'Egg Weight', + flex: 0.8, + align: 'right', + cell: ({ row }) => valueText(row.ew), + }, + { + key: 'ew_std', + header: 'Egg Weight Std', + flex: 1, + align: 'right', + cell: ({ row }) => valueText(row.ew_std), + }, ]; -const getProduksi1TableData = ( - productionResults: ProductionResult[] -): PdfTbodyCell[][] => { - return productionResults.map((pr, index) => { - return [ - { key: 'no', value: index + 1 }, - { key: 'hd', value: valueText(pr.hd), align: 'right' }, - { key: 'hd_std', value: valueText(pr.hd_std), align: 'right' }, - { key: 'fi', value: valueText(pr.fi), align: 'right' }, - { key: 'fi_std', value: valueText(pr.fi_std), align: 'right' }, - { key: 'em', value: valueText(pr.em), align: 'right' }, - { key: 'em_std', value: valueText(pr.em_std), align: 'right' }, - { key: 'ew', value: valueText(pr.ew), align: 'right' }, - { key: 'ew_std', value: valueText(pr.ew_std), align: 'right' }, - ]; - }); -}; - // ======================================== // TABLE 7: PRODUKSI (FCR, HH) // ======================================== -const getProduksi2TableColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'fcr', header: 'FCR', flex: 1, align: 'right' }, - { key: 'fcr_std', header: 'FCR Std', flex: 1.2, align: 'right' }, - { key: 'hh', header: 'Hen House', flex: 1, align: 'right' }, - { key: 'hh_std', header: 'Hen House Std', flex: 1.2, align: 'right' }, +const getProduksi2TableColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + }, + { + key: 'fcr', + header: 'FCR', + flex: 1, + align: 'right', + cell: ({ row }) => valueText(row.fcr), + }, + { + key: 'fcr_std', + header: 'FCR Std', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.fcr_std), + }, + { + key: 'hh', + header: 'Hen House', + flex: 1, + align: 'right', + cell: ({ row }) => valueText(row.hh), + }, + { + key: 'hh_std', + header: 'Hen House Std', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.hh_std), + }, ]; -const getProduksi2TableData = ( - productionResults: ProductionResult[] -): PdfTbodyCell[][] => { - return productionResults.map((pr, index) => { - return [ - { key: 'no', value: index + 1 }, - { key: 'fcr', value: valueText(pr.fcr), align: 'right' }, - { key: 'fcr_std', value: valueText(pr.fcr_std), align: 'right' }, - { key: 'hh', value: valueText(pr.hh), align: 'right' }, - { key: 'hh_std', value: valueText(pr.hh_std), align: 'right' }, - ]; - }); -}; - /** * ✅ Main PDF Component */ @@ -383,7 +472,7 @@ const ProductionResultReportPDF = ({ 1. WOA & Body Weight @@ -392,7 +481,7 @@ const ProductionResultReportPDF = ({ 2. Deplesi @@ -401,7 +490,7 @@ const ProductionResultReportPDF = ({ 3. Butiran @@ -410,7 +499,7 @@ const ProductionResultReportPDF = ({ 4. Berat (Kg) @@ -419,7 +508,7 @@ const ProductionResultReportPDF = ({ 5. Persentase @@ -430,7 +519,7 @@ const ProductionResultReportPDF = ({ @@ -439,7 +528,7 @@ const ProductionResultReportPDF = ({ 7. Produksi (FCR, HH) From 539de03a5be11b9feb466b7db8bcb336bc4ff068 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 11 Feb 2026 10:41:57 +0700 Subject: [PATCH 16/16] refactor(FE): Update section width to use full width --- src/app/report/production-result/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/report/production-result/page.tsx b/src/app/report/production-result/page.tsx index 691ea734..cdac598c 100644 --- a/src/app/report/production-result/page.tsx +++ b/src/app/report/production-result/page.tsx @@ -2,7 +2,7 @@ import ProductionResultContent from '@/components/pages/report/production-result const ProductionResultReportPage = () => { return ( -
+
);