diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index d2c91168..6d6317da 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -37,24 +37,102 @@ interface MovementFormProps { } const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { + // ===== STATE MANAGEMENT ===== const [, setMovementFormErrorMessage] = useState(''); - const [ - productWarehouseSelectInputValue, - setProductWarehouseSelectInputValue, - ] = useState(''); + const [productWarehouseSelectInputValue, setProductWarehouseSelectInputValue] = useState(''); const [selectedProducts, setSelectedProducts] = useState([]); const [selectedDeliveries, setSelectedDeliveries] = useState([]); + const [warehouseSelectInputValue, setWarehouseSelectInputValue] = useState(''); + const [supplierSelectInputValue, setSupplierSelectInputValue] = useState(''); + // ===== FORM HANDLERS ===== const { - deleteModal, movementFormErrorMessage, - isDeleteLoading, createMovementHandler, updateMovementHandler, - deleteMovementClickHandler, - confirmationModalDeleteClickHandler, } = useMovementFormHandlers(initialValues?.id); + // ===== INTERFACES ===== + interface WarehouseOptionType extends OptionType { + area?: string; + location?: string; + } + + interface ProductWarehouseOptionType extends OptionType { + product_id: number; + warehouse_id: number; + warehouse_name: string; + quantity: number; + } + + // ===== API DATA FETCHING ===== + const allProductWarehousesUrl = `${ProductWarehouseApi.basePath}`; + const { data: allProductWarehouses } = useSWR( + allProductWarehousesUrl, + ProductWarehouseApi.getAllFetcher + ); + + const warehousesUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ search: warehouseSelectInputValue }).toString()}`; + const { data: warehouses, isLoading: isLoadingWarehouses } = useSWR( + warehousesUrl, + WarehouseApi.getAllFetcher + ); + + const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ search: supplierSelectInputValue }).toString()}`; + const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR( + suppliersUrl, + SupplierApi.getAllFetcher + ); + + // ===== DATA PROCESSING ===== + const warehouseStockMap = useMemo(() => { + if (!isResponseSuccess(allProductWarehouses)) return new Map(); + + const stockMap = new Map< + number, + { totalQty: number; productCount: number } + >(); + + allProductWarehouses.data.forEach((pw) => { + const warehouseId = pw.warehouse.id; + const existing = stockMap.get(warehouseId) || { + totalQty: 0, + productCount: 0, + }; + + stockMap.set(warehouseId, { + totalQty: existing.totalQty + pw.quantity, + productCount: existing.productCount + 1, + }); + }); + + return stockMap; + }, [allProductWarehouses]); + + const warehouseOptions = isResponseSuccess(warehouses) + ? warehouses?.data.map((w) => { + const stockInfo = warehouseStockMap.get(w.id); + const stockLabel = stockInfo + ? ` (Stock: ${stockInfo.totalQty.toLocaleString('id-ID')} items, ${stockInfo.productCount} produk)` + : ' (Kosong)'; + + return { + value: w.id, + label: `${w.name}${stockLabel}`, + area: w.area?.name, + location: + 'type' in w && (w.type === 'LOKASI' || w.type === 'KANDANG') + ? w.location?.name + : undefined, + }; + }) + : []; + + const supplierOptions = isResponseSuccess(suppliers) + ? suppliers?.data.map((s) => ({ value: s.id, label: s.name })) + : []; + + // ===== FORM INITIALIZATION ===== const formikInitialValues = useMemo( () => getMovementFormInitialValues(initialValues), [initialValues] @@ -77,7 +155,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { if (d.document && d.document instanceof File) { documents.push(d.document); documentIndex = documents.length - 1; - } else { } return { @@ -122,91 +199,39 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { }, }); - const addProduct = () => { - const newProducts = [ - ...(formik.values.products || []), - { - product: null, - product_id: 0, - product_qty: 0, - }, - ]; - formik.setFieldValue('products', newProducts); - }; + // ===== PRODUCT WAREHOUSE FETCHING (after form initialization) ===== + const getProductWarehousesUrl = useCallback(() => { + const productWarehouseParams = new URLSearchParams({ + search: productWarehouseSelectInputValue, + }); + if (formik.values.source_warehouse_id) { + productWarehouseParams.append( + 'warehouse_id', + formik.values.source_warehouse_id.toString() + ); + } + return `${ProductWarehouseApi.basePath}?${productWarehouseParams.toString()}`; + }, [formik.values.source_warehouse_id, productWarehouseSelectInputValue]); - const removeProduct = useCallback( - (i: number) => { - const updatedProducts = - formik.values.products?.reduce((acc: ProductSchema[], item, index) => { - if (index !== i) { - acc.push(item); - } - return acc; - }, []) ?? []; + const productWarehousesUrl = getProductWarehousesUrl(); + const { data: productWarehouses, isLoading: isLoadingProductWarehouses } = + useSWR( + formik.values.source_warehouse_id ? productWarehousesUrl : null, + ProductWarehouseApi.getAllFetcher + ); - formik.setFieldValue('products', updatedProducts); - }, - [formik] - ); - - const bulkRemoveProduct = useCallback(() => { - const updatedProducts = - formik.values.products?.filter( - (_, idx) => !selectedProducts.includes(idx) - ) ?? []; - formik.setFieldValue('products', updatedProducts); - setSelectedProducts([]); - }, [formik, selectedProducts]); - - const addDelivery = () => { - formik.setFieldValue('deliveries', [ - ...(formik.values.deliveries || []), - { - delivery_cost: undefined, - delivery_cost_per_item: undefined, - document: null, - driver_name: '', - vehicle_plate: '', - supplier: null, - supplier_id: 0, - products: [ - { - product: null, - product_id: 0, - product_qty: 0, - }, - ], - }, - ]); - }; - - const removeDelivery = useCallback( - (i: number) => { - const updatedDeliveries = - formik.values.deliveries?.reduce( - (acc: DeliverySchema[], item, index) => { - if (index !== i) { - acc.push(item); - } - return acc; - }, - [] - ) ?? []; - - formik.setFieldValue('deliveries', updatedDeliveries); - }, - [formik] - ); - - const bulkRemoveDelivery = useCallback(() => { - const updatedDeliveries = - formik.values.deliveries?.filter( - (_, idx) => !selectedDeliveries.includes(idx) - ) ?? []; - formik.setFieldValue('deliveries', updatedDeliveries); - setSelectedDeliveries([]); - }, [formik, selectedDeliveries]); + const productWarehouseOptions = isResponseSuccess(productWarehouses) + ? productWarehouses?.data.map((pw) => ({ + value: pw.product.id, + label: `${pw.product.name} - ${pw.warehouse.name} (Stock: ${pw.quantity.toLocaleString('id-ID')})`, + product_id: pw.product.id, + warehouse_id: pw.warehouse.id, + warehouse_name: pw.warehouse.name, + quantity: pw.quantity, + })) + : []; + // ===== HELPER FUNCTIONS ===== const isRepeaterInputError = ( arrayName: T, column: T extends 'products' ? keyof ProductSchema : keyof DeliverySchema, @@ -263,118 +288,98 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { }; }; - interface WarehouseOptionType extends OptionType { - area?: string; - location?: string; - } + // ===== EVENT HANDLERS ===== + // Product Handlers + const addProduct = () => { + const newProducts = [ + ...(formik.values.products || []), + { + product: null, + product_id: 0, + product_qty: 0, + }, + ]; + formik.setFieldValue('products', newProducts); + }; - interface ProductWarehouseOptionType extends OptionType { - product_id: number; - warehouse_id: number; - warehouse_name: string; - quantity: number; - } + const removeProduct = useCallback( + (i: number) => { + const updatedProducts = + formik.values.products?.reduce((acc: ProductSchema[], item, index) => { + if (index !== i) { + acc.push(item); + } + return acc; + }, []) ?? []; - const allProductWarehousesUrl = `${ProductWarehouseApi.basePath}`; - const { data: allProductWarehouses } = useSWR( - allProductWarehousesUrl, - ProductWarehouseApi.getAllFetcher + formik.setFieldValue('products', updatedProducts); + }, + [formik] ); - const warehouseStockMap = useMemo(() => { - if (!isResponseSuccess(allProductWarehouses)) return new Map(); + const bulkRemoveProduct = useCallback(() => { + const updatedProducts = + formik.values.products?.filter( + (_, idx) => !selectedProducts.includes(idx) + ) ?? []; + formik.setFieldValue('products', updatedProducts); + setSelectedProducts([]); + }, [formik, selectedProducts]); - const stockMap = new Map< - number, - { totalQty: number; productCount: number } - >(); + // Delivery Handlers + const addDelivery = () => { + formik.setFieldValue('deliveries', [ + ...(formik.values.deliveries || []), + { + delivery_cost: undefined, + delivery_cost_per_item: undefined, + document: null, + driver_name: '', + vehicle_plate: '', + supplier: null, + supplier_id: 0, + products: [ + { + product: null, + product_id: 0, + product_qty: 0, + }, + ], + }, + ]); + }; - allProductWarehouses.data.forEach((pw) => { - const warehouseId = pw.warehouse.id; - const existing = stockMap.get(warehouseId) || { - totalQty: 0, - productCount: 0, - }; + const removeDelivery = useCallback( + (i: number) => { + const updatedDeliveries = + formik.values.deliveries?.reduce( + (acc: DeliverySchema[], item, index) => { + if (index !== i) { + acc.push(item); + } + return acc; + }, + [] + ) ?? []; - stockMap.set(warehouseId, { - totalQty: existing.totalQty + pw.quantity, - productCount: existing.productCount + 1, - }); - }); - - return stockMap; - }, [allProductWarehouses]); - - // Warehouse selection - const [warehouseSelectInputValue, setWarehouseSelectInputValue] = - useState(''); - const warehousesUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ search: warehouseSelectInputValue }).toString()}`; - const { data: warehouses, isLoading: isLoadingWarehouses } = useSWR( - warehousesUrl, - WarehouseApi.getAllFetcher + formik.setFieldValue('deliveries', updatedDeliveries); + }, + [formik] ); - const warehouseOptions = isResponseSuccess(warehouses) - ? warehouses?.data.map((w) => { - const stockInfo = warehouseStockMap.get(w.id); - const stockLabel = stockInfo - ? ` (Stock: ${stockInfo.totalQty.toLocaleString('id-ID')} items, ${stockInfo.productCount} produk)` - : ' (Kosong)'; - return { - value: w.id, - label: `${w.name}${stockLabel}`, - area: w.area?.name, - location: - 'type' in w && (w.type === 'LOKASI' || w.type === 'KANDANG') - ? w.location?.name - : undefined, - }; - }) - : []; + const bulkRemoveDelivery = useCallback(() => { + const updatedDeliveries = + formik.values.deliveries?.filter( + (_, idx) => !selectedDeliveries.includes(idx) + ) ?? []; + formik.setFieldValue('deliveries', updatedDeliveries); + setSelectedDeliveries([]); + }, [formik, selectedDeliveries]); - // Product Warehouse selection - Filter by source warehouse - const productWarehouseParams = new URLSearchParams({ - search: productWarehouseSelectInputValue, - }); - if (formik.values.source_warehouse_id) { - productWarehouseParams.append( - 'warehouse_id', - formik.values.source_warehouse_id.toString() - ); - } - const productWarehousesUrl = `${ProductWarehouseApi.basePath}?${productWarehouseParams.toString()}`; - const { data: productWarehouses, isLoading: isLoadingProductWarehouses } = - useSWR( - formik.values.source_warehouse_id ? productWarehousesUrl : null, - ProductWarehouseApi.getAllFetcher - ); - const productWarehouseOptions = isResponseSuccess(productWarehouses) - ? productWarehouses?.data.map((pw) => ({ - value: pw.product.id, - label: `${pw.product.name} - ${pw.warehouse.name} (Stock: ${pw.quantity.toLocaleString('id-ID')})`, - product_id: pw.product.id, - warehouse_id: pw.warehouse.id, - warehouse_name: pw.warehouse.name, - quantity: pw.quantity, - })) - : []; - - // Supplier selection - const [supplierSelectInputValue, setSupplierSelectInputValue] = useState(''); - const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ search: supplierSelectInputValue }).toString()}`; - const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR( - suppliersUrl, - SupplierApi.getAllFetcher - ); - const supplierOptions = isResponseSuccess(suppliers) - ? suppliers?.data.map((s) => ({ value: s.id, label: s.name })) - : []; - - // Handle cost calculation when delivery_cost changes + // Cost Calculation Handlers const handleDeliveryCostChange = useCallback( - (idx: number, value: string) => { - const numValue = parseFloat(value) || 0; - formik.setFieldValue(`deliveries.${idx}.delivery_cost`, numValue); + (idx: number, value: number) => { + formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value); const delivery = formik.values.deliveries?.[idx]; if (delivery) { @@ -382,13 +387,13 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { (sum, p) => sum + p.product_qty, 0 ); - if (productQty > 0 && numValue > 0) { - const perItem = numValue / productQty; + if (productQty > 0 && value > 0) { + const perItem = value / productQty; formik.setFieldValue( `deliveries.${idx}.delivery_cost_per_item`, perItem ); - } else if (numValue === 0) { + } else if (value === 0) { formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0); } } @@ -396,13 +401,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { [formik] ); - // Handle cost calculation when delivery_cost_per_item changes const handleDeliveryCostPerItemChange = useCallback( - (idx: number, value: string) => { - const numValue = parseFloat(value) || 0; + (idx: number, value: number) => { formik.setFieldValue( `deliveries.${idx}.delivery_cost_per_item`, - numValue + value ); const delivery = formik.values.deliveries?.[idx]; @@ -411,10 +414,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { (sum, p) => sum + p.product_qty, 0 ); - if (productQty > 0 && numValue > 0) { - const totalCost = numValue * productQty; + if (productQty > 0 && value > 0) { + const totalCost = value * productQty; formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost); - } else if (numValue === 0) { + } else if (value === 0) { formik.setFieldValue(`deliveries.${idx}.delivery_cost`, 0); } } @@ -422,57 +425,27 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { [formik] ); - // Auto-recalculate when product quantity changes - useEffect(() => { - formik.values.deliveries?.forEach((delivery, idx) => { - const productQty = delivery.products.reduce( - (sum, p) => sum + p.product_qty, - 0 - ); + const handleDeliveryCostChangeWrapper = useCallback( + (idx: number) => (e: React.ChangeEvent) => { + const rawValue = e.target.value.replace(/[^\d,.-]/g, ''); + const normalizedValue = rawValue.replace(/,/g, ''); + const value = parseFloat(normalizedValue) || 0; + handleDeliveryCostChange(idx, value); + }, + [handleDeliveryCostChange] + ); - // If delivery_cost is set, recalculate delivery_cost_per_item - if ( - delivery.delivery_cost && - delivery.delivery_cost > 0 && - productQty > 0 - ) { - const perItem = delivery.delivery_cost / productQty; - if (Math.abs((delivery.delivery_cost_per_item || 0) - perItem) > 0.01) { - formik.setFieldValue( - `deliveries.${idx}.delivery_cost_per_item`, - perItem - ); - } - } - // If delivery_cost_per_item is set, recalculate delivery_cost - else if ( - delivery.delivery_cost_per_item && - delivery.delivery_cost_per_item > 0 && - productQty > 0 - ) { - const totalCost = delivery.delivery_cost_per_item * productQty; - if (Math.abs((delivery.delivery_cost || 0) - totalCost) > 0.01) { - formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost); - } - } - }); - }, [ - formik.values.deliveries - ?.map((d) => d.products.reduce((sum, p) => sum + p.product_qty, 0)) - .join(','), - ]); - - useEffect(() => { - if ( - formik.values.source_warehouse_id && - type !== 'edit' && - type !== 'detail' - ) { - formik.setFieldValue('products', []); - formik.setFieldValue('deliveries', []); - } - }, [formik.values.source_warehouse_id]); + const handleDeliveryCostPerItemChangeWrapper = useCallback( + (idx: number) => (e: React.ChangeEvent) => { + const rawValue = e.target.value.replace(/[^\d,.-]/g, ''); + const normalizedValue = rawValue.replace(/,/g, ''); + const value = parseFloat(normalizedValue) || 0; + handleDeliveryCostPerItemChange(idx, value); + }, + [handleDeliveryCostPerItemChange] + ); + // UTILITY FUNCTIONS const getFilteredProductWarehouseOptions = useCallback(() => { return ( formik.values.products @@ -618,6 +591,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { [formik.values.deliveries, formik.values.products, type] ); + // ===== COMPUTED VALUES ===== const invalidQtyRows = useMemo( () => type === 'detail' @@ -650,6 +624,54 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { ); }, [formik.values.products, getProductQtyError, type]); + // ===== EFFECTS ===== + useEffect(() => { + formik.values.deliveries?.forEach((delivery, idx) => { + const productQty = delivery.products.reduce( + (sum, p) => sum + p.product_qty, + 0 + ); + + if ( + delivery.delivery_cost && + delivery.delivery_cost > 0 && + productQty > 0 + ) { + const perItem = delivery.delivery_cost / productQty; + if (Math.abs((delivery.delivery_cost_per_item || 0) - perItem) > 0.01) { + formik.setFieldValue( + `deliveries.${idx}.delivery_cost_per_item`, + perItem + ); + } + } else if ( + delivery.delivery_cost_per_item && + delivery.delivery_cost_per_item > 0 && + productQty > 0 + ) { + const totalCost = delivery.delivery_cost_per_item * productQty; + if (Math.abs((delivery.delivery_cost || 0) - totalCost) > 0.01) { + formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost); + } + } + }); + }, [ + formik.values.deliveries + ?.map((d) => d.products.reduce((sum, p) => sum + p.product_qty, 0)) + .join(','), + ]); + + useEffect(() => { + if ( + formik.values.source_warehouse_id && + type !== 'edit' && + type !== 'detail' + ) { + formik.setFieldValue('products', []); + formik.setFieldValue('deliveries', []); + } + }, [formik.values.source_warehouse_id]); + return ( <>
@@ -839,7 +861,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { {type !== 'detail' && ( -
{ checkbox: 'checkbox checkbox-sm', }} /> -
)} @@ -893,31 +913,29 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { {formik.values.products?.map((product, idx) => ( {type !== 'detail' && ( - -
- - ) => { - if (e.target.checked) { - setSelectedProducts([ - ...selectedProducts, - idx, - ]); - } else { - setSelectedProducts( - selectedProducts.filter((i) => i !== idx) - ); - } - }} - classNames={{ - wrapper: 'flex justify-center', - checkbox: 'checkbox checkbox-sm', - }} - /> -
+ + , + ) => { + if (e.target.checked) { + setSelectedProducts([ + ...selectedProducts, + idx, + ]); + } else { + setSelectedProducts( + selectedProducts.filter((i) => i !== idx), + ); + } + }} + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} + /> )} @@ -1061,7 +1079,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { {type !== 'detail' && ( -
{ checkbox: 'checkbox checkbox-sm', }} /> -
)} @@ -1161,33 +1177,31 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { {formik.values.deliveries?.map((delivery, idx) => ( {type !== 'detail' && ( - -
- - ) => { - if (e.target.checked) { - setSelectedDeliveries([ - ...selectedDeliveries, - idx, - ]); - } else { - setSelectedDeliveries( - selectedDeliveries.filter( - (i) => i !== idx - ) - ); - } - }} - classNames={{ - wrapper: 'flex justify-center', - checkbox: 'checkbox checkbox-sm', - }} - /> -
+ + , + ) => { + if (e.target.checked) { + setSelectedDeliveries([ + ...selectedDeliveries, + idx, + ]); + } else { + setSelectedDeliveries( + selectedDeliveries.filter( + (i) => i !== idx, + ), + ); + } + }} + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} + /> )} @@ -1373,9 +1387,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { required name={`deliveries.${idx}.delivery_cost`} value={delivery.delivery_cost || ''} - onChange={(e) => - handleDeliveryCostChange(idx, e.target.value) - } + onChange={handleDeliveryCostChangeWrapper(idx)} onBlur={formik.handleBlur} maskType='currency' decimals={0} @@ -1397,12 +1409,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { required name={`deliveries.${idx}.delivery_cost_per_item`} value={delivery.delivery_cost_per_item || ''} - onChange={(e) => - handleDeliveryCostPerItemChange( - idx, - e.target.value - ) - } + onChange={handleDeliveryCostPerItemChangeWrapper(idx)} onBlur={formik.handleBlur} maskType='currency' decimals={0}