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(),
label: Yup.string().required(),
}).nullable(),
warehouse_ids: Yup.number()
warehouse_id: Yup.number()
.required('Warehouse wajib diisi!')
.min(1, 'Produk wajib diisi!')
.min(1, 'Warehouse wajib diisi!')
.typeError('Warehouse harus berupa angka!'),
product: Yup.object({
value: Yup.number().min(1).required(),
@@ -42,17 +42,14 @@ export const PurchaseRequestFormSchema = Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
product_warehouse_id: Yup.number()
.required('Product warehouse wajib diisi!')
.min(1, 'Product warehouse wajib diisi!')
.typeError('Product warehouse harus berupa angka!'),
total_qty: Yup.number()
.required('Jumlah total wajib diisi!')
.min(1, 'Jumlah total tidak boleh negatif!')
.typeError('Jumlah total harus berupa angka!'),
product_warehouse_id: Yup.number().optional().nullable(),
sub_qty: Yup.number()
.required('Sub Qty wajib diisi!')
.min(0.001, 'Sub Qty tidak boleh negatif!')
.typeError('Sub Qty harus berupa angka!'),
price: Yup.number()
.required('Harga wajib diisi!')
.min(1, 'Harga tidak boleh negatif!')
.min(0, 'Harga tidak boleh negatif!')
.typeError('Harga harus berupa angka!'),
})
)
@@ -99,7 +96,7 @@ export const getPurchaseRequestFormInitialValues = (
label: item.warehouse.name,
}
: null,
warehouse_ids: item.warehouse_ids,
warehouse_id: item.warehouse_id,
product: item.product
? {
value: item.product.id,
@@ -113,19 +110,19 @@ export const getPurchaseRequestFormInitialValues = (
label: item.product_warehouse.product.name,
}
: null,
product_warehouse_id: item.product_warehouse_id,
total_qty: item.total_qty,
product_warehouse_id: item.product_warehouse_id || null,
sub_qty: item.sub_qty,
price: item.price,
})
) ?? [
{
warehouse: null,
warehouse_ids: 0,
warehouse_id: 0,
product: null,
product_id: 0,
product_warehouse: null,
product_warehouse_id: 0,
total_qty: 0,
product_warehouse_id: null,
sub_qty: 0,
price: 0,
},
],
@@ -1,12 +1,15 @@
'use client';
import { useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useFormik } from 'formik';
import useSWR from 'swr';
import { useRouter } from 'next/navigation';
import { Icon } from '@iconify/react';
import { toast } from 'react-hot-toast';
import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { useModal } from '@/components/Modal';
import { FormHeader } from '@/components/helper/form/FormHeader';
import { FormActions } from '@/components/helper/form/FormActions';
@@ -18,8 +21,8 @@ import {
} from './PurchaseRequestForm.schema';
import { SupplierApi } from '@/services/api/master-data';
import { WarehouseApi } from '@/services/api/master-data';
import { isResponseSuccess } from '@/lib/api-helper';
import { usePurchaseRequestFormHandlers } from './usePurchaseRequestFormHandlers';
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
import { PurchaseApi } from '@/services/api/purchasing';
import Card from '@/components/Card';
import {
@@ -36,19 +39,61 @@ const PurchaseRequestForm = ({
type = 'add',
initialValues,
}: PurchaseRequestFormProps) => {
const router = useRouter();
const deleteModal = useModal();
const [selectedPurchaseItems, setSelectedPurchaseItems] = useState<number[]>(
[]
);
const [purchaseRequestFormErrorMessage, setPurchaseRequestFormErrorMessage] =
useState('');
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const {
deleteModal,
purchaseRequestFormErrorMessage,
isDeleteLoading,
createPurchaseRequestHandler,
updatePurchaseRequestHandler,
deletePurchaseRequestClickHandler,
confirmationModalDeleteClickHandler,
} = usePurchaseRequestFormHandlers(initialValues?.id);
// ===== FORM HANDLERS =====
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: 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 =====
const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(
@@ -101,9 +146,10 @@ const PurchaseRequestForm = ({
credit_term: values.credit_term || 0,
notes: values.notes || '',
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_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,
price:
typeof item.price === 'number'
@@ -147,11 +193,12 @@ const PurchaseRequestForm = ({
...(formik.values.purchase_items || []),
{
warehouse: null,
warehouse_ids: 0,
warehouse_id: 0,
product: null,
product_id: 0,
product_warehouse: null,
product_warehouse_id: 0,
product_warehouse_id: null,
sub_qty: 0,
total_qty: 0,
price: 0,
},
@@ -180,12 +227,12 @@ const PurchaseRequestForm = ({
value: string | number
) => {
const integerFields = [
'warehouse_ids',
'warehouse_id',
'product_id',
'product_warehouse_id',
'total_qty',
];
const floatFields = ['price'];
const floatFields = ['price', 'sub_qty'];
if (integerFields.includes(field)) {
const numValue = typeof value === 'string' ? parseInt(value) || 0 : value;
@@ -320,6 +367,10 @@ const PurchaseRequestForm = ({
Product Warehouse ID
<span className='text-error'>*</span>
</th>
<th>
Sub Qty
<span className='text-error'>*</span>
</th>
<th>
Total Qty
<span className='text-error'>*</span>
@@ -358,18 +409,18 @@ const PurchaseRequestForm = ({
<td>
<TextInput
required
name={`purchase_items.${idx}.warehouse_ids`}
value={item.warehouse_ids}
name={`purchase_items.${idx}.warehouse_id`}
value={item.warehouse_id}
onChange={(e) =>
handlePurchaseItemChange(
idx,
'warehouse_ids',
'warehouse_id',
e.target.value
)
}
onBlur={formik.handleBlur}
type='number'
placeholder='Warehouse IDs'
placeholder='Warehouse ID'
readOnly={type === 'detail'}
className={{
wrapper: 'min-w-24',
@@ -399,7 +450,6 @@ const PurchaseRequestForm = ({
</td>
<td>
<TextInput
required
name={`purchase_items.${idx}.product_warehouse_id`}
value={item.product_warehouse_id || ''}
onChange={(e) =>
@@ -418,6 +468,27 @@ const PurchaseRequestForm = ({
}}
/>
</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>
<TextInput
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 { Supplier } from '@/types/api/master-data/supplier';
import { Warehouse } from '@/types/api/master-data/warehouse';
export type BasePurchase = {
id: number;
@@ -8,7 +7,6 @@ export type BasePurchase = {
po_number: string;
po_date: string;
supplier: Supplier;
warehouse: Warehouse[];
credit_term: number;
due_date: string;
grand_total: number;
@@ -24,10 +22,10 @@ export type CreatePurchaseRequestPayload = {
credit_term: number;
notes?: string | null;
purchase_items: {
warehouse_ids: number;
warehouse_id: number;
product_id: number;
product_warehouse_id: number;
total_qty: number;
product_warehouse_id?: number | null;
sub_qty: number;
price: number;
}[];
};