Added dynamic sidebar
This commit is contained in:
parent
b79f3afc2e
commit
b3a666066c
@ -5,7 +5,7 @@ import { useEffect, useRef } from "react";
|
|||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
import { AppDispatch } from "@/app/redux/types";
|
import { AppDispatch } from "@/app/redux/types";
|
||||||
import { validateAuth } from "./redux/auth/authSlice";
|
import { validateAuth } from "./redux/auth/authSlice";
|
||||||
|
import { fetchMetadata } from "./redux/metadata/metadataSlice";
|
||||||
export function AuthBootstrap() {
|
export function AuthBootstrap() {
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
const startedRef = useRef(false);
|
const startedRef = useRef(false);
|
||||||
@ -18,7 +18,7 @@ export function AuthBootstrap() {
|
|||||||
|
|
||||||
// Initial validate on mount
|
// Initial validate on mount
|
||||||
dispatch(validateAuth());
|
dispatch(validateAuth());
|
||||||
|
// dispatch(fetchMetadata());
|
||||||
// Refresh on window focus (nice UX)
|
// Refresh on window focus (nice UX)
|
||||||
const onFocus = () => dispatch(validateAuth());
|
const onFocus = () => dispatch(validateAuth());
|
||||||
window.addEventListener("focus", onFocus);
|
window.addEventListener("focus", onFocus);
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { randomUUID } from "crypto";
|
|
||||||
import { formatPhoneDisplay } from "@/app/features/UserRoles/utils";
|
import { formatPhoneDisplay } from "@/app/features/UserRoles/utils";
|
||||||
|
|
||||||
const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:8583";
|
const BE_BASE_URL = process.env.BE_BASE_URL || "http://localhost:8583";
|
||||||
|
|||||||
@ -20,21 +20,13 @@ export async function GET(request: Request) {
|
|||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const queryString = url.search ? url.search : "";
|
const queryString = url.search ? url.search : "";
|
||||||
|
|
||||||
console.log(
|
const response = await fetch(`${BE_BASE_URL}/api/v1/users${queryString}`, {
|
||||||
"[DEBUG] - URL",
|
method: "GET",
|
||||||
`${BE_BASE_URL}/api/v1/users/list${queryString}`
|
headers: {
|
||||||
);
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
const response = await fetch(
|
cache: "no-store",
|
||||||
`${BE_BASE_URL}/api/v1/users/list${queryString}`,
|
});
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
cache: "no-store",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("[DEBUG] - Response", response);
|
console.log("[DEBUG] - Response", response);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|||||||
48
app/api/metadata/route.ts
Normal file
48
app/api/metadata/route.ts
Normal file
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
// app/components/PageLinks/PageLinks.tsx
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
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 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
|
import "./PageLinks.scss"; // Keep this import
|
||||||
|
|
||||||
// Define the props interface for your PageLinks component
|
// Define the props interface for your PageLinks component
|
||||||
@ -13,19 +13,11 @@ interface IPageLinksProps extends ISidebarLink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PageLinks component
|
// PageLinks component
|
||||||
export default function PageLinks({
|
export default function PageLinks({ title, path, icon }: IPageLinksProps) {
|
||||||
title,
|
const Icon = resolveIcon(icon);
|
||||||
path,
|
console.log("Icon", Icon);
|
||||||
icon: Icon, // Destructure icon as Icon
|
|
||||||
}: // isShowIcon, // If you plan to use this prop, uncomment it and add logic
|
|
||||||
IPageLinksProps) {
|
|
||||||
return (
|
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 `<a>` tag
|
|
||||||
<Link href={path} className={clsx("page-link", "page-link__container")}>
|
<Link href={path} className={clsx("page-link", "page-link__container")}>
|
||||||
{/* Conditionally render Icon if it exists */}
|
|
||||||
{Icon && <Icon />}
|
{Icon && <Icon />}
|
||||||
<span className="page-link__text">{title}</span>
|
<span className="page-link__text">{title}</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -49,11 +49,11 @@ export default async function BackOfficeUsersPage({
|
|||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
// Handle different response structures: could be array directly, or wrapped in data/users property
|
// 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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Users users={data?.users} />
|
<Users users={users} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
export interface IUser {
|
export interface IUser {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
role: string;
|
||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
|||||||
@ -7,13 +7,17 @@ import "./EditUser.scss";
|
|||||||
|
|
||||||
const EditUser = ({ user }: { user: IUser }) => {
|
const EditUser = ({ user }: { user: IUser }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { username, lastName, email, authorities: roles, phone } = user;
|
const { username, last_name, email, groups, phone } = user;
|
||||||
const [form, setForm] = React.useState<IEditUserForm>({
|
const [form, setForm] = React.useState<IEditUserForm>({
|
||||||
firstName: username || "",
|
firstName: username || "",
|
||||||
lastName: lastName || "",
|
lastName: last_name || "",
|
||||||
email: email || "",
|
email: email || "",
|
||||||
role: roles[0] || "",
|
role: roles[0] || "",
|
||||||
phone: phone || "",
|
phone: phone || "",
|
||||||
|
groups: groups || [],
|
||||||
|
merchants: merchants || [],
|
||||||
|
jobTitle: jobTitle || "",
|
||||||
|
username: username || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@ -40,6 +44,10 @@ const EditUser = ({ user }: { user: IUser }) => {
|
|||||||
email: "",
|
email: "",
|
||||||
role: "",
|
role: "",
|
||||||
phone: "",
|
phone: "",
|
||||||
|
groups: [],
|
||||||
|
merchants: [],
|
||||||
|
jobTitle: "",
|
||||||
|
username: "",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -6,9 +6,10 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
SelectChangeEvent,
|
SelectChangeEvent,
|
||||||
} from "@mui/material";
|
} 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 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";
|
import "./DropDown.scss";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -17,7 +18,7 @@ interface Props {
|
|||||||
|
|
||||||
export default function SidebarDropdown({ onChange }: Props) {
|
export default function SidebarDropdown({ onChange }: Props) {
|
||||||
const [value, setValue] = React.useState("");
|
const [value, setValue] = React.useState("");
|
||||||
|
const sidebar = useSelector(selectNavigationSidebar)[0]?.links;
|
||||||
const handleChange = (event: SelectChangeEvent<string>) => {
|
const handleChange = (event: SelectChangeEvent<string>) => {
|
||||||
setValue(event.target.value);
|
setValue(event.target.value);
|
||||||
onChange?.(event);
|
onChange?.(event);
|
||||||
@ -42,8 +43,13 @@ export default function SidebarDropdown({ onChange }: Props) {
|
|||||||
<em className="em">Select a page</em>
|
<em className="em">Select a page</em>
|
||||||
<MenuItem value="" disabled></MenuItem>
|
<MenuItem value="" disabled></MenuItem>
|
||||||
<div className="sidebar-dropdown__container">
|
<div className="sidebar-dropdown__container">
|
||||||
{PAGE_LINKS.map((link: ISidebarLink) => (
|
{sidebar?.map((link: SidebarItem) => (
|
||||||
<PageLinks key={link.path} title={link.title} path={link.path} />
|
<PageLinks
|
||||||
|
key={link.path}
|
||||||
|
title={link.title}
|
||||||
|
path={link.path}
|
||||||
|
icon={link.icon as string}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import DashboardIcon from "@mui/icons-material/Dashboard";
|
import DashboardIcon from "@mui/icons-material/Dashboard";
|
||||||
import { useState } from "react";
|
import { ElementType, useState } from "react";
|
||||||
import { PAGE_LINKS } from "@/app/features/dashboard/sidebar/SidebarLink.constants";
|
|
||||||
import PageLinks from "../../../components/PageLinks/PageLinks";
|
import PageLinks from "../../../components/PageLinks/PageLinks";
|
||||||
import "./sideBar.scss";
|
import "./sideBar.scss";
|
||||||
import KeyboardArrowRightIcon from "@mui/icons-material/KeyboardArrowRight";
|
import KeyboardArrowRightIcon from "@mui/icons-material/KeyboardArrowRight";
|
||||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||||
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
||||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
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 {
|
interface SidebarProps {
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
@ -17,7 +20,7 @@ interface SidebarProps {
|
|||||||
|
|
||||||
const SideBar = ({ isOpen = true, onClose }: SidebarProps) => {
|
const SideBar = ({ isOpen = true, onClose }: SidebarProps) => {
|
||||||
const [openMenus, setOpenMenus] = useState<Record<string, boolean>>({});
|
const [openMenus, setOpenMenus] = useState<Record<string, boolean>>({});
|
||||||
|
const sidebar = useSelector(selectNavigationSidebar)[0]?.links;
|
||||||
const toggleMenu = (title: string) => {
|
const toggleMenu = (title: string) => {
|
||||||
setOpenMenus(prev => ({ ...prev, [title]: !prev[title] }));
|
setOpenMenus(prev => ({ ...prev, [title]: !prev[title] }));
|
||||||
};
|
};
|
||||||
@ -34,45 +37,52 @@ const SideBar = ({ isOpen = true, onClose }: SidebarProps) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{PAGE_LINKS.map(link =>
|
{sidebar?.map((link: SidebarItem) => {
|
||||||
link.children ? (
|
if (link.children) {
|
||||||
<div key={link.title}>
|
const Icon = resolveIcon(link.icon as string);
|
||||||
<button
|
return (
|
||||||
onClick={() => toggleMenu(link.title)}
|
<div key={link.title}>
|
||||||
className="sidebar__dropdown-button"
|
<button
|
||||||
>
|
onClick={() => toggleMenu(link.title)}
|
||||||
{link.icon && <link.icon />}
|
className="sidebar__dropdown-button"
|
||||||
<span className="sidebar__text">{link.title}</span>
|
>
|
||||||
<span className="sidebar__arrow">
|
{Icon && <Icon />}
|
||||||
{!openMenus[link.title] ? (
|
<span className="sidebar__text">{link.title}</span>
|
||||||
<KeyboardArrowRightIcon />
|
<span className="sidebar__arrow">
|
||||||
) : (
|
{openMenus[link.title] ? (
|
||||||
<KeyboardArrowDownIcon />
|
<KeyboardArrowDownIcon />
|
||||||
)}
|
) : (
|
||||||
</span>
|
<KeyboardArrowRightIcon />
|
||||||
</button>
|
)}
|
||||||
{openMenus[link.title] && (
|
</span>
|
||||||
<div className="sidebar__submenu">
|
</button>
|
||||||
{link.children.map(child => (
|
|
||||||
<PageLinks
|
{openMenus[link.title] && (
|
||||||
key={child.path}
|
<div className="sidebar__submenu">
|
||||||
title={child.title}
|
{link.children.map(child => (
|
||||||
path={child.path}
|
<PageLinks
|
||||||
icon={child.icon}
|
key={child.path}
|
||||||
/>
|
title={child.title}
|
||||||
))}
|
path={child.path}
|
||||||
</div>
|
icon={child.icon as ElementType}
|
||||||
)}
|
/>
|
||||||
</div>
|
))}
|
||||||
) : (
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render simple links if no children
|
||||||
|
return (
|
||||||
<PageLinks
|
<PageLinks
|
||||||
key={link.path}
|
key={link.path}
|
||||||
title={link.title}
|
title={link.title}
|
||||||
path={link.path}
|
path={link.path}
|
||||||
icon={link.icon}
|
icon={link.icon as ElementType}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
)}
|
})}
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 },
|
|
||||||
];
|
|
||||||
@ -3,6 +3,6 @@ import { ElementType } from "react";
|
|||||||
export interface ISidebarLink {
|
export interface ISidebarLink {
|
||||||
title: string;
|
title: string;
|
||||||
path: string;
|
path: string;
|
||||||
icon?: ElementType;
|
icon?: ElementType | string;
|
||||||
children?: ISidebarLink[];
|
children?: ISidebarLink[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { ThunkSuccess, ThunkError, IUserResponse } from "../types";
|
|||||||
import { IEditUserForm } from "../../features/UserRoles/User.interfaces";
|
import { IEditUserForm } from "../../features/UserRoles/User.interfaces";
|
||||||
import { RootState } from "../store";
|
import { RootState } from "../store";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import { fetchMetadata } from "../metadata/metadataSlice";
|
||||||
|
|
||||||
interface TokenInfo {
|
interface TokenInfo {
|
||||||
expiresAt: string;
|
expiresAt: string;
|
||||||
@ -168,6 +169,7 @@ export const validateAuth = createAsyncThunk<
|
|||||||
dispatch(autoLogout("Session invalid or expired"));
|
dispatch(autoLogout("Session invalid or expired"));
|
||||||
return rejectWithValue("Session invalid or expired");
|
return rejectWithValue("Session invalid or expired");
|
||||||
}
|
}
|
||||||
|
dispatch(fetchMetadata());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tokenInfo: data.tokenInfo || null,
|
tokenInfo: data.tokenInfo || null,
|
||||||
|
|||||||
90
app/redux/metadata/metadataSlice.ts
Normal file
90
app/redux/metadata/metadataSlice.ts
Normal file
@ -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<AppMetadata>) => {
|
||||||
|
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;
|
||||||
7
app/redux/metadata/selectors.ts
Normal file
7
app/redux/metadata/selectors.ts
Normal file
@ -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 || [];
|
||||||
@ -9,6 +9,7 @@ import { createEpicMiddleware, combineEpics } from "redux-observable";
|
|||||||
import advancedSearchReducer from "./advanedSearch/advancedSearchSlice";
|
import advancedSearchReducer from "./advanedSearch/advancedSearchSlice";
|
||||||
import authReducer from "./auth/authSlice";
|
import authReducer from "./auth/authSlice";
|
||||||
import uiReducer from "./ui/uiSlice";
|
import uiReducer from "./ui/uiSlice";
|
||||||
|
import metadataReducer from "./metadata/metadataSlice";
|
||||||
import userEpics from "./user/epic";
|
import userEpics from "./user/epic";
|
||||||
import authEpics from "./auth/epic";
|
import authEpics from "./auth/epic";
|
||||||
import uiEpics from "./ui/epic";
|
import uiEpics from "./ui/epic";
|
||||||
@ -38,6 +39,7 @@ const persistConfig = {
|
|||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
advancedSearch: advancedSearchReducer,
|
advancedSearch: advancedSearchReducer,
|
||||||
auth: authReducer,
|
auth: authReducer,
|
||||||
|
metadata: metadataReducer,
|
||||||
ui: uiReducer,
|
ui: uiReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
50
app/utils/iconMap.ts
Normal file
50
app/utils/iconMap.ts
Normal file
@ -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<string, ElementType> = {
|
||||||
|
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;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user