mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE-62,63,65): implement MovementForm component for managing inventory movements
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
import MovementForm from '@/components/pages/inventory/movement/form/MovementForm';
|
||||||
|
|
||||||
|
const AddMovement = () => {
|
||||||
|
return (
|
||||||
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
|
<MovementForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddMovement;
|
||||||
@@ -1,67 +1,68 @@
|
|||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export const MovementFormSchema = Yup.object({
|
export const MovementFormSchema = Yup.object({
|
||||||
alasan_transfer: Yup.string()
|
alasan_transfer: Yup.string().required('Alasan transfer wajib diisi!'),
|
||||||
.required('Alasan Transfer wajib diisi!'),
|
tanggal_transfer: Yup.string().required('Tanggal transfer wajib diisi!'),
|
||||||
tanggal_transfer: Yup.date()
|
warehouse_asal: Yup.object({
|
||||||
.required('Tanggal Transfer wajib diisi!')
|
value: Yup.number().min(1).required(),
|
||||||
.typeError('Tanggal Transfer tidak valid!'),
|
label: Yup.string().required(),
|
||||||
warehouse_asal: Yup.object({
|
}).nullable(),
|
||||||
value: Yup.number().min(1).required(),
|
warehouse_asal_id: Yup.number()
|
||||||
label: Yup.string().required(),
|
.required('Gudang asal wajib diisi!')
|
||||||
}).nullable(),
|
.typeError('Gudang asal wajib diisi!'),
|
||||||
warehouse_asal_id: Yup.number()
|
warehouse_tujuan: Yup.object({
|
||||||
.required('Gudang Asal wajib diisi!'),
|
value: Yup.number().min(1).required(),
|
||||||
warehouse_tujuan: Yup.object({
|
label: Yup.string().required(),
|
||||||
value: Yup.number().min(1).required(),
|
}).nullable(),
|
||||||
label: Yup.string().required(),
|
warehouse_tujuan_id: Yup.number()
|
||||||
}).nullable(),
|
.required('Gudang tujuan wajib diisi!')
|
||||||
warehouse_tujuan_id: Yup.number()
|
.typeError('Gudang tujuan wajib diisi!'),
|
||||||
.required('Gudang Tujuan wajib diisi!'),
|
product: Yup.array()
|
||||||
alasan: Yup.string()
|
.of(
|
||||||
.required('Alasan wajib diisi!'),
|
Yup.object({
|
||||||
product: Yup.object({
|
product: Yup.object({
|
||||||
value: Yup.number().min(1).required(),
|
value: Yup.number().min(1).required(),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
}).nullable(),
|
}).nullable(),
|
||||||
product_id: Yup.array()
|
product_id: Yup.number().required('Produk wajib diisi!'),
|
||||||
.of(Yup.number()).min(1, 'Pilih minimal 1 produk')
|
qty_product: Yup.number()
|
||||||
.required('Produk wajib diisi!'),
|
.required('Qty wajib diisi!')
|
||||||
qty_product: Yup.array()
|
.min(1, 'Qty minimal 1!')
|
||||||
.of(Yup.number().min(1, 'Kuantitas minimal 1'))
|
.typeError('Qty harus berupa angka!'),
|
||||||
.min(1, 'Pilih minimal 1 produk')
|
})
|
||||||
.required('Kuantitas wajib diisi!'),
|
)
|
||||||
ekspedisi: Yup.array().of(
|
.min(1, 'Minimal harus ada 1 produk!'),
|
||||||
Yup.object({
|
ekspedisi: Yup.array()
|
||||||
product: Yup.object({
|
.of(
|
||||||
value: Yup.number().min(1).required(),
|
Yup.object({
|
||||||
label: Yup.string().required(),
|
product: Yup.object({
|
||||||
}).nullable(),
|
value: Yup.number().min(1).required(),
|
||||||
product_id: Yup.number()
|
label: Yup.string().required(),
|
||||||
.required('Produk wajib diisi!'),
|
}).nullable(),
|
||||||
qty: Yup.number().min(1, 'Kuantitas minimal 1')
|
product_id: Yup.number().required('Produk wajib diisi!'),
|
||||||
.required('Kuantitas wajib diisi!'),
|
qty: Yup.number()
|
||||||
supplier: Yup.object({
|
.required('Qty wajib diisi!')
|
||||||
value: Yup.number().min(1).required(),
|
.min(1, 'Qty minimal 1!')
|
||||||
label: Yup.string().required(),
|
.typeError('Qty harus berupa angka!'),
|
||||||
}).nullable(),
|
supplier: Yup.object({
|
||||||
supplier_id: Yup.number()
|
value: Yup.number().min(1).required(),
|
||||||
.required('Supplier wajib diisi!'),
|
label: Yup.string().required(),
|
||||||
plat_nomor: Yup.string()
|
}).nullable(),
|
||||||
.required('Plat Nomor wajib diisi!'),
|
supplier_id: Yup.number().required('Supplier wajib diisi!'),
|
||||||
no_surat_jalan: Yup.string()
|
plat_nomor: Yup.string().required('Plat nomor wajib diisi!'),
|
||||||
.required('No Surat Jalan wajib diisi!'),
|
no_surat_jalan: Yup.string().required('No surat jalan wajib diisi!'),
|
||||||
dokumen: Yup.mixed()
|
dokumen: Yup.mixed().required('Dokumen wajib diisi!'),
|
||||||
.required('Dokumen wajib diisi!'),
|
biaya_ekspedisi: Yup.number()
|
||||||
biaya_ekspedisi: Yup.number()
|
.required('Biaya ekspedisi wajib diisi!')
|
||||||
.min(0, 'Biaya Ekspedisi minimal 0')
|
.min(0, 'Biaya minimal 0!')
|
||||||
.required('Biaya Ekspedisi wajib diisi!'),
|
.typeError('Biaya harus berupa angka!'),
|
||||||
nama_sopir: Yup.string()
|
nama_sopir: Yup.string().required('Nama sopir wajib diisi!'),
|
||||||
.required('Nama Sopir wajib diisi!'),
|
})
|
||||||
})
|
)
|
||||||
).min(1, 'Pilih minimal 1 ekspedisi').required('Ekspedisi wajib diisi!'),
|
.optional()
|
||||||
|
.default([]),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateMovementFormSchema = MovementFormSchema;
|
export const UpdateMovementFormSchema = MovementFormSchema;
|
||||||
|
|
||||||
export type MovementFormValues = Yup.InferType<typeof MovementFormSchema>;
|
export type MovementFormValues = Yup.InferType<typeof MovementFormSchema>;
|
||||||
|
|||||||
@@ -0,0 +1,751 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { FieldArray, FormikProvider, useFormik } from 'formik';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import TextInput from '@/components/input/TextInput';
|
||||||
|
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import { useModal } from '@/components/Modal';
|
||||||
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
|
||||||
|
import {
|
||||||
|
MovementFormSchema,
|
||||||
|
MovementFormValues,
|
||||||
|
UpdateMovementFormSchema,
|
||||||
|
} from '@/components/pages/inventory/movement/form/MovementForm.schema';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import {
|
||||||
|
Movement,
|
||||||
|
CreateMovementPayload,
|
||||||
|
UpdateMovementPayload,
|
||||||
|
} from '@/types/api/inventory/movement';
|
||||||
|
import {
|
||||||
|
ProductApi,
|
||||||
|
WarehouseApi,
|
||||||
|
SupplierApi,
|
||||||
|
} from '@/services/api/master-data';
|
||||||
|
import { MovementApi } from '@/services/api/inventory';
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
|
||||||
|
interface MovementFormProps {
|
||||||
|
type?: 'add' | 'edit' | 'detail';
|
||||||
|
initialValues?: Movement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const deleteModal = useModal();
|
||||||
|
|
||||||
|
const [movementFormErrorMessage, setMovementFormErrorMessage] = useState('');
|
||||||
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
|
const createMovementHandler = useCallback(
|
||||||
|
async (payload: CreateMovementPayload) => {
|
||||||
|
const res = await MovementApi.create(payload);
|
||||||
|
if (isResponseError(res)) {
|
||||||
|
setMovementFormErrorMessage(res.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.success(res?.message as string);
|
||||||
|
router.push('/inventory/movement');
|
||||||
|
},
|
||||||
|
[router]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateMovementHandler = useCallback(
|
||||||
|
async (movementId: number, payload: UpdateMovementPayload) => {
|
||||||
|
const res = await MovementApi.update(movementId, payload);
|
||||||
|
if (res?.status === 'error') {
|
||||||
|
setMovementFormErrorMessage(res.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.success(res?.message as string);
|
||||||
|
router.refresh();
|
||||||
|
router.push('/inventory/movement');
|
||||||
|
},
|
||||||
|
[router]
|
||||||
|
);
|
||||||
|
|
||||||
|
const formikInitialValues = useMemo<MovementFormValues>(
|
||||||
|
() => ({
|
||||||
|
alasan_transfer: initialValues?.alasan_transfer ?? '',
|
||||||
|
tanggal_transfer: initialValues?.tanggal_transfer ?? '',
|
||||||
|
warehouse_asal: initialValues?.warehouse_asal
|
||||||
|
? {
|
||||||
|
value: initialValues.warehouse_asal.id,
|
||||||
|
label: initialValues.warehouse_asal.name,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
warehouse_asal_id: initialValues?.warehouse_asal?.id ?? 0,
|
||||||
|
warehouse_tujuan: initialValues?.warehouse_tujuan
|
||||||
|
? {
|
||||||
|
value: initialValues.warehouse_tujuan.id,
|
||||||
|
label: initialValues.warehouse_tujuan.name,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
warehouse_tujuan_id: initialValues?.warehouse_tujuan?.id ?? 0,
|
||||||
|
product:
|
||||||
|
initialValues?.product?.map((p) => ({
|
||||||
|
product: { value: p.product.id, label: p.product.name },
|
||||||
|
product_id: p.product.id,
|
||||||
|
qty_product: p.qty_product,
|
||||||
|
})) ?? [],
|
||||||
|
ekspedisi:
|
||||||
|
initialValues?.ekspedisi?.map((e) => ({
|
||||||
|
product: { value: e.product_id, label: '' }, // Need to fetch product details
|
||||||
|
product_id: e.product_id,
|
||||||
|
qty: e.qty,
|
||||||
|
supplier: { value: e.supplier.id, label: e.supplier.name },
|
||||||
|
supplier_id: e.supplier.id,
|
||||||
|
plat_nomor: e.plat_nomor,
|
||||||
|
no_surat_jalan: e.no_surat_jalan,
|
||||||
|
dokumen: e.dokumen,
|
||||||
|
biaya_ekspedisi: e.biaya_ekspedisi,
|
||||||
|
nama_sopir: e.nama_sopir,
|
||||||
|
})) ?? [],
|
||||||
|
}),
|
||||||
|
[initialValues]
|
||||||
|
);
|
||||||
|
|
||||||
|
const formik = useFormik<MovementFormValues>({
|
||||||
|
initialValues: formikInitialValues,
|
||||||
|
validationSchema:
|
||||||
|
type === 'edit' ? UpdateMovementFormSchema : MovementFormSchema,
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
setMovementFormErrorMessage('');
|
||||||
|
const payload: CreateMovementPayload = {
|
||||||
|
alasan_transfer: values.alasan_transfer,
|
||||||
|
tanggal_transfer: values.tanggal_transfer,
|
||||||
|
warehouse_asal_id: values.warehouse_asal_id,
|
||||||
|
warehouse_tujuan_id: values.warehouse_tujuan_id,
|
||||||
|
product: (values.product ?? []).map((p) => ({
|
||||||
|
product_id: p.product_id,
|
||||||
|
qty_product: p.qty_product,
|
||||||
|
})),
|
||||||
|
ekspedisi: (values.ekspedisi ?? []).map((e) => ({
|
||||||
|
product_id: e.product_id,
|
||||||
|
qty: e.qty,
|
||||||
|
supplier_id: e.supplier_id,
|
||||||
|
plat_nomor: e.plat_nomor,
|
||||||
|
no_surat_jalan: e.no_surat_jalan,
|
||||||
|
dokumen:
|
||||||
|
e.dokumen instanceof File ? e.dokumen : (e.dokumen as string),
|
||||||
|
biaya_ekspedisi: e.biaya_ekspedisi,
|
||||||
|
nama_sopir: e.nama_sopir,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'add':
|
||||||
|
await createMovementHandler(payload);
|
||||||
|
break;
|
||||||
|
case 'edit':
|
||||||
|
await updateMovementHandler(initialValues?.id as number, payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Warehouse selection
|
||||||
|
const [warehouseSelectInputValue, setWarehouseSelectInputValue] =
|
||||||
|
useState('');
|
||||||
|
const warehousesUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ search: warehouseSelectInputValue }).toString()}`;
|
||||||
|
const { data: warehouses, isLoading: isLoadingWarehouses } = useSWR(
|
||||||
|
warehousesUrl,
|
||||||
|
WarehouseApi.getAllFetcher
|
||||||
|
);
|
||||||
|
const warehouseOptions = isResponseSuccess(warehouses)
|
||||||
|
? warehouses?.data.map((w) => ({ value: w.id, label: w.name }))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Product selection
|
||||||
|
const [productSelectInputValue, setProductSelectInputValue] = useState('');
|
||||||
|
const productsUrl = `${ProductApi.basePath}?${new URLSearchParams({ search: productSelectInputValue }).toString()}`;
|
||||||
|
const { data: products, isLoading: isLoadingProducts } = useSWR(
|
||||||
|
productsUrl,
|
||||||
|
ProductApi.getAllFetcher
|
||||||
|
);
|
||||||
|
const productOptions = isResponseSuccess(products)
|
||||||
|
? products?.data.map((p) => ({ value: p.id, label: p.name }))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Supplier selection
|
||||||
|
const [supplierSelectInputValue, setSupplierSelectInputValue] = useState('');
|
||||||
|
const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ search: supplierSelectInputValue }).toString()}`;
|
||||||
|
const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(
|
||||||
|
suppliersUrl,
|
||||||
|
SupplierApi.getAllFetcher
|
||||||
|
);
|
||||||
|
const supplierOptions = isResponseSuccess(suppliers)
|
||||||
|
? suppliers?.data.map((s) => ({ value: s.id, label: s.name }))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const deleteMovementClickHandler = () => {
|
||||||
|
deleteModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
|
setIsDeleteLoading(true);
|
||||||
|
await MovementApi.delete(initialValues?.id as number);
|
||||||
|
deleteModal.closeModal();
|
||||||
|
toast.success('Successfully delete Movement!');
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
router.push('/inventory/movement');
|
||||||
|
};
|
||||||
|
|
||||||
|
const { setValues: formikSetValues } = formik;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
formikSetValues(formikInitialValues);
|
||||||
|
}, [formikSetValues, formikInitialValues]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className='w-full max-w-xl'>
|
||||||
|
<header className='flex flex-col gap-4'>
|
||||||
|
<Button
|
||||||
|
href='/inventory/movement'
|
||||||
|
variant='link'
|
||||||
|
className='w-fit p-0 text-primary'
|
||||||
|
>
|
||||||
|
<Icon icon='uil:arrow-left' width={24} height={24} />
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
<h1 className='text-2xl font-bold text-center'>
|
||||||
|
{type === 'add' && 'Tambah Movement'}
|
||||||
|
{type === 'edit' && 'Edit Movement'}
|
||||||
|
{type === 'detail' && 'Detail Movement'}
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<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 }) => (
|
||||||
|
<>
|
||||||
|
{typeof formik.errors.product === 'string' && (
|
||||||
|
<div className='text-error'>
|
||||||
|
{formik.errors.product}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<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 }) => (
|
||||||
|
<>
|
||||||
|
{typeof formik.errors.ekspedisi === 'string' && (
|
||||||
|
<div className='text-error'>
|
||||||
|
{formik.errors.ekspedisi}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<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 */}
|
||||||
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
|
{type !== 'add' && (
|
||||||
|
<div className='flex flex-row justify-start gap-2'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='error'
|
||||||
|
onClick={deleteMovementClickHandler}
|
||||||
|
className='px-4'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:delete-outline-rounded'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
/>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
{type !== 'edit' && (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='warning'
|
||||||
|
href={`/inventory/movement/detail/edit/?movementId=${initialValues?.id}`}
|
||||||
|
className='px-4'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:edit-outline'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
/>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<div
|
||||||
|
className={cn('flex flex-row justify-end gap-2', {
|
||||||
|
'w-full': type === 'add',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type='reset'
|
||||||
|
color='warning'
|
||||||
|
className='px-4'
|
||||||
|
onClick={formik.handleReset}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
color='primary'
|
||||||
|
className='px-4'
|
||||||
|
isLoading={formik.isSubmitting}
|
||||||
|
disabled={!formik.isValid || formik.isSubmitting}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{movementFormErrorMessage && (
|
||||||
|
<div role='alert' className='alert alert-error'>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:error-outline'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
<span>{movementFormErrorMessage}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</FormikProvider>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{type !== 'add' && (
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={deleteModal.ref}
|
||||||
|
type='error'
|
||||||
|
text={`Apakah anda yakin ingin menghapus data Movement ini?`}
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'error',
|
||||||
|
isLoading: isDeleteLoading,
|
||||||
|
onClick: confirmationModalDeleteClickHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovementForm;
|
||||||
+43
-43
@@ -1,51 +1,51 @@
|
|||||||
import {BaseMetadata} from '@/types/api/api-general';
|
import { BaseMetadata } from '@/types/api/api-general';
|
||||||
import {Product} from "@/types/api/master-data/product";
|
import { Product } from '@/types/api/master-data/product';
|
||||||
import {Supplier} from "@/types/api/master-data/supplier";
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
import {Warehouse} from "@/types/api/master-data/warehouse";
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
|
|
||||||
export type BaseMovement = {
|
export type BaseMovement = {
|
||||||
id: number;
|
id: number;
|
||||||
alasan_transfer: string;
|
alasan_transfer: string;
|
||||||
tanggal_transfer: string;
|
tanggal_transfer: string;
|
||||||
warehouse_asal: Warehouse;
|
warehouse_asal: Warehouse;
|
||||||
warehouse_tujuan: Warehouse;
|
warehouse_tujuan: Warehouse;
|
||||||
product: Array<{
|
product: {
|
||||||
product: Product;
|
product: Product;
|
||||||
qty_product: number;
|
qty_product: number;
|
||||||
}>;
|
}[];
|
||||||
ekspedisi: Array<{
|
ekspedisi: {
|
||||||
product_id: number;
|
product_id: number;
|
||||||
qty: number;
|
qty: number;
|
||||||
supplier: Supplier;
|
supplier: Supplier;
|
||||||
plat_nomor: string;
|
plat_nomor: string;
|
||||||
no_surat_jalan: string;
|
no_surat_jalan: string;
|
||||||
dokumen: string;
|
dokumen: string;
|
||||||
biaya_ekspedisi: number;
|
biaya_ekspedisi: number;
|
||||||
nama_sopir: string;
|
nama_sopir: string;
|
||||||
}>;
|
}[];
|
||||||
name: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Movement = BaseMetadata & BaseMovement;
|
export type Movement = BaseMetadata & BaseMovement;
|
||||||
|
|
||||||
export type CreateMovementPayload = {
|
export type CreateMovementPayload = {
|
||||||
alasan: string;
|
alasan_transfer: string;
|
||||||
warehouse_asal_id: number;
|
tanggal_transfer: string;
|
||||||
warehouse_tujuan_id: number;
|
warehouse_asal_id: number;
|
||||||
product: Array<{
|
warehouse_tujuan_id: number;
|
||||||
product_id: number;
|
product: {
|
||||||
qty_product: number;
|
product_id: number;
|
||||||
}>;
|
qty_product: number;
|
||||||
ekspedisi: Array<{
|
}[];
|
||||||
product_id: number;
|
ekspedisi: {
|
||||||
qty: number;
|
product_id: number;
|
||||||
supplier_id: number;
|
qty: number;
|
||||||
plat_nomor: string;
|
supplier_id: number;
|
||||||
no_surat_jalan: string;
|
plat_nomor: string;
|
||||||
dokumen: string;
|
no_surat_jalan: string;
|
||||||
biaya_ekspedisi: number;
|
dokumen: string | File;
|
||||||
nama_sopir: string;
|
biaya_ekspedisi: number;
|
||||||
}>;
|
nama_sopir: string;
|
||||||
}
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
export type UpdateMovementPayload = CreateMovementPayload;
|
export type UpdateMovementPayload = CreateMovementPayload;
|
||||||
|
|||||||
Reference in New Issue
Block a user