From 510573e66f16c1f2dd3459e5343755cbeecb962b Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 13:55:31 +0700 Subject: [PATCH] refactor(FE): Add Excel export functionality for daily marketing report --- .../export/DailyMarketingExportXLSX.tsx | 118 ++++++++++++++++++ .../marketing/tab/DailyMarketingTab.tsx | 49 ++++---- 2 files changed, 141 insertions(+), 26 deletions(-) diff --git a/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx b/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx index e69de29b..8c368fbf 100644 --- a/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx +++ b/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx @@ -0,0 +1,118 @@ +'use client'; + +import ExcelJS from 'exceljs'; +import { + formatCurrency, + formatNumber, + formatDate, + formatVechicleNumber, +} from '@/lib/helper'; +import { DailyMarketingRow, SalesSummary } from '@/types/api/report/marketing'; + +interface DailyMarketingExportExcelParams { + data: DailyMarketingRow[]; + summaryTotal?: SalesSummary; + period?: string; +} + +export const generateDailyMarketingExcel = async ( + params: DailyMarketingExportExcelParams +): Promise => { + if (!params.data || params.data.length === 0) { + return; + } + + const workbook = new ExcelJS.Workbook(); + + // ===== DAILY MARKETING WORKSHEET ===== + const columns = [ + { header: 'No', key: 'no', width: 5 }, + { header: 'Tanggal Jual', key: 'soDate', width: 15 }, + { header: 'Tanggal Realisasi', key: 'realizationDate', width: 18 }, + { header: 'Aging', key: 'aging', width: 10 }, + { header: 'Gudang', key: 'warehouse', width: 25 }, + { header: 'Pelanggan', key: 'customer', width: 25 }, + { header: 'No. DO', key: 'doNumber', width: 15 }, + { header: 'Sales/Marketing', key: 'sales', width: 20 }, + { header: 'No. Polisi', key: 'vehicleNumber', width: 15 }, + { header: 'Marketing Type', key: 'marketingType', width: 15 }, + { header: 'Produk', key: 'product', width: 20 }, + { header: 'Kuantitas', key: 'qty', width: 12 }, + { header: 'Bobot Rata-Rata (Kg)', key: 'averageWeight', width: 20 }, + { header: 'Bobot Total (Kg)', key: 'totalWeight', width: 18 }, + { header: 'Harga Jual (Rp)', key: 'salesPrice', width: 18 }, + { header: 'HPP (Rp)', key: 'hppPrice', width: 15 }, + { header: 'Total (Rp)', key: 'salesAmount', width: 20 }, + ]; + + const worksheet = workbook.addWorksheet('Laporan Marketing Harian'); + worksheet.columns = columns; + + // Add data rows + params.data.forEach((item: DailyMarketingRow, index: number) => { + worksheet.addRow({ + no: index + 1, + soDate: formatDate(item.so_date, 'DD-MMM-YYYY'), + realizationDate: formatDate(item.realization_date, 'DD-MMM-YYYY'), + aging: `${item.aging_days} hari`, + warehouse: item.warehouse?.name || '', + customer: item.customer?.name || '', + doNumber: item.do_number || '', + sales: item.sales?.name || '', + vehicleNumber: formatVechicleNumber(item.vehicle_number), + marketingType: item.marketing_type || '', + product: item.product?.name || '', + qty: formatNumber(item.qty || 0), + averageWeight: formatNumber(item.average_weight_kg || 0), + totalWeight: formatNumber(item.total_weight_kg || 0), + salesPrice: formatCurrency(item.sales_price_per_kg || 0), + hppPrice: formatCurrency(item.hpp_price_per_kg || 0), + salesAmount: formatCurrency(item.sales_amount || 0), + }); + }); + + // Add TOTAL row if summary data is available + if (params.summaryTotal) { + worksheet.addRow({ + no: 'TOTAL', + soDate: 'ALL', + realizationDate: '-', + aging: '-', + warehouse: '-', + customer: '-', + doNumber: '-', + sales: '-', + vehicleNumber: '-', + marketingType: '-', + product: '-', + qty: formatNumber(params.summaryTotal.total_qty || 0), + averageWeight: formatNumber(params.summaryTotal.average_weight_kg || 0), + totalWeight: formatNumber(params.summaryTotal.total_weight_kg || 0), + salesPrice: formatNumber(params.summaryTotal.average_sales_price || 0), + hppPrice: formatCurrency(params.summaryTotal.total_hpp_price_per_kg || 0), + salesAmount: formatCurrency(params.summaryTotal.total_sales_amount || 0), + }); + } + + worksheet.columns.forEach((column) => { + if (column.width && column.width < 10) { + column.width = 10; + } + }); + + const currentDate = new Date().toISOString().split('T')[0]; + const filename = params.period + ? `laporan-marketing-harian-${params.period}.xlsx` + : `laporan-marketing-harian-${currentDate}.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/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index 5fcc630f..a687fdf5 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -15,6 +15,7 @@ import { formatNumber, formatDate, formatVechicleNumber, + formatTitleCase, } from '@/lib/helper'; import { DailyMarketingRow, @@ -23,9 +24,8 @@ import { import { isResponseSuccess } from '@/lib/api-helper'; import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; -import MenuItem from '@/components/menu/MenuItem'; -import Menu from '@/components/menu/Menu'; import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingExportPDF'; +import { generateDailyMarketingExcel } from '@/components/pages/report/marketing/export/DailyMarketingExportXLSX'; import { pdf } from '@react-pdf/renderer'; import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; @@ -336,31 +336,23 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { const handleExportExcel = useCallback(async () => { setIsExcelExportLoading(true); try { - const queryString = new URLSearchParams(); + const allDataForExport = await dailyMarketingsExport(); - if (searchValue) queryString.set('search', searchValue); - if (filterParams.area_id) - queryString.set('area_id', filterParams.area_id); - if (filterParams.location_id) - queryString.set('location_id', filterParams.location_id); - if (filterParams.warehouse_id) - queryString.set('warehouse_id', filterParams.warehouse_id); - if (filterParams.customer_id) - queryString.set('customer_id', filterParams.customer_id); - if (filterParams.start_date) - queryString.set('start_date', filterParams.start_date); - if (filterParams.end_date) - queryString.set('end_date', filterParams.end_date); - if (filterParams.filter_by) - queryString.set('filter_by', filterParams.filter_by); - if (filterParams.marketing_type) - queryString.set('marketing_type', filterParams.marketing_type); - if (filterParams.sort_by) - queryString.set('sort_by', filterParams.sort_by); + if (!allDataForExport || allDataForExport.length === 0) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } - await MarketingReportApi.exportDailyMarketingToExcel( - `?${queryString.toString()}` - ); + const period = + filterParams.start_date && filterParams.end_date + ? `${formatDate(filterParams.start_date, 'DD-MMM-YYYY')}_to_${formatDate(filterParams.end_date, 'DD-MMM-YYYY')}` + : undefined; + + await generateDailyMarketingExcel({ + data: allDataForExport, + summaryTotal: summaryTotal, + period: period, + }); toast.success('Excel berhasil dibuat dan diunduh.'); } catch { @@ -368,7 +360,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { } finally { setIsExcelExportLoading(false); } - }, [filterParams, searchValue]); + }, [filterParams, dailyMarketingsExport, summaryTotal]); const handleExportPDF = useCallback(async () => { setIsPdfExportLoading(true); @@ -588,6 +580,11 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { id: 'marketing_type', header: 'Marketing Type', accessorKey: 'marketing_type', + cell: (props) => ( + + {formatTitleCase(props.row.original.marketing_type || '-')} + + ), footer: () =>
-
, }, {