mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
refactor(FE): Refactor InventoryAdjustmentForm to simplify product
handling
This commit is contained in:
@@ -1,10 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import {
|
import { InventoryAdjustmentApi } from '@/services/api/inventory';
|
||||||
InventoryAdjustmentApi,
|
import { ProductApi } from '@/services/api/master-data';
|
||||||
ProductWarehouseApi,
|
|
||||||
} from '@/services/api/inventory';
|
|
||||||
import {
|
import {
|
||||||
CreateInventoryAdjustmentPayload,
|
CreateInventoryAdjustmentPayload,
|
||||||
InventoryAdjustment,
|
InventoryAdjustment,
|
||||||
@@ -28,8 +26,7 @@ import SelectInput, {
|
|||||||
OptionType,
|
OptionType,
|
||||||
useSelect,
|
useSelect,
|
||||||
} from '@/components/input/SelectInput';
|
} from '@/components/input/SelectInput';
|
||||||
import { components as ReactSelectComponents, OptionProps } from 'react-select';
|
|
||||||
import StatusBadge from '@/components/helper/StatusBadge';
|
|
||||||
import TextArea from '@/components/input/TextArea';
|
import TextArea from '@/components/input/TextArea';
|
||||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||||
@@ -37,7 +34,7 @@ import { Location } from '@/types/api/master-data/location';
|
|||||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
import { Product } from '@/types/api/master-data/product';
|
||||||
import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock';
|
import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
@@ -46,7 +43,6 @@ import {
|
|||||||
TRANSACTION_SUBTYPE_OPTIONS,
|
TRANSACTION_SUBTYPE_OPTIONS,
|
||||||
} from '@/config/constant';
|
} from '@/config/constant';
|
||||||
import NumberInput from '@/components/input/NumberInput';
|
import NumberInput from '@/components/input/NumberInput';
|
||||||
import { formatNumber } from '@/lib/helper';
|
|
||||||
|
|
||||||
interface InventoryAdjustmentFormProps {
|
interface InventoryAdjustmentFormProps {
|
||||||
type?: 'add' | 'detail';
|
type?: 'add' | 'detail';
|
||||||
@@ -62,8 +58,6 @@ const InventoryAdjustmentForm = ({
|
|||||||
InventoryAdjustmentFormErrorMessage,
|
InventoryAdjustmentFormErrorMessage,
|
||||||
setInventoryAdjustmentFormErrorMessage,
|
setInventoryAdjustmentFormErrorMessage,
|
||||||
] = useState('');
|
] = useState('');
|
||||||
const [quantityLabel, setQuantityLabel] = useState('Kuantitas');
|
|
||||||
const [quantityError, setQuantityError] = useState<string>('');
|
|
||||||
|
|
||||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
||||||
null
|
null
|
||||||
@@ -188,53 +182,24 @@ const InventoryAdjustmentForm = ({
|
|||||||
setInputValue: setProductInputValue,
|
setInputValue: setProductInputValue,
|
||||||
isLoadingOptions: isLoadingProductOptions,
|
isLoadingOptions: isLoadingProductOptions,
|
||||||
loadMore: loadMoreProducts,
|
loadMore: loadMoreProducts,
|
||||||
rawData: productWarehouses,
|
rawData: products,
|
||||||
} = useSelect<ProductWarehouse>(
|
} = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search');
|
||||||
selectedKandang && selectedProjectFlockLocationId
|
|
||||||
? ProductWarehouseApi.basePath
|
|
||||||
: '',
|
|
||||||
'id',
|
|
||||||
'product.name',
|
|
||||||
'search',
|
|
||||||
{
|
|
||||||
kandang_id: selectedKandang?.value?.toString() || '',
|
|
||||||
location_id: selectedProjectFlockLocationId,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const productOptions = useMemo(() => {
|
const productOptions = useMemo(() => {
|
||||||
if (!isResponseSuccess(productWarehouses)) return [];
|
if (!isResponseSuccess(products)) return [];
|
||||||
|
|
||||||
const excludedFlags = ['AYAM-AFKIR', 'AYAM-CULLING', 'AYAM-MATI'];
|
const excludedFlags = ['AYAM-AFKIR', 'AYAM-CULLING', 'AYAM-MATI'];
|
||||||
const filteredProducts = productWarehouses.data.filter((pw) => {
|
const filteredProducts = products.data.filter((product) => {
|
||||||
const productFlags = (pw.product.flags as string[]) || [];
|
const productFlags = (product.flags as string[]) || [];
|
||||||
return !productFlags.some((flag) => excludedFlags.includes(flag));
|
return !productFlags.some((flag) => excludedFlags.includes(flag));
|
||||||
});
|
});
|
||||||
|
|
||||||
return filteredProducts.map((pw) => ({
|
return filteredProducts.map((product) => ({
|
||||||
value: pw.product.id,
|
value: product.id,
|
||||||
label: pw.product.name,
|
label: product.name,
|
||||||
quantity: pw.quantity,
|
flags: product.flags,
|
||||||
flags: pw.product.flags,
|
|
||||||
}));
|
}));
|
||||||
}, [productWarehouses]);
|
}, [products]);
|
||||||
|
|
||||||
const selectedProductQuantity = useMemo(() => {
|
|
||||||
if (!selectedProduct) return 0;
|
|
||||||
const product = productOptions.find(
|
|
||||||
(opt) => opt.value === selectedProduct.value
|
|
||||||
);
|
|
||||||
return product?.quantity ?? 0;
|
|
||||||
}, [selectedProduct, productOptions]);
|
|
||||||
|
|
||||||
const isStockOutSubtype = useMemo(() => {
|
|
||||||
const subtype = selectedTransactionSubtype?.value;
|
|
||||||
return (
|
|
||||||
subtype === 'RECORDING_STOCK_OUT' ||
|
|
||||||
subtype === 'RECORDING_DEPLETION_OUT' ||
|
|
||||||
subtype === 'MARKETING_OUT'
|
|
||||||
);
|
|
||||||
}, [selectedTransactionSubtype]);
|
|
||||||
|
|
||||||
const selectedProductFlags = useMemo(() => {
|
const selectedProductFlags = useMemo(() => {
|
||||||
if (!selectedProduct) return [];
|
if (!selectedProduct) return [];
|
||||||
@@ -244,31 +209,6 @@ const InventoryAdjustmentForm = ({
|
|||||||
return (product?.flags as string[]) || [];
|
return (product?.flags as string[]) || [];
|
||||||
}, [selectedProduct, productOptions]);
|
}, [selectedProduct, productOptions]);
|
||||||
|
|
||||||
const ProductOption = useMemo(() => {
|
|
||||||
const OptionComponent = (
|
|
||||||
props: OptionProps<OptionType & { quantity?: number }, boolean>
|
|
||||||
) => {
|
|
||||||
const { data, children } = props;
|
|
||||||
const quantity = data.quantity ?? 0;
|
|
||||||
const isAvailable = quantity > 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ReactSelectComponents.Option {...props}>
|
|
||||||
<div className='flex items-center justify-between gap-2'>
|
|
||||||
<span className='flex-1'>{children}</span>
|
|
||||||
<StatusBadge
|
|
||||||
color={isAvailable ? 'success' : 'error'}
|
|
||||||
text={isAvailable ? 'Tersedia' : 'Kosong'}
|
|
||||||
className={{ badge: 'whitespace-nowrap w-fit' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ReactSelectComponents.Option>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
OptionComponent.displayName = 'ProductOption';
|
|
||||||
return OptionComponent;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const kandangOptions = useMemo(() => {
|
const kandangOptions = useMemo(() => {
|
||||||
let options: OptionType[] = [];
|
let options: OptionType[] = [];
|
||||||
|
|
||||||
@@ -378,42 +318,6 @@ const InventoryAdjustmentForm = ({
|
|||||||
return transactionType === 'PEMBELIAN' || transactionType === 'PENJUALAN';
|
return transactionType === 'PEMBELIAN' || transactionType === 'PENJUALAN';
|
||||||
}, [selectedTransactionType]);
|
}, [selectedTransactionType]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const subtype = selectedTransactionSubtype?.value;
|
|
||||||
if (
|
|
||||||
subtype === 'RECORDING_STOCK_OUT' ||
|
|
||||||
subtype === 'RECORDING_DEPLETION_OUT' ||
|
|
||||||
subtype === 'MARKETING_OUT'
|
|
||||||
) {
|
|
||||||
setQuantityLabel('Kurangi Stok');
|
|
||||||
} else if (subtype === 'RECORDING_EGG_IN' || subtype === 'PURCHASE_IN') {
|
|
||||||
setQuantityLabel('Tambah Stok');
|
|
||||||
} else {
|
|
||||||
setQuantityLabel('Kuantitas');
|
|
||||||
}
|
|
||||||
}, [selectedTransactionSubtype]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const qty = Number(formik.values.qty);
|
|
||||||
if (
|
|
||||||
isStockOutSubtype &&
|
|
||||||
selectedProduct &&
|
|
||||||
qty > 0 &&
|
|
||||||
qty > selectedProductQuantity
|
|
||||||
) {
|
|
||||||
setQuantityError(
|
|
||||||
`Kuantitas ${formatNumber(qty)} melebihi stok tersedia (${formatNumber(selectedProductQuantity)})`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setQuantityError('');
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
formik.values.qty,
|
|
||||||
isStockOutSubtype,
|
|
||||||
selectedProduct,
|
|
||||||
selectedProductQuantity,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedTransactionType?.value === 'RECORDING' && selectedProduct) {
|
if (selectedTransactionType?.value === 'RECORDING' && selectedProduct) {
|
||||||
setSelectedTransactionSubtype(null);
|
setSelectedTransactionSubtype(null);
|
||||||
@@ -541,7 +445,6 @@ const InventoryAdjustmentForm = ({
|
|||||||
|
|
||||||
const resetHandler = () => {
|
const resetHandler = () => {
|
||||||
formik.resetForm();
|
formik.resetForm();
|
||||||
setQuantityLabel('Kuantitas');
|
|
||||||
setSelectedLocation(null);
|
setSelectedLocation(null);
|
||||||
setSelectedProjectFlock(null);
|
setSelectedProjectFlock(null);
|
||||||
setSelectedKandang(null);
|
setSelectedKandang(null);
|
||||||
@@ -764,7 +667,6 @@ const InventoryAdjustmentForm = ({
|
|||||||
onChange={productChangeHandler}
|
onChange={productChangeHandler}
|
||||||
onInputChange={setProductInputValue}
|
onInputChange={setProductInputValue}
|
||||||
options={productOptions}
|
options={productOptions}
|
||||||
optionComponent={ProductOption}
|
|
||||||
onMenuScrollToBottom={loadMoreProducts}
|
onMenuScrollToBottom={loadMoreProducts}
|
||||||
isLoading={isLoadingProductOptions}
|
isLoading={isLoadingProductOptions}
|
||||||
isError={
|
isError={
|
||||||
@@ -777,14 +679,6 @@ const InventoryAdjustmentForm = ({
|
|||||||
? 'Pilih Produk'
|
? 'Pilih Produk'
|
||||||
: 'Pilih Kandang terlebih dahulu'
|
: 'Pilih Kandang terlebih dahulu'
|
||||||
}
|
}
|
||||||
inputPrefix={
|
|
||||||
selectedProduct
|
|
||||||
? 'Stock: ' +
|
|
||||||
(productOptions.find(
|
|
||||||
(opt) => opt.value === selectedProduct?.value
|
|
||||||
)?.quantity ?? 0)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
isClearable
|
isClearable
|
||||||
isSearchable
|
isSearchable
|
||||||
/>
|
/>
|
||||||
@@ -841,22 +735,14 @@ const InventoryAdjustmentForm = ({
|
|||||||
wrapper: `${selectedTransactionSubtype ? '' : 'hidden'}`,
|
wrapper: `${selectedTransactionSubtype ? '' : 'hidden'}`,
|
||||||
}}
|
}}
|
||||||
required
|
required
|
||||||
label={quantityLabel}
|
label='Kuantitas'
|
||||||
name='qty'
|
name='qty'
|
||||||
value={formik.values.qty}
|
value={formik.values.qty}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
isError={
|
isError={formik.touched.qty && Boolean(formik.errors.qty)}
|
||||||
(formik.touched.qty && Boolean(formik.errors.qty)) ||
|
errorMessage={formik.errors.qty as string}
|
||||||
Boolean(quantityError)
|
|
||||||
}
|
|
||||||
errorMessage={(formik.errors.qty as string) || quantityError}
|
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
bottomLabel={
|
|
||||||
selectedProduct
|
|
||||||
? `Tersedia: ${formatNumber(selectedProductQuantity)}`
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
{/* Number Input Price */}
|
{/* Number Input Price */}
|
||||||
<NumberInput
|
<NumberInput
|
||||||
@@ -905,7 +791,7 @@ const InventoryAdjustmentForm = ({
|
|||||||
type='submit'
|
type='submit'
|
||||||
color='primary'
|
color='primary'
|
||||||
isLoading={formik.isSubmitting}
|
isLoading={formik.isSubmitting}
|
||||||
disabled={formik.isSubmitting || Boolean(quantityError)}
|
disabled={formik.isSubmitting}
|
||||||
className='px-4 w-full sm:w-auto'
|
className='px-4 w-full sm:w-auto'
|
||||||
>
|
>
|
||||||
Submit
|
Submit
|
||||||
|
|||||||
Reference in New Issue
Block a user