refactor(FE): Add Excel export functionality for daily marketing report

This commit is contained in:
rstubryan
2026-02-12 13:55:31 +07:00
parent dbcf469123
commit 510573e66f
2 changed files with 141 additions and 26 deletions
@@ -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<void> => {
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);
};
@@ -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) => (
<span className='text-nowrap'>
{formatTitleCase(props.row.original.marketing_type || '-')}
</span>
),
footer: () => <div className='font-semibold text-gray-900'>-</div>,
},
{