mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 15:55:48 +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;
|
openMenu?: boolean;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
onInputChange?: (search: string) => void;
|
onInputChange?: (search: string) => void;
|
||||||
|
startAdornment?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectInputProps<T = OptionType> extends SelectInputBaseProps<T> {
|
interface SelectInputProps<T = OptionType> extends SelectInputBaseProps<T> {
|
||||||
@@ -82,6 +83,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
delay = 300,
|
delay = 300,
|
||||||
createables = false,
|
createables = false,
|
||||||
onInputChange,
|
onInputChange,
|
||||||
|
startAdornment,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [internalInputValue, setInternalInputValue] = useState('');
|
const [internalInputValue, setInternalInputValue] = useState('');
|
||||||
@@ -205,6 +207,28 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
components={{
|
components={{
|
||||||
...components,
|
...components,
|
||||||
...(optionComponent ? { Option: optionComponent } : {}),
|
...(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={
|
menuPortalTarget={
|
||||||
typeof document !== 'undefined' ? document.body : undefined
|
typeof document !== 'undefined' ? document.body : undefined
|
||||||
|
|||||||
@@ -111,14 +111,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
|
|
||||||
const warehouseOptions = isResponseSuccess(warehouses)
|
const warehouseOptions = isResponseSuccess(warehouses)
|
||||||
? warehouses?.data.map((w) => {
|
? warehouses?.data.map((w) => {
|
||||||
const stockInfo = warehouseStockMap.get(w.id);
|
warehouseStockMap.get(w.id);
|
||||||
const stockLabel = stockInfo
|
|
||||||
? ` (Stock: ${stockInfo.totalQty.toLocaleString('id-ID')} items, ${stockInfo.productCount} produk)`
|
|
||||||
: ' (Kosong)';
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: w.id,
|
value: w.id,
|
||||||
label: `${w.name}${stockLabel}`,
|
label: w.name,
|
||||||
area: w.area?.name,
|
area: w.area?.name,
|
||||||
location:
|
location:
|
||||||
'type' in w && (w.type === 'LOKASI' || w.type === 'KANDANG')
|
'type' in w && (w.type === 'LOKASI' || w.type === 'KANDANG')
|
||||||
@@ -223,7 +219,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
const productWarehouseOptions = isResponseSuccess(productWarehouses)
|
const productWarehouseOptions = isResponseSuccess(productWarehouses)
|
||||||
? productWarehouses?.data.map((pw) => ({
|
? productWarehouses?.data.map((pw) => ({
|
||||||
value: pw.product.id,
|
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,
|
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,
|
||||||
@@ -464,31 +460,77 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
[productWarehouseOptions, type]
|
[productWarehouseOptions, type]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getProductQtyAdornment = useCallback(
|
const getProductQtyBottomLabel = useCallback(
|
||||||
(productIdx: number) => {
|
(productIdx: number) => {
|
||||||
if (type === 'detail') return null;
|
if (type === 'detail') return undefined;
|
||||||
const product = formik.values.products?.[productIdx];
|
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 availableStock = getAvailableStock(product.product_id);
|
||||||
const requestedQty = Number(product.product_qty) || 0;
|
const requestedQty = Number(product.product_qty) || 0;
|
||||||
const remainingStock = availableStock - requestedQty;
|
const remainingStock = availableStock - requestedQty;
|
||||||
|
|
||||||
if (requestedQty > 0) {
|
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 (
|
return (
|
||||||
<span className='text-sm text-gray-600 whitespace-nowrap'>
|
<span className='text-sm text-gray-500 whitespace-nowrap'>
|
||||||
(sisa: {remainingStock.toLocaleString('id-ID')})
|
(Kosong)
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className='text-sm text-gray-600 whitespace-nowrap'>
|
<span className='text-sm text-gray-500 whitespace-nowrap'>
|
||||||
(tersedia: {availableStock.toLocaleString('id-ID')})
|
(Tersedia {stockInfo.productCount} produk)
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[formik.values.products, getAvailableStock, type]
|
[warehouseStockMap]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getProductQtyError = useCallback(
|
const getProductQtyError = useCallback(
|
||||||
@@ -501,7 +543,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
const requestedQty = Number(product.product_qty) || 0;
|
const requestedQty = Number(product.product_qty) || 0;
|
||||||
|
|
||||||
if (requestedQty > availableStock) {
|
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;
|
return null;
|
||||||
@@ -746,6 +788,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
errorMessage={formik.errors.source_warehouse_id as string}
|
errorMessage={formik.errors.source_warehouse_id as string}
|
||||||
isDisabled={type === 'detail'}
|
isDisabled={type === 'detail'}
|
||||||
isClearable
|
isClearable
|
||||||
|
startAdornment={
|
||||||
|
formik.values.source_warehouse_id
|
||||||
|
? getWarehouseStockAdornment(formik.values.source_warehouse_id)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Area and Location Info */}
|
{/* Area and Location Info */}
|
||||||
@@ -808,6 +855,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
}
|
}
|
||||||
isDisabled={type === 'detail'}
|
isDisabled={type === 'detail'}
|
||||||
isClearable
|
isClearable
|
||||||
|
startAdornment={
|
||||||
|
formik.values.destination_warehouse_id
|
||||||
|
? getWarehouseStockAdornment(formik.values.destination_warehouse_id)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Area and Location Info */}
|
{/* Area and Location Info */}
|
||||||
@@ -981,14 +1033,17 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<TextInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
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)}
|
decimalScale={0}
|
||||||
|
allowNegative={false}
|
||||||
|
thousandSeparator=","
|
||||||
|
decimalSeparator="."
|
||||||
|
bottomLabel={getProductQtyBottomLabel(idx)}
|
||||||
isError={
|
isError={
|
||||||
isRepeaterInputError(
|
isRepeaterInputError(
|
||||||
'products',
|
'products',
|
||||||
@@ -1240,13 +1295,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<TextInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
type='number'
|
|
||||||
name={`deliveries.${idx}.products.0.product_qty`}
|
name={`deliveries.${idx}.products.0.product_qty`}
|
||||||
value={delivery.products[0]?.product_qty ?? ''}
|
value={delivery.products[0]?.product_qty ?? ''}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
|
decimalScale={0}
|
||||||
|
allowNegative={false}
|
||||||
|
thousandSeparator=","
|
||||||
|
decimalSeparator="."
|
||||||
isError={
|
isError={
|
||||||
isDeliveryProductInputError(idx, 0, 'product_qty')
|
isDeliveryProductInputError(idx, 0, 'product_qty')
|
||||||
.isError || Boolean(getDeliveryQtyError(idx, 0))
|
.isError || Boolean(getDeliveryQtyError(idx, 0))
|
||||||
@@ -1257,6 +1315,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
getDeliveryQtyError(idx, 0) ||
|
getDeliveryQtyError(idx, 0) ||
|
||||||
undefined
|
undefined
|
||||||
}
|
}
|
||||||
|
bottomLabel={getDeliveryProductQtyBottomLabel(idx, 0)}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full min-w-48',
|
wrapper: 'w-full min-w-48',
|
||||||
|
|||||||
@@ -123,8 +123,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
projectFlock.kandangs.forEach((kandang) => {
|
projectFlock.kandangs.forEach((kandang) => {
|
||||||
const isAlreadyRecorded = recordedProjectFlockIds.has(projectFlock.id);
|
const isAlreadyRecorded = recordedProjectFlockIds.has(projectFlock.id);
|
||||||
const label = isAlreadyRecorded
|
const label = isAlreadyRecorded
|
||||||
? `${projectFlock.flock.name} - ${projectFlock.area.name} - ${kandang.name} (Sudah Direcord)`
|
? `${projectFlock.flock.name} - ${kandang.name} (Sudah Direcord)`
|
||||||
: `${projectFlock.flock.name} - ${projectFlock.area.name} - ${kandang.name}`;
|
: `${projectFlock.flock.name} - ${kandang.name}`;
|
||||||
|
|
||||||
options.push({
|
options.push({
|
||||||
value: projectFlock.id,
|
value: projectFlock.id,
|
||||||
@@ -140,22 +140,21 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
if (isResponseSuccess(stockProducts)) {
|
if (isResponseSuccess(stockProducts)) {
|
||||||
stockProducts.data.forEach((product) => {
|
stockProducts.data.forEach((product) => {
|
||||||
const warehouse = product.warehouse;
|
const warehouse = product.warehouse;
|
||||||
const stockText = product.quantity.toLocaleString('id-ID');
|
product.quantity.toLocaleString('en-US');
|
||||||
|
|
||||||
const hasPakanFlag = product.product.flags?.includes('PAKAN');
|
const hasPakanFlag = product.product.flags?.includes('PAKAN');
|
||||||
const hasOvkFlag = product.product.flags?.includes('OVK');
|
const hasOvkFlag = product.product.flags?.includes('OVK');
|
||||||
|
|
||||||
if (hasPakanFlag) {
|
if (hasPakanFlag) {
|
||||||
options.push({
|
options.push({
|
||||||
value: product.id,
|
value: product.id,
|
||||||
label: `[PAKAN] ${product.product.name} - ${warehouse?.name || ''} (${stockText})`
|
label: `[PAKAN] ${product.product.name} - ${warehouse?.name || ''}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasOvkFlag) {
|
if (hasOvkFlag) {
|
||||||
options.push({
|
options.push({
|
||||||
value: product.id,
|
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 availableStock = getAvailableStock(stock.product_warehouse_id);
|
||||||
const requestedUsage = Number(stock.usage_amount) || 0;
|
const requestedUsage = Number(stock.usage_amount) || 0;
|
||||||
if (requestedUsage > availableStock) {
|
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;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -311,13 +310,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
if (requestedUsage > 0) {
|
if (requestedUsage > 0) {
|
||||||
return (
|
return (
|
||||||
<span className='text-sm text-gray-600 whitespace-nowrap'>
|
<span className='text-sm text-gray-600 whitespace-nowrap'>
|
||||||
(sisa: {remainingStock.toLocaleString('id-ID')})
|
(sisa: {remainingStock.toLocaleString('en-US')})
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span className='text-sm text-gray-600 whitespace-nowrap'>
|
<span className='text-sm text-gray-600 whitespace-nowrap'>
|
||||||
(tersedia: {availableStock.toLocaleString('id-ID')})
|
(tersedia: {availableStock.toLocaleString('en-US')})
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user