import { useState, useMemo, useCallback } from 'react'; import { ChangeEventHandler } from 'react'; import useSWR from 'swr'; import Card from '@/components/Card'; import SelectInput, { useSelect, OptionType, } from '@/components/input/SelectInput'; import DateInput from '@/components/input/DateInput'; import { AreaApi } from '@/services/api/master-data'; import { SupplierApi } from '@/services/api/master-data'; import { ProductApi } from '@/services/api/master-data'; import { ProductCategoryApi } from '@/services/api/master-data'; 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 { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import Pagination from '@/components/Pagination'; import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; import MenuItem from '@/components/menu/MenuItem'; 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 ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; // ===== PAGINATION STATE ===== const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(10); // ===== SUBMISSION STATE ===== const [isSubmitted, setIsSubmitted] = useState(false); // ===== TABLE FILTER STATE ===== const { state: tableFilterState, updateFilter } = useTableFilter({ initial: { area_id: [] as string[], supplier_id: [] as string[], product_id: [] as string[], product_category_id: [] as string[], received_date: '', po_date: '', start_date: '', end_date: '', sort_by: '', filter_by: 'received_date', }, paramMap: { page: 'page', pageSize: 'limit', }, }); const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( AreaApi.basePath, 'id', 'name', 'search' ); const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } = useSelect(SupplierApi.basePath, 'id', 'name', 'search'); const { options: productOptions, isLoadingOptions: isLoadingProducts } = useSelect(ProductApi.basePath, 'id', 'name', 'search'); const { options: productCategoryOptions, isLoadingOptions: isLoadingProductCategories, } = useSelect(ProductCategoryApi.basePath, 'id', 'name', 'search'); const dataTypeOptions = useMemo( () => [ { value: 'received_date', label: 'Tanggal Terima' }, { value: 'po_date', label: 'Tanggal PO' }, ], [] ); const sortByOptions = useMemo( () => [ { value: 'ASC', label: 'Ascending' }, { value: 'DESC', label: 'Descending' }, ], [] ); const areaChangeHandler = useCallback( (val: OptionType | OptionType[] | null) => { const arr = Array.isArray(val) ? val : val ? [val] : []; updateFilter( 'area_id', arr.map((v) => String((v as OptionType).value)) ); setIsSubmitted(false); }, [updateFilter] ); const supplierChangeHandler = useCallback( (val: OptionType | OptionType[] | null) => { const arr = Array.isArray(val) ? val : val ? [val] : []; updateFilter( 'supplier_id', arr.map((v) => String((v as OptionType).value)) ); setIsSubmitted(false); }, [updateFilter] ); const productChangeHandler = useCallback( (val: OptionType | OptionType[] | null) => { const arr = Array.isArray(val) ? val : val ? [val] : []; updateFilter( 'product_id', arr.map((v) => String((v as OptionType).value)) ); setIsSubmitted(false); }, [updateFilter] ); const productCategoryChangeHandler = useCallback( (val: OptionType | OptionType[] | null) => { const arr = Array.isArray(val) ? val : val ? [val] : []; updateFilter( 'product_category_id', arr.map((v) => String((v as OptionType).value)) ); setIsSubmitted(false); }, [updateFilter] ); const dataTypeChangeHandler = useCallback( (val: OptionType | OptionType[] | null) => { const newVal = val as OptionType; const filterValue = (newVal?.value as 'received_date' | 'po_date') || 'received_date'; updateFilter('filter_by', filterValue); updateFilter('received_date', ''); updateFilter('po_date', ''); setIsSubmitted(false); }, [updateFilter] ); const sortByHandler = useCallback( (val: OptionType | OptionType[] | null) => { const newVal = val as OptionType; const sortValue = (newVal?.value as 'ASC' | 'DESC') || 'ASC'; updateFilter('sort_by', sortValue); setIsSubmitted(false); }, [updateFilter] ); const startDateChangeHandler = useCallback< ChangeEventHandler >( (e) => { const val = e.target.value; updateFilter('start_date', val || ''); setIsSubmitted(false); }, [updateFilter] ); const endDateChangeHandler = useCallback< ChangeEventHandler >( (e) => { const val = e.target.value; updateFilter('end_date', val || ''); setIsSubmitted(false); }, [updateFilter] ); const resetFilters = useCallback(() => { updateFilter('area_id', []); updateFilter('supplier_id', []); updateFilter('product_id', []); updateFilter('product_category_id', []); updateFilter('received_date', ''); updateFilter('po_date', ''); updateFilter('start_date', ''); updateFilter('end_date', ''); updateFilter('sort_by', ''); updateFilter('filter_by', 'received_date'); setIsSubmitted(false); }, [updateFilter]); const handleSubmit = useCallback(() => { setIsSubmitted(true); setCurrentPage(1); }, []); // ===== DATA FETCHING ===== const { data: purchasePerSupplier, isLoading } = useSWR( isSubmitted ? () => { 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, page: currentPage, limit: pageSize, }; return ['logistic-purchase-report', params]; } : null, ([, params]) => 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 data: LogisticPurchasePerSupplierReport['rows'] = useMemo( () => isResponseSuccess(purchasePerSupplier) ? (purchasePerSupplier?.data ?.rows as LogisticPurchasePerSupplierReport['rows']) || [] : [], [purchasePerSupplier] ); const meta = isResponseSuccess(purchasePerSupplier) && purchasePerSupplier?.meta ? purchasePerSupplier.meta : 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 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]); // ===== EXPORT HANDLERS ===== const handleExportExcel = useCallback(async () => { setIsExcelExportLoading(true); try { const allDataForExport = await logisticPurchasePerSupplierExport(); if ( !allDataForExport || !allDataForExport?.rows || allDataForExport.rows.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, } ); 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 || '', })); excelData.push({ No: 'Total', 'Tanggal Terima': '', 'Tanggal PO': '', '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, 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 filename = `laporan-pembelian-per-supplier-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; XLSX.writeFile(workbook, filename); toast.success('Excel berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat Excel. Silakan coba lagi.'); } finally { setIsExcelExportLoading(false); } }, [ logisticPurchasePerSupplierExport, tableFilterState, areaOptions, supplierOptions, ]); const handleExportPdf = useCallback(async () => { setIsPdfExportLoading(true); try { const allDataForExport = await logisticPurchasePerSupplierExport(); if ( !allDataForExport || !allDataForExport?.rows || allDataForExport.rows.length === 0 ) { toast.error('Tidak ada data untuk diekspor.'); return; } const areaName = tableFilterState.area_id.length > 0 ? tableFilterState.area_id .map( (id) => areaOptions.find((opt) => opt.value === Number(id))?.label ) .filter(Boolean) .join(', ') || 'Semua Area' : 'Semua Area'; const supplierName = tableFilterState.supplier_id.length > 0 ? tableFilterState.supplier_id .map( (id) => supplierOptions.find((opt) => opt.value === Number(id))?.label ) .filter(Boolean) .join(', ') || 'Semua Supplier' : 'Semua Supplier'; const productName = tableFilterState.product_id.length > 0 ? tableFilterState.product_id .map( (id) => productOptions.find((opt) => opt.value === Number(id))?.label ) .filter(Boolean) .join(', ') || 'Semua Produk' : 'Semua Produk'; const productCategoryName = tableFilterState.product_category_id.length > 0 ? tableFilterState.product_category_id .map( (id) => productCategoryOptions.find((opt) => opt.value === Number(id)) ?.label ) .filter(Boolean) .join(', ') || 'Semua Kategori Produk' : 'Semua Kategori Produk'; const exportParams = { area_name: areaName, supplier_name: supplierName, product_name: productName, product_category_name: productCategoryName, filter_by: tableFilterState.filter_by || 'received_date', start_date: tableFilterState.start_date || '', end_date: tableFilterState.end_date || '', }; await generatePurchasesPerSupplierPDF( allDataForExport.rows, exportParams ); toast.success('PDF berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat PDF. Silakan coba lagi.'); } finally { setIsPdfExportLoading(false); } }, [ logisticPurchasePerSupplierExport, tableFilterState, areaOptions, supplierOptions, productOptions, productCategoryOptions, ]); // ===== PAGINATION HANDLERS ===== const handlePageChange = (page: number) => { setCurrentPage(page); }; const handleRowChange = (pageSize: number) => { setPageSize(pageSize); }; const handleNextPage = () => { if (meta && currentPage < meta.total_pages) { setCurrentPage(currentPage + 1); } }; const handlePrevPage = () => { if (currentPage > 1) { setCurrentPage(currentPage - 1); } }; // ===== 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 ): ColumnDef[] => { const tableColumns: ColumnDef< LogisticPurchasePerSupplierReport['rows'][0] >[] = [ { id: 'no', header: 'No', cell: (props) => props.row.index + 1, footer: () =>
Total
, }, { id: 'received_date', header: 'Tanggal Terima', accessorKey: 'receive_date', cell: (props) => { const value = props.row.original.receive_date; return formatDate(value, 'DD MMM YYYY'); }, }, { id: 'po_date', header: 'Tanggal PO', accessorKey: 'po_date', cell: (props) => { const value = props.row.original.po_date; return formatDate(value, 'DD MMM YYYY'); }, }, { id: 'po_number', header: 'No. Referensi', accessorKey: 'po_number', cell: (props) => { const value = props.row.original.po_number; return value || '-'; }, }, { id: 'product_name', header: 'Nama Produk', accessorKey: 'product.name', cell: (props) => { const product = props.row.original.product; return product?.name || '-'; }, }, { id: 'destination_warehouse', header: 'Tujuan', accessorKey: 'warehouse.name', cell: (props) => { const warehouse = props.row.original.warehouse; return warehouse?.name || '-'; }, }, { id: 'qty', header: 'QTY', accessorKey: 'qty', cell: (props) => { const value = props.row.original.qty; return
{formatNumber(value)}
; }, footer: () => (
{formatNumber(totals.total_qty)}
), }, { id: 'price', header: 'Harga Beli (Rp)', accessorKey: 'unit_price', cell: (props) => { const value = props.row.original.unit_price; return
{formatCurrency(value)}
; }, footer: () => (
{formatCurrency(totals.total_price)}
), }, { id: 'purchase_amount', header: 'Value Harga Beli (Rp)', accessorKey: 'purchase_value', cell: (props) => { const value = props.row.original.purchase_value; return
{formatCurrency(value)}
; }, footer: () => (
{formatCurrency(totals.total_purchase_amount)}
), }, { id: 'transport', header: 'Transport (Rp)', accessorKey: 'transport_unit_price', cell: (props) => { const value = props.row.original.transport_unit_price; return
{formatCurrency(value)}
; }, footer: () => (
{formatCurrency(totals.total_transport)}
), }, { id: 'value_transport', header: 'Value Transport (Rp)', accessorKey: 'transport_value', cell: (props) => { const value = props.row.original.transport_value; return
{formatCurrency(value)}
; }, footer: () => (
{formatCurrency(totals.total_value_transport)}
), }, { id: 'total', header: 'Jumlah (Rp)', accessorKey: 'total_amount', cell: (props) => { const value = props.row.original.total_amount; return
{formatCurrency(value)}
; }, footer: () => (
{formatCurrency(totals.total_jumlah)}
), }, { id: 'expedition_vendor_name', header: 'Ekspedisi', accessorKey: 'expedition', cell: (props) => { const value = props.row.original.expedition; return value || '-'; }, }, { id: 'travel_number', header: 'Surat Jalan', accessorKey: 'delivery_number', cell: (props) => { const value = props.row.original.delivery_number; return value || '-'; }, }, ]; return tableColumns; }; return (
Export } align='end' >
(tableFilterState.area_id || []) .map(String) .includes(String(opt.value)) )} onChange={areaChangeHandler} isLoading={isLoadingAreas} isClearable /> (tableFilterState.supplier_id || []) .map(String) .includes(String(opt.value)) )} onChange={supplierChangeHandler} isLoading={isLoadingSuppliers} isClearable /> (tableFilterState.product_id || []) .map(String) .includes(String(opt.value)) )} onChange={productChangeHandler} isLoading={isLoadingProducts} isClearable />
(tableFilterState.product_category_id || []) .map(String) .includes(String(opt.value)) )} onChange={productCategoryChangeHandler} isLoading={isLoadingProductCategories} isClearable />
option.value === tableFilterState.filter_by ) || null } onChange={dataTypeChangeHandler} isLoading={false} isClearable={false} /> option.value === tableFilterState.sort_by ) || null } onChange={sortByHandler} isLoading={false} isClearable={false} />
{!isSubmitted ? (
Silakan pilih filter dan klik tombol Submit untuk menampilkan data.
) : isLoading ? (
) : data.length === 0 ? (
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, }; const totalPurchase = totals.total_jumlah; const tableColumns = getTableColumns(totals); return ( 0} className={{ containerClassName: 'w-full', tableWrapperClassName: 'overflow-x-auto mt-4', tableClassName: 'w-full table-auto text-sm', headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', headerColumnClassName: 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', bodyRowClassName: 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', bodyColumnClassName: 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', tableFooterClassName: 'bg-gray-100 font-semibold border border-gray-200', footerRowClassName: 'border-t-2 border-gray-300', footerColumnClassName: 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', paginationClassName: 'hidden', }} /> ); }) )} {meta && data.length > 0 && (
)} ); }; export default PurchasesPerSupplierTab;