From c20b1c594238977bdafc390d156e951ed542a2af Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 28 Jan 2026 11:04:06 +0700 Subject: [PATCH] refactor(FE): Refactor PDF exports to use PdfTable --- .../export/PurchasesPerSupplierExport.tsx | 313 +++----- .../sale/export/HppPerkandangExport.tsx | 702 ++++++------------ 2 files changed, 324 insertions(+), 691 deletions(-) diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx index a7967159..bd6f301a 100644 --- a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx @@ -11,6 +11,11 @@ import { } from '@react-pdf/renderer'; import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { + PdfTable, + PdfColumn, + PdfTbodyCell, +} from '@/components/helper/pdf/table'; Font.register({ family: 'Helvetica', @@ -39,117 +44,6 @@ const pdfStyles = StyleSheet.create({ marginBottom: 8, color: '#1f74bf', }, - table: { - borderWidth: 1, - borderColor: '#000000', - marginBottom: 15, - }, - tableRow: { - flexDirection: 'row', - }, - tableHeader: { - backgroundColor: '#F5F5F5', - }, - tableCell: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - textAlign: 'left', - }, - tableCellNo: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - textAlign: 'center', - }, - tableCellLast: { - flex: 1, - padding: 4, - fontSize: 8, - }, - tableCellHeader: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - paddingVertical: 12, - textAlign: 'center', - }, - tableCellHeaderRight: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - textAlign: 'right', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - paddingVertical: 12, - }, - tableCellHeaderLast: { - flex: 1, - padding: 4, - fontSize: 8, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - paddingVertical: 12, - textAlign: 'center', - }, - tableCellRight: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - textAlign: 'right', - }, - tableCellCenter: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - textAlign: 'center', - }, - tableCellCenterLast: { - flex: 1, - padding: 4, - fontSize: 8, - textAlign: 'center', - }, - tableBorderBottom: { - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - }, - supplierSection: { - marginBottom: 10, - }, - supplierSectionBreak: { - marginBottom: 15, - }, badge: { backgroundColor: '#1f74bf', color: '#FFFFFF', @@ -174,6 +68,12 @@ const pdfStyles = StyleSheet.create({ flexWrap: 'wrap', marginBottom: 8, }, + supplierSection: { + marginBottom: 10, + }, + supplierSectionBreak: { + marginBottom: 15, + }, }); interface PurchasesPerSupplierExportParams { @@ -218,6 +118,85 @@ const getParameterText = ( 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: 'purchase_value', + header: 'Nilai Pembelian', + flex: 1.5, + align: 'right', + }, + { + key: 'transport_price', + header: 'Biaya Transport', + flex: 1.2, + 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' }, +]; + +const getTableData = ( + rows: LogisticPurchasePerSupplierReport['rows'] +): PdfTbodyCell[][] => { + return rows.map((item, index) => [ + { key: 'no', value: index + 1, align: 'center' }, + { + key: 'receive_date', + value: formatDate(item.receive_date, 'DD-MMM-YYYY'), + align: 'center', + }, + { + key: 'po_date', + value: formatDate(item.po_date, 'DD-MMM-YYYY'), + align: 'center', + }, + { 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_price', + value: formatCurrency(item.transport_unit_price || 0), + align: 'right', + }, + { + key: 'total_amount', + value: formatCurrency(item.total_amount || 0), + align: 'right', + }, + { + key: 'expedition', + value: ( + + {item.expedition || '-'} + + ), + align: 'center', + }, + { key: 'delivery_number', value: item.delivery_number || '-' }, + ]); +}; + const createPDFDocument = ( supplierReports: LogisticPurchasePerSupplierReport[], params: PurchasesPerSupplierExportParams['params'] @@ -266,114 +245,10 @@ const createPDFDocument = ( {supplierReport.supplier.name} - - {/* Table Header */} - - - No - - - Tanggal Terima - - - Tanggal PO - - - Referensi - - - Produk - - - Tujuan - - - Qty - - - Harga Beli - - - Nilai Pembelian - - - Biaya Transport - - - Total - - - Armada - - - Surat Jalan - - - - {/* Table Body */} - {supplierReport.rows.map( - ( - item: LogisticPurchasePerSupplierReport['rows'][number], - index: number - ) => ( - - - {index + 1} - - - - {formatDate(item.receive_date, 'DD-MMM-YYYY')} - - - - {formatDate(item.po_date, 'DD-MMM-YYYY')} - - - {item.po_number || '-'} - - - {item.product?.name || '-'} - - - {item.warehouse?.name || '-'} - - - {formatNumber(item.qty || 0)} - - - {formatCurrency(item.unit_price || 0)} - - - {formatCurrency(item.purchase_value || 0)} - - - - {formatCurrency(item.transport_unit_price || 0)} - - - - {formatCurrency(item.total_amount || 0)} - - - - {item.expedition || '-'} - - - - {item.delivery_number || '-'} - - - ) - )} - + ); } diff --git a/src/components/pages/report/sale/export/HppPerkandangExport.tsx b/src/components/pages/report/sale/export/HppPerkandangExport.tsx index 9b05a88d..3a76d8f4 100644 --- a/src/components/pages/report/sale/export/HppPerkandangExport.tsx +++ b/src/components/pages/report/sale/export/HppPerkandangExport.tsx @@ -15,6 +15,12 @@ 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'; Font.register({ family: 'Helvetica', @@ -43,85 +49,6 @@ const pdfStyles = StyleSheet.create({ marginBottom: 8, color: '#1f74bf', }, - table: { - borderWidth: 1, - borderColor: '#000000', - marginBottom: 15, - }, - tableRow: { - flexDirection: 'row', - }, - tableHeader: { - backgroundColor: '#F5F5F5', - }, - tableCell: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - textAlign: 'left', - }, - tableCellHeader: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - paddingVertical: 12, - textAlign: 'center', - }, - tableCellHeaderRight: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - textAlign: 'right', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - paddingVertical: 12, - }, - tableCellRight: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - textAlign: 'right', - }, - tableCellCenter: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - textAlign: 'center', - }, - tableBorderBottom: { - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - }, - supplierSection: { - marginBottom: 10, - }, - supplierSectionBreak: { - marginBottom: 15, - }, parameterBadge: { backgroundColor: '#F5F5F5', color: '#333333', @@ -136,6 +63,9 @@ const pdfStyles = StyleSheet.create({ flexWrap: 'wrap', marginBottom: 8, }, + section: { + marginBottom: 15, + }, }); interface HppPerKandangExportParams { @@ -192,6 +122,215 @@ const getParameterText = (params: HppPerKandangExportParams['params']) => { return paramsText; }; +// 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' }, + { + key: 'rata_rata_bobot', + header: 'Rata-Rata Bobot (Kg)', + 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: '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' }, +]; + +const getRekapitulasiData = ( + perWeightRange: HppPerKandangPerWeightRange[] +): PdfTbodyCell[][] => { + return perWeightRange.map((group) => [ + { key: 'rentang_bw', value: group.label, align: 'center' }, + { + key: 'sisa_butir', + value: formatNumber(group.egg_production_pieces), + align: 'right', + }, + { + key: 'sisa_kg', + value: formatNumber(group.egg_production_kg), + align: 'right', + }, + { + key: 'rata_rata_bobot', + value: formatNumber(group.avg_weight_kg), + align: 'right', + }, + { + key: 'feed_supplier', + value: + group.feed_suppliers + ?.map((s: { alias?: string; name: string }) => s.alias || s.name) + .join(' | ') || '-', + }, + { + key: 'doc_supplier', + value: + group.doc_suppliers + ?.map((s: { alias?: string; name: string }) => s.alias || s.name) + .join(' | ') || '-', + }, + { + key: 'rata_harga_doc', + value: formatCurrency(group.average_doc_price_rp), + align: 'right', + }, + { + key: 'hpp_telur', + value: formatCurrency(group.egg_hpp_rp_per_kg), + align: 'right', + }, + { + key: 'nominal_sisa', + value: formatCurrency(group.egg_value_rp), + 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' }, + { + key: 'rata_rata_bobot', + header: 'Rata-Rata Bobot (Kg)', + flex: 1, + align: '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' }, + { 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' }, +]; + +const getDetailData = (rows: HppPerKandangRow[]): PdfTbodyCell[][] => { + return rows.map((item, index) => [ + { key: 'no', value: index + 1, align: 'center' }, + { 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), + align: 'right', + }, + { + key: 'sisa_butir', + value: formatNumber(item.egg_production_pieces), + align: 'right', + }, + { + key: 'sisa_kg', + value: formatNumber(item.egg_production_kg), + align: 'right', + }, + { + key: 'feed_supplier', + value: + item.feed_suppliers + ?.map((s: { alias?: string; name: string }) => s.alias || s.name) + .join(' | ') || '-', + }, + { + key: 'doc_supplier', + value: + item.doc_suppliers + ?.map((s: { alias?: string; name: string }) => s.alias || s.name) + .join(' | ') || '-', + }, + { + key: 'rata_harga_doc', + value: formatCurrency(item.average_doc_price_rp), + align: 'right', + }, + { + key: 'hpp_telur', + value: formatCurrency(item.egg_hpp_rp_per_kg), + align: 'right', + }, + { + key: 'nominal_sisa', + value: formatCurrency(item.egg_value_rp), + align: 'right', + }, + ]); +}; + +const getDetailFooter = ( + summary: HppPerKandangReport['summary'] +): 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), + align: 'right', + }, + { + key: 'sisa_butir', + value: formatNumber(summary.total.total_egg_production_pieces), + align: 'right', + }, + { + key: 'sisa_kg', + value: formatNumber(summary.total.total_egg_production_kg), + 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), + align: 'right', + }, + { + key: 'hpp_telur', + value: formatCurrency(summary.total.average_egg_hpp_rp_per_kg), + align: 'right', + }, + { + key: 'nominal_sisa', + value: formatCurrency(summary.total.total_egg_value_rp), + align: 'right', + }, + ]; +}; + const createPDFDocument = ( data: HppPerKandangExportParams['data'], params: HppPerKandangExportParams['params'] @@ -216,404 +355,23 @@ const createPDFDocument = ( {/* Rekapitulasi Section */} - + Rekapitulasi - - - {/* Table Header */} - - - Rentang BW - - - Sisa Butir - - - Sisa Kg - - - Rata-Rata Bobot (Kg) - - - Feed (Supplier) - - - DOC (Supplier) - - - Rata-Rata Harga DOC - - - HPP Telur (RP/KG) - - - Nominal Sisa - - - - {/* Table Body - Rekapitulasi */} - {rekapitulasiByWeightRange.map( - (group: HppPerKandangPerWeightRange, index: number) => ( - - - {group.label} - - - {formatNumber(group.egg_production_pieces)} - - - {formatNumber(group.egg_production_kg)} - - - {formatNumber(group.avg_weight_kg)} - - - - {group.feed_suppliers - ?.map( - (s: { alias?: string; name: string }) => - s.alias || s.name - ) - .join(' | ') || '-'} - - - - - {group.doc_suppliers - ?.map( - (s: { alias?: string; name: string }) => - s.alias || s.name - ) - .join(' | ') || '-'} - - - - {formatCurrency(group.average_doc_price_rp)} - - - {formatCurrency(group.egg_hpp_rp_per_kg)} - - - {formatCurrency(group.egg_value_rp)} - - - ) - )} - + {/* Detail Per Kandang Section */} - + Detail Per Kandang - - - {/* Table Header */} - - - No - - - Kandang - - - Rentang BW - - - Rata-Rata Bobot (Kg) - - - Sisa Butir - - - Sisa Kg (Telur) - - - Feed (Supplier) - - - DOC (Supplier) - - - Rata-Rata Harga DOC - - - HPP Telur (RP/KG) - - - Nominal Sisa - - - - {/* Table Body - Detail Per Kandang */} - {data.rows.map((item: HppPerKandangRow, index: number) => ( - - - {index + 1} - - - {item.kandang?.name || '-'} - - - - {item.weight_range.weight_min.toFixed(2)} -{' '} - {item.weight_range.weight_max.toFixed(2)} - - - - {formatNumber(item.avg_weight_kg)} - - - {formatNumber(item.egg_production_pieces)} - - - {formatNumber(item.egg_production_kg)} - - - - {item.feed_suppliers - ?.map( - (s: { alias?: string; name: string }) => - s.alias || s.name - ) - .join(' | ')} - - - - - {item.doc_suppliers - ?.map( - (s: { alias?: string; name: string }) => - s.alias || s.name - ) - .join(' | ')} - - - - {formatCurrency(item.average_doc_price_rp)} - - - {formatCurrency(item.egg_hpp_rp_per_kg)} - - - {formatCurrency(item.egg_value_rp)} - - - ))} - - {/* TOTAL Row */} - {data.summary?.total && ( - - - TOTAL - - - ALL - - - - - - - - {formatNumber(data.summary.total.average_weight_kg)} - - - - - {formatNumber( - data.summary.total.total_egg_production_pieces - )} - - - - - {formatNumber(data.summary.total.total_egg_production_kg)} - - - - - {data.rows - .flatMap((row: HppPerKandangRow) => - row.feed_suppliers?.map( - (s: { alias?: string; name: string }) => - s.alias || s.name - ) - ) - .filter( - (v: string, i: number, a: string[]) => - a.indexOf(v) === i - ) - .join(' | ') || '-'} - - - - - {data.rows - .flatMap((row: HppPerKandangRow) => - row.doc_suppliers?.map( - (s: { alias?: string; name: string }) => - s.alias || s.name - ) - ) - .filter( - (v: string, i: number, a: string[]) => - a.indexOf(v) === i - ) - .join(' | ') || '-'} - - - - - {formatCurrency( - data.summary.total.total_average_doc_price_rp - )} - - - - - {formatCurrency( - data.summary.total.average_egg_hpp_rp_per_kg - )} - - - - - {formatCurrency(data.summary.total.total_egg_value_rp)} - - - - )} - +