diff --git a/src/components/pages/inventory/movement/form/MovementForm.schema.ts b/src/components/pages/inventory/movement/form/MovementForm.schema.ts index 7a34dfbd..230244be 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.schema.ts +++ b/src/components/pages/inventory/movement/form/MovementForm.schema.ts @@ -1,6 +1,72 @@ import * as Yup from 'yup'; import { Movement } from '@/types/api/inventory/movement'; +export type ProductSchema = { + product: { + value: number; + label: string; + } | null; + product_id: number; + qty_product: number; +}; + +export type EkspedisiSchema = { + product: { + value: number; + label: string; + } | null; + product_id: number; + qty: number; + supplier: { + value: number; + label: string; + } | null; + supplier_id: number; + plat_nomor: string; + no_surat_jalan: string; + dokumen: string | File; + biaya_ekspedisi: number; + nama_sopir: string; +}; + +// Define schemas for nested objects +const ProductObjectSchema: Yup.ObjectSchema = Yup.object({ + product: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + product_id: Yup.number().required('Produk wajib diisi!'), + qty_product: Yup.number() + .required('Qty wajib diisi!') + .min(1, 'Qty minimal 1!') + .typeError('Qty harus berupa angka!'), +}); + +const EkspedisiObjectSchema: Yup.ObjectSchema = Yup.object({ + product: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + product_id: Yup.number().required('Produk wajib diisi!'), + qty: Yup.number() + .required('Qty wajib diisi!') + .min(1, 'Qty minimal 1!') + .typeError('Qty harus berupa angka!'), + supplier: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + supplier_id: Yup.number().required('Supplier wajib diisi!'), + plat_nomor: Yup.string().required('Plat nomor wajib diisi!'), + no_surat_jalan: Yup.string().required('No surat jalan wajib diisi!'), + dokumen: Yup.mixed().required('Dokumen wajib diisi!'), + biaya_ekspedisi: Yup.number() + .required('Biaya ekspedisi wajib diisi!') + .min(0, 'Biaya minimal 0!') + .typeError('Biaya harus berupa angka!'), + nama_sopir: Yup.string().required('Nama sopir wajib diisi!'), +}); + export const MovementFormSchema = Yup.object({ alasan_transfer: Yup.string().required('Alasan transfer wajib diisi!'), tanggal_transfer: Yup.string().required('Tanggal transfer wajib diisi!'), @@ -19,49 +85,9 @@ export const MovementFormSchema = Yup.object({ .required('Gudang tujuan wajib diisi!') .typeError('Gudang tujuan wajib diisi!'), product: Yup.array() - .of( - Yup.object({ - product: Yup.object({ - value: Yup.number().min(1).required(), - label: Yup.string().required(), - }).nullable(), - product_id: Yup.number().required('Produk wajib diisi!'), - qty_product: Yup.number() - .required('Qty wajib diisi!') - .min(1, 'Qty minimal 1!') - .typeError('Qty harus berupa angka!'), - }) - ) + .of(ProductObjectSchema) .min(1, 'Minimal harus ada 1 produk!'), - ekspedisi: Yup.array() - .of( - Yup.object({ - product: Yup.object({ - value: Yup.number().min(1).required(), - label: Yup.string().required(), - }).nullable(), - product_id: Yup.number().required('Produk wajib diisi!'), - qty: Yup.number() - .required('Qty wajib diisi!') - .min(1, 'Qty minimal 1!') - .typeError('Qty harus berupa angka!'), - supplier: Yup.object({ - value: Yup.number().min(1).required(), - label: Yup.string().required(), - }).nullable(), - supplier_id: Yup.number().required('Supplier wajib diisi!'), - plat_nomor: Yup.string().required('Plat nomor wajib diisi!'), - no_surat_jalan: Yup.string().required('No surat jalan wajib diisi!'), - dokumen: Yup.mixed().required('Dokumen wajib diisi!'), - biaya_ekspedisi: Yup.number() - .required('Biaya ekspedisi wajib diisi!') - .min(0, 'Biaya minimal 0!') - .typeError('Biaya harus berupa angka!'), - nama_sopir: Yup.string().required('Nama sopir wajib diisi!'), - }) - ) - .optional() - .default([]), + ekspedisi: Yup.array().of(EkspedisiObjectSchema).optional().default([]), }); export const UpdateMovementFormSchema = MovementFormSchema; diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index 839b83fe..bca69015 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useMemo, useState } from 'react'; -import { FieldArray, FormikProvider, useFormik } from 'formik'; +import { useFormik } from 'formik'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; @@ -21,6 +21,8 @@ import { MovementFormValues, UpdateMovementFormSchema, getMovementFormInitialValues, + ProductSchema, + EkspedisiSchema, } from '@/components/pages/inventory/movement/form/MovementForm.schema'; import { useMovementFormHandlers } from './useMovementFormHandlers'; import { @@ -91,6 +93,72 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { }, }); + const addProduct = () => { + const newProducts = [ + ...(formik.values.product || []), + { + product: null, + product_id: 0, + qty_product: 0, + }, + ]; + formik.setFieldValue('product', newProducts); + }; + + const removeProduct = (index: number) => { + const newProducts = formik.values.product?.filter( + (_, idx) => idx !== index + ); + formik.setFieldValue('product', newProducts); + }; + + const addEkspedisi = () => { + const newEkspedisi = [ + ...(formik.values.ekspedisi || []), + { + product: null, + product_id: 0, + qty: 0, + supplier: null, + supplier_id: 0, + plat_nomor: '', + no_surat_jalan: '', + dokumen: '', + biaya_ekspedisi: 0, + nama_sopir: '', + }, + ]; + formik.setFieldValue('ekspedisi', newEkspedisi); + }; + + const removeEkspedisi = (index: number) => { + const newEkspedisi = formik.values.ekspedisi?.filter( + (_, idx) => idx !== index + ); + formik.setFieldValue('ekspedisi', newEkspedisi); + }; + + const isRepeaterInputError = ( + arrayName: T, + column: T extends 'product' ? keyof ProductSchema : keyof EkspedisiSchema, + idx: number + ) => { + if ( + !formik.touched[arrayName] || + !Array.isArray(formik.touched[arrayName]) + ) { + return false; + } + + const touchedField = formik.touched[arrayName]?.[idx]?.[column as string]; + const errorField = formik.errors[arrayName]?.[idx] as Record< + string, + unknown + >; + + return touchedField && Boolean(errorField?.[column as string]); + }; + // Warehouse selection const [warehouseSelectInputValue, setWarehouseSelectInputValue] = useState(''); @@ -139,448 +207,448 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { title='Movement' backUrl='/inventory/movement' /> - -
- {/* Top card - Movement details */} -
-
-
- - -
-
-
- - {/* Warehouse cards */} -
-
-
- { - formik.setFieldValue('warehouse_asal', val); - formik.setFieldValue( - 'warehouse_asal_id', - (val as OptionType)?.value - ); - }} - options={warehouseOptions} - onInputChange={setWarehouseSelectInputValue} - isLoading={isLoadingWarehouses} - isError={ - formik.touched.warehouse_asal_id && - Boolean(formik.errors.warehouse_asal_id) - } - errorMessage={formik.errors.warehouse_asal_id as string} - isDisabled={type === 'detail'} - isClearable - /> -
-
- -
-
- { - formik.setFieldValue('warehouse_tujuan', val); - formik.setFieldValue( - 'warehouse_tujuan_id', - (val as OptionType)?.value - ); - }} - options={warehouseOptions} - onInputChange={setWarehouseSelectInputValue} - isLoading={isLoadingWarehouses} - isError={ - formik.touched.warehouse_tujuan_id && - Boolean(formik.errors.warehouse_tujuan_id) - } - errorMessage={formik.errors.warehouse_tujuan_id as string} - isDisabled={type === 'detail'} - isClearable - /> -
-
-
- - {/* Products table */} -
-
-

Produk

- - {({ push, remove }) => ( - <> - - - - - - - - - - {formik.values.product?.map((_, index) => ( - - - - - - )) ?? []} - -
ProdukQtyAksi
- { - formik.setFieldValue( - `product.${index}.product`, - val - ); - formik.setFieldValue( - `product.${index}.product_id`, - (val as OptionType)?.value - ); - }} - options={productOptions} - onInputChange={setProductSelectInputValue} - isLoading={isLoadingProducts} - isDisabled={type === 'detail'} - isClearable - /> - - - formik.setFieldValue( - `product.${index}.qty_product`, - e.target.value - ) - } - readOnly={type === 'detail'} - /> - - {type !== 'detail' && ( - - )} -
- {type !== 'detail' && ( - - )} - - )} -
-
-
- - {/* Ekspedisi table */} -
-
-

Ekspedisi

- - {({ push, remove }) => ( - <> - - - - - - - - - - - - - - - - {formik.values.ekspedisi?.map((ekspedisi, index) => ( - - - - - - - - - - - - )) ?? []} - -
ProdukQtySupplierPlat NomorNo Surat JalanDokumenBiaya EkspedisiNama SopirAksi
- { - formik.setFieldValue( - `ekspedisi.${index}.product`, - val - ); - formik.setFieldValue( - `ekspedisi.${index}.product_id`, - (val as OptionType)?.value - ); - }} - options={productOptions} - onInputChange={setProductSelectInputValue} - isLoading={isLoadingProducts} - isDisabled={type === 'detail'} - isClearable - /> - - - formik.setFieldValue( - `ekspedisi.${index}.qty`, - e.target.value - ) - } - readOnly={type === 'detail'} - /> - - { - formik.setFieldValue( - `ekspedisi.${index}.supplier`, - val - ); - formik.setFieldValue( - `ekspedisi.${index}.supplier_id`, - (val as OptionType)?.value - ); - }} - options={supplierOptions} - onInputChange={setSupplierSelectInputValue} - isLoading={isLoadingSuppliers} - isDisabled={type === 'detail'} - isClearable - /> - - - formik.setFieldValue( - `ekspedisi.${index}.plat_nomor`, - e.target.value - ) - } - readOnly={type === 'detail'} - /> - - - formik.setFieldValue( - `ekspedisi.${index}.no_surat_jalan`, - e.target.value - ) - } - readOnly={type === 'detail'} - /> - - { - const file = e.target.files?.[0]; - if (file) { - formik.setFieldValue( - `ekspedisi.${index}.dokumen`, - file - ); - } - }} - readOnly={type === 'detail'} - /> - - - formik.setFieldValue( - `ekspedisi.${index}.biaya_ekspedisi`, - e.target.value - ) - } - readOnly={type === 'detail'} - /> - - - formik.setFieldValue( - `ekspedisi.${index}.nama_sopir`, - e.target.value - ) - } - readOnly={type === 'detail'} - /> - - {type !== 'detail' && ( - - )} -
- {type !== 'detail' && ( - - )} - - )} -
-
-
- - {/* Action buttons */} - - type={type} - formik={formik} - editUrl={ - initialValues - ? `/inventory/movement/detail/edit/?movementId=${initialValues.id}` - : undefined - } - onDelete={deleteMovementClickHandler} - /> - - {movementFormErrorMessage && ( -
- + {/* Top card - Movement details */} +
+
+
+ + - {movementFormErrorMessage}
- )} - - +
+
+ + {/* Warehouse cards */} +
+
+
+ { + formik.setFieldValue('warehouse_asal', val); + formik.setFieldValue( + 'warehouse_asal_id', + (val as OptionType)?.value + ); + }} + options={warehouseOptions} + onInputChange={setWarehouseSelectInputValue} + isLoading={isLoadingWarehouses} + isError={ + formik.touched.warehouse_asal_id && + Boolean(formik.errors.warehouse_asal_id) + } + errorMessage={formik.errors.warehouse_asal_id as string} + isDisabled={type === 'detail'} + isClearable + /> +
+
+ +
+
+ { + formik.setFieldValue('warehouse_tujuan', val); + formik.setFieldValue( + 'warehouse_tujuan_id', + (val as OptionType)?.value + ); + }} + options={warehouseOptions} + onInputChange={setWarehouseSelectInputValue} + isLoading={isLoadingWarehouses} + isError={ + formik.touched.warehouse_tujuan_id && + Boolean(formik.errors.warehouse_tujuan_id) + } + errorMessage={formik.errors.warehouse_tujuan_id as string} + isDisabled={type === 'detail'} + isClearable + /> +
+
+
+ + {/* Products table */} +
+
+

Produk

+
+ + + + + + {type !== 'detail' && } + + + + {formik.values.product?.map((product, idx) => ( + + + + {type !== 'detail' && ( + + )} + + ))} + +
ProdukQtyAksi
+ { + formik.setFieldValue( + `product.${idx}.product`, + val + ); + formik.setFieldValue( + `product.${idx}.product_id`, + (val as OptionType)?.value + ); + }} + options={productOptions} + onInputChange={setProductSelectInputValue} + isLoading={isLoadingProducts} + isDisabled={type === 'detail'} + isClearable + isError={isRepeaterInputError( + 'product', + 'product', + idx + )} + /> + + + + +
+
+ {type !== 'detail' && ( + + )} +
+
+ + {/* Ekspedisi table */} +
+
+

Ekspedisi

+
+ + + + + + + + + + + + {type !== 'detail' && } + + + + {formik.values.ekspedisi?.map((ekspedisi, idx) => ( + + + + + + + + + + {type !== 'detail' && ( + + )} + + ))} + +
ProdukQtySupplierPlat NomorNo Surat JalanDokumenBiaya EkspedisiNama SopirAksi
+ { + formik.setFieldValue( + `ekspedisi.${idx}.product`, + val + ); + formik.setFieldValue( + `ekspedisi.${idx}.product_id`, + (val as OptionType)?.value + ); + }} + options={productOptions} + onInputChange={setProductSelectInputValue} + isLoading={isLoadingProducts} + isDisabled={type === 'detail'} + isClearable + isError={isRepeaterInputError( + 'ekspedisi', + 'product', + idx + )} + /> + + + + { + formik.setFieldValue( + `ekspedisi.${idx}.supplier`, + val + ); + formik.setFieldValue( + `ekspedisi.${idx}.supplier_id`, + (val as OptionType)?.value + ); + }} + options={supplierOptions} + onInputChange={setSupplierSelectInputValue} + isLoading={isLoadingSuppliers} + isDisabled={type === 'detail'} + isClearable + isError={isRepeaterInputError( + 'ekspedisi', + 'supplier', + idx + )} + /> + + + + + + { + const file = e.target.files?.[0]; + if (file) { + formik.setFieldValue( + `ekspedisi.${idx}.dokumen`, + file + ); + } + }} + isError={isRepeaterInputError( + 'ekspedisi', + 'dokumen', + idx + )} + readOnly={type === 'detail'} + className={{ + wrapper: 'w-full min-w-24', + }} + /> + + + + + + +
+
+ {type !== 'detail' && ( + + )} +
+
+ + {/* Action buttons */} + + type={type} + formik={formik} + editUrl={ + initialValues + ? `/inventory/movement/detail/edit/?movementId=${initialValues.id}` + : undefined + } + onDelete={deleteMovementClickHandler} + /> + + {movementFormErrorMessage && ( +
+ + {movementFormErrorMessage} +
+ )} + {type !== 'add' && (