feat(FE-62,63,65): implement MovementForm component for managing inventory movements

This commit is contained in:
rstubryan
2025-10-09 14:30:05 +07:00
parent 558a1788dc
commit 1ea9ee3069
4 changed files with 866 additions and 103 deletions
+11
View File
@@ -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
View File
@@ -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;