refactor(FE-361,363,364): Use per-supplier report arrays for export

This commit is contained in:
rstubryan
2025-12-18 14:58:54 +07:00
parent 20494657c6
commit d001b05c4e
2 changed files with 154 additions and 229 deletions
@@ -15,7 +15,10 @@ import { LogisticApi } from '@/services/api/report/logistic-stock';
import Table from '@/components/Table'; import Table from '@/components/Table';
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; import {
LogisticPurchasePerSupplierReport,
LogisticPurchasePerSupplierSummary,
} from '@/types/api/report/logistic-stock';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import Pagination from '@/components/Pagination'; import Pagination from '@/components/Pagination';
@@ -26,22 +29,6 @@ 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/PurchasesPerSupplierExport';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import { Supplier } from '@/types/api/master-data/supplier';
interface Totals {
total_qty: number;
total_price: number;
total_purchase_amount: number;
total_transport: number;
total_value_transport: number;
total_jumlah: number;
}
interface GroupedSupplierData {
id: number;
supplier: Supplier;
items: LogisticPurchasePerSupplierReport['rows'];
}
const PurchasesPerSupplierTab = () => { const PurchasesPerSupplierTab = () => {
// ===== STATE MANAGEMENT ===== // ===== STATE MANAGEMENT =====
@@ -279,11 +266,11 @@ const PurchasesPerSupplierTab = () => {
) )
); );
const data: LogisticPurchasePerSupplierReport['rows'] = useMemo( const data: LogisticPurchasePerSupplierReport[] = useMemo(
() => () =>
isResponseSuccess(purchasePerSupplier) isResponseSuccess(purchasePerSupplier)
? (purchasePerSupplier?.data ? (purchasePerSupplier?.data as unknown as LogisticPurchasePerSupplierReport[]) ||
?.rows as LogisticPurchasePerSupplierReport['rows']) || [] []
: [], : [],
[purchasePerSupplier] [purchasePerSupplier]
); );
@@ -294,8 +281,9 @@ const PurchasesPerSupplierTab = () => {
: null; : null;
// ===== EXPORT DATA FETCHER ===== // ===== EXPORT DATA FETCHER =====
const logisticPurchasePerSupplierExport = const logisticPurchasePerSupplierExport = useCallback(async (): Promise<
useCallback(async (): Promise<LogisticPurchasePerSupplierReport | null> => { LogisticPurchasePerSupplierReport[] | null
> => {
const params = { const params = {
area_id: area_id:
tableFilterState.area_id.length > 0 tableFilterState.area_id.length > 0
@@ -344,7 +332,9 @@ const PurchasesPerSupplierTab = () => {
params.limit params.limit
); );
return isResponseSuccess(response) ? response.data : null; return isResponseSuccess(response)
? (response.data as unknown as LogisticPurchasePerSupplierReport[])
: null;
}, [tableFilterState]); }, [tableFilterState]);
// ===== EXPORT HANDLERS ===== // ===== EXPORT HANDLERS =====
@@ -355,50 +345,19 @@ const PurchasesPerSupplierTab = () => {
if ( if (
!allDataForExport || !allDataForExport ||
!allDataForExport?.rows || !Array.isArray(allDataForExport) ||
allDataForExport.rows.length === 0 allDataForExport.length === 0
) { ) {
toast.error('Tidak ada data untuk diekspor.'); toast.error('Tidak ada data untuk diekspor.');
return; return;
} }
const allExportData =
allDataForExport.rows as LogisticPurchasePerSupplierReport['rows'];
const groupedBySupplier: { [key: string]: typeof allExportData } = {};
allExportData.forEach((item) => {
const supplierName = item.supplier?.name || 'Unknown Supplier';
if (!groupedBySupplier[supplierName]) {
groupedBySupplier[supplierName] = [];
}
groupedBySupplier[supplierName].push(item);
});
const workbook = XLSX.utils.book_new(); const workbook = XLSX.utils.book_new();
Object.entries(groupedBySupplier).forEach( allDataForExport.forEach((supplierReport) => {
([supplierName, supplierData]) => { const supplierData = supplierReport.rows;
const totals = supplierData.reduce( const supplierName =
(acc, item) => ({ supplierReport.supplier?.name || 'Unknown Supplier';
total_qty: acc.total_qty + (item.qty || 0),
total_price: acc.total_price + (item.unit_price || 0),
total_purchase_amount:
acc.total_purchase_amount + (item.purchase_value || 0),
total_transport:
acc.total_transport + (item.transport_unit_price || 0),
total_value_transport:
acc.total_value_transport + (item.transport_value || 0),
total_jumlah: acc.total_jumlah + (item.total_amount || 0),
}),
{
total_qty: 0,
total_price: 0,
total_purchase_amount: 0,
total_transport: 0,
total_value_transport: 0,
total_jumlah: 0,
}
);
const excelData: { [key: string]: string | number }[] = const excelData: { [key: string]: string | number }[] =
supplierData.map((item, index) => ({ supplierData.map((item, index) => ({
@@ -422,6 +381,7 @@ const PurchasesPerSupplierTab = () => {
'Surat Jalan': item.delivery_number || '', 'Surat Jalan': item.delivery_number || '',
})); }));
if (supplierReport.summary) {
excelData.push({ excelData.push({
No: 'Total', No: 'Total',
'Tanggal Terima': '', 'Tanggal Terima': '',
@@ -429,15 +389,18 @@ const PurchasesPerSupplierTab = () => {
'No. Referensi': '', 'No. Referensi': '',
'Nama Produk': '', 'Nama Produk': '',
Tujuan: '', Tujuan: '',
QTY: totals.total_qty, QTY: supplierReport.summary.total_qty || 0,
'Harga Beli (Rp)': totals.total_price, 'Harga Beli (Rp)': '',
'Value Harga Beli (Rp)': totals.total_purchase_amount, 'Value Harga Beli (Rp)':
'Transport (Rp)': totals.total_transport, supplierReport.summary.total_purchase_value || 0,
'Value Transport (Rp)': totals.total_value_transport, 'Transport (Rp)': '',
'Jumlah (Rp)': totals.total_jumlah, 'Value Transport (Rp)':
supplierReport.summary.total_transport_value || 0,
'Jumlah (Rp)': supplierReport.summary.total_amount || 0,
Ekspedisi: '', Ekspedisi: '',
'Surat Jalan': '', 'Surat Jalan': '',
}); });
}
const worksheet = XLSX.utils.json_to_sheet(excelData); const worksheet = XLSX.utils.json_to_sheet(excelData);
@@ -464,8 +427,7 @@ const PurchasesPerSupplierTab = () => {
? supplierName.substring(0, 31) ? supplierName.substring(0, 31)
: supplierName; : supplierName;
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
} });
);
const filename = `laporan-pembelian-per-supplier-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; const filename = `laporan-pembelian-per-supplier-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`;
@@ -490,12 +452,17 @@ const PurchasesPerSupplierTab = () => {
if ( if (
!allDataForExport || !allDataForExport ||
!allDataForExport?.rows || !Array.isArray(allDataForExport) ||
allDataForExport.rows.length === 0 allDataForExport.length === 0
) { ) {
toast.error('Tidak ada data untuk diekspor.'); toast.error('Tidak ada data untuk diekspor.');
return; return;
} }
const allRows = allDataForExport.flatMap(
(supplierReport) => supplierReport.rows
);
const areaName = const areaName =
tableFilterState.area_id.length > 0 tableFilterState.area_id.length > 0
? tableFilterState.area_id ? tableFilterState.area_id
@@ -551,10 +518,7 @@ const PurchasesPerSupplierTab = () => {
end_date: tableFilterState.end_date || '', end_date: tableFilterState.end_date || '',
}; };
await generatePurchasesPerSupplierPDF( await generatePurchasesPerSupplierPDF(allRows, exportParams);
allDataForExport.rows,
exportParams
);
toast.success('PDF berhasil dibuat dan diunduh.'); toast.success('PDF berhasil dibuat dan diunduh.');
} catch { } catch {
toast.error('Gagal membuat PDF. Silakan coba lagi.'); toast.error('Gagal membuat PDF. Silakan coba lagi.');
@@ -591,28 +555,8 @@ const PurchasesPerSupplierTab = () => {
} }
}; };
// ===== TABLE COLUMNS DEFINITION =====
const groupedData = useMemo(() => {
const groups: { [key: number]: GroupedSupplierData } = {};
data.forEach((item) => {
const supplierId = item.supplier?.id;
if (supplierId && !groups[supplierId]) {
groups[supplierId] = {
id: supplierId,
supplier: item.supplier,
items: [],
};
}
if (groups[supplierId]) {
groups[supplierId].items.push(item);
}
});
return Object.values(groups);
}, [data]);
const getTableColumns = ( const getTableColumns = (
totals: Totals summary: LogisticPurchasePerSupplierSummary
): ColumnDef<LogisticPurchasePerSupplierReport['rows'][0]>[] => { ): ColumnDef<LogisticPurchasePerSupplierReport['rows'][0]>[] => {
const tableColumns: ColumnDef< const tableColumns: ColumnDef<
LogisticPurchasePerSupplierReport['rows'][0] LogisticPurchasePerSupplierReport['rows'][0]
@@ -679,7 +623,7 @@ const PurchasesPerSupplierTab = () => {
}, },
footer: () => ( footer: () => (
<div className='text-right font-semibold text-gray-900'> <div className='text-right font-semibold text-gray-900'>
{formatNumber(totals.total_qty)} {formatNumber(summary.total_qty) || '-'}
</div> </div>
), ),
}, },
@@ -693,7 +637,7 @@ const PurchasesPerSupplierTab = () => {
}, },
footer: () => ( footer: () => (
<div className='text-right font-semibold text-gray-900'> <div className='text-right font-semibold text-gray-900'>
{formatCurrency(totals.total_price)} {formatCurrency(summary.total_unit_price) || '-'}
</div> </div>
), ),
}, },
@@ -707,7 +651,7 @@ const PurchasesPerSupplierTab = () => {
}, },
footer: () => ( footer: () => (
<div className='text-right font-semibold text-gray-900'> <div className='text-right font-semibold text-gray-900'>
{formatCurrency(totals.total_purchase_amount)} {formatCurrency(summary.total_purchase_value) || '-'}
</div> </div>
), ),
}, },
@@ -721,7 +665,7 @@ const PurchasesPerSupplierTab = () => {
}, },
footer: () => ( footer: () => (
<div className='text-right font-semibold text-gray-900'> <div className='text-right font-semibold text-gray-900'>
{formatCurrency(totals.total_transport)} {formatCurrency(summary.total_transport_unit_price) || '-'}
</div> </div>
), ),
}, },
@@ -735,7 +679,7 @@ const PurchasesPerSupplierTab = () => {
}, },
footer: () => ( footer: () => (
<div className='text-right font-semibold text-gray-900'> <div className='text-right font-semibold text-gray-900'>
{formatCurrency(totals.total_value_transport)} {formatCurrency(summary.total_transport_value) || '-'}
</div> </div>
), ),
}, },
@@ -749,7 +693,7 @@ const PurchasesPerSupplierTab = () => {
}, },
footer: () => ( footer: () => (
<div className='text-right font-semibold text-gray-900'> <div className='text-right font-semibold text-gray-900'>
{formatCurrency(totals.total_jumlah)} {formatCurrency(summary.total_amount) || '-'}
</div> </div>
), ),
}, },
@@ -920,54 +864,33 @@ const PurchasesPerSupplierTab = () => {
Tidak ada data yang dapat ditampilkan... Tidak ada data yang dapat ditampilkan...
</div> </div>
) : ( ) : (
groupedData.map((supplier) => { data.map((supplierReport) => {
const total_qty = supplier.items.reduce( const summary = supplierReport.summary || {
(sum, item) => sum + (item.qty || 0), total_qty: 0,
0 total_unit_price: 0,
); total_purchase_value: 0,
const total_price = supplier.items.reduce( total_transport_unit_price: 0,
(sum, item) => sum + (item.purchase_value || 0), total_transport_value: 0,
0 total_amount: 0,
);
const total_transport = supplier.items.reduce(
(sum, item) => sum + (item.transport_value || 0),
0
);
const total_value_transport = supplier.items.reduce(
(sum, item) => sum + (item.transport_value || 0),
0
);
const total_jumlah = supplier.items.reduce(
(sum, item) => sum + (item.total_amount || 0),
0
);
const totals = {
total_qty,
total_price,
total_purchase_amount: total_price,
total_transport,
total_value_transport,
total_jumlah,
}; };
const totalPurchase = totals.total_jumlah; const totalPurchase = summary.total_amount;
const tableColumns = getTableColumns(totals); const tableColumns = getTableColumns(summary);
return ( return (
<Card <Card
key={supplier.id} key={supplierReport.supplier.id}
title={supplier.supplier.name} title={supplierReport.supplier.name}
subtitle={`Total Pembelian: ${formatCurrency(totalPurchase)}`} subtitle={`Total Pembelian: ${formatCurrency(totalPurchase)}`}
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
variant='bordered' variant='bordered'
collapsible={true} collapsible={true}
> >
<Table <Table
data={supplier.items} data={supplierReport.rows}
columns={tableColumns} columns={tableColumns}
pageSize={10} pageSize={10}
renderFooter={supplier.items.length > 0} renderFooter={supplierReport.rows.length > 0}
className={{ className={{
containerClassName: 'w-full', containerClassName: 'w-full',
tableWrapperClassName: 'overflow-x-auto mt-4', tableWrapperClassName: 'overflow-x-auto mt-4',
+2
View File
@@ -21,7 +21,9 @@ export type LogisticPurchasePerSupplierReportRow = {
export type LogisticPurchasePerSupplierSummary = { export type LogisticPurchasePerSupplierSummary = {
total_qty: number; total_qty: number;
total_unit_price: number;
total_purchase_value: number; total_purchase_value: number;
total_transport_unit_price: number;
total_transport_value: number; total_transport_value: number;
total_amount: number; total_amount: number;
}; };