From d0dea834c1c18b87b72b9b156c4794b0c8f013e3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 11:54:15 +0700 Subject: [PATCH] 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);