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