mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 05:22:02 +00:00
refactor(FE-208,212): enhance PurchaseRequestForm with area and location fields, update validation and data handling for new inputs
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user