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 ===== // ===== EVENT HANDLERS =====
// Product Handlers const handleTransferDateChange = useCallback(
const addProduct = () => { (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 = [ const newProducts = [
...(formik.values.products || []), ...(formik.values.products || []),
{ {
@@ -365,22 +463,19 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
}, },
]; ];
formik.setFieldValue('products', newProducts); formik.setFieldValue('products', newProducts);
}; }, []);
const removeProduct = useCallback( const removeProduct = useCallback((i: number) => {
(i: number) => { const updatedProducts =
const updatedProducts = formik.values.products?.reduce((acc: ProductSchema[], item, index) => {
formik.values.products?.reduce((acc: ProductSchema[], item, index) => { if (index !== i) {
if (index !== i) { acc.push(item);
acc.push(item); }
} return acc;
return acc; }, []) ?? [];
}, []) ?? [];
formik.setFieldValue('products', updatedProducts); formik.setFieldValue('products', updatedProducts);
}, }, []);
[formik]
);
const bulkRemoveProduct = useCallback(() => { const bulkRemoveProduct = useCallback(() => {
const updatedProducts = const updatedProducts =
@@ -389,10 +484,60 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
) ?? []; ) ?? [];
formik.setFieldValue('products', updatedProducts); formik.setFieldValue('products', updatedProducts);
setSelectedProducts([]); setSelectedProducts([]);
}, [formik, selectedProducts]); }, [formik, selectedProducts, setSelectedProducts]);
// Delivery Handlers const handleProductChange = useCallback(
const addDelivery = () => { (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.setFieldValue('deliveries', [
...(formik.values.deliveries || []), ...(formik.values.deliveries || []),
{ {
@@ -412,25 +557,19 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
], ],
}, },
]); ]);
}; }, []);
const removeDelivery = useCallback( const removeDelivery = useCallback((i: number) => {
(i: number) => { const updatedDeliveries =
const updatedDeliveries = formik.values.deliveries?.reduce((acc: DeliverySchema[], item, index) => {
formik.values.deliveries?.reduce( if (index !== i) {
(acc: DeliverySchema[], item, index) => { acc.push(item);
if (index !== i) { }
acc.push(item); return acc;
} }, []) ?? [];
return acc;
},
[]
) ?? [];
formik.setFieldValue('deliveries', updatedDeliveries); formik.setFieldValue('deliveries', updatedDeliveries);
}, }, []);
[formik]
);
const bulkRemoveDelivery = useCallback(() => { const bulkRemoveDelivery = useCallback(() => {
const updatedDeliveries = const updatedDeliveries =
@@ -439,33 +578,101 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
) ?? []; ) ?? [];
formik.setFieldValue('deliveries', updatedDeliveries); formik.setFieldValue('deliveries', updatedDeliveries);
setSelectedDeliveries([]); setSelectedDeliveries([]);
}, [formik, selectedDeliveries]); }, [formik, selectedDeliveries, setSelectedDeliveries]);
// Cost Calculation Handlers const handleDeliverySelectAllChange = useCallback(
const handleDeliveryCostChange = useCallback( (e: React.ChangeEvent<HTMLInputElement>) => {
(idx: number, value: number) => { if (e.target.checked) {
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value); setSelectedDeliveries(
formik.values.deliveries?.map((_, idx) => idx) ?? []
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) { } else {
const perItem = value / productQty; setSelectedDeliveries([]);
formik.setFieldValue(
`deliveries.${idx}.delivery_cost_per_item`,
perItem
);
} else if (value === 0) {
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
}
} }
}, },
[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( const handleDeliveryCostPerItemChange = useCallback(
(idx: number, value: number) => { (idx: number, value: number) => {
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value); formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value);
@@ -484,7 +691,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
} }
} }
}, },
[formik] []
); );
const handleDeliveryCostChangeWrapper = useCallback( const handleDeliveryCostChangeWrapper = useCallback(
@@ -959,72 +1166,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
label='Gudang' label='Gudang'
placeholder='Pilih gudang asal...' placeholder='Pilih gudang asal...'
value={formik.values.source_warehouse} value={formik.values.source_warehouse}
onChange={(val) => { onChange={handleSourceWarehouseChange}
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);
}
}}
options={warehouseOptions} options={warehouseOptions}
onInputChange={setWarehouseSelectInputValue} onInputChange={setWarehouseSelectInputValue}
onMenuScrollToBottom={loadMoreWarehouses} onMenuScrollToBottom={loadMoreWarehouses}
@@ -1088,41 +1230,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
label='Gudang' label='Gudang'
placeholder='Pilih gudang tujuan...' placeholder='Pilih gudang tujuan...'
value={formik.values.destination_warehouse} value={formik.values.destination_warehouse}
onChange={(val) => { onChange={handleDestinationWarehouseChange}
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);
}
}}
options={warehouseOptions} options={warehouseOptions}
onInputChange={setWarehouseSelectInputValue} onInputChange={setWarehouseSelectInputValue}
isLoading={isLoadingWarehouses} isLoading={isLoadingWarehouses}
@@ -1196,18 +1304,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
selectedProducts.length && selectedProducts.length &&
formik.values.products?.length > 0 formik.values.products?.length > 0
} }
onChange={( onChange={handleProductSelectAllChange}
e: React.ChangeEvent<HTMLInputElement>
) => {
if (e.target.checked) {
setSelectedProducts(
formik.values.products?.map((_, idx) => idx) ??
[]
);
} else {
setSelectedProducts([]);
}
}}
classNames={{ classNames={{
wrapper: 'flex justify-center', wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm', checkbox: 'checkbox checkbox-sm',
@@ -1244,17 +1341,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<CheckboxInput <CheckboxInput
name={`product-${idx}`} name={`product-${idx}`}
checked={selectedProducts.includes(idx)} checked={selectedProducts.includes(idx)}
onChange={( onChange={handleProductCheckboxChange}
e: React.ChangeEvent<HTMLInputElement>
) => {
if (e.target.checked) {
setSelectedProducts([...selectedProducts, idx]);
} else {
setSelectedProducts(
selectedProducts.filter((i) => i !== idx)
);
}
}}
classNames={{ classNames={{
wrapper: 'flex justify-center', wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm', checkbox: 'checkbox checkbox-sm',
@@ -1266,41 +1353,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<SelectInput <SelectInput
required required
value={product.product ?? undefined} value={product.product ?? undefined}
onChange={(val) => { onChange={(val) => handleProductChange(idx, 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);
}}
options={productWarehouseOptions} options={productWarehouseOptions}
onInputChange={setProductWarehouseSelectInputValue} onInputChange={setProductWarehouseSelectInputValue}
onMenuScrollToBottom={loadMoreProductWarehouses} onMenuScrollToBottom={loadMoreProductWarehouses}
@@ -1427,19 +1480,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
selectedDeliveries.length && selectedDeliveries.length &&
formik.values.deliveries?.length > 0 formik.values.deliveries?.length > 0
} }
onChange={( onChange={handleDeliverySelectAllChange}
e: React.ChangeEvent<HTMLInputElement>
) => {
if (e.target.checked) {
setSelectedDeliveries(
formik.values.deliveries?.map(
(_, idx) => idx
) ?? []
);
} else {
setSelectedDeliveries([]);
}
}}
classNames={{ classNames={{
wrapper: 'flex justify-center', wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm', checkbox: 'checkbox checkbox-sm',
@@ -1522,20 +1563,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<CheckboxInput <CheckboxInput
name={`delivery-${idx}`} name={`delivery-${idx}`}
checked={selectedDeliveries.includes(idx)} checked={selectedDeliveries.includes(idx)}
onChange={( onChange={handleDeliveryCheckboxChange}
e: React.ChangeEvent<HTMLInputElement>
) => {
if (e.target.checked) {
setSelectedDeliveries([
...selectedDeliveries,
idx,
]);
} else {
setSelectedDeliveries(
selectedDeliveries.filter((i) => i !== idx)
);
}
}}
classNames={{ classNames={{
wrapper: 'flex justify-center', wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm', checkbox: 'checkbox checkbox-sm',
@@ -1548,24 +1576,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
required required
placeholder='Pilih produk...' placeholder='Pilih produk...'
value={delivery.products[0]?.product ?? undefined} value={delivery.products[0]?.product ?? undefined}
onChange={(val) => { onChange={(val) =>
formik.setFieldTouched( handleDeliveryProductChange(idx, val)
`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
);
}}
options={getFilteredProductWarehouseOptions()} options={getFilteredProductWarehouseOptions()}
isDisabled={type === 'detail'} isDisabled={type === 'detail'}
isClearable isClearable
@@ -1616,24 +1629,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
required required
placeholder='Pilih supplier...' placeholder='Pilih supplier...'
value={delivery.supplier} value={delivery.supplier}
onChange={(val) => { onChange={(val) =>
formik.setFieldTouched( handleDeliverySupplierChange(idx, val)
`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
);
}}
options={supplierOptions} options={supplierOptions}
onInputChange={setSupplierSelectInputValue} onInputChange={setSupplierSelectInputValue}
isLoading={isLoadingSuppliers} isLoading={isLoadingSuppliers}
@@ -1725,20 +1723,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<FileInput <FileInput
accept='.pdf,.jpg,.jpeg,.png' accept='.pdf,.jpg,.jpeg,.png'
name={`deliveries.${idx}.document`} name={`deliveries.${idx}.document`}
onChange={(e) => { onChange={(e) =>
const file = e.target.files?.[0]; handleDeliveryDocumentChange(idx, e)
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
);
}
}}
{...isRepeaterInputError( {...isRepeaterInputError(
'deliveries', 'deliveries',
'document', 'document',