diff --git a/app/AuthBootstrap.tsx b/app/AuthBootstrap.tsx index bec1b62..8327fca 100644 --- a/app/AuthBootstrap.tsx +++ b/app/AuthBootstrap.tsx @@ -5,7 +5,7 @@ import { useEffect, useRef } from "react"; import { useDispatch } from "react-redux"; import { AppDispatch } from "@/app/redux/types"; import { validateAuth } from "./redux/auth/authSlice"; - +import { fetchMetadata } from "./redux/metadata/metadataSlice"; export function AuthBootstrap() { const dispatch = useDispatch(); const startedRef = useRef(false); @@ -18,7 +18,7 @@ export function AuthBootstrap() { // Initial validate on mount dispatch(validateAuth()); - + // dispatch(fetchMetadata()); // Refresh on window focus (nice UX) const onFocus = () => dispatch(validateAuth()); window.addEventListener("focus", onFocus); diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index 875d48c..b3a9b41 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -1,6 +1,5 @@ import { NextResponse } from "next/server"; import { cookies } from "next/headers"; -import { randomUUID } from "crypto"; import { formatPhoneDisplay } from "@/app/features/UserRoles/utils"; const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:8583"; diff --git a/app/api/dashboard/admin/users/route.ts b/app/api/dashboard/admin/users/route.ts index 8c128da..979d8e9 100644 --- a/app/api/dashboard/admin/users/route.ts +++ b/app/api/dashboard/admin/users/route.ts @@ -20,21 +20,13 @@ export async function GET(request: Request) { const url = new URL(request.url); const queryString = url.search ? url.search : ""; - console.log( - "[DEBUG] - URL", - `${BE_BASE_URL}/api/v1/users/list${queryString}` - ); - - const response = await fetch( - `${BE_BASE_URL}/api/v1/users/list${queryString}`, - { - method: "GET", - headers: { - Authorization: `Bearer ${token}`, - }, - cache: "no-store", - } - ); + const response = await fetch(`${BE_BASE_URL}/api/v1/users${queryString}`, { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + }, + cache: "no-store", + }); console.log("[DEBUG] - Response", response); const data = await response.json(); diff --git a/app/api/metadata/route.ts b/app/api/metadata/route.ts new file mode 100644 index 0000000..26277e4 --- /dev/null +++ b/app/api/metadata/route.ts @@ -0,0 +1,48 @@ +import { NextResponse } from "next/server"; + +// Proxy to backend metadata endpoint. Assumes BACKEND_BASE_URL is set. +export async function GET() { + const { cookies } = await import("next/headers"); + const cookieStore = await cookies(); + const token = cookieStore.get("auth_token")?.value; + const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:5000"; + + if (!token) { + return NextResponse.json( + { success: false, message: "No token found" }, + { status: 401 } + ); + } + + const url = `${BE_BASE_URL.replace(/\/$/, "")}/api/v1/metadata`; + + try { + const res = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + cache: "no-store", + }); + + const data = await res.json(); + + console.log("metadata", data); + + if (!res.ok) { + return NextResponse.json( + { + success: false, + message: data?.message || "Failed to fetch metadata", + }, + { status: res.status } + ); + } + + return NextResponse.json(data); + } catch (err) { + const message = (err as Error)?.message || "Metadata proxy error"; + return NextResponse.json({ success: false, message }, { status: 500 }); + } +} diff --git a/app/components/PageLinks/PageLinks.tsx b/app/components/PageLinks/PageLinks.tsx index f2ef968..34f319f 100644 --- a/app/components/PageLinks/PageLinks.tsx +++ b/app/components/PageLinks/PageLinks.tsx @@ -1,9 +1,9 @@ -// app/components/PageLinks/PageLinks.tsx "use client"; - import Link from "next/link"; -import { ISidebarLink } from "@/app/features/dashboard/sidebar/SidebarLink.interfaces"; // Keep this import import clsx from "clsx"; // Utility to merge class names + +import { ISidebarLink } from "@/app/features/dashboard/sidebar/SidebarLink.interfaces"; // Keep this import +import { resolveIcon } from "@/app/utils/iconMap"; import "./PageLinks.scss"; // Keep this import // Define the props interface for your PageLinks component @@ -13,19 +13,11 @@ interface IPageLinksProps extends ISidebarLink { } // PageLinks component -export default function PageLinks({ - title, - path, - icon: Icon, // Destructure icon as Icon -}: // isShowIcon, // If you plan to use this prop, uncomment it and add logic -IPageLinksProps) { +export default function PageLinks({ title, path, icon }: IPageLinksProps) { + const Icon = resolveIcon(icon); + console.log("Icon", Icon); return ( - // Corrected Link usage for Next.js 13/14 App Router: - // - Removed `passHref` and `legacyBehavior` - // - Applied `className` directly to the Link component - // - Removed the nested `` tag - {/* Conditionally render Icon if it exists */} {Icon && } {title} diff --git a/app/dashboard/admin/users/page.tsx b/app/dashboard/admin/users/page.tsx index b3729b5..ed1e353 100644 --- a/app/dashboard/admin/users/page.tsx +++ b/app/dashboard/admin/users/page.tsx @@ -49,11 +49,11 @@ export default async function BackOfficeUsersPage({ const data = await res.json(); // Handle different response structures: could be array directly, or wrapped in data/users property - // const users = Array.isArray(data) ? data : data.users || data.data || []; + const users = Array.isArray(data) ? data : data.users || data.data || []; return (
- +
); } diff --git a/app/features/Pages/Admin/Users/interfaces.ts b/app/features/Pages/Admin/Users/interfaces.ts index 42c050a..7d0df97 100644 --- a/app/features/Pages/Admin/Users/interfaces.ts +++ b/app/features/Pages/Admin/Users/interfaces.ts @@ -1,6 +1,7 @@ export interface IUser { id: string; email: string; + role: string; first_name: string; last_name: string; username: string; diff --git a/app/features/UserRoles/EditUser/EditUser.tsx b/app/features/UserRoles/EditUser/EditUser.tsx index 0ce1823..8ba0fd3 100644 --- a/app/features/UserRoles/EditUser/EditUser.tsx +++ b/app/features/UserRoles/EditUser/EditUser.tsx @@ -7,13 +7,17 @@ import "./EditUser.scss"; const EditUser = ({ user }: { user: IUser }) => { const router = useRouter(); - const { username, lastName, email, authorities: roles, phone } = user; + const { username, last_name, email, groups, phone } = user; const [form, setForm] = React.useState({ firstName: username || "", - lastName: lastName || "", + lastName: last_name || "", email: email || "", role: roles[0] || "", phone: phone || "", + groups: groups || [], + merchants: merchants || [], + jobTitle: jobTitle || "", + username: username || "", }); const handleChange = (e: React.ChangeEvent) => { @@ -40,6 +44,10 @@ const EditUser = ({ user }: { user: IUser }) => { email: "", role: "", phone: "", + groups: [], + merchants: [], + jobTitle: "", + username: "", }); }; diff --git a/app/features/dashboard/header/dropDown/DropDown.tsx b/app/features/dashboard/header/dropDown/DropDown.tsx index 0e1c793..99234f1 100644 --- a/app/features/dashboard/header/dropDown/DropDown.tsx +++ b/app/features/dashboard/header/dropDown/DropDown.tsx @@ -6,9 +6,10 @@ import { MenuItem, SelectChangeEvent, } from "@mui/material"; -import { PAGE_LINKS } from "@/app/features/dashboard/sidebar/SidebarLink.constants"; -import { ISidebarLink } from "@/app/features/dashboard/sidebar/SidebarLink.interfaces"; import PageLinks from "../../../../components/PageLinks/PageLinks"; +import { SidebarItem } from "@/app/redux/metadata/metadataSlice"; +import { useSelector } from "react-redux"; +import { selectNavigationSidebar } from "@/app/redux/metadata/selectors"; import "./DropDown.scss"; interface Props { @@ -17,7 +18,7 @@ interface Props { export default function SidebarDropdown({ onChange }: Props) { const [value, setValue] = React.useState(""); - + const sidebar = useSelector(selectNavigationSidebar)[0]?.links; const handleChange = (event: SelectChangeEvent) => { setValue(event.target.value); onChange?.(event); @@ -42,8 +43,13 @@ export default function SidebarDropdown({ onChange }: Props) { Select a page
- {PAGE_LINKS.map((link: ISidebarLink) => ( - + {sidebar?.map((link: SidebarItem) => ( + ))}
diff --git a/app/features/dashboard/sidebar/Sidebar.tsx b/app/features/dashboard/sidebar/Sidebar.tsx index 15fe8ec..c1fe0b8 100644 --- a/app/features/dashboard/sidebar/Sidebar.tsx +++ b/app/features/dashboard/sidebar/Sidebar.tsx @@ -1,14 +1,17 @@ "use client"; import DashboardIcon from "@mui/icons-material/Dashboard"; -import { useState } from "react"; -import { PAGE_LINKS } from "@/app/features/dashboard/sidebar/SidebarLink.constants"; +import { ElementType, useState } from "react"; import PageLinks from "../../../components/PageLinks/PageLinks"; import "./sideBar.scss"; import KeyboardArrowRightIcon from "@mui/icons-material/KeyboardArrowRight"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; import ChevronRightIcon from "@mui/icons-material/ChevronRight"; +import { selectNavigationSidebar } from "@/app/redux/metadata/selectors"; +import { useSelector } from "react-redux"; +import { SidebarItem } from "@/app/redux/metadata/metadataSlice"; +import { resolveIcon } from "@/app/utils/iconMap"; interface SidebarProps { isOpen?: boolean; @@ -17,7 +20,7 @@ interface SidebarProps { const SideBar = ({ isOpen = true, onClose }: SidebarProps) => { const [openMenus, setOpenMenus] = useState>({}); - + const sidebar = useSelector(selectNavigationSidebar)[0]?.links; const toggleMenu = (title: string) => { setOpenMenus(prev => ({ ...prev, [title]: !prev[title] })); }; @@ -34,45 +37,52 @@ const SideBar = ({ isOpen = true, onClose }: SidebarProps) => { - {PAGE_LINKS.map(link => - link.children ? ( -
- - {openMenus[link.title] && ( -
- {link.children.map(child => ( - - ))} -
- )} -
- ) : ( + {sidebar?.map((link: SidebarItem) => { + if (link.children) { + const Icon = resolveIcon(link.icon as string); + return ( +
+ + + {openMenus[link.title] && ( +
+ {link.children.map(child => ( + + ))} +
+ )} +
+ ); + } + + // Render simple links if no children + return ( - ) - )} + ); + })} ); }; diff --git a/app/features/dashboard/sidebar/SidebarLink.constants.ts b/app/features/dashboard/sidebar/SidebarLink.constants.ts deleted file mode 100644 index 59dd8c3..0000000 --- a/app/features/dashboard/sidebar/SidebarLink.constants.ts +++ /dev/null @@ -1,84 +0,0 @@ -import HomeIcon from "@mui/icons-material/Home"; -import AccountBalanceWalletIcon from "@mui/icons-material/AccountBalanceWallet"; -import CheckCircleIcon from "@mui/icons-material/CheckCircle"; -import SearchIcon from "@mui/icons-material/Search"; -import VerifiedUserIcon from "@mui/icons-material/VerifiedUser"; -import PeopleIcon from "@mui/icons-material/People"; -import GavelIcon from "@mui/icons-material/Gavel"; -import HubIcon from "@mui/icons-material/Hub"; -import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings"; -import InsightsIcon from "@mui/icons-material/Insights"; -import ListAltIcon from "@mui/icons-material/ListAlt"; -import SettingsIcon from "@mui/icons-material/Settings"; - -import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; -import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; -import HistoryIcon from "@mui/icons-material/History"; -import FactCheckIcon from "@mui/icons-material/FactCheck"; - -import { ISidebarLink } from "@/app/features/dashboard/sidebar/SidebarLink.interfaces"; - -export const PAGE_LINKS: ISidebarLink[] = [ - { title: "Home", path: "/dashboard", icon: HomeIcon }, - - { - title: "Transaction", - path: "/dashboard/transactions", - icon: AccountBalanceWalletIcon, - children: [ - { - title: "Deposits", - path: "/dashboard/transactions/deposits", - icon: ArrowDownwardIcon, - }, - { - title: "Withdrawals", - path: "/dashboard/transactions/withdrawals", - icon: ArrowUpwardIcon, - }, - { - title: "All Transactions", - path: "/dashboard/transactions/all", - icon: HistoryIcon, - }, - ], - }, - { title: "Approve", path: "/dashboard/approve", icon: CheckCircleIcon }, - { title: "Investigate", path: "/dashboard/investigate", icon: SearchIcon }, - { title: "KYC", path: "/dashboard/kyc", icon: VerifiedUserIcon }, - { - title: "User Accounts", - path: "/dashboard/user-accounts", - icon: PeopleIcon, - }, - // { title: 'Analytics', path: '/analytics', icon: BarChartIcon }, - { title: "Rules", path: "/dashboard/rules", icon: GavelIcon }, - { title: "Rules Hub", path: "/dashboard/rules-hub", icon: HubIcon }, - { - title: "Admin", - path: "/dashboard/admin", - icon: AdminPanelSettingsIcon, - children: [ - { - title: "Manage Users", - path: "/dashboard/admin/users", - icon: PeopleIcon, - }, - { - title: "Transactions", - path: "/dashboard/admin/transactions", - icon: ListAltIcon, - }, - { - title: "Settings", - path: "dashboard/admin/settings", - icon: SettingsIcon, - }, - ], - }, - { title: "Account IQ", path: "/dashboard/account-iq", icon: InsightsIcon }, - { title: "Audits", path: "/dashboard/audits", icon: FactCheckIcon }, - // { title: 'Documentation', path: '/documentation', icon: DescriptionIcon }, - // { title: 'Support', path: '/support', icon: SupportAgentIcon }, - // { title: 'System Status', path: '/system-status', icon: WarningAmberIcon }, -]; diff --git a/app/features/dashboard/sidebar/SidebarLink.interfaces.ts b/app/features/dashboard/sidebar/SidebarLink.interfaces.ts index d274129..81b7562 100644 --- a/app/features/dashboard/sidebar/SidebarLink.interfaces.ts +++ b/app/features/dashboard/sidebar/SidebarLink.interfaces.ts @@ -3,6 +3,6 @@ import { ElementType } from "react"; export interface ISidebarLink { title: string; path: string; - icon?: ElementType; + icon?: ElementType | string; children?: ISidebarLink[]; } diff --git a/app/redux/auth/authSlice.tsx b/app/redux/auth/authSlice.tsx index 0ceb108..97f9164 100644 --- a/app/redux/auth/authSlice.tsx +++ b/app/redux/auth/authSlice.tsx @@ -3,6 +3,7 @@ import { ThunkSuccess, ThunkError, IUserResponse } from "../types"; import { IEditUserForm } from "../../features/UserRoles/User.interfaces"; import { RootState } from "../store"; import toast from "react-hot-toast"; +import { fetchMetadata } from "../metadata/metadataSlice"; interface TokenInfo { expiresAt: string; @@ -168,6 +169,7 @@ export const validateAuth = createAsyncThunk< dispatch(autoLogout("Session invalid or expired")); return rejectWithValue("Session invalid or expired"); } + dispatch(fetchMetadata()); return { tokenInfo: data.tokenInfo || null, diff --git a/app/redux/metadata/metadataSlice.ts b/app/redux/metadata/metadataSlice.ts new file mode 100644 index 0000000..0376d31 --- /dev/null +++ b/app/redux/metadata/metadataSlice.ts @@ -0,0 +1,90 @@ +import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { RootState } from "../store"; + +export interface SidebarItem { + id?: string; + title: string; + path: string; + icon?: string; // icon name from backend; map client-side if needed + permissions?: string[]; // required permissions for visibility + children?: SidebarItem[]; +} + +export interface AppMetadata { + groups?: string[]; + job_titles?: string[]; + merchants?: string[]; + message?: string; + sidebar?: SidebarItem[]; + success: boolean; +} + +interface MetadataState { + data: AppMetadata | null; + status: "idle" | "loading" | "succeeded" | "failed"; + error: string | null; + lastFetchedAt: number | null; +} + +const initialState: MetadataState = { + data: null, + status: "idle", + error: null, + lastFetchedAt: null, +}; + +export const fetchMetadata = createAsyncThunk< + AppMetadata, + void, + { rejectValue: string } +>("metadata/fetch", async (_, { rejectWithValue }) => { + try { + const res = await fetch("/api/metadata", { + method: "GET", + cache: "no-store", + }); + const data = await res.json(); + if (!res.ok) { + return rejectWithValue(data?.message || "Failed to fetch metadata"); + } + return data as AppMetadata; + } catch (err) { + return rejectWithValue((err as Error)?.message || "Network error"); + } +}); + +const metadataSlice = createSlice({ + name: "metadata", + initialState, + reducers: { + clearMetadata(state) { + state.data = null; + state.error = null; + state.status = "idle"; + state.lastFetchedAt = null; + }, + }, + extraReducers: builder => { + builder + .addCase(fetchMetadata.pending, state => { + state.status = "loading"; + state.error = null; + }) + .addCase( + fetchMetadata.fulfilled, + (state, action: PayloadAction) => { + state.status = "succeeded"; + state.data = action.payload; + state.lastFetchedAt = Date.now(); + } + ) + .addCase(fetchMetadata.rejected, (state, action) => { + state.status = "failed"; + state.error = (action.payload as string) || "Unknown error"; + }); + }, +}); + +export const { clearMetadata } = metadataSlice.actions; + +export default metadataSlice.reducer; diff --git a/app/redux/metadata/selectors.ts b/app/redux/metadata/selectors.ts new file mode 100644 index 0000000..36adaaf --- /dev/null +++ b/app/redux/metadata/selectors.ts @@ -0,0 +1,7 @@ +import { RootState } from "../store"; + +// Selectors +export const selectMetadataState = (state: RootState) => state.metadata; +export const selectAppMetadata = (state: RootState) => state.metadata.data; +export const selectNavigationSidebar = (state: RootState) => + state.metadata.data?.sidebar || []; diff --git a/app/redux/store.ts b/app/redux/store.ts index c1a76b6..71a873c 100644 --- a/app/redux/store.ts +++ b/app/redux/store.ts @@ -9,6 +9,7 @@ import { createEpicMiddleware, combineEpics } from "redux-observable"; import advancedSearchReducer from "./advanedSearch/advancedSearchSlice"; import authReducer from "./auth/authSlice"; import uiReducer from "./ui/uiSlice"; +import metadataReducer from "./metadata/metadataSlice"; import userEpics from "./user/epic"; import authEpics from "./auth/epic"; import uiEpics from "./ui/epic"; @@ -38,6 +39,7 @@ const persistConfig = { const rootReducer = combineReducers({ advancedSearch: advancedSearchReducer, auth: authReducer, + metadata: metadataReducer, ui: uiReducer, }); diff --git a/app/utils/iconMap.ts b/app/utils/iconMap.ts new file mode 100644 index 0000000..f91ce5a --- /dev/null +++ b/app/utils/iconMap.ts @@ -0,0 +1,50 @@ +import type { ElementType } from "react"; + +// MUI icons used across the app; extend as needed +import HomeIcon from "@mui/icons-material/Home"; +import AccountBalanceWalletIcon from "@mui/icons-material/AccountBalanceWallet"; +import CheckCircleIcon from "@mui/icons-material/CheckCircle"; +import SearchIcon from "@mui/icons-material/Search"; +import VerifiedUserIcon from "@mui/icons-material/VerifiedUser"; +import PeopleIcon from "@mui/icons-material/People"; +import GavelIcon from "@mui/icons-material/Gavel"; +import HubIcon from "@mui/icons-material/Hub"; +import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings"; +import InsightsIcon from "@mui/icons-material/Insights"; +import ListAltIcon from "@mui/icons-material/ListAlt"; +import SettingsIcon from "@mui/icons-material/Settings"; +import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; +import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; +import HistoryIcon from "@mui/icons-material/History"; +import FactCheckIcon from "@mui/icons-material/FactCheck"; +import WidgetsIcon from "@mui/icons-material/Widgets"; // fallback + +// Map string keys from backend to actual Icon components +const iconRegistry: Record = { + HomeIcon, + AccountBalanceWalletIcon, + CheckCircleIcon, + SearchIcon, + VerifiedUserIcon, + PeopleIcon, + GavelIcon, + HubIcon, + AdminPanelSettingsIcon, + InsightsIcon, + ListAltIcon, + SettingsIcon, + ArrowDownwardIcon, + ArrowUpwardIcon, + HistoryIcon, + FactCheckIcon, +}; + +export function resolveIcon( + icon?: ElementType | string +): ElementType | undefined { + if (!icon) return undefined; + if (typeof icon === "string") { + return iconRegistry[icon] || WidgetsIcon; // fallback to a generic icon + } + return icon; +}