refactor(FE-62,65): refactor MovementForm schema and component for improved product and ekspedisi handling

This commit is contained in:
rstubryan
2025-10-10 10:19:56 +07:00
parent 27f58051ad
commit 095190d757
2 changed files with 577 additions and 483 deletions
@@ -1,6 +1,72 @@
import * as Yup from 'yup'; import * as Yup from 'yup';
import { Movement } from '@/types/api/inventory/movement'; 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<ProductSchema> = 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<EkspedisiSchema> = 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<string | File>().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({ export const MovementFormSchema = Yup.object({
alasan_transfer: Yup.string().required('Alasan transfer wajib diisi!'), alasan_transfer: Yup.string().required('Alasan transfer wajib diisi!'),
tanggal_transfer: Yup.string().required('Tanggal 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!') .required('Gudang tujuan wajib diisi!')
.typeError('Gudang tujuan wajib diisi!'), .typeError('Gudang tujuan wajib diisi!'),
product: Yup.array() product: Yup.array()
.of( .of(ProductObjectSchema)
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!'),
})
)
.min(1, 'Minimal harus ada 1 produk!'), .min(1, 'Minimal harus ada 1 produk!'),
ekspedisi: Yup.array() ekspedisi: Yup.array().of(EkspedisiObjectSchema).optional().default([]),
.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([]),
}); });
export const UpdateMovementFormSchema = MovementFormSchema; export const UpdateMovementFormSchema = MovementFormSchema;
@@ -1,7 +1,7 @@
'use client'; 'use client';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { FieldArray, FormikProvider, useFormik } from 'formik'; import { useFormik } from 'formik';
import useSWR from 'swr'; import useSWR from 'swr';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
@@ -21,6 +21,8 @@ import {
MovementFormValues, MovementFormValues,
UpdateMovementFormSchema, UpdateMovementFormSchema,
getMovementFormInitialValues, getMovementFormInitialValues,
ProductSchema,
EkspedisiSchema,
} from '@/components/pages/inventory/movement/form/MovementForm.schema'; } from '@/components/pages/inventory/movement/form/MovementForm.schema';
import { useMovementFormHandlers } from './useMovementFormHandlers'; import { useMovementFormHandlers } from './useMovementFormHandlers';
import { 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 = <T extends 'product' | 'ekspedisi'>(
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 // Warehouse selection
const [warehouseSelectInputValue, setWarehouseSelectInputValue] = const [warehouseSelectInputValue, setWarehouseSelectInputValue] =
useState(''); useState('');
@@ -139,448 +207,448 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
title='Movement' title='Movement'
backUrl='/inventory/movement' backUrl='/inventory/movement'
/> />
<FormikProvider value={formik}> <form
<form onSubmit={formik.handleSubmit}
onSubmit={formik.handleSubmit} onReset={formik.handleReset}
onReset={formik.handleReset} className='w-full mt-8 flex flex-col gap-6'
className='w-full mt-8 flex flex-col gap-6' >
> {/* Top card - Movement details */}
{/* Top card - Movement details */} <div className='card bg-base-100 shadow mb-4'>
<div className='card bg-base-100 shadow mb-4'> <div className='card-body'>
<div className='card-body'> <div className='flex gap-4'>
<div className='flex gap-4'> <TextInput
<TextInput required
required label='Alasan Transfer'
label='Alasan Transfer' name='alasan_transfer'
name='alasan_transfer' value={formik.values.alasan_transfer}
value={formik.values.alasan_transfer} onChange={formik.handleChange}
onChange={formik.handleChange} onBlur={formik.handleBlur}
onBlur={formik.handleBlur} isError={
isError={ formik.touched.alasan_transfer &&
formik.touched.alasan_transfer && Boolean(formik.errors.alasan_transfer)
Boolean(formik.errors.alasan_transfer) }
} errorMessage={formik.errors.alasan_transfer}
errorMessage={formik.errors.alasan_transfer} readOnly={type === 'detail'}
readOnly={type === 'detail'} />
/> <TextInput
<TextInput required
required label='Tanggal Transfer'
label='Tanggal Transfer' type='date'
type='date' name='tanggal_transfer'
name='tanggal_transfer' value={formik.values.tanggal_transfer}
value={formik.values.tanggal_transfer} onChange={formik.handleChange}
onChange={formik.handleChange} onBlur={formik.handleBlur}
onBlur={formik.handleBlur} isError={
isError={ formik.touched.tanggal_transfer &&
formik.touched.tanggal_transfer && Boolean(formik.errors.tanggal_transfer)
Boolean(formik.errors.tanggal_transfer) }
} errorMessage={formik.errors.tanggal_transfer}
errorMessage={formik.errors.tanggal_transfer} readOnly={type === 'detail'}
readOnly={type === 'detail'}
/>
</div>
</div>
</div>
{/* Warehouse cards */}
<div className='grid grid-cols-2 gap-4 mb-4'>
<div className='card bg-base-100 shadow'>
<div className='card-body'>
<SelectInput
required
label='Gudang Asal'
value={formik.values.warehouse_asal ?? undefined}
onChange={(val) => {
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
/>
</div>
</div>
<div className='card bg-base-100 shadow'>
<div className='card-body'>
<SelectInput
required
label='Gudang Tujuan'
value={formik.values.warehouse_tujuan ?? undefined}
onChange={(val) => {
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
/>
</div>
</div>
</div>
{/* Products table */}
<div className='card bg-base-100 shadow mb-4'>
<div className='card-body'>
<h2 className='card-title mb-4'>Produk</h2>
<FieldArray name='product'>
{({ push, remove }) => (
<>
<table className='table'>
<thead>
<tr>
<th>Produk</th>
<th>Qty</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
{formik.values.product?.map((_, index) => (
<tr key={index}>
<td>
<SelectInput
required
value={
formik.values.product?.[index]?.product ??
undefined
}
onChange={(val) => {
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
/>
</td>
<td>
<TextInput
required
name={`product.${index}.qty_product`}
type='number'
value={
formik.values.product?.[index]
?.qty_product ?? ''
}
onChange={(e) =>
formik.setFieldValue(
`product.${index}.qty_product`,
e.target.value
)
}
readOnly={type === 'detail'}
/>
</td>
<td>
{type !== 'detail' && (
<Button
type='button'
color='error'
onClick={() => remove(index)}
>
<Icon icon='material-symbols:delete-outline' />
</Button>
)}
</td>
</tr>
)) ?? []}
</tbody>
</table>
{type !== 'detail' && (
<Button
type='button'
color='primary'
onClick={() =>
push({
product: null,
product_id: 0,
qty_product: 0,
})
}
className='mt-4'
>
<Icon icon='ic:round-plus' />
Tambah Produk
</Button>
)}
</>
)}
</FieldArray>
</div>
</div>
{/* Ekspedisi table */}
<div className='card bg-base-100 shadow mb-4'>
<div className='card-body'>
<h2 className='card-title mb-4'>Ekspedisi</h2>
<FieldArray name='ekspedisi'>
{({ push, remove }) => (
<>
<table className='table'>
<thead>
<tr>
<th>Produk</th>
<th>Qty</th>
<th>Supplier</th>
<th>Plat Nomor</th>
<th>No Surat Jalan</th>
<th>Dokumen</th>
<th>Biaya Ekspedisi</th>
<th>Nama Sopir</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
{formik.values.ekspedisi?.map((ekspedisi, index) => (
<tr key={index}>
<td>
<SelectInput
required
value={
formik.values.ekspedisi?.[index]?.product ??
undefined
}
onChange={(val) => {
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
/>
</td>
<td>
<TextInput
name={`ekspedisi.${index}.qty`}
required
type='number'
value={
formik.values.ekspedisi?.[index]?.qty ?? ''
}
onChange={(e) =>
formik.setFieldValue(
`ekspedisi.${index}.qty`,
e.target.value
)
}
readOnly={type === 'detail'}
/>
</td>
<td>
<SelectInput
required
value={
formik.values.ekspedisi?.[index]
?.supplier ?? undefined
}
onChange={(val) => {
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
/>
</td>
<td>
<TextInput
name={`ekspedisi.${index}.plat_nomor`}
required
value={
formik.values.ekspedisi?.[index]
?.plat_nomor ?? ''
}
onChange={(e) =>
formik.setFieldValue(
`ekspedisi.${index}.plat_nomor`,
e.target.value
)
}
readOnly={type === 'detail'}
/>
</td>
<td>
<TextInput
name={`ekspedisi.${index}.no_surat_jalan`}
required
value={
formik.values.ekspedisi?.[index]
?.no_surat_jalan ?? ''
}
onChange={(e) =>
formik.setFieldValue(
`ekspedisi.${index}.no_surat_jalan`,
e.target.value
)
}
readOnly={type === 'detail'}
/>
</td>
<td>
<TextInput
name={`ekspedisi.${index}.dokumen`}
required
type='file'
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
formik.setFieldValue(
`ekspedisi.${index}.dokumen`,
file
);
}
}}
readOnly={type === 'detail'}
/>
</td>
<td>
<TextInput
name={`ekspedisi.${index}.biaya_ekspedisi`}
required
type='number'
value={
formik.values.ekspedisi?.[index]
?.biaya_ekspedisi ?? ''
}
onChange={(e) =>
formik.setFieldValue(
`ekspedisi.${index}.biaya_ekspedisi`,
e.target.value
)
}
readOnly={type === 'detail'}
/>
</td>
<td>
<TextInput
name={`ekspedisi.${index}.nama_sopir`}
required
value={
formik.values.ekspedisi?.[index]
?.nama_sopir ?? ''
}
onChange={(e) =>
formik.setFieldValue(
`ekspedisi.${index}.nama_sopir`,
e.target.value
)
}
readOnly={type === 'detail'}
/>
</td>
<td>
{type !== 'detail' && (
<Button
type='button'
color='error'
onClick={() => remove(index)}
>
<Icon icon='material-symbols:delete-outline' />
</Button>
)}
</td>
</tr>
)) ?? []}
</tbody>
</table>
{type !== 'detail' && (
<Button
type='button'
color='primary'
onClick={() =>
push({
product: null,
product_id: 0,
qty: 0,
supplier: null,
supplier_id: 0,
plat_nomor: '',
no_surat_jalan: '',
dokumen: '',
biaya_ekspedisi: 0,
nama_sopir: '',
})
}
className='mt-4'
>
<Icon icon='ic:round-plus' />
Tambah Ekspedisi
</Button>
)}
</>
)}
</FieldArray>
</div>
</div>
{/* Action buttons */}
<FormActions<MovementFormValues>
type={type}
formik={formik}
editUrl={
initialValues
? `/inventory/movement/detail/edit/?movementId=${initialValues.id}`
: undefined
}
onDelete={deleteMovementClickHandler}
/>
{movementFormErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/> />
<span>{movementFormErrorMessage}</span>
</div> </div>
)} </div>
</form> </div>
</FormikProvider>
{/* Warehouse cards */}
<div className='grid grid-cols-2 gap-4 mb-4'>
<div className='card bg-base-100 shadow'>
<div className='card-body'>
<SelectInput
required
label='Gudang Asal'
value={formik.values.warehouse_asal ?? undefined}
onChange={(val) => {
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
/>
</div>
</div>
<div className='card bg-base-100 shadow'>
<div className='card-body'>
<SelectInput
required
label='Gudang Tujuan'
value={formik.values.warehouse_tujuan ?? undefined}
onChange={(val) => {
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
/>
</div>
</div>
</div>
{/* Products table */}
<div className='card bg-base-100 shadow mb-4'>
<div className='card-body'>
<h2 className='card-title mb-4'>Produk</h2>
<div className='overflow-x-auto'>
<table className='table'>
<thead>
<tr>
<th>Produk</th>
<th>Qty</th>
{type !== 'detail' && <th>Aksi</th>}
</tr>
</thead>
<tbody>
{formik.values.product?.map((product, idx) => (
<tr key={idx}>
<td>
<SelectInput
required
value={product.product ?? undefined}
onChange={(val) => {
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
)}
/>
</td>
<td>
<TextInput
required
type='number'
name={`product.${idx}.qty_product`}
value={product.qty_product ?? ''}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isRepeaterInputError(
'product',
'qty_product',
idx
)}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-24',
}}
/>
</td>
{type !== 'detail' && (
<td>
<Button
type='button'
color='error'
onClick={() => removeProduct(idx)}
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
/>
</Button>
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
{type !== 'detail' && (
<Button
type='button'
color='success'
onClick={addProduct}
className='w-fit mx-auto mt-4'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah Produk
</Button>
)}
</div>
</div>
{/* Ekspedisi table */}
<div className='card bg-base-100 shadow mb-4'>
<div className='card-body'>
<h2 className='card-title mb-4'>Ekspedisi</h2>
<div className='overflow-x-auto'>
<table className='table'>
<thead>
<tr>
<th>Produk</th>
<th>Qty</th>
<th>Supplier</th>
<th>Plat Nomor</th>
<th>No Surat Jalan</th>
<th>Dokumen</th>
<th>Biaya Ekspedisi</th>
<th>Nama Sopir</th>
{type !== 'detail' && <th>Aksi</th>}
</tr>
</thead>
<tbody>
{formik.values.ekspedisi?.map((ekspedisi, idx) => (
<tr key={idx}>
<td>
<SelectInput
required
value={ekspedisi.product ?? undefined}
onChange={(val) => {
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
)}
/>
</td>
<td>
<TextInput
required
type='number'
name={`ekspedisi.${idx}.qty`}
value={ekspedisi.qty ?? ''}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isRepeaterInputError(
'ekspedisi',
'qty',
idx
)}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-24',
}}
/>
</td>
<td>
<SelectInput
required
value={ekspedisi.supplier ?? undefined}
onChange={(val) => {
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
)}
/>
</td>
<td>
<TextInput
required
name={`ekspedisi.${idx}.plat_nomor`}
value={ekspedisi.plat_nomor ?? ''}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isRepeaterInputError(
'ekspedisi',
'plat_nomor',
idx
)}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-24',
}}
/>
</td>
<td>
<TextInput
required
name={`ekspedisi.${idx}.no_surat_jalan`}
value={ekspedisi.no_surat_jalan ?? ''}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isRepeaterInputError(
'ekspedisi',
'no_surat_jalan',
idx
)}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-24',
}}
/>
</td>
<td>
<TextInput
required
type='file'
name={`ekspedisi.${idx}.dokumen`}
onChange={(e) => {
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',
}}
/>
</td>
<td>
<TextInput
required
type='number'
name={`ekspedisi.${idx}.biaya_ekspedisi`}
value={ekspedisi.biaya_ekspedisi ?? ''}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isRepeaterInputError(
'ekspedisi',
'biaya_ekspedisi',
idx
)}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-24',
}}
/>
</td>
<td>
<TextInput
required
name={`ekspedisi.${idx}.nama_sopir`}
value={ekspedisi.nama_sopir ?? ''}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isRepeaterInputError(
'ekspedisi',
'nama_sopir',
idx
)}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-24',
}}
/>
</td>
{type !== 'detail' && (
<td>
<Button
type='button'
color='error'
onClick={() => removeEkspedisi(idx)}
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
/>
</Button>
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
{type !== 'detail' && (
<Button
type='button'
color='success'
onClick={addEkspedisi}
className='w-fit mx-auto mt-4'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah Ekspedisi
</Button>
)}
</div>
</div>
{/* Action buttons */}
<FormActions<MovementFormValues>
type={type}
formik={formik}
editUrl={
initialValues
? `/inventory/movement/detail/edit/?movementId=${initialValues.id}`
: undefined
}
onDelete={deleteMovementClickHandler}
/>
{movementFormErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{movementFormErrorMessage}</span>
</div>
)}
</form>
</section> </section>
{type !== 'add' && ( {type !== 'add' && (