diff --git a/.gitignore b/.gitignore index 5ef6a520..82965e2d 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,9 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# prettier +.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/package-lock.json b/package-lock.json index ba8fc9b0..1aa69d33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,9 +17,11 @@ "next": "15.5.3", "react": "19.1.0", "react-dom": "19.1.0", + "react-hot-toast": "^2.6.0", "react-select": "^5.10.2", "swr": "^2.3.6", "tailwind-merge": "^3.3.1", + "use-debounce": "^10.0.6", "yup": "^1.7.0", "zustand": "^5.0.8" }, @@ -4039,6 +4041,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/goober": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", + "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -5760,6 +5771,23 @@ "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==", "license": "MIT" }, + "node_modules/react-hot-toast": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -6792,6 +6820,18 @@ "punycode": "^2.1.0" } }, + "node_modules/use-debounce": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.6.tgz", + "integrity": "sha512-C5OtPyhAZgVoteO9heXMTdW7v/IbFI+8bSVKYCJrSmiWWCLsbUxiBSp4t9v0hNBTGY97bT72ydDIDyGSFWfwXg==", + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/use-isomorphic-layout-effect": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", diff --git a/package.json b/package.json index c88a9618..8adf6787 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbopack", + "dev": "eslint && next dev --turbopack", "build": "next build --turbopack", "start": "next start", "lint": "eslint" @@ -18,9 +18,11 @@ "next": "15.5.3", "react": "19.1.0", "react-dom": "19.1.0", + "react-hot-toast": "^2.6.0", "react-select": "^5.10.2", "swr": "^2.3.6", "tailwind-merge": "^3.3.1", + "use-debounce": "^10.0.6", "yup": "^1.7.0", "zustand": "^5.0.8" }, diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx new file mode 100644 index 00000000..4f2c344e --- /dev/null +++ b/src/app/dashboard/page.tsx @@ -0,0 +1,9 @@ +const Dashboard = () => { + return ( +
+

Dashboard

+
+ ); +}; + +export default Dashboard; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 793f0b93..ef28da38 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,10 @@ import type { Metadata, Viewport } from 'next'; import { Inter } from 'next/font/google'; -import './globals.css'; +import '@/app/globals.css'; + +import { Toaster } from 'react-hot-toast'; +import MainDrawer from '@/components/MainDrawer'; +import RequireAuth from '@/components/helper/RequireAuth'; const inter = Inter({ variable: '--font-inter', @@ -26,7 +30,11 @@ export default function RootLayout({ return ( - {children} + + {children} + + + ); diff --git a/src/app/master-data/area/add/page.tsx b/src/app/master-data/area/add/page.tsx new file mode 100644 index 00000000..ed23b0b7 --- /dev/null +++ b/src/app/master-data/area/add/page.tsx @@ -0,0 +1,11 @@ +import AreaForm from '@/components/pages/master-data/area/form/AreaForm'; + +const AddNonstock = () => { + return ( +
+ +
+ ); +}; + +export default AddNonstock; diff --git a/src/app/master-data/area/detail/edit/page.tsx b/src/app/master-data/area/detail/edit/page.tsx new file mode 100644 index 00000000..4b29d792 --- /dev/null +++ b/src/app/master-data/area/detail/edit/page.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import AreaForm from '@/components/pages/master-data/area/form/AreaForm'; + +import { AreaApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const AreaEdit = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const areaId = searchParams.get('areaId'); + + const { data: area, isLoading: isLoadingArea } = useSWR( + areaId, + (id: number) => AreaApi.getSingle(id) + ); + + if (!areaId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingArea && (!area || isResponseError(area))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingArea && } + {!isLoadingArea && isResponseSuccess(area) && ( + + )} +
+ ); +}; + +export default AreaEdit; 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/area/detail/page.tsx b/src/app/master-data/area/detail/page.tsx new file mode 100644 index 00000000..c786ac0d --- /dev/null +++ b/src/app/master-data/area/detail/page.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import AreaForm from '@/components/pages/master-data/area/form/AreaForm'; + +import { AreaApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const AreaDetail = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const areaId = searchParams.get('areaId'); + + const { data: area, isLoading: isLoadingArea } = useSWR( + areaId, + (id: number) => AreaApi.getSingle(id) + ); + + if (!areaId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingArea && (!area || isResponseError(area))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingArea && } + {!isLoadingArea && isResponseSuccess(area) && ( + + )} +
+ ); +}; + +export default AreaDetail; diff --git a/src/app/master-data/area/page.tsx b/src/app/master-data/area/page.tsx new file mode 100644 index 00000000..f8789af2 --- /dev/null +++ b/src/app/master-data/area/page.tsx @@ -0,0 +1,11 @@ +import AreasTable from '@/components/pages/master-data/area/AreasTable'; + +const Nonstock = () => { + return ( +
+ +
+ ); +}; + +export default Nonstock; 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/customer/add/page.tsx b/src/app/master-data/customer/add/page.tsx new file mode 100644 index 00000000..a1096f02 --- /dev/null +++ b/src/app/master-data/customer/add/page.tsx @@ -0,0 +1,11 @@ +import CustomerForm from "@/components/pages/master-data/customer/form/CustomerForm"; + +const AddCustomer = () => { + return ( +
+ +
+ ); +} + +export default AddCustomer; \ No newline at end of file diff --git a/src/app/master-data/customer/detail/edit/page.tsx b/src/app/master-data/customer/detail/edit/page.tsx new file mode 100644 index 00000000..3fe8de52 --- /dev/null +++ b/src/app/master-data/customer/detail/edit/page.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; +import { CustomerApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import CustomerForm from '@/components/pages/master-data/customer/form/CustomerForm'; + +const CustomerEdit = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const costumerId = searchParams.get('customerId'); + + const { data: costumer, isLoading: isLoadingCostumer } = useSWR( + costumerId, + (id: number) => CustomerApi.getSingle(id) + ); + + if (!costumerId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingCostumer && (!costumer || isResponseError(costumer))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingCostumer && ( + + )} + {!isLoadingCostumer && isResponseSuccess(costumer) && ( + + )} +
+ ); +}; + +export default CustomerEdit; diff --git a/src/app/master-data/customer/detail/page.tsx b/src/app/master-data/customer/detail/page.tsx new file mode 100644 index 00000000..263458c2 --- /dev/null +++ b/src/app/master-data/customer/detail/page.tsx @@ -0,0 +1,45 @@ +'use client' + +import { useRouter, useSearchParams } from "next/navigation"; +import useSWR from "swr"; +import { CustomerApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from "@/lib/api-helper"; +import CustomerForm from "@/components/pages/master-data/customer/form/CustomerForm"; + +const CustomerDetail = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const costumerId = searchParams.get("customerId"); + + const { data: costumer, isLoading: isLoadingCostumer } = useSWR( + costumerId, + (id: number) => CustomerApi.getSingle(id) + ); + + if(!costumerId){ + router.back(); + + return ( +
+ +
+ ); + } + + if(!isLoadingCostumer && (!costumer || isResponseError(costumer))){ + router.replace("/404"); + return; + } + + return ( +
+ {isLoadingCostumer && } + {!isLoadingCostumer && isResponseSuccess(costumer) && ( + + )} +
+ ) +}; + +export default CustomerDetail; diff --git a/src/app/master-data/customer/page.tsx b/src/app/master-data/customer/page.tsx new file mode 100644 index 00000000..b80401f1 --- /dev/null +++ b/src/app/master-data/customer/page.tsx @@ -0,0 +1,11 @@ +import CustomersTable from "@/components/pages/master-data/customer/CustomersTable"; + +const Customer = () => { + return ( +
+ +
+ ) +}; + +export default Customer; \ No newline at end of file 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/add/page.tsx b/src/app/master-data/kandang/add/page.tsx new file mode 100644 index 00000000..238799cd --- /dev/null +++ b/src/app/master-data/kandang/add/page.tsx @@ -0,0 +1,11 @@ +import KandangForm from '@/components/pages/master-data/kandang/form/KandangForm'; + +const AddNonstock = () => { + return ( +
+ +
+ ); +}; + +export default AddNonstock; diff --git a/src/app/master-data/kandang/detail/edit/page.tsx b/src/app/master-data/kandang/detail/edit/page.tsx new file mode 100644 index 00000000..561d6f1f --- /dev/null +++ b/src/app/master-data/kandang/detail/edit/page.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import KandangForm from '@/components/pages/master-data/kandang/form/KandangForm'; + +import { KandangApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const KandangEdit = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const kandangId = searchParams.get('kandangId'); + + const { data: kandang, isLoading: isLoadingKandang } = useSWR( + kandangId, + (id: number) => KandangApi.getSingle(id) + ); + + if (!kandangId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingKandang && (!kandang || isResponseError(kandang))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingKandang && ( + + )} + {!isLoadingKandang && isResponseSuccess(kandang) && ( + + )} +
+ ); +}; + +export default KandangEdit; 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/kandang/detail/page.tsx b/src/app/master-data/kandang/detail/page.tsx new file mode 100644 index 00000000..a5b4f0e9 --- /dev/null +++ b/src/app/master-data/kandang/detail/page.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import KandangForm from '@/components/pages/master-data/kandang/form/KandangForm'; + +import { KandangApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const KandangDetail = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const kandangId = searchParams.get('kandangId'); + + const { data: kandang, isLoading: isLoadingKandang } = useSWR( + kandangId, + (id: number) => KandangApi.getSingle(id) + ); + + if (!kandangId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingKandang && (!kandang || isResponseError(kandang))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingKandang && ( + + )} + {!isLoadingKandang && isResponseSuccess(kandang) && ( + + )} +
+ ); +}; + +export default KandangDetail; diff --git a/src/app/master-data/kandang/page.tsx b/src/app/master-data/kandang/page.tsx new file mode 100644 index 00000000..293eb0da --- /dev/null +++ b/src/app/master-data/kandang/page.tsx @@ -0,0 +1,11 @@ +import KandangsTable from '@/components/pages/master-data/kandang/KandangsTable'; + +const Nonstock = () => { + return ( +
+ +
+ ); +}; + +export default Nonstock; diff --git a/src/app/master-data/location/add/page.tsx b/src/app/master-data/location/add/page.tsx new file mode 100644 index 00000000..56f668fd --- /dev/null +++ b/src/app/master-data/location/add/page.tsx @@ -0,0 +1,11 @@ +import LocationForm from '@/components/pages/master-data/location/form/LocationForm'; + +const AddNonstock = () => { + return ( +
+ +
+ ); +}; + +export default AddNonstock; diff --git a/src/app/master-data/location/detail/edit/page.tsx b/src/app/master-data/location/detail/edit/page.tsx new file mode 100644 index 00000000..a97f5672 --- /dev/null +++ b/src/app/master-data/location/detail/edit/page.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import LocationForm from '@/components/pages/master-data/location/form/LocationForm'; + +import { LocationApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const LocationEdit = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const locationId = searchParams.get('locationId'); + + const { data: location, isLoading: isLoadingLocation } = useSWR( + locationId, + (id: number) => LocationApi.getSingle(id) + ); + + if (!locationId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingLocation && (!location || isResponseError(location))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingLocation && ( + + )} + {!isLoadingLocation && isResponseSuccess(location) && ( + + )} +
+ ); +}; + +export default LocationEdit; 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/location/detail/page.tsx b/src/app/master-data/location/detail/page.tsx new file mode 100644 index 00000000..bb0fbe4c --- /dev/null +++ b/src/app/master-data/location/detail/page.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import LocationForm from '@/components/pages/master-data/location/form/LocationForm'; + +import { LocationApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const LocationDetail = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const locationId = searchParams.get('locationId'); + + const { data: location, isLoading: isLoadingLocation } = useSWR( + locationId, + (id: number) => LocationApi.getSingle(id) + ); + + if (!locationId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingLocation && (!location || isResponseError(location))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingLocation && ( + + )} + {!isLoadingLocation && isResponseSuccess(location) && ( + + )} +
+ ); +}; + +export default LocationDetail; diff --git a/src/app/master-data/location/page.tsx b/src/app/master-data/location/page.tsx new file mode 100644 index 00000000..338fdbff --- /dev/null +++ b/src/app/master-data/location/page.tsx @@ -0,0 +1,11 @@ +import LocationsTable from '@/components/pages/master-data/location/LocationsTable'; + +const Nonstock = () => { + return ( +
+ +
+ ); +}; + +export default Nonstock; diff --git a/src/app/master-data/nonstock/add/page.tsx b/src/app/master-data/nonstock/add/page.tsx new file mode 100644 index 00000000..2bde94ed --- /dev/null +++ b/src/app/master-data/nonstock/add/page.tsx @@ -0,0 +1,11 @@ +import NonstockForm from '@/components/pages/master-data/nonstock/form/NonstockForm'; + +const AddNonstock = () => { + return ( +
+ +
+ ); +}; + +export default AddNonstock; diff --git a/src/app/master-data/nonstock/detail/edit/page.tsx b/src/app/master-data/nonstock/detail/edit/page.tsx new file mode 100644 index 00000000..3b3db5f5 --- /dev/null +++ b/src/app/master-data/nonstock/detail/edit/page.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import NonstockForm from '@/components/pages/master-data/nonstock/form/NonstockForm'; + +import { NonstockApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const NonstockEdit = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const nonstockId = searchParams.get('nonstockId'); + + const { data: nonstock, isLoading: isLoadingNonstock } = useSWR( + nonstockId, + (id: number) => NonstockApi.getSingle(id) + ); + + if (!nonstockId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingNonstock && (!nonstock || isResponseError(nonstock))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingNonstock && ( + + )} + {!isLoadingNonstock && isResponseSuccess(nonstock) && ( + + )} +
+ ); +}; + +export default NonstockEdit; 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 new file mode 100644 index 00000000..798a843e --- /dev/null +++ b/src/app/master-data/nonstock/detail/page.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import NonstockForm from '@/components/pages/master-data/nonstock/form/NonstockForm'; + +import { NonstockApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const NonstockDetail = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const nonstockId = searchParams.get('nonstockId'); + + const { data: nonstock, isLoading: isLoadingNonstock } = useSWR( + nonstockId, + (id: number) => NonstockApi.getSingle(id) + ); + + if (!nonstockId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingNonstock && (!nonstock || isResponseError(nonstock))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingNonstock && ( + + )} + {!isLoadingNonstock && isResponseSuccess(nonstock) && ( + + )} +
+ ); +}; + +export default NonstockDetail; diff --git a/src/app/master-data/nonstock/page.tsx b/src/app/master-data/nonstock/page.tsx new file mode 100644 index 00000000..0812a5e2 --- /dev/null +++ b/src/app/master-data/nonstock/page.tsx @@ -0,0 +1,11 @@ +import NonstocksTable from '@/components/pages/master-data/nonstock/NonstocksTable'; + +const Nonstock = () => { + return ( +
+ +
+ ); +}; + +export default Nonstock; 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/supplier/add/page.tsx b/src/app/master-data/supplier/add/page.tsx new file mode 100644 index 00000000..8a95c3c6 --- /dev/null +++ b/src/app/master-data/supplier/add/page.tsx @@ -0,0 +1,11 @@ +import SupplierForm from '@/components/pages/master-data/supplier/form/SupplierForm'; + +const AddSupplier = () => { + return ( +
+ +
+ ); +}; + +export default AddSupplier; \ No newline at end of file diff --git a/src/app/master-data/supplier/detail/edit/page.tsx b/src/app/master-data/supplier/detail/edit/page.tsx new file mode 100644 index 00000000..103db73d --- /dev/null +++ b/src/app/master-data/supplier/detail/edit/page.tsx @@ -0,0 +1,49 @@ +'use client'; + +import SupplierForm from '@/components/pages/master-data/supplier/form/SupplierForm'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { SupplierApi } from '@/services/api/master-data'; +import { useSearchParams, useRouter } from 'next/navigation'; +import useSWR from 'swr'; + +const SupplierEdit = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + // Get Query Params + const supplierId = searchParams.get('supplierId'); + + // Fetch Data + const { data: supplier, isLoading: isLoadingSupplier } = useSWR( + supplierId, + (id: number) => SupplierApi.getSingle(id) + ); + + if (!supplierId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingSupplier && (!supplier || isResponseError(supplier))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingSupplier && ( + + )} + {!isLoadingSupplier && isResponseSuccess(supplier) && ( + + )} +
+ ); +}; + +export default SupplierEdit; diff --git a/src/app/master-data/supplier/detail/page.tsx b/src/app/master-data/supplier/detail/page.tsx new file mode 100644 index 00000000..433fa043 --- /dev/null +++ b/src/app/master-data/supplier/detail/page.tsx @@ -0,0 +1,49 @@ +'use client'; + +import SupplierForm from '@/components/pages/master-data/supplier/form/SupplierForm'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { SupplierApi } from '@/services/api/master-data'; +import { useSearchParams, useRouter } from 'next/navigation'; +import useSWR from 'swr'; + +const SupplierDetail = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + // Get Query Params + const supplierId = searchParams.get('supplierId'); + + // Fetch Data + const { data: supplier, isLoading: isLoadingSupplier } = useSWR( + supplierId, + (id: number) => SupplierApi.getSingle(id) + ); + + if (!supplierId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingSupplier && (!supplier || isResponseError(supplier))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingSupplier && ( + + )} + {!isLoadingSupplier && isResponseSuccess(supplier) && ( + + )} +
+ ); +}; + +export default SupplierDetail; \ No newline at end of file diff --git a/src/app/master-data/supplier/page.tsx b/src/app/master-data/supplier/page.tsx new file mode 100644 index 00000000..1f54bd0d --- /dev/null +++ b/src/app/master-data/supplier/page.tsx @@ -0,0 +1,11 @@ +import SuppliersTable from "@/components/pages/master-data/supplier/SupplierTable"; + +const Supplier = () => { + return ( +
+ +
+ ); +}; + +export default Supplier; diff --git a/src/app/master-data/uom/add/page.tsx b/src/app/master-data/uom/add/page.tsx new file mode 100644 index 00000000..452aadf8 --- /dev/null +++ b/src/app/master-data/uom/add/page.tsx @@ -0,0 +1,11 @@ +import UomForm from '@/components/pages/master-data/uom/form/UomForm'; + +const AddNonstock = () => { + return ( +
+ +
+ ); +}; + +export default AddNonstock; diff --git a/src/app/master-data/uom/detail/edit/page.tsx b/src/app/master-data/uom/detail/edit/page.tsx new file mode 100644 index 00000000..48d7c823 --- /dev/null +++ b/src/app/master-data/uom/detail/edit/page.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import UomForm from '@/components/pages/master-data/uom/form/UomForm'; + +import { UomApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const UomEdit = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const uomId = searchParams.get('uomId'); + + const { data: uom, isLoading: isLoadingUom } = useSWR(uomId, (id: number) => + UomApi.getSingle(id) + ); + + if (!uomId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingUom && (!uom || isResponseError(uom))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingUom && } + {!isLoadingUom && isResponseSuccess(uom) && ( + + )} +
+ ); +}; + +export default UomEdit; 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/uom/detail/page.tsx b/src/app/master-data/uom/detail/page.tsx new file mode 100644 index 00000000..b02af02b --- /dev/null +++ b/src/app/master-data/uom/detail/page.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import UomForm from '@/components/pages/master-data/uom/form/UomForm'; + +import { UomApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const UomDetail = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const uomId = searchParams.get('uomId'); + + const { data: uom, isLoading: isLoadingUom } = useSWR(uomId, (id: number) => + UomApi.getSingle(id) + ); + + if (!uomId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingUom && (!uom || isResponseError(uom))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingUom && } + {!isLoadingUom && isResponseSuccess(uom) && ( + + )} +
+ ); +}; + +export default UomDetail; diff --git a/src/app/master-data/uom/page.tsx b/src/app/master-data/uom/page.tsx new file mode 100644 index 00000000..689b9d0d --- /dev/null +++ b/src/app/master-data/uom/page.tsx @@ -0,0 +1,11 @@ +import UomsTable from '@/components/pages/master-data/uom/UomsTable'; + +const Nonstock = () => { + return ( +
+ +
+ ); +}; + +export default Nonstock; diff --git a/src/app/master-data/warehouse/add/page.tsx b/src/app/master-data/warehouse/add/page.tsx new file mode 100644 index 00000000..7a8105a1 --- /dev/null +++ b/src/app/master-data/warehouse/add/page.tsx @@ -0,0 +1,11 @@ +import WarehouseForm from '@/components/pages/master-data/warehouse/form/WarehouseForm'; + +const AddNonstock = () => { + return ( +
+ +
+ ); +}; + +export default AddNonstock; diff --git a/src/app/master-data/warehouse/detail/edit/page.tsx b/src/app/master-data/warehouse/detail/edit/page.tsx new file mode 100644 index 00000000..a6498834 --- /dev/null +++ b/src/app/master-data/warehouse/detail/edit/page.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import WarehouseForm from '@/components/pages/master-data/warehouse/form/WarehouseForm'; + +import { WarehouseApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const WarehouseEdit = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const warehouseId = searchParams.get('warehouseId'); + + const { data: warehouse, isLoading: isLoadingWarehouse } = useSWR( + warehouseId, + (id: number) => WarehouseApi.getSingle(id) + ); + + if (!warehouseId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingWarehouse && (!warehouse || isResponseError(warehouse))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingWarehouse && ( + + )} + {!isLoadingWarehouse && isResponseSuccess(warehouse) && ( + + )} +
+ ); +}; + +export default WarehouseEdit; 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/app/master-data/warehouse/detail/page.tsx b/src/app/master-data/warehouse/detail/page.tsx new file mode 100644 index 00000000..5a7c7042 --- /dev/null +++ b/src/app/master-data/warehouse/detail/page.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import WarehouseForm from '@/components/pages/master-data/warehouse/form/WarehouseForm'; + +import { WarehouseApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const WarehouseDetail = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const warehouseId = searchParams.get('warehouseId'); + + const { data: warehouse, isLoading: isLoadingWarehouse } = useSWR( + warehouseId, + (id: number) => WarehouseApi.getSingle(id) + ); + + if (!warehouseId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingWarehouse && (!warehouse || isResponseError(warehouse))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingWarehouse && ( + + )} + {!isLoadingWarehouse && isResponseSuccess(warehouse) && ( + + )} +
+ ); +}; + +export default WarehouseDetail; diff --git a/src/app/master-data/warehouse/page.tsx b/src/app/master-data/warehouse/page.tsx new file mode 100644 index 00000000..eb5ae416 --- /dev/null +++ b/src/app/master-data/warehouse/page.tsx @@ -0,0 +1,11 @@ +import WarehousesTable from '@/components/pages/master-data/warehouse/WarehousesTable'; + +const Warehouse = () => { + return ( +
+ +
+ ); +}; + +export default Warehouse; diff --git a/src/app/page.tsx b/src/app/page.tsx index 8ac3364a..db9638df 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,4 +1,8 @@ +import { redirect } from 'next/navigation'; + export default function Home() { + redirect('/dashboard'); + return (

LTI ERP

diff --git a/src/components/Alert.tsx b/src/components/Alert.tsx new file mode 100644 index 00000000..61792d0c --- /dev/null +++ b/src/components/Alert.tsx @@ -0,0 +1,27 @@ +import { ReactNode } from 'react'; + +import { cn } from '@/lib/helper'; + +interface AlertProps { + variant?: 'outline' | 'dash' | 'soft'; + color?: 'info' | 'success' | 'warning' | 'error'; + children?: ReactNode; + className?: string; +} + +const Alert = ({ children, variant, color, className }: AlertProps) => { + const alertBaseClassName = cn('alert', { + 'alert-soft': variant === 'soft', + 'alert-outline': variant === 'outline', + 'alert-dash': variant === 'dash', + + 'alert-info': color === 'info', + 'alert-success': color === 'success', + 'alert-warning': color === 'warning', + 'alert-error': color === 'error', + }); + + return
{children}
; +}; + +export default Alert; diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 79ecb6d9..c67a29c2 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,4 +1,4 @@ -import react, { JSX } from 'react'; +import react from 'react'; import Link from 'next/link'; @@ -17,11 +17,12 @@ const Button = ({ type, href, variant, - color, + color = 'primary', isLoading, className, disabled, onClick, + ...props }: ButtonProps) => { const btnBaseClassName = cn( 'btn', @@ -49,6 +50,7 @@ const Button = ({ <> {!href && ( + + )} + + ); +}; + +export default Modal; diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx index 0eace3f0..86d3a67a 100644 --- a/src/components/Pagination.tsx +++ b/src/components/Pagination.tsx @@ -32,7 +32,11 @@ const PaginationButton = ({ const EtcPaginationButton = ({ startPage = 0, endPage = 0, - onPageItemClick = (pageNumber: number) => {}, + onPageItemClick, +}: { + startPage: number; + endPage: number; + onPageItemClick: (a: number) => void; }) => { const pages = range(startPage, endPage); @@ -86,9 +90,16 @@ const Pagination = ({ currentPage = 1, totalItems = 0, itemsPerPage = 10, - onPageChange = (pageNumber: number) => {}, + onPageChange, onPrevPage = () => {}, onNextPage = () => {}, +}: { + currentPage: number; + totalItems: number; + itemsPerPage: number; + onPageChange: (pageNumber: number) => void; + onPrevPage: () => void; + onNextPage: () => void; }) => { const totalPages = Math.ceil(totalItems / itemsPerPage) === 0 diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 53665386..cfd77df6 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { ReactNode, useCallback, useEffect, useState } from 'react'; import { flexRender, getCoreRowModel, @@ -10,6 +10,9 @@ import { TableOptions, useReactTable, ColumnDef, + FilterFn, + SortingState, + OnChangeFn, } from '@tanstack/react-table'; import { rankItem } from '@tanstack/match-sorter-utils'; import { Icon } from '@iconify/react'; @@ -30,37 +33,43 @@ interface TableClassNames { paginationClassName?: string; } -// Type for the Table component props -interface TableProps { +export interface TableProps { data: TData[]; - columns: ColumnDef[]; + columns: ColumnDef[]; pageSize?: number; + totalItems?: number; + page?: number; + onPageChange?: (page: number) => void; isLoading?: boolean; fuzzySearchValue?: string | null; onFuzzySearchValueChange?: (value: string) => void; className?: TableClassNames; + emptyContent?: ReactNode; + sorting?: SortingState; + setSorting?: OnChangeFn; + manualSorting?: boolean; } const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; -const fuzzyFilter = ( - row: any, - columnId: string, - value: string, - addMeta: (meta: any) => void -) => { - const itemRank = rankItem(row.getValue(columnId), value); - addMeta({ itemRank }); - return itemRank.passed; -}; +const emptyContentDefaultValue = ( +
+ + Tidak ada data yang dapat ditampilkan... + +
+); const Table = ({ data = [], columns = [], pageSize = 10, + totalItems, + page, + onPageChange, isLoading = false, - fuzzySearchValue = null, - onFuzzySearchValueChange = () => {}, + fuzzySearchValue, + onFuzzySearchValueChange, className = { containerClassName: '', tableWrapperClassName: '', @@ -73,12 +82,30 @@ const Table = ({ bodyColumnClassName: '', paginationClassName: '', }, + emptyContent = emptyContentDefaultValue, + sorting, + setSorting, + manualSorting = false, }: TableProps) => { + const isServerSideTable = + totalItems !== undefined && + page !== undefined && + onPageChange !== undefined; + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: pageSize, }); + const fuzzyFilter: FilterFn = useCallback( + (row, columnId, value, addMeta) => { + const itemRank = rankItem(row.getValue(columnId), value); + addMeta({ itemRank }); + return itemRank.passed; + }, + [] + ); + const tableOptions: TableOptions = { columns, data: isLoading ? (DUMMY_SKELETON_DATA as TData[]) : data, // Type assertion @@ -86,6 +113,7 @@ const Table = ({ getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), onPaginationChange: setPagination, + manualSorting, state: { pagination, globalFilter: fuzzySearchValue, @@ -101,7 +129,46 @@ const Table = ({ tableOptions.getFilteredRowModel = getFilteredRowModel(); } + if (sorting && setSorting) { + tableOptions.onSortingChange = setSorting; + tableOptions.state = { + ...tableOptions.state, + sorting, + }; + } + const table = useReactTable(tableOptions); + const { setPageSize } = table; + + const prevPageClickHandler = () => { + table.previousPage(); + + if (isServerSideTable) { + onPageChange(page - 1); + } + }; + + const nextPageClickHandler = () => { + table.nextPage(); + + if (isServerSideTable) { + onPageChange(page + 1); + } + }; + + const pageChangeHandler = (pageNumber: number) => { + const currentPage = pageNumber - 1; + + table.setPageIndex(pageNumber ? currentPage : 0); + + if (isServerSideTable) { + onPageChange(pageNumber); + } + }; + + useEffect(() => { + setPageSize(pageSize); + }, [pageSize, setPageSize]); return (
@@ -178,17 +245,23 @@ const Table = ({
- {data.length > 0 && !isLoading && ( + {(data.length === 0 || table.getRowModel().rows.length === 0) && + !isLoading && + emptyContent} + + {data.length > 0 && table.getRowModel().rows.length > 0 && !isLoading && (
table.previousPage()} - onNextPage={() => table.nextPage()} - onPageChange={(pageNumber) => - table.setPageIndex(pageNumber ? pageNumber - 1 : 0) + currentPage={ + isServerSideTable + ? page + : table.getState().pagination.pageIndex + 1 } + onPrevPage={prevPageClickHandler} + onNextPage={nextPageClickHandler} + onPageChange={pageChangeHandler} />
)} diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx new file mode 100644 index 00000000..1d9d86b4 --- /dev/null +++ b/src/components/helper/RequireAuth.tsx @@ -0,0 +1,197 @@ +'use client'; + +import { ReactNode, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import useSWRImmutable from 'swr/immutable'; + +import { useAuth } from '@/services/hooks/useAuth'; +import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { GetMeResponse } from '@/types/api/api-general'; + +// TODO: delete this later, DONT HARDCODE USER DATA +const DUMMY_USER = { + id: 1, + email: 'admin@mbugroup.id', + npk: '0001', + name: 'Super Admin', + image: null, + created_at: '2025-09-30T03:24:20.899229Z', + updated_at: '2025-09-30T03:24:20.899229Z', + roles: [ + { + id: 1, + key: 'mbu.super_admin', + name: 'MBU Administrator', + client: { + id: 1, + name: 'PT Mitra Berlian Unggas', + alias: 'MBU', + }, + permissions: [ + { + id: 1, + name: 'mbu:purchase:read', + action: 'read', + client: { + id: 1, + name: 'PT Mitra Berlian Unggas', + alias: 'MBU', + }, + }, + { + id: 2, + name: 'mbu:purchase:create', + action: 'create', + client: { + id: 1, + name: 'PT Mitra Berlian Unggas', + alias: 'MBU', + }, + }, + { + id: 3, + name: 'mbu:purchase:approve', + action: 'approve', + client: { + id: 1, + name: 'PT Mitra Berlian Unggas', + alias: 'MBU', + }, + }, + ], + }, + { + id: 2, + key: 'lti.super_admin', + name: 'LTI Administrator', + client: { + id: 2, + name: 'PT Lumbung Telur Indonesia', + alias: 'LTI', + }, + permissions: [ + { + id: 4, + name: 'lti:purchase:read', + action: 'read', + client: { + id: 2, + name: 'PT Lumbung Telur Indonesia', + alias: 'LTI', + }, + }, + { + id: 5, + name: 'lti:purchase:create', + action: 'create', + client: { + id: 2, + name: 'PT Lumbung Telur Indonesia', + alias: 'LTI', + }, + }, + { + id: 6, + name: 'lti:purchase:approve', + action: 'approve', + client: { + id: 2, + name: 'PT Lumbung Telur Indonesia', + alias: 'LTI', + }, + }, + ], + }, + { + id: 3, + key: 'manbu.super_admin', + name: 'MANBU Administrator', + client: { + id: 3, + name: 'PT Mandiri Berlian Unggas', + alias: 'MANBU', + }, + permissions: [ + { + id: 7, + name: 'manbu:purchase:read', + action: 'read', + client: { + id: 3, + name: 'PT Mandiri Berlian Unggas', + alias: 'MANBU', + }, + }, + { + id: 8, + name: 'manbu:purchase:create', + action: 'create', + client: { + id: 3, + name: 'PT Mandiri Berlian Unggas', + alias: 'MANBU', + }, + }, + { + id: 9, + name: 'manbu:purchase:approve', + action: 'approve', + client: { + id: 3, + name: 'PT Mandiri Berlian Unggas', + alias: 'MANBU', + }, + }, + ], + }, + ], +}; + +interface RequireAuthProps { + children?: ReactNode; +} + +const RequireAuth = ({ children }: RequireAuthProps) => { + const router = useRouter(); + const { setUser, setIsLoadingUser } = useAuth(); + + const { data: userResponse, isLoading: isLoadingUserResponse } = + useSWRImmutable( + '/auth/get-me', + httpClientFetcher, + { + shouldRetryOnError: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshInterval: 0, + } + ); + + useEffect(() => { + setIsLoadingUser(isLoadingUserResponse); + }, [isLoadingUserResponse, setIsLoadingUser]); + + useEffect(() => { + if (isResponseSuccess(userResponse)) { + setUser(userResponse.data); + } else { + // router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string); + // TODO: remove this later, DONT HARDCODE USER DATA + setUser(DUMMY_USER); + } + }, [userResponse, setIsLoadingUser, setUser]); + + // TODO: uncomment this later + // if (isLoadingUserResponse && !userResponse) { + // return ( + //
+ // + //
+ // ); + // } + + return <>{children}; +}; + +export default RequireAuth; \ No newline at end of file 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/DebouncedTextInput.tsx b/src/components/input/DebouncedTextInput.tsx new file mode 100644 index 00000000..4b62aaf7 --- /dev/null +++ b/src/components/input/DebouncedTextInput.tsx @@ -0,0 +1,42 @@ +'use client'; + +import { ChangeEvent, ChangeEventHandler, useEffect, useState } from 'react'; +import { useDebounce } from 'use-debounce'; + +import TextInput, { TextInputProps } from '@/components/input/TextInput'; + +interface DebouncedTextInputProps extends TextInputProps { + delay?: number; +} + +const DebouncedTextInput = (props: DebouncedTextInputProps) => { + const { delay, onChange } = props; + + const [internalChangeEvent, setInternalChangeEvent] = + useState>(); + const [internalValue, setInternalValue] = useState(props.value); + + const [debouncedChangeEvent] = useDebounce(internalChangeEvent, delay ?? 300); + const [debouncedValue] = useDebounce(internalValue, delay ?? 300); + + const internalChangeHandler: ChangeEventHandler = (e) => { + setInternalValue(e.target.value); + setInternalChangeEvent(e); + }; + + useEffect(() => { + if (debouncedChangeEvent) { + onChange?.(debouncedChangeEvent); + } + }, [debouncedValue]); + + return ( + + ); +}; + +export default DebouncedTextInput; diff --git a/src/components/input/PasswordInput.tsx b/src/components/input/PasswordInput.tsx index 993915fc..86be59d7 100644 --- a/src/components/input/PasswordInput.tsx +++ b/src/components/input/PasswordInput.tsx @@ -6,8 +6,10 @@ import { Icon } from '@iconify/react'; import TextInput, { TextInputProps } from '@/components/input/TextInput'; import Button from '@/components/Button'; -interface PasswordInputProps - extends Omit {} +type PasswordInputProps = Omit< + TextInputProps, + 'type' | 'startAdornment' | 'endAdornment' +>; const PasswordInput = (props: PasswordInputProps) => { const [type, setType] = useState('password'); diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx index 5b6ae098..930b5ed5 100644 --- a/src/components/input/SelectInput.tsx +++ b/src/components/input/SelectInput.tsx @@ -1,8 +1,9 @@ 'use client'; -import { ComponentType, ReactNode, useMemo } from 'react'; -import Select, { OptionProps, GroupBase } from 'react-select'; +import { ComponentType, ReactNode, useEffect, useMemo, useState } from 'react'; +import Select, { OptionProps, GroupBase, InputActionMeta } from 'react-select'; import makeAnimated from 'react-select/animated'; +import { useDebounce } from 'use-debounce'; import { cn } from '@/lib/helper'; @@ -41,6 +42,8 @@ interface SelectInputProps { errorMessage?: string; isAnimated?: boolean; openMenu?: boolean; + delay?: number; + onInputChange?: (search: string) => void; } const animatedComponents = makeAnimated(); @@ -65,7 +68,13 @@ const SelectInput = ({ errorMessage, isAnimated = true, openMenu, + delay = 300, + onInputChange, }: SelectInputProps) => { + const [internalInputValue, setInternalInputValue] = useState(''); + + const [debouncedInputValue] = useDebounce(internalInputValue, delay ?? 300); + const components = useMemo(() => { const base = isAnimated ? animatedComponents : {}; @@ -75,6 +84,14 @@ const SelectInput = ({ }; }, [isAnimated]); + const internalInputChangeHandler = (value: string, meta: InputActionMeta) => { + if (meta.action === 'input-change') setInternalInputValue(value); + if (meta.action === 'menu-close') setInternalInputValue(''); + }; + + useEffect(() => { + onInputChange?.(debouncedInputValue); + }, [debouncedInputValue]); return (
({ onChange={(val) => onChange?.(val as T)} options={options} menuIsOpen={openMenu} + inputValue={internalInputValue} + onInputChange={internalInputChangeHandler} isMulti={isMulti} isDisabled={isDisabled} isLoading={isLoading} diff --git a/src/components/input/TagInput.tsx b/src/components/input/TagInput.tsx new file mode 100644 index 00000000..a14b2f63 --- /dev/null +++ b/src/components/input/TagInput.tsx @@ -0,0 +1,169 @@ +'use client'; + +import React, { useState, KeyboardEvent, ChangeEvent, useEffect } from 'react'; +import { cn } from '@/lib/helper'; + +export interface TagInputProps { + label?: string; + bottomLabel?: string; + name: string; + value?: string; + placeholder?: string; + className?: { + wrapper?: string; + label?: string; + inputWrapper?: string; + input?: string; + }; + isError?: boolean; + isValid?: boolean; + disabled?: boolean; + readOnly?: boolean; + required?: boolean; + isLoading?: boolean; + errorMessage?: string; + onChange?: (value: string) => void; +} + +const TagInput: React.FC = ({ + label, + bottomLabel, + name, + value = '', + placeholder, + className, + isError, + isValid, + errorMessage, + disabled = false, + readOnly = false, + required = false, + onChange, +}) => { + const [tags, setTags] = useState(value ? value.split(',') : []); + const [inputValue, setInputValue] = useState(''); + + useEffect(() => { + if (value !== undefined && value !== tags.join(',')) { + setTags(value ? value.split(',') : []); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]); + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ',') { + e.preventDefault(); + const newTag = inputValue.trim(); + if (newTag && !tags.includes(newTag)) { + const updatedTags = [...tags, newTag]; + setTags(updatedTags); + onChange?.(updatedTags.join(',')); + } + setInputValue(''); + } + }; + + const handleRemoveTag = (tagToRemove: string) => { + const updatedTags = tags.filter((t) => t !== tagToRemove); + setTags(updatedTags); + onChange?.(updatedTags.join(',')); + }; + + const handleInputChange = (e: ChangeEvent) => { + setInputValue(e.target.value); + }; + + return ( +
+ {/* Label */} + {label && ( + + )} + + {/* Input wrapper */} +
{ + // Fokuskan input saat area diklik + const inputEl = document.getElementById(name); + inputEl?.focus(); + }} + > + {tags.map((tag) => ( +
+ {tag} + {!readOnly && ( + + )} +
+ ))} + + {!readOnly && ( + + )} +
+ + {/* Bottom label or error message */} + {!isError && bottomLabel && ( +

{bottomLabel}

+ )} + {isError &&

{errorMessage}

} +
+ ); +}; + +export default TagInput; diff --git a/src/components/input/TextArea.tsx b/src/components/input/TextArea.tsx new file mode 100644 index 00000000..b4a6c9f5 --- /dev/null +++ b/src/components/input/TextArea.tsx @@ -0,0 +1,124 @@ +'use client'; + +import { + ChangeEventHandler, + FocusEventHandler, + ReactNode, +} from 'react'; + +import { cn } from '@/lib/helper'; + +export interface TextAreaProps { + label?: string; + bottomLabel?: string; + name: string; + value?: string | number; + placeholder?: string; + className?: { + wrapper?: string; + label?: string; + inputWrapper?: string; + input?: string; + }; + isError?: boolean; + isValid?: boolean; + disabled?: boolean; + readOnly?: boolean; + required?: boolean; + isLoading?: boolean; + errorMessage?: string; + startAdornment?: ReactNode; + endAdornment?: ReactNode; + onChange?: ChangeEventHandler; + onBlur?: FocusEventHandler; + cols?: number; +} + +const TextArea = ({ + label, + bottomLabel, + name, + value, + placeholder, + className, + isError, + isValid, + errorMessage, + startAdornment, + endAdornment, + disabled = false, + required = false, + onChange, + onBlur, + readOnly = false, + isLoading = false, + cols = 3 +}: TextAreaProps) => { + return ( +
+ {label && ( + + )} + {startAdornment && startAdornment} + +