'use client'; import { ChangeEventHandler, useCallback, useMemo, useState } from 'react'; import useSWR from 'swr'; import { CellContext, ColumnDef, SortingState, Updater, } from '@tanstack/react-table'; import toast from 'react-hot-toast'; import Link from 'next/link'; import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import DateInput from '@/components/input/DateInput'; import Button from '@/components/Button'; import Modal, { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import PopoverButton from '@/components/popover/PopoverButton'; import PopoverContent from '@/components/popover/PopoverContent'; import RequirePermission from '@/components/helper/RequirePermission'; import StatusBadge from '@/components/helper/StatusBadge'; import PurchaseTableSkeleton from '@/components/pages/purchase/skeleton/PurchaseTableSkeleton'; import ButtonFilter from '@/components/helper/ButtonFilter'; import PurchaseFilterModal from '@/components/pages/purchase/PurchaseFilterModal'; import Dropdown from '@/components/dropdown/Dropdown'; import { OptionType } from '@/components/input/SelectInput'; import { cn, formatDate } from '@/lib/helper'; import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { Purchase, PurchaseFilter } from '@/types/api/purchase/purchase'; import { PurchaseApi } from '@/services/api/purchase'; import { Color } from '@/types/theme'; import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line'; type PurchaseTableFilters = { search: string; sort_by: string; order_by: string; po_date: string; approval_status: string; product_category_id: string; product_category_name: string; supplier_id: string; supplier_name: string; area_id: string; area_name: string; location_id: string; location_name: string; project_flock_id: string; project_flock_name: string; project_flock_kandang_id: string; project_flock_kandang_name: string; }; // ===== STATUS BADGE UTILITIES ===== const statusTextMap: Record = { APPROVED: 'Disetujui', Disetujui: 'Disetujui', REJECTED: 'Ditolak', Ditolak: 'Ditolak', CREATED: 'Dibuat', UPDATED: 'Diperbarui', }; const getStatusText = (status: string): string => { return statusTextMap[status] || status; }; const statusBadgeColorMap: Record = { APPROVED: 'success', Disetujui: 'success', approved: 'success', disetujui: 'success', REJECTED: 'error', Ditolak: 'error', rejected: 'error', ditolak: 'error', CREATED: 'neutral', Dibuat: 'neutral', created: 'neutral', dibuat: 'neutral', UPDATED: 'warning', Diperbarui: 'warning', updated: 'warning', diperbarui: 'warning', }; const getStatusBadgeColor = (status: string): Color => { return statusBadgeColorMap[status] || 'neutral'; }; // ===== ROW OPTIONS MENU ===== const RowOptionsMenu = ({ popoverPosition = 'bottom', props, deleteClickHandler, }: { popoverPosition: 'bottom' | 'top'; props: CellContext; deleteClickHandler: () => void; }) => { const popoverId = `purchase#${props.row.original.id}`; const popoverAnchorName = `--anchor-purchase#${props.row.original.id}`; const closePopover = () => { document.getElementById(popoverId)?.hidePopover(); }; return (
); }; const PurchaseTable = () => { // ===== TABLE FILTER STATE ===== const { state: tableFilterState, setFilters, updateFilter, setPage, setPageSize, toQueryString: getTableFilterQueryString, } = useTableFilter({ initial: { search: '', sort_by: '', order_by: '', po_date: '', approval_status: '', product_category_id: '', product_category_name: '', supplier_id: '', supplier_name: '', area_id: '', area_name: '', location_id: '', location_name: '', project_flock_id: '', project_flock_name: '', project_flock_kandang_id: '', project_flock_kandang_name: '', }, paramMap: { page: 'page', pageSize: 'limit', sort_by: 'sort_by', order_by: 'sort_order', po_date: 'po_date', approval_status: 'approval_status', product_category_id: 'product_category_id', supplier_id: 'supplier_id', area_id: 'area_id', location_id: 'location_id', project_flock_id: 'project_flock_id', project_flock_kandang_id: 'project_flock_kandang_id', }, excludeKeysFromUrl: [ 'product_category_name', 'supplier_name', 'area_name', 'location_name', 'project_flock_name', 'project_flock_kandang_name', ], persist: true, storeName: 'purchase-table', }); // ===== STATE MANAGEMENT ===== const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = useState(false); const [isExportProgressLoading, setIsExportProgressLoading] = useState(false); const [selectedPurchase, setSelectedPurchase] = useState( null ); const [exportProgressStartDate, setExportProgressStartDate] = useState(''); const [exportProgressEndDate, setExportProgressEndDate] = useState(''); const sorting: SortingState = tableFilterState.sort_by ? [ { id: tableFilterState.sort_by, desc: tableFilterState.order_by === 'desc', }, ] : []; const handleSortingChange = (updater: Updater) => { const next = typeof updater === 'function' ? updater(sorting) : updater; if (next.length > 0) { updateFilter('sort_by', next[0].id, true); updateFilter('order_by', next[0].desc ? 'desc' : 'asc', true); } else { updateFilter('sort_by', '', true); updateFilter('order_by', '', true); } }; // ===== MODAL HOOKS ===== const filterModal = useModal(); const deleteModal = useModal(); const exportProgressInputModal = useModal(); // ===== API DATA FETCHING ===== const { data: purchaseRequests, isLoading, mutate: refreshPurchaseRequests, } = useSWR( `${PurchaseApi.basePath}${getTableFilterQueryString()}`, PurchaseApi.getAllFetcher ); // ===== TABLE COLUMNS DEFINITION ===== const purchaseColumns: ColumnDef[] = [ { accessorKey: 'po_number', header: 'No. PR/PO', cell: (props) => { const { pr_number, po_number } = props.row.original; return po_number ? po_number : pr_number; }, }, { accessorKey: 'po_expedition', header: 'Ekspedisi PO', cell: (props) => { const poExpedition = props.row.original.po_expedition; if (!poExpedition || poExpedition.length === 0) return '-'; return (
    {poExpedition.map((exp, index) => { return (
  • {exp.refrence}
  • ); })}
); }, }, { accessorKey: 'supplier', header: 'Vendor', cell: (props) => props.row.original.supplier.name, }, { accessorKey: 'requester_name', header: 'Nama Pengaju', cell: (props) => props.row.original.requester_name || '-', }, { accessorKey: 'products', header: 'Produk', cell: (props) => { const products = props.row.original.products; if (!products || products.length === 0) return '-'; return (
    {products.map((product, index) => (
  • {product.name}
  • ))}
); }, }, { accessorKey: 'location', header: 'Lokasi', cell: (props) => props.row.original.location?.name || '-', }, { accessorKey: 'po_date', header: 'Tgl. PO', cell: (props) => props.row.original.po_date ? formatDate(props.row.original.po_date, 'DD MMM YYYY') : '-', }, { accessorKey: 'received_date', header: 'Tgl. Terima', cell: (props) => props.row.original.received_date ? formatDate(props.row.original.received_date, 'DD MMM YYYY') : '-', }, { accessorKey: 'due_date', header: 'Jatuh Tempo', cell: (props) => props.row.original.due_date ? formatDate(props.row.original.due_date, 'DD MMM YYYY') : '-', }, { header: 'Aging', enableSorting: false, cell: (props) => { const purchase = props.row.original; if (!purchase.po_date) return '-'; const poDate = new Date(purchase.po_date); const today = new Date(); const diffTime = Math.abs(today.getTime() - poDate.getTime()); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); return `${diffDays} hari`; }, }, { accessorKey: 'status', header: 'Status Approval', cell: (props) => { const approval = props.row.original.latest_approval; if (!approval) return '-'; const status = approval.action; let statusColor: Color = 'neutral'; if (status === 'REJECTED') { statusColor = getStatusBadgeColor(status); } else { switch (approval.step_number) { case 1: statusColor = 'neutral'; break; case 2: statusColor = 'primary'; break; case 3: statusColor = 'info'; break; case 4: statusColor = 'warning'; break; case 5: statusColor = 'success'; break; } } const statusText = approval.step_name || getStatusText(status); return ( ); }, }, { accessorKey: 'created_at', header: 'Tanggal Dibuat', cell: (props) => props.row.original.created_at ? formatDate(props.row.original.created_at, 'DD MMM YYYY') : '-', }, { header: 'Aksi', cell: (props) => { const currentPageSize = props.table.getPaginationRowModel().rows.length; const currentPageRows = props.table.getPaginationRowModel().flatRows; const currentRowRelativeIndex = currentPageRows.findIndex((r) => r.id === props.row.id) + 1; const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; const deleteClickHandler = () => { setSelectedPurchase(props.row.original); deleteModal.openModal(); }; return ( ); }, }, ]; // ===== EVENT HANDLERS ===== const confirmationModalDeleteClickHandler = useCallback(async () => { setIsDeleteLoading(true); try { const deleteResponse = await PurchaseApi.delete( selectedPurchase?.id as number ); if (isResponseSuccess(deleteResponse)) { refreshPurchaseRequests(); deleteModal.closeModal(); toast.success('Berhasil menghapus data permintaan pembelian!'); } else { toast.error(deleteResponse?.message ?? 'Gagal menghapus data!'); } } catch { toast.error('Gagal menghapus data permintaan pembelian!'); } setIsDeleteLoading(false); }, [selectedPurchase?.id, refreshPurchaseRequests, deleteModal]); const searchChangeHandler: ChangeEventHandler = useCallback( (e) => { updateFilter('search', e.target.value); }, [updateFilter] ); const filterSubmitHandler = (values: PurchaseFilter) => { setFilters({ po_date: values.poDate, product_category_id: values.category.join(','), product_category_name: values.category_labels?.map((item) => item.label).join(',') || '', approval_status: values.status.join(','), supplier_id: values.supplier_id ? String(values.supplier_id) : '', supplier_name: values.supplier_label || '', area_id: values.area_id ? String(values.area_id) : '', area_name: values.area_label || '', location_id: values.location_id ? String(values.location_id) : '', location_name: values.location_label || '', project_flock_id: values.project_flock_id ? String(values.project_flock_id) : '', project_flock_name: values.project_flock_label || '', project_flock_kandang_id: values.project_flock_kandang_id ? String(values.project_flock_kandang_id) : '', project_flock_kandang_name: values.project_flock_kandang_label || '', }); }; const filterResetHandler = () => { setFilters({ po_date: '', product_category_id: '', product_category_name: '', approval_status: '', supplier_id: '', supplier_name: '', area_id: '', area_name: '', location_id: '', location_name: '', project_flock_id: '', project_flock_name: '', project_flock_kandang_id: '', project_flock_kandang_name: '', }); }; const purchaseFilterInitialValues = useMemo(() => { const categoryIds = tableFilterState.product_category_id ? tableFilterState.product_category_id .split(',') .map((item) => item.trim()) .filter(Boolean) : []; const categoryLabels = tableFilterState.product_category_name ? tableFilterState.product_category_name .split(',') .map((item) => item.trim()) .filter(Boolean) : []; const approvalStatuses = tableFilterState.approval_status ? tableFilterState.approval_status .split(',') .map((item) => item.trim()) .filter(Boolean) : []; return { poDate: tableFilterState.po_date, category: categoryIds.map((value, index) => ({ value: Number(value), label: categoryLabels[index] || value, })), status: approvalStatuses.map((value) => ({ value, label: PURCHASE_ORDER_APPROVAL_LINE.find((item) => item.step_name === value) ?.step_name || value, })), supplier: tableFilterState.supplier_id ? ({ value: Number(tableFilterState.supplier_id), label: tableFilterState.supplier_name || tableFilterState.supplier_id, } as OptionType) : null, area: tableFilterState.area_id ? ({ value: Number(tableFilterState.area_id), label: tableFilterState.area_name || tableFilterState.area_id, } as OptionType) : null, location: tableFilterState.location_id ? ({ value: Number(tableFilterState.location_id), label: tableFilterState.location_name || tableFilterState.location_id, } as OptionType) : null, project_flock: tableFilterState.project_flock_id ? ({ value: Number(tableFilterState.project_flock_id), label: tableFilterState.project_flock_name || tableFilterState.project_flock_id, } as OptionType) : null, project_flock_kandang: tableFilterState.project_flock_kandang_id ? ({ value: Number(tableFilterState.project_flock_kandang_id), label: tableFilterState.project_flock_kandang_name || tableFilterState.project_flock_kandang_id, } as OptionType) : null, }; }, [tableFilterState]); const exportToExcel = useCallback(async () => { setIsLoadingExportingToExcel(true); try { await PurchaseApi.exportToExcel(getTableFilterQueryString()); } catch (error) { toast.error( await getErrorMessage(error, 'Gagal mengekspor data pembelian') ); } finally { setIsLoadingExportingToExcel(false); } }, [getTableFilterQueryString]); const resetExportProgressForm = useCallback(() => { setExportProgressStartDate(''); setExportProgressEndDate(''); }, []); const exportProgressStartDateChangeHandler: ChangeEventHandler = useCallback((e) => { setExportProgressStartDate(e.target.value); }, []); const exportProgressEndDateChangeHandler: ChangeEventHandler = useCallback((e) => { setExportProgressEndDate(e.target.value); }, []); const exportProgressInputToExcelClickHandler = useCallback(() => { resetExportProgressForm(); exportProgressInputModal.openModal(); }, [exportProgressInputModal, resetExportProgressForm]); const submitExportProgressInputHandler = useCallback(async () => { if (!exportProgressStartDate || !exportProgressEndDate) { return; } setIsExportProgressLoading(true); try { await PurchaseApi.exportInputProgressToExcel( exportProgressStartDate, exportProgressEndDate ); exportProgressInputModal.closeModal(); resetExportProgressForm(); toast.success('Ekspor berhasil'); } catch (error) { toast.error( await getErrorMessage(error, 'Gagal mengekspor input progress') ); } finally { setIsExportProgressLoading(false); } }, [ exportProgressEndDate, exportProgressInputModal, exportProgressStartDate, resetExportProgressForm, ]); return ( <>
} className={{ wrapper: 'w-full min-w-24 max-w-3xs', inputWrapper: 'rounded-xl! shadow-button-soft', input: 'placeholder:font-semibold placeholder:text-base-content/50', }} />
Export
} >
{/* Table Section */}
{isLoading ? (
) : !isResponseSuccess(purchaseRequests) || purchaseRequests.data?.length === 0 ? (
} />
) : ( data={ isResponseSuccess(purchaseRequests) ? purchaseRequests?.data : [] } columns={purchaseColumns} pageSize={tableFilterState.pageSize} page={ isResponseSuccess(purchaseRequests) ? purchaseRequests?.meta?.page : 0 } totalItems={ isResponseSuccess(purchaseRequests) ? purchaseRequests?.meta?.total_results : 0 } onPageChange={setPage} onPageSizeChange={setPageSize} isLoading={isLoading} sorting={sorting} setSorting={handleSortingChange} manualSorting className={{ containerClassName: cn('p-3 mb-0'), headerColumnClassName: 'text-nowrap', }} /> )}
{/* ===== MODAL COMPONENTS ===== */} deleteModal.closeModal(), }} primaryButton={{ text: 'Ya', color: 'error', isLoading: isDeleteLoading, onClick: confirmationModalDeleteClickHandler, }} />

Ekspor Input Progress

); }; export default PurchaseTable;