mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
refactor(FE-137): simplify stock display in MovementForm and RecordingForm, enhance input handling in SelectInput
This commit is contained in:
@@ -48,6 +48,7 @@ interface SelectInputBaseProps<T = OptionType> {
|
||||
openMenu?: boolean;
|
||||
delay?: number;
|
||||
onInputChange?: (search: string) => void;
|
||||
startAdornment?: ReactNode;
|
||||
}
|
||||
|
||||
interface SelectInputProps<T = OptionType> extends SelectInputBaseProps<T> {
|
||||
@@ -82,6 +83,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
||||
delay = 300,
|
||||
createables = false,
|
||||
onInputChange,
|
||||
startAdornment,
|
||||
} = props;
|
||||
|
||||
const [internalInputValue, setInternalInputValue] = useState('');
|
||||
@@ -205,6 +207,28 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
||||
components={{
|
||||
...components,
|
||||
...(optionComponent ? { Option: optionComponent } : {}),
|
||||
...(startAdornment ? {
|
||||
Control: ({ children, innerRef, innerProps, menuIsOpen, isFocused, isDisabled }) => (
|
||||
<div
|
||||
ref={innerRef}
|
||||
{...innerProps}
|
||||
className={cn(
|
||||
'w-full min-h-12! rounded-lg! border bg-white transition-shadow cursor-pointer! flex items-center',
|
||||
{
|
||||
'border-red-500! ring-2 ring-red-200': isError,
|
||||
'border-indigo-500 ring-2 ring-indigo-200': isFocused,
|
||||
'border-gray-300': !isError && !isFocused,
|
||||
'bg-gray-100 text-gray-400 cursor-not-allowed': isDisabled,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className='flex-1 px-4! py-2! gap-1 flex items-center'>
|
||||
{startAdornment}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
} : {}),
|
||||
}}
|
||||
menuPortalTarget={
|
||||
typeof document !== 'undefined' ? document.body : undefined
|
||||
|
||||
@@ -111,14 +111,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
|
||||
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}`,
|
||||
warehouseStockMap.get(w.id);
|
||||
return {
|
||||
value: w.id,
|
||||
label: w.name,
|
||||
area: w.area?.name,
|
||||
location:
|
||||
'type' in w && (w.type === 'LOKASI' || w.type === 'KANDANG')
|
||||
@@ -223,7 +219,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
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')})`,
|
||||
label: pw.product.name,
|
||||
product_id: pw.product.id,
|
||||
warehouse_id: pw.warehouse.id,
|
||||
warehouse_name: pw.warehouse.name,
|
||||
@@ -464,31 +460,77 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
[productWarehouseOptions, type]
|
||||
);
|
||||
|
||||
const getProductQtyAdornment = useCallback(
|
||||
const getProductQtyBottomLabel = useCallback(
|
||||
(productIdx: number) => {
|
||||
if (type === 'detail') return null;
|
||||
if (type === 'detail') return undefined;
|
||||
const product = formik.values.products?.[productIdx];
|
||||
if (!product || !product.product_id) return null;
|
||||
if (!product || !product.product_id) return undefined;
|
||||
|
||||
const availableStock = getAvailableStock(product.product_id);
|
||||
const requestedQty = Number(product.product_qty) || 0;
|
||||
const remainingStock = availableStock - requestedQty;
|
||||
|
||||
if (requestedQty > 0) {
|
||||
return `Sisa: ${remainingStock.toLocaleString('en-US')}`;
|
||||
}
|
||||
|
||||
return `Tersedia: ${availableStock.toLocaleString('en-US')}`;
|
||||
},
|
||||
[formik.values.products, getAvailableStock, type]
|
||||
);
|
||||
|
||||
const getDeliveryProductQtyBottomLabel = useCallback(
|
||||
(deliveryIdx: number, productIdx: number) => {
|
||||
if (type === 'detail') return undefined;
|
||||
const delivery = formik.values.deliveries?.[deliveryIdx];
|
||||
if (!delivery) return undefined;
|
||||
|
||||
const deliveryProduct = delivery.products[productIdx];
|
||||
if (!deliveryProduct || !deliveryProduct.product_id) return undefined;
|
||||
|
||||
const relatedProduct = formik.values.products?.find(
|
||||
(p) => p.product_id === deliveryProduct.product_id
|
||||
);
|
||||
if (!relatedProduct) return undefined;
|
||||
|
||||
const totalQtyUsed =
|
||||
formik.values.deliveries?.reduce((total, d, dIdx) => {
|
||||
const productQty = d.products.reduce((sum, p, pIdx) => {
|
||||
if (
|
||||
p.product_id === deliveryProduct.product_id &&
|
||||
!(dIdx === deliveryIdx && pIdx === productIdx)
|
||||
) {
|
||||
return sum + (Number(p.product_qty) || 0);
|
||||
}
|
||||
return sum;
|
||||
}, 0);
|
||||
return total + productQty;
|
||||
}, 0) || 0;
|
||||
|
||||
const availableQty = Number(relatedProduct.product_qty) - totalQtyUsed;
|
||||
return `Tersedia: ${availableQty.toLocaleString('en-US')}`;
|
||||
},
|
||||
[formik.values.deliveries, formik.values.products, type]
|
||||
);
|
||||
|
||||
const getWarehouseStockAdornment = useCallback(
|
||||
(warehouseId: number) => {
|
||||
const stockInfo = warehouseStockMap.get(warehouseId);
|
||||
if (!stockInfo) {
|
||||
return (
|
||||
<span className='text-sm text-gray-600 whitespace-nowrap'>
|
||||
(sisa: {remainingStock.toLocaleString('id-ID')})
|
||||
<span className='text-sm text-gray-500 whitespace-nowrap'>
|
||||
(Kosong)
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className='text-sm text-gray-600 whitespace-nowrap'>
|
||||
(tersedia: {availableStock.toLocaleString('id-ID')})
|
||||
<span className='text-sm text-gray-500 whitespace-nowrap'>
|
||||
(Tersedia {stockInfo.productCount} produk)
|
||||
</span>
|
||||
);
|
||||
},
|
||||
[formik.values.products, getAvailableStock, type]
|
||||
[warehouseStockMap]
|
||||
);
|
||||
|
||||
const getProductQtyError = useCallback(
|
||||
@@ -501,7 +543,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
const requestedQty = Number(product.product_qty) || 0;
|
||||
|
||||
if (requestedQty > availableStock) {
|
||||
return `Qty melebihi stok tersedia! Maksimal: ${availableStock.toLocaleString('id-ID')}`;
|
||||
return `Qty melebihi stok tersedia! Maksimal: ${availableStock.toLocaleString('en-US')}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -746,6 +788,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
errorMessage={formik.errors.source_warehouse_id as string}
|
||||
isDisabled={type === 'detail'}
|
||||
isClearable
|
||||
startAdornment={
|
||||
formik.values.source_warehouse_id
|
||||
? getWarehouseStockAdornment(formik.values.source_warehouse_id)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Area and Location Info */}
|
||||
@@ -808,6 +855,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
}
|
||||
isDisabled={type === 'detail'}
|
||||
isClearable
|
||||
startAdornment={
|
||||
formik.values.destination_warehouse_id
|
||||
? getWarehouseStockAdornment(formik.values.destination_warehouse_id)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Area and Location Info */}
|
||||
@@ -981,14 +1033,17 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<TextInput
|
||||
<NumberInput
|
||||
required
|
||||
type='number'
|
||||
name={`products.${idx}.product_qty`}
|
||||
value={product.product_qty ?? ''}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
endAdornment={getProductQtyAdornment(idx)}
|
||||
decimalScale={0}
|
||||
allowNegative={false}
|
||||
thousandSeparator=","
|
||||
decimalSeparator="."
|
||||
bottomLabel={getProductQtyBottomLabel(idx)}
|
||||
isError={
|
||||
isRepeaterInputError(
|
||||
'products',
|
||||
@@ -1240,13 +1295,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<TextInput
|
||||
<NumberInput
|
||||
required
|
||||
type='number'
|
||||
name={`deliveries.${idx}.products.0.product_qty`}
|
||||
value={delivery.products[0]?.product_qty ?? ''}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
decimalScale={0}
|
||||
allowNegative={false}
|
||||
thousandSeparator=","
|
||||
decimalSeparator="."
|
||||
isError={
|
||||
isDeliveryProductInputError(idx, 0, 'product_qty')
|
||||
.isError || Boolean(getDeliveryQtyError(idx, 0))
|
||||
@@ -1257,6 +1315,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
getDeliveryQtyError(idx, 0) ||
|
||||
undefined
|
||||
}
|
||||
bottomLabel={getDeliveryProductQtyBottomLabel(idx, 0)}
|
||||
readOnly={type === 'detail'}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-48',
|
||||
|
||||
@@ -123,8 +123,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
projectFlock.kandangs.forEach((kandang) => {
|
||||
const isAlreadyRecorded = recordedProjectFlockIds.has(projectFlock.id);
|
||||
const label = isAlreadyRecorded
|
||||
? `${projectFlock.flock.name} - ${projectFlock.area.name} - ${kandang.name} (Sudah Direcord)`
|
||||
: `${projectFlock.flock.name} - ${projectFlock.area.name} - ${kandang.name}`;
|
||||
? `${projectFlock.flock.name} - ${kandang.name} (Sudah Direcord)`
|
||||
: `${projectFlock.flock.name} - ${kandang.name}`;
|
||||
|
||||
options.push({
|
||||
value: projectFlock.id,
|
||||
@@ -140,22 +140,21 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
if (isResponseSuccess(stockProducts)) {
|
||||
stockProducts.data.forEach((product) => {
|
||||
const warehouse = product.warehouse;
|
||||
const stockText = product.quantity.toLocaleString('id-ID');
|
||||
|
||||
product.quantity.toLocaleString('en-US');
|
||||
const hasPakanFlag = product.product.flags?.includes('PAKAN');
|
||||
const hasOvkFlag = product.product.flags?.includes('OVK');
|
||||
|
||||
if (hasPakanFlag) {
|
||||
options.push({
|
||||
value: product.id,
|
||||
label: `[PAKAN] ${product.product.name} - ${warehouse?.name || ''} (${stockText})`
|
||||
label: `[PAKAN] ${product.product.name} - ${warehouse?.name || ''}`
|
||||
});
|
||||
}
|
||||
|
||||
if (hasOvkFlag) {
|
||||
options.push({
|
||||
value: product.id,
|
||||
label: `[OVK] ${product.product.name} - ${warehouse?.name || ''} (${stockText})`
|
||||
label: `[OVK] ${product.product.name} - ${warehouse?.name || ''}`
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -293,7 +292,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
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 `Jumlah pakai melebihi stok tersedia! Maksimal: ${availableStock.toLocaleString('en-US')}`;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@@ -311,13 +310,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
if (requestedUsage > 0) {
|
||||
return (
|
||||
<span className='text-sm text-gray-600 whitespace-nowrap'>
|
||||
(sisa: {remainingStock.toLocaleString('id-ID')})
|
||||
(sisa: {remainingStock.toLocaleString('en-US')})
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className='text-sm text-gray-600 whitespace-nowrap'>
|
||||
(tersedia: {availableStock.toLocaleString('id-ID')})
|
||||
(tersedia: {availableStock.toLocaleString('en-US')})
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user