'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 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 { 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(''); // ===== TYPE DEFINITIONS ===== interface ProductWarehouseOptionType extends OptionType { product_id: number; warehouse_id: number; warehouse_name: string; quantity: number; } // ===== UTILITY FUNCTIONS ===== const getPurchaseItemError = ( idx: number, field: 'warehouse_id' | 'product_warehouse_id' | 'product_id' | 'sub_qty' ): { isError: boolean; errorMessage: string } => { const touchedItem = formik.touched.purchase_items?.[idx]; const errorItem = formik.errors.purchase_items?.[idx] as | Record | undefined; if (!touchedItem) { return { isError: false, errorMessage: '' }; } const isTouched = (touchedItem as Record)?.[field]; const errorMessage = errorItem?.[field] || ''; return { isError: Boolean(isTouched && errorMessage), errorMessage: isTouched && errorMessage ? errorMessage : '', }; }; const getSupplierById = (supplierId: number): Supplier | null => { if (!isResponseSuccess(supplierRawData)) return null; return ( supplierRawData?.data.find( (supplier: Supplier) => supplier.id === supplierId ) || null ); }; // ===== 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 { 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: typeof values.credit_term === 'string' ? parseInt(values.credit_term) || 0 : values.credit_term || 0, notes: values.notes || '', purchase_items: (values.purchase_items || []).map((item) => ({ warehouse_id: typeof item.warehouse_id === 'string' ? parseInt(item.warehouse_id) || 0 : item.warehouse_id || 0, product_id: typeof item.product_id === 'string' ? parseInt(item.product_id) || 0 : item.product_id || 0, product_warehouse_id: typeof item.product_warehouse_id === 'string' ? parseInt(item.product_warehouse_id) || 0 : item.product_warehouse_id || 0, sub_qty: typeof item.sub_qty === 'string' ? parseFloat(item.sub_qty) || 0 : item.sub_qty || 0, })), }; switch (type) { case 'add': await createPurchaseRequestHandler(payload); break; case 'edit': await updatePurchaseRequestHandler( initialValues?.id as number, payload ); break; } }, }); // ===== API DATA FETCHING ===== const allProductWarehousesUrl = `${ProductWarehouseApi.basePath}`; const { data: productWarehouses, isLoading: isLoadingProductWarehouses } = useSWR(allProductWarehousesUrl, ProductWarehouseApi.getAllFetcher); const getProductWarehouseOptionsForItem = useCallback( (warehouseId: number | string) => { if (!isResponseSuccess(productWarehouses)) return []; const warehouseIdNum = typeof warehouseId === 'string' ? parseInt(warehouseId) || 0 : warehouseId; if (warehouseIdNum === 0) return []; return ( productWarehouses?.data .filter((pw) => pw.warehouse.id === warehouseIdNum) .map((pw) => ({ value: pw.product.id, label: pw.product.name, product_id: pw.product.id, warehouse_id: pw.warehouse.id, warehouse_name: pw.warehouse.name, quantity: pw.quantity, })) || [] ); }, [productWarehouses] ); const productUrl = useMemo(() => { const productIds = formik.values.purchase_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.purchase_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]); // ===== FIELD CHANGE HANDLERS ===== const supplierChangeHandler = (val: OptionType | OptionType[] | null) => { const supplier = val as OptionType | null; formik.setFieldTouched('supplier', true); 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 { // Reset credit_term field and its touched state when supplier is cleared formik.setFieldTouched('credit_term', false); formik.setFieldValue('credit_term', ''); } }; const areaChangeHandler = (val: OptionType | OptionType[] | null) => { const area = val as OptionType | null; formik.setFieldTouched('area', true); formik.setFieldValue('area', area); formik.setFieldTouched('area_id', true); formik.setFieldValue('area_id', (area as OptionType)?.value || 0); // Reset area dependent fields formik.setFieldTouched('location', false); formik.setFieldValue('location', undefined); formik.setFieldTouched('location_id', false); formik.setFieldValue('location_id', 0); setLocationSelectInputValue(''); // Reset area dependent fields in all purchase items if (formik.values.purchase_items) { formik.values.purchase_items.forEach((_, idx) => { formik.setFieldValue(`purchase_items.${idx}.warehouse`, null); formik.setFieldValue(`purchase_items.${idx}.warehouse_id`, ''); formik.setFieldValue(`purchase_items.${idx}.product_warehouse`, null); formik.setFieldValue( `purchase_items.${idx}.product_warehouse_id`, null ); formik.setFieldValue(`purchase_items.${idx}.product_id`, ''); }); } }; const locationChangeHandler = (val: OptionType | OptionType[] | null) => { const location = val as OptionType | null; formik.setFieldTouched('location', true); formik.setFieldValue('location', location); formik.setFieldTouched('location_id', true); formik.setFieldValue('location_id', (location as OptionType)?.value || 0); // Reset location dependent fields in all purchase items if (formik.values.purchase_items) { formik.values.purchase_items.forEach((_, idx) => { formik.setFieldValue(`purchase_items.${idx}.warehouse`, null); formik.setFieldValue(`purchase_items.${idx}.warehouse_id`, ''); formik.setFieldValue(`purchase_items.${idx}.product_warehouse`, null); formik.setFieldValue( `purchase_items.${idx}.product_warehouse_id`, null ); formik.setFieldValue(`purchase_items.${idx}.product_id`, ''); }); } }; // Purchase Items Handlers const addPurchaseItem = () => { const newPurchaseItems = [ ...(formik.values.purchase_items || []), { warehouse: null, warehouse_id: '', product: null, product_id: '', product_warehouse: null, product_warehouse_id: null, sub_qty: '', }, ]; formik.setFieldValue('purchase_items', newPurchaseItems); }; const removePurchaseItem = (idx: number) => { const updatedPurchaseItems = formik.values.purchase_items?.filter( (_, i) => i !== idx ); formik.setFieldValue('purchase_items', updatedPurchaseItems); }; const removeSelectedPurchaseItems = () => { const updatedPurchaseItems = formik.values.purchase_items?.filter( (_, idx) => !selectedPurchaseItems.includes(idx) ); formik.setFieldValue('purchase_items', updatedPurchaseItems); setSelectedPurchaseItems([]); }; // ===== PURCHASE ITEM OPERATIONS ===== const handlePurchaseItemChange = ( idx: number, field: 'sub_qty' | 'price', value: string | number ) => { if (field === 'sub_qty') { const numValue = typeof value === 'string' ? parseInt(value) || 0 : value; formik.setFieldValue(`purchase_items.${idx}.sub_qty`, numValue); } else if (field === 'price') { const numValue = typeof value === 'string' ? parseFloat(value) || 0 : value; formik.setFieldValue(`purchase_items.${idx}.price`, numValue); } }; return ( <>

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

{/* Basic Info Card */}
{/* Purchase Items Table */}
{type !== 'detail' && ( )} {type !== 'detail' && } {formik.values.purchase_items?.map((item, idx) => ( {type !== 'detail' && ( )} {type !== 'detail' && ( )} ))}
0 } onChange={(e) => { if (e.target.checked) { setSelectedPurchaseItems( formik.values.purchase_items?.map( (_, idx) => idx ) ?? [] ); } else { setSelectedPurchaseItems([]); } }} /> Gudang * Item * Jumlah * Estimasi Harga SatuanAction
{ if (e.target.checked) { setSelectedPurchaseItems([ ...selectedPurchaseItems, idx, ]); } else { setSelectedPurchaseItems( selectedPurchaseItems.filter((i) => i !== idx) ); } }} /> { const warehouse = val as OptionType | null; formik.setFieldValue( `purchase_items.${idx}.warehouse`, warehouse ); formik.setFieldValue( `purchase_items.${idx}.warehouse_id`, (warehouse as OptionType)?.value || 0 ); formik.setFieldTouched( `purchase_items.${idx}.product_warehouse`, false ); formik.setFieldValue( `purchase_items.${idx}.product_warehouse`, null ); formik.setFieldTouched( `purchase_items.${idx}.product_warehouse_id`, false ); formik.setFieldValue( `purchase_items.${idx}.product_warehouse_id`, 0 ); formik.setFieldTouched( `purchase_items.${idx}.product_id`, false ); formik.setFieldValue( `purchase_items.${idx}.product_id`, 0 ); }} options={warehouseOptions} onInputChange={setWarehouseSelectInputValue} isLoading={isLoadingWarehouses} isError={ getPurchaseItemError(idx, 'warehouse_id').isError } errorMessage={ getPurchaseItemError(idx, 'warehouse_id') .errorMessage } isDisabled={type === 'detail'} isClearable placeholder='Pilih Gudang' className={{ wrapper: 'min-w-32', }} /> { const productWarehouse = val as ProductWarehouseOptionType | null; formik.setFieldValue( `purchase_items.${idx}.product_warehouse`, productWarehouse ); formik.setFieldValue( `purchase_items.${idx}.product_warehouse_id`, (productWarehouse as ProductWarehouseOptionType) ?.value || 0 ); const productId = (productWarehouse as ProductWarehouseOptionType) ?.product_id || 0; formik.setFieldValue( `purchase_items.${idx}.product_id`, productId ); }} options={getProductWarehouseOptionsForItem( item.warehouse_id )} isLoading={isLoadingProductWarehouses} isError={ getPurchaseItemError(idx, 'product_warehouse_id') .isError } errorMessage={ getPurchaseItemError(idx, 'product_warehouse_id') .errorMessage } isDisabled={type === 'detail' || !item.warehouse_id} isClearable={type !== 'detail' && !!item.warehouse_id} placeholder={ !item.warehouse_id ? 'Pilih Gudang terlebih dahulu' : 'Pilih Produk' } className={{ wrapper: 'min-w-32', }} /> handlePurchaseItemChange( idx, 'sub_qty', e.target.value ) } onBlur={formik.handleBlur} placeholder='Masukkan kuantitas' readOnly={type === 'detail'} allowNegative={false} decimalScale={0} isError={getPurchaseItemError(idx, 'sub_qty').isError} errorMessage={ getPurchaseItemError(idx, 'sub_qty').errorMessage } className={{ wrapper: 'min-w-24', }} /> handlePurchaseItemChange( idx, 'price', e.target.value ) } 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;