diff --git a/src/app/(admin)/(others-pages)/profile/applications/page.tsx b/src/app/(admin)/(others-pages)/profile/applications/page.tsx index 1b20d8d..f495158 100644 --- a/src/app/(admin)/(others-pages)/profile/applications/page.tsx +++ b/src/app/(admin)/(others-pages)/profile/applications/page.tsx @@ -118,6 +118,17 @@ export default function ApplicationDetailPage() { } } }; + const formatDate = (dateString: string | null | undefined) => { + if (!dateString) return "-"; + const date = new Date(dateString); + return date.toLocaleDateString("vi-VN", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + }; // console.log("Application Detail:", application); @@ -247,7 +258,7 @@ export default function ApplicationDetailPage() { - {application?.reviewed_at} + {formatDate(application?.reviewed_at)} diff --git a/src/layout/AppSidebar.tsx b/src/layout/AppSidebar.tsx index d12a378..ed71353 100644 --- a/src/layout/AppSidebar.tsx +++ b/src/layout/AppSidebar.tsx @@ -1,8 +1,10 @@ "use client"; -import React, { useEffect, useRef, useState, useCallback } from "react"; +import React, { useEffect, useRef, useState, useCallback, useMemo } from "react"; import Link from "next/link"; import Image from "next/image"; import { usePathname } from "next/navigation"; +import { useSelector } from "react-redux"; +import { RootState } from "@/store/store"; import { useSidebar } from "../context/SidebarContext"; import { BoxCubeIcon, @@ -17,16 +19,24 @@ import { TableIcon, UserCircleIcon, } from "../icons/index"; -import SidebarWidget from "./SidebarWidget"; + +type RoleName = "ADMIN" | "MOD" | "USER" | "HISTORIAN"; type NavItem = { name: string; icon: React.ReactNode; path?: string; - subItems?: { name: string; path: string; pro?: boolean; new?: boolean }[]; + roles?: RoleName[]; + subItems?: { + name: string; + path: string; + pro?: boolean; + new?: boolean; + roles?: RoleName[]; + }[]; }; -const navItems: NavItem[] = [ +const ALL_NAV_ITEMS: NavItem[] = [ { icon: , name: "Dashboard", @@ -42,19 +52,21 @@ const navItems: NavItem[] = [ name: "User Profile", path: "/profile", }, - { name: "Forms", icon: , - subItems: [{ name: "Form Elements", path: "/form-elements", pro: false }, { name: "Role Upgrade", path: "/role-upgrade", pro: false }], + subItems: [ + // { name: "Form Elements", path: "/form-elements", pro: false }, + { name: "Role Upgrade", path: "/role-upgrade", pro: false } + ], }, { name: "Tables", icon: , subItems: [ - { name: "Basic Tables", path: "/basic-tables", pro: false }, - { name: "User Tables", path: "/user-table", pro: false }, - { name: "Applications Tables", path: "/applications-tables", pro: false }, + // { name: "Basic Tables", path: "/basic-tables", pro: false }, + { name: "User Tables", path: "/user-table", pro: false, roles: ["ADMIN", "MOD"] }, + { name: "Applications Tables", path: "/applications-tables", pro: false, roles: ["ADMIN", "MOD"] }, ], }, { @@ -67,7 +79,7 @@ const navItems: NavItem[] = [ }, ]; -const othersItems: NavItem[] = [ +const OTHERS_ITEMS: NavItem[] = [ { icon: , name: "Charts", @@ -102,64 +114,114 @@ const AppSidebar: React.FC = () => { const { isExpanded, isMobileOpen, isHovered, setIsHovered } = useSidebar(); const pathname = usePathname(); - const renderMenuItems = ( - navItems: NavItem[], - menuType: "main" | "others", - ) => ( + // Lấy data gốc từ Redux (không bị render lại vô cớ) + const rolesData = useSelector((state: RootState) => state.user.data?.roles); + + // Chỉ tạo mảng map mới khi rolesData thực sự thay đổi + const userRoles = useMemo(() => { + return rolesData?.map((r: any) => r.name) || []; + }, [rolesData]); + + // 2. Logic lọc Menu theo Role + const filterMenuByRole = useCallback((items: NavItem[]) => { + return items + .map((item) => ({ + ...item, + subItems: item.subItems?.filter((sub) => { + if (!sub.roles) return true; // Ai cũng xem được nếu không định nghĩa role + return sub.roles.some((role) => userRoles.includes(role)); + }), + })) + .filter((item) => { + // Ẩn mục cha nếu nó yêu cầu role mà user không có + if (item.roles && !item.roles.some((role) => userRoles.includes(role))) return false; + // Ẩn mục cha nếu các mục con đã bị lọc sạch (đối với menu dạng dropdown) + if (item.subItems && item.subItems.length === 0 && !item.path) return false; + return true; + }); + }, [userRoles]); + + const filteredNavItems = useMemo(() => filterMenuByRole(ALL_NAV_ITEMS), [filterMenuByRole]); + const filteredOthersItems = useMemo(() => filterMenuByRole(OTHERS_ITEMS), [filterMenuByRole]); + + // --- State quản lý đóng mở Submenu --- + const [openSubmenu, setOpenSubmenu] = useState<{ + type: "main" | "others"; + index: number; + } | null>(null); + const [subMenuHeight, setSubMenuHeight] = useState>({}); + const subMenuRefs = useRef>({}); + + const isActive = useCallback((path: string) => path === pathname, [pathname]); + + const handleSubmenuToggle = (index: number, menuType: "main" | "others") => { + setOpenSubmenu((prev) => + prev?.type === menuType && prev?.index === index ? null : { type: menuType, index } + ); + }; + + useEffect(() => { + let submenuMatched = false; + [ + { items: filteredNavItems, type: "main" }, + { items: filteredOthersItems, type: "others" }, + ].forEach(({ items, type }) => { + items.forEach((nav, index) => { + nav.subItems?.forEach((sub) => { + if (isActive(sub.path)) { + setOpenSubmenu((prev) => { + if (prev?.type === type && prev?.index === index) return prev; + return { type: type as "main" | "others", index }; + }); + submenuMatched = true; + } + }); + }); + }); + + if (!submenuMatched) { + setOpenSubmenu((prev) => (prev !== null ? null : prev)); + } + }, [pathname, isActive, filteredNavItems, filteredOthersItems]); + + useEffect(() => { + if (openSubmenu !== null) { + const key = `${openSubmenu.type}-${openSubmenu.index}`; + if (subMenuRefs.current[key]) { + setSubMenuHeight((prev) => ({ + ...prev, + [key]: subMenuRefs.current[key]?.scrollHeight || 0, + })); + } + } + }, [openSubmenu]); + + const renderMenuItems = (items: NavItem[], menuType: "main" | "others") => (
    - {navItems.map((nav, index) => ( + {items.map((nav, index) => (
  • {nav.subItems ? ( ) : ( nav.path && ( - - + + {nav.icon} {(isExpanded || isHovered || isMobileOpen) && ( @@ -170,52 +232,18 @@ const AppSidebar: React.FC = () => { )} {nav.subItems && (isExpanded || isHovered || isMobileOpen) && (
    { - subMenuRefs.current[`${menuType}-${index}`] = el; - }} + ref={(el) => { subMenuRefs.current[`${menuType}-${index}`] = el; }} className="overflow-hidden transition-all duration-300" - style={{ - height: - openSubmenu?.type === menuType && openSubmenu?.index === index - ? `${subMenuHeight[`${menuType}-${index}`]}px` - : "0px", - }} + style={{ height: openSubmenu?.type === menuType && openSubmenu?.index === index ? `${subMenuHeight[`${menuType}-${index}`]}px` : "0px" }} >
      {nav.subItems.map((subItem) => (
    • - + {subItem.name} - {subItem.new && ( - - new - - )} - {subItem.pro && ( - - pro - - )} + {subItem.new && new} + {subItem.pro && pro}
    • @@ -228,160 +256,44 @@ const AppSidebar: React.FC = () => {
    ); - const [openSubmenu, setOpenSubmenu] = useState<{ - type: "main" | "others"; - index: number; - } | null>(null); - const [subMenuHeight, setSubMenuHeight] = useState>( - {}, - ); - const subMenuRefs = useRef>({}); - - // const isActive = (path: string) => path === pathname; - const isActive = useCallback((path: string) => path === pathname, [pathname]); - - useEffect(() => { - // Check if the current path matches any submenu item - let submenuMatched = false; - ["main", "others"].forEach((menuType) => { - const items = menuType === "main" ? navItems : othersItems; - items.forEach((nav, index) => { - if (nav.subItems) { - nav.subItems.forEach((subItem) => { - if (isActive(subItem.path)) { - setOpenSubmenu({ - type: menuType as "main" | "others", - index, - }); - submenuMatched = true; - } - }); - } - }); - }); - - // If no submenu item matches, close the open submenu - if (!submenuMatched) { - setOpenSubmenu(null); - } - }, [pathname, isActive]); - - useEffect(() => { - // Set the height of the submenu items when the submenu is opened - if (openSubmenu !== null) { - const key = `${openSubmenu.type}-${openSubmenu.index}`; - if (subMenuRefs.current[key]) { - setSubMenuHeight((prevHeights) => ({ - ...prevHeights, - [key]: subMenuRefs.current[key]?.scrollHeight || 0, - })); - } - } - }, [openSubmenu]); - - const handleSubmenuToggle = (index: number, menuType: "main" | "others") => { - setOpenSubmenu((prevOpenSubmenu) => { - if ( - prevOpenSubmenu && - prevOpenSubmenu.type === menuType && - prevOpenSubmenu.index === index - ) { - return null; - } - return { type: menuType, index }; - }); - }; - return ( ); }; -export default AppSidebar; +export default AppSidebar; \ No newline at end of file