From aa21088e99cf25be75961872e1d4d6c795b16af8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 15 Oct 2025 12:30:59 +0700 Subject: [PATCH] feat(FE-62): enhance MovementForm with delivery product input error handling and validation --- .../inventory/movement/form/MovementForm.tsx | 86 ++++++++++++++++++- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index 183fe760..1af77cf7 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -214,6 +214,32 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { }; }; + const isDeliveryProductInputError = ( + deliveryIdx: number, + productIdx: number, + column: keyof DeliverySchema['products'][number] + ) => { + const touchedDelivery = formik.touched.deliveries?.[deliveryIdx]; + const errorDelivery = formik.errors.deliveries?.[deliveryIdx] as + | { products: Array> } + | undefined; + + if (!touchedDelivery?.products || !errorDelivery?.products) { + return { + isError: false, + errorMessage: undefined, + }; + } + + const touchedField = touchedDelivery.products[productIdx]?.[column]; + const errorField = errorDelivery.products[productIdx]?.[column]; + + return { + isError: Boolean(touchedField && errorField), + errorMessage: touchedField ? errorField : undefined, + }; + }; + interface WarehouseOptionType extends OptionType { area?: string; location?: string; @@ -305,11 +331,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { if (!relatedProduct) return true; const totalQtyUsed = - formik.values.deliveries?.reduce((total, d) => { + formik.values.deliveries?.reduce((total, d, dIdx) => { const productQty = d.products.reduce((sum, p, pIdx) => { if ( p.product_id === productId && - !(d === delivery && pIdx === deliveryProductIdx) + !(dIdx === deliveryIdx && pIdx === deliveryProductIdx) ) { return sum + (Number(p.product_qty) || 0); } @@ -323,6 +349,47 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { [formik.values.deliveries, formik.values.products] ); + const getDeliveryQtyError = useCallback( + (deliveryIdx: number, deliveryProductIdx: number) => { + const delivery = formik.values.deliveries?.[deliveryIdx]; + if (!delivery) return null; + + const deliveryProduct = delivery.products[deliveryProductIdx]; + if (!deliveryProduct || !deliveryProduct.product_id) return null; + + const qty = Number(deliveryProduct.product_qty) || 0; + const productId = deliveryProduct.product_id; + + const relatedProduct = formik.values.products?.find( + (p) => p.product_id === productId + ); + if (!relatedProduct) return null; + + const totalQtyUsed = + formik.values.deliveries?.reduce((total, d, dIdx) => { + const productQty = d.products.reduce((sum, p, pIdx) => { + if ( + p.product_id === productId && + !(dIdx === deliveryIdx && pIdx === deliveryProductIdx) + ) { + return sum + (Number(p.product_qty) || 0); + } + return sum; + }, 0); + return total + productQty; + }, 0) || 0; + + const availableQty = Number(relatedProduct.product_qty) - totalQtyUsed; + + if (totalQtyUsed + qty > Number(relatedProduct.product_qty)) { + return `Qty melebihi stok produk! Tersedia: ${availableQty}, Total digunakan: ${totalQtyUsed + qty}`; + } + + return null; + }, + [formik.values.deliveries, formik.values.products] + ); + const invalidQtyRows = useMemo( () => formik.values.deliveries?.flatMap((delivery, deliveryIdx) => @@ -334,7 +401,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { [formik.values.deliveries, formik.values.products, validateDeliveryQty] ); - const hasInvalidQty = invalidQtyRows.some(Boolean); + const hasInvalidQty = useMemo( + () => invalidQtyRows.some(Boolean), + [invalidQtyRows] + ); return ( <> @@ -762,6 +832,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { value={delivery.products[0]?.product_qty ?? ''} onChange={formik.handleChange} onBlur={formik.handleBlur} + isError={ + isDeliveryProductInputError(idx, 0, 'product_qty') + .isError || Boolean(getDeliveryQtyError(idx, 0)) + } + errorMessage={ + isDeliveryProductInputError(idx, 0, 'product_qty') + .errorMessage || + getDeliveryQtyError(idx, 0) || + undefined + } readOnly={type === 'detail'} className={{ wrapper: 'w-full min-w-24',