refactor(FE): Extract and memoize form event handlers

This commit is contained in:
rstubryan
2026-01-19 10:28:08 +07:00
parent 6def4e0fcd
commit dc2c2228a8
@@ -349,13 +349,111 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
};
};
const handleTransferDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
formik.setFieldValue('transfer_date', e.target.value);
};
// ===== EVENT HANDLERS =====
// Product Handlers
const addProduct = () => {
const handleTransferDateChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
formik.setFieldValue('transfer_date', e.target.value);
},
[]
);
const handleSourceWarehouseChange = useCallback(
(val: OptionType | OptionType[] | null) => {
const newSourceWarehouseId = (val as WarehouseOptionType)?.value;
if (newSourceWarehouseId) {
if (newSourceWarehouseId === formik.values.destination_warehouse_id) {
const destinationWarehouseName =
(formik.values.destination_warehouse as WarehouseOptionType)
?.label || 'gudang tujuan';
toast.error(
`Tidak bisa memilih gudang yang sama. Gudang asal tidak boleh sama dengan ${destinationWarehouseName}.`
);
return;
}
}
formik.setFieldTouched('source_warehouse', true);
formik.setFieldValue('source_warehouse', val);
formik.setFieldTouched('source_warehouse_id', true);
formik.setFieldValue('source_warehouse_id', newSourceWarehouseId);
if (
formik.errors.destination_warehouse_id ===
'Gudang tujuan tidak boleh sama dengan gudang asal!'
) {
formik.setFieldError('destination_warehouse_id', undefined);
}
if (
newSourceWarehouseId &&
newSourceWarehouseId !== formik.values.source_warehouse_id
) {
formik.setFieldValue('products', [
{
product: null,
product_id: 0,
product_qty: '',
},
]);
formik.setFieldTouched('products', false);
const updatedDeliveries = formik.values.deliveries.map(
(delivery: DeliverySchema) => ({
...delivery,
products: [
{
product: null,
product_id: 0,
product_qty: '',
},
],
})
);
formik.setFieldValue('deliveries', updatedDeliveries);
formik.setFieldTouched('deliveries', false);
}
},
[]
);
const handleDestinationWarehouseChange = useCallback(
(val: OptionType | OptionType[] | null) => {
const newDestinationWarehouseId = (val as WarehouseOptionType)?.value;
if (newDestinationWarehouseId) {
if (newDestinationWarehouseId === formik.values.source_warehouse_id) {
const sourceWarehouseName =
(formik.values.source_warehouse as WarehouseOptionType)?.label ||
'gudang asal';
toast.error(
`Tidak bisa memilih gudang yang sama. Gudang tujuan tidak boleh sama dengan ${sourceWarehouseName}.`
);
return;
}
}
formik.setFieldTouched('destination_warehouse', true);
formik.setFieldValue('destination_warehouse', val);
formik.setFieldTouched('destination_warehouse_id', true);
formik.setFieldValue(
'destination_warehouse_id',
newDestinationWarehouseId
);
if (
formik.errors.destination_warehouse_id ===
'Gudang tujuan tidak boleh sama dengan gudang asal!'
) {
formik.setFieldError('destination_warehouse_id', undefined);
}
},
[]
);
const addProduct = useCallback(() => {
const newProducts = [
...(formik.values.products || []),
{
@@ -365,22 +463,19 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
},
];
formik.setFieldValue('products', newProducts);
};
}, []);
const removeProduct = useCallback(
(i: number) => {
const updatedProducts =
formik.values.products?.reduce((acc: ProductSchema[], item, index) => {
if (index !== i) {
acc.push(item);
}
return acc;
}, []) ?? [];
const removeProduct = useCallback((i: number) => {
const updatedProducts =
formik.values.products?.reduce((acc: ProductSchema[], item, index) => {
if (index !== i) {
acc.push(item);
}
return acc;
}, []) ?? [];
formik.setFieldValue('products', updatedProducts);
},
[formik]
);
formik.setFieldValue('products', updatedProducts);
}, []);
const bulkRemoveProduct = useCallback(() => {
const updatedProducts =
@@ -389,10 +484,60 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
) ?? [];
formik.setFieldValue('products', updatedProducts);
setSelectedProducts([]);
}, [formik, selectedProducts]);
}, [formik, selectedProducts, setSelectedProducts]);
// Delivery Handlers
const addDelivery = () => {
const handleProductChange = useCallback(
(idx: number, val: OptionType | OptionType[] | null) => {
formik.setFieldTouched(`products.${idx}.product`, true);
formik.setFieldValue(`products.${idx}.product`, val);
formik.setFieldTouched(`products.${idx}.product_id`, true);
formik.setFieldValue(
`products.${idx}.product_id`,
(val as ProductWarehouseOptionType)?.value
);
const updatedDeliveries = formik.values.deliveries.map(
(delivery: DeliverySchema) => ({
...delivery,
products: [
{
product: null,
product_id: 0,
product_qty: '',
},
],
})
);
formik.setFieldValue('deliveries', updatedDeliveries);
formik.setFieldTouched('deliveries', false);
},
[]
);
const handleProductSelectAllChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.checked) {
setSelectedProducts(formik.values.products?.map((_, idx) => idx) ?? []);
} else {
setSelectedProducts([]);
}
},
[formik.values.products, setSelectedProducts]
);
const handleProductCheckboxChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const idx = Number(e.target.name.replace('product-', ''));
if (e.target.checked) {
setSelectedProducts((prev) => [...prev, idx]);
} else {
setSelectedProducts((prev) => prev.filter((i) => i !== idx));
}
},
[setSelectedProducts]
);
const addDelivery = useCallback(() => {
formik.setFieldValue('deliveries', [
...(formik.values.deliveries || []),
{
@@ -412,25 +557,19 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
],
},
]);
};
}, []);
const removeDelivery = useCallback(
(i: number) => {
const updatedDeliveries =
formik.values.deliveries?.reduce(
(acc: DeliverySchema[], item, index) => {
if (index !== i) {
acc.push(item);
}
return acc;
},
[]
) ?? [];
const removeDelivery = useCallback((i: number) => {
const updatedDeliveries =
formik.values.deliveries?.reduce((acc: DeliverySchema[], item, index) => {
if (index !== i) {
acc.push(item);
}
return acc;
}, []) ?? [];
formik.setFieldValue('deliveries', updatedDeliveries);
},
[formik]
);
formik.setFieldValue('deliveries', updatedDeliveries);
}, []);
const bulkRemoveDelivery = useCallback(() => {
const updatedDeliveries =
@@ -439,33 +578,101 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
) ?? [];
formik.setFieldValue('deliveries', updatedDeliveries);
setSelectedDeliveries([]);
}, [formik, selectedDeliveries]);
}, [formik, selectedDeliveries, setSelectedDeliveries]);
// Cost Calculation Handlers
const handleDeliveryCostChange = useCallback(
(idx: number, value: number) => {
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value);
const delivery = formik.values.deliveries?.[idx];
if (delivery) {
const productQty = delivery.products.reduce(
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
0
const handleDeliverySelectAllChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.checked) {
setSelectedDeliveries(
formik.values.deliveries?.map((_, idx) => idx) ?? []
);
if (productQty > 0 && value > 0) {
const perItem = value / productQty;
formik.setFieldValue(
`deliveries.${idx}.delivery_cost_per_item`,
perItem
);
} else if (value === 0) {
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
}
} else {
setSelectedDeliveries([]);
}
},
[formik]
[formik.values.deliveries, setSelectedDeliveries]
);
const handleDeliveryCheckboxChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const idx = Number(e.target.name.replace('delivery-', ''));
if (e.target.checked) {
setSelectedDeliveries((prev) => [...prev, idx]);
} else {
setSelectedDeliveries((prev) => prev.filter((i) => i !== idx));
}
},
[setSelectedDeliveries]
);
const handleDeliveryProductChange = useCallback(
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
formik.setFieldTouched(
`deliveries.${deliveryIdx}.products.0.product`,
true
);
formik.setFieldValue(`deliveries.${deliveryIdx}.products.0.product`, val);
formik.setFieldTouched(
`deliveries.${deliveryIdx}.products.0.product_id`,
true
);
formik.setFieldValue(
`deliveries.${deliveryIdx}.products.0.product_id`,
(val as OptionType)?.value
);
},
[]
);
const handleDeliverySupplierChange = useCallback(
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier`, true);
formik.setFieldValue(`deliveries.${deliveryIdx}.supplier`, val);
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier_id`, true);
formik.setFieldValue(
`deliveries.${deliveryIdx}.supplier_id`,
(val as OptionType)?.value
);
},
[]
);
const handleDeliveryDocumentChange = useCallback(
(deliveryIdx: number, e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
if (file.size > 5 * 1024 * 1024) {
toast.error('Ukuran dokumen maksimal 5 MB!');
e.target.value = '';
return;
}
formik.setFieldValue(`deliveries.${deliveryIdx}.document`, file);
}
},
[]
);
const handleDeliveryCostChange = useCallback((idx: number, value: number) => {
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value);
const delivery = formik.values.deliveries?.[idx];
if (delivery) {
const productQty = delivery.products.reduce(
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
0
);
if (productQty > 0 && value > 0) {
const perItem = value / productQty;
formik.setFieldValue(
`deliveries.${idx}.delivery_cost_per_item`,
perItem
);
} else if (value === 0) {
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
}
}
}, []);
const handleDeliveryCostPerItemChange = useCallback(
(idx: number, value: number) => {
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value);
@@ -484,7 +691,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
}
}
},
[formik]
[]
);
const handleDeliveryCostChangeWrapper = useCallback(
@@ -959,72 +1166,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
label='Gudang'
placeholder='Pilih gudang asal...'
value={formik.values.source_warehouse}
onChange={(val) => {
const newSourceWarehouseId = (val as WarehouseOptionType)
?.value;
if (newSourceWarehouseId) {
if (
newSourceWarehouseId ===
formik.values.destination_warehouse_id
) {
const destinationWarehouseName =
(
formik.values
.destination_warehouse as WarehouseOptionType
)?.label || 'gudang tujuan';
toast.error(
`Tidak bisa memilih gudang yang sama. Gudang asal tidak boleh sama dengan ${destinationWarehouseName}.`
);
return;
}
}
formik.setFieldTouched('source_warehouse', true);
formik.setFieldValue('source_warehouse', val);
formik.setFieldTouched('source_warehouse_id', true);
formik.setFieldValue(
'source_warehouse_id',
newSourceWarehouseId
);
if (
formik.errors.destination_warehouse_id ===
'Gudang tujuan tidak boleh sama dengan gudang asal!'
) {
formik.setFieldError('destination_warehouse_id', undefined);
}
if (
newSourceWarehouseId &&
newSourceWarehouseId !== formik.values.source_warehouse_id
) {
formik.setFieldValue('products', [
{
product: null,
product_id: 0,
product_qty: '',
},
]);
formik.setFieldTouched('products', false);
const updatedDeliveries = formik.values.deliveries.map(
(delivery) => ({
...delivery,
products: [
{
product: null,
product_id: 0,
product_qty: '',
},
],
})
);
formik.setFieldValue('deliveries', updatedDeliveries);
formik.setFieldTouched('deliveries', false);
}
}}
onChange={handleSourceWarehouseChange}
options={warehouseOptions}
onInputChange={setWarehouseSelectInputValue}
onMenuScrollToBottom={loadMoreWarehouses}
@@ -1088,41 +1230,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
label='Gudang'
placeholder='Pilih gudang tujuan...'
value={formik.values.destination_warehouse}
onChange={(val) => {
const newDestinationWarehouseId = (val as WarehouseOptionType)
?.value;
if (newDestinationWarehouseId) {
if (
newDestinationWarehouseId ===
formik.values.source_warehouse_id
) {
const sourceWarehouseName =
(formik.values.source_warehouse as WarehouseOptionType)
?.label || 'gudang asal';
toast.error(
`Tidak bisa memilih gudang yang sama. Gudang tujuan tidak boleh sama dengan ${sourceWarehouseName}.`
);
return;
}
}
formik.setFieldTouched('destination_warehouse', true);
formik.setFieldValue('destination_warehouse', val);
formik.setFieldTouched('destination_warehouse_id', true);
formik.setFieldValue(
'destination_warehouse_id',
newDestinationWarehouseId
);
if (
formik.errors.destination_warehouse_id ===
'Gudang tujuan tidak boleh sama dengan gudang asal!'
) {
formik.setFieldError('destination_warehouse_id', undefined);
}
}}
onChange={handleDestinationWarehouseChange}
options={warehouseOptions}
onInputChange={setWarehouseSelectInputValue}
isLoading={isLoadingWarehouses}
@@ -1196,18 +1304,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
selectedProducts.length &&
formik.values.products?.length > 0
}
onChange={(
e: React.ChangeEvent<HTMLInputElement>
) => {
if (e.target.checked) {
setSelectedProducts(
formik.values.products?.map((_, idx) => idx) ??
[]
);
} else {
setSelectedProducts([]);
}
}}
onChange={handleProductSelectAllChange}
classNames={{
wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm',
@@ -1244,17 +1341,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<CheckboxInput
name={`product-${idx}`}
checked={selectedProducts.includes(idx)}
onChange={(
e: React.ChangeEvent<HTMLInputElement>
) => {
if (e.target.checked) {
setSelectedProducts([...selectedProducts, idx]);
} else {
setSelectedProducts(
selectedProducts.filter((i) => i !== idx)
);
}
}}
onChange={handleProductCheckboxChange}
classNames={{
wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm',
@@ -1266,41 +1353,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<SelectInput
required
value={product.product ?? undefined}
onChange={(val) => {
formik.setFieldTouched(
`products.${idx}.product`,
true
);
formik.setFieldValue(
`products.${idx}.product`,
val
);
formik.setFieldTouched(
`products.${idx}.product_id`,
true
);
formik.setFieldValue(
`products.${idx}.product_id`,
(val as ProductWarehouseOptionType)?.value
);
const updatedDeliveries =
formik.values.deliveries.map((delivery) => ({
...delivery,
products: [
{
product: null,
product_id: 0,
product_qty: '',
},
],
}));
formik.setFieldValue(
'deliveries',
updatedDeliveries
);
formik.setFieldTouched('deliveries', false);
}}
onChange={(val) => handleProductChange(idx, val)}
options={productWarehouseOptions}
onInputChange={setProductWarehouseSelectInputValue}
onMenuScrollToBottom={loadMoreProductWarehouses}
@@ -1427,19 +1480,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
selectedDeliveries.length &&
formik.values.deliveries?.length > 0
}
onChange={(
e: React.ChangeEvent<HTMLInputElement>
) => {
if (e.target.checked) {
setSelectedDeliveries(
formik.values.deliveries?.map(
(_, idx) => idx
) ?? []
);
} else {
setSelectedDeliveries([]);
}
}}
onChange={handleDeliverySelectAllChange}
classNames={{
wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm',
@@ -1522,20 +1563,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<CheckboxInput
name={`delivery-${idx}`}
checked={selectedDeliveries.includes(idx)}
onChange={(
e: React.ChangeEvent<HTMLInputElement>
) => {
if (e.target.checked) {
setSelectedDeliveries([
...selectedDeliveries,
idx,
]);
} else {
setSelectedDeliveries(
selectedDeliveries.filter((i) => i !== idx)
);
}
}}
onChange={handleDeliveryCheckboxChange}
classNames={{
wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm',
@@ -1548,24 +1576,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
required
placeholder='Pilih produk...'
value={delivery.products[0]?.product ?? undefined}
onChange={(val) => {
formik.setFieldTouched(
`deliveries.${idx}.products.0.product`,
true
);
formik.setFieldValue(
`deliveries.${idx}.products.0.product`,
val
);
formik.setFieldTouched(
`deliveries.${idx}.products.0.product_id`,
true
);
formik.setFieldValue(
`deliveries.${idx}.products.0.product_id`,
(val as OptionType)?.value
);
}}
onChange={(val) =>
handleDeliveryProductChange(idx, val)
}
options={getFilteredProductWarehouseOptions()}
isDisabled={type === 'detail'}
isClearable
@@ -1616,24 +1629,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
required
placeholder='Pilih supplier...'
value={delivery.supplier}
onChange={(val) => {
formik.setFieldTouched(
`deliveries.${idx}.supplier`,
true
);
formik.setFieldValue(
`deliveries.${idx}.supplier`,
val
);
formik.setFieldTouched(
`deliveries.${idx}.supplier_id`,
true
);
formik.setFieldValue(
`deliveries.${idx}.supplier_id`,
(val as OptionType)?.value
);
}}
onChange={(val) =>
handleDeliverySupplierChange(idx, val)
}
options={supplierOptions}
onInputChange={setSupplierSelectInputValue}
isLoading={isLoadingSuppliers}
@@ -1725,20 +1723,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<FileInput
accept='.pdf,.jpg,.jpeg,.png'
name={`deliveries.${idx}.document`}
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
if (file.size > 5 * 1024 * 1024) {
toast.error('Ukuran dokumen maksimal 5 MB!');
e.target.value = '';
return;
}
formik.setFieldValue(
`deliveries.${idx}.document`,
file
);
}
}}
onChange={(e) =>
handleDeliveryDocumentChange(idx, e)
}
{...isRepeaterInputError(
'deliveries',
'document',