feat(FE-328-329-330): Adding Feature Inventory Product Stocks

This commit is contained in:
randy-ar
2025-12-04 02:05:34 +07:00
parent 873a4b308d
commit 3e07316678
12 changed files with 780 additions and 41 deletions
-2
View File
@@ -140,7 +140,6 @@ deploy:dev:
environment: environment:
name: development name: development
url: https://dev-lti-erp.mbugroup.id url: https://dev-lti-erp.mbugroup.id
# ====== PRODUCTION ====== # ====== PRODUCTION ======
# build:production: # build:production:
# <<: *build_template # <<: *build_template
@@ -163,4 +162,3 @@ deploy:dev:
# environment: # environment:
# name: production # name: production
+50
View File
@@ -0,0 +1,50 @@
'use client';
import InventoryProductDetail from '@/components/pages/inventory/product/detail/InventoryProductDetail';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { InventoryProductApi } from '@/services/api/inventory';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
const InventoryProductDetailPage = () => {
const router = useRouter();
const searchParams = useSearchParams();
const inventoryProductId = searchParams.get('inventoryProductId');
const { data: inventoryProduct, isLoading: isLoadingInventoryProduct } =
useSWR(inventoryProductId, (id: number) =>
InventoryProductApi.getSingle(id)
);
if (!inventoryProductId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (
!isLoadingInventoryProduct &&
(!inventoryProduct || isResponseError(inventoryProduct))
) {
router.replace('/404');
return;
}
return (
<div className='size-full'>
{isLoadingInventoryProduct && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingInventoryProduct && isResponseSuccess(inventoryProduct) && (
<InventoryProductDetail inventoryProduct={inventoryProduct.data} />
)}
</div>
);
};
export default InventoryProductDetailPage;
+11
View File
@@ -0,0 +1,11 @@
import InventoryProductTable from '@/components/pages/inventory/product/InventoryProductTable';
const InventoryProductPage = () => {
return (
<div className='size-full'>
<InventoryProductTable />
</div>
);
};
export default InventoryProductPage;
+166 -33
View File
@@ -6,9 +6,147 @@ import useSWRImmutable from 'swr/immutable';
import { useAuth } from '@/services/hooks/useAuth'; import { useAuth } from '@/services/hooks/useAuth';
import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general'; import { GetMeResponse } from '@/types/api/api-general';
import { AxiosError } from 'axios';
// TODO: delete this later, DONT HARDCODE USER DATA
const DUMMY_USER = {
id: 1,
email: 'admin@mbugroup.id',
npk: '0001',
name: 'Super Admin',
image: null,
created_at: '2025-09-30T03:24:20.899229Z',
updated_at: '2025-09-30T03:24:20.899229Z',
roles: [
{
id: 1,
key: 'mbu.super_admin',
name: 'MBU Administrator',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
permissions: [
{
id: 1,
name: 'mbu:purchase:read',
action: 'read',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
},
{
id: 2,
name: 'mbu:purchase:create',
action: 'create',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
},
{
id: 3,
name: 'mbu:purchase:approve',
action: 'approve',
client: {
id: 1,
name: 'PT Mitra Berlian Unggas',
alias: 'MBU',
},
},
],
},
{
id: 2,
key: 'lti.super_admin',
name: 'LTI Administrator',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
permissions: [
{
id: 4,
name: 'lti:purchase:read',
action: 'read',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
},
{
id: 5,
name: 'lti:purchase:create',
action: 'create',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
},
{
id: 6,
name: 'lti:purchase:approve',
action: 'approve',
client: {
id: 2,
name: 'PT Lumbung Telur Indonesia',
alias: 'LTI',
},
},
],
},
{
id: 3,
key: 'manbu.super_admin',
name: 'MANBU Administrator',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
permissions: [
{
id: 7,
name: 'manbu:purchase:read',
action: 'read',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
},
{
id: 8,
name: 'manbu:purchase:create',
action: 'create',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
},
{
id: 9,
name: 'manbu:purchase:approve',
action: 'approve',
client: {
id: 3,
name: 'PT Mandiri Berlian Unggas',
alias: 'MANBU',
},
},
],
},
],
};
interface RequireAuthProps { interface RequireAuthProps {
children?: ReactNode; children?: ReactNode;
@@ -18,20 +156,17 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
const router = useRouter(); const router = useRouter();
const { setUser, setIsLoadingUser } = useAuth(); const { setUser, setIsLoadingUser } = useAuth();
const { const { data: userResponse, isLoading: isLoadingUserResponse } =
data: userResponse, useSWRImmutable<GetMeResponse & { ok?: boolean }, unknown, SWRHttpKey>(
isLoading: isLoadingUserResponse, '/auth/sso/userinfo',
error: userErrorResponse, httpClientFetcher,
} = useSWRImmutable< {
GetMeResponse & { ok?: boolean }, shouldRetryOnError: false,
AxiosError<BaseApiResponse>, revalidateOnFocus: false,
SWRHttpKey revalidateOnReconnect: false,
>('/sso/userinfo', httpClientFetcher, { refreshInterval: 0,
shouldRetryOnError: false, }
revalidateOnFocus: false, );
revalidateOnReconnect: false,
refreshInterval: 0,
});
useEffect(() => { useEffect(() => {
setIsLoadingUser(isLoadingUserResponse); setIsLoadingUser(isLoadingUserResponse);
@@ -40,25 +175,23 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
useEffect(() => { useEffect(() => {
if (isResponseSuccess(userResponse)) { if (isResponseSuccess(userResponse)) {
setUser(userResponse.data); setUser(userResponse.data);
} else if ( } else {
isResponseError(userErrorResponse?.response?.data) && // router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string);
typeof window !== 'undefined' // TODO: remove this later, DONT HARDCODE USER DATA
) { setUser(DUMMY_USER);
router.replace(
`${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}`
);
} }
}, [userResponse, userErrorResponse, setIsLoadingUser, setUser]); }, [userResponse, setIsLoadingUser, setUser]);
if (isLoadingUserResponse && !userResponse && !userErrorResponse) { // TODO: uncomment this later
return ( // if (isLoadingUserResponse && !userResponse) {
<div className='w-full flex flex-row justify-center items-center p-4'> // return (
<span className='loading loading-spinner loading-xl' /> // <div className='w-full flex flex-row justify-center items-center p-4'>
</div> // <span className='loading loading-spinner loading-xl' />
); // </div>
} // );
// }
return <>{isResponseSuccess(userResponse) && children}</>; return <>{children}</>;
}; };
export default RequireAuth; export default RequireAuth;
@@ -6,7 +6,7 @@ import Table from '@/components/Table';
import { ROWS_OPTIONS } from '@/config/constant'; import { ROWS_OPTIONS } from '@/config/constant';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import { inventoryAdjustmentApi } from '@/services/api/inventory'; import { InventoryAdjustmentApi } from '@/services/api/inventory';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { InventoryAdjustment } from '@/types/api/inventory/adjustment'; import { InventoryAdjustment } from '@/types/api/inventory/adjustment';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
@@ -41,8 +41,8 @@ const InventoryAdjustmentTable = () => {
// Fetch Data // Fetch Data
const { data: inventoryAdjustments, isLoading } = useSWR( const { data: inventoryAdjustments, isLoading } = useSWR(
`${inventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`, `${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
inventoryAdjustmentApi.getAllFetcher InventoryAdjustmentApi.getAllFetcher
); );
// State // State
@@ -1,7 +1,7 @@
'use client'; 'use client';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { inventoryAdjustmentApi } from '@/services/api/inventory'; import { InventoryAdjustmentApi } from '@/services/api/inventory';
import { import {
CreateInventoryAdjustmentPayload, CreateInventoryAdjustmentPayload,
InventoryAdjustment, InventoryAdjustment,
@@ -52,7 +52,7 @@ const InventoryAdjustmentForm = ({
const createInventoryAdjustmentHandler = useCallback( const createInventoryAdjustmentHandler = useCallback(
async (payload: CreateInventoryAdjustmentPayload) => { async (payload: CreateInventoryAdjustmentPayload) => {
const createInventoryAdjustmentRes = const createInventoryAdjustmentRes =
await inventoryAdjustmentApi.create(payload); await InventoryAdjustmentApi.create(payload);
if (isResponseError(createInventoryAdjustmentRes)) { if (isResponseError(createInventoryAdjustmentRes)) {
setInventoryAdjustmentFormErrorMessage( setInventoryAdjustmentFormErrorMessage(
@@ -0,0 +1,224 @@
'use client';
import Button from '@/components/Button';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import Table from '@/components/Table';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { ROWS_OPTIONS } from '@/config/constant';
import { isResponseSuccess } from '@/lib/api-helper';
import { cn, formatCurrency } from '@/lib/helper';
import { InventoryProductApi } from '@/services/api/inventory';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { InventoryProduct } from '@/types/api/inventory/product';
import { Icon } from '@iconify/react';
import {
CellContext,
ColumnDef,
Row,
SortingState,
} from '@tanstack/react-table';
import { ChangeEventHandler, useMemo, useState } from 'react';
import useSWR from 'swr';
const RowOptionsMenu = ({
type = 'dropdown',
props,
}: {
type: 'dropdown' | 'collapse';
props: CellContext<InventoryProduct, unknown>;
}) => (
<RowOptionsMenuWrapper type={type}>
<Button
href={`/inventory/product/detail?inventoryProductId=${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 InventoryProductTable = () => {
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
},
});
const [sorting, setSorting] = useState<SortingState>([]);
const { data: inventoryProducts, isLoading } = useSWR(
`${InventoryProductApi.basePath}${getTableFilterQueryString()}`,
InventoryProductApi.getAllFetcher
);
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
};
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
const newVal = val as OptionType;
setPageSize(newVal.value as number);
setPage(1);
};
const columns: ColumnDef<InventoryProduct>[] = useMemo(
() => [
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorKey: 'name',
header: 'Nama',
},
{
accessorKey: 'product_price',
header: 'Harga Beli',
cell: (props) => {
return props.row.original.product_price
? formatCurrency(props.row.original.product_price)
: '-';
},
},
{
accessorKey: 'selling_price',
header: 'Harga Jual',
cell: (props) => {
return props.row.original.selling_price
? formatCurrency(props.row.original.selling_price)
: '-';
},
},
{
accessorFn: (row) => row.product_category.name,
header: 'Kategori',
},
{
accessorFn: (row) => row.uom.name,
header: 'Satuan',
},
{
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 (
<>
<div className='w-full p-0 sm:p-4'>
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<div className='w-full flex flex-row gap-2'></div>
</div>
<div className='flex justify-between items-end gap-4'>
<DebouncedTextInput
name='search'
placeholder='Cari Produk'
value={tableFilterState.search}
onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }}
/>
<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<InventoryProduct>
data={
isResponseSuccess(inventoryProducts) ? inventoryProducts?.data : []
}
columns={columns}
pageSize={tableFilterState.pageSize}
page={
isResponseSuccess(inventoryProducts)
? inventoryProducts?.meta?.page
: 0
}
totalItems={
isResponseSuccess(inventoryProducts)
? inventoryProducts?.meta?.total_results
: 0
}
onPageChange={setPage}
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
className={{
containerClassName: cn({
'mb-20':
isResponseSuccess(inventoryProducts) &&
inventoryProducts?.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',
}}
/>
</div>
</>
);
};
export default InventoryProductTable;
@@ -0,0 +1,125 @@
import Card from '@/components/Card';
import { FormHeader } from '@/components/helper/form/FormHeader';
import StockLogTable from '@/components/pages/inventory/product/detail/StockLogTable';
import StockProductWarehouseTable from '@/components/pages/inventory/product/detail/StockProductWarehouseTable';
import { formatCurrency, formatNumber } from '@/lib/helper';
import { InventoryProduct } from '@/types/api/inventory/product';
import { useMemo } from 'react';
const InventoryProductDetail = ({
inventoryProduct,
refresh,
}: {
inventoryProduct?: InventoryProduct;
refresh?: () => void;
}) => {
const totalStok = useMemo(() => {
return (
inventoryProduct?.product_warehouses?.reduce(
(total, warehouse) => total + (warehouse.current_stock || 0),
0
) || 0
);
}, [inventoryProduct]);
const stockLogs = useMemo(() => {
return (
inventoryProduct?.product_warehouses?.flatMap(
(warehouse) => warehouse.stock_logs || []
) || []
);
}, [inventoryProduct]);
return (
<div className='flex flex-col gap-4 p-4'>
<FormHeader
title='Detail Persediaan Produk'
backUrl='/inventory/product'
/>
<Card
title='Informasi Produk'
variant='bordered'
className={{
wrapper: 'w-full mt-4',
}}
>
<div className='grid grid-cols-2 gap-4'>
<div className='overflow-x-auto rounded-box border border-base-content/5 bg-base-100 mt-3'>
<table className='table'>
<tbody>
<tr>
<td className='font-semibold'>SKU</td>
<td>:</td>
<td>{inventoryProduct?.sku}</td>
</tr>
<tr>
<td className='font-semibold'>Nama Produk</td>
<td>:</td>
<td>{inventoryProduct?.name}</td>
</tr>
<tr>
<td className='font-semibold'>Kategory</td>
<td>:</td>
<td>{inventoryProduct?.product_category.name}</td>
</tr>
<tr>
<td className='font-semibold'>Satuan</td>
<td>:</td>
<td>{inventoryProduct?.uom.name}</td>
</tr>
</tbody>
</table>
</div>
<div className='overflow-x-auto rounded-box border border-base-content/5 bg-base-100 mt-3'>
<table className='table'>
<tbody>
<tr>
<td className='font-semibold'>Harga Jual</td>
<td>:</td>
<td>
{inventoryProduct?.product_price
? formatCurrency(inventoryProduct.product_price)
: '-'}
</td>
</tr>
<tr>
<td className='font-semibold'>Harga Beli</td>
<td>:</td>
<td>
{inventoryProduct?.selling_price
? formatCurrency(inventoryProduct?.selling_price)
: '-'}
</td>
</tr>
<tr>
<td className='font-semibold'>Pajak</td>
<td>:</td>
<td>
{inventoryProduct?.tax
? formatCurrency(inventoryProduct?.tax)
: '-'}
</td>
</tr>
<tr>
<td className='font-semibold'>Total Stok</td>
<td>:</td>
<td>{formatNumber(totalStok)}</td>
</tr>
</tbody>
</table>
</div>
</div>
</Card>
<StockProductWarehouseTable
productWarehouseStock={inventoryProduct?.product_warehouses ?? []}
/>
<StockLogTable stockLogs={stockLogs} />
</div>
);
};
export default InventoryProductDetail;
@@ -0,0 +1,81 @@
import Card from '@/components/Card';
import Table from '@/components/Table';
import { formatDate, formatNumber, formatTitleCase } from '@/lib/helper';
import { StockLog } from '@/types/api/inventory/product';
const StockLogTable = ({ stockLogs }: { stockLogs: StockLog[] }) => {
return (
<Card
title='Informasi Stock Produk'
collapsible
variant='bordered'
className={{
wrapper: 'w-full',
}}
>
<Table<StockLog>
data={stockLogs}
columns={[
{
header: 'ID',
accessorKey: 'id',
},
{
header: 'Tanggal',
accessorKey: 'created_at',
cell: (props) => {
return formatDate(props.row.original.created_at, 'DD-MMM-yyyy');
},
},
{
header: 'Peningkatan',
accessorKey: 'increase',
cell: (props) => {
return formatNumber(props.row.original.increase);
},
},
{
header: 'Penurunan',
accessorKey: 'decrease',
cell: (props) => {
return formatNumber(props.row.original.decrease);
},
},
{
header: 'Jenis Transaksi',
accessorKey: 'loggable_type',
cell: (props) => {
return props.row.original.loggable_type
? formatTitleCase(props.row.original.loggable_type)
: '-';
},
},
{
header: 'Catatan',
accessorKey: 'notes',
cell: (props) => {
return props.row.original.notes ? props.row.original.notes : '-';
},
},
{
header: 'Oleh',
accessorKey: 'created_by',
},
]}
className={{
containerClassName: 'mt-6',
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',
}}
/>
</Card>
);
};
export default StockLogTable;
@@ -0,0 +1,65 @@
import Card from '@/components/Card';
import Table from '@/components/Table';
import { formatNumber } from '@/lib/helper';
import {
InventoryProduct,
ProductWarehouseStock,
} from '@/types/api/inventory/product';
const StockProductWarehouseTable = ({
productWarehouseStock,
}: {
productWarehouseStock?: ProductWarehouseStock[];
}) => {
return (
<Card
title='Informasi Gudang'
collapsible
variant='bordered'
className={{
wrapper: 'w-full',
}}
>
<Table<ProductWarehouseStock>
data={productWarehouseStock ?? []}
columns={[
{
header: 'Nama Gudang',
accessorKey: 'warehouse_name',
},
{
header: 'Lokasi',
accessorKey: 'location',
cell: (props) => {
return Boolean(props.row.original.location)
? props.row.original.location
: '-';
},
},
{
header: 'Stok',
accessorFn(row) {
return row.current_stock;
},
cell: (props) => {
return formatNumber(props.row.original.current_stock);
},
},
]}
className={{
containerClassName: 'mt-6',
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',
}}
/>
</Card>
);
};
export default StockProductWarehouseTable;
+8 -1
View File
@@ -12,6 +12,7 @@ import {
CreateInventoryAdjustmentPayload, CreateInventoryAdjustmentPayload,
InventoryAdjustment, InventoryAdjustment,
} from '@/types/api/inventory/adjustment'; } from '@/types/api/inventory/adjustment';
import { InventoryProduct } from '@/types/api/inventory/product';
export const ProductWarehouseApi = new BaseApiService< export const ProductWarehouseApi = new BaseApiService<
ProductWarehouse, ProductWarehouse,
@@ -25,8 +26,14 @@ export const MovementApi = new BaseApiService<
unknown unknown
>('/inventory/transfers'); >('/inventory/transfers');
export const inventoryAdjustmentApi = new BaseApiService< export const InventoryAdjustmentApi = new BaseApiService<
InventoryAdjustment, InventoryAdjustment,
CreateInventoryAdjustmentPayload, CreateInventoryAdjustmentPayload,
unknown unknown
>('/inventory/adjustments'); >('/inventory/adjustments');
export const InventoryProductApi = new BaseApiService<
InventoryProduct,
unknown,
unknown
>('/inventory/product-stocks');
+45
View File
@@ -0,0 +1,45 @@
import { BaseMetadata } from '@/types/api/api-general';
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
import { ProductCategory } from '@/types/api/master-data/product-category';
import { Supplier } from '@/types/api/master-data/supplier';
import { Uom } from '@/types/api/master-data/uom';
export type BaseInventoryProduct = {
id: number;
name: string;
brand: string;
sku: string;
product_price: number;
selling_price?: number;
tax?: number;
expiry_period?: number;
uom: Uom;
product_category: ProductCategory;
suppliers: Supplier[];
flags: string[];
product_warehouses?: ProductWarehouseStock[];
};
export type ProductWarehouseStock = {
id: number;
product_id: number;
warehouse_id: number;
warehouse_name: string;
location: Location | string;
current_stock: number;
stock_logs: StockLog[];
};
export type StockLog = {
id: number;
increase: number;
decrease: number;
loggable_type: string;
loggable_id: number;
notes: string;
product_warehouse_id: number;
created_by: number;
created_at: string;
};
export type InventoryProduct = BaseInventoryProduct & BaseMetadata;