refactor(FE-208,212): update PurchaseOrderForm and PurchaseOrderStaffApprovalForm for improved validation and dynamic item handling

This commit is contained in:
rstubryan
2025-11-18 20:43:57 +07:00
parent 0d025ba34c
commit 1b90d657ff
2 changed files with 66 additions and 112 deletions
@@ -90,15 +90,16 @@ const PurchaseStaffApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseStaffAppro
purchase_item: Yup.object({ purchase_item: Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
label: Yup.string().required(), label: Yup.string().required(),
}).nullable(), })
.nullable()
.optional(),
purchase_item_id: Yup.number() purchase_item_id: Yup.number()
.min(1, 'Purchase item is required!') .min(1, 'Purchase item is required!')
.required('Purchase item is required!') .required('Purchase item is required!')
.test( .test(
'is-valid-purchase-item', 'is-valid-purchase-item-id',
'Purchase item must be selected!', 'Purchase item ID must be valid!',
function (value) { function (value) {
if (!this.parent.purchase_item) return true;
return Boolean(value && value > 0); return Boolean(value && value > 0);
} }
) )
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
@@ -8,7 +8,7 @@ import { useSearchParams } from 'next/navigation';
import Button from '@/components/Button'; import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput'; import TextInput from '@/components/input/TextInput';
import NumberInput from '@/components/input/NumberInput'; import NumberInput from '@/components/input/NumberInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput'; import { OptionType } from '@/components/input/SelectInput';
import { import {
PurchaseRequestStaffApprovalFormDefaultValues, PurchaseRequestStaffApprovalFormDefaultValues,
@@ -56,7 +56,7 @@ const PurchaseOrderStaffApprovalForm = ({
// ===== UTILITY FUNCTIONS ===== // ===== UTILITY FUNCTIONS =====
const getPurchaseItemError = ( const getPurchaseItemError = (
idx: number, idx: number,
field: 'purchase_item_id' | 'price' | 'total_price' field: 'price' | 'total_price'
): { isError: boolean; errorMessage: string } => { ): { isError: boolean; errorMessage: string } => {
const touchedItem = formik.touched.items?.[idx]; const touchedItem = formik.touched.items?.[idx];
const errorItem = formik.errors.items?.[idx] as const errorItem = formik.errors.items?.[idx] as
@@ -135,20 +135,20 @@ const PurchaseOrderStaffApprovalForm = ({
onSubmit: async (values) => { onSubmit: async (values) => {
const payload: CreateStaffApprovalRequestPayload = { const payload: CreateStaffApprovalRequestPayload = {
notes: values.notes || '', notes: values.notes || '',
items: (values.items || []).map((item) => ({ items: purchaseItems.map((purchaseItem, idx) => {
purchase_item_id: const formItem = values.items?.[idx];
typeof item.purchase_item_id === 'string' return {
? parseInt(item.purchase_item_id) || 0 purchase_item_id: purchaseItem.value,
: item.purchase_item_id || 0,
price: price:
typeof item.price === 'string' typeof formItem?.price === 'string'
? parseFloat(item.price) || 0 ? parseFloat(formItem.price) || 0
: item.price || 0, : formItem?.price || 0,
total_price: total_price:
typeof item.total_price === 'string' typeof formItem?.total_price === 'string'
? parseFloat(item.total_price) || 0 ? parseFloat(formItem.total_price) || 0
: item.total_price || 0, : formItem?.total_price || 0,
})), };
}),
}; };
switch (type) { switch (type) {
@@ -170,9 +170,9 @@ const PurchaseOrderStaffApprovalForm = ({
if (initialValues?.items) { if (initialValues?.items) {
return initialValues.items.map((item) => ({ return initialValues.items.map((item) => ({
value: item.id, value: item.id,
label: `${item.product.name} ${item.quantity}`, label: `${item.product.name} ${item.sub_qty}`,
id: item.id, id: item.id,
quantity: item.quantity, quantity: item.sub_qty,
product: { product: {
name: item.product.name, name: item.product.name,
product_category: item.product.product_category, product_category: item.product.product_category,
@@ -187,25 +187,18 @@ const PurchaseOrderStaffApprovalForm = ({
return []; return [];
}, [initialValues?.items]); }, [initialValues?.items]);
const getPurchaseItemOptions = useCallback(() => { // Ensure form values are properly set when purchaseItems change
return purchaseItems; useEffect(() => {
if (purchaseItems.length > 0) {
const updatedItems = purchaseItems.map((purchaseItem) => ({
purchase_item_id: purchaseItem.value,
price: '',
total_price: '',
}));
formik.setFieldValue('items', updatedItems);
}
}, [purchaseItems]); }, [purchaseItems]);
// ===== FIELD CHANGE HANDLERS =====
const purchaseItemChangeHandler = (
idx: number,
val: OptionType | OptionType[] | null
) => {
const purchaseItem = val as PurchaseItemOptionType | null;
formik.setFieldTouched(`items.${idx}.purchase_item`, true);
formik.setFieldValue(`items.${idx}.purchase_item`, purchaseItem);
formik.setFieldTouched(`items.${idx}.purchase_item_id`, true);
formik.setFieldValue(
`items.${idx}.purchase_item_id`,
(purchaseItem as OptionType)?.value || 0
);
};
// ===== PURCHASE ITEM OPERATIONS ===== // ===== PURCHASE ITEM OPERATIONS =====
const handlePurchaseItemChange = ( const handlePurchaseItemChange = (
idx: number, idx: number,
@@ -217,31 +210,28 @@ const PurchaseOrderStaffApprovalForm = ({
typeof value === 'string' ? parseFloat(value) || 0 : value; typeof value === 'string' ? parseFloat(value) || 0 : value;
formik.setFieldValue(`items.${idx}.${field}`, numValue); formik.setFieldValue(`items.${idx}.${field}`, numValue);
if (field === 'price') { const selectedItem = purchaseItems[idx];
const selectedItem = purchaseItems.find(
(p) => p.value === formik.values.items?.[idx]?.purchase_item_id if (
); field === 'price' &&
if (selectedItem && selectedItem.quantity && numValue > 0) { selectedItem &&
selectedItem.quantity > 0 &&
numValue >= 0
) {
const calculatedTotal = numValue * selectedItem.quantity; const calculatedTotal = numValue * selectedItem.quantity;
formik.setFieldValue(`items.${idx}.total_price`, calculatedTotal); formik.setFieldValue(`items.${idx}.total_price`, calculatedTotal);
} }
}
if (field === 'total_price') {
const selectedItem = purchaseItems.find(
(p) => p.value === formik.values.items?.[idx]?.purchase_item_id
);
if ( if (
field === 'total_price' &&
selectedItem && selectedItem &&
selectedItem.quantity &&
selectedItem.quantity > 0 && selectedItem.quantity > 0 &&
numValue > 0 numValue >= 0
) { ) {
const calculatedPrice = numValue / selectedItem.quantity; const calculatedPrice = numValue / selectedItem.quantity;
formik.setFieldValue(`items.${idx}.price`, calculatedPrice); formik.setFieldValue(`items.${idx}.price`, calculatedPrice);
} }
} }
}
}; };
return ( return (
@@ -260,10 +250,6 @@ const PurchaseOrderStaffApprovalForm = ({
<table className='table'> <table className='table'>
<thead> <thead>
<tr> <tr>
<th>
Item
<span className='text-error'>*</span>
</th>
<th>Gudang</th> <th>Gudang</th>
<th>Produk</th> <th>Produk</th>
<th>Jenis Produk</th> <th>Jenis Produk</th>
@@ -280,43 +266,16 @@ const PurchaseOrderStaffApprovalForm = ({
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{formik.values.items?.map((item, idx) => { {purchaseItems?.map((purchaseItem, idx) => {
const selectedPurchaseItem = purchaseItems.find( const formItem = formik.values.items?.[idx];
(p) => p.value === item.purchase_item_id
);
return ( return (
<tr key={`purchase-item-${idx}`}> <tr key={`purchase-item-${idx}`}>
<td>
<SelectInput
required
isClearable={true}
value={item.purchase_item}
key={`purchase-item-${idx}`}
onChange={(val) =>
purchaseItemChangeHandler(idx, val)
}
options={getPurchaseItemOptions()}
isError={
getPurchaseItemError(idx, 'purchase_item_id')
.isError
}
errorMessage={
getPurchaseItemError(idx, 'purchase_item_id')
.errorMessage
}
placeholder='Pilih Item...'
className={{
wrapper: 'min-w-52 md:min-w-72 lg:min-w-80',
}}
/>
</td>
<td> <td>
<TextInput <TextInput
name={`items.${idx}.warehouse`} name={`items.${idx}.warehouse`}
type='text' type='text'
value={selectedPurchaseItem?.warehouse?.name || ''} value={purchaseItem?.warehouse?.name || ''}
readOnly={true} readOnly={true}
placeholder='Pilih item terlebih dahulu'
className={{ className={{
wrapper: 'min-w-40 md:min-w-52 lg:min-w-64', wrapper: 'min-w-40 md:min-w-52 lg:min-w-64',
}} }}
@@ -327,9 +286,8 @@ const PurchaseOrderStaffApprovalForm = ({
<TextInput <TextInput
name={`items.${idx}.product_name`} name={`items.${idx}.product_name`}
type='text' type='text'
value={selectedPurchaseItem?.product?.name || ''} value={purchaseItem?.product?.name || ''}
readOnly={true} readOnly={true}
placeholder='Pilih item terlebih dahulu'
className={{ className={{
wrapper: 'min-w-52 md:min-w-72 lg:min-w-80', wrapper: 'min-w-52 md:min-w-72 lg:min-w-80',
}} }}
@@ -341,14 +299,13 @@ const PurchaseOrderStaffApprovalForm = ({
name={`items.${idx}.product_category`} name={`items.${idx}.product_category`}
type='text' type='text'
value={ value={
typeof selectedPurchaseItem?.product typeof purchaseItem?.product?.product_category ===
?.product_category === 'string' 'string'
? selectedPurchaseItem.product.product_category ? purchaseItem.product.product_category
: selectedPurchaseItem?.product?.product_category : purchaseItem?.product?.product_category?.name ||
?.name || '' ''
} }
readOnly={true} readOnly={true}
placeholder='Pilih item terlebih dahulu'
className={{ className={{
wrapper: 'min-w-40 md:min-w-52 lg:min-w-64', wrapper: 'min-w-40 md:min-w-52 lg:min-w-64',
}} }}
@@ -360,14 +317,11 @@ const PurchaseOrderStaffApprovalForm = ({
name={`items.${idx}.quantity`} name={`items.${idx}.quantity`}
type='text' type='text'
value={ value={
selectedPurchaseItem?.quantity purchaseItem?.quantity
? selectedPurchaseItem.quantity.toLocaleString( ? purchaseItem.quantity.toLocaleString('id-ID')
'id-ID'
)
: '' : ''
} }
readOnly={true} readOnly={true}
placeholder='Pilih item terlebih dahulu'
className={{ className={{
wrapper: 'min-w-24', wrapper: 'min-w-24',
}} }}
@@ -378,9 +332,8 @@ const PurchaseOrderStaffApprovalForm = ({
<TextInput <TextInput
name={`items.${idx}.uom`} name={`items.${idx}.uom`}
type='text' type='text'
value={selectedPurchaseItem?.product?.uom?.name || ''} value={purchaseItem?.product?.uom?.name || ''}
readOnly={true} readOnly={true}
placeholder='Pilih item terlebih dahulu'
className={{ className={{
wrapper: 'min-w-24', wrapper: 'min-w-24',
}} }}
@@ -391,7 +344,7 @@ const PurchaseOrderStaffApprovalForm = ({
<NumberInput <NumberInput
required required
name={`items.${idx}.price`} name={`items.${idx}.price`}
value={item.price || ''} value={formItem?.price || ''}
onChange={(e) => onChange={(e) =>
handlePurchaseItemChange( handlePurchaseItemChange(
idx, idx,
@@ -419,7 +372,7 @@ const PurchaseOrderStaffApprovalForm = ({
<NumberInput <NumberInput
required required
name={`items.${idx}.total_price`} name={`items.${idx}.total_price`}
value={item.total_price || ''} value={formItem?.total_price || ''}
onChange={(e) => onChange={(e) =>
handlePurchaseItemChange( handlePurchaseItemChange(
idx, idx,