From 17589cb2b495a2545cf4bad9a0f3c766fd5f16b8 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Mon, 2 Feb 2026 22:00:42 +0700 Subject: [PATCH] refactor(FE): refactor UI Sales Order and Delivery Order --- src/app/marketing/page.tsx | 4 +- src/components/helper/form/FormErrors.tsx | 4 +- src/components/pages/ApprovalSteps.tsx | 5 +- .../marketing/DeliveryOrderFormModal.tsx | 809 ++++++++++++++++++ .../pages/marketing/MarketingDetail.tsx | 572 +++++++++++++ .../pages/marketing/MarketingTable.tsx | 132 ++- .../pages/marketing/SalesOrderFormModal.tsx | 534 ++++++------ .../marketing/form/MarketingForm.schema.ts | 129 ++- .../pages/marketing/form/MarketingForm.tsx | 16 +- .../delivery-order/DeliverOrderProduct.tsx | 401 +++++---- .../sales-order/SalesOrderProductForm.tsx | 25 +- .../table-view/DeliveryOrderProductTable.tsx | 371 ++++---- .../table-view/SalesOrderProductTable.tsx | 87 +- .../marketing/pdf/DeliveryOrderExport.tsx | 37 +- .../pages/marketing/pdf/SalesOrderExport.tsx | 33 +- 15 files changed, 2338 insertions(+), 821 deletions(-) create mode 100644 src/components/pages/marketing/DeliveryOrderFormModal.tsx create mode 100644 src/components/pages/marketing/MarketingDetail.tsx diff --git a/src/app/marketing/page.tsx b/src/app/marketing/page.tsx index 70eb4549..d8b4bdcf 100644 --- a/src/app/marketing/page.tsx +++ b/src/app/marketing/page.tsx @@ -1,3 +1,4 @@ +import DeliveryOrderFormModal from '@/components/pages/marketing/DeliveryOrderFormModal'; import MarketingTable from '@/components/pages/marketing/MarketingTable'; import SalesOrderFormModal from '@/components/pages/marketing/SalesOrderFormModal'; @@ -6,7 +7,8 @@ const Marketing = () => {
- + +
); }; diff --git a/src/components/helper/form/FormErrors.tsx b/src/components/helper/form/FormErrors.tsx index d4959e8b..60ec1687 100644 --- a/src/components/helper/form/FormErrors.tsx +++ b/src/components/helper/form/FormErrors.tsx @@ -13,6 +13,7 @@ const AlertErrorList = ({ formErrorList, className, onClose, + title, }: { formErrorList: string[]; className?: { @@ -26,6 +27,7 @@ const AlertErrorList = ({ li?: string; }; onClose: () => void; + title?: string; }) => { if (formErrorList.length === 0) return null; @@ -51,7 +53,7 @@ const AlertErrorList = ({ height={20} /> - Terdapat {formErrorList.length} error pada form: + {title || `Terdapat ${formErrorList.length} error pada form:`} + +
+ +

+ View Details +

+
+
+

+ Informasi Penjualan +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Label + +
+
Value
+
+
No. Sales Order + {marketing.data.so_number} +
Nama Pelanggan + {marketing.data.customer.name} +
Status + {formatTitleCase( + marketing.data.latest_approval.step_name + )} +
+ Tanggal Penjualan + + {formatDate(marketing.data.so_date, 'DD MMM yyyy')} +
Total Penjualan + {formatCurrency(grandTotal || 0)} +
+ Dokumen Penjualan + + +
+
+
+ +
+
+
+ {step === 2 && ( + <> + + +
+ + )} +

+ Informasi +

+
+ {step === 1 && ( + + + + )} +
+ {step === 1 && ( + + )} +
+

+ {step == 2 && 'Ubah '} Informasi{' '} + {step == 2 ? 'Delivery' : 'Produk'} +

+ {step === 1 && ( + + )} + {step === 2 && ( + + )} +
+ {formErrorMessage && ( +
+ setFormErrorMessage('')} + /> +
+ )} + {formErrorList && ( +
+ +
+ )} + {step === 1 && ( +
+ + +
+ )} +
+ + )} +
+ + { + closeModalHandler(); + }, + }} + > + + + + + + + + ); +}; + +export default DeliveryOrderFormModal; diff --git a/src/components/pages/marketing/MarketingDetail.tsx b/src/components/pages/marketing/MarketingDetail.tsx new file mode 100644 index 00000000..f8f7d269 --- /dev/null +++ b/src/components/pages/marketing/MarketingDetail.tsx @@ -0,0 +1,572 @@ +'use client'; + +import Button from '@/components/Button'; +import Card from '@/components/Card'; +import { FormHeader } from '@/components/helper/form/FormHeader'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; +import ApprovalSteps, { + useApprovalSteps, +} from '@/components/pages/ApprovalSteps'; +import Table from '@/components/Table'; +import { MARKETING_APPROVAL_LINE } from '@/config/approval-line'; +import { + cn, + formatCurrency, + formatDate, + formatNumber, + formatTitleCase, + formatVechicleNumber, +} from '@/lib/helper'; +import { + MarketingApi, + SalesOrderApi, +} from '@/services/api/marketing/marketing'; +import { + BaseDelivery, + BaseSalesOrder, + Marketing, +} from '@/types/api/marketing/marketing'; +import { Icon } from '@iconify/react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; +import toast from 'react-hot-toast'; +import SalesOrderExport from '@/components/pages/marketing/pdf/SalesOrderExport'; +import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport'; +import RequirePermission from '@/components/helper/RequirePermission'; +import Badge from '@/components/Badge'; + +const MarketingDetail = ({ + initialValues, + refresh, +}: { + initialValues?: Marketing; + refresh?: () => void; +}) => { + const router = useRouter(); + const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>( + 'APPROVED' + ); + const [grandTotal, setGrandTotal] = useState( + initialValues?.sales_order + ?.map((item) => item.total_price) + .reduce((a, b) => a + b, 0) + ); + const [isLoading, setIsLoading] = useState(false); + + const deleteModal = useModal(); + const confirmationModal = useModal(); + const deliveryModal = useModal(); + const { + approvals, + isLoading: isLoadingApproval, + refresh: refreshApproval, + } = useApprovalSteps({ + latestApproval: initialValues?.latest_approval, + approvalLines: MARKETING_APPROVAL_LINE, + moduleName: 'MARKETINGS', + moduleId: initialValues?.id as number as unknown as string, + }); + + const approveClickHandler = () => { + setApprovalAction('APPROVED'); + confirmationModal.openModal(); + }; + + const rejectClickHandler = () => { + setApprovalAction('REJECTED'); + confirmationModal.openModal(); + }; + + const deleteClickHandler = () => { + deleteModal.openModal(); + }; + + const confirmationModalDeleteClickHandler = async () => { + setIsLoading(true); + const res = await MarketingApi.delete(initialValues?.id as number); + deleteModal.closeModal(); + router.push('/marketing'); + toast.success(res?.message as string); + setIsLoading(false); + }; + + const confirmationModalApproveClickHandler = async (notes: string) => { + setIsLoading(true); + const res = await SalesOrderApi.singleApproval( + initialValues?.id as number, + approvalAction, + notes + ); + setIsLoading(false); + confirmationModal.closeModal(); + toast.success(res?.message as string); + refresh?.(); + refreshApproval?.(); + }; + + const confirmationModalDeliveryClickHandler = async (notes: string) => { + setIsLoading(true); + const res = await SalesOrderApi.delivery( + initialValues?.id as number, + notes + ); + setIsLoading(false); + deliveryModal.closeModal(); + toast.success(res?.message as string); + refresh?.(); + refreshApproval?.(); + router.push( + `/marketing/detail/delivery-orders/edit?marketingId=${initialValues?.id}` + ); + }; + + const approval = initialValues?.latest_approval; + const isRejected = approval?.action == 'REJECTED'; + const isApproved = approval?.action == 'APPROVED'; + + return ( + <> +
+ 2 ? 'Delivery Order' : 'Sales Order'}`} + backUrl='/marketing' + /> + {!isLoadingApproval && approvals && ( + + )} +
+ {initialValues?.latest_approval?.step_number == 1 && ( + <> + + + + + + + + + )} + {initialValues?.latest_approval?.step_number != 1 && ( + <> + + + + + )} +
+ + +
+ + + + + + + + {Number(initialValues?.latest_approval?.step_number) > 2 && ( + + + + + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {Number(initialValues?.latest_approval?.step_number) > 2 && ( + + + + + + )} + +
+ No. Sales Order + : + {initialValues?.so_number} +
+ No. Delivery Order + : + {initialValues?.delivery_order + ?.map((item) => item.do_number) + .join(', ')} +
Nama Pelanggan:{initialValues?.customer?.name}
Status: + + + {isRejected + ? 'Ditolak' + : formatTitleCase(approval?.step_name || '')} + +
Tanggal Penjualan:{formatDate(initialValues?.so_date, 'DD MMM yyyy')}
Total Penjualan:{formatCurrency(grandTotal as number)}
Catatan:{initialValues?.notes ?? '-'}
Dokumen Penjualan: + +
Dokumen Pengiriman: + {initialValues?.delivery_order?.map((item, index) => ( + + ))} +
+
+
+ {initialValues?.sales_order && ( + + + data={initialValues?.sales_order} + columns={[ + { + header: 'Kandang', + accessorFn(row) { + return row.product_warehouse.warehouse.name; + }, + }, + { + header: 'Produk', + accessorFn(row) { + return row.product_warehouse.product.name; + }, + }, + { + header: 'Harga Satuan (Rp)', + accessorFn(row) { + return formatCurrency(row.unit_price); + }, + }, + { + header: 'Total Bobot (Kg)', + accessorFn(row) { + return formatNumber(row.total_weight); + }, + }, + { + header: 'Kuantitas', + accessorFn(row) { + return formatNumber(row.qty); + }, + }, + { + header: 'Avg. Bobot (Kg)', + accessorFn(row) { + return formatNumber(row.avg_weight); + }, + }, + { + header: 'Total Penjualan (Rp)', + accessorFn(row) { + return formatCurrency(row.total_price); + }, + }, + ]} + className={{ + containerClassName: cn({ + 'mb-20': + initialValues?.sales_order && + initialValues?.sales_order?.length === 0, + }), + tableWrapperClassName: 'overflow-x-auto min-h-full!', + tableClassName: 'font-inter w-full table-auto min-h-full!', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', + bodyRowClassName: 'border-b border-b-gray-200', + bodyColumnClassName: + 'px-6 py-3 last:flex last:flex-row last:justify-end', + paginationClassName: 'hidden', + }} + /> + + )} + {initialValues?.delivery_order && ( + + {initialValues?.delivery_order.map((delivery, index) => { + return ( +
+ +
+
+ Nomor DO : {delivery.do_number} +
+
+ + data={delivery.deliveries} + columns={[ + { + header: 'Tanggal Pengiriman', + accessorFn() { + return formatDate( + delivery.delivery_date, + 'DD MMM yyyy' + ); + }, + }, + { + header: 'No. Polisi', + accessorFn(row) { + return formatVechicleNumber(row.vehicle_number); + }, + }, + { + header: 'Kandang', + accessorFn(row) { + return row.product_warehouse.warehouse.name; + }, + }, + { + header: 'Produk', + accessorFn(row) { + return row.product_warehouse.product.name; + }, + }, + { + header: 'Harga Satuan (Rp)', + accessorFn(row) { + return formatCurrency(row.unit_price); + }, + }, + { + header: 'Total Bobot (Kg)', + accessorFn(row) { + return formatNumber(row.total_weight); + }, + }, + { + header: 'Kuantitas', + accessorFn(row) { + return formatNumber(row.qty); + }, + }, + { + header: 'Avg. Bobot (Kg)', + accessorFn(row) { + return formatNumber(row.avg_weight); + }, + }, + { + header: 'Total Penjualan (Rp)', + accessorFn(row) { + return formatCurrency(row.total_price); + }, + }, + ]} + className={{ + containerClassName: cn({ + 'mb-20': + initialValues?.sales_order && + initialValues?.sales_order?.length === 0, + }), + tableWrapperClassName: 'overflow-x-auto min-h-full!', + tableClassName: + 'font-inter w-full table-auto min-h-full!', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', + bodyRowClassName: 'border-b border-b-gray-200', + bodyColumnClassName: + 'px-6 py-3 last:flex last:flex-row last:justify-end', + paginationClassName: 'hidden', + }} + /> +
+
+ +
+
+ ); + })} +
+ )} +
+ {initialValues?.latest_approval?.step_number != 3 && ( + <> + + + + + )} + + + +
+
+ + + + + ); +}; + +export default MarketingDetail; diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 5d838cbc..e09617aa 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -2,19 +2,10 @@ import Button from '@/components/Button'; import CheckboxInput from '@/components/input/CheckboxInput'; -import SelectInput, { - OptionType, - useSelect, -} from '@/components/input/SelectInput'; import Modal, { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import Table from '@/components/Table'; -import RowCollapseOptions from '@/components/table/RowCollapseOptions'; -import RowDropdownOptions from '@/components/table/RowDropdownOptions'; -import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector'; -import { TableToolbar } from '@/components/table/TableToolbar'; -import { ROWS_OPTIONS } from '@/config/constant'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { cn, formatCurrency, formatDate, formatTitleCase } from '@/lib/helper'; import { @@ -30,17 +21,12 @@ import { import { Icon } from '@iconify/react'; import { CellContext, ColumnDef, Row } from '@tanstack/react-table'; import { useRouter } from 'next/navigation'; -import { useCallback, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import useSWR from 'swr'; import RequirePermission from '@/components/helper/RequirePermission'; import { useAuth } from '@/services/hooks/useAuth'; -import { CustomerApi, ProductApi } from '@/services/api/master-data'; -import { MARKETING_APPROVAL_LINE } from '@/config/approval-line'; -import Badge from '@/components/Badge'; import ButtonFilter from '@/components/helper/ButtonFilter'; -import Menu from '@/components/menu/Menu'; -import MenuItem from '@/components/menu/MenuItem'; import Dropdown from '@/components/Dropdown'; import PopoverButton from '@/components/popover/PopoverButton'; import PopoverContent from '@/components/popover/PopoverContent'; @@ -59,15 +45,13 @@ const RowsOptionsMenu = ({ deliveryClickHandler?: () => void; popoverPosition?: 'top' | 'bottom'; }) => { - const showEditButton = - props.row.original.latest_approval.action !== 'APPROVED' && - props.row.original.latest_approval.action !== 'REJECTED'; - - const showDeleteButton = showEditButton; - 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 != 3 && ( <>
@@ -747,15 +739,9 @@ const MarketingTable = () => { }, ]} className={{ - containerClassName: 'p-6', - tableWrapperClassName: 'overflow-x-auto min-h-full!', - tableClassName: 'font-inter w-full table-auto min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', - bodyColumnClassName: - 'px-6 py-3 last:flex last:flex-row last:justify-end', + containerClassName: 'p-4', + headerColumnClassName: 'whitespace-nowrap', + bodyColumnClassName: 'last:flex last:flex-row last:justify-end', paginationClassName: 'hidden', }} isLoading={isLoadingMarketing} diff --git a/src/components/pages/marketing/SalesOrderFormModal.tsx b/src/components/pages/marketing/SalesOrderFormModal.tsx index b93db64a..57f1e18c 100644 --- a/src/components/pages/marketing/SalesOrderFormModal.tsx +++ b/src/components/pages/marketing/SalesOrderFormModal.tsx @@ -2,6 +2,7 @@ import Button from '@/components/Button'; import AlertErrorList from '@/components/helper/form/FormErrors'; +import RequirePermission from '@/components/helper/RequirePermission'; import DateInput from '@/components/input/DateInput'; import DebouncedTextArea from '@/components/input/DebouncedTextArea'; import NumberInput from '@/components/input/NumberInput'; @@ -9,9 +10,15 @@ import { OptionType, useSelect } from '@/components/input/SelectInput'; import SelectInputRadio from '@/components/input/SelectInputRadio'; import Modal, { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import { + DeliveryProductToFieldValues, + mergeSOwithDO, + SalesProductToFieldValues, +} from '@/components/pages/marketing/form/MarketingForm'; import { DeliveryOrderFormValues, DeliveryOrderSchema, + getFilledMarketingFormInitialValues, SalesOrderFormValues, SalesOrderSchema, } from '@/components/pages/marketing/form/MarketingForm.schema'; @@ -20,7 +27,7 @@ import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/r import SalesOrderProductForm from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm'; import SalesOrderProductTable from '@/components/pages/marketing/form/table-view/SalesOrderProductTable'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { formatCurrency, formatDate } from '@/lib/helper'; +import { formatDate } from '@/lib/helper'; import { MarketingApi, SalesOrderApi, @@ -30,8 +37,6 @@ import { UserApi } from '@/services/api/user'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import { CreatedUser } from '@/types/api/api-general'; import { - BaseDeliveryOrder, - BaseSalesOrder, CreateSalesOrderPayload, CreateSalesOrderProductPayload, Marketing, @@ -49,112 +54,22 @@ import useSWR, { useSWRConfig } from 'swr'; const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable); const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm); -const SalesProductToFieldValues = ( - product: BaseSalesOrder -): SalesOrderProductFormValues => { - return { - id: product.id, - vehicle_number: product.vehicle_number, - kandang_id: product.product_warehouse.warehouse.id, - kandang: { - value: product.product_warehouse.warehouse.id, - label: product.product_warehouse.warehouse.name, - }, - product_warehouse: { - value: product.product_warehouse.id, - label: product.product_warehouse.product.name, - }, - product_warehouse_id: product.product_warehouse.id, - unit_price: product.unit_price, - total_weight: product.total_weight, - qty: product.qty, - avg_weight: product.avg_weight, - total_price: product.total_price, - }; -}; -const DeliveryProductToFieldValues = ( - salesOrders: BaseSalesOrder[], - delivery: BaseDeliveryOrder -): DeliveryOrderProductFormValues[] => { - const data = delivery.deliveries.map((item) => { - const soId = salesOrders.find( - (so) => so.product_warehouse.id === item.product_warehouse.id - )?.id; - return { - id: soId, - unit_price: item.unit_price, - total_weight: item.total_weight, - qty: item.qty, - avg_weight: item.avg_weight, - total_price: item.total_price, - vehicle_number: item.vehicle_number, - delivery_date: formatDate(delivery.delivery_date, 'yyyy-MM-DD'), - do_number: delivery.do_number, - marketing_product_id: soId, - marketing_product: { - id: soId, - vehicle_number: item.vehicle_number, - kandang_id: item.product_warehouse.warehouse.id, - kandang: { - value: item.product_warehouse.warehouse.id, - label: item.product_warehouse.warehouse.name, - }, - product_warehouse: { - value: item.product_warehouse.id, - label: item.product_warehouse.product.name, - }, - product_warehouse_id: item.product_warehouse.id, - unit_price: item.unit_price, - total_weight: item.total_weight, - qty: item.qty, - avg_weight: item.avg_weight, - total_price: item.total_price, - }, - } as DeliveryOrderProductFormValues; - }); - return data; -}; -const mergeSOwithDO = ( - salesOrders: SalesOrderProductFormValues[], - deliveryOrders: DeliveryOrderProductFormValues[] -): DeliveryOrderProductFormValues[] => { - return salesOrders.map((so) => { - const delivery = deliveryOrders.find( - (d) => d?.marketing_product_id === so.id - ); - - return { - ...so, // nilai dasar dari sales order - marketing_product_id: so.id, - delivery_date: delivery?.delivery_date || undefined, - do_number: delivery?.do_number || undefined, - vehicle_number: delivery?.vehicle_number || so.vehicle_number, - unit_price: delivery?.unit_price, - total_weight: delivery?.total_weight, - qty: delivery?.qty, - avg_weight: delivery?.avg_weight, - total_price: delivery?.total_price, - marketing_product: so, // jika ada, override - } as DeliveryOrderProductFormValues; - }); -}; - const SalesOrderFormModal = ({ initialValues, - afterSubmit, - formType, }: { initialValues?: Marketing; - afterSubmit?: () => void; - formType: 'add' | 'edit' | 'add_deliver' | 'edit_deliver'; }) => { const router = useRouter(); const searchParams = useSearchParams(); const modalAction = searchParams.get('action'); - const MarketingId = searchParams.get('id'); + const marketingId = searchParams.get('id'); - const isModalActionForForm = modalAction === 'add' || modalAction === 'edit'; + const isModalActionForForm = + modalAction === 'add' || + modalAction === 'edit' || + modalAction === 'add_delivery' || + modalAction === 'edit_delivery'; const { mutate } = useSWRConfig(); @@ -164,11 +79,11 @@ const SalesOrderFormModal = ({ ); }; - const { data: Marketing, isLoading: isLoadingMarketing } = useSWR( - isModalActionForForm && MarketingId - ? ['detail-marketing', MarketingId] + const { data: marketing, isLoading: isLoadingMarketing } = useSWR( + isModalActionForForm && marketingId + ? `detail-marketing-${marketingId}` : undefined, - ([, id]) => MarketingApi.getSingle(Number(id)) + () => MarketingApi.getSingle(Number(marketingId)) ); // ================== FETCH OPTIONS ================== @@ -194,6 +109,7 @@ const SalesOrderFormModal = ({ const formModal = useModal(); const successModal = useModal(); + const deleteModal = useModal(); const formRef = useRef(null); const textareaRef = useRef(null); @@ -257,13 +173,13 @@ const SalesOrderFormModal = ({ enableReinitialize: true, initialValues: formikInitialValues, validationSchema: - formType == 'add_deliver' || formType == 'edit_deliver' + modalAction == 'add_deliver' || modalAction == 'edit_deliver' ? DeliveryOrderSchema : SalesOrderSchema, validateOnMount: true, onSubmit: async (values) => { const payload = - formType != 'add_deliver' && formType != 'edit_deliver' + modalAction != 'add_deliver' && modalAction != 'edit_deliver' ? ({ customer_id: values.customer_id as number, sales_person_id: values.sales_person_id as number, @@ -305,7 +221,7 @@ const SalesOrderFormModal = ({ }) .filter((item) => Boolean(item)), } as UpdateDeliveryOrderPayload); - switch (formType) { + switch (modalAction) { case 'add': await createMarketingHandler(payload as CreateSalesOrderPayload); break; @@ -315,19 +231,16 @@ const SalesOrderFormModal = ({ default: break; } - afterSubmit?.(); }, }); // ===== Formik Error List ===== - const { formErrorList, close, handleFormSubmit } = useFormikErrorList( - formik, - { + const { formErrorList, setFormErrorList, close, handleFormSubmit } = + useFormikErrorList(formik, { onAfterSubmit: () => { router.push('/marketing'); }, - } - ); + }); // ================== FORM REPEATER HANDLER ================== const createMarketingHandler = async (values: CreateSalesOrderPayload) => { @@ -336,6 +249,7 @@ const SalesOrderFormModal = ({ if (isResponseSuccess(createMarketingRes)) { closeModalHandler(false); successModal.openModal(); + refreshMarketing(); } if (isResponseError(createMarketingRes)) { toast.error(createMarketingRes?.message as string); @@ -344,13 +258,19 @@ const SalesOrderFormModal = ({ }; const updateMarketingHandler = async (values: UpdateSalesOrderPayload) => { setIsLoading(true); + if (!marketingId) { + toast.error('Marketing ID is required'); + setIsLoading(false); + return; + } const updateMarketingRes = await SalesOrderApi.update( - initialValues?.id as number, + Number(marketingId), values ); if (isResponseSuccess(updateMarketingRes)) { closeModalHandler(false); successModal.openModal(); + refreshMarketing(); } if (isResponseError(updateMarketingRes)) { toast.error(updateMarketingRes?.message as string); @@ -382,6 +302,26 @@ const SalesOrderFormModal = ({ [] ); + const deleteClickHandler = () => { + deleteModal.openModal(); + }; + + const confirmationModalDeleteClickHandler = async () => { + if (!marketingId) { + toast.error(`Tidak ada data yang valid untuk di hapus.`); + deleteModal.closeModal(); + return; + } + setIsLoading(true); + const res = await MarketingApi.delete(Number(marketingId)); + deleteModal.closeModal(); + toast.success(res?.message as string); + setIsLoading(false); + refreshMarketing(); + closeModalHandler(); + router.replace('/marketing'); + }; + // ================== SALES ORDER HANDLER ================== const handleDeleteSO = useCallback( (id: number) => { @@ -393,11 +333,15 @@ const SalesOrderFormModal = ({ }, [memoSalesOrder] ); + const handleDeleteAllSO = useCallback(() => { + formik.setFieldValue('sales_order', []); + }, [memoSalesOrder]); const handleEditSO = useCallback( (id: number) => { const currentProducts = formik.values.sales_order; const selectedProduct = currentProducts.find((p) => p.id == id); setSelectedMarketingProduct(selectedProduct ?? null); + setStep(2); }, [memoSalesOrder] ); @@ -457,6 +401,7 @@ const SalesOrderFormModal = ({ }, [modalAction]); const closeModalHandler = (shouldPushToRoute: boolean = true) => { + refreshMarketing(); if (shouldPushToRoute) { formik.resetForm(); textareaRef.current?.setAttribute('value', ''); @@ -477,6 +422,39 @@ const SalesOrderFormModal = ({ ); }, [memoSalesOrder]); + useEffect(() => { + const getFilledInitialValues = async () => { + if (marketingId && isResponseSuccess(marketing)) { + const filledInitialValues = await getFilledMarketingFormInitialValues( + marketing.data + ); + + formik.setValues(filledInitialValues); + setStep(3); + } + + if (isResponseError(marketing)) { + router.push('/marketing'); + closeModalHandler(); + toast.error(marketing.message); + } + }; + + getFilledInitialValues(); + }, [marketingId, marketing]); + + // Reset error message when step changes + useEffect(() => { + setFormErrorList([]); + setFormErrorMessage(''); + }, [step]); + + useEffect(() => { + if (memoSalesOrder.length === 0) { + setStep(1); + } + }, [memoSalesOrder]); + return ( <> -
-
- - -
- -

- {modalAction === 'add' ? 'Add' : 'Edit'} Sales Order -

-
-
-

- Informasi Umum -

- - - - - - Rp - } - /> - - - -
- {step === 1 && ( + {step <= 3 && ( +
+
- )} + +
+ +

+ {modalAction === 'add' ? 'Add' : 'Edit'} Sales Order +

+
+
+

+ Informasi Order +

+ + + + + + Rp + } + /> + + + +
+ {step === 1 && ( + + )} +
-
+ )} {step === 2 && (
@@ -639,7 +624,7 @@ const SalesOrderFormModal = ({ className='p-0 text-black hover:text-base-content' > @@ -650,20 +635,22 @@ const SalesOrderFormModal = ({ )}

- Tambah Produk + {selectedMarketingProduct?.id ? 'Ubah' : 'Tambah'} Produk

- + + +
-
+
Informasi Produk - + + +
{memoSalesOrder.length > 0 && ( -
+
)} + + item.id === selectedProduct?.value + )?.marketing_product?.product_warehouse?.label, + } as OptionType) + : null + } + onChange={(value) => { + const selected = value as OptionType; + setSelectedProduct(selected); -
- item.id === selectedProduct?.value - )?.marketing_product?.product_warehouse?.label, - } as OptionType) - : null - } - onChange={(value) => { - const selected = value as OptionType; - setSelectedProduct(selected); - - const so = salesOrders?.find( - (item) => item.id === selected?.value - ); - if (!so) { - formik.setValues({ - ...formik.values, - marketing_product_id: undefined, - marketing_product: null, - qty: '', - unit_price: '', - total_price: '', - avg_weight: '', - total_weight: '', - vehicle_number: '', - }); - return; - } - + const so = salesOrders?.find((item) => item.id === selected?.value); + if (!so) { formik.setValues({ ...formik.values, - marketing_product_id: selected.value as number, - marketing_product: SalesProductToFieldValues(so), - qty: so.qty, - unit_price: so.unit_price, - total_price: so.total_price, - avg_weight: so.avg_weight, - total_weight: so.total_weight, - vehicle_number: so.vehicle_number, + marketing_product_id: undefined, + marketing_product: null, + qty: '', + unit_price: '', + total_price: '', + avg_weight: '', + total_weight: '', + vehicle_number: '', }); - }} - startAdornment={ - selectedProduct && ( - - { - exisitingValues?.find( - (item) => item.id === selectedProduct?.value - )?.marketing_product?.kandang?.label - } - - ) + return; } - isClearable - isError={Boolean(formik.errors.marketing_product_id)} - errorMessage={formik.errors.marketing_product_id} - required - /> - - -
-
-
- { - formik.handleChange(e); - setCurrentInput(e.target.name); - }} - onBlur={() => handleBlurField('qty')} - isError={Boolean(formik.errors.qty)} - errorMessage={formik.errors.qty} - placeholder='Masukan Kuantitas' - endAdornment={ -
- - {isResponseSuccess(productData) - ? productData?.data?.uom.name - : ''} - -
- } - bottomLabel={ - formik.values.marketing_product_id - ? 'Stok dijual: ' + - salesOrders?.find( - (item) => item.id === formik.values.marketing_product_id - )?.qty + - ' ' + - (isResponseSuccess(productData) - ? productData?.data?.uom.name - : '') - : '' - } - /> - { - formik.handleChange(e); - setCurrentInput(e.target.name); - }} - onBlur={() => handleBlurField('unit_price')} - isError={Boolean(formik.errors.unit_price)} - errorMessage={formik.errors.unit_price} - placeholder='Masukan Harga Satuan' - /> - { - formik.handleChange(e); - setCurrentInput(e.target.name); - }} - onBlur={() => handleBlurField('avg_weight')} - isError={Boolean(formik.errors.avg_weight)} - errorMessage={formik.errors.avg_weight} - placeholder='Masukan Bobot Rata-rata' - /> - { - formik.handleChange(e); - setCurrentInput(e.target.name); - }} - onBlur={() => handleBlurField('total_weight')} - isError={Boolean(formik.errors.total_weight)} - errorMessage={formik.errors.total_weight} - placeholder='Masukan Total Bobot' - /> + formik.setValues({ + ...formik.values, + marketing_product_id: selected.value as number, + marketing_product: SalesProductToFieldValues(so), + qty: so.qty, + unit_price: so.unit_price, + total_price: so.total_price, + avg_weight: so.avg_weight, + total_weight: so.total_weight, + vehicle_number: so.vehicle_number, + }); + }} + startAdornment={ + selectedProduct && ( + item.id === selectedProduct?.value + )?.marketing_product?.kandang?.label ?? '' + } + color='success' + className={{ + badge: 'whitespace-nowrap w-fit font-semibold', + }} + /> + ) + } + isClearable + isError={Boolean(formik.errors.marketing_product_id)} + errorMessage={formik.errors.marketing_product_id} + required + /> - { - formik.handleChange(e); - setCurrentInput(e.target.name); - }} - onBlur={() => handleBlurField('total_price')} - isError={Boolean(formik.errors.total_price)} - errorMessage={formik.errors.total_price} - placeholder='Masukan Total Penjualan' - /> -
+ + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('qty')} + isError={Boolean(formik.errors.qty)} + errorMessage={formik.errors.qty} + placeholder='Masukan Kuantitas' + endAdornment={ +
+ + {isResponseSuccess(productData) + ? productData?.data?.uom.name + : ''} + +
+ } + bottomLabel={ + formik.values.marketing_product_id + ? 'Stok dijual: ' + + salesOrders?.find( + (item) => item.id === formik.values.marketing_product_id + )?.qty + + ' ' + + (isResponseSuccess(productData) + ? productData?.data?.uom.name + : '') + : '' + } + /> + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('unit_price')} + isError={Boolean(formik.errors.unit_price)} + errorMessage={formik.errors.unit_price} + placeholder='Masukan Harga Satuan' + /> + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('avg_weight')} + isError={Boolean(formik.errors.avg_weight)} + errorMessage={formik.errors.avg_weight} + placeholder='Masukan Bobot Rata-rata' + /> + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('total_weight')} + isError={Boolean(formik.errors.total_weight)} + errorMessage={formik.errors.total_weight} + placeholder='Masukan Total Bobot' + /> - + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('total_price')} + isError={Boolean(formik.errors.total_price)} + errorMessage={formik.errors.total_price} + placeholder='Masukan Total Penjualan' + /> -
- + + +
+ +
diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx index c839d9d3..3161054c 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx @@ -6,27 +6,20 @@ import { SalesOrderProductSchema, } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema'; import { RefObject, useMemo, useState } from 'react'; -import SelectInput, { - OptionType, - useSelect, -} from '@/components/input/SelectInput'; +import { OptionType, useSelect } from '@/components/input/SelectInput'; import { Kandang } from '@/types/api/master-data/kandang'; -import { ProductApi, UomApi, WarehouseApi } from '@/services/api/master-data'; +import { WarehouseApi } from '@/services/api/master-data'; import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; import { ProductWarehouseApi } from '@/services/api/inventory'; import NumberInput from '@/components/input/NumberInput'; import Button from '@/components/Button'; import { isResponseSuccess } from '@/lib/api-helper'; -import { - formatCurrency, - formatNumber, - formatVechicleNumber, -} from '@/lib/helper'; +import { formatNumber, formatVechicleNumber } from '@/lib/helper'; import PatternInput from '@/components/input/PatternInput'; import Alert from '@/components/Alert'; import AlertErrorList from '@/components/helper/form/FormErrors'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; -import useSWR from 'swr'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; const roundWeight = (value: number) => Number(value.toFixed(2)); const roundPrice = (value: number) => Math.round(value); @@ -282,7 +275,7 @@ const SalesOrderProductForm = ({ return ( <> @@ -311,7 +304,7 @@ const SalesOrderProductForm = ({ } errorMessage={formik.errors.vehicle_number} /> - - -
+
diff --git a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx index 6b6f6a81..885cd8fc 100644 --- a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx @@ -1,21 +1,27 @@ -import Table from '@/components/Table'; import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema'; import Button from '@/components/Button'; import { Icon } from '@iconify/react'; -import * as TanStack from '@tanstack/react-table'; -import { useMemo, useRef } from 'react'; -import { - cn, - formatCurrency, - formatDate, - formatNumber, - formatVechicleNumber, -} from '@/lib/helper'; +import { useRef } from 'react'; +import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport'; +import { Marketing } from '@/types/api/marketing/marketing'; type DeliveryOrderProductTableProps = { data: DeliveryOrderProductFormValues[]; - formType?: 'add' | 'edit' | 'add_deliver' | 'edit_deliver'; - onEdit: (id: number) => void; + formType?: + | 'add' + | 'edit' + | 'add_deliver' + | 'edit_deliver' + | 'add_delivery' + | 'edit_delivery' + | 'detail' + | 'rejected' + | 'pending' + | string + | null; + marketing?: Marketing; + onEdit: (id: number, values: DeliveryOrderProductFormValues) => void; onDelete: (id: number) => void; onAddProductClick: () => void; }; @@ -26,201 +32,168 @@ const DeliveryOrderProductTable = ({ onEdit, onDelete, onAddProductClick, + marketing, }: DeliveryOrderProductTableProps) => { const onEditRef = useRef(onEdit); onEditRef.current = onEdit; const onDeleteRef = useRef(onDelete); onDeleteRef.current = onDelete; - const canAddData = data.filter((item) => !Boolean(item.qty)); - - const columns = useMemo(() => { - const cols = [ - { - accessorFn: (row: DeliveryOrderProductFormValues) => row.do_number, - header: 'No. Pengiriman', - cell: ( - props: TanStack.CellContext - ) => ( - <> - {props.row.original.do_number ? props.row.original.do_number : '-'} - - ), - }, - { - accessorFn: (row: DeliveryOrderProductFormValues) => row.vehicle_number, - header: 'No. Polisi', - cell: ( - props: TanStack.CellContext - ) => - props.row.original.vehicle_number - ? formatVechicleNumber(props.row.original.vehicle_number as string) - : '-', - }, - { - accessorFn: (row: DeliveryOrderProductFormValues) => - row.marketing_product?.kandang?.label, - header: 'Kandang', - }, - { - accessorFn: (row: DeliveryOrderProductFormValues) => - row.marketing_product?.product_warehouse?.label, - header: 'Produk', - }, - { - accessorFn: (row: DeliveryOrderProductFormValues) => - row.delivery_date - ? formatDate(row.delivery_date as string, 'DD MMM YYYY') - : '-', - header: 'Tanggal Delivery', - cell: ( - props: TanStack.CellContext - ) => - props.row.original.delivery_date - ? formatDate( - props.row.original.delivery_date as string, - 'DD MMM YYYY' - ) - : '-', - }, - { - accessorFn: (row: DeliveryOrderProductFormValues) => row.unit_price, - header: 'Harga Satuan (Rp)', - cell: ( - props: TanStack.CellContext - ) => - props.row.original.unit_price - ? formatCurrency( - parseFloat(props.row.original.unit_price as string) - ) - : '-', - }, - { - accessorFn: (row: DeliveryOrderProductFormValues) => row.total_weight, - header: 'Total Bobot (Kg)', - cell: ( - props: TanStack.CellContext - ) => - props.row.original.total_weight - ? formatNumber( - parseFloat(props.row.original.total_weight as string) - ) - : '-', - }, - { - accessorFn: (row: DeliveryOrderProductFormValues) => row.qty, - header: 'Kuantitas', - cell: ( - props: TanStack.CellContext - ) => - props.row.original.qty - ? formatNumber(parseFloat(props.row.original.qty as string)) - : '-', - }, - { - accessorFn: (row: DeliveryOrderProductFormValues) => row.avg_weight, - header: 'Avg. Bobot (Kg)', - cell: ( - props: TanStack.CellContext - ) => - props.row.original.avg_weight - ? formatNumber(parseFloat(props.row.original.avg_weight as string)) - : '-', - }, - { - accessorFn: (row: DeliveryOrderProductFormValues) => row.total_price, - header: 'Total Penjualan (Rp)', - cell: ( - props: TanStack.CellContext - ) => - props.row.original.total_price - ? formatCurrency( - parseFloat(props.row.original.total_price as string) - ) - : '-', - }, - - { - header: 'Aksi', - cell: ( - props: TanStack.CellContext - ) => ( -
- <> - {props.row.original.qty && ( - <> - - - - )} - {!props.row.original.qty && '-'} - -
- ), - }, - ]; - if (formType == 'add_deliver') { - return cols.filter((col) => col.header != 'No. Pengiriman'); - } - return cols; - }, [formType, onEditRef]); - return ( <> - - data={data} - columns={columns} - className={{ - tableWrapperClassName: 'overflow-x-auto min-h-full!', - tableClassName: 'font-inter w-full table-auto min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-2 py-2 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end first:flex first:flex-row first:justify-start', - bodyRowClassName: 'border-b border-b-gray-200', - bodyColumnClassName: - 'px-2 py-2 last:flex last:flex-row last:justify-end', - paginationClassName: 'hidden', - }} - emptyContent={ -
- Belum ada data pengiriman -
- } - /> -
- +
+ {data.map((item) => { + const doItem = marketing?.delivery_order?.find( + (doItem) => doItem.do_number === item.do_number + ); + return ( +
+ + + + + + + <> + + + + + {item.do_number && ( + + + + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {doItem && ( + + + + + )} + + +
+ Label + +
+
Value
+ {(formType === 'add_delivery' || + formType === 'edit_delivery' || + formType === 'detail') && ( +
+ + +
+ )} +
+
Tanggal Pengiriman + {item.delivery_date ? ( + formatDate(item.delivery_date, 'DD MMM YYYY') + ) : ( + Belum diisi + )} +
No. Pengiriman{item.do_number}
No. Polisi + {item.vehicle_number} +
Gudang + {item.marketing_product?.product_warehouse?.label} +
Produk + {item.marketing_product?.product_warehouse?.label} +
Qty + {item.qty + ? `${formatNumber(parseFloat(item.qty as string))} ${item.marketing_product?.uom ?? ''}` + : '-'} +
Avg Bobot + {item.avg_weight + ? formatNumber( + parseFloat(item.avg_weight as string) + ) + ' Kg' + : '-'} +
Total Bobot + {formatNumber(parseFloat(item.total_weight as string))} +
Total Harga Satuan + {formatCurrency(parseFloat(item.unit_price as string))} +
Total Penjualan + {formatCurrency(parseFloat(item.total_price as string))} +
+ Dokumen Pengiriman + + +
+
+ ); + })}
); diff --git a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx index 4e8247be..5ac9eede 100644 --- a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx @@ -1,16 +1,14 @@ 'use client'; import Button from '@/components/Button'; -import Table from '@/components/Table'; import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema'; import { - cn, formatCurrency, formatNumber, formatVechicleNumber, } from '@/lib/helper'; import { Icon } from '@iconify/react'; -import { useMemo, useRef, useState } from 'react'; +import { useMemo, useRef } from 'react'; import * as TanStack from '@tanstack/react-table'; import CheckboxInput from '@/components/input/CheckboxInput'; @@ -156,15 +154,51 @@ const SalesOrderProductTable = ({ style={{ borderRadius: '0.5rem', }} - className='border-none' + className='border-none w-full' > - + - + Label - Value +
+
Value
+ {formType !== 'success' && ( +
+ + +
+ )} +
<> @@ -178,48 +212,47 @@ const SalesOrderProductTable = ({ {item.product_warehouse?.label} - - Kategori - Telur - Produk {item.product_warehouse?.label} - - Tipe Konversi - Peti 15Kg - - - Total Peti - - {formatNumber(parseFloat(item.qty as string))} - - Total Bobot - {formatNumber(parseFloat(item.total_weight as string))} + {item.total_weight + ? formatNumber( + parseFloat(item.total_weight as string) + ) + ' Kg' + : '-'} - Total Butir Telur + Avg Bobot - {formatNumber(parseFloat(item.qty as string))} + {item.avg_weight + ? formatNumber(parseFloat(item.avg_weight as string)) + + ' Kg' + : '-'} + + + + Qty + + {`${formatNumber(parseFloat(item.qty as string))} ${item.uom || ''}`} Total Harga Satuan - {formatNumber(parseFloat(item.unit_price as string))} + {formatCurrency(parseFloat(item.unit_price as string))} Total Penjualan - {formatNumber(parseFloat(item.total_price as string))} + {formatCurrency(parseFloat(item.total_price as string))} @@ -233,7 +266,7 @@ const SalesOrderProductTable = ({ ) : null; @@ -87,11 +99,6 @@ const PDFDocument = ({ {/* Header Section */} - PT LUMBUNG TELUR INDONESIA SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. diff --git a/src/components/pages/marketing/pdf/SalesOrderExport.tsx b/src/components/pages/marketing/pdf/SalesOrderExport.tsx index 5d892853..5767c049 100644 --- a/src/components/pages/marketing/pdf/SalesOrderExport.tsx +++ b/src/components/pages/marketing/pdf/SalesOrderExport.tsx @@ -6,6 +6,7 @@ import { useMemo, useState } from 'react'; import { formatDate, formatNumber } from '@/lib/helper'; import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles'; import toast from 'react-hot-toast'; +import { useSearchParams } from 'next/navigation'; interface SalesOrderExportProps { data?: Marketing; @@ -15,6 +16,9 @@ interface SalesOrderExportProps { const SalesOrderExport = ({ data }: SalesOrderExportProps) => { const [isGeneratingPDF, setIsGeneratingPDF] = useState(false); const salesData = data; + const searchParams = useSearchParams(); + const action = searchParams.get('action'); + const id = searchParams.get('id'); const handleDownloadPDF = async () => { if (!salesData) { @@ -24,24 +28,32 @@ const SalesOrderExport = ({ data }: SalesOrderExportProps) => { setIsGeneratingPDF(true); try { const blob = await pdf().toBlob(); - const url = URL.createObjectURL(blob); + const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); + link.style.display = 'none'; link.href = url; link.download = `${salesData?.so_number || 'sales-order'}.pdf`; + link.setAttribute('target', '_blank'); + link.setAttribute('rel', 'noopener noreferrer'); document.body.appendChild(link); link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); + + // Delay cleanup to ensure download starts + setTimeout(() => { + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + }, 150); } catch (error) { toast.error('Failed to generate PDF. Please try again.'); } finally { setIsGeneratingPDF(false); + window.location.href = `/marketing?action=${action}&id=${id}`; } }; if (!salesData) { return ( -
+
No sales order data available
); @@ -49,12 +61,14 @@ const SalesOrderExport = ({ data }: SalesOrderExportProps) => { return salesData?.so_number && salesData.so_number !== 'Belum dibuat' ? ( ) : null; @@ -71,11 +85,6 @@ const PDFDocument = ({ data }: { data: Marketing }) => { {/* Header Section */} - PT LUMBUNG TELUR INDONESIA SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.