From d3c4706d87b62cf5136288cae2bbcaf76b457624 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Sun, 16 Nov 2025 23:19:28 +0700 Subject: [PATCH] refactor(FE-177-166-167): separate table repeater component and adjust data types with new API Payload --- src/app/marketing/sales-orders/add/page.tsx | 2 +- .../sales-orders/detail/edit/page.tsx | 2 +- .../marketing/sales-orders/detail/page.tsx | 2 +- src/app/marketing/sales-orders/page.tsx | 2 +- ...SalesOrderTable.tsx => MarketingTable.tsx} | 27 +- .../MarketingDetail.tsx} | 42 +-- .../MarketingForm.schema.ts} | 7 +- .../SalesForm.tsx => form/MarketingForm.tsx} | 298 ++++----------- .../DeliverOrderProduct.schema.ts | 0 .../delivery-order/DeliverOrderProduct.tsx | 0 .../sales-order/SalesOrderProduct.schema.ts} | 13 +- .../sales-order/SalesOrderProductForm.tsx} | 59 +-- .../table-view/SalesOrderProductTable.tsx | 200 ++++++++++ src/dummy/marketing.dummy.ts | 349 ++++++++++-------- src/services/api/marketing/marketing.ts | 46 +++ src/types/api/marketing/marketing.d.ts | 40 +- 16 files changed, 593 insertions(+), 496 deletions(-) rename src/components/pages/marketing/{sales-orders/SalesOrderTable.tsx => MarketingTable.tsx} (94%) rename src/components/pages/marketing/{sales-orders/detail/SalesOrderDetail.tsx => detail/MarketingDetail.tsx} (89%) rename src/components/pages/marketing/{sales-orders/form/SalesForm.schema.ts => form/MarketingForm.schema.ts} (84%) rename src/components/pages/marketing/{sales-orders/form/SalesForm.tsx => form/MarketingForm.tsx} (57%) create mode 100644 src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema.ts create mode 100644 src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx rename src/components/pages/marketing/{sales-orders/form/repeater/MarketingProduct.schema.ts => form/repeater/sales-order/SalesOrderProduct.schema.ts} (82%) rename src/components/pages/marketing/{sales-orders/form/repeater/MarketingProductForm.tsx => form/repeater/sales-order/SalesOrderProductForm.tsx} (84%) create mode 100644 src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx diff --git a/src/app/marketing/sales-orders/add/page.tsx b/src/app/marketing/sales-orders/add/page.tsx index e60085ef..8d64e66d 100644 --- a/src/app/marketing/sales-orders/add/page.tsx +++ b/src/app/marketing/sales-orders/add/page.tsx @@ -1,4 +1,4 @@ -import SalesForm from '@/components/pages/marketing/sales-orders/form/SalesForm'; +import SalesForm from '@/components/pages/marketing/form/MarketingForm'; const AddSalesOrder = () => { return ( diff --git a/src/app/marketing/sales-orders/detail/edit/page.tsx b/src/app/marketing/sales-orders/detail/edit/page.tsx index 660468f3..2b41f144 100644 --- a/src/app/marketing/sales-orders/detail/edit/page.tsx +++ b/src/app/marketing/sales-orders/detail/edit/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import SalesForm from '@/components/pages/marketing/sales-orders/form/SalesForm'; +import SalesForm from '@/components/pages/marketing/form/MarketingForm'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { MarketingApi } from '@/services/api/marketing/marketing'; import { useRouter, useSearchParams } from 'next/navigation'; diff --git a/src/app/marketing/sales-orders/detail/page.tsx b/src/app/marketing/sales-orders/detail/page.tsx index 0ac71f56..ca08f41b 100644 --- a/src/app/marketing/sales-orders/detail/page.tsx +++ b/src/app/marketing/sales-orders/detail/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import SalesOrderDetail from '@/components/pages/marketing/sales-orders/detail/SalesOrderDetail'; +import SalesOrderDetail from '@/components/pages/marketing/detail/MarketingDetail'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { MarketingApi } from '@/services/api/marketing/marketing'; import { useRouter, useSearchParams } from 'next/navigation'; diff --git a/src/app/marketing/sales-orders/page.tsx b/src/app/marketing/sales-orders/page.tsx index 3494b6a1..427db57d 100644 --- a/src/app/marketing/sales-orders/page.tsx +++ b/src/app/marketing/sales-orders/page.tsx @@ -1,4 +1,4 @@ -import SalesOrderTable from '@/components/pages/marketing/sales-orders/SalesOrderTable'; +import SalesOrderTable from '@/components/pages/marketing/MarketingTable'; const SalesOrder = () => { return ( diff --git a/src/components/pages/marketing/sales-orders/SalesOrderTable.tsx b/src/components/pages/marketing/MarketingTable.tsx similarity index 94% rename from src/components/pages/marketing/sales-orders/SalesOrderTable.tsx rename to src/components/pages/marketing/MarketingTable.tsx index cb7a9649..2acffe7c 100644 --- a/src/components/pages/marketing/sales-orders/SalesOrderTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -12,16 +12,10 @@ 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, - formatDate, - formatVechicleNumber, -} from '@/lib/helper'; +import { cn, formatCurrency, formatDate } from '@/lib/helper'; import { MarketingApi } from '@/services/api/marketing/marketing'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { Marketing, MarketingProduct } from '@/types/api/marketing/marketing'; -import { Customer } from '@/types/api/master-data/customer'; +import { BaseSalesOrder, Marketing } from '@/types/api/marketing/marketing'; import { Icon } from '@iconify/react'; import { CellContext } from '@tanstack/react-table'; import { useCallback, useState } from 'react'; @@ -241,13 +235,13 @@ const SalesOrderTable = () => { }, { accessorFn: (row) => - row.marketing_products + 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?.marketing_products + props.row.original?.sales_order ?.map((product) => product.total_price) .reduce((a, b) => a + b, 0) ?? 0 ); @@ -257,8 +251,8 @@ const SalesOrderTable = () => { accessorKey: 'marketing_products.length', header: 'Product Details', cell: (props) => { - if (props?.row?.original?.marketing_products?.length) { - if (props?.row?.original?.marketing_products?.length > 1) { + if (props?.row?.original?.sales_order?.length) { + if (props?.row?.original?.sales_order?.length > 1) { return ( ); } else { - const product = props?.row?.original?.marketing_products[0]; + const product = props?.row?.original?.sales_order[0]; return <>{product?.product_warehouse?.product?.name}; } } @@ -379,10 +372,10 @@ const SalesOrderTable = () => { - + data={ isResponseSuccess(marketing) && selectedItem - ? (selectedItem?.marketing_products ?? []) + ? (selectedItem?.sales_order ?? []) : [] } columns={[ diff --git a/src/components/pages/marketing/sales-orders/detail/SalesOrderDetail.tsx b/src/components/pages/marketing/detail/MarketingDetail.tsx similarity index 89% rename from src/components/pages/marketing/sales-orders/detail/SalesOrderDetail.tsx rename to src/components/pages/marketing/detail/MarketingDetail.tsx index e0262d9d..feca11f9 100644 --- a/src/components/pages/marketing/sales-orders/detail/SalesOrderDetail.tsx +++ b/src/components/pages/marketing/detail/MarketingDetail.tsx @@ -10,15 +10,9 @@ import ApprovalSteps, { } 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'; +import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { MarketingApi } from '@/services/api/marketing/marketing'; -import { Marketing, MarketingProduct } from '@/types/api/marketing/marketing'; +import { BaseSalesOrder, Marketing } from '@/types/api/marketing/marketing'; import { Icon } from '@iconify/react'; import { useState } from 'react'; import toast from 'react-hot-toast'; @@ -34,7 +28,7 @@ const SalesOrderDetail = ({ 'APPROVED' ); const [grandTotal, setGrandTotal] = useState( - initialValues?.marketing_products + initialValues?.sales_order ?.map((item) => item.total_price) .reduce((a, b) => a + b, 0) ); @@ -48,7 +42,7 @@ const SalesOrderDetail = ({ isLoading: isLoadingApproval, refresh: refreshApproval, } = useApprovalSteps({ - latestApproval: initialValues?.approval, + latestApproval: initialValues?.latest_approval, approvalLines: MARKETING_APPROVAL_LINE, moduleName: 'MARKETINGS', moduleId: initialValues?.id as number as unknown as string, @@ -114,12 +108,12 @@ const SalesOrderDetail = ({ )}
- {initialValues?.approval?.step_number != 3 && ( + {initialValues?.latest_approval?.step_number != 3 && ( <> )} - {initialValues?.approval?.step_number == 2 && ( + {initialValues?.latest_approval?.step_number == 2 && (
- {initialValues?.marketing_products && ( + {initialValues?.sales_order && ( - - data={initialValues?.marketing_products} + + data={initialValues?.sales_order} columns={[ - { - header: 'No. Polisi', - accessorFn(row) { - return formatVechicleNumber( - row.delivery_product?.vehicle_number as string - ); - }, - }, { header: 'Kandang', accessorFn(row) { @@ -251,8 +237,8 @@ const SalesOrderDetail = ({ className={{ containerClassName: cn({ 'mb-20': - initialValues?.marketing_products && - initialValues?.marketing_products?.length === 0, + 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!', diff --git a/src/components/pages/marketing/sales-orders/form/SalesForm.schema.ts b/src/components/pages/marketing/form/MarketingForm.schema.ts similarity index 84% rename from src/components/pages/marketing/sales-orders/form/SalesForm.schema.ts rename to src/components/pages/marketing/form/MarketingForm.schema.ts index 6cd4a3be..4d03faae 100644 --- a/src/components/pages/marketing/sales-orders/form/SalesForm.schema.ts +++ b/src/components/pages/marketing/form/MarketingForm.schema.ts @@ -1,9 +1,8 @@ import * as Yup from 'yup'; -import { MarketingProduct } from '@/types/api/marketing/marketing'; import { SalesOrderProductFormValues, SalesOrderProductSchema, -} from './repeater/MarketingProduct.schema'; +} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema'; type MarketingSchemaType = { customer_id: number | undefined; @@ -20,7 +19,7 @@ type MarketingSchemaType = { }; type SalesOrderSchemaType = MarketingSchemaType & { - marketing_products: SalesOrderProductFormValues[]; + sales_order: SalesOrderProductFormValues[]; }; export const SalesOrderSchema: Yup.ObjectSchema = @@ -33,7 +32,7 @@ export const SalesOrderSchema: Yup.ObjectSchema = }).nullable(), so_date: Yup.string().required('Tanggal wajib diisi!'), notes: Yup.string().required('Catatan wajib diisi!'), - marketing_products: Yup.array() + sales_order: Yup.array() .of(SalesOrderProductSchema) .min(1, 'Produk wajib diisi!') .required('Produk wajib diisi!'), diff --git a/src/components/pages/marketing/sales-orders/form/SalesForm.tsx b/src/components/pages/marketing/form/MarketingForm.tsx similarity index 57% rename from src/components/pages/marketing/sales-orders/form/SalesForm.tsx rename to src/components/pages/marketing/form/MarketingForm.tsx index 9286fe93..66f7b815 100644 --- a/src/components/pages/marketing/sales-orders/form/SalesForm.tsx +++ b/src/components/pages/marketing/form/MarketingForm.tsx @@ -10,36 +10,56 @@ import SelectInput, { } from '@/components/input/SelectInput'; 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, formatDate, formatNumber } from '@/lib/helper'; +import { formatCurrency, formatDate } from '@/lib/helper'; import { + BaseSalesOrder, CreateSalesOrderPayload, CreateSalesOrderProductPayload, Marketing, - MarketingProduct, } from '@/types/api/marketing/marketing'; import { Icon } from '@iconify/react'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import MarketingProductForm from './repeater/MarketingProductForm'; -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 { SalesOrderFormValues, SalesOrderSchema } from './SalesForm.schema'; +import { SalesOrderFormValues, SalesOrderSchema } from './MarketingForm.schema'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { MarketingApi } from '@/services/api/marketing/marketing'; -import { SalesOrderProductFormValues } from './repeater/MarketingProduct.schema'; +import { SalesOrderProductFormValues } from './repeater/sales-order/SalesOrderProduct.schema'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import toast from 'react-hot-toast'; import { useRouter } from 'next/navigation'; +import SalesOrderProductTable from './table-view/SalesOrderProductTable'; +import SalesOrderProductForm from './repeater/sales-order/SalesOrderProductForm'; + +const MarketingProductToFieldValues = ( + product: BaseSalesOrder +): SalesOrderProductFormValues => { + return { + 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 SalesForm = ({ formType = 'add', initialValues, afterSubmit, }: { - formType?: 'add' | 'edit'; + formType?: 'add' | 'edit' | 'deliver'; initialValues?: Marketing; afterSubmit?: () => void; }) => { @@ -49,10 +69,14 @@ const SalesForm = ({ const [isLoading, setIsLoading] = useState(false); const [selectedMarketingProduct, setSelectedMarketingProduct] = - useState(null); + useState(null); const [rawMarketingProducts, setRawMarketingProducts] = useState< - MarketingProduct[] - >(initialValues?.marketing_products || []); + SalesOrderProductFormValues[] + >( + initialValues?.sales_order.map((item) => + MarketingProductToFieldValues(item) + ) || [] + ); const [selectedCustomer, setSelectedCustomer] = useState( initialValues?.customer ? { value: initialValues.customer.id, label: initialValues.customer.name } @@ -63,63 +87,54 @@ const SalesForm = ({ parseInt(item) ); const [grandTotal, setGrandTotal] = useState( - initialValues?.marketing_products + initialValues?.sales_order ?.map((item) => item.total_price) .reduce((a, b) => a + b, 0) ?? 0 ); const { options: customerOptions, - rawData: customerRawData, isLoadingOptions: isLoadingCustomerOptions, } = useSelect(CustomerApi.basePath, 'id', 'name'); - const handleAddProduct = useCallback(() => { - addProductModal.openModal(); - }, [addProductModal]); - const handleDeleteProduct = useCallback((id: number) => { - setRawMarketingProducts((prev) => prev.filter((p) => p.id !== id)); - }, []); + const handleDeleteProduct = useCallback( + (product_warehouse_id: number, kandang_id: number) => { + setRawMarketingProducts((prev) => + prev.filter( + (p) => + p.product_warehouse_id !== product_warehouse_id && + p.kandang_id !== kandang_id + ) + ); + }, + [] + ); const handleBulkDeleteProduct = () => { setRawMarketingProducts((prev) => - prev.filter((product) => !selectedRowIds.includes(product.id)) + prev.filter( + (product) => + !selectedRowIds.includes( + parseInt(`${product.product_warehouse_id}${product.kandang_id}`) + ) + ) ); }; const handleDelete = () => { deleteModal.openModal(); }; + const handleAddProductClick = useCallback(() => { + setSelectedMarketingProduct(null); // Pastikan form tambah + addProductModal.openModal(); + }, [addProductModal]); const handleAddSubmitProduct = useCallback( - async ( - tableValue: CreateSalesOrderProductPayload, - fieldValues: SalesOrderProductFormValues - ) => { - const newMarketingProduct: MarketingProduct = { - id: rawMarketingProducts.length + 1, - product_warehouse: tableValue.product_warehouse!, - unit_price: tableValue.unit_price as number, - total_weight: tableValue.total_weight as number, - qty: tableValue.qty as number, - avg_weight: tableValue.avg_weight as number, - total_price: tableValue.total_price as number, - delivery_product: { - id: rawMarketingProducts.length + 1, - vehicle_number: tableValue.vehicle_number 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, - avg_weight: tableValue.avg_weight as number, - total_price: tableValue.total_price as number, - }, - }; - - setRawMarketingProducts((prev) => [...prev, newMarketingProduct]); + async (values: SalesOrderProductFormValues) => { + setRawMarketingProducts((prev) => [...prev, values]); formik.setValues({ ...formik.values, - marketing_products: [...formik.values.marketing_products, fieldValues], + sales_order: [...formik.values.sales_order, values], }); - setGrandTotal((prev) => prev + (tableValue.total_price as number)); + setGrandTotal((prev) => prev + (values.total_price as number)); addProductModal.closeModal(); }, [rawMarketingProducts.length, addProductModal] @@ -176,41 +191,18 @@ const SalesForm = ({ router.push('/marketing/sales-orders'); }; - const MarketingProductToFieldValues = ( - product: MarketingProduct - ): SalesOrderProductFormValues => { - return { - vehicle_number: product.delivery_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.product.id, - label: product.product_warehouse.product.name, - }, - product_warehouse_id: product.product_warehouse.product.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 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, + sales_person_id: initialValues?.sales_person?.id || 1, customer: { value: initialValues?.customer?.id as number, label: initialValues?.customer?.name as string, }, - marketing_products: - initialValues?.marketing_products?.map((product) => + sales_order: + initialValues?.sales_order?.map((product) => MarketingProductToFieldValues(product) ) ?? [], }; @@ -227,9 +219,9 @@ const SalesForm = ({ 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.map((product) => { + marketing_products: values.sales_order.map((product) => { return { - vehicle_number: product.vehicle_number as string, + vehicle_number: 'D 1234 XXXX', kandang_id: product.kandang_id as number, product_warehouse_id: product.product_warehouse_id as number, unit_price: parseFloat(product.unit_price as string), @@ -261,23 +253,14 @@ const SalesForm = ({ }, [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, + (total, product) => total + parseFloat(product.total_price as string), 0 ); // Perbarui nilai formik.values.marketing_products - formik.setFieldValue( - 'marketing_products', - newMarketingProductValues, - false - ); + formik.setFieldValue('marketing_products', rawMarketingProducts, false); // Parameter ketiga (false) untuk menghindari validasi secara langsung // Perbarui state grandTotal @@ -287,85 +270,6 @@ const SalesForm = ({ setRowSelection({}); }, [rawMarketingProducts]); - const columns = useMemo( - () => [ - { - id: 'select', - header: ({ table }: { table: TanStack.Table }) => ( -
- -
- ), - cell: ({ row }: { row: TanStack.Row }) => ( -
- -
- ), - }, - { - accessorFn: (row: MarketingProduct) => - row.delivery_product?.vehicle_number, - header: 'No. Polisi', - }, - { - accessorFn: (row: MarketingProduct) => - row.product_warehouse.warehouse.name, - header: 'Kandang', - }, - { - accessorFn: (row: MarketingProduct) => - row.product_warehouse.product.name, - header: 'Produk', - }, - { - accessorFn: (row: MarketingProduct) => formatCurrency(row.unit_price), - header: 'Harga Satuan (Rp)', - }, - { - accessorFn: (row: MarketingProduct) => formatNumber(row.total_weight), - header: 'Total Bobot (Kg)', - }, - { - accessorFn: (row: MarketingProduct) => formatNumber(row.qty), - header: 'Kuantitas', - }, - { - accessorFn: (row: MarketingProduct) => formatNumber(row.avg_weight), - header: 'Avg. Bobot (Kg)', - }, - { - accessorFn: (row: MarketingProduct) => formatCurrency(row.total_price), - header: 'Total Penjualan (Rp)', - }, - { - header: 'Aksi', - cell: (props: TanStack.CellContext) => ( -
- -
- ), - }, - ], - [handleDeleteProduct, initialValues, rawMarketingProducts] // dependensi tunggal - ); - return ( <>
- {JSON.stringify(formik.values.marketing_products)} + {JSON.stringify(formik.values.sales_order)}
- {JSON.stringify(formik.values.marketing_products)} + {JSON.stringify(formik.values.sales_order)}
{JSON.stringify(formik.errors)} - - rowSelection={rowSelection} - setRowSelection={setRowSelection} + - Belum ada data penjualan - - } + onDelete={handleDeleteProduct} + onBulkDelete={handleBulkDeleteProduct} + onAddProductClick={handleAddProductClick} /> -
- - {selectedRowIds.length > 0 && ( - - )} -