refactor(FE-212): update PurchaseRequestForm schema and validation, streamline warehouse handling and add sub quantity field

This commit is contained in:
rstubryan
2025-11-03 11:04:07 +07:00
parent 1d79e8de1d
commit b4a9c86c2a
4 changed files with 111 additions and 118 deletions
@@ -26,9 +26,9 @@ export const PurchaseRequestFormSchema = Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
label: Yup.string().required(), label: Yup.string().required(),
}).nullable(), }).nullable(),
warehouse_ids: Yup.number() warehouse_id: Yup.number()
.required('Warehouse wajib diisi!') .required('Warehouse wajib diisi!')
.min(1, 'Produk wajib diisi!') .min(1, 'Warehouse wajib diisi!')
.typeError('Warehouse harus berupa angka!'), .typeError('Warehouse harus berupa angka!'),
product: Yup.object({ product: Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
@@ -42,17 +42,14 @@ export const PurchaseRequestFormSchema = Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
label: Yup.string().required(), label: Yup.string().required(),
}).nullable(), }).nullable(),
product_warehouse_id: Yup.number() product_warehouse_id: Yup.number().optional().nullable(),
.required('Product warehouse wajib diisi!') sub_qty: Yup.number()
.min(1, 'Product warehouse wajib diisi!') .required('Sub Qty wajib diisi!')
.typeError('Product warehouse harus berupa angka!'), .min(0.001, 'Sub Qty tidak boleh negatif!')
total_qty: Yup.number() .typeError('Sub Qty harus berupa angka!'),
.required('Jumlah total wajib diisi!')
.min(1, 'Jumlah total tidak boleh negatif!')
.typeError('Jumlah total harus berupa angka!'),
price: Yup.number() price: Yup.number()
.required('Harga wajib diisi!') .required('Harga wajib diisi!')
.min(1, 'Harga tidak boleh negatif!') .min(0, 'Harga tidak boleh negatif!')
.typeError('Harga harus berupa angka!'), .typeError('Harga harus berupa angka!'),
}) })
) )
@@ -99,7 +96,7 @@ export const getPurchaseRequestFormInitialValues = (
label: item.warehouse.name, label: item.warehouse.name,
} }
: null, : null,
warehouse_ids: item.warehouse_ids, warehouse_id: item.warehouse_id,
product: item.product product: item.product
? { ? {
value: item.product.id, value: item.product.id,
@@ -113,19 +110,19 @@ export const getPurchaseRequestFormInitialValues = (
label: item.product_warehouse.product.name, label: item.product_warehouse.product.name,
} }
: null, : null,
product_warehouse_id: item.product_warehouse_id, product_warehouse_id: item.product_warehouse_id || null,
total_qty: item.total_qty, sub_qty: item.sub_qty,
price: item.price, price: item.price,
}) })
) ?? [ ) ?? [
{ {
warehouse: null, warehouse: null,
warehouse_ids: 0, warehouse_id: 0,
product: null, product: null,
product_id: 0, product_id: 0,
product_warehouse: null, product_warehouse: null,
product_warehouse_id: 0, product_warehouse_id: null,
total_qty: 0, sub_qty: 0,
price: 0, price: 0,
}, },
], ],
@@ -1,12 +1,15 @@
'use client'; 'use client';
import { useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import useSWR from 'swr'; import useSWR from 'swr';
import { useRouter } from 'next/navigation';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { toast } from 'react-hot-toast';
import Button from '@/components/Button'; import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput'; import TextInput from '@/components/input/TextInput';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { useModal } from '@/components/Modal';
import { FormHeader } from '@/components/helper/form/FormHeader'; import { FormHeader } from '@/components/helper/form/FormHeader';
import { FormActions } from '@/components/helper/form/FormActions'; import { FormActions } from '@/components/helper/form/FormActions';
@@ -18,8 +21,8 @@ import {
} from './PurchaseRequestForm.schema'; } from './PurchaseRequestForm.schema';
import { SupplierApi } from '@/services/api/master-data'; import { SupplierApi } from '@/services/api/master-data';
import { WarehouseApi } from '@/services/api/master-data'; import { WarehouseApi } from '@/services/api/master-data';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
import { usePurchaseRequestFormHandlers } from './usePurchaseRequestFormHandlers'; import { PurchaseApi } from '@/services/api/purchasing';
import Card from '@/components/Card'; import Card from '@/components/Card';
import { import {
@@ -36,19 +39,61 @@ const PurchaseRequestForm = ({
type = 'add', type = 'add',
initialValues, initialValues,
}: PurchaseRequestFormProps) => { }: PurchaseRequestFormProps) => {
const router = useRouter();
const deleteModal = useModal();
const [selectedPurchaseItems, setSelectedPurchaseItems] = useState<number[]>( const [selectedPurchaseItems, setSelectedPurchaseItems] = useState<number[]>(
[] []
); );
const [purchaseRequestFormErrorMessage, setPurchaseRequestFormErrorMessage] =
useState('');
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const { // ===== FORM HANDLERS =====
deleteModal, const createPurchaseRequestHandler = useCallback(
purchaseRequestFormErrorMessage, async (payload: CreatePurchaseRequestPayload) => {
isDeleteLoading, const res = await PurchaseApi.create(payload);
createPurchaseRequestHandler, if (isResponseError(res)) {
updatePurchaseRequestHandler, setPurchaseRequestFormErrorMessage(res.message);
deletePurchaseRequestClickHandler, return;
confirmationModalDeleteClickHandler, }
} = usePurchaseRequestFormHandlers(initialValues?.id); toast.success(res?.message as string);
router.push('/purchase');
},
[router]
);
const updatePurchaseRequestHandler = useCallback(
async (
purchaseRequestId: number,
payload: CreatePurchaseRequestPayload
) => {
const res = await PurchaseApi.update(purchaseRequestId, payload);
if (isResponseError(res)) {
setPurchaseRequestFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
router.refresh();
router.push('/purchase');
},
[router]
);
const deletePurchaseRequestClickHandler = useCallback(() => {
deleteModal.openModal();
}, [deleteModal]);
const confirmationModalDeleteClickHandler = useCallback(async () => {
if (!initialValues?.id) return;
setIsDeleteLoading(true);
await PurchaseApi.delete(initialValues.id);
deleteModal.closeModal();
toast.success('Successfully delete Purchase Request!');
setIsDeleteLoading(false);
router.push('/purchase');
}, [deleteModal, initialValues?.id, router]);
// ===== API DATA FETCHING ===== // ===== API DATA FETCHING =====
const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR( const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(
@@ -101,9 +146,10 @@ const PurchaseRequestForm = ({
credit_term: values.credit_term || 0, credit_term: values.credit_term || 0,
notes: values.notes || '', notes: values.notes || '',
purchase_items: (values.purchase_items || []).map((item) => ({ purchase_items: (values.purchase_items || []).map((item) => ({
warehouse_ids: item.warehouse_ids || 0, warehouse_id: item.warehouse_id || 0,
product_id: item.product_id || 0, product_id: item.product_id || 0,
product_warehouse_id: item.product_warehouse_id || 0, product_warehouse_id: item.product_warehouse_id || undefined,
sub_qty: item.sub_qty || 0,
total_qty: item.total_qty || 0, total_qty: item.total_qty || 0,
price: price:
typeof item.price === 'number' typeof item.price === 'number'
@@ -147,11 +193,12 @@ const PurchaseRequestForm = ({
...(formik.values.purchase_items || []), ...(formik.values.purchase_items || []),
{ {
warehouse: null, warehouse: null,
warehouse_ids: 0, warehouse_id: 0,
product: null, product: null,
product_id: 0, product_id: 0,
product_warehouse: null, product_warehouse: null,
product_warehouse_id: 0, product_warehouse_id: null,
sub_qty: 0,
total_qty: 0, total_qty: 0,
price: 0, price: 0,
}, },
@@ -180,12 +227,12 @@ const PurchaseRequestForm = ({
value: string | number value: string | number
) => { ) => {
const integerFields = [ const integerFields = [
'warehouse_ids', 'warehouse_id',
'product_id', 'product_id',
'product_warehouse_id', 'product_warehouse_id',
'total_qty', 'total_qty',
]; ];
const floatFields = ['price']; const floatFields = ['price', 'sub_qty'];
if (integerFields.includes(field)) { if (integerFields.includes(field)) {
const numValue = typeof value === 'string' ? parseInt(value) || 0 : value; const numValue = typeof value === 'string' ? parseInt(value) || 0 : value;
@@ -320,6 +367,10 @@ const PurchaseRequestForm = ({
Product Warehouse ID Product Warehouse ID
<span className='text-error'>*</span> <span className='text-error'>*</span>
</th> </th>
<th>
Sub Qty
<span className='text-error'>*</span>
</th>
<th> <th>
Total Qty Total Qty
<span className='text-error'>*</span> <span className='text-error'>*</span>
@@ -358,18 +409,18 @@ const PurchaseRequestForm = ({
<td> <td>
<TextInput <TextInput
required required
name={`purchase_items.${idx}.warehouse_ids`} name={`purchase_items.${idx}.warehouse_id`}
value={item.warehouse_ids} value={item.warehouse_id}
onChange={(e) => onChange={(e) =>
handlePurchaseItemChange( handlePurchaseItemChange(
idx, idx,
'warehouse_ids', 'warehouse_id',
e.target.value e.target.value
) )
} }
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
type='number' type='number'
placeholder='Warehouse IDs' placeholder='Warehouse ID'
readOnly={type === 'detail'} readOnly={type === 'detail'}
className={{ className={{
wrapper: 'min-w-24', wrapper: 'min-w-24',
@@ -399,7 +450,6 @@ const PurchaseRequestForm = ({
</td> </td>
<td> <td>
<TextInput <TextInput
required
name={`purchase_items.${idx}.product_warehouse_id`} name={`purchase_items.${idx}.product_warehouse_id`}
value={item.product_warehouse_id || ''} value={item.product_warehouse_id || ''}
onChange={(e) => onChange={(e) =>
@@ -418,6 +468,27 @@ const PurchaseRequestForm = ({
}} }}
/> />
</td> </td>
<td>
<TextInput
required
name={`purchase_items.${idx}.sub_qty`}
value={item.sub_qty}
onChange={(e) =>
handlePurchaseItemChange(
idx,
'sub_qty',
e.target.value
)
}
onBlur={formik.handleBlur}
type='number'
placeholder='Sub Qty'
readOnly={type === 'detail'}
className={{
wrapper: 'min-w-24',
}}
/>
</td>
<td> <td>
<TextInput <TextInput
required required
@@ -1,73 +0,0 @@
import { useCallback, useState } from 'react';
import { useRouter } from 'next/navigation';
import { toast } from 'react-hot-toast';
import { useModal } from '@/components/Modal';
import { PurchaseApi } from '@/services/api/purchasing';
import {
CreatePurchaseRequestPayload,
UpdatePurchaseRequestPayload,
} from '@/types/api/purchase/purchase';
import { isResponseError } from '@/lib/api-helper';
export const usePurchaseRequestFormHandlers = (initialValuesId?: number) => {
const router = useRouter();
const deleteModal = useModal();
const [purchaseRequestFormErrorMessage, setPurchaseRequestFormErrorMessage] =
useState('');
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const createPurchaseRequestHandler = useCallback(
async (payload: CreatePurchaseRequestPayload) => {
const res = await PurchaseApi.create(payload);
if (isResponseError(res)) {
setPurchaseRequestFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
router.push('/purchase');
},
[router]
);
const updatePurchaseRequestHandler = useCallback(
async (
purchaseRequestId: number,
payload: UpdatePurchaseRequestPayload
) => {
const res = await PurchaseApi.update(purchaseRequestId, payload);
if (res?.status === 'error') {
setPurchaseRequestFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
router.refresh();
router.push('/purchase');
},
[router]
);
const deletePurchaseRequestClickHandler = useCallback(() => {
deleteModal.openModal();
}, [deleteModal]);
const confirmationModalDeleteClickHandler = useCallback(async () => {
if (!initialValuesId) return;
setIsDeleteLoading(true);
await PurchaseApi.delete(initialValuesId);
deleteModal.closeModal();
toast.success('Successfully delete Purchase Request!');
setIsDeleteLoading(false);
router.push('/purchase');
}, [deleteModal, initialValuesId, router]);
return {
deleteModal,
purchaseRequestFormErrorMessage,
isDeleteLoading,
createPurchaseRequestHandler,
updatePurchaseRequestHandler,
deletePurchaseRequestClickHandler,
confirmationModalDeleteClickHandler,
};
};
+3 -5
View File
@@ -1,6 +1,5 @@
import { BaseMetadata } from '@/types/api/api-general'; import { BaseMetadata } from '@/types/api/api-general';
import { Supplier } from '@/types/api/master-data/supplier'; import { Supplier } from '@/types/api/master-data/supplier';
import { Warehouse } from '@/types/api/master-data/warehouse';
export type BasePurchase = { export type BasePurchase = {
id: number; id: number;
@@ -8,7 +7,6 @@ export type BasePurchase = {
po_number: string; po_number: string;
po_date: string; po_date: string;
supplier: Supplier; supplier: Supplier;
warehouse: Warehouse[];
credit_term: number; credit_term: number;
due_date: string; due_date: string;
grand_total: number; grand_total: number;
@@ -24,10 +22,10 @@ export type CreatePurchaseRequestPayload = {
credit_term: number; credit_term: number;
notes?: string | null; notes?: string | null;
purchase_items: { purchase_items: {
warehouse_ids: number; warehouse_id: number;
product_id: number; product_id: number;
product_warehouse_id: number; product_warehouse_id?: number | null;
total_qty: number; sub_qty: number;
price: number; price: number;
}[]; }[];
}; };