import Button from '@/components/Button'; import Card from '@/components/Card'; import Dropdown from '@/components/Dropdown'; import DateInput from '@/components/input/DateInput'; import { useSelect } from '@/components/input/SelectInput'; import Modal, { useModal } from '@/components/Modal'; import Table from '@/components/Table'; import { isResponseSuccess } from '@/lib/api-helper'; import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; 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 { LogisticPurchasePerSupplierReport, LogisticPurchasePerSupplierSummary, } from '@/types/api/report/logistic-stock'; import { generatePurchasesPerSupplierExcel } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX'; import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF'; import { Icon } from '@iconify/react'; import { ColumnDef } from '@tanstack/react-table'; import { useCallback, useEffect, useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import useSWR from 'swr'; import { useFormik } from 'formik'; import { PurchasesPerSupplierFilterSchema, PurchasesPerSupplierFilterType, } from '@/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton'; import ButtonFilter from '@/components/helper/ButtonFilter'; interface PurchasesPerSupplierTabProps { tabId: string; } interface FilterParams { area_id?: string; supplier_id?: string; product_id?: string; product_category_id?: string; start_date?: string; end_date?: string; sort_by?: string; filter_by?: string; } const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { // ===== 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] = useState(10); // ===== SUBMISSION STATE ===== const [filterParams, setFilterParams] = useState({}); const [isSubmitted, setIsSubmitted] = useState(false); const [dateErrorShown, setDateErrorShown] = useState(false); const [hasDateError, setHasDateError] = useState(false); const filterModal = useModal(); // ===== OPTIONS ===== const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( AreaApi.basePath, 'id', 'name', 'search' ); const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } = useSelect(SupplierApi.basePath, 'id', 'name', 'search', { category: 'SAPRONAK', }); 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 handleFilterModalOpen = () => { filterModal.openModal(); formik.validateForm(); }; // ===== FORMIK SETUP ===== const formik = useFormik({ initialValues: { start_date: null, end_date: null, area_ids: null, supplier_ids: null, product_ids: null, product_category_ids: null, filter_by: null, sort_by: null, }, validationSchema: PurchasesPerSupplierFilterSchema, onSubmit: (values, { setSubmitting }) => { setFilterParams({ start_date: values.start_date || undefined, end_date: values.end_date || undefined, area_id: values.area_ids || undefined, supplier_id: values.supplier_ids || undefined, product_id: values.product_ids || undefined, product_category_id: values.product_category_ids || undefined, filter_by: values.filter_by || undefined, sort_by: values.sort_by || undefined, }); filterModal.closeModal(); setIsSubmitted(true); setCurrentPage(1); setSubmitting(false); }, onReset: () => { setFilterParams({}); setIsSubmitted(false); setCurrentPage(1); setHasDateError(false); if (dateErrorShown) { toast.dismiss(); setDateErrorShown(false); } }, }); // ===== DATE CHANGE HANDLERS ===== const handleStartDateChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; formik.setFieldValue('start_date', value || null); if (value && formik.values.end_date) { const startDate = new Date(value); const endDateObj = new Date(formik.values.end_date); if (endDateObj < startDate) { setHasDateError(true); if (!dateErrorShown) { toast.error('Tanggal akhir tidak boleh masa lampau', { duration: Infinity, }); setDateErrorShown(true); } } else { setHasDateError(false); if (dateErrorShown) { toast.dismiss(); setDateErrorShown(false); } } } else { setHasDateError(false); } }, [formik, dateErrorShown] ); const handleEndDateChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; formik.setFieldValue('end_date', value || null); if (value && formik.values.start_date) { const startDateObj = new Date(formik.values.start_date); const endDate = new Date(value); if (endDate < startDateObj) { setHasDateError(true); if (!dateErrorShown) { toast.error('Tanggal akhir tidak boleh masa lampau', { duration: Infinity, }); setDateErrorShown(true); } return; } } setHasDateError(false); if (dateErrorShown) { toast.dismiss(); setDateErrorShown(false); } }, [formik, dateErrorShown] ); // ===== DERIVED VALUES ===== const areaIdsValue = useMemo(() => { if (!formik.values.area_ids) return []; const ids = formik.values.area_ids.split(','); return areaOptions.filter((opt) => ids.includes(String(opt.value))); }, [formik.values.area_ids, areaOptions]); const supplierIdsValue = useMemo(() => { if (!formik.values.supplier_ids) return []; const ids = formik.values.supplier_ids.split(','); return supplierOptions.filter((opt) => ids.includes(String(opt.value))); }, [formik.values.supplier_ids, supplierOptions]); const productIdsValue = useMemo(() => { if (!formik.values.product_ids) return []; const ids = formik.values.product_ids.split(','); return productOptions.filter((opt) => ids.includes(String(opt.value))); }, [formik.values.product_ids, productOptions]); const productCategoryIdsValue = useMemo(() => { if (!formik.values.product_category_ids) return []; const ids = formik.values.product_category_ids.split(','); return productCategoryOptions.filter((opt) => ids.includes(String(opt.value)) ); }, [formik.values.product_category_ids, productCategoryOptions]); const filterByValue = useMemo(() => { if (!formik.values.filter_by) return null; return ( dataTypeOptions.find((opt) => opt.value === formik.values.filter_by) || null ); }, [formik.values.filter_by, dataTypeOptions]); const sortByValue = useMemo(() => { if (!formik.values.sort_by) return null; return ( sortByOptions.find((opt) => opt.value === formik.values.sort_by) || null ); }, [formik.values.sort_by, sortByOptions]); // ===== DATA FETCHING ===== const { data: purchasePerSupplier, isLoading } = useSWR( isSubmitted ? () => { const params = { area_id: filterParams.area_id, supplier_id: filterParams.supplier_id, product_id: filterParams.product_id, product_category_id: filterParams.product_category_id, start_date: filterParams.start_date, end_date: filterParams.end_date, sort_by: filterParams.sort_by, filter_by: filterParams.filter_by, 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.filter_by === 'received_date' ? params.start_date : undefined, params.filter_by === 'po_date' ? params.start_date : undefined, params.start_date, params.end_date, params.sort_by, params.filter_by, params.page, params.limit ) ); const data: LogisticPurchasePerSupplierReport[] = useMemo( () => isResponseSuccess(purchasePerSupplier) ? (purchasePerSupplier?.data as unknown as LogisticPurchasePerSupplierReport[]) || [] : [], [purchasePerSupplier] ); // ===== EXPORT DATA FETCHER ===== const logisticPurchasePerSupplierExport = useCallback(async (): Promise< LogisticPurchasePerSupplierReport[] | null > => { const params = { area_id: filterParams.area_id, supplier_id: filterParams.supplier_id, product_id: filterParams.product_id, product_category_id: filterParams.product_category_id, start_date: filterParams.start_date, end_date: filterParams.end_date, sort_by: filterParams.sort_by, filter_by: filterParams.filter_by, limit: 100, page: 1, }; const response = await LogisticApi.getLogisticPurchasePerSupplierReport( params.area_id, params.supplier_id, params.product_id, params.product_category_id, params.filter_by === 'received_date' ? params.start_date : undefined, params.filter_by === 'po_date' ? params.start_date : undefined, params.start_date, params.end_date, params.sort_by, params.filter_by, params.page, params.limit ); return isResponseSuccess(response) ? (response.data as unknown as LogisticPurchasePerSupplierReport[]) : null; }, [filterParams]); // ===== EXPORT HANDLERS ===== const handleExportExcel = useCallback(async () => { setIsExcelExportLoading(true); try { const allDataForExport = await logisticPurchasePerSupplierExport(); if ( !allDataForExport || !Array.isArray(allDataForExport) || allDataForExport.length === 0 ) { toast.error('Tidak ada data untuk diekspor.'); return; } await generatePurchasesPerSupplierExcel({ data: allDataForExport }); toast.success('Excel berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat Excel. Silakan coba lagi.'); } finally { setIsExcelExportLoading(false); } }, [logisticPurchasePerSupplierExport]); const handleExportPdf = useCallback(async () => { setIsPdfExportLoading(true); try { const allDataForExport = await logisticPurchasePerSupplierExport(); if ( !allDataForExport || !Array.isArray(allDataForExport) || allDataForExport.length === 0 ) { toast.error('Tidak ada data untuk diekspor.'); return; } const areaName = filterParams.area_id ? areaOptions .filter((opt) => filterParams.area_id?.split(',').includes(String(opt.value)) ) .map((opt) => opt.label) .join(', ') || 'Semua Area' : 'Semua Area'; const supplierName = filterParams.supplier_id ? supplierOptions .filter((opt) => filterParams.supplier_id?.split(',').includes(String(opt.value)) ) .map((opt) => opt.label) .join(', ') || 'Semua Supplier' : 'Semua Supplier'; const productName = filterParams.product_id ? productOptions .filter((opt) => filterParams.product_id?.split(',').includes(String(opt.value)) ) .map((opt) => opt.label) .join(', ') || 'Semua Produk' : 'Semua Produk'; const productCategoryName = filterParams.product_category_id ? productCategoryOptions .filter((opt) => filterParams.product_category_id ?.split(',') .includes(String(opt.value)) ) .map((opt) => opt.label) .join(', ') || 'Semua Kategori Produk' : 'Semua Kategori Produk'; const exportParams = { area_name: areaName, supplier_name: supplierName, product_name: productName, product_category_name: productCategoryName, filter_by: filterParams.filter_by, start_date: filterParams.start_date, end_date: filterParams.end_date, }; await generatePurchasesPerSupplierPDF({ data: allDataForExport, params: exportParams, }); toast.success('PDF berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat PDF. Silakan coba lagi.'); } finally { setIsPdfExportLoading(false); } }, [ logisticPurchasePerSupplierExport, filterParams, areaOptions, supplierOptions, productOptions, productCategoryOptions, ]); // ===== REGISTER TAB ACTIONS TO STORE ===== const setTabActions = useTabActionsStore((state) => state.setTabActions); const clearTabActions = useTabActionsStore((state) => state.clearTabActions); useEffect(() => { setTabActions( tabId,
Export
} >
); }, [ tabId, formik.values, isAnyExportLoading, filterModal.open, setTabActions, ]); useEffect(() => { return () => { clearTabActions(tabId); }; }, [tabId, clearTabActions]); const getTableColumns = ( summary: LogisticPurchasePerSupplierSummary ): 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', enableSorting: false, cell: (props) => { const value = props.row.original.receive_date; return formatDate(value, 'DD MMM YYYY'); }, }, { id: 'po_date', header: 'Tanggal PO', accessorKey: 'po_date', enableSorting: false, cell: (props) => { const value = props.row.original.po_date; return formatDate(value, 'DD MMM YYYY'); }, }, { id: 'po_number', header: 'No. Referensi', accessorKey: 'po_number', enableSorting: false, cell: (props) => { const value = props.row.original.po_number; return value || '-'; }, }, { id: 'product_name', header: 'Nama Produk', accessorKey: 'product.name', enableSorting: false, cell: (props) => { const product = props.row.original.product; return product?.name || '-'; }, }, { id: 'destination_warehouse', header: 'Tujuan', accessorKey: 'warehouse.name', enableSorting: false, cell: (props) => { const warehouse = props.row.original.warehouse; return warehouse?.name || '-'; }, }, { id: 'qty', header: 'QTY', accessorKey: 'qty', enableSorting: false, cell: (props) => { const value = props.row.original.qty; return
{formatNumber(value)}
; }, footer: () => (
{formatNumber(summary.total_qty) || '-'}
), }, { id: 'price', header: 'Harga Beli (Rp)', accessorKey: 'unit_price', enableSorting: false, cell: (props) => { const value = props.row.original.unit_price; return
{formatCurrency(value)}
; }, footer: () => (
{formatCurrency(summary.total_unit_price) || '-'}
), }, { id: 'purchase_amount', header: 'Value Harga Beli (Rp)', accessorKey: 'purchase_value', enableSorting: false, cell: (props) => { const value = props.row.original.purchase_value; return
{formatCurrency(value)}
; }, footer: () => (
{formatCurrency(summary.total_purchase_value) || '-'}
), }, { id: 'transport', header: 'Transport (Rp)', accessorKey: 'transport_unit_price', enableSorting: false, cell: (props) => { const value = props.row.original.transport_unit_price; return
{formatCurrency(value)}
; }, footer: () => (
{formatCurrency(summary.total_transport_unit_price) || '-'}
), }, { id: 'value_transport', header: 'Value Transport (Rp)', accessorKey: 'transport_value', enableSorting: false, cell: (props) => { const value = props.row.original.transport_value; return
{formatCurrency(value)}
; }, footer: () => (
{formatCurrency(summary.total_transport_value) || '-'}
), }, { id: 'total', header: 'Jumlah (Rp)', accessorKey: 'total_amount', enableSorting: false, cell: (props) => { const value = props.row.original.total_amount; return
{formatCurrency(value)}
; }, footer: () => (
{formatCurrency(summary.total_amount) || '-'}
), }, { id: 'expedition_vendor_name', header: 'Ekspedisi', accessorKey: 'expedition', enableSorting: false, cell: (props) => { const value = props.row.original.expedition; return value || '-'; }, }, { id: 'travel_number', header: 'Surat Jalan', accessorKey: 'delivery_number', enableSorting: false, cell: (props) => { const value = props.row.original.delivery_number; return value || '-'; }, }, ]; return tableColumns; }; return ( <>
{!isSubmitted ? ( } title='No Filters Selected' subtitle='Please choose filters to narrow down your results and make your search easier.' /> ) : isLoading ? ( } title='Memuat Data Pembelian Per Supplier' subtitle='Silakan tunggu sebentar...' /> ) : data.length === 0 ? ( } title='Data Not Yet Available' subtitle='Please change your filters to get the data.' /> ) : ( 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 = summary.total_amount; const tableColumns = getTableColumns(summary); return ( 0} className={{ containerClassName: 'w-full mb-0!', tableWrapperClassName: 'overflow-x-auto rounded-tr-none rounded-tl-none', 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', }} /> ); }) )} {/* Filter Modal */} {/* Modal Header */}

Filter Data

{/* Date Filter */}

{/* Area Filter */} { formik.setFieldValue( 'area_ids', Array.isArray(val) && val.length > 0 ? val.map((v) => String(v.value)).join(',') : null ); }} isLoading={isLoadingAreas} isClearable className={{ wrapper: 'w-full' }} /> {/* Supplier Filter */} { formik.setFieldValue( 'supplier_ids', Array.isArray(val) && val.length > 0 ? val.map((v) => String(v.value)).join(',') : null ); }} isLoading={isLoadingSuppliers} isClearable className={{ wrapper: 'w-full' }} /> {/* Product Filter */} { formik.setFieldValue( 'product_ids', Array.isArray(val) && val.length > 0 ? val.map((v) => String(v.value)).join(',') : null ); }} isLoading={isLoadingProducts} isClearable className={{ wrapper: 'w-full' }} /> {/* Product Category Filter */} { formik.setFieldValue( 'product_category_ids', Array.isArray(val) && val.length > 0 ? val.map((v) => String(v.value)).join(',') : null ); }} isLoading={isLoadingProductCategories} isClearable className={{ wrapper: 'w-full' }} /> {/* Filter By Type */} { if (!Array.isArray(val)) { formik.setFieldValue( 'filter_by', val?.value?.toString() || null ); } }} className={{ wrapper: 'w-full' }} isClearable={true} /> {/* Sort By */} { if (!Array.isArray(val)) { formik.setFieldValue( 'sort_by', val?.value?.toString() || null ); } }} className={{ wrapper: 'w-full' }} isClearable={true} />
{/* Modal Footer */}
); }; export default PurchasesPerSupplierTab;