feat(FE-114,137): implement stock validation in RecordingForm to manage usage limits and enhance user feedback

This commit is contained in:
rstubryan
2025-10-24 12:45:07 +07:00
parent 0c49978033
commit d2c485fdf0
@@ -6,7 +6,6 @@ import useSWR from 'swr';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Button from '@/components/Button'; import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput';
import NumberInput from '@/components/input/NumberInput'; import NumberInput from '@/components/input/NumberInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput'; import SelectInput, { OptionType } from '@/components/input/SelectInput';
import CheckboxInput from '@/components/input/CheckboxInput'; import CheckboxInput from '@/components/input/CheckboxInput';
@@ -265,6 +264,79 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
manuallyEditedRows, // Include manually edited rows in dependencies 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 (
<span className='text-sm text-gray-600 whitespace-nowrap'>
(sisa: {remainingStock.toLocaleString('id-ID')})
</span>
);
}
return (
<span className='text-sm text-gray-600 whitespace-nowrap'>
(tersedia: {availableStock.toLocaleString('id-ID')})
</span>
);
},
[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 // EVENT HANDLERS - Body Weights
const addBodyWeight = () => { const addBodyWeight = () => {
const newBodyWeights = [ const newBodyWeights = [
@@ -440,7 +512,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
}, [stockProducts, getProjectFlockLocation()]); }, [stockProducts, getProjectFlockLocation()]);
// Handle stock usage amount change // Handle stock usage amount change - simplified following MovementForm pattern
const handleStockUsageAmountChangeWrapper = useCallback( const handleStockUsageAmountChangeWrapper = useCallback(
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => { (idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(e.target.value.replace(/[^\d.-]/g, '')) || 0; 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`, `stocks.${idx}.product_warehouse_id`,
option?.value || 0 option?.value || 0
); );
// Auto-populate notes with product name by finding it in stockProducts data // Auto-populate notes with product name by finding it in stockProducts data
if (option?.value && isResponseSuccess(stockProducts)) { if (option?.value && isResponseSuccess(stockProducts)) {
const selectedProduct = stockProducts.data.find( const selectedProduct = stockProducts.data.find(
@@ -957,6 +1030,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
/> />
</td> </td>
<td> <td>
<div className="flex flex-col gap-1">
<NumberInput <NumberInput
required required
name={`stocks.${idx}.usage_amount`} name={`stocks.${idx}.usage_amount`}
@@ -970,18 +1044,22 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
decimalSeparator='' decimalSeparator=''
isError={ isError={
isRepeaterInputError('stocks', 'usage_amount', idx) isRepeaterInputError('stocks', 'usage_amount', idx)
.isError .isError || Boolean(getStockUsageError(idx))
} }
errorMessage={ errorMessage={
isRepeaterInputError('stocks', 'usage_amount', idx) isRepeaterInputError('stocks', 'usage_amount', idx)
.errorMessage .errorMessage ||
getStockUsageError(idx) ||
undefined
} }
readOnly={type === 'detail'} readOnly={type === 'detail'}
className={{ className={{
wrapper: 'w-full min-w-24', wrapper: 'w-full min-w-24',
}} }}
placeholder='Jumlah Pakai' placeholder="Jumlah Pakai"
/> />
{type !== 'detail' && getStockUsageAdornment(idx)}
</div>
</td> </td>
{type !== 'detail' && ( {type !== 'detail' && (
<td> <td>
@@ -1226,6 +1304,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
: undefined : undefined
} }
onDelete={deleteRecordingClickHandler} onDelete={deleteRecordingClickHandler}
disableSubmit={hasExceededStock}
/> />
{recordingFormErrorMessage && ( {recordingFormErrorMessage && (
<div role='alert' className='alert alert-error'> <div role='alert' className='alert alert-error'>