mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-23 23:05:46 +00:00
fix(FE): refactor sales order form create
This commit is contained in:
@@ -1,9 +1,12 @@
|
|||||||
import MarketingTable from '@/components/pages/marketing/MarketingTable';
|
import MarketingTable from '@/components/pages/marketing/MarketingTable';
|
||||||
|
import SalesOrderFormModal from '@/components/pages/marketing/SalesOrderFormModal';
|
||||||
|
|
||||||
const Marketing = () => {
|
const Marketing = () => {
|
||||||
return (
|
return (
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
<MarketingTable />
|
<MarketingTable />
|
||||||
|
|
||||||
|
<SalesOrderFormModal formType='add' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Alert from '@/components/Alert';
|
import Alert from '@/components/Alert';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
@@ -10,34 +11,66 @@ import { useState } from 'react';
|
|||||||
*/
|
*/
|
||||||
const AlertErrorList = ({
|
const AlertErrorList = ({
|
||||||
formErrorList,
|
formErrorList,
|
||||||
|
className,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
formErrorList: string[];
|
formErrorList: string[];
|
||||||
|
className?: {
|
||||||
|
alert?: string;
|
||||||
|
button?: string;
|
||||||
|
headerWrapper?: string;
|
||||||
|
headerIcon?: string;
|
||||||
|
headerText?: string;
|
||||||
|
titleWrapper?: string;
|
||||||
|
ul?: string;
|
||||||
|
li?: string;
|
||||||
|
};
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
if (formErrorList.length === 0) return null;
|
if (formErrorList.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert color='error' className='w-full flex flex-col gap-2 px-4'>
|
<Alert
|
||||||
<div className='flex justify-between items-center gap-2 w-full'>
|
color='error'
|
||||||
<div className='flex items-center gap-2'>
|
className={cn(
|
||||||
<Icon icon='material-symbols:error-outline' width={24} height={24} />
|
'w-full flex flex-col gap-2 px-3 rounded-lg',
|
||||||
<span className='font-semibold'>
|
className?.alert
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex justify-between items-center gap-2 w-full',
|
||||||
|
className?.headerWrapper
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={cn('flex items-center gap-2', className?.titleWrapper)}>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:error-outline'
|
||||||
|
className={cn(className?.headerIcon)}
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
<span className={cn('font-semibold text-sm', className?.headerText)}>
|
||||||
Terdapat {formErrorList.length} error pada form:
|
Terdapat {formErrorList.length} error pada form:
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
variant='link'
|
variant='link'
|
||||||
className='ml-auto p-0 w-fit text-white'
|
className={cn('ml-auto p-0 w-fit text-white', className?.button)}
|
||||||
color='none'
|
color='none'
|
||||||
>
|
>
|
||||||
<Icon icon='material-symbols:close' width={24} height={24} />
|
<Icon icon='material-symbols:close' width={20} height={20} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ul className='list-disc list-inside pl-8 space-y-1 w-full'>
|
<ul
|
||||||
|
className={cn(
|
||||||
|
'list-disc list-inside pl-4 space-y-1.5 w-full',
|
||||||
|
className?.ul
|
||||||
|
)}
|
||||||
|
>
|
||||||
{formErrorList.map((error, index) => (
|
{formErrorList.map((error, index) => (
|
||||||
<li key={index} className='text-sm'>
|
<li key={index} className={cn('text-sm', className?.li)}>
|
||||||
{error}
|
{error}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import TextArea, { TextAreaProps } from '@/components/input/TextArea';
|
|||||||
|
|
||||||
interface DebouncedTextAreaProps extends TextAreaProps {
|
interface DebouncedTextAreaProps extends TextAreaProps {
|
||||||
delay?: number;
|
delay?: number;
|
||||||
|
ref?: React.RefObject<HTMLTextAreaElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DebouncedTextArea = (props: DebouncedTextAreaProps) => {
|
const DebouncedTextArea = (props: DebouncedTextAreaProps) => {
|
||||||
@@ -19,6 +20,11 @@ const DebouncedTextArea = (props: DebouncedTextAreaProps) => {
|
|||||||
const [debouncedChangeEvent] = useDebounce(internalChangeEvent, delay ?? 300);
|
const [debouncedChangeEvent] = useDebounce(internalChangeEvent, delay ?? 300);
|
||||||
const [debouncedValue] = useDebounce(internalValue, delay ?? 300);
|
const [debouncedValue] = useDebounce(internalValue, delay ?? 300);
|
||||||
|
|
||||||
|
// Sync internal value with external props.value when it changes (e.g., form reset)
|
||||||
|
useEffect(() => {
|
||||||
|
setInternalValue(props.value);
|
||||||
|
}, [props.value]);
|
||||||
|
|
||||||
const internalChangeHandler: ChangeEventHandler<HTMLTextAreaElement> = (
|
const internalChangeHandler: ChangeEventHandler<HTMLTextAreaElement> = (
|
||||||
e
|
e
|
||||||
) => {
|
) => {
|
||||||
@@ -35,6 +41,7 @@ const DebouncedTextArea = (props: DebouncedTextAreaProps) => {
|
|||||||
return (
|
return (
|
||||||
<TextArea
|
<TextArea
|
||||||
{...props}
|
{...props}
|
||||||
|
ref={props.ref}
|
||||||
value={internalValue}
|
value={internalValue}
|
||||||
onChange={internalChangeHandler}
|
onChange={internalChangeHandler}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export interface TextAreaProps {
|
|||||||
onChange?: ChangeEventHandler<HTMLTextAreaElement>;
|
onChange?: ChangeEventHandler<HTMLTextAreaElement>;
|
||||||
onBlur?: FocusEventHandler<HTMLTextAreaElement>;
|
onBlur?: FocusEventHandler<HTMLTextAreaElement>;
|
||||||
rows?: number;
|
rows?: number;
|
||||||
|
ref?: React.RefObject<HTMLTextAreaElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TextArea = ({
|
const TextArea = ({
|
||||||
@@ -49,6 +50,7 @@ const TextArea = ({
|
|||||||
readOnly = false,
|
readOnly = false,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
rows = 3,
|
rows = 3,
|
||||||
|
ref,
|
||||||
}: TextAreaProps) => {
|
}: TextAreaProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -99,6 +101,7 @@ const TextArea = ({
|
|||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{(isLoading || endAdornment) && (
|
{(isLoading || endAdornment) && (
|
||||||
|
|||||||
@@ -520,7 +520,15 @@ const MarketingTable = () => {
|
|||||||
<div className='flex flex-row justify-between p-3 border-b border-base-content/10'>
|
<div className='flex flex-row justify-between p-3 border-b border-base-content/10'>
|
||||||
<div className='flex flex-row gap-3'>
|
<div className='flex flex-row gap-3'>
|
||||||
<RequirePermission permissions='lti.marketing.sales_order.create'>
|
<RequirePermission permissions='lti.marketing.sales_order.create'>
|
||||||
<Button className='font-semibold text-base-100 text-sm rounded-lg shadow-button-soft px-3 py-2.5'>
|
<Button
|
||||||
|
href={{
|
||||||
|
pathname: '/marketing',
|
||||||
|
query: {
|
||||||
|
action: 'add',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
className='font-semibold text-base-100 text-sm rounded-lg shadow-button-soft px-3 py-2.5'
|
||||||
|
>
|
||||||
<Icon icon='heroicons:plus' width={20} height={20} />
|
<Icon icon='heroicons:plus' width={20} height={20} />
|
||||||
Add Sales Order
|
Add Sales Order
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,744 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||||
|
import DateInput from '@/components/input/DateInput';
|
||||||
|
import DebouncedTextArea from '@/components/input/DebouncedTextArea';
|
||||||
|
import NumberInput from '@/components/input/NumberInput';
|
||||||
|
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 {
|
||||||
|
DeliveryOrderFormValues,
|
||||||
|
DeliveryOrderSchema,
|
||||||
|
SalesOrderFormValues,
|
||||||
|
SalesOrderSchema,
|
||||||
|
} from '@/components/pages/marketing/form/MarketingForm.schema';
|
||||||
|
import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema';
|
||||||
|
import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
||||||
|
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 {
|
||||||
|
MarketingApi,
|
||||||
|
SalesOrderApi,
|
||||||
|
} from '@/services/api/marketing/marketing';
|
||||||
|
import { CustomerApi } from '@/services/api/master-data';
|
||||||
|
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,
|
||||||
|
UpdateDeliveryOrderPayload,
|
||||||
|
UpdateSalesOrderPayload,
|
||||||
|
} from '@/types/api/marketing/marketing';
|
||||||
|
import { Customer } from '@/types/api/master-data/customer';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
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 isModalActionForForm = modalAction === 'add' || modalAction === 'edit';
|
||||||
|
|
||||||
|
const { mutate } = useSWRConfig();
|
||||||
|
|
||||||
|
const refreshMarketing = () => {
|
||||||
|
mutate(
|
||||||
|
(key) => typeof key === 'string' && key.includes(MarketingApi.basePath)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: Marketing, isLoading: isLoadingMarketing } = useSWR(
|
||||||
|
isModalActionForForm && MarketingId
|
||||||
|
? ['detail-marketing', MarketingId]
|
||||||
|
: undefined,
|
||||||
|
([, id]) => MarketingApi.getSingle(Number(id))
|
||||||
|
);
|
||||||
|
|
||||||
|
// ================== FETCH OPTIONS ==================
|
||||||
|
const {
|
||||||
|
options: customerOptions,
|
||||||
|
isLoadingOptions: isLoadingCustomerOptions,
|
||||||
|
setInputValue: setInputCustomerValue,
|
||||||
|
loadMore: loadMoreCustomer,
|
||||||
|
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
|
||||||
|
const {
|
||||||
|
options: salesOptions,
|
||||||
|
isLoadingOptions: isLoadingSalesOptions,
|
||||||
|
setInputValue: setInputSalesValue,
|
||||||
|
loadMore: loadMoreSales,
|
||||||
|
} = useSelect<CreatedUser>(UserApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step 1: General Information
|
||||||
|
* Step 2: Repeater Add Product
|
||||||
|
* Step 3: Submit
|
||||||
|
*/
|
||||||
|
const [step, setStep] = useState(1);
|
||||||
|
|
||||||
|
const formModal = useModal();
|
||||||
|
const successModal = useModal();
|
||||||
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
const [formikLastValues, setFormikLastValues] = useState<
|
||||||
|
SalesOrderFormValues | undefined
|
||||||
|
>(undefined);
|
||||||
|
const [formErrorMessage, setFormErrorMessage] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [selectedMarketingProduct, setSelectedMarketingProduct] =
|
||||||
|
useState<SalesOrderProductFormValues | null>(null);
|
||||||
|
const [selectedDeliveryProduct, setSelectedDeliveryProduct] =
|
||||||
|
useState<DeliveryOrderProductFormValues | null>(null);
|
||||||
|
const [deliveryFormState, setDeliveryFormState] = useState<'add' | 'edit'>(
|
||||||
|
'add'
|
||||||
|
);
|
||||||
|
const [deliveryOrderValues, setDeliveryOrderValues] = useState<
|
||||||
|
DeliveryOrderProductFormValues[]
|
||||||
|
>(
|
||||||
|
mergeSOwithDO(
|
||||||
|
initialValues?.sales_order?.map(SalesProductToFieldValues) ?? [],
|
||||||
|
initialValues?.delivery_order?.flatMap((delivery) =>
|
||||||
|
DeliveryProductToFieldValues(initialValues.sales_order, delivery)
|
||||||
|
) ?? []
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// ================== SETUP FORMIK ==================
|
||||||
|
const formikInitialValues = useMemo<
|
||||||
|
SalesOrderFormValues & DeliveryOrderFormValues
|
||||||
|
>(() => {
|
||||||
|
return {
|
||||||
|
so_date: initialValues?.so_date || undefined,
|
||||||
|
notes: initialValues?.notes || undefined,
|
||||||
|
customer_id: initialValues?.customer?.id || undefined,
|
||||||
|
sales_person_id: initialValues?.sales_person?.id || undefined,
|
||||||
|
sales_person: initialValues?.sales_person
|
||||||
|
? {
|
||||||
|
value: initialValues.sales_person.id,
|
||||||
|
label: initialValues.sales_person.name,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
customer: initialValues?.customer
|
||||||
|
? {
|
||||||
|
value: initialValues.customer.id,
|
||||||
|
label: initialValues.customer.name,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
sales_order:
|
||||||
|
initialValues?.sales_order?.map((product) =>
|
||||||
|
SalesProductToFieldValues(product)
|
||||||
|
) ?? [],
|
||||||
|
delivery_order: mergeSOwithDO(
|
||||||
|
initialValues?.sales_order?.map(SalesProductToFieldValues) ?? [],
|
||||||
|
initialValues?.delivery_order?.flatMap((delivery) =>
|
||||||
|
DeliveryProductToFieldValues(initialValues.sales_order, delivery)
|
||||||
|
) ?? []
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}, [initialValues]);
|
||||||
|
const formik = useFormik<SalesOrderFormValues & DeliveryOrderFormValues>({
|
||||||
|
enableReinitialize: true,
|
||||||
|
initialValues: formikInitialValues,
|
||||||
|
validationSchema:
|
||||||
|
formType == 'add_deliver' || formType == 'edit_deliver'
|
||||||
|
? DeliveryOrderSchema
|
||||||
|
: SalesOrderSchema,
|
||||||
|
validateOnMount: true,
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
const payload =
|
||||||
|
formType != 'add_deliver' && formType != 'edit_deliver'
|
||||||
|
? ({
|
||||||
|
customer_id: values.customer_id as number,
|
||||||
|
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.sales_order.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)
|
||||||
|
: ({
|
||||||
|
marketing_id: initialValues?.id as number,
|
||||||
|
delivery_products: values.delivery_order
|
||||||
|
.map((product) => {
|
||||||
|
if (Boolean(product.delivery_date)) {
|
||||||
|
return {
|
||||||
|
marketing_product_id:
|
||||||
|
product.marketing_product_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),
|
||||||
|
delivery_date: formatDate(
|
||||||
|
product.delivery_date as string,
|
||||||
|
'yyyy-MM-DD'
|
||||||
|
),
|
||||||
|
vehicle_number: product.vehicle_number,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((item) => Boolean(item)),
|
||||||
|
} as UpdateDeliveryOrderPayload);
|
||||||
|
switch (formType) {
|
||||||
|
case 'add':
|
||||||
|
await createMarketingHandler(payload as CreateSalesOrderPayload);
|
||||||
|
break;
|
||||||
|
case 'edit':
|
||||||
|
await updateMarketingHandler(payload as UpdateSalesOrderPayload);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
afterSubmit?.();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== Formik Error List =====
|
||||||
|
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(
|
||||||
|
formik,
|
||||||
|
{
|
||||||
|
onAfterSubmit: () => {
|
||||||
|
router.push('/marketing');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ================== FORM REPEATER HANDLER ==================
|
||||||
|
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
const createMarketingRes = await SalesOrderApi.create(values);
|
||||||
|
if (isResponseSuccess(createMarketingRes)) {
|
||||||
|
closeModalHandler(false);
|
||||||
|
successModal.openModal();
|
||||||
|
}
|
||||||
|
if (isResponseError(createMarketingRes)) {
|
||||||
|
toast.error(createMarketingRes?.message as string);
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
const updateMarketingHandler = async (values: UpdateSalesOrderPayload) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
const updateMarketingRes = await SalesOrderApi.update(
|
||||||
|
initialValues?.id as number,
|
||||||
|
values
|
||||||
|
);
|
||||||
|
if (isResponseSuccess(updateMarketingRes)) {
|
||||||
|
closeModalHandler(false);
|
||||||
|
successModal.openModal();
|
||||||
|
}
|
||||||
|
if (isResponseError(updateMarketingRes)) {
|
||||||
|
toast.error(updateMarketingRes?.message as string);
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const memoSalesOrder = formik.values.sales_order;
|
||||||
|
|
||||||
|
// ================== HANDLER ==================
|
||||||
|
const nextButtonHandler = () => {
|
||||||
|
setStep(step + 1);
|
||||||
|
};
|
||||||
|
const prevButtonHandler = () => {
|
||||||
|
setStep(step - 1);
|
||||||
|
};
|
||||||
|
const handleChangeCustomer = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
formik.setFieldValue('customer_id', (val as OptionType)?.value);
|
||||||
|
formik.setFieldValue('customer', val as OptionType);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const handleChangeSalesPerson = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
formik.setFieldValue('sales_person_id', (val as OptionType)?.value);
|
||||||
|
formik.setFieldValue('sales_person', val as OptionType);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ================== SALES ORDER HANDLER ==================
|
||||||
|
const handleDeleteSO = useCallback(
|
||||||
|
(id: number) => {
|
||||||
|
const currentProducts = formik.values.sales_order;
|
||||||
|
formik.setFieldValue(
|
||||||
|
'sales_order',
|
||||||
|
currentProducts.filter((p) => p.id != id)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[memoSalesOrder]
|
||||||
|
);
|
||||||
|
const handleEditSO = useCallback(
|
||||||
|
(id: number) => {
|
||||||
|
const currentProducts = formik.values.sales_order;
|
||||||
|
const selectedProduct = currentProducts.find((p) => p.id == id);
|
||||||
|
setSelectedMarketingProduct(selectedProduct ?? null);
|
||||||
|
},
|
||||||
|
[memoSalesOrder]
|
||||||
|
);
|
||||||
|
const handleAddSOClick = useCallback(() => {
|
||||||
|
setSelectedMarketingProduct(null);
|
||||||
|
if (step === 3) {
|
||||||
|
setStep(2);
|
||||||
|
} else {
|
||||||
|
prevButtonHandler();
|
||||||
|
}
|
||||||
|
}, [step]);
|
||||||
|
const handleAddSubmitSO = useCallback(
|
||||||
|
async (values: SalesOrderProductFormValues, id?: number) => {
|
||||||
|
const currentProducts = formik.values.sales_order;
|
||||||
|
|
||||||
|
const newValues = {
|
||||||
|
...values,
|
||||||
|
id: values.id ?? Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let updatedProducts = [];
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
// Overwrite
|
||||||
|
updatedProducts = currentProducts.map((item) =>
|
||||||
|
item.id === id ? newValues : item
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Add new item
|
||||||
|
updatedProducts = [...currentProducts, newValues];
|
||||||
|
}
|
||||||
|
|
||||||
|
formik.setFieldValue('sales_order', updatedProducts);
|
||||||
|
nextButtonHandler();
|
||||||
|
},
|
||||||
|
[memoSalesOrder, nextButtonHandler]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isNextButtonDisabled = useMemo(() => {
|
||||||
|
if (step === 1) {
|
||||||
|
return Boolean(
|
||||||
|
!formik.values.customer_id ||
|
||||||
|
!formik.values.sales_person_id ||
|
||||||
|
!formik.values.so_date ||
|
||||||
|
!formik.values.notes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, [step, formik.values]);
|
||||||
|
|
||||||
|
// ================== EFFECT ==================
|
||||||
|
useEffect(() => {
|
||||||
|
if (modalAction === 'add' || modalAction === 'edit') {
|
||||||
|
formModal.openModal();
|
||||||
|
}
|
||||||
|
}, [modalAction]);
|
||||||
|
|
||||||
|
const closeModalHandler = (shouldPushToRoute: boolean = true) => {
|
||||||
|
if (shouldPushToRoute) {
|
||||||
|
formik.resetForm();
|
||||||
|
textareaRef.current?.setAttribute('value', '');
|
||||||
|
successModal.closeModal();
|
||||||
|
router.push('/marketing');
|
||||||
|
}
|
||||||
|
|
||||||
|
setStep(1);
|
||||||
|
setFormErrorMessage('');
|
||||||
|
formModal.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const grandTotal = useMemo(() => {
|
||||||
|
return memoSalesOrder.reduce(
|
||||||
|
(total, product) =>
|
||||||
|
total + parseFloat((product.total_price as string) || '0'),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}, [memoSalesOrder]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
ref={formModal.ref}
|
||||||
|
position='end'
|
||||||
|
className={{
|
||||||
|
modalBox: 'w-full sm:w-fit p-3 rounded-xl bg-transparent shadow-none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
className='w-full min-h-full flex flex-col sm:flex-row items-stretch bg-base-100 rounded-xl overflow-y-auto'
|
||||||
|
>
|
||||||
|
<div className='w-full sm:w-[446px]'>
|
||||||
|
<div className='w-full p-4 flex flex-row items-stretch gap-3 border-b border-base-content/10'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={() => closeModalHandler()}
|
||||||
|
className='p-0 text-black hover:text-base-content'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className='w-px border-none bg-base-content/10' />
|
||||||
|
|
||||||
|
<h4 className='text-sm font-medium text-base-content/50'>
|
||||||
|
{modalAction === 'add' ? 'Add' : 'Edit'} Sales Order
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
className='w-full p-4 flex flex-col border-b border-base-content/10'
|
||||||
|
ref={formRef}
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
>
|
||||||
|
<h4 className='text-base font-medium text-base-content/50 font-roboto'>
|
||||||
|
Informasi Umum
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<SelectInputRadio
|
||||||
|
required
|
||||||
|
label='Pelanggan'
|
||||||
|
options={customerOptions}
|
||||||
|
isLoading={isLoadingCustomerOptions}
|
||||||
|
value={formik.values.customer}
|
||||||
|
onChange={handleChangeCustomer}
|
||||||
|
onInputChange={setInputCustomerValue}
|
||||||
|
onMenuScrollToBottom={loadMoreCustomer}
|
||||||
|
isError={
|
||||||
|
formik.touched.customer_id &&
|
||||||
|
Boolean(formik.errors.customer_id)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.customer_id}
|
||||||
|
isClearable
|
||||||
|
placeholder='Pilih Pelanggan'
|
||||||
|
isDisabled={
|
||||||
|
formType === 'add_deliver' ||
|
||||||
|
formType === 'edit_deliver' ||
|
||||||
|
formType === 'edit'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DateInput
|
||||||
|
required
|
||||||
|
name='so_date'
|
||||||
|
label='Tanggal'
|
||||||
|
value={formik.values.so_date}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
isError={
|
||||||
|
formik.touched.so_date && Boolean(formik.errors.so_date)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.so_date}
|
||||||
|
placeholder='Pilih Tanggal'
|
||||||
|
readOnly={
|
||||||
|
formType == 'add_deliver' || formType == 'edit_deliver'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<SelectInputRadio
|
||||||
|
required
|
||||||
|
label='Sales'
|
||||||
|
options={salesOptions}
|
||||||
|
isLoading={isLoadingSalesOptions}
|
||||||
|
value={formik.values.sales_person}
|
||||||
|
onChange={handleChangeSalesPerson}
|
||||||
|
onInputChange={setInputSalesValue}
|
||||||
|
onMenuScrollToBottom={loadMoreSales}
|
||||||
|
isError={
|
||||||
|
formik.touched.sales_person_id &&
|
||||||
|
Boolean(formik.errors.sales_person_id)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.sales_person_id}
|
||||||
|
isClearable
|
||||||
|
placeholder='Pilih Sales'
|
||||||
|
isDisabled={
|
||||||
|
formType === 'add_deliver' || formType === 'edit_deliver'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DebouncedTextArea
|
||||||
|
ref={textareaRef}
|
||||||
|
required
|
||||||
|
name='notes'
|
||||||
|
label='Catatan'
|
||||||
|
rows={3}
|
||||||
|
placeholder='Masukan catatan penjualan'
|
||||||
|
value={formik.values.notes || ''}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
isError={formik.touched.notes && Boolean(formik.errors.notes)}
|
||||||
|
errorMessage={formik.errors.notes}
|
||||||
|
disabled={
|
||||||
|
formType === 'add_deliver' || formType === 'edit_deliver'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
name='total_price'
|
||||||
|
label='Total Penjualan'
|
||||||
|
placeholder='Tambah produk terlebih dahulu'
|
||||||
|
value={grandTotal || ''}
|
||||||
|
readOnly
|
||||||
|
startAdornment={
|
||||||
|
<span className='font-semibold py-1 text-xs'>Rp</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<AlertErrorList
|
||||||
|
className={{
|
||||||
|
alert: 'w-full mt-4',
|
||||||
|
}}
|
||||||
|
formErrorList={formErrorList}
|
||||||
|
onClose={close}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className='w-full p-4'>
|
||||||
|
{step === 1 && (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
onClick={nextButtonHandler}
|
||||||
|
disabled={isNextButtonDisabled}
|
||||||
|
className='w-full p-3 rounded-lg text-sm text-base-100'
|
||||||
|
>
|
||||||
|
Tambah Produk
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{step === 2 && (
|
||||||
|
<div className='w-full min-h-full flex flex-col sm:w-[446px] border-l border-base-content/10'>
|
||||||
|
<div className='w-full p-4 flex flex-row items-center justify-between gap-3 border-b border-base-content/10'>
|
||||||
|
<div className='w-full flex flex-row items-stretch gap-3'>
|
||||||
|
{memoSalesOrder.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={() => {
|
||||||
|
setStep(3);
|
||||||
|
}}
|
||||||
|
className='p-0 text-black hover:text-base-content'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:arrow-left'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className='w-px border-none bg-base-content/10' />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h4 className='text-sm font-medium text-base-content/50'>
|
||||||
|
Tambah Produk
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={() => closeModalHandler()}
|
||||||
|
className='p-0 text-error hover:text-base-content'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:trash' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-1 flex-col'>
|
||||||
|
<MemoizedSalesOrderProductForm
|
||||||
|
onSubmitForm={handleAddSubmitSO}
|
||||||
|
initialValues={selectedMarketingProduct ?? undefined}
|
||||||
|
exisitingValues={memoSalesOrder}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{step === 3 && (
|
||||||
|
<div className='w-full min-h-full flex flex-col sm:w-[446px] border-l border-base-content/10 relative'>
|
||||||
|
<div className='w-full p-4 flex flex-row items-center justify-between gap-3 border-b border-base-content/10'>
|
||||||
|
<h4 className='text-sm font-medium text-base-content/50'>
|
||||||
|
Informasi Produk
|
||||||
|
</h4>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={() => closeModalHandler()}
|
||||||
|
className='p-0 text-error hover:text-base-content'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:trash' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-1 flex-col'>
|
||||||
|
{memoSalesOrder.length > 0 && (
|
||||||
|
<div className='p-4'>
|
||||||
|
<MemoizedSalesOrderProductTable
|
||||||
|
formType={formType}
|
||||||
|
data={memoSalesOrder}
|
||||||
|
onDelete={handleDeleteSO}
|
||||||
|
onEdit={handleEditSO}
|
||||||
|
onAddProductClick={handleAddSOClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className='p-4 w-full'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
className='justify-center w-full rounded-lg text-center text-sm text-base-100'
|
||||||
|
onClick={() => formRef.current?.requestSubmit()}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={successModal.ref}
|
||||||
|
iconPosition='left'
|
||||||
|
type='success'
|
||||||
|
text='Data Berhasil Ditambahkan'
|
||||||
|
subtitleText='Data sales order telah berhasil disimpan.'
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Oke',
|
||||||
|
color: 'primary',
|
||||||
|
className: 'rounded-lg',
|
||||||
|
onClick: (e) => {
|
||||||
|
closeModalHandler();
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MemoizedSalesOrderProductTable
|
||||||
|
formType={'success'}
|
||||||
|
data={memoSalesOrder}
|
||||||
|
onDelete={handleDeleteSO}
|
||||||
|
onEdit={handleEditSO}
|
||||||
|
onAddProductClick={handleAddSOClick}
|
||||||
|
/>
|
||||||
|
</ConfirmationModal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SalesOrderFormModal;
|
||||||
@@ -677,7 +677,7 @@ const MarketingForm = ({
|
|||||||
}}
|
}}
|
||||||
variant='bordered'
|
variant='bordered'
|
||||||
>
|
>
|
||||||
<MemoizedSalesOrderProductTable
|
{/* <MemoizedSalesOrderProductTable
|
||||||
formType={formType}
|
formType={formType}
|
||||||
data={memoSalesOrder}
|
data={memoSalesOrder}
|
||||||
rowSelection={rowSOSelection}
|
rowSelection={rowSOSelection}
|
||||||
@@ -687,7 +687,7 @@ const MarketingForm = ({
|
|||||||
onEdit={handleEditSO}
|
onEdit={handleEditSO}
|
||||||
onBulkDelete={handleBulkDeleteSO}
|
onBulkDelete={handleBulkDeleteSO}
|
||||||
onAddProductClick={handleAddSOClick}
|
onAddProductClick={handleAddSOClick}
|
||||||
/>
|
/> */}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Input Table Repeater Delivery Order */}
|
{/* Input Table Repeater Delivery Order */}
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ const SalesOrderProductForm = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form
|
<form
|
||||||
className='size-full'
|
className='size-full flex flex-col p-4 relative overflow-x-hidden'
|
||||||
onSubmit={handleFormSubmit}
|
onSubmit={handleFormSubmit}
|
||||||
onReset={handleResetForm}
|
onReset={handleResetForm}
|
||||||
>
|
>
|
||||||
@@ -293,7 +293,6 @@ const SalesOrderProductForm = ({
|
|||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className='grid sm:grid-cols-3 gap-4 z-200'>
|
|
||||||
<PatternInput
|
<PatternInput
|
||||||
name='vehicle_number'
|
name='vehicle_number'
|
||||||
label='No. Polisi'
|
label='No. Polisi'
|
||||||
@@ -352,9 +351,6 @@ const SalesOrderProductForm = ({
|
|||||||
}
|
}
|
||||||
errorMessage={formik.errors.product_warehouse_id}
|
errorMessage={formik.errors.product_warehouse_id}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className='divider my-6'></div>
|
|
||||||
<div className='grid sm:grid-cols-3 gap-4 z-200'>
|
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Kuantitas'
|
label='Kuantitas'
|
||||||
@@ -450,20 +446,19 @@ const SalesOrderProductForm = ({
|
|||||||
errorMessage={formik.errors.total_price}
|
errorMessage={formik.errors.total_price}
|
||||||
placeholder='Masukan Total Penjualan'
|
placeholder='Masukan Total Penjualan'
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-4'>
|
<div className='mt-4'>
|
||||||
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-row justify-end gap-3 mt-4'>
|
<div className='h-18' />
|
||||||
<Button type='reset' color='warning' onClick={handleResetForm}>
|
|
||||||
Reset
|
<div className='absolute p-4 sm:w-[446px] bottom-0 right-0'>
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
isLoading={formik.isSubmitting}
|
isLoading={formik.isSubmitting}
|
||||||
disabled={formik.isSubmitting}
|
disabled={formik.isSubmitting}
|
||||||
|
className='w-full rounded-lg'
|
||||||
>
|
>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -16,27 +16,17 @@ import CheckboxInput from '@/components/input/CheckboxInput';
|
|||||||
|
|
||||||
type SalesOrderProductTableProps = {
|
type SalesOrderProductTableProps = {
|
||||||
data: SalesOrderProductFormValues[];
|
data: SalesOrderProductFormValues[];
|
||||||
formType: 'add' | 'edit' | 'add_deliver' | 'edit_deliver';
|
formType: 'add' | 'edit' | 'add_deliver' | 'edit_deliver' | 'success';
|
||||||
rowSelection: Record<string, boolean>;
|
|
||||||
setRowSelection: React.Dispatch<
|
|
||||||
React.SetStateAction<Record<string, boolean>>
|
|
||||||
>;
|
|
||||||
selectedRowIds: number[];
|
|
||||||
onDelete: (id: number) => void;
|
onDelete: (id: number) => void;
|
||||||
onEdit: (id: number) => void;
|
onEdit: (id: number) => void;
|
||||||
onBulkDelete: () => void;
|
|
||||||
onAddProductClick: () => void;
|
onAddProductClick: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SalesOrderProductTable = ({
|
const SalesOrderProductTable = ({
|
||||||
data,
|
data,
|
||||||
formType,
|
formType,
|
||||||
rowSelection,
|
|
||||||
setRowSelection,
|
|
||||||
selectedRowIds,
|
|
||||||
onDelete,
|
onDelete,
|
||||||
onEdit,
|
onEdit,
|
||||||
onBulkDelete,
|
|
||||||
onAddProductClick,
|
onAddProductClick,
|
||||||
}: SalesOrderProductTableProps) => {
|
}: SalesOrderProductTableProps) => {
|
||||||
const onDeleteRef = useRef(onDelete);
|
const onDeleteRef = useRef(onDelete);
|
||||||
@@ -156,67 +146,100 @@ const SalesOrderProductTable = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Table<SalesOrderProductFormValues>
|
<div className='size-full flex flex-col relative overflow-x-hidden gap-3'>
|
||||||
rowSelection={rowSelection}
|
{data.map((item) => (
|
||||||
setRowSelection={setRowSelection}
|
|
||||||
data={data}
|
|
||||||
columns={
|
|
||||||
formType == 'add_deliver' || formType == 'edit_deliver'
|
|
||||||
? columns.filter(
|
|
||||||
(col) => col.header != 'Aksi' && col.id != 'select'
|
|
||||||
)
|
|
||||||
: 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 first:flex first:flex-row first:justify-start',
|
|
||||||
paginationClassName: 'hidden',
|
|
||||||
}}
|
|
||||||
emptyContent={
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className='rounded-lg border border-tools-table-outline border-base-content/5'
|
||||||
'w-full h-16 flex flex-col justify-center items-center gap-2'
|
key={`table-${item.id}`}
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<span className='text-gray-500'>Belum ada data penjualan</span>
|
<table
|
||||||
|
style={{
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
}}
|
||||||
|
className='border-none'
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr className='border-b border-tools-table-outline border-base-content/5'>
|
||||||
|
<th className='text-start not-first:font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||||
|
Label
|
||||||
|
</th>
|
||||||
|
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||||
|
Value
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<>
|
||||||
|
<tr>
|
||||||
|
<td className='text-sm px-4 py-3'>No. Polisi</td>
|
||||||
|
<td className='text-sm px-4 py-3'>{item.vehicle_number}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className='text-sm px-4 py-3'>Gudang</td>
|
||||||
|
<td className='text-sm px-4 py-3'>
|
||||||
|
{item.product_warehouse?.label}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className='text-sm px-4 py-3'>Kategori</td>
|
||||||
|
<td className='text-sm px-4 py-3'>Telur</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className='text-sm px-4 py-3'>Produk</td>
|
||||||
|
<td className='text-sm px-4 py-3'>
|
||||||
|
{item.product_warehouse?.label}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className='text-sm px-4 py-3'>Tipe Konversi</td>
|
||||||
|
<td className='text-sm px-4 py-3'>Peti 15Kg</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className='text-sm px-4 py-3'>Total Peti</td>
|
||||||
|
<td className='text-sm px-4 py-3'>
|
||||||
|
{formatNumber(parseFloat(item.qty as string))}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className='text-sm px-4 py-3'>Total Bobot</td>
|
||||||
|
<td className='text-sm px-4 py-3'>
|
||||||
|
{formatNumber(parseFloat(item.total_weight as string))}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className='text-sm px-4 py-3'>Total Butir Telur</td>
|
||||||
|
<td className='text-sm px-4 py-3'>
|
||||||
|
{formatNumber(parseFloat(item.qty as string))}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className='text-sm px-4 py-3'>Total Harga Satuan</td>
|
||||||
|
<td className='text-sm px-4 py-3'>
|
||||||
|
{formatNumber(parseFloat(item.unit_price as string))}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className='text-sm px-4 py-3'>Total Penjualan</td>
|
||||||
|
<td className='text-sm px-4 py-3'>
|
||||||
|
{formatNumber(parseFloat(item.total_price as string))}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
}
|
))}
|
||||||
/>
|
{formType != 'add_deliver' &&
|
||||||
{formType != 'add_deliver' && formType != 'edit_deliver' && (
|
formType != 'edit_deliver' &&
|
||||||
<div className='flex flex-row gap-3 mt-3'>
|
formType != 'success' && (
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='justify-start w-fit py-1 text-sm'
|
className='justify-center w-full rounded-lg text-center text-sm mt-5'
|
||||||
onClick={onAddProductClick}
|
onClick={onAddProductClick}
|
||||||
>
|
>
|
||||||
<Icon icon='mdi:plus' width={16} height={16} />
|
|
||||||
Tambah Produk
|
Tambah Produk
|
||||||
</Button>
|
</Button>
|
||||||
{selectedRowIds.length > 0 && (
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='outline'
|
|
||||||
color='error'
|
|
||||||
className='justify-start w-fit py-1 text-sm'
|
|
||||||
onClick={onBulkDelete}
|
|
||||||
>
|
|
||||||
<Icon icon='mdi:trash' width={16} height={16} />
|
|
||||||
Hapus
|
|
||||||
{selectedRowIds.length > 0
|
|
||||||
? ` (${selectedRowIds.length})`
|
|
||||||
: ''}{' '}
|
|
||||||
Produk
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useState } from 'react';
|
|||||||
interface UseFormikErrorListOptions {
|
interface UseFormikErrorListOptions {
|
||||||
onBeforeSubmit?: (e: React.FormEvent<HTMLFormElement>) => boolean | void;
|
onBeforeSubmit?: (e: React.FormEvent<HTMLFormElement>) => boolean | void;
|
||||||
onAfterValidation?: () => void | Promise<void>;
|
onAfterValidation?: () => void | Promise<void>;
|
||||||
|
onAfterSubmit?: () => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useFormikErrorList = <T>(
|
export const useFormikErrorList = <T>(
|
||||||
@@ -49,6 +50,11 @@ export const useFormikErrorList = <T>(
|
|||||||
|
|
||||||
// Submit form
|
// Submit form
|
||||||
formik.handleSubmit();
|
formik.handleSubmit();
|
||||||
|
|
||||||
|
// Call onAfterSubmit callback if validation passed
|
||||||
|
if (options?.onAfterSubmit) {
|
||||||
|
await options.onAfterSubmit();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user