diff --git a/src/app/globals.css b/src/app/globals.css
index c3d05c67..e50e020d 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -48,3 +48,8 @@
html {
scrollbar-gutter: initial;
}
+
+.react-select__menu-portal {
+ position: relative;
+ z-index: 99999 !important;
+}
diff --git a/src/app/marketing/sales-orders/add/page.tsx b/src/app/marketing/sales-orders/add/page.tsx
index ed193137..e60085ef 100644
--- a/src/app/marketing/sales-orders/add/page.tsx
+++ b/src/app/marketing/sales-orders/add/page.tsx
@@ -1,7 +1,9 @@
+import SalesForm from '@/components/pages/marketing/sales-orders/form/SalesForm';
+
const AddSalesOrder = () => {
return (
-
-
Tambah Sales Order
+
+
);
};
diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx
index 958d88dd..ea53d2c9 100644
--- a/src/components/Modal.tsx
+++ b/src/components/Modal.tsx
@@ -48,6 +48,7 @@ export const useModal = () => {
interface ModalProps {
ref: RefObject
;
+ id?: string;
children?: ReactNode;
closeOnBackdrop?: boolean;
className?: {
@@ -56,7 +57,13 @@ interface ModalProps {
};
}
-const Modal = ({ ref, children, closeOnBackdrop, className }: ModalProps) => {
+const Modal = ({
+ ref,
+ id,
+ children,
+ closeOnBackdrop,
+ className,
+}: ModalProps) => {
const handleBackdropClick = (e: React.MouseEvent) => {
if (closeOnBackdrop && e.target === ref.current) {
ref.current?.close();
@@ -66,6 +73,7 @@ const Modal = ({ ref, children, closeOnBackdrop, className }: ModalProps) => {
return (
);
};
+
const SalesOrderTable = () => {
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
+ const [approveAction, setApproveAction] = useState<
+ 'approve' | 'reject' | null
+ >(null);
+ const [rowSelection, setRowSelection] = useState>({});
+ const selectedRowIds = Object.keys(rowSelection).filter(
+ (id) => rowSelection[id]
+ );
+
+ const deleteModal = useModal();
+ const confirmationModal = useModal();
+
const searchChangeHandler = useCallback(
(e: React.ChangeEvent) => {
setSearch(e.target.value);
@@ -91,139 +104,267 @@ const SalesOrderTable = () => {
[]
);
+ const approveClickHandler = () => {
+ setApproveAction('approve');
+ confirmationModal.openModal();
+ };
+
+ const rejectClickHandler = () => {
+ setApproveAction('reject');
+ confirmationModal.openModal();
+ };
+
+ const {
+ state: tableFilterState,
+ updateFilter,
+ toQueryString: getTableFilterToQueryString,
+ } = useTableFilter({
+ initial: {
+ search: '',
+ },
+ paramMap: {
+ page: 'page',
+ pageSize: 'limit',
+ },
+ });
+
return (
-
-
-
+
+
+
+
+
+
+
+ Approve
+
+
+
+
+ Reject
+
+
+
+
(
+
+
+
+ ),
+ cell: ({ row }) => (
+
+
+
+ ),
+ },
+ {
+ accessorKey: 'so_number',
+ header: 'No. Order',
+ },
+ {
+ accessorKey: 'so_date',
+ header: 'Tanggal',
+ },
+ {
+ accessorKey: 'approval.step_name',
+ header: 'Status',
+ },
+ {
+ accessorKey: 'customer.name',
+ header: 'Customer',
+ },
+ {
+ accessorKey: 'grand_total',
+ header: 'Grand Total',
+ },
+ {
+ accessorKey: 'marketing_products.length',
+ header: 'Product Details',
+ cell: (props) => (
+
+ {props.row.original.marketing_products?.map((product) => (
+ -
+ {product.product_warehouse.product.name} - Qty:{' '}
+ {product.qty}
+
+ ))}
+
+ ),
+ },
+ {
+ header: 'Aksi',
+ cell: (props) => {
+ const currentPageSize =
+ props.table.getPaginationRowModel().rows.length;
+ const currentPageRows =
+ props.table.getPaginationRowModel().flatRows;
+ const currentRowRelativeIndex =
+ currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
+
+ const isLast2Rows =
+ currentRowRelativeIndex > currentPageSize - 2;
+
+ const deleteClickHandler = () => {};
+
+ return (
+ <>
+ {currentPageSize > 2 && (
+
+
+
+ )}
+
+ {currentPageSize <= 2 && (
+
+
+
+ )}
+ >
+ );
+ },
+ },
+ ]}
+ pageSize={pageSize}
+ page={page}
+ onPageChange={setPage}
+ 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-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',
}}
- search={{
- value: search,
- onChange: searchChangeHandler,
- placeholder: 'Cari Sales Order',
- }}
- />
-
- pageSize * (page - 1) + props.row.index + 1,
- },
- {
- accessorKey: 'so_number',
- header: 'No. Order',
- },
- {
- accessorKey: 'so_date',
- header: 'Tanggal',
- },
- {
- accessorKey: 'approval.step_name',
- header: 'Status',
- },
- {
- accessorKey: 'customer.name',
- header: 'Customer',
- },
- {
- accessorKey: 'grand_total',
- header: 'Grand Total',
- },
- {
- accessorKey: 'marketing_products.length',
- header: 'Product Details',
- cell: (props) => (
-
- {props.row.original.marketing_products?.map((product) => (
- -
- {product.product_warehouse.product.name} - Qty:{' '}
- {product.qty}
-
- ))}
-
- ),
- },
- {
- header: 'Aksi',
- cell: (props) => {},
- },
- ]}
- pageSize={pageSize}
- page={page}
- onPageChange={setPage}
- 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-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',
+
-
+
+
+ >
);
};
export default SalesOrderTable;
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 e69de29b..cc6c5ee5 100644
--- a/src/components/pages/marketing/sales-orders/form/SalesForm.schema.ts
+++ b/src/components/pages/marketing/sales-orders/form/SalesForm.schema.ts
@@ -0,0 +1,27 @@
+import * as Yup from 'yup';
+import { MarketingProduct } from '@/types/api/marketing/marketing';
+import {
+ MarketingProductFormValues,
+ MarketingProductSchema,
+} from './repeater/MarketingProduct.schema';
+
+type MarketingSchema = {
+ customer_id: number | undefined;
+ so_date: string | undefined;
+ notes: string | undefined;
+ marketing_products: MarketingProductFormValues[];
+};
+
+export const MarketingSchema: Yup.ObjectSchema = Yup.object({
+ customer_id: Yup.number().required('Customer wajib diisi!'),
+ 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!'),
+});
+
+export const UpdateMarketingSchema = MarketingSchema;
+
+export type MarketingFormValues = 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 e69de29b..ec7a7076 100644
--- a/src/components/pages/marketing/sales-orders/form/SalesForm.tsx
+++ b/src/components/pages/marketing/sales-orders/form/SalesForm.tsx
@@ -0,0 +1,448 @@
+'use client';
+
+import Button from '@/components/Button';
+import Card from '@/components/Card';
+import { FormHeader } from '@/components/helper/form/FormHeader';
+import DateInput from '@/components/input/DateInput';
+import SelectInput, {
+ OptionType,
+ useSelect,
+} from '@/components/input/SelectInput';
+import TextArea from '@/components/input/TextArea';
+import Modal, { useModal } from '@/components/Modal';
+import Table from '@/components/Table';
+import { cn, formatCurrency, formatNumber } from '@/lib/helper';
+import {
+ CreateMarketingPayload,
+ CreateMarketingProductPayload,
+ Marketing,
+ MarketingProduct,
+} from '@/types/api/marketing/marketing';
+import { Icon } from '@iconify/react';
+import { useEffect, 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 { MarketingFormValues, MarketingSchema } from './SalesForm.schema';
+import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
+import { MarketingApi } from '@/services/api/marketing';
+import { MarketingProductFormValues } from './repeater/MarketingProduct.schema';
+
+const SalesForm = ({
+ formType = 'add',
+ initialValues,
+}: {
+ formType?: 'add' | 'edit';
+ initialValues?: Marketing;
+}) => {
+ const addProductModal = useModal();
+
+ const [selectedMarketingProduct, setSelectedMarketingProduct] =
+ useState(null);
+ const [marketingProducts, setMarketingProducts] = useState<
+ MarketingProduct[]
+ >(initialValues?.marketing_products || []);
+ const [selectedCustomer, setSelectedCustomer] = useState(
+ null
+ );
+ const [rowSelection, setRowSelection] = useState>({});
+ const selectedRowIds = Object.keys(rowSelection).map((item) =>
+ parseInt(item)
+ );
+ const [grandTotal, setGrandTotal] = useState(0);
+
+ const {
+ options: customerOptions,
+ rawData: customerRawData,
+ isLoadingOptions: isLoadingCustomerOptions,
+ } = useSelect(CustomerApi.basePath, 'id', 'name');
+
+ const handleAddProduct = () => {
+ addProductModal.openModal();
+ };
+ const handleDeleteProduct = (id: number) => {
+ setMarketingProducts((prev) => prev.filter((product) => product.id !== id));
+ };
+ const handleBulkDeleteProduct = () => {
+ setMarketingProducts((prev) =>
+ prev.filter((product) => !selectedRowIds.includes(product.id))
+ );
+ };
+ const handleAddSubmitProduct = async (
+ values: CreateMarketingProductPayload
+ ) => {
+ 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,
+ 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,
+ },
+ };
+ 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,
+ ],
+ });
+ setGrandTotal((prev) => prev + (values.total_price as number));
+ addProductModal.closeModal();
+ };
+ const handleChangeCustomer = (val: OptionType | OptionType[] | null) => {
+ setSelectedCustomer(val as OptionType);
+ formik.setFieldValue('customer_id', (val as OptionType)?.value);
+ };
+
+ const createProjectFlockHandler = async (values: CreateMarketingPayload) => {
+ console.log(values);
+ const createMarketingRes = await MarketingApi.create(values);
+ if (isResponseSuccess(createMarketingRes)) {
+ console.log(createMarketingRes);
+ }
+ if (isResponseError(createMarketingRes)) {
+ console.log(createMarketingRes);
+ }
+ };
+ const updateProjectFlockHandler = async (values: CreateMarketingPayload) => {
+ console.log(values);
+ const createMarketingRes = await MarketingApi.update(
+ initialValues?.id as number,
+ values
+ );
+ if (isResponseSuccess(createMarketingRes)) {
+ console.log(createMarketingRes);
+ }
+ if (isResponseError(createMarketingRes)) {
+ console.log(createMarketingRes);
+ }
+ };
+
+ const formik = useFormik({
+ enableReinitialize: true,
+ initialValues: {
+ so_date: undefined,
+ notes: '',
+ customer_id: initialValues?.customer?.id || undefined,
+ marketing_products: [],
+ },
+ validationSchema: MarketingSchema,
+ onSubmit: async (values) => {
+ const payload = {
+ customer_id: values.customer_id as number,
+ date: values.so_date as string,
+ notes: values.notes as string,
+ marketing_products: values.marketing_products,
+ } as CreateMarketingPayload;
+ switch (formType) {
+ case 'add':
+ createProjectFlockHandler(payload);
+ break;
+ case 'edit':
+ updateProjectFlockHandler(payload);
+ break;
+ default:
+ break;
+ }
+ },
+ });
+
+ const { setValues: formikSetValues } = formik;
+
+ useEffect(() => {
+ formikSetValues(formik.initialValues);
+ }, [formikSetValues, formik.initialValues]);
+
+ return (
+ <>
+