'use client'; import Button from '@/components/Button'; import CheckboxInput from '@/components/input/CheckboxInput'; import DateInput from '@/components/input/DateInput'; import TextArea from '@/components/input/TextArea'; import Modal, { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import Table from '@/components/Table'; import { getErrorMessage, isResponseError, isResponseSuccess, } from '@/lib/api-helper'; import { cn, formatCurrency, formatDate, formatTitleCase } from '@/lib/helper'; import { MarketingApi, SalesOrderApi, } from '@/services/api/marketing/marketing'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { BaseApiResponse } from '@/types/api/api-general'; import { BaseSalesOrder, Marketing, MarketingFilter, } from '@/types/api/marketing/marketing'; import { Icon } from '@iconify/react'; import { CellContext, ColumnDef, Row, SortingState, Updater, } from '@tanstack/react-table'; import { useRouter } from 'next/navigation'; import { ChangeEventHandler, useCallback, useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import useSWR from 'swr'; import RequirePermission from '@/components/helper/RequirePermission'; import Dropdown from '@/components/Dropdown'; import PopoverButton from '@/components/popover/PopoverButton'; import PopoverContent from '@/components/popover/PopoverContent'; import StatusBadge from '@/components/helper/StatusBadge'; import MarketingFilterModal from '@/components/pages/marketing/MarketingFilter'; import ButtonFilter from '@/components/helper/ButtonFilter'; import MarketingTableSkeleton from '@/components/pages/marketing/skeleton/MarketingTableSkeleton'; const RowsOptionsMenu = ({ props, deleteClickHandler, deliveryClickHandler, popoverPosition, }: { type: 'dropdown' | 'collapse'; props: CellContext; deleteClickHandler: () => void; deliveryClickHandler?: () => void; popoverPosition?: 'top' | 'bottom'; }) => { const popoverId = `marketing#${props.row.original.id}`; const popoverAnchorName = `--anchor-marketing#${props.row.original.id}`; const isDeliveryRejected = props.row.original.latest_approval.action === 'REJECTED' && props.row.original.latest_approval.step_number === 3; return (
{props.row.original.latest_approval.step_number != 1 && !isDeliveryRejected && ( <> )} {props.row.original.latest_approval.step_number != 3 && ( <> )}
); }; const MarketingTable = () => { const [approveAction, setApproveAction] = useState<'APPROVED' | 'REJECTED'>( 'APPROVED' ); const [selectedItem, setSelectedItem] = useState(null); const [rowSelection, setRowSelection] = useState>({}); const [bulkDeliveryDate, setBulkDeliveryDate] = useState(''); const [bulkDeliveryNotes, setBulkDeliveryNotes] = useState(''); const [isSubmittingBulkDelivery, setIsSubmittingBulkDelivery] = useState(false); const [isExportProgressLoading, setIsExportProgressLoading] = useState(false); const [exportProgressStartDate, setExportProgressStartDate] = useState(''); const [exportProgressEndDate, setExportProgressEndDate] = useState(''); const router = useRouter(); const deleteModal = useModal(); const confirmationModal = useModal(); const productsModal = useModal(); const deliveryModal = useModal(); const bulkDeliveryModal = useModal(); const exportProgressInputModal = useModal(); const filterModal = useModal(); const { state: tableFilterState, updateFilter, setPage, setPageSize, toQueryString: getTableFilterToQueryString, } = useTableFilter({ initial: { search: '', product_ids: '', product_names: '', status: '', status_name: '', customer_id: '', customer_name: '', project_flock_id: '', project_flock_name: '', project_flock_kandang_id: '', project_flock_kandang_name: '', sort_by: '', order_by: '', }, paramMap: { page: 'page', pageSize: 'limit', product_ids: 'product_ids', status: 'status', customer_id: 'customer_id', project_flock_id: 'project_flock_id', project_flock_kandang_id: 'project_flock_kandang_id', sort_by: 'sort_by', order_by: 'sort_order', }, excludeKeysFromUrl: [ 'product_names', 'status_name', 'customer_name', 'project_flock_name', 'project_flock_kandang_name', ], persist: true, storeName: 'marketing-table', }); 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); } }; // ===== FETCH DATA ===== const { data: marketing, isLoading: isLoadingMarketing, mutate: refreshMarketing, } = useSWR( `${MarketingApi.basePath}${getTableFilterToQueryString()}`, MarketingApi.getAllFetcher ); // ===== HANDLER ===== const filterSubmitHandler = (values: MarketingFilter) => { updateFilter( 'product_ids', values.product_ids?.map((item) => item.toString()).join(','), true ); updateFilter('product_names', values.product_names?.join(',')); updateFilter('status', values.status ? values.status.toString() : '', true); updateFilter('status_name', values.status_name, true); updateFilter( 'customer_id', values.customer_id ? values.customer_id.toString() : '', true ); updateFilter('customer_name', values.customer_name, true); updateFilter( 'project_flock_id', values.project_flock_id ? values.project_flock_id.toString() : '', true ); updateFilter('project_flock_name', values.project_flock_name ?? '', true); updateFilter( 'project_flock_kandang_id', values.project_flock_kandang_id ? values.project_flock_kandang_id.toString() : '', true ); updateFilter( 'project_flock_kandang_name', values.project_flock_kandang_name ?? '', true ); }; const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = useState(false); const [isApproveLoading, setIsApproveLoading] = useState(false); const [isDeliveryLoading, setIsDeliveryLoading] = useState(false); const filterResetHandler = () => { updateFilter('product_ids', '', true); updateFilter('product_names', '', true); updateFilter('status', '', true); updateFilter('status_name', '', true); updateFilter('customer_id', '', true); updateFilter('customer_name', '', true); updateFilter('project_flock_id', '', true); updateFilter('project_flock_name', '', true); updateFilter('project_flock_kandang_id', '', true); updateFilter('project_flock_kandang_name', '', true); }; const approveClickHandler = () => { setApproveAction('APPROVED'); if (selectedApprovalStep === 2) { bulkDeliveryModal.openModal(); return; } confirmationModal.openModal(); }; const rejectClickHandler = () => { setApproveAction('REJECTED'); confirmationModal.openModal(); }; const productsClickHandler = useCallback( (item: Marketing) => { setSelectedItem(item); productsModal.openModal(); }, [productsModal] ); const deleteMarketingHandler = async () => { const deleteMarketingRes = await MarketingApi.delete( selectedItem?.id as number ); if (isResponseSuccess(deleteMarketingRes)) { confirmationModal.closeModal(); toast.success(deleteMarketingRes?.message as string); } if (isResponseError(deleteMarketingRes)) { confirmationModal.closeModal(); toast.error(deleteMarketingRes?.message as string); } refreshMarketing(); deleteModal.closeModal(); }; const allData = isResponseSuccess(marketing) ? marketing.data : []; const selectedRowsData = allData.filter( (row) => rowSelection[row.id.toString()] ); const selectedApprovalStep = selectedRowsData.length > 0 ? selectedRowsData[0].latest_approval.step_number : null; const eligibleSelectedRows = selectedRowsData.filter((row) => { const approval = row.latest_approval; if (approval.action === 'REJECTED') { return false; } if (selectedApprovalStep === null) { return approval.step_number === 1 || approval.step_number === 2; } return approval.step_number === selectedApprovalStep; }); const hasApprovable = eligibleSelectedRows.length > 0; const hasRejectable = eligibleSelectedRows.length > 0; const disableApprove = !hasApprovable; const disableReject = !hasRejectable; const idsToProcess = eligibleSelectedRows.map((row) => row.id); const nextApprovalStatus = selectedApprovalStep === 1 ? 'SALES_ORDER' : selectedApprovalStep === 2 ? 'DELIVERY_ORDER' : null; const productIds = tableFilterState.product_ids ? tableFilterState.product_ids .split(',') .map((item) => item.trim()) .filter(Boolean) : []; const productLabels = tableFilterState.product_names ? tableFilterState.product_names .split(',') .map((item) => item.trim()) .filter(Boolean) : []; const marketingFilterInitialValues = { product_ids: productIds.map((value, idx) => ({ value: Number(value), label: productLabels[idx] || '-', })), status: tableFilterState.status ? { value: tableFilterState.status, label: tableFilterState.status_name, } : null, customer: tableFilterState.customer_id ? { value: Number(tableFilterState.customer_id), label: tableFilterState.customer_name, } : null, project_flock: tableFilterState.project_flock_id ? { value: Number(tableFilterState.project_flock_id), label: tableFilterState.project_flock_name, } : null, project_flock_kandang: tableFilterState.project_flock_kandang_id ? { value: Number(tableFilterState.project_flock_kandang_id), label: tableFilterState.project_flock_kandang_name, } : null, }; const approveMarketingHandler = async (notes: string) => { if (idsToProcess.length === 0) { toast.error(`Tidak ada data yang valid untuk di ${approveAction}.`); confirmationModal.closeModal(); return; } if (approveAction === 'APPROVED' && selectedApprovalStep !== 1) { toast.error('Approve tahap ini harus menggunakan tanggal pengiriman.'); confirmationModal.closeModal(); return; } if (approveAction === 'APPROVED' && !nextApprovalStatus) { toast.error('Status approval berikutnya tidak valid.'); confirmationModal.closeModal(); return; } setIsApproveLoading(true); try { const approveMarketingRes: BaseApiResponse | undefined = approveAction === 'APPROVED' ? await MarketingApi.bulkApprovals( idsToProcess, nextApprovalStatus as 'SALES_ORDER' | 'DELIVERY_ORDER', '', notes || `APPROVED marketing ${idsToProcess.join(', ')}` ) : await SalesOrderApi.bulkApprovals( idsToProcess, approveAction, notes ); if (isResponseSuccess(approveMarketingRes)) { confirmationModal.closeModal(); toast.success(approveMarketingRes?.message as string); setRowSelection({}); } refreshMarketing(); } finally { setIsApproveLoading(false); } }; const bulkDeliveryDateChangeHandler: ChangeEventHandler = ( e ) => { setBulkDeliveryDate(e.target.value); }; const bulkDeliveryNotesChangeHandler: ChangeEventHandler< HTMLTextAreaElement > = (e) => { setBulkDeliveryNotes(e.target.value); }; const submitBulkDeliveryApprovalHandler = async ( selectedIds: number[], deliveryDate: string, notes: string ) => { if (selectedIds.length === 0) { toast.error('Tidak ada data yang valid untuk diproses.'); return; } if (!deliveryDate) { toast.error('Tanggal pengiriman wajib diisi.'); return; } setIsSubmittingBulkDelivery(true); try { const bulkDeliveryApprovalRes = await MarketingApi.bulkApprovals( selectedIds, 'DELIVERY_ORDER', deliveryDate, notes || `APPROVED delivery marketing ${selectedIds.join(', ')}` ); if (isResponseError(bulkDeliveryApprovalRes)) { toast.error(bulkDeliveryApprovalRes?.message as string); return; } if (!isResponseSuccess(bulkDeliveryApprovalRes)) { toast.error('Gagal memproses bulk approve delivery.'); return; } toast.success(bulkDeliveryApprovalRes?.message as string); bulkDeliveryModal.closeModal(); setBulkDeliveryDate(''); setBulkDeliveryNotes(''); setRowSelection({}); refreshMarketing(); } finally { setIsSubmittingBulkDelivery(false); } }; const confirmationModalDeliveryClickHandler = async (notes: string) => { setIsDeliveryLoading(true); try { const res = await SalesOrderApi.delivery( selectedItem?.id as number, notes ); deliveryModal.closeModal(); toast.success(res?.message as string); refreshMarketing?.(); router.push( `/marketing/detail/delivery-orders/edit?id=${selectedItem?.id}` ); } finally { setIsDeliveryLoading(false); } }; const getRowCanSelect = useCallback( (row: Row): boolean => { const approval = row.original.latest_approval; const isSelectableStep = approval?.step_number === 1 || approval?.step_number === 2; if (!isSelectableStep || approval?.action === 'REJECTED') { return false; } if (selectedApprovalStep === null) { return true; } return approval?.step_number === selectedApprovalStep; }, [selectedApprovalStep] ); const exportToExcelHandler = async () => { setIsLoadingExportingToExcel(true); await MarketingApi.exportToExcel(getTableFilterToQueryString()); setIsLoadingExportingToExcel(false); }; const resetExportProgressForm = () => { setExportProgressStartDate(''); setExportProgressEndDate(''); }; const exportProgressStartDateChangeHandler: ChangeEventHandler< HTMLInputElement > = (e) => { setExportProgressStartDate(e.target.value); }; const exportProgressEndDateChangeHandler: ChangeEventHandler< HTMLInputElement > = (e) => { setExportProgressEndDate(e.target.value); }; const exportProgressInputToExcelClickHandler = () => { resetExportProgressForm(); exportProgressInputModal.openModal(); }; const submitExportProgressInputHandler = async () => { if (!exportProgressStartDate || !exportProgressEndDate) { return; } setIsExportProgressLoading(true); try { await MarketingApi.exportInputProgressToExcel( exportProgressStartDate, exportProgressEndDate ); exportProgressInputModal.closeModal(); resetExportProgressForm(); toast.success('Ekspor berhasil'); } catch (error) { toast.error( await getErrorMessage(error, 'Gagal mengekspor input progress') ); } finally { setIsExportProgressLoading(false); } }; const columns = useMemo[]>(() => { return [ { id: 'select', size: 1, header: ({ table }) => { const allRows = table.getRowModel().rows; const stepForBulkSelection = selectedApprovalStep ?? allRows.find(getRowCanSelect)?.original.latest_approval.step_number; const selectableRows = allRows.filter((row) => { if (!getRowCanSelect(row)) { return false; } if (!stepForBulkSelection) { return false; } return ( row.original.latest_approval.step_number === stepForBulkSelection ); }); const allSelected = selectableRows.length > 0 && selectableRows.every((row) => row.getIsSelected()); const someSelected = selectableRows.some((row) => row.getIsSelected()) && !allSelected; const toggleSelectableRows = () => { const shouldSelect = !allSelected; selectableRows.forEach((row) => row.toggleSelected(shouldSelect)); }; return (
); }, cell: ({ row }) => { const canSelect = getRowCanSelect(row); return (
); }, }, { accessorKey: 'so_number', header: 'No. Order', cell: (props) => { return props.row.original.do_number ? props.row.original.do_number : props.row.original.so_number; }, }, { accessorKey: 'so_date', header: 'Tanggal', cell: (props) => { return formatDate(props.row.original.so_date, 'DD MMM yyyy'); }, }, { accessorKey: 'status', header: 'Status', cell: (props) => { const approval = props.row.original.latest_approval; const isRejected = approval?.action == 'REJECTED'; const isApproved = approval?.action == 'APPROVED'; const isUpdated = approval?.action == 'UPDATED'; return ( ); }, }, { accessorKey: 'customer', header: 'Customer', cell: (props) => props.row.original.customer.name, }, { accessorKey: 'grand_total', accessorFn: (row) => row.sales_order ?.map((product) => product.total_price) .reduce((a, b) => a + b, 0) ?? 0, header: 'Grand Total', cell: (props) => { return formatCurrency( props.row.original?.sales_order ?.map((product) => product.total_price) .reduce((a, b) => a + b, 0) ?? 0 ); }, }, { accessorKey: 'marketing_products.length', header: 'Product Details', enableSorting: false, cell: (props) => { if (props?.row?.original?.sales_order?.length) { if (props?.row?.original?.sales_order?.length > 1) { return ( ); } else { const product = props?.row?.original?.sales_order[0]; return <>{product?.product_warehouse?.product?.name}; } } }, }, { accessorKey: 'created_at', header: 'Tanggal Dibuat', cell: (props) => props.row.original.created_at ? formatDate(props.row.original.created_at, 'DD MMM yyyy') : '-', }, { id: 'actions', maxSize: 80, 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 = () => { setSelectedItem(props.row.original); deleteModal.openModal(); }; const deliveryClickHandler = () => { setSelectedItem(props.row.original); deliveryModal.openModal(); }; return ( ); }, }, ]; }, [ deleteModal, deliveryModal, getRowCanSelect, productsClickHandler, selectedApprovalStep, ]); return ( <>
{idsToProcess.length > 0 && ( <>
)}
{ filterModal.openModal(); }} className='px-3 py-2.5' /> Export
} className={{ content: 'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden', }} >
{isLoadingMarketing ? (
) : !isResponseSuccess(marketing) || marketing.data?.length === 0 ? (
} />
) : ( )}

Bulk Approve Delivery

Pilih tanggal pengiriman untuk approve {idsToProcess.length} data penjualan tahap 2.