From e1d070b3af8de60ba8798c1239b2184519bafd81 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 3 Nov 2025 15:02:00 +0700 Subject: [PATCH] refactor(FE-208,212): update PurchaseRequestForm schema and validation, enhance credit term handling and improve error messages for required fields --- .../form/PurchaseRequestForm.schema.ts | 76 ++++++++++++------- .../purchase/form/PurchaseRequestForm.tsx | 54 ++++++++++--- 2 files changed, 92 insertions(+), 38 deletions(-) diff --git a/src/components/pages/purchase/form/PurchaseRequestForm.schema.ts b/src/components/pages/purchase/form/PurchaseRequestForm.schema.ts index 8a84b3f4..27cbdf5a 100644 --- a/src/components/pages/purchase/form/PurchaseRequestForm.schema.ts +++ b/src/components/pages/purchase/form/PurchaseRequestForm.schema.ts @@ -17,7 +17,7 @@ type PurchaseRequestFormSchemaType = { label: string; } | null; location_id: number; - credit_term: number | string; + credit_term: number | string | null; notes: string | null; purchase_items: { warehouse?: { @@ -65,29 +65,38 @@ const PurchaseItemObjectSchema: Yup.ObjectSchema = label: Yup.string().required(), }).nullable(), warehouse_id: Yup.number() - .required('Warehouse wajib diisi!') - .min(1, 'Warehouse wajib diisi!') - .typeError('Warehouse harus berupa angka!'), + .required('Gudang wajib dipilih!') + .min(1, 'Gudang wajib dipilih!') + .typeError('Gudang wajib dipilih'), product: Yup.object({ value: Yup.number().min(1).required(), label: Yup.string().required(), }).nullable(), product_id: Yup.number() - .required('Produk wajib diisi!') - .min(1, 'Produk wajib diisi!') - .typeError('Produk harus berupa angka!'), + .required('Produk wajib dipilih!') + .min(1, 'Produk wajib dipilih!') + .typeError('Produk wajib dipilih!'), product_warehouse: Yup.object({ value: Yup.number().min(1).required(), label: Yup.string().required(), }).nullable(), product_warehouse_id: Yup.number() - .required('Product Warehouse wajib diisi!') - .min(0.001, 'Product Warehouse tidak boleh negatif!') - .typeError('Product Warehouse harus berupa angka!'), - sub_qty: Yup.number() + .required('Produk wajib dipilih!') + .min(1, 'Produk wajib dipilih!') + .typeError('Produk wajib dipilih!'), + sub_qty: Yup.mixed() .required('Sub Qty wajib diisi!') - .min(0.001, 'Sub Qty tidak boleh negatif!') - .typeError('Sub Qty harus berupa angka!'), + .test( + '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 = @@ -97,35 +106,50 @@ export const PurchaseRequestFormSchema: Yup.ObjectSchema 0); + }) + .typeError('Supplier wajib dipilih!'), area: Yup.object({ value: Yup.number().min(1).required(), label: Yup.string().required(), }).nullable(), area_id: Yup.number() - .required('Area wajib diisi!') - .test('is-valid-area', 'Area wajib diisi!', function (value) { + .required('Area wajib dipilih!') + .test('is-valid-area', 'Area wajib dipilih!', function (value) { if (!this.parent.area) return true; return Boolean(value && value > 0); }) - .typeError('Area wajib diisi!'), + .typeError('Area wajib dipilih!'), location: Yup.object({ value: Yup.number().min(1).required(), label: Yup.string().required(), }).nullable(), location_id: Yup.number() - .required('Lokasi wajib diisi!') - .test('is-valid-location', 'Lokasi wajib diisi!', function (value) { + .required('Lokasi wajib dipilih!') + .test('is-valid-location', 'Lokasi wajib dipilih!', function (value) { if (!this.parent.location) return true; return Boolean(value && value > 0); }) - .typeError('Lokasi wajib diisi!'), - credit_term: Yup.number() - .required('Termin kredit wajib diisi!') - .min(1, 'Termin kredit tidak boleh negatif!') - .typeError('Termin kredit harus berupa angka!'), + .typeError('Lokasi wajib dipilih!'), + credit_term: Yup.lazy((value, context) => { + const supplier_id = context.parent.supplier_id; + const hasSupplier = supplier_id && supplier_id > 0; + + if (!hasSupplier) { + return Yup.mixed() + .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), purchase_items: Yup.array() .of(PurchaseItemObjectSchema) diff --git a/src/components/pages/purchase/form/PurchaseRequestForm.tsx b/src/components/pages/purchase/form/PurchaseRequestForm.tsx index dce21146..a6ef549f 100644 --- a/src/components/pages/purchase/form/PurchaseRequestForm.tsx +++ b/src/components/pages/purchase/form/PurchaseRequestForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { useFormik } from 'formik'; import useSWR from 'swr'; import { useRouter } from 'next/navigation'; @@ -8,6 +8,7 @@ import { Icon } from '@iconify/react'; import { toast } from 'react-hot-toast'; import Button from '@/components/Button'; import TextInput from '@/components/input/TextInput'; +import NumberInput from '@/components/input/NumberInput'; import SelectInput, { OptionType, useSelect, @@ -27,6 +28,7 @@ import { LocationApi, WarehouseApi, } from '@/services/api/master-data'; +import { Supplier } from '@/types/api/master-data/supplier'; import { ProductWarehouseApi } from '@/services/api/inventory'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; 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 ===== const createPurchaseRequestHandler = useCallback( async (payload: CreatePurchaseRequestPayload) => { @@ -155,7 +166,8 @@ const PurchaseRequestForm = ({ setInputValue: setSupplierSelectInputValue, options: supplierOptions, isLoadingOptions: isLoadingSuppliers, - } = useSelect(SupplierApi.basePath, 'id', 'name', 'search'); + rawData: supplierRawData, + } = useSelect(SupplierApi.basePath, 'id', 'name', 'search'); const { inputValue: areaSelectInputValue, @@ -353,6 +365,20 @@ const PurchaseRequestForm = ({ formik.setFieldValue('supplier', supplier); formik.setFieldTouched('supplier_id', true); 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) => { @@ -509,15 +535,15 @@ const PurchaseRequestForm = ({ onInputChange={setSupplierSelectInputValue} isLoading={isLoadingSuppliers} isError={ - formik.touched.supplier_id && - Boolean(formik.errors.supplier_id) + formik.touched.supplier && Boolean(formik.errors.supplier_id) } errorMessage={formik.errors.supplier_id as string} isDisabled={type === 'detail'} isClearable /> -