mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-23 14:55:44 +00:00
Merge branch 'dev/restu' of gitlab.com:mbugroup/lti-web-client into feat/FE/US-78/TASK-170-174-slicing-ui-and-validation-create-daily-recording-laying-form
This commit is contained in:
@@ -1,24 +1,56 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { ChangeEventHandler, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { SortingState } from '@tanstack/react-table';
|
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { useModal } from '@/components/Modal';
|
import { Icon } from '@iconify/react';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
|
||||||
import { Movement } from '@/types/api/inventory/movement';
|
import { Movement } from '@/types/api/inventory/movement';
|
||||||
import { MovementApi } from '@/services/api/inventory';
|
import { MovementApi } from '@/services/api/inventory';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
|
import { Product } from '@/types/api/master-data/product';
|
||||||
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
import { TableToolbar } from '@/components/table/TableToolbar';
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
|
import Button from '@/components/Button';
|
||||||
import { OptionType } from '@/components/input/SelectInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
|
import SelectInput from '@/components/input/SelectInput';
|
||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
import { TableRowOptions } from '@/components/table/TableRowOptions';
|
|
||||||
|
const RowOptionsMenu = ({
|
||||||
|
type = 'dropdown',
|
||||||
|
props,
|
||||||
|
}: {
|
||||||
|
type: 'dropdown' | 'collapse';
|
||||||
|
props: CellContext<Movement, unknown>;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
tabIndex={type === 'dropdown' ? 0 : undefined}
|
||||||
|
className={cn(
|
||||||
|
{
|
||||||
|
'dropdown-content': type === 'dropdown',
|
||||||
|
'mt-2': type === 'collapse',
|
||||||
|
},
|
||||||
|
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
href={`/inventory/movement/detail/?movementId=${props.row.original.id}`}
|
||||||
|
variant='ghost'
|
||||||
|
color='primary'
|
||||||
|
className='justify-start text-sm'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const MovementTable = () => {
|
const MovementTable = () => {
|
||||||
const {
|
const {
|
||||||
@@ -28,30 +60,47 @@ const MovementTable = () => {
|
|||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
} = useTableFilter({
|
} = useTableFilter({
|
||||||
initial: { search: '' },
|
initial: {
|
||||||
paramMap: { page: 'page', pageSize: 'limit' },
|
search: '',
|
||||||
|
product: '',
|
||||||
|
warehouse: '',
|
||||||
|
},
|
||||||
|
paramMap: {
|
||||||
|
page: 'page',
|
||||||
|
pageSize: 'limit',
|
||||||
|
product: 'product_id',
|
||||||
|
warehouse: 'warehouse_id',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [selectedMovement, setSelectedMovement] = useState<
|
|
||||||
Movement | undefined
|
|
||||||
>(undefined);
|
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
|
||||||
|
|
||||||
const deleteModal = useModal();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: movements,
|
setInputValue: setProductInputValue,
|
||||||
isLoading,
|
options: productOptions,
|
||||||
mutate: refreshMovements,
|
isLoadingOptions: isLoadingProductOptions,
|
||||||
} = useSWR(
|
} = useSelect<Product>('/products', 'id', 'name');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setWarehouseInputValue,
|
||||||
|
options: warehouseOptions,
|
||||||
|
isLoadingOptions: isLoadingWarehouseOptions,
|
||||||
|
} = useSelect<Warehouse>('/warehouses', 'id', 'name');
|
||||||
|
|
||||||
|
const [selectedProduct, setSelectedProduct] = useState<OptionType | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [selectedWarehouse, setSelectedWarehouse] = useState<OptionType | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: movements, isLoading } = useSWR(
|
||||||
`${MovementApi.basePath}${getTableFilterQueryString()}`,
|
`${MovementApi.basePath}${getTableFilterQueryString()}`,
|
||||||
MovementApi.getAllFetcher
|
MovementApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
const searchChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
updateFilter('search', e.target.value);
|
updateFilter('search', e.target.value);
|
||||||
setPage(1);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
@@ -60,167 +109,178 @@ const MovementTable = () => {
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const productChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
setIsDeleteLoading(true);
|
setSelectedProduct(val as OptionType);
|
||||||
try {
|
updateFilter('product', val ? ((val as OptionType).value as string) : '');
|
||||||
await MovementApi.delete(selectedMovement?.id as number);
|
|
||||||
refreshMovements();
|
|
||||||
deleteModal.closeModal();
|
|
||||||
} finally {
|
|
||||||
setIsDeleteLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
setSelectedWarehouse(val as OptionType);
|
||||||
|
updateFilter('warehouse', val ? ((val as OptionType).value as string) : '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const movementColumns: ColumnDef<Movement>[] = [
|
||||||
|
{
|
||||||
|
header: '#',
|
||||||
|
cell: (props) =>
|
||||||
|
tableFilterState.pageSize * (tableFilterState.page - 1) +
|
||||||
|
props.row.index +
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorFn: (row) => row.source_warehouse?.name,
|
||||||
|
header: 'Gudang Asal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorFn: (row) => row.destination_warehouse?.name,
|
||||||
|
header: 'Gudang Tujuan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'transfer_reason',
|
||||||
|
header: 'Catatan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'transfer_date',
|
||||||
|
header: 'Tanggal',
|
||||||
|
cell: (props) =>
|
||||||
|
new Date(props.row.original.transfer_date).toLocaleDateString('id-ID'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorFn: (row) => {
|
||||||
|
const totalCost = row.deliveries?.reduce(
|
||||||
|
(sum, d) => sum + (d.shipping_cost_total || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
return totalCost?.toLocaleString('id-ID');
|
||||||
|
},
|
||||||
|
header: 'Biaya Pengiriman',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Aksi',
|
||||||
|
cell: (props) => {
|
||||||
|
const currentPageSize = props.table.getPaginationRowModel().rows.length;
|
||||||
|
const currentPageRows = props.table.getPaginationRowModel().flatRows;
|
||||||
|
const currentRowRelativeIndex =
|
||||||
|
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
||||||
|
|
||||||
|
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{currentPageSize > 2 && (
|
||||||
|
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
||||||
|
<RowOptionsMenu type='dropdown' props={props} />
|
||||||
|
</RowDropdownOptions>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentPageSize <= 2 && (
|
||||||
|
<RowCollapseOptions>
|
||||||
|
<RowOptionsMenu type='dropdown' props={props} />
|
||||||
|
</RowCollapseOptions>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-4'>
|
<>
|
||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='w-full p-0 sm:p-4'>
|
||||||
<TableToolbar
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
addButton={{
|
<div className='w-full flex flex-col xl:flex-row justify-between items-end xl:items-center gap-2'>
|
||||||
href: '/inventory/movement/add',
|
<div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'>
|
||||||
label: 'Tambah Movement',
|
<Button
|
||||||
|
href='/inventory/movement/add'
|
||||||
|
color='primary'
|
||||||
|
className='w-full sm:w-fit'
|
||||||
|
>
|
||||||
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
|
Tambah Movement
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DebouncedTextInput
|
||||||
|
name='search'
|
||||||
|
placeholder='Cari Movement'
|
||||||
|
value={tableFilterState.search}
|
||||||
|
onChange={searchChangeHandler}
|
||||||
|
className={{ wrapper: 'sm:max-w-3xs' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-12 justify-end gap-4'>
|
||||||
|
<SelectInput
|
||||||
|
label='Produk'
|
||||||
|
options={productOptions}
|
||||||
|
isLoading={isLoadingProductOptions}
|
||||||
|
value={selectedProduct}
|
||||||
|
onChange={productChangeHandler}
|
||||||
|
onInputChange={setProductInputValue}
|
||||||
|
isClearable
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-4',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Gudang'
|
||||||
|
options={warehouseOptions}
|
||||||
|
isLoading={isLoadingWarehouseOptions}
|
||||||
|
value={selectedWarehouse}
|
||||||
|
onChange={warehouseChangeHandler}
|
||||||
|
onInputChange={setWarehouseInputValue}
|
||||||
|
isClearable
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-4',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Baris'
|
||||||
|
options={ROWS_OPTIONS}
|
||||||
|
value={{
|
||||||
|
label: String(tableFilterState.pageSize),
|
||||||
|
value: tableFilterState.pageSize,
|
||||||
|
}}
|
||||||
|
onChange={pageSizeChangeHandler}
|
||||||
|
className={{
|
||||||
|
wrapper:
|
||||||
|
'col-span-6 sm:col-span-4 max-w-28 sm:justify-self-end',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table<Movement>
|
||||||
|
data={isResponseSuccess(movements) ? movements?.data : []}
|
||||||
|
columns={movementColumns}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
page={isResponseSuccess(movements) ? movements?.meta?.page : 0}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(movements) ? movements?.meta?.total_results : 0
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
isLoading={isLoading}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={setSorting}
|
||||||
|
className={{
|
||||||
|
containerClassName: cn({
|
||||||
|
'mb-20':
|
||||||
|
isResponseSuccess(movements) && movements?.data?.length === 0,
|
||||||
|
}),
|
||||||
|
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||||
|
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||||
|
headerRowClassName: 'border-b border-b-gray-200',
|
||||||
|
headerColumnClassName:
|
||||||
|
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
||||||
|
bodyRowClassName: 'border-b border-b-gray-200',
|
||||||
|
bodyColumnClassName:
|
||||||
|
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
||||||
}}
|
}}
|
||||||
search={{
|
|
||||||
value: tableFilterState.search,
|
|
||||||
onChange: searchChangeHandler,
|
|
||||||
placeholder: 'Cari Movement',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TableRowSizeSelector
|
|
||||||
value={tableFilterState.pageSize}
|
|
||||||
onChange={pageSizeChangeHandler}
|
|
||||||
options={ROWS_OPTIONS}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
<Table<Movement>
|
|
||||||
data={isResponseSuccess(movements) ? movements?.data : []}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
header: '#',
|
|
||||||
cell: (props) =>
|
|
||||||
tableFilterState.pageSize * (tableFilterState.page - 1) +
|
|
||||||
props.row.index +
|
|
||||||
1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorFn: (row) => row.source_warehouse?.name,
|
|
||||||
header: 'Gudang Asal',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorFn: (row) => row.destination_warehouse?.name,
|
|
||||||
header: 'Gudang Tujuan',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'transfer_reason',
|
|
||||||
header: 'Catatan',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'transfer_date',
|
|
||||||
header: 'Tanggal',
|
|
||||||
cell: (props) =>
|
|
||||||
new Date(props.row.original.transfer_date).toLocaleDateString(
|
|
||||||
'id-ID'
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorFn: (row) => {
|
|
||||||
const totalCost = row.deliveries?.reduce(
|
|
||||||
(sum, d) => sum + (d.shipping_cost_total || 0),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
return totalCost?.toLocaleString('id-ID');
|
|
||||||
},
|
|
||||||
header: 'Biaya Pengiriman',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Aksi',
|
|
||||||
cell: (props) => {
|
|
||||||
const currentPageSize =
|
|
||||||
props.table.getPaginationRowModel().rows.length;
|
|
||||||
const currentPageRows =
|
|
||||||
props.table.getPaginationRowModel().flatRows;
|
|
||||||
const currentRowRelativeIndex =
|
|
||||||
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
|
||||||
|
|
||||||
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
|
||||||
|
|
||||||
const deleteClickHandler = () => {
|
|
||||||
setSelectedMovement(props.row.original);
|
|
||||||
deleteModal.openModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{currentPageSize > 2 && (
|
|
||||||
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
|
||||||
<TableRowOptions
|
|
||||||
type='dropdown'
|
|
||||||
recordId={props.row.original.id}
|
|
||||||
basePath='/inventory/movement'
|
|
||||||
queryParam='movementId'
|
|
||||||
showEdit={false}
|
|
||||||
showDelete={false}
|
|
||||||
/>
|
|
||||||
</RowDropdownOptions>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{currentPageSize <= 2 && (
|
|
||||||
<RowCollapseOptions>
|
|
||||||
<TableRowOptions
|
|
||||||
type='collapse'
|
|
||||||
recordId={props.row.original.id}
|
|
||||||
basePath='/inventory/movement'
|
|
||||||
queryParam='movementId'
|
|
||||||
showEdit={false}
|
|
||||||
showDelete={false}
|
|
||||||
/>
|
|
||||||
</RowCollapseOptions>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
pageSize={tableFilterState.pageSize}
|
|
||||||
page={isResponseSuccess(movements) ? movements?.meta?.page : 0}
|
|
||||||
totalItems={
|
|
||||||
isResponseSuccess(movements) ? movements?.meta?.total_results : 0
|
|
||||||
}
|
|
||||||
onPageChange={setPage}
|
|
||||||
isLoading={isLoading}
|
|
||||||
sorting={sorting}
|
|
||||||
setSorting={setSorting}
|
|
||||||
className={{
|
|
||||||
containerClassName: cn({
|
|
||||||
'mb-20':
|
|
||||||
isResponseSuccess(movements) && movements?.data?.length === 0,
|
|
||||||
}),
|
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
|
||||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
|
||||||
headerColumnClassName:
|
|
||||||
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
|
||||||
bodyRowClassName: 'border-b border-b-gray-200',
|
|
||||||
bodyColumnClassName:
|
|
||||||
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +1,82 @@
|
|||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { Movement } from '@/types/api/inventory/movement';
|
import { Movement } from '@/types/api/inventory/movement';
|
||||||
|
|
||||||
|
type MovementFormSchemaType = {
|
||||||
|
transfer_reason: string;
|
||||||
|
transfer_date: string;
|
||||||
|
source_warehouse?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
area?: string;
|
||||||
|
location?: string;
|
||||||
|
} | null;
|
||||||
|
source_warehouse_id: number;
|
||||||
|
destination_warehouse?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
area?: string;
|
||||||
|
location?: string;
|
||||||
|
} | null;
|
||||||
|
destination_warehouse_id: number;
|
||||||
|
products: {
|
||||||
|
product?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
product_id: number;
|
||||||
|
product_qty: number | string;
|
||||||
|
}[];
|
||||||
|
deliveries: {
|
||||||
|
delivery_cost?: number | string;
|
||||||
|
delivery_cost_per_item?: number | string;
|
||||||
|
document?: File | string | null;
|
||||||
|
document_path?: string | null;
|
||||||
|
driver_name: string;
|
||||||
|
vehicle_plate: string;
|
||||||
|
supplier?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
supplier_id: number;
|
||||||
|
products: {
|
||||||
|
product?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
product_id: number;
|
||||||
|
product_qty: number | string;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
export type ProductSchema = {
|
export type ProductSchema = {
|
||||||
product: {
|
product?: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
} | null;
|
} | null;
|
||||||
product_id: number;
|
product_id: number;
|
||||||
product_qty: number;
|
product_qty: number | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeliverySchema = {
|
export type DeliverySchema = {
|
||||||
delivery_cost?: number | undefined;
|
delivery_cost?: number | string;
|
||||||
delivery_cost_per_item?: number | undefined;
|
delivery_cost_per_item?: number | string;
|
||||||
document?: File | string | null;
|
document?: File | string | null;
|
||||||
document_path?: string | null;
|
document_path?: string | null;
|
||||||
driver_name: string;
|
driver_name: string;
|
||||||
vehicle_plate: string;
|
vehicle_plate: string;
|
||||||
supplier: {
|
supplier?: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
} | null;
|
} | null;
|
||||||
supplier_id: number;
|
supplier_id: number;
|
||||||
products: {
|
products: {
|
||||||
product: {
|
product?: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
} | null;
|
} | null;
|
||||||
product_id: number;
|
product_id: number;
|
||||||
product_qty: number;
|
product_qty: number | string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -102,7 +150,7 @@ const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
|
|||||||
.required('Produk wajib diisi!'),
|
.required('Produk wajib diisi!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const MovementFormSchema = Yup.object({
|
export const MovementFormSchema: Yup.ObjectSchema<MovementFormSchemaType> = Yup.object({
|
||||||
transfer_reason: Yup.string().required('Alasan transfer wajib diisi!'),
|
transfer_reason: Yup.string().required('Alasan transfer wajib diisi!'),
|
||||||
transfer_date: Yup.string().required('Tanggal transfer wajib diisi!'),
|
transfer_date: Yup.string().required('Tanggal transfer wajib diisi!'),
|
||||||
source_warehouse: Yup.object({
|
source_warehouse: Yup.object({
|
||||||
@@ -133,8 +181,6 @@ export const MovementFormSchema = Yup.object({
|
|||||||
.required('Pengiriman wajib diisi!'),
|
.required('Pengiriman wajib diisi!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateMovementFormSchema = MovementFormSchema;
|
|
||||||
|
|
||||||
export type MovementFormValues = Yup.InferType<typeof MovementFormSchema>;
|
export type MovementFormValues = Yup.InferType<typeof MovementFormSchema>;
|
||||||
|
|
||||||
export const getMovementFormInitialValues = (
|
export const getMovementFormInitialValues = (
|
||||||
|
|||||||
@@ -8,26 +8,27 @@ import { Icon } from '@iconify/react';
|
|||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import TextInput from '@/components/input/TextInput';
|
import TextInput from '@/components/input/TextInput';
|
||||||
import NumberInput from '@/components/input/NumberInput';
|
import NumberInput from '@/components/input/NumberInput';
|
||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
import SelectInput, {
|
||||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
OptionType,
|
||||||
import { FormActions } from '@/components/helper/form/FormActions';
|
useSelect,
|
||||||
|
} from '@/components/input/SelectInput';
|
||||||
import {
|
import {
|
||||||
CreateMovementPayload,
|
CreateMovementPayload,
|
||||||
Movement,
|
Movement,
|
||||||
} from '@/types/api/inventory/movement';
|
} from '@/types/api/inventory/movement';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
import {
|
import {
|
||||||
MovementFormSchema,
|
MovementFormSchema,
|
||||||
MovementFormValues,
|
MovementFormValues,
|
||||||
UpdateMovementFormSchema,
|
|
||||||
getMovementFormInitialValues,
|
getMovementFormInitialValues,
|
||||||
ProductSchema,
|
ProductSchema,
|
||||||
DeliverySchema,
|
DeliverySchema,
|
||||||
} from '@/components/pages/inventory/movement/form/MovementForm.schema';
|
} from '@/components/pages/inventory/movement/form/MovementForm.schema';
|
||||||
import { useMovementFormHandlers } from './useMovementFormHandlers';
|
|
||||||
import { SupplierApi, WarehouseApi } from '@/services/api/master-data';
|
import { SupplierApi, WarehouseApi } from '@/services/api/master-data';
|
||||||
import { ProductWarehouseApi } from '@/services/api/inventory';
|
import { ProductWarehouseApi } from '@/services/api/inventory';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
|
import { MovementApi } from '@/services/api/inventory';
|
||||||
import FileInput from '@/components/input/FileInput';
|
import FileInput from '@/components/input/FileInput';
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
import Badge from '@/components/Badge';
|
import Badge from '@/components/Badge';
|
||||||
@@ -38,24 +39,38 @@ interface MovementFormProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
// ===== STATE MANAGEMENT =====
|
// ===== STATE MANAGEMENT =====
|
||||||
const [, setMovementFormErrorMessage] = useState('');
|
const [movementFormErrorMessage, setMovementFormErrorMessage] = useState('');
|
||||||
const [
|
const [
|
||||||
productWarehouseSelectInputValue,
|
productWarehouseSelectInputValue,
|
||||||
setProductWarehouseSelectInputValue,
|
setProductWarehouseSelectInputValue,
|
||||||
] = useState('');
|
] = useState('');
|
||||||
const [selectedProducts, setSelectedProducts] = useState<number[]>([]);
|
const [selectedProducts, setSelectedProducts] = useState<number[]>([]);
|
||||||
const [selectedDeliveries, setSelectedDeliveries] = useState<number[]>([]);
|
const [selectedDeliveries, setSelectedDeliveries] = useState<number[]>([]);
|
||||||
const [warehouseSelectInputValue, setWarehouseSelectInputValue] =
|
|
||||||
useState('');
|
|
||||||
const [supplierSelectInputValue, setSupplierSelectInputValue] = useState('');
|
|
||||||
|
|
||||||
// ===== FORM HANDLERS =====
|
// ===== FORM HANDLERS =====
|
||||||
const {
|
const createMovementHandler = useCallback(
|
||||||
movementFormErrorMessage,
|
async (payload: CreateMovementPayload, documents: File[] = []) => {
|
||||||
createMovementHandler,
|
const formData = new FormData();
|
||||||
updateMovementHandler,
|
formData.append('data', JSON.stringify(payload));
|
||||||
} = useMovementFormHandlers(initialValues?.id);
|
documents.forEach((file, index) => {
|
||||||
|
formData.append(`documents[${index}]`, file);
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await MovementApi.create(
|
||||||
|
formData as unknown as CreateMovementPayload
|
||||||
|
);
|
||||||
|
if (isResponseError(res)) {
|
||||||
|
setMovementFormErrorMessage(res.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.success(res?.message as string);
|
||||||
|
router.push('/inventory/movement');
|
||||||
|
},
|
||||||
|
[router]
|
||||||
|
);
|
||||||
|
|
||||||
// ===== INTERFACES =====
|
// ===== INTERFACES =====
|
||||||
interface WarehouseOptionType extends OptionType {
|
interface WarehouseOptionType extends OptionType {
|
||||||
@@ -77,18 +92,25 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
ProductWarehouseApi.getAllFetcher
|
ProductWarehouseApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ===== USE SELECT HOOKS =====
|
||||||
|
const {
|
||||||
|
inputValue: warehouseSelectInputValue,
|
||||||
|
setInputValue: setWarehouseSelectInputValue,
|
||||||
|
isLoadingOptions: isLoadingWarehouses,
|
||||||
|
} = useSelect(WarehouseApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setSupplierSelectInputValue,
|
||||||
|
options: supplierOptions,
|
||||||
|
isLoadingOptions: isLoadingSuppliers,
|
||||||
|
} = useSelect(SupplierApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
const warehousesUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ search: warehouseSelectInputValue }).toString()}`;
|
const warehousesUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ search: warehouseSelectInputValue }).toString()}`;
|
||||||
const { data: warehouses, isLoading: isLoadingWarehouses } = useSWR(
|
const { data: warehouses } = useSWR(
|
||||||
warehousesUrl,
|
warehousesUrl,
|
||||||
WarehouseApi.getAllFetcher
|
WarehouseApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ search: supplierSelectInputValue }).toString()}`;
|
|
||||||
const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(
|
|
||||||
suppliersUrl,
|
|
||||||
SupplierApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== DATA PROCESSING =====
|
// ===== DATA PROCESSING =====
|
||||||
const warehouseStockMap = useMemo(() => {
|
const warehouseStockMap = useMemo(() => {
|
||||||
if (!isResponseSuccess(allProductWarehouses)) return new Map();
|
if (!isResponseSuccess(allProductWarehouses)) return new Map();
|
||||||
@@ -114,8 +136,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
return stockMap;
|
return stockMap;
|
||||||
}, [allProductWarehouses]);
|
}, [allProductWarehouses]);
|
||||||
|
|
||||||
const warehouseOptions = isResponseSuccess(warehouses)
|
const warehouseOptions = useMemo(() => {
|
||||||
? warehouses?.data.map((w) => {
|
if (!isResponseSuccess(warehouses)) return [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
warehouses?.data.map((w) => {
|
||||||
warehouseStockMap.get(w.id);
|
warehouseStockMap.get(w.id);
|
||||||
return {
|
return {
|
||||||
value: w.id,
|
value: w.id,
|
||||||
@@ -126,12 +151,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
? w.location?.name
|
? w.location?.name
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
})
|
}) || []
|
||||||
: [];
|
);
|
||||||
|
}, [warehouses, warehouseStockMap]);
|
||||||
const supplierOptions = isResponseSuccess(suppliers)
|
|
||||||
? suppliers?.data.map((s) => ({ value: s.id, label: s.name }))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// ===== FORM INITIALIZATION =====
|
// ===== FORM INITIALIZATION =====
|
||||||
const formikInitialValues = useMemo<MovementFormValues>(
|
const formikInitialValues = useMemo<MovementFormValues>(
|
||||||
@@ -141,8 +163,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
|
|
||||||
const formik = useFormik<MovementFormValues>({
|
const formik = useFormik<MovementFormValues>({
|
||||||
initialValues: formikInitialValues,
|
initialValues: formikInitialValues,
|
||||||
validationSchema:
|
validationSchema: MovementFormSchema,
|
||||||
type === 'edit' ? UpdateMovementFormSchema : MovementFormSchema,
|
|
||||||
validateOnChange: true,
|
validateOnChange: true,
|
||||||
validateOnBlur: true,
|
validateOnBlur: true,
|
||||||
validateOnMount: false,
|
validateOnMount: false,
|
||||||
@@ -150,7 +171,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
setMovementFormErrorMessage('');
|
setMovementFormErrorMessage('');
|
||||||
const documents: File[] = [];
|
const documents: File[] = [];
|
||||||
const deliveriesPayload = values.deliveries.map((d, idx) => {
|
const deliveriesPayload = values.deliveries.map((d) => {
|
||||||
let documentIndex = 0;
|
let documentIndex = 0;
|
||||||
|
|
||||||
if (d.document && d.document instanceof File) {
|
if (d.document && d.document instanceof File) {
|
||||||
@@ -159,8 +180,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
delivery_cost: d.delivery_cost ?? 0,
|
delivery_cost: parseInt((d.delivery_cost || '').toString()) || 0,
|
||||||
delivery_cost_per_item: d.delivery_cost_per_item ?? 0,
|
delivery_cost_per_item:
|
||||||
|
parseInt((d.delivery_cost_per_item || '').toString()) || 0,
|
||||||
document_index: documentIndex,
|
document_index: documentIndex,
|
||||||
document_path: d.document_path,
|
document_path: d.document_path,
|
||||||
driver_name: d.driver_name,
|
driver_name: d.driver_name,
|
||||||
@@ -168,7 +190,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
supplier_id: d.supplier_id,
|
supplier_id: d.supplier_id,
|
||||||
products: d.products.map((p) => ({
|
products: d.products.map((p) => ({
|
||||||
product_id: p.product_id,
|
product_id: p.product_id,
|
||||||
product_qty: p.product_qty,
|
product_qty: parseInt(p.product_qty.toString()) || 0,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -180,7 +202,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
destination_warehouse_id: values.destination_warehouse_id,
|
destination_warehouse_id: values.destination_warehouse_id,
|
||||||
products: values.products.map((p) => ({
|
products: values.products.map((p) => ({
|
||||||
product_id: p.product_id,
|
product_id: p.product_id,
|
||||||
product_qty: p.product_qty,
|
product_qty: parseInt(p.product_qty.toString()) || 0,
|
||||||
})),
|
})),
|
||||||
deliveries: deliveriesPayload,
|
deliveries: deliveriesPayload,
|
||||||
};
|
};
|
||||||
@@ -189,13 +211,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
case 'add':
|
case 'add':
|
||||||
await createMovementHandler(payload, documents);
|
await createMovementHandler(payload, documents);
|
||||||
break;
|
break;
|
||||||
case 'edit':
|
|
||||||
await updateMovementHandler(
|
|
||||||
initialValues?.id as number,
|
|
||||||
payload,
|
|
||||||
documents
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -297,7 +312,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
{
|
{
|
||||||
product: null,
|
product: null,
|
||||||
product_id: 0,
|
product_id: 0,
|
||||||
product_qty: 0,
|
product_qty: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
formik.setFieldValue('products', newProducts);
|
formik.setFieldValue('products', newProducts);
|
||||||
@@ -332,8 +347,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
formik.setFieldValue('deliveries', [
|
formik.setFieldValue('deliveries', [
|
||||||
...(formik.values.deliveries || []),
|
...(formik.values.deliveries || []),
|
||||||
{
|
{
|
||||||
delivery_cost: undefined,
|
delivery_cost: '',
|
||||||
delivery_cost_per_item: undefined,
|
delivery_cost_per_item: '',
|
||||||
document: null,
|
document: null,
|
||||||
driver_name: '',
|
driver_name: '',
|
||||||
vehicle_plate: '',
|
vehicle_plate: '',
|
||||||
@@ -343,7 +358,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
{
|
{
|
||||||
product: null,
|
product: null,
|
||||||
product_id: 0,
|
product_id: 0,
|
||||||
product_qty: 0,
|
product_qty: '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -385,7 +400,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
const delivery = formik.values.deliveries?.[idx];
|
const delivery = formik.values.deliveries?.[idx];
|
||||||
if (delivery) {
|
if (delivery) {
|
||||||
const productQty = delivery.products.reduce(
|
const productQty = delivery.products.reduce(
|
||||||
(sum, p) => sum + p.product_qty,
|
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
if (productQty > 0 && value > 0) {
|
if (productQty > 0 && value > 0) {
|
||||||
@@ -409,7 +424,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
const delivery = formik.values.deliveries?.[idx];
|
const delivery = formik.values.deliveries?.[idx];
|
||||||
if (delivery) {
|
if (delivery) {
|
||||||
const productQty = delivery.products.reduce(
|
const productQty = delivery.products.reduce(
|
||||||
(sum, p) => sum + p.product_qty,
|
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
if (productQty > 0 && value > 0) {
|
if (productQty > 0 && value > 0) {
|
||||||
@@ -683,36 +698,38 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formik.values.deliveries?.forEach((delivery, idx) => {
|
formik.values.deliveries?.forEach((delivery, idx) => {
|
||||||
const productQty = delivery.products.reduce(
|
const productQty = delivery.products.reduce(
|
||||||
(sum, p) => sum + p.product_qty,
|
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
const deliveryCost =
|
||||||
delivery.delivery_cost &&
|
parseInt((delivery.delivery_cost || '').toString()) || 0;
|
||||||
delivery.delivery_cost > 0 &&
|
const deliveryCostPerItem =
|
||||||
productQty > 0
|
parseInt((delivery.delivery_cost_per_item || '').toString()) || 0;
|
||||||
) {
|
|
||||||
const perItem = delivery.delivery_cost / productQty;
|
if (deliveryCost > 0 && productQty > 0) {
|
||||||
if (Math.abs((delivery.delivery_cost_per_item || 0) - perItem) > 0.01) {
|
const perItem = deliveryCost / productQty;
|
||||||
|
if (Math.abs(deliveryCostPerItem - perItem) > 0.01) {
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
`deliveries.${idx}.delivery_cost_per_item`,
|
`deliveries.${idx}.delivery_cost_per_item`,
|
||||||
perItem
|
perItem
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (deliveryCostPerItem > 0 && productQty > 0) {
|
||||||
delivery.delivery_cost_per_item &&
|
const totalCost = deliveryCostPerItem * productQty;
|
||||||
delivery.delivery_cost_per_item > 0 &&
|
if (Math.abs(deliveryCost - totalCost) > 0.01) {
|
||||||
productQty > 0
|
|
||||||
) {
|
|
||||||
const totalCost = delivery.delivery_cost_per_item * productQty;
|
|
||||||
if (Math.abs((delivery.delivery_cost || 0) - totalCost) > 0.01) {
|
|
||||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
|
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, totalCost);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
formik.values.deliveries
|
formik.values.deliveries
|
||||||
?.map((d) => d.products.reduce((sum, p) => sum + p.product_qty, 0))
|
?.map((d) =>
|
||||||
|
d.products.reduce(
|
||||||
|
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
)
|
||||||
.join(','),
|
.join(','),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -730,11 +747,21 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full'>
|
<section className='w-full'>
|
||||||
<FormHeader
|
<header className='flex flex-col gap-4'>
|
||||||
type={type}
|
<Button
|
||||||
title='Movement'
|
href='/inventory/movement'
|
||||||
backUrl='/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>
|
||||||
<form
|
<form
|
||||||
onSubmit={formik.handleSubmit}
|
onSubmit={formik.handleSubmit}
|
||||||
onReset={formik.handleReset}
|
onReset={formik.handleReset}
|
||||||
@@ -748,6 +775,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
required
|
required
|
||||||
label='Alasan Transfer'
|
label='Alasan Transfer'
|
||||||
name='transfer_reason'
|
name='transfer_reason'
|
||||||
|
placeholder='Masukkan alasan transfer...'
|
||||||
value={formik.values.transfer_reason}
|
value={formik.values.transfer_reason}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
@@ -785,6 +813,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<SelectInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
label='Gudang'
|
label='Gudang'
|
||||||
|
placeholder='Pilih gudang asal...'
|
||||||
value={formik.values.source_warehouse}
|
value={formik.values.source_warehouse}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
formik.setFieldTouched('source_warehouse', true);
|
formik.setFieldTouched('source_warehouse', true);
|
||||||
@@ -852,6 +881,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<SelectInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
label='Gudang'
|
label='Gudang'
|
||||||
|
placeholder='Pilih gudang tujuan...'
|
||||||
value={formik.values.destination_warehouse}
|
value={formik.values.destination_warehouse}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
formik.setFieldTouched('destination_warehouse', true);
|
formik.setFieldTouched('destination_warehouse', true);
|
||||||
@@ -1038,8 +1068,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
}
|
}
|
||||||
placeholder={
|
placeholder={
|
||||||
!formik.values.source_warehouse_id
|
!formik.values.source_warehouse_id
|
||||||
? 'Pilih gudang asal terlebih dahulu'
|
? 'Pilih gudang asal terlebih dahulu...'
|
||||||
: 'Pilih produk'
|
: 'Pilih produk...'
|
||||||
}
|
}
|
||||||
isClearable
|
isClearable
|
||||||
{...isRepeaterInputError(
|
{...isRepeaterInputError(
|
||||||
@@ -1057,6 +1087,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
name={`products.${idx}.product_qty`}
|
name={`products.${idx}.product_qty`}
|
||||||
|
placeholder='Masukkan kuantitas...'
|
||||||
value={product.product_qty ?? ''}
|
value={product.product_qty ?? ''}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
@@ -1277,6 +1308,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<td>
|
<td>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
|
placeholder='Pilih produk...'
|
||||||
value={delivery.products[0]?.product ?? undefined}
|
value={delivery.products[0]?.product ?? undefined}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
formik.setFieldTouched(
|
formik.setFieldTouched(
|
||||||
@@ -1317,6 +1349,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
name={`deliveries.${idx}.products.0.product_qty`}
|
name={`deliveries.${idx}.products.0.product_qty`}
|
||||||
|
placeholder='Masukkan kuantitas...'
|
||||||
value={delivery.products[0]?.product_qty ?? ''}
|
value={delivery.products[0]?.product_qty ?? ''}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
@@ -1347,6 +1380,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<td>
|
<td>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
|
placeholder='Pilih supplier...'
|
||||||
value={delivery.supplier}
|
value={delivery.supplier}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
formik.setFieldTouched(
|
formik.setFieldTouched(
|
||||||
@@ -1386,6 +1420,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
required
|
required
|
||||||
name={`deliveries.${idx}.vehicle_plate`}
|
name={`deliveries.${idx}.vehicle_plate`}
|
||||||
|
placeholder='Masukkan plat nomor...'
|
||||||
value={delivery.vehicle_plate}
|
value={delivery.vehicle_plate}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
@@ -1463,6 +1498,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
name={`deliveries.${idx}.delivery_cost`}
|
name={`deliveries.${idx}.delivery_cost`}
|
||||||
|
placeholder='Masukkan biaya pengiriman...'
|
||||||
value={delivery.delivery_cost || ''}
|
value={delivery.delivery_cost || ''}
|
||||||
onChange={handleDeliveryCostChangeWrapper(idx)}
|
onChange={handleDeliveryCostChangeWrapper(idx)}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
@@ -1487,6 +1523,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
name={`deliveries.${idx}.delivery_cost_per_item`}
|
name={`deliveries.${idx}.delivery_cost_per_item`}
|
||||||
|
placeholder='Masukkan biaya per item...'
|
||||||
value={delivery.delivery_cost_per_item || ''}
|
value={delivery.delivery_cost_per_item || ''}
|
||||||
onChange={handleDeliveryCostPerItemChangeWrapper(
|
onChange={handleDeliveryCostPerItemChangeWrapper(
|
||||||
idx
|
idx
|
||||||
@@ -1513,6 +1550,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
required
|
required
|
||||||
name={`deliveries.${idx}.driver_name`}
|
name={`deliveries.${idx}.driver_name`}
|
||||||
|
placeholder='Masukkan nama sopir...'
|
||||||
value={delivery.driver_name}
|
value={delivery.driver_name}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
@@ -1582,11 +1620,30 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action buttons */}
|
{/* Action buttons */}
|
||||||
<FormActions<MovementFormValues>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
type={type}
|
{type !== 'detail' && (
|
||||||
formik={formik}
|
<div className='flex flex-row justify-end gap-2 w-full'>
|
||||||
disableSubmit={hasInvalidQty || hasExceededStock}
|
<Button type='reset' color='warning' className='px-4'>
|
||||||
/>
|
Reset
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
color='primary'
|
||||||
|
className='px-4'
|
||||||
|
isLoading={formik.isSubmitting}
|
||||||
|
disabled={
|
||||||
|
hasInvalidQty ||
|
||||||
|
hasExceededStock ||
|
||||||
|
!formik.isValid ||
|
||||||
|
formik.isSubmitting
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{movementFormErrorMessage && (
|
{movementFormErrorMessage && (
|
||||||
<div role='alert' className='alert alert-error'>
|
<div role='alert' className='alert alert-error'>
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
import { useCallback, useState } from 'react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { toast } from 'react-hot-toast';
|
|
||||||
import { useModal } from '@/components/Modal';
|
|
||||||
import { MovementApi } from '@/services/api/inventory';
|
|
||||||
import {
|
|
||||||
CreateMovementPayload,
|
|
||||||
UpdateMovementPayload,
|
|
||||||
} from '@/types/api/inventory/movement';
|
|
||||||
import { isResponseError } from '@/lib/api-helper';
|
|
||||||
|
|
||||||
export const useMovementFormHandlers = (initialValuesId?: number) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const deleteModal = useModal();
|
|
||||||
const [movementFormErrorMessage, setMovementFormErrorMessage] = useState('');
|
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
|
||||||
|
|
||||||
const createMovementHandler = useCallback(
|
|
||||||
async (payload: CreateMovementPayload, documents: File[] = []) => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('data', JSON.stringify(payload));
|
|
||||||
documents.forEach((file, index) => {
|
|
||||||
formData.append(`documents[${index}]`, file);
|
|
||||||
});
|
|
||||||
|
|
||||||
const res = await MovementApi.create(
|
|
||||||
formData as unknown as CreateMovementPayload
|
|
||||||
);
|
|
||||||
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,
|
|
||||||
documents: File[] = []
|
|
||||||
) => {
|
|
||||||
let finalPayload: UpdateMovementPayload | FormData;
|
|
||||||
|
|
||||||
if (documents.length > 0) {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('data', JSON.stringify(payload));
|
|
||||||
documents.forEach((file, index) => {
|
|
||||||
formData.append(`documents[${index}]`, file);
|
|
||||||
});
|
|
||||||
|
|
||||||
finalPayload = formData as unknown as UpdateMovementPayload;
|
|
||||||
} else {
|
|
||||||
finalPayload = payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await MovementApi.update(movementId, finalPayload);
|
|
||||||
if (res?.status === 'error') {
|
|
||||||
setMovementFormErrorMessage(res.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
toast.success(res?.message as string);
|
|
||||||
router.refresh();
|
|
||||||
router.push('/inventory/movement');
|
|
||||||
},
|
|
||||||
[router]
|
|
||||||
);
|
|
||||||
|
|
||||||
const deleteMovementClickHandler = useCallback(() => {
|
|
||||||
deleteModal.openModal();
|
|
||||||
}, [deleteModal]);
|
|
||||||
|
|
||||||
const confirmationModalDeleteClickHandler = useCallback(async () => {
|
|
||||||
if (!initialValuesId) return;
|
|
||||||
|
|
||||||
setIsDeleteLoading(true);
|
|
||||||
await MovementApi.delete(initialValuesId);
|
|
||||||
deleteModal.closeModal();
|
|
||||||
toast.success('Successfully delete Movement!');
|
|
||||||
setIsDeleteLoading(false);
|
|
||||||
router.push('/inventory/movement');
|
|
||||||
}, [deleteModal, initialValuesId, router]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
deleteModal,
|
|
||||||
movementFormErrorMessage,
|
|
||||||
isDeleteLoading,
|
|
||||||
createMovementHandler,
|
|
||||||
updateMovementHandler,
|
|
||||||
deleteMovementClickHandler,
|
|
||||||
confirmationModalDeleteClickHandler,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
+12
-4
@@ -1,9 +1,17 @@
|
|||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export const ProductCategoryFormSchema = Yup.object({
|
type ProductCategoryFormSchemaType = {
|
||||||
code: Yup.string().required('Kode wajib diisi!').max(3, 'Kode kategori produk melebihi 3 karakter!'),
|
code: string;
|
||||||
name: Yup.string().required('Nama wajib diisi!'),
|
name: string;
|
||||||
});
|
};
|
||||||
|
|
||||||
|
export const ProductCategoryFormSchema: Yup.ObjectSchema<ProductCategoryFormSchemaType> =
|
||||||
|
Yup.object({
|
||||||
|
code: Yup.string()
|
||||||
|
.required('Kode wajib diisi!')
|
||||||
|
.max(3, 'Kode kategori produk melebihi 3 karakter!'),
|
||||||
|
name: Yup.string().required('Nama wajib diisi!'),
|
||||||
|
});
|
||||||
|
|
||||||
export const UpdateProductCategoryFormSchema = ProductCategoryFormSchema;
|
export const UpdateProductCategoryFormSchema = ProductCategoryFormSchema;
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ interface ProductCategoryFormProps {
|
|||||||
initialValues?: ProductCategory;
|
initialValues?: ProductCategory;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductCategoryForm = ({ type = 'add', initialValues }: ProductCategoryFormProps) => {
|
const ProductCategoryForm = ({
|
||||||
|
type = 'add',
|
||||||
|
initialValues,
|
||||||
|
}: ProductCategoryFormProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
|
|
||||||
@@ -68,16 +71,20 @@ const ProductCategoryForm = ({ type = 'add', initialValues }: ProductCategoryFor
|
|||||||
[router]
|
[router]
|
||||||
);
|
);
|
||||||
|
|
||||||
const formikInitialValues = useMemo<ProductCategoryFormValues>(() => {
|
const formikInitialValues = useMemo<ProductCategoryFormValues>(
|
||||||
return {
|
() => ({
|
||||||
code: initialValues?.code ?? '',
|
code: initialValues?.code ?? '',
|
||||||
name: initialValues?.name ?? '',
|
name: initialValues?.name ?? '',
|
||||||
};
|
}),
|
||||||
}, [initialValues]);
|
[initialValues]
|
||||||
|
);
|
||||||
|
|
||||||
const formik = useFormik<ProductCategoryFormValues>({
|
const formik = useFormik<ProductCategoryFormValues>({
|
||||||
initialValues: formikInitialValues,
|
initialValues: formikInitialValues,
|
||||||
validationSchema: type === 'edit' ? UpdateProductCategoryFormSchema : ProductCategoryFormSchema,
|
validationSchema:
|
||||||
|
type === 'edit'
|
||||||
|
? UpdateProductCategoryFormSchema
|
||||||
|
: ProductCategoryFormSchema,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
setFormErrorMessage('');
|
setFormErrorMessage('');
|
||||||
|
|
||||||
@@ -91,7 +98,10 @@ const ProductCategoryForm = ({ type = 'add', initialValues }: ProductCategoryFor
|
|||||||
await createProductCategoryHandler(payload);
|
await createProductCategoryHandler(payload);
|
||||||
break;
|
break;
|
||||||
case 'edit':
|
case 'edit':
|
||||||
await updateProductCategoryHandler(initialValues?.id as number, payload);
|
await updateProductCategoryHandler(
|
||||||
|
initialValues?.id as number,
|
||||||
|
payload
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -109,7 +119,7 @@ const ProductCategoryForm = ({ type = 'add', initialValues }: ProductCategoryFor
|
|||||||
await ProductCategoryApi.delete(initialValues?.id as number);
|
await ProductCategoryApi.delete(initialValues?.id as number);
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
toast.success('Successfully delete Product Category!');
|
toast.success('Berhasil menghapus data Kategori Produk!');
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
router.push('/master-data/product-category');
|
router.push('/master-data/product-category');
|
||||||
};
|
};
|
||||||
@@ -120,7 +130,7 @@ const ProductCategoryForm = ({ type = 'add', initialValues }: ProductCategoryFor
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full max-w-xl'>
|
<section className='w-full max-w-2xl'>
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button
|
<Button
|
||||||
href='/master-data/product-category'
|
href='/master-data/product-category'
|
||||||
@@ -132,9 +142,9 @@ const ProductCategoryForm = ({ type = 'add', initialValues }: ProductCategoryFor
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<h1 className='text-2xl font-bold text-center'>
|
<h1 className='text-2xl font-bold text-center'>
|
||||||
{type === 'add' && 'Tambah Product Category'}
|
{type === 'add' && 'Tambah Kategori Produk'}
|
||||||
{type === 'edit' && 'Edit Product Category'}
|
{type === 'edit' && 'Edit Kategori Produk'}
|
||||||
{type === 'detail' && 'Detail Product Category'}
|
{type === 'detail' && 'Detail Kategori Produk'}
|
||||||
</h1>
|
</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -148,7 +158,7 @@ const ProductCategoryForm = ({ type = 'add', initialValues }: ProductCategoryFor
|
|||||||
required
|
required
|
||||||
label='Kode'
|
label='Kode'
|
||||||
name='code'
|
name='code'
|
||||||
placeholder='Masukkan kode kategori produk'
|
placeholder='Masukkan kode...'
|
||||||
value={formik.values.code}
|
value={formik.values.code}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
@@ -160,7 +170,7 @@ const ProductCategoryForm = ({ type = 'add', initialValues }: ProductCategoryFor
|
|||||||
required
|
required
|
||||||
label='Nama'
|
label='Nama'
|
||||||
name='name'
|
name='name'
|
||||||
placeholder='Masukkan nama kategori produk'
|
placeholder='Masukkan nama...'
|
||||||
value={formik.values.name}
|
value={formik.values.name}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
@@ -247,7 +257,7 @@ const ProductCategoryForm = ({ type = 'add', initialValues }: ProductCategoryFor
|
|||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
ref={deleteModal.ref}
|
ref={deleteModal.ref}
|
||||||
type='error'
|
type='error'
|
||||||
text={`Apakah anda yakin ingin menghapus data Product Category ini (${initialValues?.name})?`}
|
text={`Apakah anda yakin ingin menghapus data Kategori Produk ini (${initialValues?.name})?`}
|
||||||
secondaryButton={{
|
secondaryButton={{
|
||||||
text: 'Tidak',
|
text: 'Tidak',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,53 +1,83 @@
|
|||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export const ProductFormSchema = Yup.object({
|
type ProductFormSchemaType = {
|
||||||
name: Yup.string().required('Nama wajib diisi!'),
|
name: string;
|
||||||
brand: Yup.string().required('Merek wajib diisi!'),
|
brand: string;
|
||||||
sku: Yup.string().required('SKU wajib diisi!'),
|
sku: string;
|
||||||
uom: Yup.object({
|
uom?: {
|
||||||
value: Yup.number().min(1).required(),
|
value: number;
|
||||||
label: Yup.string().required(),
|
label: string;
|
||||||
}).nullable(),
|
} | null;
|
||||||
uom_id: Yup.number().required('Satuan wajib diisi!').typeError('Satuan wajib diisi!'),
|
uom_id: number;
|
||||||
product_category: Yup.object({
|
product_category?: {
|
||||||
value: Yup.number().min(1).required(),
|
value: number;
|
||||||
label: Yup.string().required(),
|
label: string;
|
||||||
}).nullable(),
|
} | null;
|
||||||
product_category_id: Yup.number()
|
product_category_id: number;
|
||||||
|
product_price: number | string;
|
||||||
|
selling_price: number | string;
|
||||||
|
tax: number | string;
|
||||||
|
expiry_period: number | string;
|
||||||
|
supplier_ids: number[];
|
||||||
|
flags: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
|
||||||
|
Yup.object({
|
||||||
|
name: Yup.string().required('Nama wajib diisi!'),
|
||||||
|
brand: Yup.string().required('Merek wajib diisi!'),
|
||||||
|
sku: Yup.string().required('SKU wajib diisi!'),
|
||||||
|
|
||||||
|
uom: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable().required('Satuan wajib diisi!'),
|
||||||
|
|
||||||
|
uom_id: Yup.number()
|
||||||
|
.required('Satuan wajib diisi!')
|
||||||
|
.typeError('Satuan wajib diisi!'),
|
||||||
|
|
||||||
|
product_category: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable().required('Kategori produk wajib diisi!'),
|
||||||
|
|
||||||
|
product_category_id: Yup.number()
|
||||||
.required('Kategori produk wajib diisi!')
|
.required('Kategori produk wajib diisi!')
|
||||||
.typeError('Kategori produk wajib diisi!'),
|
.typeError('Kategori produk wajib diisi!'),
|
||||||
product_price: Yup.number()
|
|
||||||
|
product_price: Yup.number()
|
||||||
.required('Harga produk wajib diisi!')
|
.required('Harga produk wajib diisi!')
|
||||||
.typeError('Harga produk wajib diisi!')
|
.typeError('Harga produk wajib diisi!')
|
||||||
.min(0, 'Harga produk tidak boleh kurang dari 0!'),
|
.min(0, 'Harga produk tidak boleh kurang dari 0!'),
|
||||||
selling_price: Yup.number()
|
|
||||||
|
selling_price: Yup.number()
|
||||||
.required('Harga jual wajib diisi!')
|
.required('Harga jual wajib diisi!')
|
||||||
.typeError('Harga jual wajib diisi!')
|
.typeError('Harga jual wajib diisi!')
|
||||||
.min(0, 'Harga jual tidak boleh kurang dari 0!'),
|
.min(0, 'Harga jual tidak boleh kurang dari 0!'),
|
||||||
tax: Yup.number()
|
|
||||||
|
tax: Yup.number()
|
||||||
.required('Pajak wajib diisi!')
|
.required('Pajak wajib diisi!')
|
||||||
.typeError('Pajak wajib diisi!')
|
.typeError('Pajak wajib diisi!')
|
||||||
.min(0, 'Pajak tidak boleh kurang dari 0!')
|
.min(0, 'Pajak tidak boleh kurang dari 0!')
|
||||||
.max(100, 'Pajak tidak boleh lebih dari 100%!'),
|
.max(100, 'Pajak tidak boleh lebih dari 100%!'),
|
||||||
expiry_period: Yup.number()
|
|
||||||
|
expiry_period: Yup.number()
|
||||||
.required('Periode kadaluarsa wajib diisi!')
|
.required('Periode kadaluarsa wajib diisi!')
|
||||||
.typeError('Periode kadaluarsa wajib diisi!')
|
.typeError('Periode kadaluarsa wajib diisi!')
|
||||||
.min(0, 'Periode kadaluarsa tidak boleh kurang dari 0!'),
|
.min(0, 'Periode kadaluarsa tidak boleh kurang dari 0!'),
|
||||||
supplier: Yup.object({
|
|
||||||
value: Yup.number().min(1).required(),
|
supplier_ids: Yup.array()
|
||||||
label: Yup.string().required(),
|
.of(Yup.number().required().typeError('Supplier tidak valid!'))
|
||||||
}).nullable(),
|
|
||||||
supplier_ids: Yup.array()
|
|
||||||
.of(Yup.number().typeError('Supplier tidak valid!'))
|
|
||||||
.min(1, 'Minimal harus ada 1 supplier!')
|
.min(1, 'Minimal harus ada 1 supplier!')
|
||||||
.required('Supplier wajib diisi!'),
|
.required('Supplier wajib diisi!'),
|
||||||
flags: Yup.array()
|
|
||||||
.of(Yup.string())
|
flags: Yup.array()
|
||||||
|
.of(Yup.string().required())
|
||||||
.min(1, 'Minimal harus ada 1 flag!')
|
.min(1, 'Minimal harus ada 1 flag!')
|
||||||
.required('Flag wajib diisi!'),
|
.required('Flag wajib diisi!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateProductFormSchema = ProductFormSchema;
|
export const UpdateProductFormSchema = ProductFormSchema;
|
||||||
|
|
||||||
export type ProductFormValues = Yup.InferType<typeof ProductFormSchema>;
|
export type ProductFormValues = Yup.InferType<typeof ProductFormSchema>;
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import useSWR from 'swr';
|
|||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import TextInput from '@/components/input/TextInput';
|
import TextInput from '@/components/input/TextInput';
|
||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
import NumberInput from '@/components/input/NumberInput';
|
||||||
|
import SelectInput, {
|
||||||
|
OptionType,
|
||||||
|
useSelect,
|
||||||
|
} from '@/components/input/SelectInput';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
|
||||||
@@ -24,7 +28,12 @@ import {
|
|||||||
CreateProductPayload,
|
CreateProductPayload,
|
||||||
UpdateProductPayload,
|
UpdateProductPayload,
|
||||||
} from '@/types/api/master-data/product';
|
} from '@/types/api/master-data/product';
|
||||||
import { UomApi, ProductCategoryApi, SupplierApi, ProductApi } from '@/services/api/master-data';
|
import {
|
||||||
|
UomApi,
|
||||||
|
ProductCategoryApi,
|
||||||
|
SupplierApi,
|
||||||
|
ProductApi,
|
||||||
|
} from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { PRODUCT_FLAG_OPTIONS } from '@/config/constant';
|
import { PRODUCT_FLAG_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -67,30 +76,36 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
[router]
|
[router]
|
||||||
);
|
);
|
||||||
|
|
||||||
const formikInitialValues = useMemo<ProductFormValues>(() => ({
|
const formikInitialValues = useMemo<ProductFormValues>(
|
||||||
name: initialValues?.name ?? '',
|
() => ({
|
||||||
brand: initialValues?.brand ?? '',
|
name: initialValues?.name ?? '',
|
||||||
sku: initialValues?.sku ?? '',
|
brand: initialValues?.brand ?? '',
|
||||||
uom: initialValues?.uom
|
sku: initialValues?.sku ?? '',
|
||||||
? { value: initialValues.uom.id, label: initialValues.uom.name }
|
uom: initialValues?.uom
|
||||||
: null,
|
? { value: initialValues.uom.id, label: initialValues.uom.name }
|
||||||
uom_id: initialValues?.uom?.id ?? 0,
|
: undefined,
|
||||||
product_category: initialValues?.product_category
|
uom_id: initialValues?.uom?.id ?? 0,
|
||||||
? { value: initialValues.product_category.id, label: initialValues.product_category.name }
|
product_category: initialValues?.product_category
|
||||||
: null,
|
? {
|
||||||
product_category_id: initialValues?.product_category?.id ?? 0,
|
value: initialValues.product_category.id,
|
||||||
product_price: initialValues?.product_price ?? 0,
|
label: initialValues.product_category.name,
|
||||||
selling_price: initialValues?.selling_price ?? 0,
|
}
|
||||||
tax: initialValues?.tax ?? 0,
|
: undefined,
|
||||||
expiry_period: initialValues?.expiry_period ?? 0,
|
product_category_id: initialValues?.product_category?.id ?? 0,
|
||||||
supplier: null, // not used for payload, just for UI
|
product_price: initialValues?.product_price ?? '',
|
||||||
supplier_ids: initialValues?.suppliers?.map(s => s.id) ?? [],
|
selling_price: initialValues?.selling_price ?? '',
|
||||||
flags: initialValues?.flags ?? [],
|
tax: initialValues?.tax ?? '',
|
||||||
}), [initialValues]);
|
expiry_period: initialValues?.expiry_period ?? '',
|
||||||
|
supplier_ids: initialValues?.suppliers?.map((s) => s.id) ?? [],
|
||||||
|
flags: initialValues?.flags ?? [],
|
||||||
|
}),
|
||||||
|
[initialValues]
|
||||||
|
);
|
||||||
|
|
||||||
const formik = useFormik<ProductFormValues>({
|
const formik = useFormik<ProductFormValues>({
|
||||||
initialValues: formikInitialValues,
|
initialValues: formikInitialValues,
|
||||||
validationSchema: type === 'edit' ? UpdateProductFormSchema : ProductFormSchema,
|
validationSchema:
|
||||||
|
type === 'edit' ? UpdateProductFormSchema : ProductFormSchema,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
setProductFormErrorMessage('');
|
setProductFormErrorMessage('');
|
||||||
const payload: CreateProductPayload = {
|
const payload: CreateProductPayload = {
|
||||||
@@ -99,12 +114,16 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
sku: values.sku,
|
sku: values.sku,
|
||||||
uom_id: values.uom_id,
|
uom_id: values.uom_id,
|
||||||
product_category_id: values.product_category_id,
|
product_category_id: values.product_category_id,
|
||||||
product_price: values.product_price,
|
product_price: parseInt(values.product_price.toString()) || 0,
|
||||||
selling_price: values.selling_price,
|
selling_price: parseInt(values.selling_price.toString()) || 0,
|
||||||
tax: values.tax,
|
tax: parseInt(values.tax.toString()) || 0,
|
||||||
expiry_period: values.expiry_period,
|
expiry_period: parseInt(values.expiry_period.toString()) || 0,
|
||||||
supplier_ids: (values.supplier_ids ?? []).filter((id): id is number => typeof id === 'number'),
|
supplier_ids: values.supplier_ids.filter(
|
||||||
flags: (values.flags ?? []).filter((f): f is string => typeof f === 'string'),
|
(id): id is number => typeof id === 'number'
|
||||||
|
),
|
||||||
|
flags: values.flags.filter(
|
||||||
|
(f): f is string => typeof f === 'string'
|
||||||
|
),
|
||||||
};
|
};
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'add':
|
case 'add':
|
||||||
@@ -120,12 +139,11 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
const { setValues: formikSetValues } = formik;
|
const { setValues: formikSetValues } = formik;
|
||||||
|
|
||||||
// UOM
|
// UOM
|
||||||
const [uomSelectInputValue, setUomSelectInputValue] = useState('');
|
const {
|
||||||
const uomsUrl = `${UomApi.basePath}?${new URLSearchParams({ search: uomSelectInputValue ?? '' }).toString()}`;
|
setInputValue: setUomSelectInputValue,
|
||||||
const { data: uoms, isLoading: isLoadingUoms } = useSWR(uomsUrl, UomApi.getAllFetcher);
|
options: uomOptions,
|
||||||
const uomOptions = isResponseSuccess(uoms)
|
isLoadingOptions: isLoadingUoms,
|
||||||
? uoms?.data.map((uom) => ({ value: uom.id, label: uom.name }))
|
} = useSelect(UomApi.basePath, 'id', 'name');
|
||||||
: [];
|
|
||||||
const uomChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const uomChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldTouched('uom', true);
|
formik.setFieldTouched('uom', true);
|
||||||
formik.setFieldValue('uom', val);
|
formik.setFieldValue('uom', val);
|
||||||
@@ -134,12 +152,11 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Product Category
|
// Product Category
|
||||||
const [categorySelectInputValue, setCategorySelectInputValue] = useState('');
|
const {
|
||||||
const categoriesUrl = `${ProductCategoryApi.basePath}?${new URLSearchParams({ search: categorySelectInputValue ?? '' }).toString()}`;
|
setInputValue: setCategorySelectInputValue,
|
||||||
const { data: categories, isLoading: isLoadingCategories } = useSWR(categoriesUrl, ProductCategoryApi.getAllFetcher);
|
options: categoryOptions,
|
||||||
const categoryOptions = isResponseSuccess(categories)
|
isLoadingOptions: isLoadingCategories,
|
||||||
? categories?.data.map((cat) => ({ value: cat.id, label: cat.name }))
|
} = useSelect(ProductCategoryApi.basePath, 'id', 'name');
|
||||||
: [];
|
|
||||||
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldTouched('product_category', true);
|
formik.setFieldTouched('product_category', true);
|
||||||
formik.setFieldValue('product_category', val);
|
formik.setFieldValue('product_category', val);
|
||||||
@@ -147,19 +164,25 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
formik.setFieldValue('product_category_id', (val as OptionType)?.value);
|
formik.setFieldValue('product_category_id', (val as OptionType)?.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Supplier (multi select)
|
// Supplier (multi select) - using SWR to filter by category
|
||||||
const [supplierSelectInputValue, setSupplierSelectInputValue] = useState('');
|
const [supplierSelectInputValue, setSupplierSelectInputValue] = useState('');
|
||||||
const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ search: supplierSelectInputValue ?? '' }).toString()}`;
|
const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ search: supplierSelectInputValue ?? '' }).toString()}`;
|
||||||
const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(suppliersUrl, SupplierApi.getAllFetcher);
|
const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(
|
||||||
|
suppliersUrl,
|
||||||
|
SupplierApi.getAllFetcher
|
||||||
|
);
|
||||||
const supplierOptions = isResponseSuccess(suppliers)
|
const supplierOptions = isResponseSuccess(suppliers)
|
||||||
? suppliers?.data
|
? suppliers?.data
|
||||||
.filter((sup) => sup.category === 'SAPRONAK')
|
.filter((sup) => sup.category === 'SAPRONAK')
|
||||||
.map((sup) => ({ value: sup.id, label: sup.name }))
|
.map((sup) => ({ value: sup.id, label: sup.name }))
|
||||||
: [];
|
: [];
|
||||||
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
||||||
formik.setFieldTouched('supplier_ids', true);
|
formik.setFieldTouched('supplier_ids', true);
|
||||||
formik.setFieldValue('supplier_ids', arr.map((v) => (v as OptionType).value));
|
formik.setFieldValue(
|
||||||
|
'supplier_ids',
|
||||||
|
arr.map((v) => (v as OptionType).value)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteProductClickHandler = () => {
|
const deleteProductClickHandler = () => {
|
||||||
@@ -181,7 +204,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full max-w-xl'>
|
<section className='w-full max-w-2xl'>
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button
|
<Button
|
||||||
href='/master-data/product'
|
href='/master-data/product'
|
||||||
@@ -207,7 +230,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
required
|
required
|
||||||
label='Nama'
|
label='Nama'
|
||||||
name='name'
|
name='name'
|
||||||
placeholder='Masukkan nama produk'
|
placeholder='Masukkan nama...'
|
||||||
value={formik.values.name}
|
value={formik.values.name}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
@@ -219,7 +242,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
required
|
required
|
||||||
label='Merek'
|
label='Merek'
|
||||||
name='brand'
|
name='brand'
|
||||||
placeholder='Masukkan merek produk'
|
placeholder='Masukkan merek...'
|
||||||
value={formik.values.brand}
|
value={formik.values.brand}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
@@ -231,7 +254,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
required
|
required
|
||||||
label='SKU'
|
label='SKU'
|
||||||
name='sku'
|
name='sku'
|
||||||
placeholder='Masukkan SKU produk'
|
placeholder='Masukkan SKU...'
|
||||||
value={formik.values.sku}
|
value={formik.values.sku}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
@@ -242,6 +265,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
<SelectInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
label='Satuan'
|
label='Satuan'
|
||||||
|
placeholder='Pilih satuan...'
|
||||||
value={formik.values.uom ?? undefined}
|
value={formik.values.uom ?? undefined}
|
||||||
onChange={uomChangeHandler}
|
onChange={uomChangeHandler}
|
||||||
options={uomOptions}
|
options={uomOptions}
|
||||||
@@ -255,78 +279,113 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
<SelectInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
label='Kategori Produk'
|
label='Kategori Produk'
|
||||||
|
placeholder='Pilih kategori produk...'
|
||||||
value={formik.values.product_category ?? undefined}
|
value={formik.values.product_category ?? undefined}
|
||||||
onChange={categoryChangeHandler}
|
onChange={categoryChangeHandler}
|
||||||
options={categoryOptions}
|
options={categoryOptions}
|
||||||
onInputChange={setCategorySelectInputValue}
|
onInputChange={setCategorySelectInputValue}
|
||||||
isLoading={isLoadingCategories}
|
isLoading={isLoadingCategories}
|
||||||
isError={formik.touched.product_category_id && Boolean(formik.errors.product_category_id)}
|
isError={
|
||||||
|
formik.touched.product_category_id &&
|
||||||
|
Boolean(formik.errors.product_category_id)
|
||||||
|
}
|
||||||
errorMessage={formik.errors.product_category_id as string}
|
errorMessage={formik.errors.product_category_id as string}
|
||||||
isDisabled={type === 'detail'}
|
isDisabled={type === 'detail'}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Harga Produk'
|
label='Harga Produk'
|
||||||
name='product_price'
|
name='product_price'
|
||||||
type='number'
|
placeholder='Masukkan harga produk...'
|
||||||
placeholder='Masukkan harga produk'
|
|
||||||
value={formik.values.product_price}
|
value={formik.values.product_price}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
isError={formik.touched.product_price && Boolean(formik.errors.product_price)}
|
decimalScale={2}
|
||||||
|
allowNegative={false}
|
||||||
|
thousandSeparator=','
|
||||||
|
decimalSeparator='.'
|
||||||
|
inputPrefix='Rp '
|
||||||
|
isError={
|
||||||
|
formik.touched.product_price &&
|
||||||
|
Boolean(formik.errors.product_price)
|
||||||
|
}
|
||||||
errorMessage={formik.errors.product_price as string}
|
errorMessage={formik.errors.product_price as string}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Harga Jual'
|
label='Harga Jual'
|
||||||
name='selling_price'
|
name='selling_price'
|
||||||
type='number'
|
placeholder='Masukkan harga jual...'
|
||||||
placeholder='Masukkan harga jual'
|
|
||||||
value={formik.values.selling_price}
|
value={formik.values.selling_price}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
isError={formik.touched.selling_price && Boolean(formik.errors.selling_price)}
|
decimalScale={2}
|
||||||
|
allowNegative={false}
|
||||||
|
thousandSeparator=','
|
||||||
|
decimalSeparator='.'
|
||||||
|
inputPrefix='Rp '
|
||||||
|
isError={
|
||||||
|
formik.touched.selling_price &&
|
||||||
|
Boolean(formik.errors.selling_price)
|
||||||
|
}
|
||||||
errorMessage={formik.errors.selling_price as string}
|
errorMessage={formik.errors.selling_price as string}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Pajak (%)'
|
label='Pajak (%)'
|
||||||
name='tax'
|
name='tax'
|
||||||
type='number'
|
placeholder='Masukkan pajak...'
|
||||||
placeholder='Masukkan pajak'
|
|
||||||
value={formik.values.tax}
|
value={formik.values.tax}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
|
decimalScale={2}
|
||||||
|
allowNegative={false}
|
||||||
|
thousandSeparator=','
|
||||||
|
decimalSeparator='.'
|
||||||
|
inputSuffix='%'
|
||||||
isError={formik.touched.tax && Boolean(formik.errors.tax)}
|
isError={formik.touched.tax && Boolean(formik.errors.tax)}
|
||||||
errorMessage={formik.errors.tax as string}
|
errorMessage={formik.errors.tax as string}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Periode Kadaluarsa (hari)'
|
label='Periode Kadaluarsa (hari)'
|
||||||
name='expiry_period'
|
name='expiry_period'
|
||||||
type='number'
|
placeholder='Masukkan periode kadaluarsa...'
|
||||||
placeholder='Masukkan periode kadaluarsa'
|
|
||||||
value={formik.values.expiry_period}
|
value={formik.values.expiry_period}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
isError={formik.touched.expiry_period && Boolean(formik.errors.expiry_period)}
|
decimalScale={0}
|
||||||
|
allowNegative={false}
|
||||||
|
thousandSeparator=','
|
||||||
|
decimalSeparator='.'
|
||||||
|
inputSuffix='hari'
|
||||||
|
isError={
|
||||||
|
formik.touched.expiry_period &&
|
||||||
|
Boolean(formik.errors.expiry_period)
|
||||||
|
}
|
||||||
errorMessage={formik.errors.expiry_period as string}
|
errorMessage={formik.errors.expiry_period as string}
|
||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
label='Supplier'
|
label='Supplier'
|
||||||
|
placeholder='Pilih supplier...'
|
||||||
isMulti
|
isMulti
|
||||||
value={supplierOptions.filter(opt => formik.values.supplier_ids.includes(opt.value))}
|
value={supplierOptions.filter((opt) =>
|
||||||
|
(formik.values.supplier_ids || []).includes(opt.value)
|
||||||
|
)}
|
||||||
onChange={supplierChangeHandler}
|
onChange={supplierChangeHandler}
|
||||||
options={supplierOptions}
|
options={supplierOptions}
|
||||||
onInputChange={setSupplierSelectInputValue}
|
onInputChange={setSupplierSelectInputValue}
|
||||||
isLoading={isLoadingSuppliers}
|
isLoading={isLoadingSuppliers}
|
||||||
isError={formik.touched.supplier_ids && Boolean(formik.errors.supplier_ids)}
|
isError={
|
||||||
|
formik.touched.supplier_ids &&
|
||||||
|
Boolean(formik.errors.supplier_ids)
|
||||||
|
}
|
||||||
errorMessage={formik.errors.supplier_ids as string}
|
errorMessage={formik.errors.supplier_ids as string}
|
||||||
isDisabled={type === 'detail'}
|
isDisabled={type === 'detail'}
|
||||||
isClearable
|
isClearable
|
||||||
@@ -334,11 +393,17 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
<SelectInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
label='Flags'
|
label='Flags'
|
||||||
|
placeholder='Pilih flags...'
|
||||||
isMulti
|
isMulti
|
||||||
value={PRODUCT_FLAG_OPTIONS.filter(opt => formik.values.flags.includes(opt.value))}
|
value={PRODUCT_FLAG_OPTIONS.filter((opt) =>
|
||||||
onChange={val => {
|
(formik.values.flags || []).includes(opt.value)
|
||||||
|
)}
|
||||||
|
onChange={(val) => {
|
||||||
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
||||||
formik.setFieldValue('flags', arr.map((v) => (v as OptionType).value));
|
formik.setFieldValue(
|
||||||
|
'flags',
|
||||||
|
arr.map((v) => (v as OptionType).value)
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
options={PRODUCT_FLAG_OPTIONS}
|
options={PRODUCT_FLAG_OPTIONS}
|
||||||
isError={formik.touched.flags && Boolean(formik.errors.flags)}
|
isError={formik.touched.flags && Boolean(formik.errors.flags)}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
CreateMovementPayload,
|
CreateMovementPayload,
|
||||||
Movement,
|
Movement,
|
||||||
UpdateMovementPayload,
|
|
||||||
} from '@/types/api/inventory/movement';
|
} from '@/types/api/inventory/movement';
|
||||||
import {
|
import {
|
||||||
CreateInventoryAdjustmentPayload,
|
CreateInventoryAdjustmentPayload,
|
||||||
@@ -23,7 +22,7 @@ export const ProductWarehouseApi = new BaseApiService<
|
|||||||
export const MovementApi = new BaseApiService<
|
export const MovementApi = new BaseApiService<
|
||||||
Movement,
|
Movement,
|
||||||
CreateMovementPayload,
|
CreateMovementPayload,
|
||||||
UpdateMovementPayload
|
unknown
|
||||||
>('/inventory/transfers');
|
>('/inventory/transfers');
|
||||||
|
|
||||||
export const inventoryAdjustmentApi = new BaseApiService<
|
export const inventoryAdjustmentApi = new BaseApiService<
|
||||||
|
|||||||
-2
@@ -71,5 +71,3 @@ export type CreateMovementPayload = {
|
|||||||
}[];
|
}[];
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateMovementPayload = CreateMovementPayload;
|
|
||||||
|
|||||||
Reference in New Issue
Block a user