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' && (
+
+ )}
+ >
+ );
+};
+
+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 !== '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 Non Stock'}
- {type === 'edit' && 'Edit Non Stock'}
- {type === 'detail' && 'Detail Non Stock'}
-
-
+
-
+
+ {type !== 'add' && (
+
+
+
+ Delete
+
- {nonstockFormErrorMessage && (
-
-
- {nonstockFormErrorMessage}
+ {type !== 'edit' && (
+
+
+ Edit
+
+ )}
)}
- >
- )}
-
-
+
+ {type !== 'detail' && (
+
+
+ Reset
+
+
+
+ Submit
+
+
+ )}
+
+
+ {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 (
+
+
+
+ Detail
+
+
+
+ Edit
+
+
+
+ Delete
+
+
+ );
+};
+
+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 (
+ <>
+
+
+
+
+
+
+ Tambah Product Category
+
+
+
+
+
+
+
+
+
+ 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 (
+ <>
+
+
+
+
+ Kembali
+
+
+
+ {type === 'add' && 'Tambah Product Category'}
+ {type === 'edit' && 'Edit Product Category'}
+ {type === 'detail' && 'Detail Product Category'}
+
+
+
+
+
+
+ {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;
+}) => (
+
+
+
+ Detail
+
+
+
+ Edit
+
+
+
+ Delete
+
+
+);
+
+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 (
+ <>
+
+
+
+
+
+
+ Tambah Produk
+
+
+
+
+
+
+
+
+
+ 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 (
+ <>
+
+
+
+
+ Kembali
+
+
+ {type === 'add' && 'Tambah Produk'}
+ {type === 'edit' && 'Edit Produk'}
+ {type === 'detail' && 'Detail Produk'}
+
+
+
+
+ {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