mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 07:45:47 +00:00
refactor(FE-212): update PurchaseRequestForm schema and validation, streamline warehouse handling and add sub quantity field
This commit is contained in:
@@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
Vendored
+3
-5
@@ -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;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user