refactor(FE-208,212): update PurchaseRequestForm schema and validation, enhance credit term handling and improve error messages for required fields

This commit is contained in:
rstubryan
2025-11-03 15:02:00 +07:00
parent 4149c51a7b
commit e1d070b3af
2 changed files with 92 additions and 38 deletions
@@ -17,7 +17,7 @@ type PurchaseRequestFormSchemaType = {
label: string; label: string;
} | null; } | null;
location_id: number; location_id: number;
credit_term: number | string; credit_term: number | string | null;
notes: string | null; notes: string | null;
purchase_items: { purchase_items: {
warehouse?: { warehouse?: {
@@ -65,29 +65,38 @@ const PurchaseItemObjectSchema: Yup.ObjectSchema<PurchaseItemSchema> =
label: Yup.string().required(), label: Yup.string().required(),
}).nullable(), }).nullable(),
warehouse_id: Yup.number() warehouse_id: Yup.number()
.required('Warehouse wajib diisi!') .required('Gudang wajib dipilih!')
.min(1, 'Warehouse wajib diisi!') .min(1, 'Gudang wajib dipilih!')
.typeError('Warehouse harus berupa angka!'), .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(),
}).nullable(), }).nullable(),
product_id: Yup.number() product_id: Yup.number()
.required('Produk wajib diisi!') .required('Produk wajib dipilih!')
.min(1, 'Produk wajib diisi!') .min(1, 'Produk wajib dipilih!')
.typeError('Produk harus berupa angka!'), .typeError('Produk wajib dipilih!'),
product_warehouse: Yup.object({ product_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() product_warehouse_id: Yup.number()
.required('Product Warehouse wajib diisi!') .required('Produk wajib dipilih!')
.min(0.001, 'Product Warehouse tidak boleh negatif!') .min(1, 'Produk wajib dipilih!')
.typeError('Product Warehouse harus berupa angka!'), .typeError('Produk wajib dipilih!'),
sub_qty: Yup.number() sub_qty: Yup.mixed<string | number>()
.required('Sub Qty wajib diisi!') .required('Sub Qty wajib diisi!')
.min(0.001, 'Sub Qty tidak boleh negatif!') .test(
.typeError('Sub Qty harus berupa angka!'), 'is-valid-sub-qty',
'Kuantitas harus berupa angka lebih dari 0!',
function (value) {
if (value === '' || value === null || value === undefined)
return false;
const numValue =
typeof value === 'string' ? parseFloat(value) : value;
return !isNaN(numValue) && numValue > 0;
}
),
}); });
export const PurchaseRequestFormSchema: Yup.ObjectSchema<PurchaseRequestFormSchemaType> = export const PurchaseRequestFormSchema: Yup.ObjectSchema<PurchaseRequestFormSchemaType> =
@@ -97,35 +106,50 @@ export const PurchaseRequestFormSchema: Yup.ObjectSchema<PurchaseRequestFormSche
label: Yup.string().required(), label: Yup.string().required(),
}).nullable(), }).nullable(),
supplier_id: Yup.number() supplier_id: Yup.number()
.required('Supplier wajib diisi!') .required('Supplier wajib dipilih!')
.min(1, 'Supplier wajib diisi!') .test('is-valid-supplier', 'Supplier wajib dipilih!', function (value) {
.typeError('Supplier wajib diisi!'), if (!this.parent.supplier) return true;
return Boolean(value && value > 0);
})
.typeError('Supplier wajib dipilih!'),
area: Yup.object({ area: Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
label: Yup.string().required(), label: Yup.string().required(),
}).nullable(), }).nullable(),
area_id: Yup.number() area_id: Yup.number()
.required('Area wajib diisi!') .required('Area wajib dipilih!')
.test('is-valid-area', 'Area wajib diisi!', function (value) { .test('is-valid-area', 'Area wajib dipilih!', function (value) {
if (!this.parent.area) return true; if (!this.parent.area) return true;
return Boolean(value && value > 0); return Boolean(value && value > 0);
}) })
.typeError('Area wajib diisi!'), .typeError('Area wajib dipilih!'),
location: Yup.object({ location: Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
label: Yup.string().required(), label: Yup.string().required(),
}).nullable(), }).nullable(),
location_id: Yup.number() location_id: Yup.number()
.required('Lokasi wajib diisi!') .required('Lokasi wajib dipilih!')
.test('is-valid-location', 'Lokasi wajib diisi!', function (value) { .test('is-valid-location', 'Lokasi wajib dipilih!', function (value) {
if (!this.parent.location) return true; if (!this.parent.location) return true;
return Boolean(value && value > 0); return Boolean(value && value > 0);
}) })
.typeError('Lokasi wajib diisi!'), .typeError('Lokasi wajib dipilih!'),
credit_term: Yup.number() credit_term: Yup.lazy((value, context) => {
.required('Termin kredit wajib diisi!') const supplier_id = context.parent.supplier_id;
.min(1, 'Termin kredit tidak boleh negatif!') const hasSupplier = supplier_id && supplier_id > 0;
.typeError('Termin kredit harus berupa angka!'),
if (!hasSupplier) {
return Yup.mixed<string | number>()
.nullable()
.default(null)
.transform(() => null);
}
return Yup.number()
.required('Jumlah hari jatuh tempo wajib diisi!')
.min(1, 'Jumlah hari jatuh tempo minimal 1 hari!')
.typeError('Jumlah hari jatuh tempo harus berupa angka!');
}),
notes: Yup.string().nullable().default(null), notes: Yup.string().nullable().default(null),
purchase_items: Yup.array() purchase_items: Yup.array()
.of(PurchaseItemObjectSchema) .of(PurchaseItemObjectSchema)
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useCallback, useEffect, 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 { useRouter } from 'next/navigation';
@@ -8,6 +8,7 @@ import { Icon } from '@iconify/react';
import { toast } from 'react-hot-toast'; 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 NumberInput from '@/components/input/NumberInput';
import SelectInput, { import SelectInput, {
OptionType, OptionType,
useSelect, useSelect,
@@ -27,6 +28,7 @@ import {
LocationApi, LocationApi,
WarehouseApi, WarehouseApi,
} from '@/services/api/master-data'; } from '@/services/api/master-data';
import { Supplier } from '@/types/api/master-data/supplier';
import { ProductWarehouseApi } from '@/services/api/inventory'; import { ProductWarehouseApi } from '@/services/api/inventory';
import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
import { PurchaseApi } from '@/services/api/purchasing'; import { PurchaseApi } from '@/services/api/purchasing';
@@ -96,6 +98,15 @@ const PurchaseRequestForm = ({
}; };
}; };
const getSupplierById = (supplierId: number): Supplier | null => {
if (!isResponseSuccess(supplierRawData)) return null;
return (
supplierRawData?.data.find(
(supplier: Supplier) => supplier.id === supplierId
) || null
);
};
// ===== FORM HANDLERS ===== // ===== FORM HANDLERS =====
const createPurchaseRequestHandler = useCallback( const createPurchaseRequestHandler = useCallback(
async (payload: CreatePurchaseRequestPayload) => { async (payload: CreatePurchaseRequestPayload) => {
@@ -155,7 +166,8 @@ const PurchaseRequestForm = ({
setInputValue: setSupplierSelectInputValue, setInputValue: setSupplierSelectInputValue,
options: supplierOptions, options: supplierOptions,
isLoadingOptions: isLoadingSuppliers, isLoadingOptions: isLoadingSuppliers,
} = useSelect(SupplierApi.basePath, 'id', 'name', 'search'); rawData: supplierRawData,
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search');
const { const {
inputValue: areaSelectInputValue, inputValue: areaSelectInputValue,
@@ -353,6 +365,20 @@ const PurchaseRequestForm = ({
formik.setFieldValue('supplier', supplier); formik.setFieldValue('supplier', supplier);
formik.setFieldTouched('supplier_id', true); formik.setFieldTouched('supplier_id', true);
formik.setFieldValue('supplier_id', (supplier as OptionType)?.value || 0); formik.setFieldValue('supplier_id', (supplier as OptionType)?.value || 0);
if (supplier?.value) {
const supplierId =
typeof supplier.value === 'string'
? parseInt(supplier.value)
: supplier.value;
const supplierData = getSupplierById(supplierId);
if (supplierData?.due_date) {
formik.setFieldTouched('credit_term', true);
formik.setFieldValue('credit_term', supplierData.due_date.toString());
}
} else {
formik.setFieldValue('credit_term', '');
}
}; };
const areaChangeHandler = (val: OptionType | OptionType[] | null) => { const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -509,15 +535,15 @@ const PurchaseRequestForm = ({
onInputChange={setSupplierSelectInputValue} onInputChange={setSupplierSelectInputValue}
isLoading={isLoadingSuppliers} isLoading={isLoadingSuppliers}
isError={ isError={
formik.touched.supplier_id && formik.touched.supplier && Boolean(formik.errors.supplier_id)
Boolean(formik.errors.supplier_id)
} }
errorMessage={formik.errors.supplier_id as string} errorMessage={formik.errors.supplier_id as string}
isDisabled={type === 'detail'} isDisabled={type === 'detail'}
isClearable isClearable
/> />
<TextInput
required <NumberInput
required={!!formik.values.supplier_id}
label='Jatuh tempo (hari)' label='Jatuh tempo (hari)'
name='credit_term' name='credit_term'
value={formik.values.credit_term || ''} value={formik.values.credit_term || ''}
@@ -528,9 +554,15 @@ const PurchaseRequestForm = ({
Boolean(formik.errors.credit_term) Boolean(formik.errors.credit_term)
} }
errorMessage={formik.errors.credit_term as string} errorMessage={formik.errors.credit_term as string}
readOnly={type === 'detail'} readOnly={type === 'detail' || !formik.values.supplier_id}
type='number' disabled={type === 'detail' || !formik.values.supplier_id}
placeholder='Masukkan Credit Term' allowNegative={false}
decimalScale={0}
placeholder={
!formik.values.supplier_id
? 'Pilih Vendor terlebih dahulu'
: 'Masukkan jumlah hari jatuh tempo'
}
/> />
<SelectInput <SelectInput
@@ -542,9 +574,7 @@ const PurchaseRequestForm = ({
options={areaOptions} options={areaOptions}
onInputChange={setAreaSelectInputValue} onInputChange={setAreaSelectInputValue}
isLoading={isLoadingAreas} isLoading={isLoadingAreas}
isError={ isError={formik.touched.area && Boolean(formik.errors.area_id)}
formik.touched.area && Boolean(formik.errors.area_id)
}
errorMessage={formik.errors.area_id as string} errorMessage={formik.errors.area_id as string}
isDisabled={type === 'detail'} isDisabled={type === 'detail'}
isClearable isClearable