refactor(FE-208,212): enhance PurchaseRequestForm with area and location fields, update validation and data handling for new inputs

This commit is contained in:
rstubryan
2025-11-03 13:01:17 +07:00
parent 408250d7ed
commit 7b19cd4a21
2 changed files with 267 additions and 82 deletions
@@ -7,6 +7,16 @@ type PurchaseRequestFormSchemaType = {
label: string;
} | null;
supplier_id: number;
area?: {
value: number;
label: string;
} | null;
area_id: number;
location?: {
value: number;
label: string;
} | null;
location_id: number;
credit_term: number | string;
notes: string | null;
purchase_items: {
@@ -90,6 +100,22 @@ export const PurchaseRequestFormSchema: Yup.ObjectSchema<PurchaseRequestFormSche
.required('Supplier wajib diisi!')
.min(1, 'Supplier wajib diisi!')
.typeError('Supplier wajib diisi!'),
area: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
area_id: Yup.number()
.required('Area wajib diisi!')
.min(1, 'Area wajib diisi!')
.typeError('Area wajib diisi!'),
location: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
location_id: Yup.number()
.required('Lokasi wajib diisi!')
.min(1, 'Lokasi wajib diisi!')
.typeError('Lokasi wajib diisi!'),
credit_term: Yup.number()
.required('Termin kredit wajib diisi!')
.min(1, 'Termin kredit tidak boleh negatif!')
@@ -118,6 +144,10 @@ export const getPurchaseRequestFormInitialValues = (
}
: null,
supplier_id: initialValues?.supplier?.id ?? 0,
area: null,
area_id: 0,
location: null,
location_id: 0,
credit_term: initialValues?.credit_term ?? '',
notes: initialValues?.notes ?? null,
purchase_items: [],
@@ -1,6 +1,6 @@
'use client';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useFormik } from 'formik';
import useSWR from 'swr';
import { useRouter } from 'next/navigation';
@@ -8,6 +8,10 @@ import { Icon } from '@iconify/react';
import { toast } from 'react-hot-toast';
import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput';
import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { useModal } from '@/components/Modal';
@@ -17,8 +21,13 @@ import {
getPurchaseRequestFormInitialValues,
UpdatePurchaseRequestFormSchema,
} from './PurchaseRequestForm.schema';
import { SupplierApi } from '@/services/api/master-data';
import { WarehouseApi } from '@/services/api/master-data';
import {
SupplierApi,
AreaApi,
LocationApi,
WarehouseApi,
} from '@/services/api/master-data';
import { ProductWarehouseApi } from '@/services/api/inventory';
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
import { PurchaseApi } from '@/services/api/purchasing';
@@ -46,6 +55,46 @@ const PurchaseRequestForm = ({
const [purchaseRequestFormErrorMessage, setPurchaseRequestFormErrorMessage] =
useState('');
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [
productWarehouseSelectInputValue,
setProductWarehouseSelectInputValue,
] = useState('');
// ===== INTERFACES =====
interface WarehouseOptionType extends OptionType {
area?: string;
location?: string;
}
interface ProductWarehouseOptionType extends OptionType {
product_id: number;
warehouse_id: number;
warehouse_name: string;
quantity: number;
}
// ===== HELPER FUNCTIONS =====
const getPurchaseItemError = (
idx: number,
field: 'warehouse_id' | 'product_warehouse_id' | 'product_id' | 'sub_qty'
): { isError: boolean; errorMessage: string } => {
const touchedItem = formik.touched.purchase_items?.[idx];
const errorItem = formik.errors.purchase_items?.[idx] as
| Record<string, string>
| undefined;
if (!touchedItem || !errorItem) {
return { isError: false, errorMessage: '' };
}
const isTouched = (touchedItem as Record<string, boolean>)?.[field];
const errorMessage = errorItem?.[field] || '';
return {
isError: Boolean(isTouched && errorMessage),
errorMessage: isTouched && errorMessage ? errorMessage : '',
};
};
// ===== FORM HANDLERS =====
const createPurchaseRequestHandler = useCallback(
@@ -94,37 +143,86 @@ const PurchaseRequestForm = ({
}, [deleteModal, initialValues?.id, router]);
// ===== API DATA FETCHING =====
const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(
SupplierApi.basePath,
SupplierApi.getAllFetcher
const allProductWarehousesUrl = `${ProductWarehouseApi.basePath}`;
const { data: allProductWarehouses } = useSWR(
allProductWarehousesUrl,
ProductWarehouseApi.getAllFetcher
);
const { data: warehouses, isLoading: isLoadingWarehouses } = useSWR(
WarehouseApi.basePath,
// ===== USE SELECT HOOKS =====
const {
inputValue: supplierSelectInputValue,
setInputValue: setSupplierSelectInputValue,
options: supplierOptions,
isLoadingOptions: isLoadingSuppliers,
} = useSelect(SupplierApi.basePath, 'id', 'name', 'search');
const {
inputValue: areaSelectInputValue,
setInputValue: setAreaSelectInputValue,
options: areaOptions,
isLoadingOptions: isLoadingAreas,
} = useSelect(AreaApi.basePath, 'id', 'name', 'search');
const {
inputValue: locationSelectInputValue,
setInputValue: setLocationSelectInputValue,
options: locationOptions,
isLoadingOptions: isLoadingLocations,
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
const {
inputValue: warehouseSelectInputValue,
setInputValue: setWarehouseSelectInputValue,
isLoadingOptions: isLoadingWarehouses,
} = useSelect(WarehouseApi.basePath, 'id', 'name', 'search');
const warehousesUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ search: warehouseSelectInputValue }).toString()}`;
const { data: warehouses } = useSWR(
warehousesUrl,
WarehouseApi.getAllFetcher
);
// ===== DATA PROCESSING =====
const supplierOptions = useMemo(() => {
if (!isResponseSuccess(suppliers)) return [];
return (
suppliers?.data.map((supplier) => ({
value: supplier.id,
label: supplier.name,
})) || []
);
}, [suppliers]);
const warehouseOptions = useMemo(() => {
if (!isResponseSuccess(warehouses)) return [];
return (
warehouses?.data.map((warehouse) => ({
value: warehouse.id,
label: warehouse.name,
warehouses?.data.map((w) => ({
value: w.id,
label: w.name,
area: w.area?.name,
location:
'type' in w && (w.type === 'LOKASI' || w.type === 'KANDANG')
? w.location?.name
: undefined,
})) || []
);
}, [warehouses]);
// ===== PRODUCT WAREHOUSE FETCHING =====
const getProductWarehousesUrl = useCallback(() => {
const productWarehouseParams = new URLSearchParams({
search: productWarehouseSelectInputValue,
});
return `${ProductWarehouseApi.basePath}?${productWarehouseParams.toString()}`;
}, [productWarehouseSelectInputValue]);
const productWarehousesUrl = getProductWarehousesUrl();
const { data: productWarehouses, isLoading: isLoadingProductWarehouses } =
useSWR(productWarehousesUrl, ProductWarehouseApi.getAllFetcher);
const productWarehouseOptions = isResponseSuccess(productWarehouses)
? productWarehouses?.data.map((pw) => ({
value: pw.product.id,
label: pw.product.name,
product_id: pw.product.id,
warehouse_id: pw.warehouse.id,
warehouse_name: pw.warehouse.name,
quantity: pw.quantity,
}))
: [];
const formikInitialValues = useMemo<PurchaseRequestFormValues>(
() => getPurchaseRequestFormInitialValues(initialValues),
[initialValues]
@@ -184,18 +282,28 @@ const PurchaseRequestForm = ({
});
// ===== EVENT HANDLERS =====
const supplierChangeHandler = (val: string) => {
const supplierId = parseInt(val) || 0;
formik.setFieldValue('supplier_id', supplierId);
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
const supplier = val as OptionType | null;
formik.setFieldTouched('supplier', true);
formik.setFieldValue('supplier', supplier);
formik.setFieldTouched('supplier_id', true);
formik.setFieldValue('supplier_id', (supplier as OptionType)?.value || 0);
};
const selectedSupplier = supplierOptions.find(
(option) => option.value === supplierId
);
if (selectedSupplier) {
formik.setFieldValue('supplier', selectedSupplier);
} else {
formik.setFieldValue('supplier', null);
}
const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
const area = val as OptionType | null;
formik.setFieldTouched('area', true);
formik.setFieldValue('area', area);
formik.setFieldTouched('area_id', true);
formik.setFieldValue('area_id', (area as OptionType)?.value || 0);
};
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
const location = val as OptionType | null;
formik.setFieldTouched('location', true);
formik.setFieldValue('location', location);
formik.setFieldTouched('location_id', true);
formik.setFieldValue('location_id', (location as OptionType)?.value || 0);
};
// Purchase Items Handlers
@@ -293,21 +401,22 @@ const PurchaseRequestForm = ({
: 'grid grid-cols-1 md:grid-cols-2 gap-6'
}
>
<TextInput
<SelectInput
required
label='Vendor'
name='supplier_id'
value={formik.values.supplier_id}
onChange={(e) => supplierChangeHandler(e.target.value)}
onBlur={formik.handleBlur}
placeholder='Pilih Vendor...'
value={formik.values.supplier}
onChange={supplierChangeHandler}
options={supplierOptions}
onInputChange={setSupplierSelectInputValue}
isLoading={isLoadingSuppliers}
isError={
formik.touched.supplier_id &&
Boolean(formik.errors.supplier_id)
}
errorMessage={formik.errors.supplier_id as string}
readOnly={type === 'detail'}
type='number'
placeholder='Masukkan Supplier ID'
isDisabled={type === 'detail'}
isClearable
/>
<TextInput
required
@@ -326,26 +435,39 @@ const PurchaseRequestForm = ({
placeholder='Masukkan Credit Term'
/>
<TextInput
<SelectInput
required
label='Area'
name='area_id'
onChange={(e) => {}}
onBlur={formik.handleBlur}
readOnly={type === 'detail'}
type='number'
placeholder='Pilih Area'
placeholder='Pilih Area...'
value={formik.values.area}
onChange={areaChangeHandler}
options={areaOptions}
onInputChange={setAreaSelectInputValue}
isLoading={isLoadingAreas}
isError={
formik.touched.area_id && Boolean(formik.errors.area_id)
}
errorMessage={formik.errors.area_id as string}
isDisabled={type === 'detail'}
isClearable
/>
<TextInput
<SelectInput
required
label='Lokasi'
name='location_id'
onChange={(e) => {}}
onBlur={formik.handleBlur}
readOnly={type === 'detail'}
type='number'
placeholder='Pilih Lokasi'
placeholder='Pilih Lokasi...'
value={formik.values.location}
onChange={locationChangeHandler}
options={locationOptions}
onInputChange={setLocationSelectInputValue}
isLoading={isLoadingLocations}
isError={
formik.touched.location_id &&
Boolean(formik.errors.location_id)
}
errorMessage={formik.errors.location_id as string}
isDisabled={type === 'detail'}
isClearable
/>
<div className={type === 'detail' ? 'col-span-1' : 'col-span-2'}>
@@ -442,43 +564,76 @@ const PurchaseRequestForm = ({
</td>
)}
<td>
<TextInput
<SelectInput
required
name={`purchase_items.${idx}.warehouse_id`}
value={item.warehouse_id || ''}
onChange={(e) =>
handlePurchaseItemChange(
idx,
'warehouse_id',
e.target.value
)
value={item.warehouse}
onChange={(val) => {
const warehouse = val as OptionType | null;
formik.setFieldValue(
`purchase_items.${idx}.warehouse`,
warehouse
);
formik.setFieldValue(
`purchase_items.${idx}.warehouse_id`,
(warehouse as OptionType)?.value || 0
);
}}
options={warehouseOptions}
onInputChange={setWarehouseSelectInputValue}
isLoading={isLoadingWarehouses}
isError={
getPurchaseItemError(idx, 'warehouse_id').isError
}
onBlur={formik.handleBlur}
type='number'
placeholder='Masukkan Warehouse ID'
readOnly={type === 'detail'}
errorMessage={
getPurchaseItemError(idx, 'warehouse_id')
.errorMessage
}
isDisabled={type === 'detail'}
isClearable
placeholder='Pilih Gudang'
className={{
wrapper: 'min-w-24',
wrapper: 'min-w-32',
}}
/>
</td>
<td>
<TextInput
name={`purchase_items.${idx}.product_warehouse_id`}
value={item.product_warehouse_id || ''}
onChange={(e) =>
handlePurchaseItemChange(
idx,
'product_warehouse_id',
e.target.value
)
<SelectInput
required
value={item.product_warehouse}
onChange={(val) => {
const productWarehouse =
val as ProductWarehouseOptionType | null;
formik.setFieldValue(
`purchase_items.${idx}.product_warehouse`,
productWarehouse
);
formik.setFieldValue(
`purchase_items.${idx}.product_warehouse_id`,
(productWarehouse as ProductWarehouseOptionType)
?.value || 0
);
formik.setFieldValue(
`purchase_items.${idx}.product_id`,
(productWarehouse as ProductWarehouseOptionType)
?.product_id || 0
);
}}
options={productWarehouseOptions}
onInputChange={setProductWarehouseSelectInputValue}
isLoading={isLoadingProductWarehouses}
isError={
getPurchaseItemError(idx, 'product_warehouse_id')
.isError
}
onBlur={formik.handleBlur}
type='number'
placeholder='Product Warehouse ID'
readOnly={type === 'detail'}
errorMessage={
getPurchaseItemError(idx, 'product_warehouse_id')
.errorMessage
}
isDisabled={type === 'detail'}
isClearable
placeholder='Pilih Produk'
className={{
wrapper: 'min-w-24',
wrapper: 'min-w-32',
}}
/>
</td>