From 3fdb10ec7f381a049e4912e563e7deba388bbaed Mon Sep 17 00:00:00 2001 From: randy-ar Date: Fri, 14 Nov 2025 15:52:58 +0700 Subject: [PATCH] feat(FE-177): refactor sales order management with new schema and API integration --- .../sales-orders/detail/edit/page.tsx | 5 +- .../marketing/sales-orders/detail/page.tsx | 13 +- .../sales-orders/SalesOrderTable.tsx | 24 +++- .../sales-orders/detail/SalesOrderDetail.tsx | 71 ++++++---- .../sales-orders/form/SalesForm.schema.ts | 44 +++--- .../marketing/sales-orders/form/SalesForm.tsx | 131 ++++++++++++------ .../sales-orders/form/SalesFormRepeater.ts | 0 .../sales-orders/form/SalesFromRepeater.tsx | 0 .../form/repeater/MarketingProduct.schema.ts | 15 +- .../form/repeater/MarketingProductForm.tsx | 31 ++--- src/config/approval-line.ts | 15 ++ src/dummy/marketing.dummy.ts | 26 ++-- src/lib/helper.ts | 2 +- src/services/api/marketing/marketing.ts | 75 ++-------- src/types/api/marketing/marketing.d.ts | 50 ++++--- 15 files changed, 280 insertions(+), 222 deletions(-) delete mode 100644 src/components/pages/marketing/sales-orders/form/SalesFormRepeater.ts delete mode 100644 src/components/pages/marketing/sales-orders/form/SalesFromRepeater.tsx diff --git a/src/app/marketing/sales-orders/detail/edit/page.tsx b/src/app/marketing/sales-orders/detail/edit/page.tsx index 86cafcb6..660468f3 100644 --- a/src/app/marketing/sales-orders/detail/edit/page.tsx +++ b/src/app/marketing/sales-orders/detail/edit/page.tsx @@ -12,8 +12,9 @@ const EditSalesOrder = () => { const soId = searchParams.get('salesOrderId'); - const { data: marketing, isLoading: isLoading } = useSWR(soId, (id: number) => - MarketingApi.getSingle(id) + const { data: marketing, isLoading: isLoading } = useSWR( + `get-so-${soId}`, + () => MarketingApi.getSingle(soId ? parseInt(soId) : 0) ); if (!soId) { diff --git a/src/app/marketing/sales-orders/detail/page.tsx b/src/app/marketing/sales-orders/detail/page.tsx index 22d2651c..0ac71f56 100644 --- a/src/app/marketing/sales-orders/detail/page.tsx +++ b/src/app/marketing/sales-orders/detail/page.tsx @@ -12,9 +12,11 @@ const DetailSalesOrder = () => { const soId = searchParams.get('salesOrderId'); - const { data: marketing, isLoading: isLoading } = useSWR(soId, (id: number) => - MarketingApi.getSingle(id) - ); + const { + data: marketing, + isLoading: isLoading, + mutate: refreshMarketing, + } = useSWR(soId, (id: number) => MarketingApi.getSingle(id)); if (!soId) { router.back(); @@ -35,7 +37,10 @@ const DetailSalesOrder = () => {
{isLoading && } {!isLoading && isResponseSuccess(marketing) && ( - + )}
); diff --git a/src/components/pages/marketing/sales-orders/SalesOrderTable.tsx b/src/components/pages/marketing/sales-orders/SalesOrderTable.tsx index 1e8edaba..cb7a9649 100644 --- a/src/components/pages/marketing/sales-orders/SalesOrderTable.tsx +++ b/src/components/pages/marketing/sales-orders/SalesOrderTable.tsx @@ -12,7 +12,12 @@ import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector'; import { TableToolbar } from '@/components/table/TableToolbar'; import { ROWS_OPTIONS } from '@/config/constant'; import { isResponseSuccess } from '@/lib/api-helper'; -import { cn, formatCurrency, formatVechicleNumber } from '@/lib/helper'; +import { + cn, + formatCurrency, + formatDate, + formatVechicleNumber, +} from '@/lib/helper'; import { MarketingApi } from '@/services/api/marketing/marketing'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { Marketing, MarketingProduct } from '@/types/api/marketing/marketing'; @@ -216,12 +221,15 @@ const SalesOrderTable = () => { ), }, { - accessorKey: 'so_number', + accessorKey: 'name', header: 'No. Order', }, { accessorKey: 'so_date', header: 'Tanggal', + cell: (props) => { + return formatDate(props.row.original.so_date, 'DD MMM yyyy'); + }, }, { accessorKey: 'approval.step_name', @@ -232,8 +240,18 @@ const SalesOrderTable = () => { header: 'Customer', }, { - accessorKey: 'grand_total', + accessorFn: (row) => + row.marketing_products + ?.map((product) => product.total_price) + .reduce((a, b) => a + b, 0) ?? 0, header: 'Grand Total', + cell: (props) => { + return formatCurrency( + props.row.original?.marketing_products + ?.map((product) => product.total_price) + .reduce((a, b) => a + b, 0) ?? 0 + ); + }, }, { accessorKey: 'marketing_products.length', diff --git a/src/components/pages/marketing/sales-orders/detail/SalesOrderDetail.tsx b/src/components/pages/marketing/sales-orders/detail/SalesOrderDetail.tsx index e67a0644..e0262d9d 100644 --- a/src/components/pages/marketing/sales-orders/detail/SalesOrderDetail.tsx +++ b/src/components/pages/marketing/sales-orders/detail/SalesOrderDetail.tsx @@ -5,10 +5,15 @@ import Card from '@/components/Card'; import { FormHeader } from '@/components/helper/form/FormHeader'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; +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, formatVechicleNumber, } from '@/lib/helper'; @@ -20,27 +25,42 @@ import toast from 'react-hot-toast'; const SalesOrderDetail = ({ initialValues, - refreshValues, + refresh, }: { initialValues?: Marketing; - refreshValues?: () => void; + refresh?: () => void; }) => { - const [approvalAction, setApprovalAction] = useState<'approve' | 'reject'>( - 'approve' + const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>( + 'APPROVED' + ); + const [grandTotal, setGrandTotal] = useState( + initialValues?.marketing_products + ?.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?.approval, + approvalLines: MARKETING_APPROVAL_LINE, + moduleName: 'MARKETINGS', + moduleId: initialValues?.id as number as unknown as string, + }); const approveClickHandler = () => { - setApprovalAction('approve'); + setApprovalAction('APPROVED'); confirmationModal.openModal(); }; const rejectClickHandler = () => { - setApprovalAction('reject'); + setApprovalAction('REJECTED'); confirmationModal.openModal(); }; @@ -54,23 +74,24 @@ const SalesOrderDetail = ({ const confirmationModalDeleteClickHandler = async () => { setIsLoading(true); - // await MarketingApi.delete(initialValues?.id as number); + const res = await MarketingApi.delete(initialValues?.id as number); setIsLoading(false); deleteModal.closeModal(); - toast.success('Successfully deleted Sales Order!'); - refreshValues?.(); + toast.success(res?.message as string); + refresh?.(); }; const confirmationModalApproveClickHandler = async () => { setIsLoading(true); - // await MarketingApi.singleApproval( - // initialValues?.id as number, - // approvalAction - // ); + const res = await MarketingApi.singleApproval( + initialValues?.id as number, + approvalAction + ); setIsLoading(false); confirmationModal.closeModal(); - toast.success('Successfully approved Sales Order!'); - refreshValues?.(); + toast.success(res?.message as string); + refresh?.(); + refreshApproval?.(); }; const confirmationModalDeliveryClickHandler = async () => { @@ -79,7 +100,7 @@ const SalesOrderDetail = ({ setIsLoading(false); deliveryModal.closeModal(); toast.success('Successfully delivered Sales Order!'); - refreshValues?.(); + refresh?.(); }; return ( @@ -89,6 +110,9 @@ const SalesOrderDetail = ({ title='Detail Sales Order' backUrl='/marketing/sales-orders' /> + {!isLoadingApproval && approvals && ( + + )}
{initialValues?.approval?.step_number != 3 && ( <> @@ -117,6 +141,7 @@ const SalesOrderDetail = ({ )}
+ : - {initialValues?.so_number} + {initialValues?.name} Nama Pelanggan @@ -146,14 +171,12 @@ const SalesOrderDetail = ({ Tanggal Penjualan : - {initialValues?.so_date} + {formatDate(initialValues?.so_date, 'DD MMM yyyy')} Total Penjualan : - - {formatCurrency(initialValues?.grand_total as number)} - + {formatCurrency(grandTotal as number)} Catatan @@ -178,7 +201,7 @@ const SalesOrderDetail = ({ header: 'No. Polisi', accessorFn(row) { return formatVechicleNumber( - row.marketing_delivery_products?.vehicle_number as string + row.delivery_product?.vehicle_number as string ); }, }, @@ -275,14 +298,14 @@ const SalesOrderDetail = ({ /> = Yup.object({ - customer_id: Yup.number().required('Customer wajib diisi!'), - customer: Yup.object({ - value: Yup.number().required(), - label: Yup.string().required(), - }).nullable(), - so_date: Yup.string().required('Tanggal wajib diisi!'), - notes: Yup.string().required('Catatan wajib diisi!'), - marketing_products: Yup.array() - .of(MarketingProductSchema) - .min(1, 'Minimal harus ada 1 produk!') - .required('Produk wajib diisi!'), -}); +type SalesOrderSchemaType = MarketingSchemaType & { + marketing_products: SalesOrderProductFormValues[]; +}; -export const UpdateMarketingSchema = MarketingSchema; +export const SalesOrderSchema: Yup.ObjectSchema = + Yup.object({ + customer_id: Yup.number().required('Customer wajib diisi!'), + sales_person_id: Yup.number().required('Sales Person wajib diisi!'), + customer: Yup.object({ + value: Yup.number().required(), + label: Yup.string().required(), + }).nullable(), + so_date: Yup.string().required('Tanggal wajib diisi!'), + notes: Yup.string().required('Catatan wajib diisi!'), + marketing_products: Yup.array() + .of(SalesOrderProductSchema) + .min(1, 'Produk wajib diisi!') + .required('Produk wajib diisi!'), + }); -export type MarketingFormValues = Yup.InferType; +export const UpdateSalesOrderSchema = SalesOrderSchema; + +export type SalesOrderFormValues = Yup.InferType; diff --git a/src/components/pages/marketing/sales-orders/form/SalesForm.tsx b/src/components/pages/marketing/sales-orders/form/SalesForm.tsx index 2973f6ad..9286fe93 100644 --- a/src/components/pages/marketing/sales-orders/form/SalesForm.tsx +++ b/src/components/pages/marketing/sales-orders/form/SalesForm.tsx @@ -12,10 +12,10 @@ import TextArea from '@/components/input/TextArea'; import Modal, { useModal } from '@/components/Modal'; import * as TanStack from '@tanstack/react-table'; import Table from '@/components/Table'; // Keep this import -import { cn, formatCurrency, formatNumber } from '@/lib/helper'; +import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { - CreateMarketingPayload, - CreateMarketingProductPayload, + CreateSalesOrderPayload, + CreateSalesOrderProductPayload, Marketing, MarketingProduct, } from '@/types/api/marketing/marketing'; @@ -26,10 +26,10 @@ import CheckboxInput from '@/components/input/CheckboxInput'; import { Customer } from '@/types/api/master-data/customer'; import { CustomerApi } from '@/services/api/master-data'; import { useFormik } from 'formik'; -import { MarketingFormValues, MarketingSchema } from './SalesForm.schema'; +import { SalesOrderFormValues, SalesOrderSchema } from './SalesForm.schema'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { MarketingApi } from '@/services/api/marketing/marketing'; -import { MarketingProductFormValues } from './repeater/MarketingProduct.schema'; +import { SalesOrderProductFormValues } from './repeater/MarketingProduct.schema'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import toast from 'react-hot-toast'; import { useRouter } from 'next/navigation'; @@ -37,9 +37,11 @@ import { useRouter } from 'next/navigation'; const SalesForm = ({ formType = 'add', initialValues, + afterSubmit, }: { formType?: 'add' | 'edit'; initialValues?: Marketing; + afterSubmit?: () => void; }) => { const router = useRouter(); const addProductModal = useModal(); @@ -61,11 +63,9 @@ const SalesForm = ({ parseInt(item) ); const [grandTotal, setGrandTotal] = useState( - initialValues?.grand_total ?? 0 - ); - const marketingProducts = useMemo( - () => rawMarketingProducts, - [rawMarketingProducts] + initialValues?.marketing_products + ?.map((item) => item.total_price) + .reduce((a, b) => a + b, 0) ?? 0 ); const { @@ -91,8 +91,8 @@ const SalesForm = ({ const handleAddSubmitProduct = useCallback( async ( - tableValue: CreateMarketingProductPayload, - fieldValues: MarketingProductFormValues + tableValue: CreateSalesOrderProductPayload, + fieldValues: SalesOrderProductFormValues ) => { const newMarketingProduct: MarketingProduct = { id: rawMarketingProducts.length + 1, @@ -102,10 +102,10 @@ const SalesForm = ({ qty: tableValue.qty as number, avg_weight: tableValue.avg_weight as number, total_price: tableValue.total_price as number, - marketing_delivery_products: { + delivery_product: { id: rawMarketingProducts.length + 1, vehicle_number: tableValue.vehicle_number as string, - delivery_date: tableValue.delivery_date as string, + delivery_date: '' as string, unit_price: tableValue.unit_price as number, total_weight: tableValue.total_weight as number, qty: tableValue.qty as number, @@ -133,32 +133,30 @@ const SalesForm = ({ [selectedCustomer, setSelectedCustomer] ); - const createMarketingHandler = async (values: CreateMarketingPayload) => { + const createMarketingHandler = async (values: CreateSalesOrderPayload) => { console.log(values); const createMarketingRes = await MarketingApi.create(values); if (isResponseSuccess(createMarketingRes)) { - console.log(createMarketingRes); + toast.success(createMarketingRes?.message as string); + router.push('/marketing/sales-orders'); } if (isResponseError(createMarketingRes)) { - console.log(createMarketingRes); + toast.error(createMarketingRes?.message as string); } - toast.success('Successfully created Sales Order!'); - router.push('/marketing/sales-orders'); }; - const updateMarketingHandler = async (values: CreateMarketingPayload) => { + const updateMarketingHandler = async (values: CreateSalesOrderPayload) => { console.log(values); - const createMarketingRes = await MarketingApi.update( + const updateMarketingRes = await MarketingApi.update( initialValues?.id as number, values ); - if (isResponseSuccess(createMarketingRes)) { - console.log(createMarketingRes); + if (isResponseSuccess(updateMarketingRes)) { + toast.success(updateMarketingRes?.message as string); + router.push('/marketing/sales-orders'); } - if (isResponseError(createMarketingRes)) { - console.log(createMarketingRes); + if (isResponseError(updateMarketingRes)) { + toast.error(updateMarketingRes?.message as string); } - toast.success('Successfully updated Sales Order!'); - router.push('/marketing/sales-orders'); }; const deleteMarketingHandler = async () => { setIsLoading(true); @@ -180,9 +178,9 @@ const SalesForm = ({ const MarketingProductToFieldValues = ( product: MarketingProduct - ): MarketingProductFormValues => { + ): SalesOrderProductFormValues => { return { - vehicle_number: product.marketing_delivery_products?.vehicle_number, + vehicle_number: product.delivery_product?.vehicle_number, kandang_id: product.product_warehouse.warehouse.id, kandang: { value: product.product_warehouse.warehouse.id, @@ -196,19 +194,17 @@ const SalesForm = ({ unit_price: product.unit_price, total_weight: product.total_weight, qty: product.qty, - uom: product.product_warehouse?.product?.uom?.name, avg_weight: product.avg_weight, total_price: product.total_price, - delivery_date: product.marketing_delivery_products?.delivery_date, }; }; - const formik = useFormik({ - enableReinitialize: true, - initialValues: { + const formikInitialValues = useMemo(() => { + return { so_date: initialValues?.so_date || undefined, notes: initialValues?.notes || undefined, customer_id: initialValues?.customer?.id || undefined, + sales_person_id: initialValues?.sales_person_id || 1, customer: { value: initialValues?.customer?.id as number, label: initialValues?.customer?.name as string, @@ -217,15 +213,33 @@ const SalesForm = ({ initialValues?.marketing_products?.map((product) => MarketingProductToFieldValues(product) ) ?? [], - }, - validationSchema: MarketingSchema, + }; + }, [initialValues]); + + const formik = useFormik({ + enableReinitialize: true, + initialValues: formikInitialValues, + validationSchema: SalesOrderSchema, + validateOnMount: true, onSubmit: async (values) => { const payload = { customer_id: values.customer_id as number, - date: values.so_date as string, + sales_person_id: values.sales_person_id as number, + date: formatDate(values.so_date as string, 'yyyy-MM-DD'), notes: values.notes as string, - marketing_products: values.marketing_products, - } as CreateMarketingPayload; + marketing_products: values.marketing_products.map((product) => { + return { + vehicle_number: product.vehicle_number as string, + kandang_id: product.kandang_id as number, + product_warehouse_id: product.product_warehouse_id as number, + unit_price: parseFloat(product.unit_price as string), + total_weight: parseFloat(product.total_weight as string), + qty: parseFloat(product.qty as string), + avg_weight: parseFloat(product.avg_weight as string), + total_price: parseFloat(product.total_price as string), + } as CreateSalesOrderProductPayload; + }), + } as CreateSalesOrderPayload; switch (formType) { case 'add': createMarketingHandler(payload); @@ -236,6 +250,7 @@ const SalesForm = ({ default: break; } + afterSubmit?.(); }, }); @@ -245,6 +260,33 @@ const SalesForm = ({ formikSetValues(formik.initialValues); }, [formikSetValues, formik.initialValues]); + useEffect(() => { + // Konversi array MarketingProduct ke format SalesOrderProductFormValues + const newMarketingProductValues = rawMarketingProducts.map((product) => + MarketingProductToFieldValues(product) + ); + + // Hitung Grand Total baru + const newGrandTotal = rawMarketingProducts.reduce( + (total, product) => total + product.total_price, + 0 + ); + + // Perbarui nilai formik.values.marketing_products + formik.setFieldValue( + 'marketing_products', + newMarketingProductValues, + false + ); + // Parameter ketiga (false) untuk menghindari validasi secara langsung + + // Perbarui state grandTotal + setGrandTotal(newGrandTotal); + + // Reset row selection setiap kali daftar produk berubah (opsional, tapi disarankan) + setRowSelection({}); + }, [rawMarketingProducts]); + const columns = useMemo( () => [ { @@ -273,7 +315,7 @@ const SalesForm = ({ }, { accessorFn: (row: MarketingProduct) => - row.marketing_delivery_products?.vehicle_number, + row.delivery_product?.vehicle_number, header: 'No. Polisi', }, { @@ -321,7 +363,7 @@ const SalesForm = ({ ), }, ], - [handleDeleteProduct] // dependensi tunggal + [handleDeleteProduct, initialValues, rawMarketingProducts] // dependensi tunggal ); return ( @@ -372,10 +414,15 @@ const SalesForm = ({ wrapper: 'bg-white w-full', }} > + {JSON.stringify(formik.values.marketing_products)} +
+ {JSON.stringify(formik.values.marketing_products)} +
+ {JSON.stringify(formik.errors)} rowSelection={rowSelection} setRowSelection={setRowSelection} - data={marketingProducts} + data={rawMarketingProducts} columns={columns} className={{ tableWrapperClassName: 'overflow-x-auto min-h-full!', diff --git a/src/components/pages/marketing/sales-orders/form/SalesFormRepeater.ts b/src/components/pages/marketing/sales-orders/form/SalesFormRepeater.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/components/pages/marketing/sales-orders/form/SalesFromRepeater.tsx b/src/components/pages/marketing/sales-orders/form/SalesFromRepeater.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/src/components/pages/marketing/sales-orders/form/repeater/MarketingProduct.schema.ts b/src/components/pages/marketing/sales-orders/form/repeater/MarketingProduct.schema.ts index 58705af3..084eb4f8 100644 --- a/src/components/pages/marketing/sales-orders/form/repeater/MarketingProduct.schema.ts +++ b/src/components/pages/marketing/sales-orders/form/repeater/MarketingProduct.schema.ts @@ -15,13 +15,13 @@ type MarketingProductSchemaType = { unit_price: string | number | undefined; total_weight: string | number | undefined; qty: string | number | undefined; - uom: string | undefined | null; avg_weight: string | number | undefined; total_price: string | number | undefined; - delivery_date?: string | undefined | null; }; -export const MarketingProductSchema: Yup.ObjectSchema = +type SalesOrderProductSchemaType = MarketingProductSchemaType; + +const MarketingProductSchema: Yup.ObjectSchema = Yup.object({ vehicle_number: Yup.string().required('No. Polisi wajib diisi!'), kandang: Yup.object({ @@ -47,16 +47,17 @@ export const MarketingProductSchema: Yup.ObjectSchema = + MarketingProductSchema; + +export type SalesOrderProductFormValues = Yup.InferType< + typeof SalesOrderProductSchema >; diff --git a/src/components/pages/marketing/sales-orders/form/repeater/MarketingProductForm.tsx b/src/components/pages/marketing/sales-orders/form/repeater/MarketingProductForm.tsx index 0069a61e..2ce990dd 100644 --- a/src/components/pages/marketing/sales-orders/form/repeater/MarketingProductForm.tsx +++ b/src/components/pages/marketing/sales-orders/form/repeater/MarketingProductForm.tsx @@ -1,17 +1,15 @@ 'use client'; -import TextInput from '@/components/input/TextInput'; import { - CreateMarketingPayload, - CreateMarketingProductPayload, + CreateSalesOrderProductPayload, MarketingProduct, } from '@/types/api/marketing/marketing'; import { useFormik } from 'formik'; import { - MarketingProductFormValues, - MarketingProductSchema, + SalesOrderProductFormValues, + SalesOrderProductSchema, } from './MarketingProduct.schema'; -import { RefObject, use, useEffect, useRef, useState } from 'react'; +import { RefObject, useEffect, useState } from 'react'; import SelectInput, { OptionType, useSelect, @@ -36,8 +34,8 @@ const MarketingProductForm = ({ data: MarketingProduct[]; modalRef?: RefObject; onSubmitForm?: ( - tableValues: CreateMarketingProductPayload, - fieldValues: MarketingProductFormValues + tableValues: CreateSalesOrderProductPayload, + fieldValues: SalesOrderProductFormValues ) => Promise; }) => { // State @@ -96,11 +94,11 @@ const MarketingProductForm = ({ }; // Formik - const formik = useFormik({ + const formik = useFormik({ enableReinitialize: true, initialValues: { vehicle_number: - initialValues?.marketing_delivery_products?.vehicle_number || undefined, + initialValues?.delivery_product?.vehicle_number || undefined, kandang_id: initialValues?.product_warehouse.warehouse.id || undefined, kandang: { value: initialValues?.product_warehouse.warehouse.id as number, @@ -115,15 +113,10 @@ const MarketingProductForm = ({ unit_price: initialValues?.unit_price || undefined, total_weight: initialValues?.total_weight || undefined, qty: initialValues?.qty || undefined, - uom: initialValues?.product_warehouse?.product?.uom?.name || undefined, avg_weight: initialValues?.avg_weight || undefined, total_price: initialValues?.total_price || undefined, - delivery_date: - initialValues?.marketing_delivery_products?.delivery_date || - new Date().toDateString() || - undefined, }, - validationSchema: MarketingProductSchema, + validationSchema: SalesOrderProductSchema, onSubmit: async (values) => { setFormErrorMessage(''); if ( @@ -137,7 +130,7 @@ const MarketingProductForm = ({ (item: Kandang) => item.id === values.kandang_id ); - const marketingProduct: CreateMarketingProductPayload = { + const marketingProduct: CreateSalesOrderProductPayload = { id: initialValues?.id || undefined, vehicle_number: formatVechicleNumber(values.vehicle_number as string), kandang_id: values.kandang_id as number, @@ -147,10 +140,8 @@ const MarketingProductForm = ({ unit_price: values.unit_price as number, total_weight: values.total_weight as number, qty: values.qty as number, - uom: values.uom as string, avg_weight: values.avg_weight as number, total_price: values.total_price as number, - delivery_date: values.delivery_date as string, }; onSubmitForm?.(marketingProduct, values); @@ -178,10 +169,8 @@ const MarketingProductForm = ({ unit_price: '', total_weight: '', qty: '', - uom: '', avg_weight: '', total_price: '', - delivery_date: new Date().toDateString(), }, }); }; diff --git a/src/config/approval-line.ts b/src/config/approval-line.ts index 52e4290f..997d473f 100644 --- a/src/config/approval-line.ts +++ b/src/config/approval-line.ts @@ -32,3 +32,18 @@ export const TRANSFER_TO_LAYING_APPROVAL_LINE: ApprovalLine = [ step_name: 'Disetujui', }, ] as const; + +export const MARKETING_APPROVAL_LINE: ApprovalLine = [ + { + step_number: 1, + step_name: 'Pengajuan', + }, + { + step_number: 2, + step_name: 'Sales Order', + }, + { + step_number: 3, + step_name: 'Delivery Order', + }, +]; diff --git a/src/dummy/marketing.dummy.ts b/src/dummy/marketing.dummy.ts index 7cbf4317..1d6dde87 100644 --- a/src/dummy/marketing.dummy.ts +++ b/src/dummy/marketing.dummy.ts @@ -176,8 +176,7 @@ export const dummyMarketings: Marketing[] = [ { id: 1, status: 'APPROVED', - so_number: 'SO-001-2025', - so_docs: 'https://example.com/docs/so001.pdf', + name: 'SO-001-2025', so_date: format(new Date(), 'yyyy-MM-dd'), customer: { id: 1, @@ -193,9 +192,8 @@ export const dummyMarketings: Marketing[] = [ created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'), updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'), }, - sales_person: createdUser, + sales_person_id: createdUser.id, notes: 'Pengiriman awal bulan.', - grand_total: 7500000, approval: { step_number: 1, step_name: 'Pengajuan Order', @@ -212,7 +210,7 @@ export const dummyMarketings: Marketing[] = [ total_weight: 250, total_price: 7500000, product_warehouse: dummyProductWarehouses[0], - marketing_delivery_products: { + delivery_product: { id: 1, qty: 100, unit_price: 75000, @@ -233,8 +231,7 @@ export const dummyMarketings: Marketing[] = [ { id: 2, status: 'APPROVED', - so_number: 'SO-002-2025', - so_docs: 'https://example.com/docs/so002.pdf', + name: 'SO-002-2025', so_date: format(new Date(), 'yyyy-MM-dd'), customer: { id: 2, @@ -250,9 +247,8 @@ export const dummyMarketings: Marketing[] = [ created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'), updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'), }, - sales_person: createdUser, + sales_person_id: createdUser.id, notes: 'Pesanan kedua untuk stok akhir tahun.', - grand_total: 3750000, approval: { step_number: 2, step_name: 'Sales Order', @@ -269,7 +265,7 @@ export const dummyMarketings: Marketing[] = [ total_weight: 125, total_price: 3750000, product_warehouse: dummyProductWarehouses[1], - marketing_delivery_products: { + delivery_product: { id: 2, qty: 50, unit_price: 75000, @@ -290,8 +286,7 @@ export const dummyMarketings: Marketing[] = [ { id: 3, status: 'APPROVED', - so_number: 'SO-003-2025', - so_docs: 'https://example.com/docs/so003.pdf', + name: 'SO-003-2025', so_date: format(new Date(), 'yyyy-MM-dd'), customer: { id: 3, @@ -307,9 +302,8 @@ export const dummyMarketings: Marketing[] = [ created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'), updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'), }, - sales_person: createdUser, + sales_person_id: createdUser.id, notes: 'Order untuk pengiriman ke luar kota.', - grand_total: 5600000, approval: { step_number: 3, step_name: 'Delivery Order', @@ -326,7 +320,7 @@ export const dummyMarketings: Marketing[] = [ total_weight: 192, total_price: 5600000, product_warehouse: dummyProductWarehouses[0], - marketing_delivery_products: { + delivery_product: { id: 3, qty: 80, unit_price: 70000, @@ -345,7 +339,7 @@ export const dummyMarketings: Marketing[] = [ total_weight: 192, total_price: 5600000, product_warehouse: dummyProductWarehouses[0], - marketing_delivery_products: { + delivery_product: { id: 3, qty: 80, unit_price: 70000, diff --git a/src/lib/helper.ts b/src/lib/helper.ts index 777fa6dd..d210c646 100644 --- a/src/lib/helper.ts +++ b/src/lib/helper.ts @@ -32,7 +32,7 @@ export const formatNumber = ( export function formatVechicleNumber(value: string): string { let result = ''; - for (let i = 0; i < value.length; i++) { + for (let i = 0; i < (value?.length ?? 0); i++) { const curr = value[i]; const prev = value[i - 1]; diff --git a/src/services/api/marketing/marketing.ts b/src/services/api/marketing/marketing.ts index a5c3b8fb..48435716 100644 --- a/src/services/api/marketing/marketing.ts +++ b/src/services/api/marketing/marketing.ts @@ -5,82 +5,25 @@ import { httpClient } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; import { Marketing, - CreateMarketingPayload, - UpdateMarketingPayload, + CreateSalesOrderPayload, + UpdateSalesOrderPayload, } from '@/types/api/marketing/marketing'; export class MarketingService extends BaseApiService< Marketing, - CreateMarketingPayload, - UpdateMarketingPayload + CreateSalesOrderPayload, + UpdateSalesOrderPayload > { constructor(basePath: string = '/marketing') { super(basePath); } - /** - * Override: Get all marketing data (dummy mode) - */ - override async getAllFetcher( - endpoint: string - ): Promise> { - // simulasi loading - await sleep(750); - - // data dummy sementara - const DUMMY_MARKETING_DATA: BaseApiResponse = { - code: 200, - status: 'success', - message: 'Berhasil mengambil data marketing (dummy)', - data: dummyMarketings, - }; - - return DUMMY_MARKETING_DATA; - } - - /** - * Override: Get single marketing data (dummy mode) - */ - override async getSingle( - id: number - ): Promise | undefined> { - // simulasi delay - await new Promise((res) => setTimeout(res, 500)); - - const marketing = dummyMarketings.find((marketing) => { - console.log('marketing', marketing); - console.log('id-m', marketing.id); - console.log('id-p', id); - console.log('id', marketing.id == id); - return marketing.id == id; - }); - console.log('marketings', dummyMarketings); - console.log('marketing', marketing); - - if (marketing) { - // misalnya fetch dari dummy - return { - code: 200, - status: 'success', - message: 'Data marketing berhasil diambil.', - data: marketing, - }; - } else { - // jika tidak ditemukan - throw { - code: 404, - status: 'error', - message: 'Data marketing tidak ditemukan.', - }; - } - } - /** * Approve single marketing data */ async singleApproval( id: number, - action: 'approve' | 'reject' + action: 'APPROVED' | 'REJECTED' ): Promise | undefined> { try { const path = `${this.basePath}/approvals`; @@ -88,7 +31,7 @@ export class MarketingService extends BaseApiService< method: 'POST', body: { action: action, - approval_ids: [id], + approvable_ids: [id], notes: `${action} marketing ${id}`, }, }); @@ -103,7 +46,7 @@ export class MarketingService extends BaseApiService< */ async bulkApprovals( ids: number[], - action: 'approve' | 'reject' + action: 'APPROVED' | 'REJECTED' ): Promise | undefined> { try { const path = `${this.basePath}/approvals`; @@ -111,7 +54,7 @@ export class MarketingService extends BaseApiService< method: 'POST', body: { action: action, - approval_ids: ids, + approvable_ids: ids, notes: `${action} marketing ${ids.join(', ')}`, }, }); @@ -122,4 +65,4 @@ export class MarketingService extends BaseApiService< } } -export const MarketingApi = new MarketingService('/marketing'); +export const MarketingApi = new MarketingService('/marketing/sales-orders'); diff --git a/src/types/api/marketing/marketing.d.ts b/src/types/api/marketing/marketing.d.ts index 331d95d3..aaa0bdf0 100644 --- a/src/types/api/marketing/marketing.d.ts +++ b/src/types/api/marketing/marketing.d.ts @@ -7,16 +7,17 @@ import { import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; import { Kandang } from '@/types/api/master-data/kandang'; +/** + * Base Data Response + */ export type BaseMarketing = { id: number; status?: string; - so_number: string; + name: string; customer: Customer; - so_docs: string; so_date: string; - sales_person: CreatedUser; + sales_person_id: number; notes: string; - grand_total: number; approval: BaseApproval; marketing_products?: MarketingProduct[]; }; @@ -29,7 +30,7 @@ export type MarketingProduct = { total_weight: number; total_price: number; product_warehouse: ProductWarehouse; - marketing_delivery_products?: MarketingDeliveryProducts; + delivery_product?: MarketingDeliveryProducts; }; export type MarketingDeliveryProducts = { @@ -39,34 +40,49 @@ export type MarketingDeliveryProducts = { avg_weight: number; total_weight: number; total_price: number; - delivery_date: string; + delivery_date: string | null; vehicle_number: string; - do_number?: string | undefined; + do_number?: string | undefined; // Uncertain }; export type Marketing = BaseMetadata & BaseMarketing; -export type CreateMarketingPayload = { +/** + * Base Data Payload + */ +export type BaseCreateMarketingPayload = { customer_id: number; + sales_person_id: number; date: string; notes: string; - marketing_products: CreateMarketingProductPayload[]; }; -export type UpdateMarketingPayload = CreateMarketingPayload; -export type CreateMarketingProductPayload = { - id?: number; +export type BaseCreateMarketingProductPayload = { vehicle_number: string; kandang_id: string | number | undefined; - kandang: Kandang | undefined; product_warehouse_id: string | number | undefined; - product_warehouse: ProductWarehouse | undefined; unit_price: string | number | undefined; total_weight: string | number | undefined; qty: string | number | undefined; - uom: string | undefined; avg_weight: string | number | undefined; total_price: string | number | undefined; - delivery_date?: string | null; }; -export type UpdateMarketingProductPayload = CreateMarketingProductPayload; + +/** + * Payload Data Types Sales Order + */ + +export type CreateSalesOrderPayload = BaseCreateMarketingPayload & { + marketing_products: CreateSalesOrderProductPayload[]; +}; + +export type CreateSalesOrderProductPayload = + BaseCreateMarketingProductPayload & { + id?: number; + kandang?: Kandang | undefined; + product_warehouse?: ProductWarehouse | undefined; + }; + +export type UpdateSalesOrderProductPayload = CreateSalesOrderProductPayload; + +export type UpdateSalesOrderPayload = CreateSalesOrderPayload;