| @@ -1,2 +1,3 @@ | |||||
| API_URL=http://localhost:8090/api | API_URL=http://localhost:8090/api | ||||
| NEXTAUTH_SECRET=secret | |||||
| NEXTAUTH_SECRET=secret | |||||
| NEXT_PUBLIC_API_URL=http://localhost:8090/api | |||||
| @@ -1,3 +1,4 @@ | |||||
| API_URL=http://localhost:8090/api | API_URL=http://localhost:8090/api | ||||
| NEXTAUTH_SECRET=secret | NEXTAUTH_SECRET=secret | ||||
| NEXTAUTH_URL=https://fpsms-uat.2fi-solutions.com | |||||
| NEXTAUTH_URL=https://fpsms-uat.2fi-solutions.com | |||||
| NEXT_PUBLIC_API_URL=http://localhost:8090/api | |||||
| @@ -0,0 +1,38 @@ | |||||
| "use client"; | |||||
| import React, {createContext, useContext, useEffect, useState} from 'react'; | |||||
| import axiosInstance, {SetupAxiosInterceptors} from './axiosInstance'; | |||||
| const AxiosContext = createContext(axiosInstance); | |||||
| const TokenContext = createContext({ | |||||
| setAccessToken: (token: string | null) => {}, | |||||
| }); | |||||
| export const AxiosProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { | |||||
| const [accessToken, setAccessToken] = useState<string | null>(localStorage.getItem("accessToken")); | |||||
| useEffect(() => { | |||||
| if (accessToken) { | |||||
| axiosInstance.defaults.headers.Authorization = `Bearer ${accessToken}`; | |||||
| SetupAxiosInterceptors(accessToken); | |||||
| console.log("[debug] Updated accessToken:", accessToken); | |||||
| } | |||||
| }, [accessToken]); | |||||
| return ( | |||||
| <AxiosContext.Provider value={axiosInstance}> | |||||
| <TokenContext.Provider value={{ setAccessToken }}> | |||||
| {children} | |||||
| </TokenContext.Provider> | |||||
| </AxiosContext.Provider> | |||||
| ); | |||||
| }; | |||||
| // Custom hook to use Axios instance | |||||
| export const useAxios = () => { | |||||
| return useContext(AxiosContext); | |||||
| }; | |||||
| // Custom hook to manage access token | |||||
| export const useToken = () => { | |||||
| return useContext(TokenContext); | |||||
| }; | |||||
| @@ -0,0 +1,56 @@ | |||||
| import axios from 'axios'; | |||||
| const axiosInstance = axios.create({ | |||||
| baseURL: process.env.API_URL, | |||||
| }); | |||||
| // Clear existing interceptors to prevent multiple registrations | |||||
| const clearInterceptors = () => { | |||||
| const requestInterceptor = axiosInstance.interceptors.request.use(); | |||||
| const responseInterceptor = axiosInstance.interceptors.response.use(); | |||||
| if (requestInterceptor) { | |||||
| axiosInstance.interceptors.request.eject(requestInterceptor); | |||||
| } | |||||
| if (responseInterceptor) { | |||||
| axiosInstance.interceptors.response.eject(responseInterceptor); | |||||
| } | |||||
| }; | |||||
| export const SetupAxiosInterceptors = (inputToken: string | null) => { | |||||
| console.log("[debug] set up interceptors", inputToken); | |||||
| // Clear existing interceptors | |||||
| clearInterceptors(); | |||||
| // Request interceptor | |||||
| axiosInstance.interceptors.request.use( | |||||
| (config) => { | |||||
| // Use the token passed in or retrieve from localStorage | |||||
| const token = inputToken ?? localStorage.getItem('accessToken'); | |||||
| if (token) { | |||||
| config.headers.Authorization = `Bearer ${token}`; | |||||
| } | |||||
| return config; | |||||
| }, | |||||
| (error) => { | |||||
| return Promise.reject(error); | |||||
| } | |||||
| ); | |||||
| console.log("[debug] set up interceptors2", inputToken); | |||||
| // Response interceptor | |||||
| axiosInstance.interceptors.response.use( | |||||
| (response) => { | |||||
| return response; | |||||
| }, | |||||
| (error) => { | |||||
| console.error('API Error:', error.response?.data || error); | |||||
| return Promise.reject(error); | |||||
| } | |||||
| ); | |||||
| }; | |||||
| export default axiosInstance; | |||||
| @@ -6,6 +6,7 @@ import Box from "@mui/material/Box"; | |||||
| import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; | import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; | ||||
| import Stack from "@mui/material/Stack"; | import Stack from "@mui/material/Stack"; | ||||
| import Breadcrumb from "@/components/Breadcrumb"; | import Breadcrumb from "@/components/Breadcrumb"; | ||||
| import {AxiosProvider} from "@/app/(main)/axios/AxiosProvider"; | |||||
| export default async function MainLayout({ | export default async function MainLayout({ | ||||
| children, | children, | ||||
| @@ -19,23 +20,25 @@ export default async function MainLayout({ | |||||
| } | } | ||||
| return ( | return ( | ||||
| <> | |||||
| <AppBar | |||||
| profileName={session.user.name!} | |||||
| avatarImageSrc={session.user.image || undefined} | |||||
| /> | |||||
| <Box | |||||
| component="main" | |||||
| sx={{ | |||||
| marginInlineStart: { xs: 0, xl: NAVIGATION_CONTENT_WIDTH }, | |||||
| padding: { xs: "1rem", sm: "1.5rem", lg: "3rem" }, | |||||
| }} | |||||
| > | |||||
| <Stack spacing={2}> | |||||
| <Breadcrumb /> | |||||
| {children} | |||||
| </Stack> | |||||
| </Box> | |||||
| </> | |||||
| <AxiosProvider> | |||||
| <> | |||||
| <AppBar | |||||
| profileName={session.user.name!} | |||||
| avatarImageSrc={session.user.image || undefined} | |||||
| /> | |||||
| <Box | |||||
| component="main" | |||||
| sx={{ | |||||
| marginInlineStart: { xs: 0, xl: NAVIGATION_CONTENT_WIDTH }, | |||||
| padding: { xs: "1rem", sm: "1.5rem", lg: "3rem" }, | |||||
| }} | |||||
| > | |||||
| <Stack spacing={2}> | |||||
| <Breadcrumb /> | |||||
| {children} | |||||
| </Stack> | |||||
| </Box> | |||||
| </> | |||||
| </AxiosProvider> | |||||
| ); | ); | ||||
| } | } | ||||
| @@ -1,14 +1,17 @@ | |||||
| "use client"; | "use client"; | ||||
| import { useCallback, useMemo, useState } from "react"; | |||||
| import {useCallback, useEffect, useMemo, useState} from "react"; | |||||
| import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
| import { ItemsResult } from "@/app/api/settings/item"; | |||||
| import {fetchAllItemsByPage, ItemsResult} from "@/app/api/settings/item"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import SearchResults, { Column } from "../SearchResults"; | import SearchResults, { Column } from "../SearchResults"; | ||||
| import { EditNote } from "@mui/icons-material"; | import { EditNote } from "@mui/icons-material"; | ||||
| import { useRouter, useSearchParams } from "next/navigation"; | import { useRouter, useSearchParams } from "next/navigation"; | ||||
| import { GridDeleteIcon } from "@mui/x-data-grid"; | import { GridDeleteIcon } from "@mui/x-data-grid"; | ||||
| import { TypeEnum } from "@/app/utils/typeEnum"; | import { TypeEnum } from "@/app/utils/typeEnum"; | ||||
| import axios from "axios"; | |||||
| import {BASE_API_URL, NEXT_PUBLIC_API_URL} from "@/config/api"; | |||||
| import axiosInstance from "@/app/(main)/axios/axiosInstance"; | |||||
| type Props = { | type Props = { | ||||
| items: ItemsResult[]; | items: ItemsResult[]; | ||||
| @@ -20,6 +23,11 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||||
| const [filteredItems, setFilteredItems] = useState<ItemsResult[]>(items); | const [filteredItems, setFilteredItems] = useState<ItemsResult[]>(items); | ||||
| const { t } = useTranslation("items"); | const { t } = useTranslation("items"); | ||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const [filterObj, setFilterObj] = useState(); | |||||
| const [pagingController, setPagingController] = useState({ | |||||
| pageNum: 1, | |||||
| pageSize: 10, | |||||
| }) | |||||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | ||||
| () => { | () => { | ||||
| @@ -70,27 +78,61 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||||
| [filteredItems] | [filteredItems] | ||||
| ); | ); | ||||
| const onReset = useCallback(() => { | |||||
| setFilteredItems(items); | |||||
| }, [items]); | |||||
| useEffect(() => { | |||||
| if (filterObj) { | |||||
| refetchData(filterObj); | |||||
| } | |||||
| }, [filterObj, pagingController]); | |||||
| const refetchData = async (filterObj: SearchQuery) => { | |||||
| // Make sure the API endpoint is correct | |||||
| const params ={ | |||||
| pageNum: pagingController.pageNum, | |||||
| pageSize: pagingController.pageSize, | |||||
| ...filterObj, | |||||
| } | |||||
| try { | |||||
| console.log("[debug] axiosInstance", axiosInstance) | |||||
| const response = await axiosInstance.get<ItemsResult[]>(`${NEXT_PUBLIC_API_URL}/items/getRecordByPage`, { params }); | |||||
| console.log("[debug] resposne", response) | |||||
| setFilteredItems(response.data.records); | |||||
| return response; // Return the data from the response | |||||
| } catch (error) { | |||||
| console.error('Error fetching items:', error); | |||||
| throw error; // Rethrow the error for further handling | |||||
| } | |||||
| }; | |||||
| const onReset = useCallback(() => { | |||||
| setFilteredItems(items); | |||||
| }, [items]); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <SearchBox | <SearchBox | ||||
| criteria={searchCriteria} | criteria={searchCriteria} | ||||
| onSearch={(query) => { | onSearch={(query) => { | ||||
| setFilteredItems( | |||||
| items.filter((pm) => { | |||||
| return ( | |||||
| pm.code.toLowerCase().includes(query.code.toLowerCase()) && | |||||
| pm.name.toLowerCase().includes(query.name.toLowerCase()) | |||||
| ); | |||||
| // setFilteredItems( | |||||
| // items.filter((pm) => { | |||||
| // return ( | |||||
| // pm.code.toLowerCase().includes(query.code.toLowerCase()) && | |||||
| // pm.name.toLowerCase().includes(query.name.toLowerCase()) | |||||
| // ); | |||||
| // }) | |||||
| // ); | |||||
| // @ts-ignore | |||||
| setFilterObj({ | |||||
| ...query | |||||
| }) | }) | ||||
| ); | |||||
| }} | }} | ||||
| onReset={onReset} | onReset={onReset} | ||||
| /> | /> | ||||
| <SearchResults<ItemsResult> items={filteredItems} columns={columns} /> | |||||
| <SearchResults<ItemsResult> | |||||
| items={filteredItems} | |||||
| columns={columns} | |||||
| setPagingController={setPagingController} | |||||
| pagingController={pagingController} | |||||
| /> | |||||
| </> | </> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -12,6 +12,8 @@ import { useRouter } from "next/navigation"; | |||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||
| import { SubmitHandler, useForm } from "react-hook-form"; | import { SubmitHandler, useForm } from "react-hook-form"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import {SetupAxiosInterceptors} from "@/app/(main)/axios/axiosInstance"; | |||||
| import {useToken} from "@/app/(main)/axios/AxiosProvider"; | |||||
| type LoginFields = { | type LoginFields = { | ||||
| username: string; | username: string; | ||||
| @@ -47,6 +49,7 @@ const LoginForm: React.FC = () => { | |||||
| const [serverError, setServerError] = useState<string>(); | const [serverError, setServerError] = useState<string>(); | ||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const { setAccessToken } = useToken(); | |||||
| const onSubmit: SubmitHandler<LoginFields> = async (data) => { | const onSubmit: SubmitHandler<LoginFields> = async (data) => { | ||||
| const res = await signIn("credentials", { | const res = await signIn("credentials", { | ||||
| @@ -62,7 +65,10 @@ const LoginForm: React.FC = () => { | |||||
| // set auth to local storage | // set auth to local storage | ||||
| const session = await getSession() as SessionWithAbilities | const session = await getSession() as SessionWithAbilities | ||||
| // @ts-ignore | |||||
| window.localStorage.setItem("accessToken", session?.accessToken) | |||||
| setAccessToken(session?.accessToken); | |||||
| SetupAxiosInterceptors(session?.accessToken); | |||||
| // console.log(session) | // console.log(session) | ||||
| window.localStorage.setItem("abilities", JSON.stringify(session?.abilities)) | window.localStorage.setItem("abilities", JSON.stringify(session?.abilities)) | ||||
| @@ -48,6 +48,8 @@ function SearchResults<T extends ResultWithId>({ | |||||
| items, | items, | ||||
| columns, | columns, | ||||
| noWrapper, | noWrapper, | ||||
| pagingController, | |||||
| setPagingController | |||||
| }: Props<T>) { | }: Props<T>) { | ||||
| const [page, setPage] = React.useState(0); | const [page, setPage] = React.useState(0); | ||||
| const [rowsPerPage, setRowsPerPage] = React.useState(10); | const [rowsPerPage, setRowsPerPage] = React.useState(10); | ||||
| @@ -57,6 +59,10 @@ function SearchResults<T extends ResultWithId>({ | |||||
| newPage, | newPage, | ||||
| ) => { | ) => { | ||||
| setPage(newPage); | setPage(newPage); | ||||
| setPagingController({ | |||||
| ...pagingController, | |||||
| pageNum: newPage, | |||||
| }) | |||||
| }; | }; | ||||
| const handleChangeRowsPerPage: TablePaginationProps["onRowsPerPageChange"] = ( | const handleChangeRowsPerPage: TablePaginationProps["onRowsPerPageChange"] = ( | ||||
| @@ -64,6 +70,10 @@ function SearchResults<T extends ResultWithId>({ | |||||
| ) => { | ) => { | ||||
| setRowsPerPage(+event.target.value); | setRowsPerPage(+event.target.value); | ||||
| setPage(0); | setPage(0); | ||||
| setPagingController({ | |||||
| ...pagingController, | |||||
| pageNum: +event.target.value, | |||||
| }) | |||||
| }; | }; | ||||
| const table = ( | const table = ( | ||||
| @@ -1,2 +1,3 @@ | |||||
| export const BASE_API_URL = `${process.env.API_URL}`; | export const BASE_API_URL = `${process.env.API_URL}`; | ||||
| export const LOGIN_API_PATH = `${BASE_API_URL}/login`; | export const LOGIN_API_PATH = `${BASE_API_URL}/login`; | ||||
| export const NEXT_PUBLIC_API_URL= `${process.env.NEXT_PUBLIC_API_URL}`; | |||||