mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
refactor(FE-62,65): refactor MovementForm schema and component for improved product and ekspedisi handling
This commit is contained in:
@@ -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<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({
|
||||
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;
|
||||
|
||||
@@ -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 = <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
|
||||
const [warehouseSelectInputValue, setWarehouseSelectInputValue] =
|
||||
useState('');
|
||||
@@ -139,448 +207,448 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
title='Movement'
|
||||
backUrl='/inventory/movement'
|
||||
/>
|
||||
<FormikProvider value={formik}>
|
||||
<form
|
||||
onSubmit={formik.handleSubmit}
|
||||
onReset={formik.handleReset}
|
||||
className='w-full mt-8 flex flex-col gap-6'
|
||||
>
|
||||
{/* Top card - Movement details */}
|
||||
<div className='card bg-base-100 shadow mb-4'>
|
||||
<div className='card-body'>
|
||||
<div className='flex gap-4'>
|
||||
<TextInput
|
||||
required
|
||||
label='Alasan Transfer'
|
||||
name='alasan_transfer'
|
||||
value={formik.values.alasan_transfer}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={
|
||||
formik.touched.alasan_transfer &&
|
||||
Boolean(formik.errors.alasan_transfer)
|
||||
}
|
||||
errorMessage={formik.errors.alasan_transfer}
|
||||
readOnly={type === 'detail'}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label='Tanggal Transfer'
|
||||
type='date'
|
||||
name='tanggal_transfer'
|
||||
value={formik.values.tanggal_transfer}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={
|
||||
formik.touched.tanggal_transfer &&
|
||||
Boolean(formik.errors.tanggal_transfer)
|
||||
}
|
||||
errorMessage={formik.errors.tanggal_transfer}
|
||||
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}
|
||||
<form
|
||||
onSubmit={formik.handleSubmit}
|
||||
onReset={formik.handleReset}
|
||||
className='w-full mt-8 flex flex-col gap-6'
|
||||
>
|
||||
{/* Top card - Movement details */}
|
||||
<div className='card bg-base-100 shadow mb-4'>
|
||||
<div className='card-body'>
|
||||
<div className='flex gap-4'>
|
||||
<TextInput
|
||||
required
|
||||
label='Alasan Transfer'
|
||||
name='alasan_transfer'
|
||||
value={formik.values.alasan_transfer}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={
|
||||
formik.touched.alasan_transfer &&
|
||||
Boolean(formik.errors.alasan_transfer)
|
||||
}
|
||||
errorMessage={formik.errors.alasan_transfer}
|
||||
readOnly={type === 'detail'}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label='Tanggal Transfer'
|
||||
type='date'
|
||||
name='tanggal_transfer'
|
||||
value={formik.values.tanggal_transfer}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={
|
||||
formik.touched.tanggal_transfer &&
|
||||
Boolean(formik.errors.tanggal_transfer)
|
||||
}
|
||||
errorMessage={formik.errors.tanggal_transfer}
|
||||
readOnly={type === 'detail'}
|
||||
/>
|
||||
<span>{movementFormErrorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</FormikProvider>
|
||||
</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>
|
||||
<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>
|
||||
|
||||
{type !== 'add' && (
|
||||
|
||||
Reference in New Issue
Block a user