From fcc2fced06765b6124ab4d100404e9619b9f407a Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 6 Nov 2025 16:57:17 +0700 Subject: [PATCH] feat(FE-166-167-168): slicing ui create, edit dan detail sales order --- .../sales-orders/detail/edit/page.tsx | 38 ++- .../marketing/sales-orders/detail/page.tsx | 39 ++- .../sales-orders/SalesOrderTable.tsx | 63 +--- .../sales-orders/detail/SalesOrderDetail.tsx | 258 ++++++++++++++++ .../sales-orders/form/SalesForm.schema.ts | 11 + .../marketing/sales-orders/form/SalesForm.tsx | 75 ++--- .../form/repeater/MarketingProductForm.tsx | 7 +- src/dummy/marketing.dummy.ts | 285 ++++++++++++++++++ src/services/api/marketing.ts | 6 - src/services/api/marketing/marketing.ts | 113 +++++++ 10 files changed, 785 insertions(+), 110 deletions(-) create mode 100644 src/components/pages/marketing/sales-orders/detail/SalesOrderDetail.tsx create mode 100644 src/dummy/marketing.dummy.ts delete mode 100644 src/services/api/marketing.ts create mode 100644 src/services/api/marketing/marketing.ts diff --git a/src/app/marketing/sales-orders/detail/edit/page.tsx b/src/app/marketing/sales-orders/detail/edit/page.tsx index cc0722ea..86cafcb6 100644 --- a/src/app/marketing/sales-orders/detail/edit/page.tsx +++ b/src/app/marketing/sales-orders/detail/edit/page.tsx @@ -1,7 +1,41 @@ +'use client'; + +import SalesForm from '@/components/pages/marketing/sales-orders/form/SalesForm'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { MarketingApi } from '@/services/api/marketing/marketing'; +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + const EditSalesOrder = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const soId = searchParams.get('salesOrderId'); + + const { data: marketing, isLoading: isLoading } = useSWR(soId, (id: number) => + MarketingApi.getSingle(id) + ); + + if (!soId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoading && (!marketing || isResponseError(marketing))) { + router.replace('/404'); + return; + } return ( -
-

Edit Sales Order

+
+ {isLoading && } + {!isLoading && isResponseSuccess(marketing) && ( + + )}
); }; diff --git a/src/app/marketing/sales-orders/detail/page.tsx b/src/app/marketing/sales-orders/detail/page.tsx index ee377cfa..22d2651c 100644 --- a/src/app/marketing/sales-orders/detail/page.tsx +++ b/src/app/marketing/sales-orders/detail/page.tsx @@ -1,7 +1,42 @@ +'use client'; + +import SalesOrderDetail from '@/components/pages/marketing/sales-orders/detail/SalesOrderDetail'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { MarketingApi } from '@/services/api/marketing/marketing'; +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + const DetailSalesOrder = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const soId = searchParams.get('salesOrderId'); + + const { data: marketing, isLoading: isLoading } = useSWR(soId, (id: number) => + MarketingApi.getSingle(id) + ); + + if (!soId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoading && (!marketing || isResponseError(marketing))) { + router.replace('/404'); + return; + } + return ( -
-

Detail Sales Order

+
+ {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 52a67b58..dd5b03aa 100644 --- a/src/components/pages/marketing/sales-orders/SalesOrderTable.tsx +++ b/src/components/pages/marketing/sales-orders/SalesOrderTable.tsx @@ -11,13 +11,16 @@ 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 { isResponseSuccess } from '@/lib/api-helper'; import { cn } 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 { Icon } from '@iconify/react'; import { CellContext } from '@tanstack/react-table'; import { useCallback, useState } from 'react'; +import useSWR from 'swr'; const RowsOptionsMenu = ({ type = 'dropdown', @@ -85,6 +88,12 @@ const SalesOrderTable = () => { (id) => rowSelection[id] ); + const { + data: marketing, + isLoading: isLoadingMarketing, + mutate: refreshMarketing, + } = useSWR(MarketingApi.basePath, MarketingApi.getAllFetcher); + const deleteModal = useModal(); const confirmationModal = useModal(); @@ -171,59 +180,7 @@ const SalesOrderTable = () => { void; +}) => { + const [approvalAction, setApprovalAction] = useState<'approve' | 'reject'>( + 'approve' + ); + const [isLoading, setIsLoading] = useState(false); + + const deleteModal = useModal(); + const confirmationModal = useModal(); + + const approveClickHandler = () => { + setApprovalAction('approve'); + confirmationModal.openModal(); + }; + + const rejectClickHandler = () => { + setApprovalAction('reject'); + confirmationModal.openModal(); + }; + + const deleteClickHandler = () => { + deleteModal.openModal(); + }; + + const confirmationModalDeleteClickHandler = async () => { + setIsLoading(true); + // await MarketingApi.delete(initialValues?.id as number); + setIsLoading(false); + deleteModal.closeModal(); + toast.success('Successfully deleted Sales Order!'); + refreshValues?.(); + }; + + const confirmationModalApproveClickHandler = async () => { + setIsLoading(true); + await MarketingApi.singleApproval( + initialValues?.id as number, + approvalAction + ); + setIsLoading(false); + confirmationModal.closeModal(); + toast.success('Successfully approved Sales Order!'); + refreshValues?.(); + }; + + return ( + <> +
+ +
+ + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ No. Sales Order + :{initialValues?.so_number}
Nama Pelanggan:{initialValues?.customer?.name}
Status:{initialValues?.status}
Tanggal Penjualan:{initialValues?.so_date}
Total Penjualan: + {formatCurrency(initialValues?.grand_total as number)} +
Catatan:{initialValues?.notes ?? '-'}
+
+ + {initialValues?.marketing_products && ( + + + data={initialValues?.marketing_products} + columns={[ + { + header: 'No. Polisi', + accessorFn(row) { + return formatVechicleNumber( + row.marketing_delivery_products?.vehicle_number as string + ); + }, + }, + { + 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?.marketing_products && + initialValues?.marketing_products?.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', + }} + /> + + )} +
+ + +
+
+ + + + ); +}; + +export default SalesOrderDetail; diff --git a/src/components/pages/marketing/sales-orders/form/SalesForm.schema.ts b/src/components/pages/marketing/sales-orders/form/SalesForm.schema.ts index cc6c5ee5..c8e6ebc2 100644 --- a/src/components/pages/marketing/sales-orders/form/SalesForm.schema.ts +++ b/src/components/pages/marketing/sales-orders/form/SalesForm.schema.ts @@ -7,6 +7,13 @@ import { type MarketingSchema = { customer_id: number | undefined; + customer: + | { + value: number; + label: string; + } + | undefined + | null; so_date: string | undefined; notes: string | undefined; marketing_products: MarketingProductFormValues[]; @@ -14,6 +21,10 @@ type MarketingSchema = { export const MarketingSchema: Yup.ObjectSchema = 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() diff --git a/src/components/pages/marketing/sales-orders/form/SalesForm.tsx b/src/components/pages/marketing/sales-orders/form/SalesForm.tsx index ec7a7076..32d03766 100644 --- a/src/components/pages/marketing/sales-orders/form/SalesForm.tsx +++ b/src/components/pages/marketing/sales-orders/form/SalesForm.tsx @@ -27,7 +27,7 @@ import { CustomerApi } from '@/services/api/master-data'; import { useFormik } from 'formik'; import { MarketingFormValues, MarketingSchema } from './SalesForm.schema'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { MarketingApi } from '@/services/api/marketing'; +import { MarketingApi } from '@/services/api/marketing/marketing'; import { MarketingProductFormValues } from './repeater/MarketingProduct.schema'; const SalesForm = ({ @@ -45,7 +45,9 @@ const SalesForm = ({ MarketingProduct[] >(initialValues?.marketing_products || []); const [selectedCustomer, setSelectedCustomer] = useState( - null + initialValues?.customer + ? { value: initialValues.customer.id, label: initialValues.customer.name } + : null ); const [rowSelection, setRowSelection] = useState>({}); const selectedRowIds = Object.keys(rowSelection).map((item) => @@ -71,56 +73,35 @@ const SalesForm = ({ ); }; const handleAddSubmitProduct = async ( - values: CreateMarketingProductPayload + tableValue: CreateMarketingProductPayload, + fieldValues: MarketingProductFormValues ) => { const newMarketingProduct: MarketingProduct = { id: marketingProducts.length + 1, - product_warehouse: values.product_warehouse!, - unit_price: values.unit_price as number, - total_weight: values.total_weight as number, - qty: values.qty as number, - avg_weight: values.avg_weight as number, - total_price: values.total_price as number, + 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, marketing_delivery_products: { id: marketingProducts.length + 1, - vehicle_number: values.vehicle_number as string, - delivery_date: values.delivery_date as string, - unit_price: values.unit_price as number, - total_weight: values.total_weight as number, - qty: values.qty as number, - avg_weight: values.avg_weight as number, - total_price: values.total_price as number, + vehicle_number: tableValue.vehicle_number as string, + delivery_date: tableValue.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, }, }; - const newMarketingProductPayload: MarketingProductFormValues = { - vehicle_number: values.vehicle_number as string, - kandang_id: values.kandang_id as number, - kandang: { - value: values.kandang_id as number, - label: values.kandang?.name as string, - }, - product_warehouse_id: values.product_warehouse_id as number, - product_warehouse: { - value: values.product_warehouse?.id as number, - label: values.product_warehouse?.product.name as string, - }, - unit_price: values.unit_price, - total_weight: values.total_weight, - qty: values.qty, - uom: values.uom as string, - avg_weight: values.avg_weight, - total_price: values.total_price, - delivery_date: values.delivery_date as string, - }; + setMarketingProducts((prev) => [...prev, newMarketingProduct]); formik.setValues({ ...formik.values, - marketing_products: [ - ...formik.values.marketing_products, - newMarketingProductPayload, - ], + marketing_products: [...formik.values.marketing_products, fieldValues], }); - setGrandTotal((prev) => prev + (values.total_price as number)); + setGrandTotal((prev) => prev + (tableValue.total_price as number)); addProductModal.closeModal(); }; const handleChangeCustomer = (val: OptionType | OptionType[] | null) => { @@ -155,9 +136,13 @@ const SalesForm = ({ const formik = useFormik({ enableReinitialize: true, initialValues: { - so_date: undefined, - notes: '', + so_date: initialValues?.so_date || undefined, + notes: initialValues?.notes || undefined, customer_id: initialValues?.customer?.id || undefined, + customer: { + value: initialValues?.customer?.id as number, + label: initialValues?.customer?.name as string, + }, marketing_products: [], }, validationSchema: MarketingSchema, @@ -195,7 +180,7 @@ const SalesForm = ({ onReset={formik.handleReset} > )} - {JSON.stringify(formik.errors)}