refactor(FE-62,65): enhance product quantity display and stock information in MovementForm

This commit is contained in:
rstubryan
2025-10-17 19:23:19 +07:00
parent f05d367a5d
commit 0676411dd5
@@ -266,6 +266,13 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
location?: string; location?: string;
} }
interface ProductWarehouseOptionType extends OptionType {
product_id: number;
warehouse_id: number;
warehouse_name: string;
quantity: number;
}
// Warehouse selection // Warehouse selection
const [warehouseSelectInputValue, setWarehouseSelectInputValue] = const [warehouseSelectInputValue, setWarehouseSelectInputValue] =
useState(''); useState('');
@@ -304,8 +311,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
); );
const productWarehouseOptions = isResponseSuccess(productWarehouses) const productWarehouseOptions = isResponseSuccess(productWarehouses)
? productWarehouses?.data.map((pw) => ({ ? productWarehouses?.data.map((pw) => ({
value: pw.id, value: pw.product.id,
label: pw.product.name, label: `${pw.product.name} - ${pw.warehouse.name} (Stock: ${pw.quantity.toLocaleString('id-ID')})`,
product_id: pw.product.id, product_id: pw.product.id,
warehouse_id: pw.warehouse.id, warehouse_id: pw.warehouse.id,
warehouse_name: pw.warehouse.name, warehouse_name: pw.warehouse.name,
@@ -438,6 +445,33 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
[productWarehouseOptions, type] [productWarehouseOptions, type]
); );
const getProductQtyAdornment = useCallback(
(productIdx: number) => {
if (type === 'detail') return null;
const product = formik.values.products?.[productIdx];
if (!product || !product.product_id) return null;
const availableStock = getAvailableStock(product.product_id);
const requestedQty = Number(product.product_qty) || 0;
const remainingStock = availableStock - requestedQty;
if (requestedQty > 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.products, getAvailableStock, type]
);
const getProductQtyError = useCallback( const getProductQtyError = useCallback(
(productIdx: number) => { (productIdx: number) => {
if (type === 'detail') return null; if (type === 'detail') return null;
@@ -813,7 +847,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
); );
formik.setFieldValue( formik.setFieldValue(
`products.${idx}.product_id`, `products.${idx}.product_id`,
(val as OptionType)?.value (val as ProductWarehouseOptionType)?.value
); );
}} }}
options={productWarehouseOptions} options={productWarehouseOptions}
@@ -837,46 +871,35 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
/> />
</td> </td>
<td> <td>
<div className='flex flex-col gap-2'> <TextInput
<TextInput required
required type='number'
type='number' name={`products.${idx}.product_qty`}
name={`products.${idx}.product_qty`} value={product.product_qty ?? ''}
value={product.product_qty ?? ''} onChange={formik.handleChange}
onChange={formik.handleChange} onBlur={formik.handleBlur}
onBlur={formik.handleBlur} endAdornment={getProductQtyAdornment(idx)}
isError={ isError={
isRepeaterInputError( isRepeaterInputError(
'products', 'products',
'product_qty', 'product_qty',
idx idx
).isError || Boolean(getProductQtyError(idx)) ).isError || Boolean(getProductQtyError(idx))
} }
errorMessage={ errorMessage={
isRepeaterInputError( isRepeaterInputError(
'products', 'products',
'product_qty', 'product_qty',
idx idx
).errorMessage || ).errorMessage ||
getProductQtyError(idx) || getProductQtyError(idx) ||
undefined undefined
} }
readOnly={type === 'detail'} readOnly={type === 'detail'}
className={{ className={{
wrapper: 'w-full min-w-24', wrapper: 'w-full min-w-48',
}} }}
/> />
{type !== 'detail' && product.product_id && (
<div className='text-sm text-gray-600'>
<span className='font-semibold'>
Stok tersedia:
</span>{' '}
{getAvailableStock(
product.product_id
).toLocaleString('id-ID')}
</div>
)}
</div>
</td> </td>
{type !== 'detail' && ( {type !== 'detail' && (
<td> <td>