diff --git a/.gitignore b/.gitignore index beb014a7..82965e2d 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,7 @@ yarn-error.log* next-env.d.ts # prettier -.prettierrc \ No newline at end of file +.prettierrc + +# idea +.idea diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..250df482 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,15 @@ +{ + "singleQuote": true, + "jsxSingleQuote": true, + "endOfLine": "lf", + "arrowParens": "always", + "bracketSpacing": true, + "embeddedLanguageFormatting": "auto", + "htmlWhitespaceSensitivity": "css", + "printWidth": 80, + "proseWrap": "preserve", + "quoteProps": "as-needed", + "semi": true, + "tabWidth": 2, + "trailingComma": "es5" +} diff --git a/src/app/master-data/area/detail/layout.tsx b/src/app/master-data/area/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/master-data/area/detail/layout.tsx @@ -0,0 +1,11 @@ +import SuspenseHelper from '@/components/helper/SuspenseHelper'; + +const Layout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return {children}; +}; + +export default Layout; diff --git a/src/app/master-data/bank/add/page.tsx b/src/app/master-data/bank/add/page.tsx new file mode 100644 index 00000000..0bb6e532 --- /dev/null +++ b/src/app/master-data/bank/add/page.tsx @@ -0,0 +1,11 @@ +import BankForm from '@/components/pages/master-data/bank/form/BankForm'; + +const AddBank = () => { + return ( +
+ +
+ ); +}; + +export default AddBank; diff --git a/src/app/master-data/bank/detail/edit/page.tsx b/src/app/master-data/bank/detail/edit/page.tsx new file mode 100644 index 00000000..a0939af9 --- /dev/null +++ b/src/app/master-data/bank/detail/edit/page.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import BankForm from '@/components/pages/master-data/bank/form/BankForm'; + +import { BankApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const BankEdit = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const bankId = searchParams.get('bankId'); + + const { data: bank, isLoading: isLoadingBank } = useSWR( + bankId, + (id: number) => BankApi.getSingle(id) + ); + + if (!bankId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingBank && (!bank || isResponseError(bank))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingBank && } + {!isLoadingBank && isResponseSuccess(bank) && ( + + )} +
+ ); +}; + +export default BankEdit; diff --git a/src/app/master-data/bank/detail/layout.tsx b/src/app/master-data/bank/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/master-data/bank/detail/layout.tsx @@ -0,0 +1,11 @@ +import SuspenseHelper from '@/components/helper/SuspenseHelper'; + +const Layout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return {children}; +}; + +export default Layout; diff --git a/src/app/master-data/bank/detail/page.tsx b/src/app/master-data/bank/detail/page.tsx new file mode 100644 index 00000000..bd1661d8 --- /dev/null +++ b/src/app/master-data/bank/detail/page.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import BankForm from '@/components/pages/master-data/bank/form/BankForm'; + +import { BankApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const BankDetail = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const bankId = searchParams.get('bankId'); + + const { data: bank, isLoading: isLoadingBank } = useSWR( + bankId, + (id: number) => BankApi.getSingle(id) + ); + + if (!bankId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingBank && (!bank || isResponseError(bank))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingBank && } + {!isLoadingBank && isResponseSuccess(bank) && ( + + )} +
+ ); +}; + +export default BankDetail; diff --git a/src/app/master-data/bank/page.tsx b/src/app/master-data/bank/page.tsx new file mode 100644 index 00000000..3f913c55 --- /dev/null +++ b/src/app/master-data/bank/page.tsx @@ -0,0 +1,11 @@ +import BanksTable from '@/components/pages/master-data/bank/BanksTable'; + +const Bank = () => { + return ( +
+ +
+ ); +}; + +export default Bank; diff --git a/src/app/master-data/fcr/add/page.tsx b/src/app/master-data/fcr/add/page.tsx new file mode 100644 index 00000000..9a74034d --- /dev/null +++ b/src/app/master-data/fcr/add/page.tsx @@ -0,0 +1,11 @@ +import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm'; + +const AddFcr = () => { + return ( +
+ +
+ ); +}; + +export default AddFcr; diff --git a/src/app/master-data/fcr/detail/edit/page.tsx b/src/app/master-data/fcr/detail/edit/page.tsx new file mode 100644 index 00000000..54277e8a --- /dev/null +++ b/src/app/master-data/fcr/detail/edit/page.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm'; + +import { FcrApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { FcrWithStandards } from '@/types/api/master-data/fcr'; + +const FcrEdit = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const fcrId = searchParams.get('fcrId'); + + const { data: fcr, isLoading: isLoadingFcr } = useSWR( + fcrId, + (id: number) => + FcrApi.getSingle(id) as Promise< + BaseApiResponse | undefined + > + ); + + if (!fcrId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingFcr && (!fcr || isResponseError(fcr))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingFcr && } + {!isLoadingFcr && isResponseSuccess(fcr) && ( + + )} +
+ ); +}; + +export default FcrEdit; diff --git a/src/app/master-data/fcr/detail/layout.tsx b/src/app/master-data/fcr/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/master-data/fcr/detail/layout.tsx @@ -0,0 +1,11 @@ +import SuspenseHelper from '@/components/helper/SuspenseHelper'; + +const Layout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return {children}; +}; + +export default Layout; diff --git a/src/app/master-data/fcr/detail/page.tsx b/src/app/master-data/fcr/detail/page.tsx new file mode 100644 index 00000000..5db1ab32 --- /dev/null +++ b/src/app/master-data/fcr/detail/page.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm'; + +import { FcrApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { FcrWithStandards } from '@/types/api/master-data/fcr'; +import { BaseApiResponse } from '@/types/api/api-general'; + +const FcrDetail = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const fcrId = searchParams.get('fcrId'); + + const { data: fcr, isLoading: isLoadingFcr } = useSWR( + fcrId, + (id: number) => + FcrApi.getSingle(id) as Promise< + BaseApiResponse | undefined + > + ); + + if (!fcrId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingFcr && (!fcr || isResponseError(fcr))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingFcr && } + {!isLoadingFcr && isResponseSuccess(fcr) && ( + + )} +
+ ); +}; + +export default FcrDetail; diff --git a/src/app/master-data/fcr/page.tsx b/src/app/master-data/fcr/page.tsx new file mode 100644 index 00000000..9ca9c55d --- /dev/null +++ b/src/app/master-data/fcr/page.tsx @@ -0,0 +1,11 @@ +import FcrsTable from '@/components/pages/master-data/fcr/FcrsTable'; + +const Fcr = () => { + return ( +
+ +
+ ); +}; + +export default Fcr; diff --git a/src/app/master-data/kandang/detail/layout.tsx b/src/app/master-data/kandang/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/master-data/kandang/detail/layout.tsx @@ -0,0 +1,11 @@ +import SuspenseHelper from '@/components/helper/SuspenseHelper'; + +const Layout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return {children}; +}; + +export default Layout; diff --git a/src/app/master-data/location/detail/layout.tsx b/src/app/master-data/location/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/master-data/location/detail/layout.tsx @@ -0,0 +1,11 @@ +import SuspenseHelper from '@/components/helper/SuspenseHelper'; + +const Layout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return {children}; +}; + +export default Layout; diff --git a/src/app/master-data/nonstock/detail/edit/page.tsx b/src/app/master-data/nonstock/detail/edit/page.tsx index a0fbb6b3..3b3db5f5 100644 --- a/src/app/master-data/nonstock/detail/edit/page.tsx +++ b/src/app/master-data/nonstock/detail/edit/page.tsx @@ -5,8 +5,8 @@ import useSWR from 'swr'; import NonstockForm from '@/components/pages/master-data/nonstock/form/NonstockForm'; -import { getNonstock } from '@/services/api/master-data/nonstock'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { NonstockApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; const NonstockEdit = () => { const router = useRouter(); @@ -16,7 +16,7 @@ const NonstockEdit = () => { const { data: nonstock, isLoading: isLoadingNonstock } = useSWR( nonstockId, - getNonstock + (id: number) => NonstockApi.getSingle(id) ); if (!nonstockId) { @@ -29,7 +29,7 @@ const NonstockEdit = () => { ); } - if (!isLoadingNonstock && !nonstock) { + if (!isLoadingNonstock && (!nonstock || isResponseError(nonstock))) { router.replace('/404'); return; } diff --git a/src/app/master-data/nonstock/detail/layout.tsx b/src/app/master-data/nonstock/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/master-data/nonstock/detail/layout.tsx @@ -0,0 +1,11 @@ +import SuspenseHelper from '@/components/helper/SuspenseHelper'; + +const Layout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return {children}; +}; + +export default Layout; diff --git a/src/app/master-data/nonstock/detail/page.tsx b/src/app/master-data/nonstock/detail/page.tsx index 375ec999..798a843e 100644 --- a/src/app/master-data/nonstock/detail/page.tsx +++ b/src/app/master-data/nonstock/detail/page.tsx @@ -5,8 +5,8 @@ import useSWR from 'swr'; import NonstockForm from '@/components/pages/master-data/nonstock/form/NonstockForm'; -import { getNonstock } from '@/services/api/master-data/nonstock'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { NonstockApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; const NonstockDetail = () => { const router = useRouter(); @@ -16,7 +16,7 @@ const NonstockDetail = () => { const { data: nonstock, isLoading: isLoadingNonstock } = useSWR( nonstockId, - getNonstock + (id: number) => NonstockApi.getSingle(id) ); if (!nonstockId) { @@ -29,7 +29,7 @@ const NonstockDetail = () => { ); } - if (!isLoadingNonstock && !nonstock) { + if (!isLoadingNonstock && (!nonstock || isResponseError(nonstock))) { router.replace('/404'); return; } diff --git a/src/app/master-data/product-category/add/page.tsx b/src/app/master-data/product-category/add/page.tsx new file mode 100644 index 00000000..0993ba7a --- /dev/null +++ b/src/app/master-data/product-category/add/page.tsx @@ -0,0 +1,11 @@ +import ProductCategoryForm from "@/components/pages/master-data/product-category/form/ProductCategoryForm"; + +const AddProductCategory = () => { + return ( +
+ +
+ ); +}; + +export default AddProductCategory; \ No newline at end of file diff --git a/src/app/master-data/product-category/detail/edit/page.tsx b/src/app/master-data/product-category/detail/edit/page.tsx new file mode 100644 index 00000000..6bc10644 --- /dev/null +++ b/src/app/master-data/product-category/detail/edit/page.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import ProductCategoryForm from '@/components/pages/master-data/product-category/form/ProductCategoryForm'; + +import { ProductCategoryApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const ProductCategoryEdit = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const productCategoryId = searchParams.get('productCategoryId'); + + const { data: productCategory, isLoading: isLoadingProductCategory } = useSWR( + productCategoryId, + (id: number) => ProductCategoryApi.getSingle(id) + ); + + if (!productCategoryId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingProductCategory && (!productCategory || isResponseError(productCategory))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingProductCategory && } + {!isLoadingProductCategory && isResponseSuccess(productCategory) && ( + + )} +
+ ); +} + +export default ProductCategoryEdit; \ No newline at end of file diff --git a/src/app/master-data/product-category/detail/layout.tsx b/src/app/master-data/product-category/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/master-data/product-category/detail/layout.tsx @@ -0,0 +1,11 @@ +import SuspenseHelper from '@/components/helper/SuspenseHelper'; + +const Layout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return {children}; +}; + +export default Layout; diff --git a/src/app/master-data/product-category/detail/page.tsx b/src/app/master-data/product-category/detail/page.tsx new file mode 100644 index 00000000..cba06fdb --- /dev/null +++ b/src/app/master-data/product-category/detail/page.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import ProductCategoryForm from '@/components/pages/master-data/product-category/form/ProductCategoryForm'; + +import { ProductCategoryApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const ProductCategoryDetail = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const productCategoryId = searchParams.get('productCategoryId'); + + const { data: productCategory, isLoading: isLoadingProductCategory } = useSWR( + productCategoryId, + (id: number) => ProductCategoryApi.getSingle(id) + ); + + if (!productCategoryId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingProductCategory && (!productCategory || isResponseError(productCategory))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingProductCategory && } + {!isLoadingProductCategory && isResponseSuccess(productCategory) && ( + + )} +
+ ); +}; + +export default ProductCategoryDetail; diff --git a/src/app/master-data/product-category/page.tsx b/src/app/master-data/product-category/page.tsx new file mode 100644 index 00000000..5ec6d555 --- /dev/null +++ b/src/app/master-data/product-category/page.tsx @@ -0,0 +1,11 @@ +import ProductCategoryTable from "@/components/pages/master-data/product-category/ProductCategoryTable"; + +const ProductCategory = () => { + return ( +
+ +
+ ); +}; + +export default ProductCategory; \ No newline at end of file diff --git a/src/app/master-data/product/add/page.tsx b/src/app/master-data/product/add/page.tsx new file mode 100644 index 00000000..7cc995b6 --- /dev/null +++ b/src/app/master-data/product/add/page.tsx @@ -0,0 +1,11 @@ +import ProductForm from '@/components/pages/master-data/product/form/ProductForm'; + +const AddProduct = () => { + return ( +
+ +
+ ); +}; + +export default AddProduct; \ No newline at end of file diff --git a/src/app/master-data/product/detail/edit/page.tsx b/src/app/master-data/product/detail/edit/page.tsx new file mode 100644 index 00000000..96cfdc42 --- /dev/null +++ b/src/app/master-data/product/detail/edit/page.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import ProductForm from '@/components/pages/master-data/product/form/ProductForm'; +import { ProductApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const ProductEdit = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const productId = searchParams.get('productId'); + + const { data: product, isLoading } = useSWR( + productId, + (id: number) => ProductApi.getSingle(id) + ); + + if (!productId) { + router.back(); + return ( +
+ +
+ ); + } + + if (!isLoading && (!product || isResponseError(product))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoading && } + {!isLoading && isResponseSuccess(product) && ( + + )} +
+ ); +}; + +export default ProductEdit; \ No newline at end of file diff --git a/src/app/master-data/product/detail/layout.tsx b/src/app/master-data/product/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/master-data/product/detail/layout.tsx @@ -0,0 +1,11 @@ +import SuspenseHelper from '@/components/helper/SuspenseHelper'; + +const Layout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return {children}; +}; + +export default Layout; diff --git a/src/app/master-data/product/detail/page.tsx b/src/app/master-data/product/detail/page.tsx new file mode 100644 index 00000000..916a44d0 --- /dev/null +++ b/src/app/master-data/product/detail/page.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import ProductForm from '@/components/pages/master-data/product/form/ProductForm'; +import { ProductApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const ProductDetail = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const productId = searchParams.get('productId'); + + const { data: product, isLoading } = useSWR( + productId, + (id: number) => ProductApi.getSingle(id) + ); + + if (!productId) { + router.back(); + return ( +
+ +
+ ); + } + + if (!isLoading && (!product || isResponseError(product))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoading && } + {!isLoading && isResponseSuccess(product) && ( + + )} +
+ ); +}; + +export default ProductDetail; \ No newline at end of file diff --git a/src/app/master-data/product/page.tsx b/src/app/master-data/product/page.tsx new file mode 100644 index 00000000..6014aeb9 --- /dev/null +++ b/src/app/master-data/product/page.tsx @@ -0,0 +1,11 @@ +import ProductsTable from "@/components/pages/master-data/product/ProductTable"; + +const Product = () => { + return ( +
+ +
+ ); +}; + +export default Product; \ No newline at end of file diff --git a/src/app/master-data/uom/detail/layout.tsx b/src/app/master-data/uom/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/master-data/uom/detail/layout.tsx @@ -0,0 +1,11 @@ +import SuspenseHelper from '@/components/helper/SuspenseHelper'; + +const Layout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return {children}; +}; + +export default Layout; diff --git a/src/app/master-data/warehouse/detail/layout.tsx b/src/app/master-data/warehouse/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/master-data/warehouse/detail/layout.tsx @@ -0,0 +1,11 @@ +import SuspenseHelper from '@/components/helper/SuspenseHelper'; + +const Layout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return {children}; +}; + +export default Layout; diff --git a/src/components/MainDrawer.tsx b/src/components/MainDrawer.tsx index 6a5e6f38..309cddf2 100644 --- a/src/components/MainDrawer.tsx +++ b/src/components/MainDrawer.tsx @@ -199,7 +199,7 @@ const MainDrawer = ({ if (!hasSubmenu) return; - const activeSubmenu = menu.submenu.find((item) => + const activeSubmenu = menu.submenu?.find((item) => isPathActive(pathname, item.link) ); diff --git a/src/components/helper/SuspenseHelper.tsx b/src/components/helper/SuspenseHelper.tsx new file mode 100644 index 00000000..a151cd9d --- /dev/null +++ b/src/components/helper/SuspenseHelper.tsx @@ -0,0 +1,23 @@ +'use client'; + +import { Suspense } from 'react'; + +const SuspenseHelper = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return ( + + + + } + > + {children} + + ); +}; + +export default SuspenseHelper; diff --git a/src/components/input/TextInput.tsx b/src/components/input/TextInput.tsx index 4a1cecd2..eec312c1 100644 --- a/src/components/input/TextInput.tsx +++ b/src/components/input/TextInput.tsx @@ -122,7 +122,9 @@ const TextInput = ({ {!isError && bottomLabel && (

{bottomLabel}

)} - {isError &&

{errorMessage}

} + {isError && errorMessage && ( +

{errorMessage}

+ )} ); }; diff --git a/src/components/pages/master-data/bank/BanksTable.tsx b/src/components/pages/master-data/bank/BanksTable.tsx new file mode 100644 index 00000000..0d084491 --- /dev/null +++ b/src/components/pages/master-data/bank/BanksTable.tsx @@ -0,0 +1,289 @@ +'use client'; + +import { ChangeEventHandler, useEffect, useState } from 'react'; +import useSWR from 'swr'; +import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; +import toast from 'react-hot-toast'; + +import { Icon } from '@iconify/react'; +import Table from '@/components/Table'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import Button from '@/components/Button'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import RowDropdownOptions from '@/components/table/RowDropdownOptions'; +import RowCollapseOptions from '@/components/table/RowCollapseOptions'; + +import { Bank } from '@/types/api/master-data/bank'; +import { BankApi } from '@/services/api/master-data'; +import { cn } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { ROWS_OPTIONS } from '@/config/constant'; + +const RowOptionsMenu = ({ + type = 'dropdown', + props, + deleteClickHandler, +}: { + type: 'dropdown' | 'collapse'; + props: CellContext; + deleteClickHandler: () => void; +}) => { + return ( +
+ + + + + +
+ ); +}; + +const BanksTable = () => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { search: '', nameSort: '' }, + paramMap: { page: 'page', pageSize: 'limit', nameSort: 'sort_name' }, + }); + + const { + data: banks, + isLoading, + mutate: refreshBanks, + } = useSWR( + `${BankApi.basePath}${getTableFilterQueryString()}`, + BankApi.getAllFetcher + ); + + const deleteModal = useModal(); + + const [selectedBank, setSelectedBank] = useState(undefined); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const [sorting, setSorting] = useState([]); + + const banksColumns: ColumnDef[] = [ + { + header: '#', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + accessorKey: 'alias', + header: 'Alias', + }, + { + accessorKey: 'account_number', + header: 'No. Rekening', + }, + { + accessorKey: 'owner', + header: 'Pemilik', + cell: (props) => (props.getValue() ? props.getValue() : '-'), + }, + { + 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 = () => { + setSelectedBank(props.row.original); + deleteModal.openModal(); + }; + + return ( + <> + {currentPageSize > 2 && ( + + + + )} + + {currentPageSize <= 2 && ( + + + + )} + + ); + }, + }, + ]; + + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + + await BankApi.delete(selectedBank?.id as number); + refreshBanks(); + + deleteModal.closeModal(); + toast.success('Successfully delete Bank!'); + setIsDeleteLoading(false); + }; + + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; + + const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { + const newVal = val as OptionType; + + setPageSize(newVal.value as number); + }; + + // track sorting + useEffect(() => { + const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); + + if (!isNameSorted) { + updateFilter('nameSort', ''); + } else { + updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); + } + }, [sorting]); + + return ( + <> +
+
+
+
+ +
+ + +
+ +
+ +
+
+ + + data={isResponseSuccess(banks) ? banks?.data : []} + columns={banksColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(banks) ? banks?.meta?.page : 0} + totalItems={isResponseSuccess(banks) ? banks?.meta?.total_results : 0} + onPageChange={setPage} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn({ + 'mb-20': isResponseSuccess(banks) && banks?.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', + }} + /> +
+ + + + ); +}; + +export default BanksTable; diff --git a/src/components/pages/master-data/bank/form/BankForm.schema.ts b/src/components/pages/master-data/bank/form/BankForm.schema.ts new file mode 100644 index 00000000..0bf48c76 --- /dev/null +++ b/src/components/pages/master-data/bank/form/BankForm.schema.ts @@ -0,0 +1,14 @@ +import * as Yup from 'yup'; + +export const BankFormSchema = Yup.object({ + name: Yup.string().required('Nama wajib diisi!'), + alias: Yup.string() + .max(5, 'Maksimal 5 karakter!') + .required('Alias wajib diisi!'), + account_number: Yup.string().required('Rekening wajib diisi!'), + owner: Yup.string(), +}); + +export const UpdateBankFormSchema = BankFormSchema; + +export type BankFormValues = Yup.InferType; diff --git a/src/components/pages/master-data/bank/form/BankForm.tsx b/src/components/pages/master-data/bank/form/BankForm.tsx new file mode 100644 index 00000000..442d5c76 --- /dev/null +++ b/src/components/pages/master-data/bank/form/BankForm.tsx @@ -0,0 +1,301 @@ +'use client'; + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { useFormik } from 'formik'; +import { toast } from 'react-hot-toast'; + +import { Icon } from '@iconify/react'; +import Button from '@/components/Button'; +import TextInput from '@/components/input/TextInput'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; + +import { + BankFormSchema, + BankFormValues, + UpdateBankFormSchema, +} from '@/components/pages/master-data/bank/form/BankForm.schema'; +import { isResponseError } from '@/lib/api-helper'; +import { + CreateBankPayload, + Bank, + UpdateBankPayload, +} from '@/types/api/master-data/bank'; +import { BankApi } from '@/services/api/master-data'; +import { cn } from '@/lib/helper'; + +interface BankFormProps { + type?: 'add' | 'edit' | 'detail'; + initialValues?: Bank; +} + +const BankForm = ({ type = 'add', initialValues }: BankFormProps) => { + const router = useRouter(); + const deleteModal = useModal(); + + const [bankFormErrorMessage, setBankFormErrorMessage] = useState(''); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const createBankHandler = useCallback( + async (payload: CreateBankPayload) => { + const createBankRes = await BankApi.create(payload); + + if (isResponseError(createBankRes)) { + setBankFormErrorMessage(createBankRes.message); + return; + } + + toast.success(createBankRes?.message as string); + router.push('/master-data/bank'); + }, + [router] + ); + + const updateBankHandler = useCallback( + async (bankId: number, payload: UpdateBankPayload) => { + const updateBankRes = await BankApi.update(bankId, payload); + + if (updateBankRes?.status === 'error') { + setBankFormErrorMessage(updateBankRes.message); + return; + } + + toast.success(updateBankRes?.message as string); + router.refresh(); + router.push('/master-data/bank'); + }, + [router] + ); + + const formikInitialValues = useMemo(() => { + return { + name: initialValues?.name ?? '', + alias: initialValues?.alias ?? '', + account_number: initialValues?.account_number ?? '', + owner: initialValues?.owner, + }; + }, [initialValues]); + + const formik = useFormik({ + initialValues: formikInitialValues, + validationSchema: type === 'edit' ? UpdateBankFormSchema : BankFormSchema, + onSubmit: async (values) => { + setBankFormErrorMessage(''); + + const bankPayload: CreateBankPayload = { + name: values.name, + alias: values.alias, + account_number: values.account_number.toString(), + owner: values.owner ? values.owner : '', + }; + + switch (type) { + case 'add': + await createBankHandler(bankPayload); + break; + + case 'edit': + await updateBankHandler(initialValues?.id as number, bankPayload); + break; + } + }, + }); + + const { setValues: formikSetValues } = formik; + + const deleteBankClickHandler = () => { + deleteModal.openModal(); + }; + + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + + await BankApi.delete(initialValues?.id as number); + + deleteModal.closeModal(); + toast.success('Successfully delete Bank!'); + setIsDeleteLoading(false); + router.push('/master-data/bank'); + }; + + useEffect(() => { + formikSetValues(formikInitialValues); + }, [formikSetValues, formikInitialValues]); + + return ( + <> +
+
+ + +

+ {type === 'add' && 'Tambah Bank'} + {type === 'edit' && 'Edit Bank'} + {type === 'detail' && 'Detail Bank'} +

+
+ +
+
+ + + + + + + +
+ +
+ {type !== 'add' && ( +
+ + + {type !== 'edit' && ( + + )} +
+ )} + + {type !== 'detail' && ( +
+ + + +
+ )} +
+ + {bankFormErrorMessage && ( +
+ + {bankFormErrorMessage} +
+ )} +
+
+ + {type !== 'add' && ( + + )} + + ); +}; + +export default BankForm; diff --git a/src/components/pages/master-data/fcr/FcrsTable.tsx b/src/components/pages/master-data/fcr/FcrsTable.tsx new file mode 100644 index 00000000..5f0285bb --- /dev/null +++ b/src/components/pages/master-data/fcr/FcrsTable.tsx @@ -0,0 +1,276 @@ +'use client'; + +import { ChangeEventHandler, useEffect, useState } from 'react'; +import useSWR from 'swr'; +import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; +import toast from 'react-hot-toast'; + +import { Icon } from '@iconify/react'; +import Table from '@/components/Table'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import Button from '@/components/Button'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import RowDropdownOptions from '@/components/table/RowDropdownOptions'; +import RowCollapseOptions from '@/components/table/RowCollapseOptions'; + +import { Fcr } from '@/types/api/master-data/fcr'; +import { FcrApi } from '@/services/api/master-data'; +import { cn } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { ROWS_OPTIONS } from '@/config/constant'; + +const RowOptionsMenu = ({ + type = 'dropdown', + props, + deleteClickHandler, +}: { + type: 'dropdown' | 'collapse'; + props: CellContext; + deleteClickHandler: () => void; +}) => { + return ( +
+ + + + + +
+ ); +}; + +const FcrsTable = () => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { search: '', nameSort: '' }, + paramMap: { page: 'page', pageSize: 'limit', nameSort: 'sort_name' }, + }); + + const { + data: fcrs, + isLoading, + mutate: refreshFcrs, + } = useSWR( + `${FcrApi.basePath}${getTableFilterQueryString()}`, + FcrApi.getAllFetcher + ); + + const deleteModal = useModal(); + + const [selectedFcr, setSelectedFcr] = useState(undefined); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const [sorting, setSorting] = useState([]); + + const fcrsColumns: ColumnDef[] = [ + { + header: '#', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + 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 = () => { + setSelectedFcr(props.row.original); + deleteModal.openModal(); + }; + + return ( + <> + {currentPageSize > 2 && ( + + + + )} + + {currentPageSize <= 2 && ( + + + + )} + + ); + }, + }, + ]; + + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + + await FcrApi.delete(selectedFcr?.id as number); + refreshFcrs(); + + deleteModal.closeModal(); + toast.success('Successfully delete FCR!'); + setIsDeleteLoading(false); + }; + + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; + + const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { + const newVal = val as OptionType; + + setPageSize(newVal.value as number); + }; + + // track sorting + useEffect(() => { + const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); + + if (!isNameSorted) { + updateFilter('nameSort', ''); + } else { + updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); + } + }, [sorting]); + + return ( + <> +
+
+
+
+ +
+ + +
+ +
+ +
+
+ + + data={isResponseSuccess(fcrs) ? fcrs?.data : []} + columns={fcrsColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(fcrs) ? fcrs?.meta?.page : 0} + totalItems={isResponseSuccess(fcrs) ? fcrs?.meta?.total_results : 0} + onPageChange={setPage} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn({ + 'mb-20': isResponseSuccess(fcrs) && fcrs?.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', + }} + /> +
+ + + + ); +}; + +export default FcrsTable; diff --git a/src/components/pages/master-data/fcr/form/FcrForm.schema.ts b/src/components/pages/master-data/fcr/form/FcrForm.schema.ts new file mode 100644 index 00000000..21b0b9ee --- /dev/null +++ b/src/components/pages/master-data/fcr/form/FcrForm.schema.ts @@ -0,0 +1,26 @@ +import * as Yup from 'yup'; + +const FcrStandardSchema: Yup.ObjectSchema<{ + weight: number | string; + fcr_number: number | string; + mortality: number | string; +}> = Yup.object({ + weight: Yup.number().nullable().required('Bobot wajib diisi!'), + fcr_number: Yup.number() + .nullable() + .typeError('FCR harus angka!') + .required('FCR harus diisi!'), + mortality: Yup.number().nullable().required('Mortalitas wajib diisi!'), +}); + +export const FcrFormSchema = Yup.object({ + name: Yup.string().required('Nama wajib diisi!'), + fcrStandards: Yup.array() + .of(FcrStandardSchema) + .min(1, 'Minimal 1 FCR Standard diisi1') + .required('FCR wajib diisi!'), +}); + +export const UpdateFcrFormSchema = FcrFormSchema; + +export type FcrFormValues = Yup.InferType; diff --git a/src/components/pages/master-data/fcr/form/FcrForm.tsx b/src/components/pages/master-data/fcr/form/FcrForm.tsx new file mode 100644 index 00000000..ab17eda8 --- /dev/null +++ b/src/components/pages/master-data/fcr/form/FcrForm.tsx @@ -0,0 +1,389 @@ +'use client'; + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { useFormik } from 'formik'; +import { toast } from 'react-hot-toast'; + +import { Icon } from '@iconify/react'; +import Button from '@/components/Button'; +import TextInput from '@/components/input/TextInput'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; + +import { + FcrFormSchema, + FcrFormValues, + UpdateFcrFormSchema, +} from '@/components/pages/master-data/fcr/form/FcrForm.schema'; +import { isResponseError } from '@/lib/api-helper'; +import { + CreateFcrPayload, + Fcr, + FcrWithStandards, + UpdateFcrPayload, +} from '@/types/api/master-data/fcr'; +import { FcrApi } from '@/services/api/master-data'; +import { cn } from '@/lib/helper'; + +interface FcrFormProps { + type?: 'add' | 'edit' | 'detail'; + initialValues?: FcrWithStandards; +} + +const FcrForm = ({ type = 'add', initialValues }: FcrFormProps) => { + const router = useRouter(); + const deleteModal = useModal(); + + const [fcrFormErrorMessage, setFcrFormErrorMessage] = useState(''); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const createFcrHandler = useCallback( + async (payload: CreateFcrPayload) => { + const createFcrRes = await FcrApi.create(payload); + + if (isResponseError(createFcrRes)) { + setFcrFormErrorMessage(createFcrRes.message); + return; + } + + toast.success(createFcrRes?.message as string); + router.push('/master-data/fcr'); + }, + [router] + ); + + const updateFcrHandler = useCallback( + async (fcrId: number, payload: UpdateFcrPayload) => { + const updateFcrRes = await FcrApi.update(fcrId, payload); + + if (updateFcrRes?.status === 'error') { + setFcrFormErrorMessage(updateFcrRes.message); + return; + } + + toast.success(updateFcrRes?.message as string); + router.refresh(); + router.push('/master-data/fcr'); + }, + [router] + ); + + const formikInitialValues = useMemo(() => { + return { + name: initialValues?.name ?? '', + fcrStandards: initialValues?.fcr_standards + ? initialValues?.fcr_standards + : [ + { + weight: '', + fcr_number: '', + mortality: '', + }, + ], + }; + }, [initialValues]); + + const formik = useFormik({ + initialValues: formikInitialValues, + validationSchema: type === 'edit' ? UpdateFcrFormSchema : FcrFormSchema, + onSubmit: async (values) => { + setFcrFormErrorMessage(''); + + const fcrPayload: CreateFcrPayload = { + name: values.name, + fcr_standards: values.fcrStandards as CreateFcrPayload['fcr_standards'], + }; + + switch (type) { + case 'add': + await createFcrHandler(fcrPayload); + break; + + case 'edit': + await updateFcrHandler(initialValues?.id as number, fcrPayload); + break; + } + }, + }); + + const { setValues: formikSetValues } = formik; + + const addFcrStandard = () => + formik.setFieldValue('fcrStandards', [ + ...formik.values.fcrStandards, + { + weight: '', + fcr_number: '', + mortality: '', + }, + ]); + + const removeFcrStandard = (i: number) => + formik.setFieldValue( + 'fcrStandards', + formik.values.fcrStandards.filter((_, idx) => idx !== i) + ); + + const deleteFcrClickHandler = () => { + deleteModal.openModal(); + }; + + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + + await FcrApi.delete(initialValues?.id as number); + + deleteModal.closeModal(); + toast.success('Successfully delete FCR!'); + setIsDeleteLoading(false); + router.push('/master-data/fcr'); + }; + + const isRepeaterInputError = ( + column: keyof CreateFcrPayload['fcr_standards'][0], + idx: number + ) => { + return ( + formik.touched.fcrStandards?.[idx]?.[column] && + Boolean( + formik.errors.fcrStandards?.[idx] instanceof Object && + formik.errors.fcrStandards?.[idx]?.[column] + ) + ); + }; + + useEffect(() => { + formikSetValues(formikInitialValues); + }, [formikSetValues, formikInitialValues]); + + return ( + <> +
+
+ + +

+ {type === 'add' && 'Tambah FCR'} + {type === 'edit' && 'Edit FCR'} + {type === 'detail' && 'Detail FCR'} +

+
+ +
+
+ + +
+
+ + + + + + + {type !== 'detail' && } + + + + + {formik.values.fcrStandards.map((fcrStandard, idx) => ( + + + + + {type !== 'detail' && ( + + )} + + ))} + +
BobotFCRMortalitasAksi
+ + + + + + + +
+
+
+ + {type !== 'detail' && ( + + )} +
+ +
+ {type !== 'add' && ( +
+ + + {type !== 'edit' && ( + + )} +
+ )} + + {type !== 'detail' && ( +
+ + + +
+ )} +
+ + {fcrFormErrorMessage && ( +
+ + {fcrFormErrorMessage} +
+ )} +
+
+ + {type !== 'add' && ( + + )} + + ); +}; + +export default FcrForm; diff --git a/src/components/pages/master-data/nonstock/NonstocksTable.tsx b/src/components/pages/master-data/nonstock/NonstocksTable.tsx index 4a2dbc5b..462b3488 100644 --- a/src/components/pages/master-data/nonstock/NonstocksTable.tsx +++ b/src/components/pages/master-data/nonstock/NonstocksTable.tsx @@ -1,20 +1,31 @@ 'use client'; -import { ChangeEventHandler, useState } from 'react'; +import { ChangeEventHandler, useCallback, useEffect, useState } from 'react'; import useSWR from 'swr'; -import { CellContext, ColumnDef } from '@tanstack/react-table'; +import { + CellContext, + ColumnDef, + ColumnSort, + SortingState, +} from '@tanstack/react-table'; +import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; import Table from '@/components/Table'; -import TextInput from '@/components/input/TextInput'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Button from '@/components/Button'; -import Collapse from '@/components/Collapse'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import RowDropdownOptions from '@/components/table/RowDropdownOptions'; +import RowCollapseOptions from '@/components/table/RowCollapseOptions'; -import { httpClientFetcher } from '@/services/http/client'; -import { Nonstock, NonstocksResponse } from '@/types/api/master-data/nonstock'; +import { Nonstock } from '@/types/api/master-data/nonstock'; +import { NonstockApi } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; -import { deleteNonstock } from '@/services/api/master-data/nonstock'; import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { ROWS_OPTIONS } from '@/config/constant'; const RowOptionsMenu = ({ type = 'dropdown', @@ -23,7 +34,7 @@ const RowOptionsMenu = ({ }: { type: 'dropdown' | 'collapse'; props: CellContext; - deleteClickHandler: () => Promise; + deleteClickHandler: () => void; }) => { return (
; - isLast2Rows: boolean; - deleteClickHandler: () => Promise; -}) => { - return ( -
- - - -
- ); -}; - -const RowCollapseOptions = ({ - props, - deleteClickHandler, -}: { - props: CellContext; - deleteClickHandler: () => Promise; -}) => { - return ( - - - - } - className='w-fit' - titleClassName='p-0! justify-self-end' - > - - - ); -}; - const NonstocksTable = () => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { search: '', nameSort: '', locationSort: '', picSort: '' }, + paramMap: { + page: 'page', + pageSize: 'limit', + nameSort: 'sort_name', + locationSort: 'sort_location', + picSort: ' sort_pic', + }, + }); + const { data: nonstocks, isLoading, mutate: refreshNonstocks, - } = useSWR('/master-data/nonstocks', httpClientFetcher); + } = useSWR( + `${NonstockApi.basePath}${getTableFilterQueryString()}`, + NonstockApi.getAllFetcher + ); - const [searchValue, setSearchValue] = useState(''); + const deleteModal = useModal(); + + const [selectedNonstock, setSelectedNonstock] = useState< + Nonstock | undefined + >(undefined); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const [sorting, setSorting] = useState([]); const nonstocksColumns: ColumnDef[] = [ { header: '#', - cell: (props) => props.row.index + 1, + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, }, { - header: 'Nama', accessorKey: 'name', + header: 'Nama', + }, + { + accessorKey: 'uom', + header: 'UOM', + cell: (props) => props.row.original.uom.name, + }, + { + accessorKey: 'suppliers', + header: 'Supplier', + cell: (props) => { + const supplierNames = props.row.original.suppliers.map( + (supplier) => supplier.name + ); + + return supplierNames.join(', ') || '-'; + }, + }, + { + accessorKey: 'flags', + header: 'Flag', + cell: (props) => props.row.original.flags?.join(', ') || '-', }, { header: 'Aksi', @@ -157,33 +164,31 @@ const NonstocksTable = () => { const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; - const deleteClickHandler = async () => { - const confirmation = confirm( - 'Apakah anda yakin untuk menghapus non stock ini?' - ); - - if (confirmation) { - await deleteNonstock(props.row.original.id); - refreshNonstocks(); - alert('Nonstock berhasil dihapus!'); - } + const deleteClickHandler = () => { + setSelectedNonstock(props.row.original); + deleteModal.openModal(); }; return ( <> {currentPageSize > 2 && ( - + + + )} {currentPageSize <= 2 && ( - + + + )} ); @@ -191,52 +196,133 @@ const NonstocksTable = () => { }, ]; - const searchChangeHandler: ChangeEventHandler = (e) => { - setSearchValue(e.target.value); + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + + await NonstockApi.delete(selectedNonstock?.id as number); + refreshNonstocks(); + + deleteModal.closeModal(); + toast.success('Successfully delete Nonstock!'); + setIsDeleteLoading(false); }; + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; + + const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { + const newVal = val as OptionType; + + setPageSize(newVal.value as number); + }; + + const updateSortingFilter = useCallback( + ( + sortName: Exclude, + sortFilter: ColumnSort | undefined + ) => { + if (!sortFilter) { + updateFilter(sortName, ''); + } else { + updateFilter(sortName, sortFilter.desc ? 'desc' : 'asc'); + } + }, + [updateFilter] + ); + + // track sorting + useEffect(() => { + const nameSortFilter = sorting.find((sortItem) => sortItem.id === 'name'); + const locationSortFilter = sorting.find( + (sortItem) => sortItem.id === 'location' + ); + const picSortFilter = sorting.find((sortItem) => sortItem.id === 'pic'); + + updateSortingFilter('nameSort', nameSortFilter); + updateSortingFilter('locationSort', locationSortFilter); + updateSortingFilter('picSort', picSortFilter); + }, [sorting]); + return ( -
-
-
- + <> +
+
+
+
+ +
+ + +
+ +
+ +
- + data={isResponseSuccess(nonstocks) ? nonstocks?.data : []} + columns={nonstocksColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(nonstocks) ? nonstocks?.meta?.page : 0} + totalItems={ + isResponseSuccess(nonstocks) ? nonstocks?.meta?.total_results : 0 + } + onPageChange={setPage} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn({ + 'mb-20': + isResponseSuccess(nonstocks) && nonstocks?.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', + }} />
- - data={isResponseSuccess(nonstocks) ? nonstocks?.data : []} - columns={nonstocksColumns} - pageSize={10} - fuzzySearchValue={searchValue} - onFuzzySearchValueChange={setSearchValue} - isLoading={isLoading} - className={{ - containerClassName: cn({ - 'mb-20': - isResponseSuccess(nonstocks) && nonstocks?.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', + -
+ ); }; diff --git a/src/components/pages/master-data/nonstock/form/NonstockForm.schema.ts b/src/components/pages/master-data/nonstock/form/NonstockForm.schema.ts index 50f69c7d..8039ef76 100644 --- a/src/components/pages/master-data/nonstock/form/NonstockForm.schema.ts +++ b/src/components/pages/master-data/nonstock/form/NonstockForm.schema.ts @@ -2,6 +2,22 @@ import * as Yup from 'yup'; export const NonstockFormSchema = Yup.object({ name: Yup.string().required('Nama wajib diisi!'), + + uomId: Yup.number().min(1, 'UOM wajib diisi!').required('UOM wajib diisi!'), + uom: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + + supplierIds: Yup.array().of(Yup.number().min(0, 'Supplier wajib diisi!')), + suppliers: Yup.array().of( + Yup.object({ + value: Yup.number().min(0).required(), + label: Yup.string().required(), + }) + ), + + flags: Yup.array().of(Yup.string()).notRequired(), }); export const UpdateNonstockFormSchema = NonstockFormSchema; diff --git a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx index 33dcba54..7a67c9a7 100644 --- a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx +++ b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx @@ -3,26 +3,31 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useFormik } from 'formik'; +import { toast } from 'react-hot-toast'; +import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; import TextInput from '@/components/input/TextInput'; +import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; import { NonstockFormSchema, NonstockFormValues, UpdateNonstockFormSchema, } from '@/components/pages/master-data/nonstock/form/NonstockForm.schema'; -import { isResponseError } from '@/lib/api-helper'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { - CreateNonstockPayload, Nonstock, + CreateNonstockPayload, UpdateNonstockPayload, } from '@/types/api/master-data/nonstock'; -import { - createNonstock, - updateNonstock, -} from '@/services/api/master-data/nonstock'; +import { NonstockApi, SupplierApi, UomApi } from '@/services/api/master-data'; +import { cn } from '@/lib/helper'; +import { flags } from '@/types/api/api-general'; +import { SUPPLIER_FLAG_OPTIONS } from '@/config/constant'; interface NonstockFormProps { type?: 'add' | 'edit' | 'detail'; @@ -31,19 +36,21 @@ interface NonstockFormProps { const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { const router = useRouter(); + const deleteModal = useModal(); const [nonstockFormErrorMessage, setNonstockFormErrorMessage] = useState(''); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); const createNonstockHandler = useCallback( async (payload: CreateNonstockPayload) => { - const createNonstockRes = await createNonstock(payload); + const createNonstockRes = await NonstockApi.create(payload); if (isResponseError(createNonstockRes)) { setNonstockFormErrorMessage(createNonstockRes.message); return; } - alert(createNonstockRes?.message); + toast.success(createNonstockRes?.message as string); router.push('/master-data/nonstock'); }, [router] @@ -51,14 +58,14 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { const updateNonstockHandler = useCallback( async (nonstockId: number, payload: UpdateNonstockPayload) => { - const updateNonstockRes = await updateNonstock(nonstockId, payload); + const updateNonstockRes = await NonstockApi.update(nonstockId, payload); if (updateNonstockRes?.status === 'error') { setNonstockFormErrorMessage(updateNonstockRes.message); return; } - alert(updateNonstockRes?.message); + toast.success(updateNonstockRes?.message as string); router.refresh(); router.push('/master-data/nonstock'); }, @@ -68,6 +75,22 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { const formikInitialValues = useMemo(() => { return { name: initialValues?.name ?? '', + uomId: initialValues?.uom_id ?? 0, + uom: initialValues?.uom + ? { + value: initialValues?.uom.id, + label: initialValues?.uom.name, + } + : null, + supplierIds: + initialValues?.suppliers.map((supplier) => supplier.id) ?? [], + suppliers: + initialValues?.suppliers.map((supplier) => ({ + value: supplier.id, + label: supplier.name, + })) ?? [], + + flags: initialValues?.flags ?? [], }; }, [initialValues]); @@ -80,6 +103,9 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { const nonstockPayload: CreateNonstockPayload = { name: values.name, + uom_id: values.uomId, + supplier_ids: values.supplierIds as number[], + flags: values.flags as flags[], }; switch (type) { @@ -97,81 +123,268 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { }, }); + const { setValues: formikSetValues } = formik; + + // UOM + const [uomSelectInputValue, setUomSelectInputValue] = useState(''); + + const uomsUrl = `${UomApi.basePath}?${new URLSearchParams({ + search: uomSelectInputValue ?? '', + }).toString()}`; + + const { data: uoms, isLoading: isLoadingUoms } = useSWR( + uomsUrl, + UomApi.getAllFetcher + ); + + const uomOptions = isResponseSuccess(uoms) + ? uoms?.data.map((uom) => ({ + value: uom.id, + label: uom.name, + })) + : []; + + const uomChangeHandler = (val: OptionType | OptionType[] | null) => { + formik.setFieldTouched('uom', true); + formik.setFieldValue('uom', val); + + formik.setFieldTouched('uomId', true); + formik.setFieldValue('uomId', (val as OptionType)?.value); + }; + + // supplier + const [supplierSelectInputValue, setSupplierSelectInputValue] = useState(''); + + const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ + search: supplierSelectInputValue ?? '', + }).toString()}`; + + const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR( + suppliersUrl, + SupplierApi.getAllFetcher + ); + + const supplierOptions = isResponseSuccess(suppliers) + ? suppliers?.data + .filter((sup) => sup.category === 'BOP') + .map((supplier) => ({ + value: supplier.id, + label: supplier.name, + })) + : []; + + const supplierChangeHandler = (val: OptionType | OptionType[] | null) => { + formik.setFieldTouched('suppliers', true); + formik.setFieldValue('suppliers', val); + + const supplierIds = (val as OptionType[]).map( + (supplier) => supplier.value as number + ); + + formik.setFieldTouched('supplierIds', true); + formik.setFieldValue('supplierIds', supplierIds); + }; + + const deleteNonstockClickHandler = () => { + deleteModal.openModal(); + }; + + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + + await NonstockApi.delete(initialValues?.id as number); + + deleteModal.closeModal(); + toast.success('Successfully delete Nonstock!'); + setIsDeleteLoading(false); + router.push('/master-data/nonstock'); + }; + + const flagsChangeHandler = (val: OptionType | OptionType[] | null) => { + const formattedFlags = (val as OptionType[]).map( + (flag) => flag.value as string + ); + + formik.setFieldValue('flags', formattedFlags); + }; + useEffect(() => { - formik.setValues(formikInitialValues); - }, [formikInitialValues]); + formikSetValues(formikInitialValues); + }, [formikSetValues, formikInitialValues]); return ( -
-
- + +

+ {type === 'add' && 'Tambah Nonstock'} + {type === 'edit' && 'Edit Nonstock'} + {type === 'detail' && 'Detail Nonstock'} +

+
+ +
- - Kembali - +
+ -

- {type === 'add' && 'Tambah Non Stock'} - {type === 'edit' && 'Edit Non Stock'} - {type === 'detail' && 'Detail Non Stock'} -

- + - -
- -
+ - {type !== 'detail' && ( - <> -
- + + formik.values.flags?.includes(opt.value) + )} + onChange={flagsChangeHandler} + options={SUPPLIER_FLAG_OPTIONS} + isError={formik.touched.flags && Boolean(formik.errors.flags)} + errorMessage={formik.errors.flags as string} + isDisabled={type === 'detail'} + isClearable + /> +
- -
+
+ {type !== 'add' && ( +
+ - {nonstockFormErrorMessage && ( -
- - {nonstockFormErrorMessage} + {type !== 'edit' && ( + + )}
)} - - )} - -
+ + {type !== 'detail' && ( +
+ + + +
+ )} +
+ + {nonstockFormErrorMessage && ( +
+ + {nonstockFormErrorMessage} +
+ )} + + + + {type !== 'add' && ( + + )} + ); }; diff --git a/src/components/pages/master-data/product-category/ProductCategoryTable.tsx b/src/components/pages/master-data/product-category/ProductCategoryTable.tsx new file mode 100644 index 00000000..f8413ab6 --- /dev/null +++ b/src/components/pages/master-data/product-category/ProductCategoryTable.tsx @@ -0,0 +1,266 @@ +'use client'; + +import { ChangeEventHandler, useEffect, useState } from 'react'; +import useSWR from 'swr'; +import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; +import toast from 'react-hot-toast'; + +import { Icon } from '@iconify/react'; +import Table from '@/components/Table'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import Button from '@/components/Button'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import RowDropdownOptions from '@/components/table/RowDropdownOptions'; +import RowCollapseOptions from '@/components/table/RowCollapseOptions'; + +import { ProductCategory } from '@/types/api/master-data/product-category'; +import { ProductCategoryApi } from '@/services/api/master-data'; +import { cn } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { ROWS_OPTIONS } from '@/config/constant'; + +const RowOptionsMenu = ({ + type = 'dropdown', + props, + deleteClickHandler, +}: { + type: 'dropdown' | 'collapse'; + props: CellContext; + deleteClickHandler: () => void; +}) => { + return ( +
+ + + +
+ ); +}; + +const ProductCategoryTable = () => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { search: '', nameSort: '' }, + paramMap: { page: 'page', pageSize: 'limit', nameSort: 'sort_name' }, + }); + + const { + data: productCategories, + isLoading, + mutate: refreshProductCategories, + } = useSWR( + `${ProductCategoryApi.basePath}${getTableFilterQueryString()}`, + ProductCategoryApi.getAllFetcher + ); + + const deleteModal = useModal(); + + const [selectedProductCategory, setSelectedProductCategory] = useState(undefined); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const [sorting, setSorting] = useState([]); + + const productCategoryColumns: ColumnDef[] = [ + { + header: '#', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'code', + header: 'Code', + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + 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 = () => { + setSelectedProductCategory(props.row.original); + deleteModal.openModal(); + }; + + return ( + <> + {currentPageSize > 2 && ( + + + + )} + + {currentPageSize <= 2 && ( + + + + )} + + ); + }, + }, + ]; + + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + + await ProductCategoryApi.delete(selectedProductCategory?.id as number); + refreshProductCategories(); + + deleteModal.closeModal(); + toast.success('Successfully delete Product Category!'); + setIsDeleteLoading(false); + }; + + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; + + const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { + const newVal = val as OptionType; + setPageSize(newVal.value as number); + }; + + useEffect(() => { + const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); + if (!isNameSorted) { + updateFilter('nameSort', ''); + } else { + updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); + } + }, [sorting]); + + return ( + <> +
+
+
+
+ +
+ +
+
+ +
+
+ + data={isResponseSuccess(productCategories) ? productCategories?.data : []} + columns={productCategoryColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(productCategories) ? productCategories?.meta?.page : 0} + totalItems={isResponseSuccess(productCategories) ? productCategories?.meta?.total_results : 0} + onPageChange={setPage} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn({ + 'mb-20': isResponseSuccess(productCategories) && productCategories?.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', + }} + /> +
+ + + ); +}; + +export default ProductCategoryTable; \ No newline at end of file diff --git a/src/components/pages/master-data/product-category/form/ProductCategoryForm.schema.ts b/src/components/pages/master-data/product-category/form/ProductCategoryForm.schema.ts new file mode 100644 index 00000000..102bb812 --- /dev/null +++ b/src/components/pages/master-data/product-category/form/ProductCategoryForm.schema.ts @@ -0,0 +1,10 @@ +import * as Yup from 'yup'; + +export const ProductCategoryFormSchema = 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 type ProductCategoryFormValues = Yup.InferType; \ No newline at end of file diff --git a/src/components/pages/master-data/product-category/form/ProductCategoryForm.tsx b/src/components/pages/master-data/product-category/form/ProductCategoryForm.tsx new file mode 100644 index 00000000..453670f3 --- /dev/null +++ b/src/components/pages/master-data/product-category/form/ProductCategoryForm.tsx @@ -0,0 +1,266 @@ +'use client'; + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { useFormik } from 'formik'; +import { toast } from 'react-hot-toast'; + +import { Icon } from '@iconify/react'; +import Button from '@/components/Button'; +import TextInput from '@/components/input/TextInput'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; + +import { + ProductCategoryFormSchema, + ProductCategoryFormValues, + UpdateProductCategoryFormSchema, +} from '@/components/pages/master-data/product-category/form/ProductCategoryForm.schema'; +import { isResponseError } from '@/lib/api-helper'; +import { + ProductCategory, + CreateProductCategoryPayload, + UpdateProductCategoryPayload, +} from '@/types/api/master-data/product-category'; +import { ProductCategoryApi } from '@/services/api/master-data'; +import { cn } from '@/lib/helper'; + +interface ProductCategoryFormProps { + type?: 'add' | 'edit' | 'detail'; + initialValues?: ProductCategory; +} + +const ProductCategoryForm = ({ type = 'add', initialValues }: ProductCategoryFormProps) => { + const router = useRouter(); + const deleteModal = useModal(); + + const [formErrorMessage, setFormErrorMessage] = useState(''); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const createProductCategoryHandler = useCallback( + async (payload: CreateProductCategoryPayload) => { + const res = await ProductCategoryApi.create(payload); + + if (isResponseError(res)) { + setFormErrorMessage(res.message); + return; + } + + toast.success(res?.message as string); + router.push('/master-data/product-category'); + }, + [router] + ); + + const updateProductCategoryHandler = useCallback( + async (id: number, payload: UpdateProductCategoryPayload) => { + const res = await ProductCategoryApi.update(id, payload); + + if (res?.status === 'error') { + setFormErrorMessage(res.message); + return; + } + + toast.success(res?.message as string); + router.refresh(); + router.push('/master-data/product-category'); + }, + [router] + ); + + const formikInitialValues = useMemo(() => { + return { + code: initialValues?.code ?? '', + name: initialValues?.name ?? '', + }; + }, [initialValues]); + + const formik = useFormik({ + initialValues: formikInitialValues, + validationSchema: type === 'edit' ? UpdateProductCategoryFormSchema : ProductCategoryFormSchema, + onSubmit: async (values) => { + setFormErrorMessage(''); + + const payload: CreateProductCategoryPayload = { + code: values.code, + name: values.name, + }; + + switch (type) { + case 'add': + await createProductCategoryHandler(payload); + break; + case 'edit': + await updateProductCategoryHandler(initialValues?.id as number, payload); + break; + } + }, + }); + + const { setValues: formikSetValues } = formik; + + const deleteProductCategoryClickHandler = () => { + deleteModal.openModal(); + }; + + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + + await ProductCategoryApi.delete(initialValues?.id as number); + + deleteModal.closeModal(); + toast.success('Successfully delete Product Category!'); + setIsDeleteLoading(false); + router.push('/master-data/product-category'); + }; + + useEffect(() => { + formikSetValues(formikInitialValues); + }, [formikSetValues, formikInitialValues]); + + return ( + <> +
+
+ + +

+ {type === 'add' && 'Tambah Product Category'} + {type === 'edit' && 'Edit Product Category'} + {type === 'detail' && 'Detail Product Category'} +

+
+ +
+
+ + +
+ +
+ {type !== 'add' && ( +
+ + + {type !== 'edit' && ( + + )} +
+ )} + + {type !== 'detail' && ( +
+ + + +
+ )} +
+ + {formErrorMessage && ( +
+ + {formErrorMessage} +
+ )} +
+
+ + {type !== 'add' && ( + + )} + + ); +}; + +export default ProductCategoryForm; \ No newline at end of file diff --git a/src/components/pages/master-data/product/ProductTable.tsx b/src/components/pages/master-data/product/ProductTable.tsx new file mode 100644 index 00000000..ab256548 --- /dev/null +++ b/src/components/pages/master-data/product/ProductTable.tsx @@ -0,0 +1,350 @@ +'use client'; + +import { ChangeEventHandler, useCallback, useEffect, useState } from 'react'; +import useSWR from 'swr'; +import { + CellContext, + ColumnDef, + ColumnSort, + SortingState, +} from '@tanstack/react-table'; +import toast from 'react-hot-toast'; + +import { Icon } from '@iconify/react'; +import Table from '@/components/Table'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import Button from '@/components/Button'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import RowDropdownOptions from '@/components/table/RowDropdownOptions'; +import RowCollapseOptions from '@/components/table/RowCollapseOptions'; + +import { Product } from '@/types/api/master-data/product'; +import { ProductApi } from '@/services/api/master-data'; +import { cn } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { ROWS_OPTIONS } from '@/config/constant'; + +const RowOptionsMenu = ({ + type = 'dropdown', + props, + deleteClickHandler, +}: { + type: 'dropdown' | 'collapse'; + props: CellContext; + deleteClickHandler: () => void; +}) => ( +
+ + + +
+); + +const ProductsTable = () => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { + search: '', + nameSort: '', + skuSort: '', + brandSort: '', + categorySort: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + nameSort: 'sort_name', + skuSort: 'sort_sku', + brandSort: 'sort_brand', + categorySort: 'sort_category', + }, + }); + + const { + data: products, + isLoading, + mutate: refreshProducts, + } = useSWR( + `${ProductApi.basePath}${getTableFilterQueryString()}`, + ProductApi.getAllFetcher + ); + + const deleteModal = useModal(); + const [selectedProduct, setSelectedProduct] = useState(undefined); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const [sorting, setSorting] = useState([]); + + const productsColumns: ColumnDef[] = [ + { + header: '#', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + accessorKey: 'sku', + header: 'SKU', + }, + { + accessorKey: 'brand', + header: 'Merek', + }, + { + accessorKey: 'product_category', + header: 'Kategori', + cell: (props) => props.row.original.product_category?.name ?? '-', + }, + { + accessorKey: 'uom', + header: 'Satuan', + cell: (props) => props.row.original.uom?.name ?? '-', + }, + { + accessorKey: 'product_price', + header: 'Harga Produk', + cell: (props) => props.row.original.product_price?.toLocaleString() ?? '-', + }, + { + accessorKey: 'selling_price', + header: 'Harga Jual', + cell: (props) => props.row.original.selling_price?.toLocaleString() ?? '-', + }, + { + accessorKey: 'tax', + header: 'Pajak (%)', + cell: (props) => props.row.original.tax ?? '-', + }, + { + accessorKey: 'expiry_period', + header: 'Kadaluarsa (hari)', + cell: (props) => props.row.original.expiry_period ?? '-', + }, + { + accessorKey: 'suppliers', + header: 'Supplier', + cell: (props) => + props.row.original.suppliers?.map((s) => s.name).join(', ') || '-', + }, + { + accessorKey: 'flags', + header: 'Flags', + cell: (props) => + props.row.original.flags?.length + ? props.row.original.flags.join(', ') + : '-', + }, + { + 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 = () => { + setSelectedProduct(props.row.original); + deleteModal.openModal(); + }; + + return ( + <> + {currentPageSize > 2 && ( + + + + )} + {currentPageSize <= 2 && ( + + + + )} + + ); + }, + }, + ]; + + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + await ProductApi.delete(selectedProduct?.id as number); + refreshProducts(); + deleteModal.closeModal(); + toast.success('Successfully delete Product!'); + setIsDeleteLoading(false); + }; + + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; + + const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { + const newVal = val as OptionType; + setPageSize(newVal.value as number); + }; + + const updateSortingFilter = useCallback( + ( + sortName: Exclude, + sortFilter: ColumnSort | undefined + ) => { + if (!sortFilter) { + updateFilter(sortName, ''); + } else { + updateFilter(sortName, sortFilter.desc ? 'desc' : 'asc'); + } + }, + [updateFilter] + ); + + useEffect(() => { + const nameSortFilter = sorting.find((sortItem) => sortItem.id === 'name'); + const skuSortFilter = sorting.find((sortItem) => sortItem.id === 'sku'); + const brandSortFilter = sorting.find((sortItem) => sortItem.id === 'brand'); + const categorySortFilter = sorting.find((sortItem) => sortItem.id === 'product_category'); + + updateSortingFilter('nameSort', nameSortFilter); + updateSortingFilter('skuSort', skuSortFilter); + updateSortingFilter('brandSort', brandSortFilter); + updateSortingFilter('categorySort', categorySortFilter); + }, [sorting]); + + return ( + <> +
+
+
+
+ +
+ +
+
+ +
+
+ + data={isResponseSuccess(products) ? products?.data : []} + columns={productsColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(products) ? products?.meta?.page : 0} + totalItems={ + isResponseSuccess(products) ? products?.meta?.total_results : 0 + } + onPageChange={setPage} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn({ + 'mb-20': + isResponseSuccess(products) && products?.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', + }} + /> +
+ + + ); +}; + +export default ProductsTable; \ No newline at end of file diff --git a/src/components/pages/master-data/product/form/ProductForm.schema.ts b/src/components/pages/master-data/product/form/ProductForm.schema.ts new file mode 100644 index 00000000..eea9abf7 --- /dev/null +++ b/src/components/pages/master-data/product/form/ProductForm.schema.ts @@ -0,0 +1,53 @@ +import * as Yup from 'yup'; + +export const ProductFormSchema = 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(), + 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(), + product_category_id: Yup.number() + .required('Kategori produk wajib diisi!') + .typeError('Kategori produk 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: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + supplier_ids: Yup.array() + .of(Yup.number().typeError('Supplier tidak valid!')) + .min(1, 'Minimal harus ada 1 supplier!') + .required('Supplier wajib diisi!'), + flags: Yup.array() + .of(Yup.string()) + .min(1, 'Minimal harus ada 1 flag!') + .required('Flag wajib diisi!'), +}); + +export const UpdateProductFormSchema = ProductFormSchema; + +export type ProductFormValues = Yup.InferType; + diff --git a/src/components/pages/master-data/product/form/ProductForm.tsx b/src/components/pages/master-data/product/form/ProductForm.tsx new file mode 100644 index 00000000..02afbfc9 --- /dev/null +++ b/src/components/pages/master-data/product/form/ProductForm.tsx @@ -0,0 +1,438 @@ +'use client'; + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { useFormik } from 'formik'; +import { toast } from 'react-hot-toast'; +import useSWR from 'swr'; + +import { Icon } from '@iconify/react'; +import Button from '@/components/Button'; +import TextInput from '@/components/input/TextInput'; +import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; + +import { + ProductFormSchema, + ProductFormValues, + UpdateProductFormSchema, +} from '@/components/pages/master-data/product/form/ProductForm.schema'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { + Product, + CreateProductPayload, + UpdateProductPayload, +} from '@/types/api/master-data/product'; +import { UomApi, ProductCategoryApi, SupplierApi, ProductApi } from '@/services/api/master-data'; +import { cn } from '@/lib/helper'; +import { PRODUCT_FLAG_OPTIONS } from '@/config/constant'; + +interface ProductFormProps { + type?: 'add' | 'edit' | 'detail'; + initialValues?: Product; +} + +const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { + const router = useRouter(); + const deleteModal = useModal(); + + const [productFormErrorMessage, setProductFormErrorMessage] = useState(''); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const createProductHandler = useCallback( + async (payload: CreateProductPayload) => { + const res = await ProductApi.create(payload); + if (isResponseError(res)) { + setProductFormErrorMessage(res.message); + return; + } + toast.success(res?.message as string); + router.push('/master-data/product'); + }, + [router] + ); + + const updateProductHandler = useCallback( + async (productId: number, payload: UpdateProductPayload) => { + const res = await ProductApi.update(productId, payload); + if (res?.status === 'error') { + setProductFormErrorMessage(res.message); + return; + } + toast.success(res?.message as string); + router.refresh(); + router.push('/master-data/product'); + }, + [router] + ); + + const formikInitialValues = useMemo(() => ({ + name: initialValues?.name ?? '', + brand: initialValues?.brand ?? '', + sku: initialValues?.sku ?? '', + uom: initialValues?.uom + ? { value: initialValues.uom.id, label: initialValues.uom.name } + : null, + uom_id: initialValues?.uom?.id ?? 0, + product_category: initialValues?.product_category + ? { value: initialValues.product_category.id, label: initialValues.product_category.name } + : null, + product_category_id: initialValues?.product_category?.id ?? 0, + product_price: initialValues?.product_price ?? 0, + selling_price: initialValues?.selling_price ?? 0, + tax: initialValues?.tax ?? 0, + expiry_period: initialValues?.expiry_period ?? 0, + supplier: null, // not used for payload, just for UI + supplier_ids: initialValues?.suppliers?.map(s => s.id) ?? [], + flags: initialValues?.flags ?? [], + }), [initialValues]); + + const formik = useFormik({ + initialValues: formikInitialValues, + validationSchema: type === 'edit' ? UpdateProductFormSchema : ProductFormSchema, + onSubmit: async (values) => { + setProductFormErrorMessage(''); + const payload: CreateProductPayload = { + name: values.name, + brand: values.brand, + sku: values.sku, + uom_id: values.uom_id, + product_category_id: values.product_category_id, + product_price: values.product_price, + selling_price: values.selling_price, + tax: values.tax, + expiry_period: values.expiry_period, + supplier_ids: (values.supplier_ids ?? []).filter((id): id is number => typeof id === 'number'), + flags: (values.flags ?? []).filter((f): f is string => typeof f === 'string'), + }; + switch (type) { + case 'add': + await createProductHandler(payload); + break; + case 'edit': + await updateProductHandler(initialValues?.id as number, payload); + break; + } + }, + }); + + const { setValues: formikSetValues } = formik; + + // UOM + const [uomSelectInputValue, setUomSelectInputValue] = useState(''); + const uomsUrl = `${UomApi.basePath}?${new URLSearchParams({ search: uomSelectInputValue ?? '' }).toString()}`; + const { data: uoms, isLoading: isLoadingUoms } = useSWR(uomsUrl, UomApi.getAllFetcher); + const uomOptions = isResponseSuccess(uoms) + ? uoms?.data.map((uom) => ({ value: uom.id, label: uom.name })) + : []; + const uomChangeHandler = (val: OptionType | OptionType[] | null) => { + formik.setFieldTouched('uom', true); + formik.setFieldValue('uom', val); + formik.setFieldTouched('uom_id', true); + formik.setFieldValue('uom_id', (val as OptionType)?.value); + }; + + // Product Category + const [categorySelectInputValue, setCategorySelectInputValue] = useState(''); + const categoriesUrl = `${ProductCategoryApi.basePath}?${new URLSearchParams({ search: categorySelectInputValue ?? '' }).toString()}`; + const { data: categories, isLoading: isLoadingCategories } = useSWR(categoriesUrl, ProductCategoryApi.getAllFetcher); + const categoryOptions = isResponseSuccess(categories) + ? categories?.data.map((cat) => ({ value: cat.id, label: cat.name })) + : []; + const categoryChangeHandler = (val: OptionType | OptionType[] | null) => { + formik.setFieldTouched('product_category', true); + formik.setFieldValue('product_category', val); + formik.setFieldTouched('product_category_id', true); + formik.setFieldValue('product_category_id', (val as OptionType)?.value); + }; + + // Supplier (multi select) + const [supplierSelectInputValue, setSupplierSelectInputValue] = useState(''); + const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ search: supplierSelectInputValue ?? '' }).toString()}`; + const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(suppliersUrl, SupplierApi.getAllFetcher); + const supplierOptions = isResponseSuccess(suppliers) + ? suppliers?.data + .filter((sup) => sup.category === 'SAPRONAK') + .map((sup) => ({ value: sup.id, label: sup.name })) + : []; + const supplierChangeHandler = (val: OptionType | OptionType[] | null) => { + const arr = Array.isArray(val) ? val : val ? [val] : []; + formik.setFieldTouched('supplier_ids', true); + formik.setFieldValue('supplier_ids', arr.map((v) => (v as OptionType).value)); + }; + + const deleteProductClickHandler = () => { + deleteModal.openModal(); + }; + + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + await ProductApi.delete(initialValues?.id as number); + deleteModal.closeModal(); + toast.success('Successfully delete Product!'); + setIsDeleteLoading(false); + router.push('/master-data/product'); + }; + + useEffect(() => { + formikSetValues(formikInitialValues); + }, [formikSetValues, formikInitialValues]); + + return ( + <> +
+
+ +

+ {type === 'add' && 'Tambah Produk'} + {type === 'edit' && 'Edit Produk'} + {type === 'detail' && 'Detail Produk'} +

+
+
+
+ + + + + + + + + + formik.values.supplier_ids.includes(opt.value))} + onChange={supplierChangeHandler} + options={supplierOptions} + onInputChange={setSupplierSelectInputValue} + isLoading={isLoadingSuppliers} + isError={formik.touched.supplier_ids && Boolean(formik.errors.supplier_ids)} + errorMessage={formik.errors.supplier_ids as string} + isDisabled={type === 'detail'} + isClearable + /> + formik.values.flags.includes(opt.value))} + onChange={val => { + const arr = Array.isArray(val) ? val : val ? [val] : []; + formik.setFieldValue('flags', arr.map((v) => (v as OptionType).value)); + }} + options={PRODUCT_FLAG_OPTIONS} + isError={formik.touched.flags && Boolean(formik.errors.flags)} + errorMessage={formik.errors.flags as string} + isDisabled={type === 'detail'} + isClearable + /> +
+
+ {type !== 'add' && ( +
+ + {type !== 'edit' && ( + + )} +
+ )} + {type !== 'detail' && ( +
+ + +
+ )} +
+ {productFormErrorMessage && ( +
+ + {productFormErrorMessage} +
+ )} +
+
+ {type !== 'add' && ( + + )} + + ); +}; + +export default ProductForm; \ No newline at end of file diff --git a/src/components/pages/master-data/supplier/form/SupplierForm.tsx b/src/components/pages/master-data/supplier/form/SupplierForm.tsx index 9e0ac83a..fb4630ae 100644 --- a/src/components/pages/master-data/supplier/form/SupplierForm.tsx +++ b/src/components/pages/master-data/supplier/form/SupplierForm.tsx @@ -11,7 +11,11 @@ import { import { useRouter } from 'next/navigation'; import { useCallback, useEffect, useMemo, useState } from 'react'; import toast from 'react-hot-toast'; -import { SupplierFormSchema, SupplierFormValues, UpdateSupplierFormSchema } from './SupplierForm.schema'; +import { + SupplierFormSchema, + SupplierFormValues, + UpdateSupplierFormSchema, +} from './SupplierForm.schema'; import { useFormik } from 'formik'; import SelectInput, { OptionType } from '@/components/input/SelectInput'; import { Icon } from '@iconify/react'; @@ -127,7 +131,8 @@ const SupplierForm = ({ const formik = useFormik({ initialValues: formikInitialValues, enableReinitialize: true, - validationSchema: formType === 'edit' ? UpdateSupplierFormSchema : SupplierFormSchema, + validationSchema: + formType === 'edit' ? UpdateSupplierFormSchema : SupplierFormSchema, onSubmit: async (values) => { // reset error message setSupplierFormErrorMessage(''); @@ -274,7 +279,9 @@ const SupplierForm = ({ onChange={categoryChangeHandler} options={categoryOptions} onInputChange={setCategorySelectInputValue} - isError={formik.touched.category && Boolean(formik.errors.category)} + isError={ + formik.touched.category && Boolean(formik.errors.category) + } errorMessage={formik.errors.category as string} isDisabled={formType === 'detail'} isClearable @@ -285,7 +292,9 @@ const SupplierForm = ({ label='Hatchery' value={hatcheryTagInputValue} onChange={(value) => formik.setFieldValue('hatchery', value)} - isError={formik.touched.hatchery && Boolean(formik.errors.hatchery)} + isError={ + formik.touched.hatchery && Boolean(formik.errors.hatchery) + } errorMessage={formik.errors.hatchery} readOnly={formType === 'detail'} /> diff --git a/src/config/constant.ts b/src/config/constant.ts index 5a9314bd..48cec0fe 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -1,4 +1,11 @@ -export const MAIN_DRAWER_LINKS = [ +type MAIN_DRAWER_MENU = { + title: string; + link: string; + icon: string; + submenu?: MAIN_DRAWER_MENU[]; +}; + +export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [ { title: 'Dashboard', link: '/dashboard', @@ -62,7 +69,7 @@ export const MAIN_DRAWER_LINKS = [ }, { title: 'FCR', - link: '/master-data/FCR', + link: '/master-data/fcr', icon: 'fluent:food-chicken-leg-16-regular', }, { @@ -129,3 +136,19 @@ export const CATEGORY_OPTIONS = [ value: 'SAPRONAK', }, ]; + +export const PRODUCT_FLAG_OPTIONS = [ + { label: 'DOC', value: 'DOC' }, + { label: 'PAKAN', value: 'PAKAN' }, + { label: 'PRE-STARTER', value: 'PRE-STARTER' }, + { label: 'STARTER', value: 'STARTER' }, + { label: 'FINISHER', value: 'FINISHER' }, + { label: 'OVK', value: 'OVK' }, + { label: 'OBAT', value: 'OBAT' }, + { label: 'VITAMIN', value: 'VITAMIN' }, + { label: 'KIMIA', value: 'KIMIA' }, +]; + +export const SUPPLIER_FLAG_OPTIONS = [ + { label: 'EKSPEDISI', value: 'EKSPEDISI' }, +]; diff --git a/src/services/api/master-data.ts b/src/services/api/master-data.ts index 58c30953..dce528e7 100644 --- a/src/services/api/master-data.ts +++ b/src/services/api/master-data.ts @@ -29,11 +29,36 @@ import { Customer, UpdateCustomerPayload, } from '@/types/api/master-data/customer'; +import { + CreateProductCategoryPayload, + ProductCategory, + UpdateProductCategoryPayload, +} from '@/types/api/master-data/product-category'; +import { + CreateProductPayload, + Product, + UpdateProductPayload, +} from '@/types/api/master-data/product'; import { CreateSupplierPayload, Supplier, UpdateSupplierPayload, } from '@/types/api/master-data/supplier'; +import { + CreateNonstockPayload, + Nonstock, + UpdateNonstockPayload, +} from '@/types/api/master-data/nonstock'; +import { + Bank, + CreateBankPayload, + UpdateBankPayload, +} from '@/types/api/master-data/bank'; +import { + CreateFcrPayload, + Fcr, + UpdateFcrPayload, +} from '@/types/api/master-data/fcr'; export const UomApi = new BaseApiService< Uom, @@ -70,9 +95,38 @@ export const CustomerApi = new BaseApiService< CreateCustomerPayload, UpdateCustomerPayload >('/master-data/customers'); +export const ProductCategoryApi = new BaseApiService< + ProductCategory, + CreateProductCategoryPayload, + UpdateProductCategoryPayload +>('/master-data/product-categories'); + +export const ProductApi = new BaseApiService< + Product, + CreateProductPayload, + UpdateProductPayload +>('/master-data/products'); export const SupplierApi = new BaseApiService< Supplier, CreateSupplierPayload, UpdateSupplierPayload ->('/master-data/suppliers'); \ No newline at end of file +>('/master-data/suppliers'); + +export const NonstockApi = new BaseApiService< + Nonstock, + CreateNonstockPayload, + UpdateNonstockPayload +>('/master-data/nonstocks'); + +export const BankApi = new BaseApiService< + Bank, + CreateBankPayload, + UpdateBankPayload +>('/master-data/banks'); + +export const FcrApi = new BaseApiService< + Fcr, + CreateFcrPayload, + UpdateFcrPayload +>('/master-data/fcrs'); diff --git a/src/services/api/master-data/nonstock.ts b/src/services/api/master-data/nonstock.ts deleted file mode 100644 index 7340e37b..00000000 --- a/src/services/api/master-data/nonstock.ts +++ /dev/null @@ -1,87 +0,0 @@ -import axios from 'axios'; -import { httpClient } from '@/services/http/client'; - -import { - CreateNonstockPayload, - DeleteNonstockResponse, - NonstockResponse, - UpdateNonstockPayload, -} from '@/types/api/master-data/nonstock'; - -export const getNonstock = async (nonstockId: number) => { - try { - const getNonstockRes = await httpClient( - `/master-data/nonstocks/${nonstockId}` - ); - - return getNonstockRes; - } catch (error: unknown) { - if (axios.isAxiosError(error)) { - return error.response?.data; - } - - return undefined; - } -}; - -export const createNonstock = async (payload: CreateNonstockPayload) => { - try { - const createNonstockRes = await httpClient( - '/master-data/nonstocks', - { - method: 'POST', - body: payload, - } - ); - - return createNonstockRes; - } catch (error: unknown) { - if (axios.isAxiosError(error)) { - return error.response?.data; - } - - return undefined; - } -}; - -export const updateNonstock = async ( - nonstockId: number, - payload: UpdateNonstockPayload -) => { - try { - const updateNonstockRes = await httpClient( - `/master-data/nonstocks/${nonstockId}`, - { - method: 'PATCH', - body: payload, - } - ); - - return updateNonstockRes; - } catch (error: unknown) { - if (axios.isAxiosError(error)) { - return error.response?.data; - } - - return undefined; - } -}; - -export const deleteNonstock = async (nonstockId: number) => { - try { - const deleteNonstockRes = await httpClient( - `/master-data/nonstocks/${nonstockId}`, - { - method: 'DELETE', - } - ); - - return deleteNonstockRes; - } catch (error) { - if (axios.isAxiosError(error)) { - return error.response?.data; - } - - return undefined; - } -}; diff --git a/src/types/api/api-general.d.ts b/src/types/api/api-general.d.ts index 8a4c4de7..6a3fc6be 100644 --- a/src/types/api/api-general.d.ts +++ b/src/types/api/api-general.d.ts @@ -53,3 +53,16 @@ export type BaseMetadata = { export type Override = Omit & Overrides; + +export type flags = + | 'PAKAN' + | 'OBAT' + | 'VITAMIN' + | 'KIMIA' + | 'EKSPEDISI' + | 'IS_ACTIVE' + | 'DOC' + | 'PRE-STARTER' + | 'STARTER' + | 'FINISHER' + | 'OVK'; diff --git a/src/types/api/master-data/bank.d.ts b/src/types/api/master-data/bank.d.ts new file mode 100644 index 00000000..0b23b446 --- /dev/null +++ b/src/types/api/master-data/bank.d.ts @@ -0,0 +1,20 @@ +import { BaseMetadata } from '@/types/api/api-general'; + +export type BaseBank = { + id: number; + name: string; + alias: string; + owner?: string; + account_number: string; +}; + +export type Bank = BaseMetadata & BaseBank; + +export type CreateBankPayload = { + name: string; + alias: string; + account_number: string; + owner?: string; +}; + +export type UpdateBankPayload = CreateBankPayload; diff --git a/src/types/api/master-data/fcr.d.ts b/src/types/api/master-data/fcr.d.ts new file mode 100644 index 00000000..45ad25e5 --- /dev/null +++ b/src/types/api/master-data/fcr.d.ts @@ -0,0 +1,30 @@ +import { BaseMetadata } from '@/types/api/api-general'; + +export type BaseFcr = { + id: number; + name: string; +}; + +export type FcrStandard = { + id: number; + weight: number; + fcr_number: number; + mortality: number; +}; + +export type Fcr = BaseMetadata & BaseFcr; + +export type FcrWithStandards = Fcr & { + fcr_standards: FcrStandard[]; +}; + +export type CreateFcrPayload = { + name: string; + fcr_standards: { + weight: number; + fcr_number: number; + mortality: number; + }[]; +}; + +export type UpdateFcrPayload = CreateFcrPayload; diff --git a/src/types/api/master-data/nonstock.d.ts b/src/types/api/master-data/nonstock.d.ts index 682f7852..e4e79d8e 100644 --- a/src/types/api/master-data/nonstock.d.ts +++ b/src/types/api/master-data/nonstock.d.ts @@ -1,18 +1,23 @@ -import { BaseApiResponse } from '@/types/api/api-general'; +import { BaseApiResponse, BaseMetadata, flags } from '@/types/api/api-general'; +import { BaseUom } from '@/types/api/master-data/uom'; +import { BaseSupplier } from '@/types/api/master-data/supplier'; -export type Nonstock = { +export type BaseNonstock = { id: number; name: string; + uom_id: number; + uom: BaseUom; + suppliers: BaseSupplier[]; + flags: flags[]; }; +export type Nonstock = BaseMetadata & BaseNonstock; + export type CreateNonstockPayload = { name: string; + uom_id: number; + supplier_ids: number[]; + flags: flags[]; }; export type UpdateNonstockPayload = CreateNonstockPayload; - -export type NonstockResponse = BaseApiResponse; - -export type NonstocksResponse = BaseApiResponse; - -export type DeleteNonstockResponse = BaseApiResponse; diff --git a/src/types/api/master-data/product-category.d.ts b/src/types/api/master-data/product-category.d.ts new file mode 100644 index 00000000..3dd6203e --- /dev/null +++ b/src/types/api/master-data/product-category.d.ts @@ -0,0 +1,16 @@ +import { BaseMetadata } from '@/types/api/api-general'; + +export type BaseProductCategory = { + id: number; + code: string; + name: string; +}; + +export type ProductCategory = BaseMetadata & BaseProductCategory; + +export type CreateProductCategoryPayload = { + code: string; + name: string; +}; + +export type UpdateProductCategoryPayload = CreateProductCategoryPayload; diff --git a/src/types/api/master-data/product.d.ts b/src/types/api/master-data/product.d.ts new file mode 100644 index 00000000..d4039750 --- /dev/null +++ b/src/types/api/master-data/product.d.ts @@ -0,0 +1,37 @@ +import { BaseMetadata } from '@/types/api/api-general'; +import { Uom } from '@/types/api/master-data/uom'; +import { ProductCategory } from '@/types/api/master-data/product-category'; +import { Supplier } from '@/types/api/master-data/supplier'; + +export type BaseProduct = { + 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[]; +}; + +export type Product = BaseMetadata & BaseProduct; + +export type CreateProductPayload = { + name: string; + brand: string; + sku: string; + uom_id: number; + product_category_id: number; + product_price: number; + selling_price: number; + tax: number; + expiry_period: number; + supplier_ids: number[]; + flags: string[]; +}; + +export type UpdateProductPayload = CreateProductPayload; \ No newline at end of file diff --git a/src/types/api/master-data/supplier.d.ts b/src/types/api/master-data/supplier.d.ts index eef3cba7..e6998a25 100644 --- a/src/types/api/master-data/supplier.d.ts +++ b/src/types/api/master-data/supplier.d.ts @@ -1,21 +1,4 @@ -// { -// // "name": "PT CHAROEN POKPHAND INDONESIA Tbk", -// "name": "BOP Vendor", -// // "alias": "CPI", -// "alias": "BOP", -// "pic": "Super Admin", -// "type": "BISNIS", // "BISNIS" | "INDIVIDUAL" -// // "category": "SAPRONAK", // "BOP" | "SAPRONAK" -// "category": "BOP", // "BOP" | "SAPRONAK" -// "hatchery": "Kopo,Tasik", // Comma Separated // nullable -// "phone": "086172527361", -// "email": "abdulazis@gmail.com", -// "address": "Banten", -// "npwp": "0197239080712", // nullable -// "account_number": "192039801283", // nullable -// "due_date": 1 // day -// } -import { BaseMetadata, CreatedUser } from "@/types/api/api-general"; +import { BaseMetadata } from '@/types/api/api-general'; export type BaseSupplier = { id: number; @@ -31,6 +14,7 @@ export type BaseSupplier = { npwp: string; account_number: string; due_date: number; + balance?: number; } export type Supplier = BaseMetadata & BaseSupplier; @@ -48,6 +32,7 @@ export type CreateSupplierPayload = { npwp: string; account_number: string; due_date: number; + balance?: number; } export type UpdateSupplierPayload = CreateSupplierPayload; \ No newline at end of file