@@ -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}`; |