| @@ -1,2 +1,3 @@ | |||
| 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 | |||
| 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 Stack from "@mui/material/Stack"; | |||
| import Breadcrumb from "@/components/Breadcrumb"; | |||
| import {AxiosProvider} from "@/app/(main)/axios/AxiosProvider"; | |||
| export default async function MainLayout({ | |||
| children, | |||
| @@ -19,23 +20,25 @@ export default async function MainLayout({ | |||
| } | |||
| 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"; | |||
| import { useCallback, useMemo, useState } from "react"; | |||
| import {useCallback, useEffect, useMemo, useState} from "react"; | |||
| 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 SearchResults, { Column } from "../SearchResults"; | |||
| import { EditNote } from "@mui/icons-material"; | |||
| import { useRouter, useSearchParams } from "next/navigation"; | |||
| import { GridDeleteIcon } from "@mui/x-data-grid"; | |||
| 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 = { | |||
| items: ItemsResult[]; | |||
| @@ -20,6 +23,11 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||
| const [filteredItems, setFilteredItems] = useState<ItemsResult[]>(items); | |||
| const { t } = useTranslation("items"); | |||
| const router = useRouter(); | |||
| const [filterObj, setFilterObj] = useState(); | |||
| const [pagingController, setPagingController] = useState({ | |||
| pageNum: 1, | |||
| pageSize: 10, | |||
| }) | |||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||
| () => { | |||
| @@ -70,27 +78,61 @@ const ItemsSearch: React.FC<Props> = ({ items }) => { | |||
| [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 ( | |||
| <> | |||
| <SearchBox | |||
| criteria={searchCriteria} | |||
| 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} | |||
| /> | |||
| <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 { SubmitHandler, useForm } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import {SetupAxiosInterceptors} from "@/app/(main)/axios/axiosInstance"; | |||
| import {useToken} from "@/app/(main)/axios/AxiosProvider"; | |||
| type LoginFields = { | |||
| username: string; | |||
| @@ -47,6 +49,7 @@ const LoginForm: React.FC = () => { | |||
| const [serverError, setServerError] = useState<string>(); | |||
| const router = useRouter(); | |||
| const { setAccessToken } = useToken(); | |||
| const onSubmit: SubmitHandler<LoginFields> = async (data) => { | |||
| const res = await signIn("credentials", { | |||
| @@ -62,7 +65,10 @@ const LoginForm: React.FC = () => { | |||
| // set auth to local storage | |||
| const session = await getSession() as SessionWithAbilities | |||
| // @ts-ignore | |||
| window.localStorage.setItem("accessToken", session?.accessToken) | |||
| setAccessToken(session?.accessToken); | |||
| SetupAxiosInterceptors(session?.accessToken); | |||
| // console.log(session) | |||
| window.localStorage.setItem("abilities", JSON.stringify(session?.abilities)) | |||
| @@ -48,6 +48,8 @@ function SearchResults<T extends ResultWithId>({ | |||
| items, | |||
| columns, | |||
| noWrapper, | |||
| pagingController, | |||
| setPagingController | |||
| }: Props<T>) { | |||
| const [page, setPage] = React.useState(0); | |||
| const [rowsPerPage, setRowsPerPage] = React.useState(10); | |||
| @@ -57,6 +59,10 @@ function SearchResults<T extends ResultWithId>({ | |||
| newPage, | |||
| ) => { | |||
| setPage(newPage); | |||
| setPagingController({ | |||
| ...pagingController, | |||
| pageNum: newPage, | |||
| }) | |||
| }; | |||
| const handleChangeRowsPerPage: TablePaginationProps["onRowsPerPageChange"] = ( | |||
| @@ -64,6 +70,10 @@ function SearchResults<T extends ResultWithId>({ | |||
| ) => { | |||
| setRowsPerPage(+event.target.value); | |||
| setPage(0); | |||
| setPagingController({ | |||
| ...pagingController, | |||
| pageNum: +event.target.value, | |||
| }) | |||
| }; | |||
| const table = ( | |||
| @@ -1,2 +1,3 @@ | |||
| export const BASE_API_URL = `${process.env.API_URL}`; | |||
| export const LOGIN_API_PATH = `${BASE_API_URL}/login`; | |||
| export const NEXT_PUBLIC_API_URL= `${process.env.NEXT_PUBLIC_API_URL}`; | |||