'use client'; import { useCallback, 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 { PurchaseRequestFormSchema, PurchaseRequestFormValues, getPurchaseRequestFormInitialValues, UpdatePurchaseRequestFormSchema, } from './PurchaseRequestForm.schema'; import { SupplierApi, AreaApi, LocationApi, WarehouseApi, ProductApi, } from '@/services/api/master-data'; import { Supplier } from '@/types/api/master-data/supplier'; import { Product } from '@/types/api/master-data/product'; import { ProductWarehouseApi } from '@/services/api/inventory'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; import { PurchaseRequestApi } 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(''); // ===== TYPE DEFINITIONS ===== interface ProductWarehouseOptionType extends OptionType { product_id: number; product_warehouse_id: number; warehouse_id: number; warehouse_name: string; qty: number; } // ===== UTILITY FUNCTIONS ===== const getPurchaseItemError = ( idx: number, field: 'product_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]?.[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 PurchaseRequestApi.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 PurchaseRequestApi.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 PurchaseRequestApi.delete(initialValues.id); deleteModal.closeModal(); toast.success('Successfully delete Purchase Request!'); setIsDeleteLoading(false); router.push('/purchase'); }, [deleteModal, initialValues?.id, router]); // ===== SELECT INPUT DATA ===== const { inputValue: supplierSelectInputValue, setInputValue: setSupplierSelectInputValue, options: supplierOptions, isLoadingOptions: isLoadingSuppliers, rawData: supplierRawData, } = useSelect(SupplierApi.basePath, 'id', 'name', 'search'); const { inputValue: areaSelectInputValue, setInputValue: setAreaSelectInputValue, options: areaOptions, isLoadingOptions: isLoadingAreas, } = useSelect(AreaApi.basePath, 'id', 'name', 'search'); const { inputValue: warehouseSelectInputValue, setInputValue: setWarehouseSelectInputValue, isLoadingOptions: isLoadingWarehouses, } = useSelect(WarehouseApi.basePath, 'id', 'name', 'search'); // ===== FORM CONFIGURATION ===== const formikInitialValues = useMemo( () => getPurchaseRequestFormInitialValues(initialValues), [initialValues] ); const formik = useFormik({ initialValues: formikInitialValues, validationSchema: type === 'edit' ? UpdatePurchaseRequestFormSchema : PurchaseRequestFormSchema, validateOnChange: true, validateOnBlur: 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: values.credit_term || 0, notes: values.notes || '', items: (values.items || []).map((item) => ({ warehouse_id: typeof values.warehouse_id === 'string' ? parseInt(values.warehouse_id) || 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, })), }; switch (type) { case 'add': await createPurchaseRequestHandler(payload); break; case 'edit': await updatePurchaseRequestHandler( initialValues?.id as number, payload ); break; } }, }); // ===== API DATA FETCHING ===== const productWarehousesUrl = useMemo(() => { if (!formik.values.warehouse_id || formik.values.warehouse_id === 0) { 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 ); const productData = useMemo(() => { if (!isResponseSuccess(productsResponse)) return {}; const data: Record = {}; productsResponse?.data?.forEach((product: Product) => { data[product.id] = product; }); return data; }, [productsResponse]); const locationsUrl = useMemo(() => { const params = new URLSearchParams({ search: locationSelectInputValue, ...(formik.values.area_id && formik.values.area_id > 0 ? { area_id: formik.values.area_id.toString() } : {}), }); return `${LocationApi.basePath}?${params.toString()}`; }, [locationSelectInputValue, formik.values.area_id]); const { data: locations, isLoading: isLoadingLocations } = useSWR( locationsUrl, LocationApi.getAllFetcher ); const locationOptions = useMemo(() => { if (!isResponseSuccess(locations)) return []; return ( locations?.data.map((location) => ({ value: location.id, label: location.name, })) || [] ); }, [locations]); const warehousesUrl = useMemo(() => { const params = new URLSearchParams({ search: warehouseSelectInputValue }); if (formik.values.area_id && formik.values.area_id > 0) { params.append('area_id', formik.values.area_id.toString()); } if (formik.values.location_id && formik.values.location_id > 0) { params.append('location_id', formik.values.location_id.toString()); } return `${WarehouseApi.basePath}?${params.toString()}`; }, [ warehouseSelectInputValue, formik.values.area_id, formik.values.location_id, ]); const { data: warehouses } = useSWR( warehousesUrl, WarehouseApi.getAllFetcher ); const warehouseOptions = useMemo(() => { if (!isResponseSuccess(warehouses)) return []; return ( 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]); // Purchase Items Handlers const addPurchaseItem = () => { const newPurchaseItems = [ ...(formik.values.items || []), { product_warehouse: null, product_warehouse_id: null, product: null, product_id: '', qty: '', }, ]; formik.setFieldValue('items', newPurchaseItems); }; 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([]); }; // ===== 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'}

{/* Basic Info Card */}
{ const supplier = val as OptionType | null; const supplierId = supplier?.value ? typeof supplier.value === 'string' ? parseInt(supplier.value) : supplier.value : 0; formik.setFieldTouched('supplier_id', true); formik.setFieldValue('supplier_id', supplierId); formik.setFieldTouched('supplier', true); formik.setFieldValue('supplier', supplier); if (supplierId > 0 && isResponseSuccess(supplierRawData)) { const supplierData = supplierRawData.data.find( (s: Supplier) => s.id === supplierId ); if (supplierData?.due_date) { formik.setFieldTouched('credit_term', true); formik.setFieldValue( 'credit_term', supplierData.due_date.toString() ); } } else { formik.setFieldTouched('credit_term', false); formik.setFieldValue('credit_term', ''); } }} options={supplierOptions} onInputChange={setSupplierSelectInputValue} isLoading={isLoadingSuppliers} isError={ formik.touched.supplier_id && Boolean(formik.errors.supplier_id) } errorMessage={formik.errors.supplier_id as string} isDisabled={type === 'detail'} isClearable /> { 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); formik.setFieldTouched('location', false); formik.setFieldValue('location', undefined); formik.setFieldTouched('location_id', false); formik.setFieldValue('location_id', 0); setLocationSelectInputValue(''); if (formik.values.items) { 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_id`, ''); }); } }} 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 /> { 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); if (formik.values.items) { 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_id`, ''); }); } }} 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' || !formik.values.area_id} isClearable={type !== 'detail' && !!formik.values.area_id} key={`location-${formik.values.area_id}`} />
{/* 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', }} /> { const warehouse = val as OptionType | null; formik.setFieldTouched('warehouse_id', true); formik.setFieldValue( 'warehouse_id', (warehouse as OptionType)?.value || 0 ); formik.setFieldTouched('warehouse', true); formik.setFieldValue('warehouse', warehouse); if (formik.values.items) { 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_id`, '' ); }); } }} options={warehouseOptions} onInputChange={setWarehouseSelectInputValue} isLoading={isLoadingWarehouses} isError={ formik.touched.warehouse_id && Boolean(formik.errors.warehouse_id) } errorMessage={formik.errors.warehouse_id as string} isDisabled={ type === 'detail' || !formik.values.area_id || !formik.values.location_id } isClearable={ type !== 'detail' && !!formik.values.area_id && !!formik.values.location_id } key={`warehouse-${formik.values.area_id}-${formik.values.location_id}`} /> { const productWarehouse = 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 = (productWarehouse as ProductWarehouseOptionType) ?.product_id || 0; formik.setFieldTouched( `items.${idx}.product_id`, true ); formik.setFieldValue( `items.${idx}.product_id`, productId ); }} options={getProductWarehouseOptionsForItem()} isLoading={isLoadingProductWarehouses} isError={ getPurchaseItemError(idx, 'product_warehouse_id') .isError } errorMessage={ getPurchaseItemError(idx, 'product_warehouse_id') .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' } className={{ wrapper: 'min-w-32', }} /> handlePurchaseItemChange(idx, 'qty', e.target.value) } onBlur={formik.handleBlur} placeholder='Masukkan kuantitas' readOnly={type === 'detail'} allowNegative={false} decimalScale={0} isError={getPurchaseItemError(idx, 'qty').isError} errorMessage={ getPurchaseItemError(idx, 'qty').errorMessage } className={{ wrapper: 'min-w-24', }} /> {}} onBlur={formik.handleBlur} type='text' className={{ wrapper: 'min-w-24', }} disabled={true} readOnly={true} inputPrefix={'Rp'} placeholder={ item.product_id ? 'Loading...' : 'Pilih produk terlebih dahulu' } bottomLabel={ item.product_id && productData[item.product_id] ? `Harga per unit: Rp ${productData[ item.product_id ].product_price.toLocaleString('en-US')}` : '' } />
{type !== 'detail' && (
{selectedPurchaseItems.length > 0 && ( )}
)}
{/* Action buttons */}
{type !== 'detail' && (
)} {type === 'detail' && (
)}
{purchaseRequestFormErrorMessage && (
{purchaseRequestFormErrorMessage}
)}
{type !== 'add' && ( )} ); }; export default PurchaseRequestForm;