refactor(FE-208,212): rename product_warehouse to warehouse and update related schema and form handling

This commit is contained in:
rstubryan
2025-11-19 16:21:46 +07:00
parent 8bda56e5d3
commit 14f216a352
2 changed files with 113 additions and 198 deletions
@@ -18,18 +18,13 @@ type PurchaseRequestFormSchemaType = {
label: string; label: string;
} | null; } | null;
location_id: number; location_id: number;
warehouse?: {
value: number;
label: string;
} | null;
warehouse_id: number;
notes: string | null; notes: string | null;
items: { items: {
product_warehouse?: { warehouse?: {
value: number; value: number;
label: string; label: string;
} | null; } | null;
product_warehouse_id: number; warehouse_id: number;
product?: { product?: {
value: number; value: number;
label: string; label: string;
@@ -40,11 +35,11 @@ type PurchaseRequestFormSchemaType = {
}; };
export type PurchaseItemSchema = { export type PurchaseItemSchema = {
product_warehouse?: { warehouse?: {
value: number; value: number;
label: string; label: string;
} | null; } | null;
product_warehouse_id: number; warehouse_id: number;
product?: { product?: {
value: number; value: number;
label: string; label: string;
@@ -55,14 +50,14 @@ export type PurchaseItemSchema = {
const PurchaseItemObjectSchema: Yup.ObjectSchema<PurchaseItemSchema> = const PurchaseItemObjectSchema: Yup.ObjectSchema<PurchaseItemSchema> =
Yup.object({ Yup.object({
product_warehouse: Yup.object({ warehouse: 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() warehouse_id: Yup.number()
.required('Produk wajib dipilih!') .required('Gudang wajib dipilih!')
.min(1, 'Produk wajib dipilih!') .min(1, 'Gudang wajib dipilih!')
.typeError('Produk wajib dipilih!'), .typeError('Gudang wajib dipilih!'),
product: Yup.object({ product: Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
label: Yup.string().required(), label: Yup.string().required(),
@@ -107,14 +102,6 @@ export const PurchaseRequestFormSchema: Yup.ObjectSchema<PurchaseRequestFormSche
.required('Lokasi wajib dipilih!') .required('Lokasi wajib dipilih!')
.min(1, 'Lokasi wajib dipilih!') .min(1, 'Lokasi wajib dipilih!')
.typeError('Lokasi wajib dipilih!'), .typeError('Lokasi wajib dipilih!'),
warehouse: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}),
warehouse_id: Yup.number()
.required('Gudang wajib dipilih!')
.min(1, 'Gudang wajib dipilih!')
.typeError('Gudang wajib dipilih!'),
notes: Yup.string().nullable().default(null), notes: Yup.string().nullable().default(null),
items: Yup.array() items: Yup.array()
.of(PurchaseItemObjectSchema) .of(PurchaseItemObjectSchema)
@@ -154,13 +141,6 @@ export const getPurchaseRequestFormInitialValues = (
} }
: null, : null,
location_id: initialValues?.location?.id ?? 0, location_id: initialValues?.location?.id ?? 0,
warehouse: initialValues?.warehouse
? {
value: initialValues.warehouse.id,
label: initialValues.warehouse.name,
}
: undefined,
warehouse_id: initialValues?.warehouse?.id ?? 0,
notes: initialValues?.notes ?? null, notes: initialValues?.notes ?? null,
items: [], items: [],
}); });
@@ -32,7 +32,6 @@ import {
} from '@/services/api/master-data'; } from '@/services/api/master-data';
import { Supplier } from '@/types/api/master-data/supplier'; import { Supplier } from '@/types/api/master-data/supplier';
import { Product } from '@/types/api/master-data/product'; import { Product } from '@/types/api/master-data/product';
import { ProductWarehouseApi } from '@/services/api/inventory';
import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
import { PurchaseRequestApi } from '@/services/api/purchase'; import { PurchaseRequestApi } from '@/services/api/purchase';
@@ -63,18 +62,15 @@ const PurchaseRequestForm = ({
useState(''); useState('');
// ===== TYPE DEFINITIONS ===== // ===== TYPE DEFINITIONS =====
interface ProductWarehouseOptionType extends OptionType { interface ProductOptionType {
product_id: number; value: number;
product_warehouse_id: number; label: string;
warehouse_id: number;
warehouse_name: string;
qty: number;
} }
// ===== UTILITY FUNCTIONS ===== // ===== UTILITY FUNCTIONS =====
const isRepeaterInputError = ( const isRepeaterInputError = (
idx: number, idx: number,
field: 'product_warehouse_id' | 'product_id' | 'qty' field: 'warehouse_id' | 'product_id' | 'qty'
): { isError: boolean; errorMessage: string } => { ): { isError: boolean; errorMessage: string } => {
if (!formik.touched.items || !Array.isArray(formik.touched.items)) { if (!formik.touched.items || !Array.isArray(formik.touched.items)) {
return { return {
@@ -83,7 +79,13 @@ const PurchaseRequestForm = ({
}; };
} }
const touchedField = formik.touched.items[idx]?.[field]; const touchedField = (
formik.touched.items[idx] as Partial<{
warehouse_id: boolean;
product_id: boolean;
qty: boolean;
}>
)?.[field];
const errorItem = formik.errors.items?.[idx] as const errorItem = formik.errors.items?.[idx] as
| Record<string, string> | Record<string, string>
| undefined; | undefined;
@@ -142,7 +144,6 @@ const PurchaseRequestForm = ({
// ===== SELECT INPUT DATA ===== // ===== SELECT INPUT DATA =====
const { const {
inputValue: supplierSelectInputValue,
setInputValue: setSupplierSelectInputValue, setInputValue: setSupplierSelectInputValue,
options: supplierOptions, options: supplierOptions,
isLoadingOptions: isLoadingSuppliers, isLoadingOptions: isLoadingSuppliers,
@@ -150,7 +151,6 @@ const PurchaseRequestForm = ({
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search'); } = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search');
const { const {
inputValue: areaSelectInputValue,
setInputValue: setAreaSelectInputValue, setInputValue: setAreaSelectInputValue,
options: areaOptions, options: areaOptions,
isLoadingOptions: isLoadingAreas, isLoadingOptions: isLoadingAreas,
@@ -176,27 +176,23 @@ const PurchaseRequestForm = ({
: PurchaseRequestFormSchema, : PurchaseRequestFormSchema,
validateOnChange: true, validateOnChange: true,
validateOnBlur: true, validateOnBlur: true,
validateOnMount: false,
enableReinitialize: true,
onSubmit: async (values) => { onSubmit: async (values) => {
const payload: CreatePurchaseRequestPayload = { const payload: CreatePurchaseRequestPayload = {
supplier_id: supplier_id:
typeof values.supplier_id === 'string' typeof values.supplier_id === 'string'
? parseInt(values.supplier_id) || 0 ? parseInt(values.supplier_id) || 0
: values.supplier_id || 0, : values.supplier_id || 0,
credit_term: values.credit_term || 0, credit_term:
typeof values.credit_term === 'string'
? parseInt(values.credit_term) || 0
: values.credit_term || 0,
notes: values.notes || '', notes: values.notes || '',
items: (values.items || []).map((item) => ({ items: (values.items || []).map((item) => ({
warehouse_id: warehouse_id: Number(item.warehouse_id) || 0,
typeof values.warehouse_id === 'string' product_id: Number(item.product_id) || 0,
? parseInt(values.warehouse_id) || 0 qty: Number(item.qty) || 0,
: values.warehouse_id || 0,
product_id:
typeof item.product_id === 'string'
? parseInt(item.product_id) || 0
: item.product_id || 0,
qty:
typeof item.qty === 'string'
? parseFloat(item.qty) || 0
: item.qty || 0,
})), })),
}; };
@@ -215,57 +211,22 @@ const PurchaseRequestForm = ({
}); });
// ===== API DATA FETCHING ===== // ===== API DATA FETCHING =====
const productWarehousesUrl = useMemo(() => { const { data: productsResponse, isLoading: isLoadingProducts } = useSWR(
if (!formik.values.warehouse_id || formik.values.warehouse_id === 0) { `${ProductApi.basePath}`,
return null;
}
const params = new URLSearchParams({
warehouse_id: formik.values.warehouse_id.toString(),
});
return `${ProductWarehouseApi.basePath}?${params.toString()}`;
}, [formik.values.warehouse_id]);
const { data: productWarehouses, isLoading: isLoadingProductWarehouses } =
useSWR(productWarehousesUrl, ProductWarehouseApi.getAllFetcher);
const getProductWarehouseOptionsForItem = useCallback(() => {
if (!isResponseSuccess(productWarehouses)) return [];
return (
productWarehouses?.data.map((pw) => ({
value: pw.id,
label: pw.product.name,
product_id: pw.product.id,
product_warehouse_id: pw.id,
warehouse_id: pw.warehouse.id,
warehouse_name: pw.warehouse.name,
qty: pw.quantity,
})) || []
);
}, [productWarehouses]);
const productUrl = useMemo(() => {
const productIds =
formik.values.items
?.filter(
(item) => item.product_id && typeof item.product_id === 'number'
)
.map((item) => item.product_id as number) || [];
return productIds.length > 0
? `${ProductApi.basePath}?${new URLSearchParams({
id: productIds.join(','),
}).toString()}`
: null;
}, [formik.values.items]);
const { data: productsResponse } = useSWR(
productUrl,
ProductApi.getAllFetcher ProductApi.getAllFetcher
); );
const productOptions = useMemo(() => {
if (!isResponseSuccess(productsResponse)) return [];
return (
productsResponse?.data.map((product: Product) => ({
value: product.id,
label: product.name,
})) || []
);
}, [productsResponse]);
const productData = useMemo(() => { const productData = useMemo(() => {
if (!isResponseSuccess(productsResponse)) return {}; if (!isResponseSuccess(productsResponse)) return {};
@@ -340,19 +301,18 @@ const PurchaseRequestForm = ({
); );
}, [warehouses]); }, [warehouses]);
// Purchase Items Handlers
const addPurchaseItem = () => { const addPurchaseItem = () => {
const newPurchaseItems = [ const newItems = [
...(formik.values.items || []), ...(formik.values.items || []),
{ {
product_warehouse: null, warehouse: null,
product_warehouse_id: null, warehouse_id: 0,
product: null, product: null,
product_id: '', product_id: 0,
qty: '', qty: 0,
}, },
]; ];
formik.setFieldValue('items', newPurchaseItems); formik.setFieldValue('items', newItems);
}; };
const removePurchaseItem = (idx: number) => { const removePurchaseItem = (idx: number) => {
@@ -445,9 +405,25 @@ const PurchaseRequestForm = ({
supplierData.due_date.toString() supplierData.due_date.toString()
); );
} }
if (formik.values.items) {
formik.values.items.forEach((_, idx) => {
formik.setFieldValue(`items.${idx}.product`, null);
formik.setFieldValue(`items.${idx}.product_id`, 0);
formik.setFieldValue(`items.${idx}.qty`, 0);
});
}
} else { } else {
formik.setFieldTouched('credit_term', false); formik.setFieldTouched('credit_term', false);
formik.setFieldValue('credit_term', ''); formik.setFieldValue('credit_term', '');
if (formik.values.items) {
formik.values.items.forEach((_, idx) => {
formik.setFieldValue(`items.${idx}.product`, null);
formik.setFieldValue(`items.${idx}.product_id`, 0);
formik.setFieldValue(`items.${idx}.qty`, 0);
});
}
} }
}} }}
options={supplierOptions} options={supplierOptions}
@@ -508,16 +484,8 @@ const PurchaseRequestForm = ({
if (formik.values.items) { if (formik.values.items) {
formik.values.items.forEach((_, idx) => { formik.values.items.forEach((_, idx) => {
formik.setFieldValue(
`items.${idx}.product_warehouse`,
null
);
formik.setFieldValue(
`items.${idx}.product_warehouse_id`,
null
);
formik.setFieldValue(`items.${idx}.product`, null); formik.setFieldValue(`items.${idx}.product`, null);
formik.setFieldValue(`items.${idx}.product_id`, ''); formik.setFieldValue(`items.${idx}.product_id`, 0);
}); });
} }
}} }}
@@ -553,16 +521,8 @@ const PurchaseRequestForm = ({
if (formik.values.items) { if (formik.values.items) {
formik.values.items.forEach((_, idx) => { formik.values.items.forEach((_, idx) => {
formik.setFieldValue(
`items.${idx}.product_warehouse`,
null
);
formik.setFieldValue(
`items.${idx}.product_warehouse_id`,
null
);
formik.setFieldValue(`items.${idx}.product`, null); formik.setFieldValue(`items.${idx}.product`, null);
formik.setFieldValue(`items.${idx}.product_id`, ''); formik.setFieldValue(`items.${idx}.product_id`, 0);
}); });
} }
}} }}
@@ -691,45 +651,42 @@ const PurchaseRequestForm = ({
? 'Pilih Gudang...' ? 'Pilih Gudang...'
: 'Pilih Area dan Lokasi terlebih dahulu' : 'Pilih Area dan Lokasi terlebih dahulu'
} }
value={formik.values.warehouse} value={item.warehouse}
onChange={(val) => { onChange={(val) => {
const warehouse = val as OptionType | null; const warehouse = val as OptionType | null;
formik.setFieldTouched('warehouse_id', true); const warehouseId =
formik.setFieldValue( (warehouse as OptionType)?.value || 0;
'warehouse_id',
(warehouse as OptionType)?.value || 0 formik.setFieldTouched(
`items.${idx}.warehouse`,
true
); );
formik.setFieldTouched('warehouse', true); formik.setFieldValue(
formik.setFieldValue('warehouse', warehouse); `items.${idx}.warehouse`,
if (formik.values.items) { warehouse
formik.values.items.forEach((_, idx) => { );
formik.setFieldValue( formik.setFieldTouched(
`items.${idx}.product_warehouse`, `items.${idx}.warehouse_id`,
null true
); );
formik.setFieldValue( formik.setFieldValue(
`items.${idx}.product_warehouse_id`, `items.${idx}.warehouse_id`,
null warehouseId
); );
formik.setFieldValue(
`items.${idx}.product`, formik.setFieldValue(`items.${idx}.product`, null);
null formik.setFieldValue(`items.${idx}.product_id`, 0);
);
formik.setFieldValue(
`items.${idx}.product_id`,
''
);
});
}
}} }}
options={warehouseOptions} options={warehouseOptions}
onInputChange={setWarehouseSelectInputValue} onInputChange={setWarehouseSelectInputValue}
isLoading={isLoadingWarehouses} isLoading={isLoadingWarehouses}
isError={ isError={
formik.touched.warehouse_id && isRepeaterInputError(idx, 'warehouse_id').isError
Boolean(formik.errors.warehouse_id) }
errorMessage={
isRepeaterInputError(idx, 'warehouse_id')
.errorMessage
} }
errorMessage={formik.errors.warehouse_id as string}
isDisabled={ isDisabled={
type === 'detail' || type === 'detail' ||
!formik.values.area_id || !formik.values.area_id ||
@@ -746,32 +703,20 @@ const PurchaseRequestForm = ({
<td> <td>
<SelectInput <SelectInput
required required
value={item.product_warehouse} value={item.product ?? undefined}
key={`product-warehouse-${idx}-${formik.values.warehouse_id}`}
onChange={(val) => { onChange={(val) => {
const productWarehouse = const product = val as ProductOptionType | null;
val as ProductWarehouseOptionType | null;
formik.setFieldTouched(
`items.${idx}.product_warehouse`,
true
);
formik.setFieldValue(
`items.${idx}.product_warehouse`,
productWarehouse
);
formik.setFieldTouched(
`items.${idx}.product_warehouse_id`,
true
);
formik.setFieldValue(
`items.${idx}.product_warehouse_id`,
(productWarehouse as ProductWarehouseOptionType)
?.value || 0
);
const productId = const productId =
(productWarehouse as ProductWarehouseOptionType) (product as ProductOptionType)?.value || 0;
?.product_id || 0;
formik.setFieldTouched(
`items.${idx}.product`,
true
);
formik.setFieldValue(
`items.${idx}.product`,
product
);
formik.setFieldTouched( formik.setFieldTouched(
`items.${idx}.product_id`, `items.${idx}.product_id`,
true true
@@ -781,27 +726,17 @@ const PurchaseRequestForm = ({
productId productId
); );
}} }}
options={getProductWarehouseOptionsForItem()} options={productOptions}
isLoading={isLoadingProductWarehouses} isLoading={isLoadingProducts}
isError={ isError={
isRepeaterInputError(idx, 'product_warehouse_id') isRepeaterInputError(idx, 'product_id').isError
.isError
} }
errorMessage={ errorMessage={
isRepeaterInputError(idx, 'product_warehouse_id') isRepeaterInputError(idx, 'product_id').errorMessage
.errorMessage
}
isDisabled={
type === 'detail' || !formik.values.warehouse_id
}
isClearable={
type !== 'detail' && !!formik.values.warehouse_id
}
placeholder={
!formik.values.warehouse_id
? 'Pilih Gudang terlebih dahulu'
: 'Pilih Produk'
} }
isDisabled={type === 'detail'}
isClearable={type !== 'detail'}
placeholder='Pilih Produk'
className={{ className={{
wrapper: 'min-w-32', wrapper: 'min-w-32',
}} }}
@@ -841,7 +776,7 @@ const PurchaseRequestForm = ({
).toLocaleString('en-US') ).toLocaleString('en-US')
: '' : ''
} }
onChange={(e) => {}} onChange={() => {}}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
type='text' type='text'
className={{ className={{