'use client'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useFormik } from 'formik'; import useSWR from 'swr'; import { useRouter } from 'next/navigation'; 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 CheckboxInput from '@/components/input/CheckboxInput'; import SelectInput, { OptionType, useSelect, } from '@/components/input/SelectInput'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import { useModal } from '@/components/Modal'; import AlertErrorList from '@/components/helper/form/FormErrors'; import { PurchaseRequestFormSchema, PurchaseRequestFormValues, getPurchaseRequestFormInitialValues, UpdatePurchaseRequestFormSchema, } from '@/components/pages/purchase/form/request/PurchaseRequestForm.schema'; import { SupplierApi, AreaApi, LocationApi, WarehouseApi, } from '@/services/api/master-data'; import { Supplier, SupplierProducts } from '@/types/api/master-data/supplier'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; import { formatNumber } from '@/lib/helper'; import { getUniqueFormikErrors } from '@/lib/formik-helper'; import { PurchaseApi } from '@/services/api/purchase'; import Card from '@/components/Card'; import { CreatePurchaseRequestPayload, Purchase, } from '@/types/api/purchase/purchase'; interface PurchaseRequestFormProps { type?: 'add' | 'edit' | 'detail'; initialValues?: Purchase; } const PurchaseRequestForm = ({ type = 'add', initialValues, }: PurchaseRequestFormProps) => { const router = useRouter(); const deleteModal = useModal(); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [locationSelectInputValue, setLocationSelectInputValue] = useState(''); const [selectedPurchaseItems, setSelectedPurchaseItems] = useState( [] ); const [purchaseRequestFormErrorMessage, setPurchaseRequestFormErrorMessage] = useState(''); const [formErrorList, setFormErrorList] = useState([]); const [selectedArea, setSelectedArea] = useState(''); const [selectedLocation, setSelectedLocation] = useState(''); const [disabledLocation, setDisabledLocation] = useState(true); // ===== UTILITY FUNCTIONS ===== const isRepeaterInputError = ( idx: number, field: 'warehouse_id' | 'product_id' | 'qty' ): { isError: boolean; errorMessage: string } => { if (!formik.touched.items || !Array.isArray(formik.touched.items)) { return { isError: false, errorMessage: '', }; } const touchedField = ( formik.touched.items[idx] as Partial<{ warehouse_id: boolean; product_id: boolean; qty: boolean; }> )?.[field]; const errorItem = formik.errors.items?.[idx] as | Record | undefined; return { isError: Boolean(touchedField && Boolean(errorItem?.[field])), errorMessage: touchedField && errorItem?.[field] ? errorItem[field] : '', }; }; // ===== SUBMISSION HANDLERS ===== const createPurchaseRequestHandler = useCallback( async (payload: CreatePurchaseRequestPayload) => { const res = await PurchaseApi.create(payload); if (isResponseError(res)) { setPurchaseRequestFormErrorMessage(res.message); return; } toast.success(res?.message as string); router.push('/purchase'); }, [router] ); const updatePurchaseRequestHandler = useCallback( async ( purchaseRequestId: number, payload: CreatePurchaseRequestPayload ) => { const res = await PurchaseApi.update(purchaseRequestId, payload); if (isResponseError(res)) { setPurchaseRequestFormErrorMessage(res.message); return; } toast.success(res?.message as string); router.refresh(); router.push('/purchase'); }, [router] ); const deletePurchaseRequestClickHandler = useCallback(() => { deleteModal.openModal(); }, [deleteModal]); const confirmationModalDeleteClickHandler = useCallback(async () => { if (!initialValues?.id) return; setIsDeleteLoading(true); await PurchaseApi.delete(initialValues.id); deleteModal.closeModal(); toast.success('Successfully delete Purchase Request!'); setIsDeleteLoading(false); router.push('/purchase'); }, [deleteModal, initialValues?.id, router]); // ===== SELECT INPUT DATA ===== const { setInputValue: setSupplierSelectInputValue, options: supplierOptions, isLoadingOptions: isLoadingSuppliers, rawData: supplierRawData, loadMore: loadMoreSuppliers, hasMore: hasMoreSuppliers, } = useSelect(SupplierApi.basePath, 'id', 'name', 'search', { category: 'SAPRONAK', }); const { setInputValue: setAreaSelectInputValue, options: areaOptions, isLoadingOptions: isLoadingAreas, } = useSelect(AreaApi.basePath, 'id', 'name', 'search'); const { options: locationOptions, isLoadingOptions: isLoadingLocations, loadMore: loadMoreLocations, hasMore: hasMoreLocations, } = useSelect(LocationApi.basePath, 'id', 'name', '', { area_id: selectedArea != '' ? selectedArea : ((initialValues?.area?.id ?? '') as string), }); const { inputValue: warehouseSelectInputValue, setInputValue: setWarehouseSelectInputValue, options: warehouseOptions, isLoadingOptions: isLoadingWarehouses, loadMore: loadMoreWarehouses, hasMore: hasMoreWarehouses, } = useSelect(WarehouseApi.basePath, 'id', 'name', 'search', { area_id: selectedArea != '' ? selectedArea : ((initialValues?.area?.id ?? '') as string), location_id: selectedLocation != '' ? selectedLocation : ((initialValues?.location?.id ?? '') as string), }); // ===== FORM CONFIGURATION ===== const formikInitialValues = useMemo( () => getPurchaseRequestFormInitialValues(initialValues), [initialValues] ); const formik = useFormik({ initialValues: formikInitialValues, validationSchema: type === 'edit' ? UpdatePurchaseRequestFormSchema : PurchaseRequestFormSchema, validateOnChange: true, validateOnBlur: true, validateOnMount: false, enableReinitialize: true, onSubmit: async (values) => { const payload: CreatePurchaseRequestPayload = { supplier_id: typeof values.supplier_id === 'string' ? parseInt(values.supplier_id) || 0 : values.supplier_id || 0, credit_term: typeof values.credit_term === 'string' ? parseInt(values.credit_term) || 0 : values.credit_term || 0, notes: values.notes || '', items: (values.items || []).map((item) => ({ warehouse_id: Number(item.warehouse_id) || 0, product_id: Number(item.product_id) || 0, qty: Number(item.qty) || 0, })), }; switch (type) { case 'add': await createPurchaseRequestHandler(payload); break; case 'edit': await updatePurchaseRequestHandler( initialValues?.id as number, payload ); break; } }, }); const handleValidateForm = async () => { const errors = await formik.validateForm(); if (Object.keys(errors).length > 0) { const errorMessages = getUniqueFormikErrors(errors); setFormErrorList(errorMessages); return; } }; const handleFormSubmit = (e: React.FormEvent) => { e.preventDefault(); handleValidateForm(); formik.handleSubmit(e); }; // ===== API DATA FETCHING ===== const { data: supplierData, isLoading: isLoadingProducts } = useSWR( formik.values.supplier_id && Number(formik.values.supplier_id) > 0 ? formik.values.supplier_id?.toString() : null, (id: string) => SupplierApi.getSingle(Number(id)) ); const supplierProductOptions = useMemo(() => { if (!supplierData || !isResponseSuccess(supplierData)) { return []; } const supplier = supplierData.data as SupplierProducts; const products = supplier.products || []; return products.map((product) => ({ value: product.id, label: product.name, })); }, [supplierData]); const supplierProductData = useMemo(() => { if (!supplierData || !isResponseSuccess(supplierData)) { return {}; } const supplier = supplierData.data as SupplierProducts; const products = supplier.products || []; const data: Record[0]> = {}; products.forEach((product) => { data[product.id] = product; }); return data; }, [supplierData]); const addPurchaseItem = () => { const newItems = [ ...(formik.values.items || []), { warehouse: null, warehouse_id: 0, product: null, product_id: 0, qty: 0, }, ]; formik.setFieldValue('items', newItems); }; const removePurchaseItem = (idx: number) => { const updatedPurchaseItems = formik.values.items?.filter( (_, i) => i !== idx ); formik.setFieldValue('items', updatedPurchaseItems); }; const removeSelectedPurchaseItems = () => { const updatedPurchaseItems = formik.values.items?.filter( (_, idx) => !selectedPurchaseItems.includes(idx) ); formik.setFieldValue('items', updatedPurchaseItems); setSelectedPurchaseItems([]); }; // ===== UTILITY FUNCTIONS ===== const updateCreditTermBasedOnSupplier = useCallback( (supplierId: number) => { if (supplierId > 0 && isResponseSuccess(supplierRawData)) { const supplierData = supplierRawData.data.find( (s: Supplier) => s.id === supplierId ); if (supplierData?.due_date) { formik.setFieldTouched('credit_term', false); formik.setFieldValue('credit_term', supplierData.due_date.toString()); } else { formik.setFieldTouched('credit_term', false); formik.setFieldValue('credit_term', ''); } } else { formik.setFieldTouched('credit_term', false); formik.setFieldValue('credit_term', ''); } }, [supplierRawData] ); const resetPurchaseItems = useCallback(() => { if (formik.values.items) { formik.values.items.forEach((_, idx) => { formik.setFieldTouched(`items.${idx}.product`, false); formik.setFieldValue(`items.${idx}.product`, null); formik.setFieldTouched(`items.${idx}.product_id`, false); formik.setFieldValue(`items.${idx}.product_id`, 0); formik.setFieldTouched(`items.${idx}.qty`, false); formik.setFieldValue(`items.${idx}.qty`, 0); }); } }, []); // ===== SIDE EFFECTS ===== useEffect(() => { if (formik.values.supplier_id && Number(formik.values.supplier_id) > 0) { updateCreditTermBasedOnSupplier(Number(formik.values.supplier_id)); resetPurchaseItems(); } else { formik.setFieldTouched('credit_term', false); formik.setFieldValue('credit_term', ''); resetPurchaseItems(); } }, [formik.values.supplier_id]); useEffect(() => { if (type !== 'add' && initialValues) { if (initialValues.area?.id) { setSelectedArea(initialValues.area.id.toString()); setDisabledLocation(false); } if (initialValues.location?.id) { setSelectedLocation(initialValues.location.id.toString()); } } }, [type, initialValues]); // ===== FORM HANDLERS ===== const handleSupplierChange = useCallback( (val: OptionType | OptionType[] | null) => { const supplier = val as OptionType | null; const supplierId = Number(supplier?.value); formik.setFieldTouched('supplier', true); formik.setFieldValue('supplier', supplier); formik.setFieldTouched('supplier_id', true); formik.setFieldValue('supplier_id', supplierId); }, [] ); const handleCreditTermChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; formik.setFieldTouched('credit_term', true); formik.setFieldValue('credit_term', value); }, [] ); const handleCreditTermBlur = useCallback( (e: React.FocusEvent) => { formik.handleBlur(e); }, [formik] ); const handleAreaChange = useCallback( (val: OptionType | OptionType[] | null) => { const area = val as OptionType | null; formik.setFieldTouched('area_id', true); formik.setFieldValue('area_id', (area as OptionType)?.value || 0); formik.setFieldTouched('area', true); formik.setFieldValue('area', area); setSelectedArea((area as OptionType)?.value as string); setSelectedLocation(''); const disabled = (area as OptionType)?.value == null; setDisabledLocation(disabled); formik.setFieldTouched('location_id', false); formik.setFieldValue('location_id', 0); formik.setFieldTouched('location', false); formik.setFieldValue('location', null); }, [] ); const handleLocationChange = useCallback( (val: OptionType | OptionType[] | null) => { const location = val as OptionType | null; formik.setFieldTouched('location_id', true); formik.setFieldValue('location_id', (location as OptionType)?.value || 0); formik.setFieldTouched('location', true); formik.setFieldValue('location', location); setSelectedLocation((location as OptionType)?.value as string); }, [] ); const handleWarehouseChange = useCallback( (idx: number, val: OptionType | OptionType[] | null) => { const warehouse = val as OptionType | null; const warehouseId = (warehouse as OptionType)?.value || 0; formik.setFieldTouched(`items.${idx}.warehouse`, true); formik.setFieldValue(`items.${idx}.warehouse`, warehouse); formik.setFieldTouched(`items.${idx}.warehouse_id`, true); formik.setFieldValue(`items.${idx}.warehouse_id`, warehouseId); }, [] ); // ===== PURCHASE ITEM OPERATIONS ===== const handlePurchaseItemChange = ( idx: number, field: 'qty', value: string | number ) => { if (field === 'qty') { const numValue = typeof value === 'string' ? parseFloat(value) || 0 : value; formik.setFieldTouched(`items.${idx}.qty`, true); formik.setFieldValue(`items.${idx}.qty`, numValue); } }; return ( <>

{type === 'add' && 'Tambah Purchase Request'} {type === 'edit' && 'Edit Purchase Request'} {type === 'detail' && 'Detail Purchase Request'}

{purchaseRequestFormErrorMessage && (
{purchaseRequestFormErrorMessage}
)} {/* Error List Alert */} {formErrorList.length > 0 && ( setFormErrorList([])} /> )} {/* Basic Info Card */}
{/* Purchase Items Table */}
{type !== 'detail' && ( )} {type !== 'detail' && } {formik.values.items?.map((item, idx) => ( {type !== 'detail' && ( )} {type !== 'detail' && ( )} ))}
0 } onChange={( e: React.ChangeEvent ) => { if (e.target.checked) { setSelectedPurchaseItems( formik.values.items?.map((_, idx) => idx) ?? [] ); } else { setSelectedPurchaseItems([]); } }} classNames={{ wrapper: 'flex justify-center', checkbox: 'checkbox checkbox-sm', }} /> Gudang * Item * Jumlah * Estimasi Harga SatuanAction
) => { if (e.target.checked) { setSelectedPurchaseItems([ ...selectedPurchaseItems, idx, ]); } else { setSelectedPurchaseItems( selectedPurchaseItems.filter((i) => i !== idx) ); } }} classNames={{ wrapper: 'flex justify-center', checkbox: 'checkbox checkbox-sm', }} /> handleWarehouseChange(idx, val)} options={warehouseOptions} onInputChange={setWarehouseSelectInputValue} isLoading={isLoadingWarehouses} onMenuScrollToBottom={loadMoreWarehouses} isError={ isRepeaterInputError(idx, 'warehouse_id').isError } errorMessage={ isRepeaterInputError(idx, 'warehouse_id') .errorMessage } isDisabled={type === 'detail'} isClearable={type !== 'detail'} className={{ wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80', }} /> { const product = val as OptionType | null; const productId = (product as OptionType)?.value || 0; formik.setFieldTouched( `items.${idx}.product`, true ); formik.setFieldValue( `items.${idx}.product`, product ); formik.setFieldTouched( `items.${idx}.product_id`, true ); formik.setFieldValue( `items.${idx}.product_id`, productId ); }} options={supplierProductOptions} isLoading={isLoadingProducts} isError={ isRepeaterInputError(idx, 'product_id').isError } errorMessage={ isRepeaterInputError(idx, 'product_id').errorMessage } isDisabled={ type === 'detail' || !formik.values.supplier_id } isClearable={ type !== 'detail' && !!formik.values.supplier_id } placeholder={ !formik.values.supplier_id ? 'Pilih Vendor terlebih dahulu' : 'Pilih Produk' } className={{ wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80', }} /> handlePurchaseItemChange(idx, 'qty', e.target.value) } onBlur={formik.handleBlur} placeholder={ !formik.values.supplier_id ? 'Pilih Vendor terlebih dahulu' : 'Masukkan kuantitas' } readOnly={ type === 'detail' || !formik.values.supplier_id } disabled={ type === 'detail' || !formik.values.supplier_id } allowNegative={false} decimalScale={0} isError={isRepeaterInputError(idx, 'qty').isError} errorMessage={ isRepeaterInputError(idx, 'qty').errorMessage } className={{ wrapper: 'w-full min-w-32 md:min-w-48 lg:min-w-52', }} /> {}} onBlur={formik.handleBlur} type='text' className={{ wrapper: 'w-full min-w-32 md:min-w-48 lg:min-w-52', }} disabled={true} readOnly={true} inputPrefix={'Rp'} placeholder={ item.product_id ? 'Loading...' : 'Pilih produk terlebih dahulu' } bottomLabel={ item.product_id && supplierProductData[item.product_id] ? `Harga per unit: Rp ${formatNumber( supplierProductData[item.product_id] .ProductPrice )}` : '' } />
{type !== 'detail' && (
{selectedPurchaseItems.length > 0 && ( )}
)}
{/* Action buttons */}
{type !== 'detail' && (
)} {type === 'detail' && (
)}
{type !== 'add' && ( )} ); }; export default PurchaseRequestForm;