mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE-114,137): implement stock validation in RecordingForm to manage usage limits and enhance user feedback
This commit is contained in:
@@ -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'>
|
||||||
|
|||||||
Reference in New Issue
Block a user