fix(resolve): fix resolve merge

This commit is contained in:
rstubryan
2025-11-03 10:12:12 +07:00
12 changed files with 600 additions and 502 deletions
+11 -16
View File
@@ -13,7 +13,6 @@
"axios": "^1.12.2", "axios": "^1.12.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"formik": "^2.4.6", "formik": "^2.4.6",
"inputmask": "^5.0.9",
"moment": "^2.30.1", "moment": "^2.30.1",
"next": "15.5.3", "next": "15.5.3",
"react": "19.1.0", "react": "19.1.0",
@@ -31,7 +30,6 @@
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
"@iconify/react": "^6.0.2", "@iconify/react": "^6.0.2",
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/inputmask": "^5.0.7",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
@@ -1639,13 +1637,6 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"node_modules/@types/inputmask": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/@types/inputmask/-/inputmask-5.0.7.tgz",
"integrity": "sha512-uojbVPWzBQ/n/0jc/d16fLqmGasFIptbrLD2WrCPWArlk+5PgblOqH4EDkI3AoobXLAlOK5yF01V8jMmvMG5qg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.15", "version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -1681,6 +1672,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@@ -1750,6 +1742,7 @@
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/scope-manager": "8.46.2",
"@typescript-eslint/types": "8.46.2", "@typescript-eslint/types": "8.46.2",
@@ -2267,6 +2260,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -2800,7 +2794,8 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/daisyui": { "node_modules/daisyui": {
"version": "5.3.10", "version": "5.3.10",
@@ -3228,6 +3223,7 @@
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -3401,6 +3397,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@rtsao/scc": "^1.1.0", "@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9", "array-includes": "^3.1.9",
@@ -4203,12 +4200,6 @@
"node": ">=0.8.19" "node": ">=0.8.19"
} }
}, },
"node_modules/inputmask": {
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/inputmask/-/inputmask-5.0.9.tgz",
"integrity": "sha512-s0lUfqcEbel+EQXtehXqwCJGShutgieOaIImFKC/r4reYNvX3foyrChl6LOEvaEgxEbesePIrw1Zi2jhZaDZbQ==",
"license": "MIT"
},
"node_modules/internal-slot": { "node_modules/internal-slot": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -5745,6 +5736,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -5754,6 +5746,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.26.0" "scheduler": "^0.26.0"
}, },
@@ -6552,6 +6545,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -6719,6 +6713,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
-2
View File
@@ -16,7 +16,6 @@
"axios": "^1.12.2", "axios": "^1.12.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"formik": "^2.4.6", "formik": "^2.4.6",
"inputmask": "^5.0.9",
"moment": "^2.30.1", "moment": "^2.30.1",
"next": "15.5.3", "next": "15.5.3",
"react": "19.1.0", "react": "19.1.0",
@@ -34,7 +33,6 @@
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
"@iconify/react": "^6.0.2", "@iconify/react": "^6.0.2",
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
"@types/inputmask": "^5.0.7",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
@@ -1,24 +1,46 @@
'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'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
const RowOptionsMenu = ({
type = 'dropdown',
props,
}: {
type: 'dropdown' | 'collapse';
props: CellContext<Movement, unknown>;
}) => (
<RowOptionsMenuWrapper type={type}>
<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>
</RowOptionsMenuWrapper>
);
const MovementTable = () => { const MovementTable = () => {
const { const {
@@ -28,30 +50,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 +99,179 @@ 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='collapse' 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 sm:flex-row justify-between items-end sm:items-center gap-2'>
href: '/inventory/movement/add', <div className='w-full flex flex-row gap-2'>
label: 'Tambah', <Button
href='/inventory/movement/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</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,
};
};
@@ -1,14 +1,18 @@
import * as Yup from 'yup'; import * as Yup from 'yup';
export const ProductCategoryFormSchema = Yup.object({ type ProductCategoryFormSchemaType = {
code: Yup.string() code: string;
.required('Kode wajib diisi!') name: string;
.max(3, 'Kode kategori produk melebihi 3 karakter!'), };
name: Yup.string().required('Nama wajib diisi!'),
}); 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;
export type ProductCategoryFormValues = Yup.InferType< export type ProductCategoryFormValues = Yup.InferType<typeof ProductCategoryFormSchema>;
typeof ProductCategoryFormSchema
>;
@@ -71,12 +71,13 @@ const ProductCategoryForm = ({
[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,
@@ -118,7 +119,7 @@ const ProductCategoryForm = ({
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');
}; };
@@ -129,7 +130,7 @@ const ProductCategoryForm = ({
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'
@@ -141,9 +142,9 @@ const ProductCategoryForm = ({
</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>
@@ -157,7 +158,7 @@ const ProductCategoryForm = ({
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}
@@ -169,7 +170,7 @@ const ProductCategoryForm = ({
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}
@@ -256,7 +257,7 @@ const ProductCategoryForm = ({
<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,82 @@
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() uom_id: number;
.required('Satuan wajib diisi!') product_category?: {
.typeError('Satuan wajib diisi!'), value: number;
product_category: Yup.object({ label: string;
value: Yup.number().min(1).required(), } | null;
label: Yup.string().required(), product_category_id: number;
}).nullable(), product_price: number | string;
product_category_id: Yup.number() selling_price: number | string;
.required('Kategori produk wajib diisi!') tax: number | string;
.typeError('Kategori produk wajib diisi!'), expiry_period: number | string;
product_price: Yup.number() supplier_ids: number[];
.required('Harga produk wajib diisi!') flags: string[];
.typeError('Harga produk wajib diisi!') };
.min(0, 'Harga produk tidak boleh kurang dari 0!'),
selling_price: Yup.number() export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
.required('Harga jual wajib diisi!') Yup.object({
.typeError('Harga jual wajib diisi!') name: Yup.string().required('Nama wajib diisi!'),
.min(0, 'Harga jual tidak boleh kurang dari 0!'), brand: Yup.string().required('Merek wajib diisi!'),
tax: Yup.number() sku: Yup.string().required('SKU wajib diisi!'),
.required('Pajak wajib diisi!')
.typeError('Pajak wajib diisi!') uom: Yup.object({
.min(0, 'Pajak tidak boleh kurang dari 0!') value: Yup.number().min(1).required(),
.max(100, 'Pajak tidak boleh lebih dari 100%!'), label: Yup.string().required(),
expiry_period: Yup.number() }).nullable().required('Satuan wajib diisi!'),
.required('Periode kadaluarsa wajib diisi!')
.typeError('Periode kadaluarsa wajib diisi!') uom_id: Yup.number()
.min(0, 'Periode kadaluarsa tidak boleh kurang dari 0!'), .required('Satuan wajib diisi!')
supplier: Yup.object({ .typeError('Satuan wajib diisi!'),
value: Yup.number().min(1).required(),
label: Yup.string().required(), product_category: Yup.object({
}).nullable(), value: Yup.number().min(1).required(),
supplier_ids: Yup.array() label: Yup.string().required(),
.of(Yup.number().typeError('Supplier tidak valid!')) }).nullable().required('Kategori produk wajib diisi!'),
.min(1, 'Minimal harus ada 1 supplier!')
.required('Supplier wajib diisi!'), product_category_id: Yup.number()
flags: Yup.array() .required('Kategori produk wajib diisi!')
.of(Yup.string()) .typeError('Kategori produk wajib diisi!'),
.min(1, 'Minimal harus ada 1 flag!')
.required('Flag wajib diisi!'), product_price: Yup.number()
}); .required('Harga produk wajib diisi!')
.typeError('Harga produk wajib diisi!')
.min(0, 'Harga produk tidak boleh kurang dari 0!'),
selling_price: Yup.number()
.required('Harga jual wajib diisi!')
.typeError('Harga jual wajib diisi!')
.min(0, 'Harga jual tidak boleh kurang dari 0!'),
tax: Yup.number()
.required('Pajak wajib diisi!')
.typeError('Pajak wajib diisi!')
.min(0, 'Pajak tidak boleh kurang dari 0!')
.max(100, 'Pajak tidak boleh lebih dari 100%!'),
expiry_period: Yup.number()
.required('Periode kadaluarsa wajib diisi!')
.typeError('Periode kadaluarsa wajib diisi!')
.min(0, 'Periode kadaluarsa tidak boleh kurang dari 0!'),
supplier_ids: Yup.array()
.of(Yup.number().required().typeError('Supplier tidak valid!'))
.min(1, 'Minimal harus ada 1 supplier!')
.required('Supplier wajib diisi!'),
flags: Yup.array()
.of(Yup.string().required())
.min(1, 'Minimal harus ada 1 flag!')
.required('Flag wajib diisi!'),
});
export const UpdateProductFormSchema = ProductFormSchema; export const UpdateProductFormSchema = 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';
@@ -79,20 +83,19 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
sku: initialValues?.sku ?? '', sku: initialValues?.sku ?? '',
uom: initialValues?.uom uom: initialValues?.uom
? { value: initialValues.uom.id, label: initialValues.uom.name } ? { value: initialValues.uom.id, label: initialValues.uom.name }
: null, : undefined,
uom_id: initialValues?.uom?.id ?? 0, uom_id: initialValues?.uom?.id ?? 0,
product_category: initialValues?.product_category product_category: initialValues?.product_category
? { ? {
value: initialValues.product_category.id, value: initialValues.product_category.id,
label: initialValues.product_category.name, label: initialValues.product_category.name,
} }
: null, : undefined,
product_category_id: initialValues?.product_category?.id ?? 0, product_category_id: initialValues?.product_category?.id ?? 0,
product_price: initialValues?.product_price ?? 0, product_price: initialValues?.product_price ?? '',
selling_price: initialValues?.selling_price ?? 0, selling_price: initialValues?.selling_price ?? '',
tax: initialValues?.tax ?? 0, tax: initialValues?.tax ?? '',
expiry_period: initialValues?.expiry_period ?? 0, expiry_period: initialValues?.expiry_period ?? '',
supplier: null, // not used for payload, just for UI
supplier_ids: initialValues?.suppliers?.map((s) => s.id) ?? [], supplier_ids: initialValues?.suppliers?.map((s) => s.id) ?? [],
flags: initialValues?.flags ?? [], flags: initialValues?.flags ?? [],
}), }),
@@ -111,14 +114,14 @@ 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( supplier_ids: values.supplier_ids.filter(
(id): id is number => typeof id === 'number' (id): id is number => typeof id === 'number'
), ),
flags: (values.flags ?? []).filter( flags: values.flags.filter(
(f): f is string => typeof f === 'string' (f): f is string => typeof f === 'string'
), ),
}; };
@@ -136,15 +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( options: uomOptions,
uomsUrl, isLoadingOptions: isLoadingUoms,
UomApi.getAllFetcher } = useSelect(UomApi.basePath, 'id', 'name');
);
const uomOptions = isResponseSuccess(uoms)
? uoms?.data.map((uom) => ({ value: uom.id, label: uom.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);
@@ -153,15 +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( options: categoryOptions,
categoriesUrl, isLoadingOptions: isLoadingCategories,
ProductCategoryApi.getAllFetcher } = useSelect(ProductCategoryApi.basePath, 'id', 'name');
);
const categoryOptions = isResponseSuccess(categories)
? categories?.data.map((cat) => ({ value: cat.id, label: cat.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);
@@ -169,7 +164,7 @@ 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( const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(
@@ -209,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'
@@ -235,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}
@@ -247,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}
@@ -259,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}
@@ -270,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}
@@ -283,6 +279,7 @@ 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}
@@ -296,15 +293,19 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
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}
decimalScale={2}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
inputPrefix='Rp '
isError={ isError={
formik.touched.product_price && formik.touched.product_price &&
Boolean(formik.errors.product_price) Boolean(formik.errors.product_price)
@@ -312,15 +313,19 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
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}
decimalScale={2}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
inputPrefix='Rp '
isError={ isError={
formik.touched.selling_price && formik.touched.selling_price &&
Boolean(formik.errors.selling_price) Boolean(formik.errors.selling_price)
@@ -328,28 +333,36 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
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}
decimalScale={0}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
inputSuffix='hari'
isError={ isError={
formik.touched.expiry_period && formik.touched.expiry_period &&
Boolean(formik.errors.expiry_period) Boolean(formik.errors.expiry_period)
@@ -360,9 +373,10 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
<SelectInput <SelectInput
required required
label='Supplier' label='Supplier'
placeholder='Pilih supplier...'
isMulti isMulti
value={supplierOptions.filter((opt) => value={supplierOptions.filter((opt) =>
formik.values.supplier_ids.includes(opt.value) (formik.values.supplier_ids || []).includes(opt.value)
)} )}
onChange={supplierChangeHandler} onChange={supplierChangeHandler}
options={supplierOptions} options={supplierOptions}
@@ -379,9 +393,10 @@ 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) => value={PRODUCT_FLAG_OPTIONS.filter((opt) =>
formik.values.flags.includes(opt.value) (formik.values.flags || []).includes(opt.value)
)} )}
onChange={(val) => { onChange={(val) => {
const arr = Array.isArray(val) ? val : val ? [val] : []; const arr = Array.isArray(val) ? val : val ? [val] : [];
+1 -2
View File
@@ -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
View File
@@ -71,5 +71,3 @@ export type CreateMovementPayload = {
}[]; }[];
}[]; }[];
}; };
export type UpdateMovementPayload = CreateMovementPayload;