mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 15:55:48 +00:00
feat(FE-65): add validation for quantity and required fields in MovementForm
This commit is contained in:
@@ -8,6 +8,7 @@ interface FormActionsProps<T> {
|
|||||||
formik: FormikContextType<T>;
|
formik: FormikContextType<T>;
|
||||||
editUrl?: string;
|
editUrl?: string;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
|
disableSubmit?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FormActions = <T,>({
|
export const FormActions = <T,>({
|
||||||
@@ -15,6 +16,7 @@ export const FormActions = <T,>({
|
|||||||
formik,
|
formik,
|
||||||
editUrl,
|
editUrl,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
disableSubmit = false,
|
||||||
}: FormActionsProps<T>) => {
|
}: FormActionsProps<T>) => {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
@@ -71,7 +73,7 @@ export const FormActions = <T,>({
|
|||||||
color='primary'
|
color='primary'
|
||||||
className='px-4'
|
className='px-4'
|
||||||
isLoading={formik.isSubmitting}
|
isLoading={formik.isSubmitting}
|
||||||
disabled={!formik.isValid || formik.isSubmitting}
|
disabled={disableSubmit || !formik.isValid || formik.isSubmitting}
|
||||||
>
|
>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -51,7 +51,17 @@ const EkspedisiObjectSchema: Yup.ObjectSchema<EkspedisiSchema> = Yup.object({
|
|||||||
qty: Yup.number()
|
qty: Yup.number()
|
||||||
.required('Qty wajib diisi!')
|
.required('Qty wajib diisi!')
|
||||||
.min(1, 'Qty minimal 1!')
|
.min(1, 'Qty minimal 1!')
|
||||||
.typeError('Qty harus berupa angka!'),
|
.typeError('Qty harus berupa angka!')
|
||||||
|
.test('max-product-qty', 'Qty melebihi stok produk!', function (value) {
|
||||||
|
const { product_id } = this.parent;
|
||||||
|
const products = (this.options.context?.product ?? []) as {
|
||||||
|
product_id: number;
|
||||||
|
qty_product: number;
|
||||||
|
}[];
|
||||||
|
const product = products.find((p) => p.product_id === product_id);
|
||||||
|
if (!product) return true;
|
||||||
|
return (value ?? 0) <= Number(product.qty_product);
|
||||||
|
}),
|
||||||
supplier: Yup.object({
|
supplier: Yup.object({
|
||||||
value: Yup.number().min(1).required(),
|
value: Yup.number().min(1).required(),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
@@ -108,8 +118,12 @@ export const MovementFormSchema = Yup.object({
|
|||||||
.typeError('Gudang tujuan wajib diisi!'),
|
.typeError('Gudang tujuan wajib diisi!'),
|
||||||
product: Yup.array()
|
product: Yup.array()
|
||||||
.of(ProductObjectSchema)
|
.of(ProductObjectSchema)
|
||||||
.min(1, 'Minimal harus ada 1 produk!'),
|
.min(1, 'Minimal harus ada 1 produk!')
|
||||||
ekspedisi: Yup.array().of(EkspedisiObjectSchema).optional().default([]),
|
.required('Produk wajib diisi!'),
|
||||||
|
ekspedisi: Yup.array()
|
||||||
|
.of(EkspedisiObjectSchema)
|
||||||
|
.min(1, 'Minimal harus ada 1 ekspedisi!')
|
||||||
|
.required('Ekspedisi wajib diisi!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateMovementFormSchema = MovementFormSchema;
|
export const UpdateMovementFormSchema = MovementFormSchema;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useFormik } from 'formik';
|
import { FormikProps, useFormik } from 'formik';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
@@ -32,13 +32,30 @@ import {
|
|||||||
} from '@/services/api/master-data';
|
} from '@/services/api/master-data';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import FileInput from '@/components/input/FileInput';
|
import FileInput from '@/components/input/FileInput';
|
||||||
import { containsFile } from '@/lib/form-data';
|
|
||||||
|
|
||||||
interface MovementFormProps {
|
interface MovementFormProps {
|
||||||
type?: 'add' | 'edit' | 'detail';
|
type?: 'add' | 'edit' | 'detail';
|
||||||
initialValues?: Movement;
|
initialValues?: Movement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getEkspedisiFieldError(
|
||||||
|
formik: FormikProps<MovementFormValues>,
|
||||||
|
idx: number,
|
||||||
|
field: keyof EkspedisiSchema
|
||||||
|
) {
|
||||||
|
const errorObj = formik.errors.ekspedisi?.[idx];
|
||||||
|
const touched = formik.touched.ekspedisi?.[idx]?.[field];
|
||||||
|
const isError =
|
||||||
|
touched &&
|
||||||
|
typeof errorObj === 'object' &&
|
||||||
|
!!(errorObj as Record<string, unknown>)?.[field];
|
||||||
|
const errorMessage =
|
||||||
|
typeof errorObj === 'object'
|
||||||
|
? (errorObj as Record<string, string>)?.[field]
|
||||||
|
: undefined;
|
||||||
|
return { isError, errorMessage };
|
||||||
|
}
|
||||||
|
|
||||||
const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||||
const [, setMovementFormErrorMessage] = useState('');
|
const [, setMovementFormErrorMessage] = useState('');
|
||||||
const [selectedProducts, setSelectedProducts] = useState<number[]>([]);
|
const [selectedProducts, setSelectedProducts] = useState<number[]>([]);
|
||||||
@@ -63,12 +80,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
initialValues: formikInitialValues,
|
initialValues: formikInitialValues,
|
||||||
validationSchema:
|
validationSchema:
|
||||||
type === 'edit' ? UpdateMovementFormSchema : MovementFormSchema,
|
type === 'edit' ? UpdateMovementFormSchema : MovementFormSchema,
|
||||||
|
validateOnChange: true,
|
||||||
|
validateOnBlur: true,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
console.log(
|
|
||||||
'Dokumen:',
|
|
||||||
values.ekspedisi?.map((e) => e.dokumen)
|
|
||||||
);
|
|
||||||
|
|
||||||
setMovementFormErrorMessage('');
|
setMovementFormErrorMessage('');
|
||||||
const payload: CreateMovementPayload = {
|
const payload: CreateMovementPayload = {
|
||||||
alasan_transfer: values.alasan_transfer,
|
alasan_transfer: values.alasan_transfer,
|
||||||
@@ -95,9 +109,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('containsFile:', containsFile(payload));
|
|
||||||
console.log('payload:', payload);
|
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'add':
|
case 'add':
|
||||||
await createMovementHandler(payload);
|
await createMovementHandler(payload);
|
||||||
@@ -292,12 +303,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
const validateEkspedisiQty = (ekspedisiIdx: number, qty: number) => {
|
const validateEkspedisiQty = (ekspedisiIdx: number, qty: number) => {
|
||||||
const productId = formik.values.ekspedisi?.[ekspedisiIdx]?.product_id;
|
const productId = formik.values.ekspedisi?.[ekspedisiIdx]?.product_id;
|
||||||
if (!productId) return true;
|
if (!productId) return true;
|
||||||
|
|
||||||
const relatedProduct = formik.values.product?.find(
|
const relatedProduct = formik.values.product?.find(
|
||||||
(p) => p.product_id === productId
|
(p) => p.product_id === productId
|
||||||
);
|
);
|
||||||
if (!relatedProduct) return true;
|
if (!relatedProduct) return true;
|
||||||
|
|
||||||
const totalQtyUsed =
|
const totalQtyUsed =
|
||||||
formik.values.ekspedisi?.reduce((total, eks, i) => {
|
formik.values.ekspedisi?.reduce((total, eks, i) => {
|
||||||
if (eks.product_id === productId && i !== ekspedisiIdx) {
|
if (eks.product_id === productId && i !== ekspedisiIdx) {
|
||||||
@@ -305,10 +314,15 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
}
|
}
|
||||||
return total;
|
return total;
|
||||||
}, 0) || 0;
|
}, 0) || 0;
|
||||||
|
|
||||||
return totalQtyUsed + qty <= Number(relatedProduct.qty_product);
|
return totalQtyUsed + qty <= Number(relatedProduct.qty_product);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const invalidQtyRows =
|
||||||
|
formik.values.ekspedisi?.map((eks, idx) => {
|
||||||
|
const qty = Number(eks.qty) || 0;
|
||||||
|
return !validateEkspedisiQty(idx, qty);
|
||||||
|
}) ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full max-w-5xl'>
|
<section className='w-full max-w-5xl'>
|
||||||
@@ -735,22 +749,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
type='number'
|
type='number'
|
||||||
name={`ekspedisi.${idx}.qty`}
|
name={`ekspedisi.${idx}.qty`}
|
||||||
value={ekspedisi.qty ?? ''}
|
value={ekspedisi.qty ?? ''}
|
||||||
onChange={(e) => {
|
onChange={formik.handleChange}
|
||||||
const newQty = Number(e.target.value);
|
|
||||||
if (validateEkspedisiQty(idx, newQty)) {
|
|
||||||
formik.handleChange(e);
|
|
||||||
} else {
|
|
||||||
toast.error(
|
|
||||||
'Quantity exceeds available product quantity'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
isError={isRepeaterInputError(
|
{...getEkspedisiFieldError(formik, idx, 'qty')}
|
||||||
'ekspedisi',
|
|
||||||
'qty',
|
|
||||||
idx
|
|
||||||
)}
|
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full min-w-24',
|
wrapper: 'w-full min-w-24',
|
||||||
@@ -790,10 +791,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
value={ekspedisi.plat_nomor ?? ''}
|
value={ekspedisi.plat_nomor ?? ''}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
isError={isRepeaterInputError(
|
{...getEkspedisiFieldError(
|
||||||
'ekspedisi',
|
formik,
|
||||||
'plat_nomor',
|
idx,
|
||||||
idx
|
'plat_nomor'
|
||||||
)}
|
)}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
className={{
|
className={{
|
||||||
@@ -808,10 +809,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
value={ekspedisi.no_surat_jalan ?? ''}
|
value={ekspedisi.no_surat_jalan ?? ''}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
isError={isRepeaterInputError(
|
{...getEkspedisiFieldError(
|
||||||
'ekspedisi',
|
formik,
|
||||||
'no_surat_jalan',
|
idx,
|
||||||
idx
|
'no_surat_jalan'
|
||||||
)}
|
)}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
className={{
|
className={{
|
||||||
@@ -866,10 +867,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
value={ekspedisi.biaya_ekspedisi ?? ''}
|
value={ekspedisi.biaya_ekspedisi ?? ''}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
isError={isRepeaterInputError(
|
{...getEkspedisiFieldError(
|
||||||
'ekspedisi',
|
formik,
|
||||||
'biaya_ekspedisi',
|
idx,
|
||||||
idx
|
'biaya_ekspedisi'
|
||||||
)}
|
)}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
className={{
|
className={{
|
||||||
@@ -882,10 +883,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
disabled
|
disabled
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
isError={isRepeaterInputError(
|
{...getEkspedisiFieldError(
|
||||||
'ekspedisi',
|
formik,
|
||||||
'biaya_ekspedisi_per_item',
|
idx,
|
||||||
idx
|
'biaya_ekspedisi_per_item'
|
||||||
)}
|
)}
|
||||||
name={`ekspedisi.${idx}.biaya_ekspedisi_per_item`}
|
name={`ekspedisi.${idx}.biaya_ekspedisi_per_item`}
|
||||||
value={
|
value={
|
||||||
@@ -909,10 +910,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
value={ekspedisi.nama_sopir ?? ''}
|
value={ekspedisi.nama_sopir ?? ''}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
isError={isRepeaterInputError(
|
{...getEkspedisiFieldError(
|
||||||
'ekspedisi',
|
formik,
|
||||||
'nama_sopir',
|
idx,
|
||||||
idx
|
'nama_sopir'
|
||||||
)}
|
)}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
className={{
|
className={{
|
||||||
@@ -982,6 +983,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onDelete={deleteMovementClickHandler}
|
onDelete={deleteMovementClickHandler}
|
||||||
|
disableSubmit={invalidQtyRows.some(Boolean)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{movementFormErrorMessage && (
|
{movementFormErrorMessage && (
|
||||||
|
|||||||
Reference in New Issue
Block a user