From d2c485fdf0304194796b44694ad6bd0d5c144c66 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 24 Oct 2025 12:45:07 +0700 Subject: [PATCH] feat(FE-114,137): implement stock validation in RecordingForm to manage usage limits and enhance user feedback --- .../recording/form/RecordingForm.tsx | 89 +++++++++++++++++-- 1 file changed, 84 insertions(+), 5 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 766cfe1d..f974bdcd 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -6,7 +6,6 @@ import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; -import TextInput from '@/components/input/TextInput'; import NumberInput from '@/components/input/NumberInput'; import SelectInput, { OptionType } from '@/components/input/SelectInput'; import CheckboxInput from '@/components/input/CheckboxInput'; @@ -265,6 +264,79 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { manuallyEditedRows, // Include manually edited rows in dependencies ]); + // Stock validation functions - Following MovementForm pattern + const getAvailableStock = useCallback( + (productWarehouseId: number) => { + if (type === 'detail') return 0; + + if (!isResponseSuccess(stockProducts)) return 0; + + const productWarehouse = stockProducts.data.find( + (pw) => pw.id === productWarehouseId + ); + + return productWarehouse?.quantity ?? 0; + }, + [stockProducts, type] + ); + + const getStockUsageError = useCallback( + (stockIdx: number) => { + if (type === 'detail') return null; + + const stock = formik.values.stocks?.[stockIdx]; + if (!stock || !stock.product_warehouse_id) return null; + + const availableStock = getAvailableStock(stock.product_warehouse_id); + const requestedUsage = Number(stock.usage_amount) || 0; + + if (requestedUsage > availableStock) { + return `Jumlah pakai melebihi stok tersedia! Maksimal: ${availableStock.toLocaleString('id-ID')}`; + } + + return null; + }, + [formik.values.stocks, getAvailableStock, type] + ); + + const getStockUsageAdornment = useCallback( + (stockIdx: number) => { + if (type === 'detail') return null; + + const stock = formik.values.stocks?.[stockIdx]; + if (!stock || !stock.product_warehouse_id) return null; + + const availableStock = getAvailableStock(stock.product_warehouse_id); + const requestedUsage = Number(stock.usage_amount) || 0; + const remainingStock = availableStock - requestedUsage; + + if (requestedUsage > 0) { + return ( + + (sisa: {remainingStock.toLocaleString('id-ID')}) + + ); + } + + return ( + + (tersedia: {availableStock.toLocaleString('id-ID')}) + + ); + }, + [formik.values.stocks, getAvailableStock, type] + ); + + const hasExceededStock = useMemo(() => { + if (type === 'detail') return false; + + return ( + formik.values.stocks?.some((stock, idx) => { + return getStockUsageError(idx) !== null; + }) ?? false + ); + }, [formik.values.stocks, getStockUsageError, type]); + // EVENT HANDLERS - Body Weights const addBodyWeight = () => { const newBodyWeights = [ @@ -440,7 +512,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }, [stockProducts, getProjectFlockLocation()]); - // Handle stock usage amount change + // Handle stock usage amount change - simplified following MovementForm pattern const handleStockUsageAmountChangeWrapper = useCallback( (idx: number) => (e: React.ChangeEvent) => { const value = parseInt(e.target.value.replace(/[^\d.-]/g, '')) || 0; @@ -920,6 +992,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { `stocks.${idx}.product_warehouse_id`, option?.value || 0 ); + // Auto-populate notes with product name by finding it in stockProducts data if (option?.value && isResponseSuccess(stockProducts)) { const selectedProduct = stockProducts.data.find( @@ -957,6 +1030,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { /> +
{ decimalSeparator='' isError={ isRepeaterInputError('stocks', 'usage_amount', idx) - .isError + .isError || Boolean(getStockUsageError(idx)) } errorMessage={ isRepeaterInputError('stocks', 'usage_amount', idx) - .errorMessage + .errorMessage || + getStockUsageError(idx) || + undefined } readOnly={type === 'detail'} className={{ wrapper: 'w-full min-w-24', }} - placeholder='Jumlah Pakai' + placeholder="Jumlah Pakai" /> + {type !== 'detail' && getStockUsageAdornment(idx)} +
{type !== 'detail' && ( @@ -1226,6 +1304,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { : undefined } onDelete={deleteRecordingClickHandler} + disableSubmit={hasExceededStock} /> {recordingFormErrorMessage && (