From d001b05c4e020ee91f4188e5dba4b0da2ac74303 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 14:58:54 +0700 Subject: [PATCH] refactor(FE-361,363,364): Use per-supplier report arrays for export --- .../tab/PurchasesPerSupplierTab.tsx | 381 +++++++----------- src/types/api/report/logistic-stock.d.ts | 2 + 2 files changed, 154 insertions(+), 229 deletions(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 90018e98..81913748 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -15,7 +15,10 @@ import { LogisticApi } from '@/services/api/report/logistic-stock'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; 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 { useTableFilter } from '@/services/hooks/useTableFilter'; 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 toast from 'react-hot-toast'; 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 = () => { // ===== STATE MANAGEMENT ===== @@ -279,11 +266,11 @@ const PurchasesPerSupplierTab = () => { ) ); - const data: LogisticPurchasePerSupplierReport['rows'] = useMemo( + const data: LogisticPurchasePerSupplierReport[] = useMemo( () => isResponseSuccess(purchasePerSupplier) - ? (purchasePerSupplier?.data - ?.rows as LogisticPurchasePerSupplierReport['rows']) || [] + ? (purchasePerSupplier?.data as unknown as LogisticPurchasePerSupplierReport[]) || + [] : [], [purchasePerSupplier] ); @@ -294,58 +281,61 @@ const PurchasesPerSupplierTab = () => { : null; // ===== EXPORT DATA FETCHER ===== - const logisticPurchasePerSupplierExport = - useCallback(async (): Promise => { - const params = { - area_id: - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id.join(',') - : undefined, - supplier_id: - tableFilterState.supplier_id.length > 0 - ? tableFilterState.supplier_id.join(',') - : undefined, - product_id: - tableFilterState.product_id.length > 0 - ? tableFilterState.product_id.join(',') - : undefined, - product_category_id: - tableFilterState.product_category_id.length > 0 - ? tableFilterState.product_category_id.join(',') - : undefined, - received_date: - tableFilterState.filter_by === 'received_date' - ? tableFilterState.start_date || undefined - : undefined, - po_date: - tableFilterState.filter_by === 'po_date' - ? tableFilterState.start_date || undefined - : undefined, - start_date: tableFilterState.start_date || undefined, - end_date: tableFilterState.end_date || undefined, - sort_by: tableFilterState.sort_by || undefined, - filter_by: tableFilterState.filter_by || undefined, - limit: 10000, - page: 1, - }; + const logisticPurchasePerSupplierExport = useCallback(async (): Promise< + LogisticPurchasePerSupplierReport[] | null + > => { + const params = { + area_id: + tableFilterState.area_id.length > 0 + ? tableFilterState.area_id.join(',') + : undefined, + supplier_id: + tableFilterState.supplier_id.length > 0 + ? tableFilterState.supplier_id.join(',') + : undefined, + product_id: + tableFilterState.product_id.length > 0 + ? tableFilterState.product_id.join(',') + : undefined, + product_category_id: + tableFilterState.product_category_id.length > 0 + ? tableFilterState.product_category_id.join(',') + : undefined, + received_date: + tableFilterState.filter_by === 'received_date' + ? tableFilterState.start_date || undefined + : undefined, + po_date: + tableFilterState.filter_by === 'po_date' + ? tableFilterState.start_date || undefined + : undefined, + start_date: tableFilterState.start_date || undefined, + end_date: tableFilterState.end_date || undefined, + sort_by: tableFilterState.sort_by || undefined, + filter_by: tableFilterState.filter_by || undefined, + limit: 10000, + page: 1, + }; - const response = await LogisticApi.getLogisticPurchasePerSupplierReport( - params.area_id, - params.supplier_id, - params.product_id, - params.product_category_id, - params.received_date, - params.po_date, - params.start_date, - params.end_date, - params.sort_by, - params.filter_by, - params.page, - params.limit - ); + const response = await LogisticApi.getLogisticPurchasePerSupplierReport( + params.area_id, + params.supplier_id, + params.product_id, + params.product_category_id, + params.received_date, + params.po_date, + params.start_date, + params.end_date, + params.sort_by, + params.filter_by, + params.page, + params.limit + ); - return isResponseSuccess(response) ? response.data : null; - }, [tableFilterState]); + return isResponseSuccess(response) + ? (response.data as unknown as LogisticPurchasePerSupplierReport[]) + : null; + }, [tableFilterState]); // ===== EXPORT HANDLERS ===== const handleExportExcel = useCallback(async () => { @@ -355,73 +345,43 @@ const PurchasesPerSupplierTab = () => { if ( !allDataForExport || - !allDataForExport?.rows || - allDataForExport.rows.length === 0 + !Array.isArray(allDataForExport) || + allDataForExport.length === 0 ) { toast.error('Tidak ada data untuk diekspor.'); 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(); - Object.entries(groupedBySupplier).forEach( - ([supplierName, supplierData]) => { - const totals = supplierData.reduce( - (acc, item) => ({ - 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, - } - ); + allDataForExport.forEach((supplierReport) => { + const supplierData = supplierReport.rows; + const supplierName = + supplierReport.supplier?.name || 'Unknown Supplier'; - const excelData: { [key: string]: string | number }[] = - supplierData.map((item, index) => ({ - No: index + 1, - 'Tanggal Terima': item.receive_date - ? formatDate(item.receive_date, 'DD MMM YYYY') - : '', - 'Tanggal PO': item.po_date - ? formatDate(item.po_date, 'DD MMM YYYY') - : '', - 'No. Referensi': item.po_number || '', - 'Nama Produk': item.product?.name || '', - Tujuan: item.warehouse?.name || '', - QTY: item.qty || 0, - 'Harga Beli (Rp)': item.unit_price || 0, - 'Value Harga Beli (Rp)': item.purchase_value || 0, - 'Transport (Rp)': item.transport_unit_price || 0, - 'Value Transport (Rp)': item.transport_value || 0, - 'Jumlah (Rp)': item.total_amount || 0, - Ekspedisi: item.expedition || '', - 'Surat Jalan': item.delivery_number || '', - })); + const excelData: { [key: string]: string | number }[] = + supplierData.map((item, index) => ({ + No: index + 1, + 'Tanggal Terima': item.receive_date + ? formatDate(item.receive_date, 'DD MMM YYYY') + : '', + 'Tanggal PO': item.po_date + ? formatDate(item.po_date, 'DD MMM YYYY') + : '', + 'No. Referensi': item.po_number || '', + 'Nama Produk': item.product?.name || '', + Tujuan: item.warehouse?.name || '', + QTY: item.qty || 0, + 'Harga Beli (Rp)': item.unit_price || 0, + 'Value Harga Beli (Rp)': item.purchase_value || 0, + 'Transport (Rp)': item.transport_unit_price || 0, + 'Value Transport (Rp)': item.transport_value || 0, + 'Jumlah (Rp)': item.total_amount || 0, + Ekspedisi: item.expedition || '', + 'Surat Jalan': item.delivery_number || '', + })); + if (supplierReport.summary) { excelData.push({ No: 'Total', 'Tanggal Terima': '', @@ -429,43 +389,45 @@ const PurchasesPerSupplierTab = () => { 'No. Referensi': '', 'Nama Produk': '', Tujuan: '', - QTY: totals.total_qty, - 'Harga Beli (Rp)': totals.total_price, - 'Value Harga Beli (Rp)': totals.total_purchase_amount, - 'Transport (Rp)': totals.total_transport, - 'Value Transport (Rp)': totals.total_value_transport, - 'Jumlah (Rp)': totals.total_jumlah, + QTY: supplierReport.summary.total_qty || 0, + 'Harga Beli (Rp)': '', + 'Value Harga Beli (Rp)': + supplierReport.summary.total_purchase_value || 0, + 'Transport (Rp)': '', + 'Value Transport (Rp)': + supplierReport.summary.total_transport_value || 0, + 'Jumlah (Rp)': supplierReport.summary.total_amount || 0, Ekspedisi: '', 'Surat Jalan': '', }); - - const worksheet = XLSX.utils.json_to_sheet(excelData); - - const colWidths = [ - { wch: 5 }, // No - { wch: 15 }, // Tanggal Terima - { wch: 15 }, // Tanggal PO - { wch: 15 }, // No. Referensi - { wch: 30 }, // Nama Produk - { wch: 20 }, // Tujuan - { wch: 10 }, // QTY - { wch: 18 }, // Harga Beli - { wch: 20 }, // Value Harga Beli - { wch: 15 }, // Transport - { wch: 20 }, // Value Transport - { wch: 18 }, // Jumlah - { wch: 15 }, // Ekspedisi - { wch: 15 }, // Surat Jalan - ]; - worksheet['!cols'] = colWidths; - - const sheetName = - supplierName.length > 31 - ? supplierName.substring(0, 31) - : supplierName; - XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); } - ); + + const worksheet = XLSX.utils.json_to_sheet(excelData); + + const colWidths = [ + { wch: 5 }, // No + { wch: 15 }, // Tanggal Terima + { wch: 15 }, // Tanggal PO + { wch: 15 }, // No. Referensi + { wch: 30 }, // Nama Produk + { wch: 20 }, // Tujuan + { wch: 10 }, // QTY + { wch: 18 }, // Harga Beli + { wch: 20 }, // Value Harga Beli + { wch: 15 }, // Transport + { wch: 20 }, // Value Transport + { wch: 18 }, // Jumlah + { wch: 15 }, // Ekspedisi + { wch: 15 }, // Surat Jalan + ]; + worksheet['!cols'] = colWidths; + + const sheetName = + supplierName.length > 31 + ? supplierName.substring(0, 31) + : supplierName; + XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); + }); const filename = `laporan-pembelian-per-supplier-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; @@ -490,12 +452,17 @@ const PurchasesPerSupplierTab = () => { if ( !allDataForExport || - !allDataForExport?.rows || - allDataForExport.rows.length === 0 + !Array.isArray(allDataForExport) || + allDataForExport.length === 0 ) { toast.error('Tidak ada data untuk diekspor.'); return; } + + const allRows = allDataForExport.flatMap( + (supplierReport) => supplierReport.rows + ); + const areaName = tableFilterState.area_id.length > 0 ? tableFilterState.area_id @@ -551,10 +518,7 @@ const PurchasesPerSupplierTab = () => { end_date: tableFilterState.end_date || '', }; - await generatePurchasesPerSupplierPDF( - allDataForExport.rows, - exportParams - ); + await generatePurchasesPerSupplierPDF(allRows, exportParams); toast.success('PDF berhasil dibuat dan diunduh.'); } catch { 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 = ( - totals: Totals + summary: LogisticPurchasePerSupplierSummary ): ColumnDef[] => { const tableColumns: ColumnDef< LogisticPurchasePerSupplierReport['rows'][0] @@ -679,7 +623,7 @@ const PurchasesPerSupplierTab = () => { }, footer: () => (
- {formatNumber(totals.total_qty)} + {formatNumber(summary.total_qty) || '-'}
), }, @@ -693,7 +637,7 @@ const PurchasesPerSupplierTab = () => { }, footer: () => (
- {formatCurrency(totals.total_price)} + {formatCurrency(summary.total_unit_price) || '-'}
), }, @@ -707,7 +651,7 @@ const PurchasesPerSupplierTab = () => { }, footer: () => (
- {formatCurrency(totals.total_purchase_amount)} + {formatCurrency(summary.total_purchase_value) || '-'}
), }, @@ -721,7 +665,7 @@ const PurchasesPerSupplierTab = () => { }, footer: () => (
- {formatCurrency(totals.total_transport)} + {formatCurrency(summary.total_transport_unit_price) || '-'}
), }, @@ -735,7 +679,7 @@ const PurchasesPerSupplierTab = () => { }, footer: () => (
- {formatCurrency(totals.total_value_transport)} + {formatCurrency(summary.total_transport_value) || '-'}
), }, @@ -749,7 +693,7 @@ const PurchasesPerSupplierTab = () => { }, footer: () => (
- {formatCurrency(totals.total_jumlah)} + {formatCurrency(summary.total_amount) || '-'}
), }, @@ -920,54 +864,33 @@ const PurchasesPerSupplierTab = () => { Tidak ada data yang dapat ditampilkan... ) : ( - groupedData.map((supplier) => { - const total_qty = supplier.items.reduce( - (sum, item) => sum + (item.qty || 0), - 0 - ); - const total_price = supplier.items.reduce( - (sum, item) => sum + (item.purchase_value || 0), - 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, + data.map((supplierReport) => { + const summary = supplierReport.summary || { + total_qty: 0, + total_unit_price: 0, + total_purchase_value: 0, + total_transport_unit_price: 0, + total_transport_value: 0, + total_amount: 0, }; - const totalPurchase = totals.total_jumlah; - const tableColumns = getTableColumns(totals); + const totalPurchase = summary.total_amount; + const tableColumns = getTableColumns(summary); return ( 0} + renderFooter={supplierReport.rows.length > 0} className={{ containerClassName: 'w-full', tableWrapperClassName: 'overflow-x-auto mt-4', diff --git a/src/types/api/report/logistic-stock.d.ts b/src/types/api/report/logistic-stock.d.ts index 0d5e60c3..e5f0f2c6 100644 --- a/src/types/api/report/logistic-stock.d.ts +++ b/src/types/api/report/logistic-stock.d.ts @@ -21,7 +21,9 @@ export type LogisticPurchasePerSupplierReportRow = { export type LogisticPurchasePerSupplierSummary = { total_qty: number; + total_unit_price: number; total_purchase_value: number; + total_transport_unit_price: number; total_transport_value: number; total_amount: number; };