mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 07:45:47 +00:00
refactor(FE-64): integrate product and supplier selection with API data fetching in MovementForm
This commit is contained in:
@@ -37,24 +37,102 @@ interface MovementFormProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||||
|
// ===== STATE MANAGEMENT =====
|
||||||
const [, setMovementFormErrorMessage] = useState('');
|
const [, setMovementFormErrorMessage] = useState('');
|
||||||
const [
|
const [productWarehouseSelectInputValue, setProductWarehouseSelectInputValue] = useState('');
|
||||||
productWarehouseSelectInputValue,
|
|
||||||
setProductWarehouseSelectInputValue,
|
|
||||||
] = useState('');
|
|
||||||
const [selectedProducts, setSelectedProducts] = useState<number[]>([]);
|
const [selectedProducts, setSelectedProducts] = useState<number[]>([]);
|
||||||
const [selectedDeliveries, setSelectedDeliveries] = useState<number[]>([]);
|
const [selectedDeliveries, setSelectedDeliveries] = useState<number[]>([]);
|
||||||
|
const [warehouseSelectInputValue, setWarehouseSelectInputValue] = useState('');
|
||||||
|
const [supplierSelectInputValue, setSupplierSelectInputValue] = useState('');
|
||||||
|
|
||||||
|
// ===== FORM HANDLERS =====
|
||||||
const {
|
const {
|
||||||
deleteModal,
|
|
||||||
movementFormErrorMessage,
|
movementFormErrorMessage,
|
||||||
isDeleteLoading,
|
|
||||||
createMovementHandler,
|
createMovementHandler,
|
||||||
updateMovementHandler,
|
updateMovementHandler,
|
||||||
deleteMovementClickHandler,
|
|
||||||
confirmationModalDeleteClickHandler,
|
|
||||||
} = useMovementFormHandlers(initialValues?.id);
|
} = useMovementFormHandlers(initialValues?.id);
|
||||||
|
|
||||||
|
// ===== INTERFACES =====
|
||||||
|
interface WarehouseOptionType extends OptionType {
|
||||||
|
area?: string;
|
||||||
|
location?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProductWarehouseOptionType extends OptionType {
|
||||||
|
product_id: number;
|
||||||
|
warehouse_id: number;
|
||||||
|
warehouse_name: string;
|
||||||
|
quantity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== API DATA FETCHING =====
|
||||||
|
const allProductWarehousesUrl = `${ProductWarehouseApi.basePath}`;
|
||||||
|
const { data: allProductWarehouses } = useSWR(
|
||||||
|
allProductWarehousesUrl,
|
||||||
|
ProductWarehouseApi.getAllFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const warehousesUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ search: warehouseSelectInputValue }).toString()}`;
|
||||||
|
const { data: warehouses, isLoading: isLoadingWarehouses } = useSWR(
|
||||||
|
warehousesUrl,
|
||||||
|
WarehouseApi.getAllFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ search: supplierSelectInputValue }).toString()}`;
|
||||||
|
const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(
|
||||||
|
suppliersUrl,
|
||||||
|
SupplierApi.getAllFetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== DATA PROCESSING =====
|
||||||
|
const warehouseStockMap = useMemo(() => {
|
||||||
|
if (!isResponseSuccess(allProductWarehouses)) return new Map();
|
||||||
|
|
||||||
|
const stockMap = new Map<
|
||||||
|
number,
|
||||||
|
{ totalQty: number; productCount: number }
|
||||||
|
>();
|
||||||
|
|
||||||
|
allProductWarehouses.data.forEach((pw) => {
|
||||||
|
const warehouseId = pw.warehouse.id;
|
||||||
|
const existing = stockMap.get(warehouseId) || {
|
||||||
|
totalQty: 0,
|
||||||
|
productCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
stockMap.set(warehouseId, {
|
||||||
|
totalQty: existing.totalQty + pw.quantity,
|
||||||
|
productCount: existing.productCount + 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return stockMap;
|
||||||
|
}, [allProductWarehouses]);
|
||||||
|
|
||||||
|
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}`,
|
||||||
|
area: w.area?.name,
|
||||||
|
location:
|
||||||
|
'type' in w && (w.type === 'LOKASI' || w.type === 'KANDANG')
|
||||||
|
? w.location?.name
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const supplierOptions = isResponseSuccess(suppliers)
|
||||||
|
? suppliers?.data.map((s) => ({ value: s.id, label: s.name }))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// ===== FORM INITIALIZATION =====
|
||||||
const formikInitialValues = useMemo<MovementFormValues>(
|
const formikInitialValues = useMemo<MovementFormValues>(
|
||||||
() => getMovementFormInitialValues(initialValues),
|
() => getMovementFormInitialValues(initialValues),
|
||||||
[initialValues]
|
[initialValues]
|
||||||
@@ -77,7 +155,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
if (d.document && d.document instanceof File) {
|
if (d.document && d.document instanceof File) {
|
||||||
documents.push(d.document);
|
documents.push(d.document);
|
||||||
documentIndex = documents.length - 1;
|
documentIndex = documents.length - 1;
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -122,91 +199,39 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const addProduct = () => {
|
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
|
||||||
const newProducts = [
|
const getProductWarehousesUrl = useCallback(() => {
|
||||||
...(formik.values.products || []),
|
const productWarehouseParams = new URLSearchParams({
|
||||||
{
|
search: productWarehouseSelectInputValue,
|
||||||
product: null,
|
});
|
||||||
product_id: 0,
|
if (formik.values.source_warehouse_id) {
|
||||||
product_qty: 0,
|
productWarehouseParams.append(
|
||||||
},
|
'warehouse_id',
|
||||||
];
|
formik.values.source_warehouse_id.toString()
|
||||||
formik.setFieldValue('products', newProducts);
|
);
|
||||||
};
|
}
|
||||||
|
return `${ProductWarehouseApi.basePath}?${productWarehouseParams.toString()}`;
|
||||||
|
}, [formik.values.source_warehouse_id, productWarehouseSelectInputValue]);
|
||||||
|
|
||||||
const removeProduct = useCallback(
|
const productWarehousesUrl = getProductWarehousesUrl();
|
||||||
(i: number) => {
|
const { data: productWarehouses, isLoading: isLoadingProductWarehouses } =
|
||||||
const updatedProducts =
|
useSWR(
|
||||||
formik.values.products?.reduce((acc: ProductSchema[], item, index) => {
|
formik.values.source_warehouse_id ? productWarehousesUrl : null,
|
||||||
if (index !== i) {
|
ProductWarehouseApi.getAllFetcher
|
||||||
acc.push(item);
|
);
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []) ?? [];
|
|
||||||
|
|
||||||
formik.setFieldValue('products', updatedProducts);
|
const productWarehouseOptions = isResponseSuccess(productWarehouses)
|
||||||
},
|
? productWarehouses?.data.map((pw) => ({
|
||||||
[formik]
|
value: pw.product.id,
|
||||||
);
|
label: `${pw.product.name} - ${pw.warehouse.name} (Stock: ${pw.quantity.toLocaleString('id-ID')})`,
|
||||||
|
product_id: pw.product.id,
|
||||||
const bulkRemoveProduct = useCallback(() => {
|
warehouse_id: pw.warehouse.id,
|
||||||
const updatedProducts =
|
warehouse_name: pw.warehouse.name,
|
||||||
formik.values.products?.filter(
|
quantity: pw.quantity,
|
||||||
(_, idx) => !selectedProducts.includes(idx)
|
}))
|
||||||
) ?? [];
|
: [];
|
||||||
formik.setFieldValue('products', updatedProducts);
|
|
||||||
setSelectedProducts([]);
|
|
||||||
}, [formik, selectedProducts]);
|
|
||||||
|
|
||||||
const addDelivery = () => {
|
|
||||||
formik.setFieldValue('deliveries', [
|
|
||||||
...(formik.values.deliveries || []),
|
|
||||||
{
|
|
||||||
delivery_cost: undefined,
|
|
||||||
delivery_cost_per_item: undefined,
|
|
||||||
document: null,
|
|
||||||
driver_name: '',
|
|
||||||
vehicle_plate: '',
|
|
||||||
supplier: null,
|
|
||||||
supplier_id: 0,
|
|
||||||
products: [
|
|
||||||
{
|
|
||||||
product: null,
|
|
||||||
product_id: 0,
|
|
||||||
product_qty: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
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]
|
|
||||||
);
|
|
||||||
|
|
||||||
const bulkRemoveDelivery = useCallback(() => {
|
|
||||||
const updatedDeliveries =
|
|
||||||
formik.values.deliveries?.filter(
|
|
||||||
(_, idx) => !selectedDeliveries.includes(idx)
|
|
||||||
) ?? [];
|
|
||||||
formik.setFieldValue('deliveries', updatedDeliveries);
|
|
||||||
setSelectedDeliveries([]);
|
|
||||||
}, [formik, selectedDeliveries]);
|
|
||||||
|
|
||||||
|
// ===== HELPER FUNCTIONS =====
|
||||||
const isRepeaterInputError = <T extends 'products' | 'deliveries'>(
|
const isRepeaterInputError = <T extends 'products' | 'deliveries'>(
|
||||||
arrayName: T,
|
arrayName: T,
|
||||||
column: T extends 'products' ? keyof ProductSchema : keyof DeliverySchema,
|
column: T extends 'products' ? keyof ProductSchema : keyof DeliverySchema,
|
||||||
@@ -263,118 +288,98 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
interface WarehouseOptionType extends OptionType {
|
// ===== EVENT HANDLERS =====
|
||||||
area?: string;
|
// Product Handlers
|
||||||
location?: string;
|
const addProduct = () => {
|
||||||
}
|
const newProducts = [
|
||||||
|
...(formik.values.products || []),
|
||||||
|
{
|
||||||
|
product: null,
|
||||||
|
product_id: 0,
|
||||||
|
product_qty: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
formik.setFieldValue('products', newProducts);
|
||||||
|
};
|
||||||
|
|
||||||
interface ProductWarehouseOptionType extends OptionType {
|
const removeProduct = useCallback(
|
||||||
product_id: number;
|
(i: number) => {
|
||||||
warehouse_id: number;
|
const updatedProducts =
|
||||||
warehouse_name: string;
|
formik.values.products?.reduce((acc: ProductSchema[], item, index) => {
|
||||||
quantity: number;
|
if (index !== i) {
|
||||||
}
|
acc.push(item);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []) ?? [];
|
||||||
|
|
||||||
const allProductWarehousesUrl = `${ProductWarehouseApi.basePath}`;
|
formik.setFieldValue('products', updatedProducts);
|
||||||
const { data: allProductWarehouses } = useSWR(
|
},
|
||||||
allProductWarehousesUrl,
|
[formik]
|
||||||
ProductWarehouseApi.getAllFetcher
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const warehouseStockMap = useMemo(() => {
|
const bulkRemoveProduct = useCallback(() => {
|
||||||
if (!isResponseSuccess(allProductWarehouses)) return new Map();
|
const updatedProducts =
|
||||||
|
formik.values.products?.filter(
|
||||||
|
(_, idx) => !selectedProducts.includes(idx)
|
||||||
|
) ?? [];
|
||||||
|
formik.setFieldValue('products', updatedProducts);
|
||||||
|
setSelectedProducts([]);
|
||||||
|
}, [formik, selectedProducts]);
|
||||||
|
|
||||||
const stockMap = new Map<
|
// Delivery Handlers
|
||||||
number,
|
const addDelivery = () => {
|
||||||
{ totalQty: number; productCount: number }
|
formik.setFieldValue('deliveries', [
|
||||||
>();
|
...(formik.values.deliveries || []),
|
||||||
|
{
|
||||||
|
delivery_cost: undefined,
|
||||||
|
delivery_cost_per_item: undefined,
|
||||||
|
document: null,
|
||||||
|
driver_name: '',
|
||||||
|
vehicle_plate: '',
|
||||||
|
supplier: null,
|
||||||
|
supplier_id: 0,
|
||||||
|
products: [
|
||||||
|
{
|
||||||
|
product: null,
|
||||||
|
product_id: 0,
|
||||||
|
product_qty: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
allProductWarehouses.data.forEach((pw) => {
|
const removeDelivery = useCallback(
|
||||||
const warehouseId = pw.warehouse.id;
|
(i: number) => {
|
||||||
const existing = stockMap.get(warehouseId) || {
|
const updatedDeliveries =
|
||||||
totalQty: 0,
|
formik.values.deliveries?.reduce(
|
||||||
productCount: 0,
|
(acc: DeliverySchema[], item, index) => {
|
||||||
};
|
if (index !== i) {
|
||||||
|
acc.push(item);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
stockMap.set(warehouseId, {
|
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||||
totalQty: existing.totalQty + pw.quantity,
|
},
|
||||||
productCount: existing.productCount + 1,
|
[formik]
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return stockMap;
|
|
||||||
}, [allProductWarehouses]);
|
|
||||||
|
|
||||||
// Warehouse selection
|
|
||||||
const [warehouseSelectInputValue, setWarehouseSelectInputValue] =
|
|
||||||
useState('');
|
|
||||||
const warehousesUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ search: warehouseSelectInputValue }).toString()}`;
|
|
||||||
const { data: warehouses, isLoading: isLoadingWarehouses } = useSWR(
|
|
||||||
warehousesUrl,
|
|
||||||
WarehouseApi.getAllFetcher
|
|
||||||
);
|
);
|
||||||
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 {
|
const bulkRemoveDelivery = useCallback(() => {
|
||||||
value: w.id,
|
const updatedDeliveries =
|
||||||
label: `${w.name}${stockLabel}`,
|
formik.values.deliveries?.filter(
|
||||||
area: w.area?.name,
|
(_, idx) => !selectedDeliveries.includes(idx)
|
||||||
location:
|
) ?? [];
|
||||||
'type' in w && (w.type === 'LOKASI' || w.type === 'KANDANG')
|
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||||
? w.location?.name
|
setSelectedDeliveries([]);
|
||||||
: undefined,
|
}, [formik, selectedDeliveries]);
|
||||||
};
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// Product Warehouse selection - Filter by source warehouse
|
// Cost Calculation Handlers
|
||||||
const productWarehouseParams = new URLSearchParams({
|
|
||||||
search: productWarehouseSelectInputValue,
|
|
||||||
});
|
|
||||||
if (formik.values.source_warehouse_id) {
|
|
||||||
productWarehouseParams.append(
|
|
||||||
'warehouse_id',
|
|
||||||
formik.values.source_warehouse_id.toString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const productWarehousesUrl = `${ProductWarehouseApi.basePath}?${productWarehouseParams.toString()}`;
|
|
||||||
const { data: productWarehouses, isLoading: isLoadingProductWarehouses } =
|
|
||||||
useSWR(
|
|
||||||
formik.values.source_warehouse_id ? productWarehousesUrl : null,
|
|
||||||
ProductWarehouseApi.getAllFetcher
|
|
||||||
);
|
|
||||||
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')})`,
|
|
||||||
product_id: pw.product.id,
|
|
||||||
warehouse_id: pw.warehouse.id,
|
|
||||||
warehouse_name: pw.warehouse.name,
|
|
||||||
quantity: pw.quantity,
|
|
||||||
}))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// Supplier selection
|
|
||||||
const [supplierSelectInputValue, setSupplierSelectInputValue] = useState('');
|
|
||||||
const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ search: supplierSelectInputValue }).toString()}`;
|
|
||||||
const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(
|
|
||||||
suppliersUrl,
|
|
||||||
SupplierApi.getAllFetcher
|
|
||||||
);
|
|
||||||
const supplierOptions = isResponseSuccess(suppliers)
|
|
||||||
? suppliers?.data.map((s) => ({ value: s.id, label: s.name }))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// Handle cost calculation when delivery_cost changes
|
|
||||||
const handleDeliveryCostChange = useCallback(
|
const handleDeliveryCostChange = useCallback(
|
||||||
(idx: number, value: string) => {
|
(idx: number, value: number) => {
|
||||||
const numValue = parseFloat(value) || 0;
|
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value);
|
||||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, numValue);
|
|
||||||
|
|
||||||
const delivery = formik.values.deliveries?.[idx];
|
const delivery = formik.values.deliveries?.[idx];
|
||||||
if (delivery) {
|
if (delivery) {
|
||||||
@@ -382,13 +387,13 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
(sum, p) => sum + p.product_qty,
|
(sum, p) => sum + p.product_qty,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
if (productQty > 0 && numValue > 0) {
|
if (productQty > 0 && value > 0) {
|
||||||
const perItem = numValue / productQty;
|
const perItem = value / productQty;
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
`deliveries.${idx}.delivery_cost_per_item`,
|
`deliveries.${idx}.delivery_cost_per_item`,
|
||||||
perItem
|
perItem
|
||||||
);
|
);
|
||||||
} else if (numValue === 0) {
|
} else if (value === 0) {
|
||||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
|
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -396,13 +401,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
[formik]
|
[formik]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle cost calculation when delivery_cost_per_item changes
|
|
||||||
const handleDeliveryCostPerItemChange = useCallback(
|
const handleDeliveryCostPerItemChange = useCallback(
|
||||||
(idx: number, value: string) => {
|
(idx: number, value: number) => {
|
||||||
const numValue = parseFloat(value) || 0;
|
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
`deliveries.${idx}.delivery_cost_per_item`,
|
`deliveries.${idx}.delivery_cost_per_item`,
|
||||||
numValue
|
value
|
||||||
);
|
);
|
||||||
|
|
||||||
const delivery = formik.values.deliveries?.[idx];
|
const delivery = formik.values.deliveries?.[idx];
|
||||||
@@ -411,10 +414,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
(sum, p) => sum + p.product_qty,
|
(sum, p) => sum + p.product_qty,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
if (productQty > 0 && numValue > 0) {
|
if (productQty > 0 && value > 0) {
|
||||||
const totalCost = numValue * productQty;
|
const totalCost = value * productQty;
|
||||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
|
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
|
||||||
} else if (numValue === 0) {
|
} else if (value === 0) {
|
||||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, 0);
|
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -422,57 +425,27 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
[formik]
|
[formik]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Auto-recalculate when product quantity changes
|
const handleDeliveryCostChangeWrapper = useCallback(
|
||||||
useEffect(() => {
|
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
formik.values.deliveries?.forEach((delivery, idx) => {
|
const rawValue = e.target.value.replace(/[^\d,.-]/g, '');
|
||||||
const productQty = delivery.products.reduce(
|
const normalizedValue = rawValue.replace(/,/g, '');
|
||||||
(sum, p) => sum + p.product_qty,
|
const value = parseFloat(normalizedValue) || 0;
|
||||||
0
|
handleDeliveryCostChange(idx, value);
|
||||||
);
|
},
|
||||||
|
[handleDeliveryCostChange]
|
||||||
|
);
|
||||||
|
|
||||||
// If delivery_cost is set, recalculate delivery_cost_per_item
|
const handleDeliveryCostPerItemChangeWrapper = useCallback(
|
||||||
if (
|
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
delivery.delivery_cost &&
|
const rawValue = e.target.value.replace(/[^\d,.-]/g, '');
|
||||||
delivery.delivery_cost > 0 &&
|
const normalizedValue = rawValue.replace(/,/g, '');
|
||||||
productQty > 0
|
const value = parseFloat(normalizedValue) || 0;
|
||||||
) {
|
handleDeliveryCostPerItemChange(idx, value);
|
||||||
const perItem = delivery.delivery_cost / productQty;
|
},
|
||||||
if (Math.abs((delivery.delivery_cost_per_item || 0) - perItem) > 0.01) {
|
[handleDeliveryCostPerItemChange]
|
||||||
formik.setFieldValue(
|
);
|
||||||
`deliveries.${idx}.delivery_cost_per_item`,
|
|
||||||
perItem
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If delivery_cost_per_item is set, recalculate delivery_cost
|
|
||||||
else if (
|
|
||||||
delivery.delivery_cost_per_item &&
|
|
||||||
delivery.delivery_cost_per_item > 0 &&
|
|
||||||
productQty > 0
|
|
||||||
) {
|
|
||||||
const totalCost = delivery.delivery_cost_per_item * productQty;
|
|
||||||
if (Math.abs((delivery.delivery_cost || 0) - totalCost) > 0.01) {
|
|
||||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [
|
|
||||||
formik.values.deliveries
|
|
||||||
?.map((d) => d.products.reduce((sum, p) => sum + p.product_qty, 0))
|
|
||||||
.join(','),
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
formik.values.source_warehouse_id &&
|
|
||||||
type !== 'edit' &&
|
|
||||||
type !== 'detail'
|
|
||||||
) {
|
|
||||||
formik.setFieldValue('products', []);
|
|
||||||
formik.setFieldValue('deliveries', []);
|
|
||||||
}
|
|
||||||
}, [formik.values.source_warehouse_id]);
|
|
||||||
|
|
||||||
|
// UTILITY FUNCTIONS
|
||||||
const getFilteredProductWarehouseOptions = useCallback(() => {
|
const getFilteredProductWarehouseOptions = useCallback(() => {
|
||||||
return (
|
return (
|
||||||
formik.values.products
|
formik.values.products
|
||||||
@@ -618,6 +591,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
[formik.values.deliveries, formik.values.products, type]
|
[formik.values.deliveries, formik.values.products, type]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ===== COMPUTED VALUES =====
|
||||||
const invalidQtyRows = useMemo(
|
const invalidQtyRows = useMemo(
|
||||||
() =>
|
() =>
|
||||||
type === 'detail'
|
type === 'detail'
|
||||||
@@ -650,6 +624,54 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
);
|
);
|
||||||
}, [formik.values.products, getProductQtyError, type]);
|
}, [formik.values.products, getProductQtyError, type]);
|
||||||
|
|
||||||
|
// ===== EFFECTS =====
|
||||||
|
useEffect(() => {
|
||||||
|
formik.values.deliveries?.forEach((delivery, idx) => {
|
||||||
|
const productQty = delivery.products.reduce(
|
||||||
|
(sum, p) => sum + p.product_qty,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
delivery.delivery_cost &&
|
||||||
|
delivery.delivery_cost > 0 &&
|
||||||
|
productQty > 0
|
||||||
|
) {
|
||||||
|
const perItem = delivery.delivery_cost / productQty;
|
||||||
|
if (Math.abs((delivery.delivery_cost_per_item || 0) - perItem) > 0.01) {
|
||||||
|
formik.setFieldValue(
|
||||||
|
`deliveries.${idx}.delivery_cost_per_item`,
|
||||||
|
perItem
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
delivery.delivery_cost_per_item &&
|
||||||
|
delivery.delivery_cost_per_item > 0 &&
|
||||||
|
productQty > 0
|
||||||
|
) {
|
||||||
|
const totalCost = delivery.delivery_cost_per_item * productQty;
|
||||||
|
if (Math.abs((delivery.delivery_cost || 0) - totalCost) > 0.01) {
|
||||||
|
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
formik.values.deliveries
|
||||||
|
?.map((d) => d.products.reduce((sum, p) => sum + p.product_qty, 0))
|
||||||
|
.join(','),
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
formik.values.source_warehouse_id &&
|
||||||
|
type !== 'edit' &&
|
||||||
|
type !== 'detail'
|
||||||
|
) {
|
||||||
|
formik.setFieldValue('products', []);
|
||||||
|
formik.setFieldValue('deliveries', []);
|
||||||
|
}
|
||||||
|
}, [formik.values.source_warehouse_id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full'>
|
<section className='w-full'>
|
||||||
@@ -839,7 +861,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<tr>
|
<tr>
|
||||||
{type !== 'detail' && (
|
{type !== 'detail' && (
|
||||||
<th>
|
<th>
|
||||||
<div className='flex justify-center'>
|
|
||||||
<CheckboxInput
|
<CheckboxInput
|
||||||
name='select-all-products'
|
name='select-all-products'
|
||||||
checked={
|
checked={
|
||||||
@@ -865,7 +886,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
checkbox: 'checkbox checkbox-sm',
|
checkbox: 'checkbox checkbox-sm',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
<th>
|
<th>
|
||||||
@@ -893,31 +913,29 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
{formik.values.products?.map((product, idx) => (
|
{formik.values.products?.map((product, idx) => (
|
||||||
<tr key={`product-row-${idx}-${product.product_id}`}>
|
<tr key={`product-row-${idx}-${product.product_id}`}>
|
||||||
{type !== 'detail' && (
|
{type !== 'detail' && (
|
||||||
<td>
|
<td className="!align-middle">
|
||||||
<div className='flex justify-center'>
|
<CheckboxInput
|
||||||
<CheckboxInput
|
name={`product-${idx}`}
|
||||||
name={`product-${idx}`}
|
checked={selectedProducts.includes(idx)}
|
||||||
checked={selectedProducts.includes(idx)}
|
onChange={(
|
||||||
onChange={(
|
e: React.ChangeEvent<HTMLInputElement>,
|
||||||
e: React.ChangeEvent<HTMLInputElement>
|
) => {
|
||||||
) => {
|
if (e.target.checked) {
|
||||||
if (e.target.checked) {
|
setSelectedProducts([
|
||||||
setSelectedProducts([
|
...selectedProducts,
|
||||||
...selectedProducts,
|
idx,
|
||||||
idx,
|
]);
|
||||||
]);
|
} else {
|
||||||
} else {
|
setSelectedProducts(
|
||||||
setSelectedProducts(
|
selectedProducts.filter((i) => i !== idx),
|
||||||
selectedProducts.filter((i) => i !== idx)
|
);
|
||||||
);
|
}
|
||||||
}
|
}}
|
||||||
}}
|
classNames={{
|
||||||
classNames={{
|
wrapper: 'flex justify-center',
|
||||||
wrapper: 'flex justify-center',
|
checkbox: 'checkbox checkbox-sm',
|
||||||
checkbox: 'checkbox checkbox-sm',
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
<td>
|
<td>
|
||||||
@@ -1061,7 +1079,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<tr>
|
<tr>
|
||||||
{type !== 'detail' && (
|
{type !== 'detail' && (
|
||||||
<th>
|
<th>
|
||||||
<div className='flex justify-center'>
|
|
||||||
<CheckboxInput
|
<CheckboxInput
|
||||||
name='select-all-deliveries'
|
name='select-all-deliveries'
|
||||||
checked={
|
checked={
|
||||||
@@ -1087,7 +1104,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
checkbox: 'checkbox checkbox-sm',
|
checkbox: 'checkbox checkbox-sm',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
<th>
|
<th>
|
||||||
@@ -1161,33 +1177,31 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
{formik.values.deliveries?.map((delivery, idx) => (
|
{formik.values.deliveries?.map((delivery, idx) => (
|
||||||
<tr key={`delivery-row-${idx}`}>
|
<tr key={`delivery-row-${idx}`}>
|
||||||
{type !== 'detail' && (
|
{type !== 'detail' && (
|
||||||
<td>
|
<td className="!align-middle">
|
||||||
<div className='flex justify-center'>
|
<CheckboxInput
|
||||||
<CheckboxInput
|
name={`delivery-${idx}`}
|
||||||
name={`delivery-${idx}`}
|
checked={selectedDeliveries.includes(idx)}
|
||||||
checked={selectedDeliveries.includes(idx)}
|
onChange={(
|
||||||
onChange={(
|
e: React.ChangeEvent<HTMLInputElement>,
|
||||||
e: React.ChangeEvent<HTMLInputElement>
|
) => {
|
||||||
) => {
|
if (e.target.checked) {
|
||||||
if (e.target.checked) {
|
setSelectedDeliveries([
|
||||||
setSelectedDeliveries([
|
...selectedDeliveries,
|
||||||
...selectedDeliveries,
|
idx,
|
||||||
idx,
|
]);
|
||||||
]);
|
} else {
|
||||||
} else {
|
setSelectedDeliveries(
|
||||||
setSelectedDeliveries(
|
selectedDeliveries.filter(
|
||||||
selectedDeliveries.filter(
|
(i) => i !== idx,
|
||||||
(i) => i !== idx
|
),
|
||||||
)
|
);
|
||||||
);
|
}
|
||||||
}
|
}}
|
||||||
}}
|
classNames={{
|
||||||
classNames={{
|
wrapper: 'flex justify-center',
|
||||||
wrapper: 'flex justify-center',
|
checkbox: 'checkbox checkbox-sm',
|
||||||
checkbox: 'checkbox checkbox-sm',
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
<td>
|
<td>
|
||||||
@@ -1373,9 +1387,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
required
|
required
|
||||||
name={`deliveries.${idx}.delivery_cost`}
|
name={`deliveries.${idx}.delivery_cost`}
|
||||||
value={delivery.delivery_cost || ''}
|
value={delivery.delivery_cost || ''}
|
||||||
onChange={(e) =>
|
onChange={handleDeliveryCostChangeWrapper(idx)}
|
||||||
handleDeliveryCostChange(idx, e.target.value)
|
|
||||||
}
|
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
maskType='currency'
|
maskType='currency'
|
||||||
decimals={0}
|
decimals={0}
|
||||||
@@ -1397,12 +1409,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
required
|
required
|
||||||
name={`deliveries.${idx}.delivery_cost_per_item`}
|
name={`deliveries.${idx}.delivery_cost_per_item`}
|
||||||
value={delivery.delivery_cost_per_item || ''}
|
value={delivery.delivery_cost_per_item || ''}
|
||||||
onChange={(e) =>
|
onChange={handleDeliveryCostPerItemChangeWrapper(idx)}
|
||||||
handleDeliveryCostPerItemChange(
|
|
||||||
idx,
|
|
||||||
e.target.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
maskType='currency'
|
maskType='currency'
|
||||||
decimals={0}
|
decimals={0}
|
||||||
|
|||||||
Reference in New Issue
Block a user