Files
edr-platform/apps/edr-freight-web/backoffice/src/layout/TopBar.tsx
2026-08-12 12:28:37 +00:00

340 lines
13 KiB
TypeScript

"use client";
import {
ChevronDown,
Globe,
LogOut,
User,
Key,
Home,
Menu,
} from "lucide-react";
import { Button } from "@/shared/common/ui/button";
import { Avatar, AvatarFallback } from "@/shared/common/ui/avatar";
import { useAuthUser } from "@/shared/hooks/useAuthUser";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/shared/common/ui/dropdown-menu";
import { useTranslation } from "react-i18next";
import { NavLink, useNavigate, useLocation } from "react-router-dom";
import { useRef, useEffect, useState } from "react";
import { FiBell } from "react-icons/fi";
import { useUser } from "@/shared/context/UserContext";
import {
UI_LANGUAGE_OPTIONS,
getUiLanguageLabel,
resolveUiLanguage,
} from "@/shared/i18n/uiLanguages";
import { useNotifications } from "@/shared/hooks/useNotification";
import NotificationList from "@/record-management/components/NotificationList";
import {
resolveModuleConfig,
useTenantConfig,
} from "@/layout/components/TenantConfig";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/shared/common/ui/tooltip";
export const TopBar = () => {
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const { pathname } = useLocation();
const { logout } = useAuthUser();
const userDetails = useUser();
const { config: tenantConfig } = useTenantConfig();
const moduleConfig = resolveModuleConfig(tenantConfig);
const isAdminModuleVisible = userDetails?.roles?.some(
(role: { key: string }) =>
role.key === "unit_admin" ||
role.key === "organization_admin" ||
role.key === "super_admin",
);
const visibleModuleCount = [
moduleConfig.recordManagement,
moduleConfig.performance,
moduleConfig.objective,
moduleConfig.dms,
moduleConfig.siteManagement && isAdminModuleVisible,
].filter(Boolean).length;
const shouldShowHomePageLink = visibleModuleCount > 1;
const fullName = userDetails?.name?.en || t("user");
const splittedName = fullName.trim().split(" ");
const initials =
splittedName.length === 1
? splittedName[0][0]
: `${splittedName[0][0]}${splittedName[1][0]}`;
const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng);
};
const currentLanguage = resolveUiLanguage(i18n.language);
const handleLogout = () => {
logout();
};
const userRoles = userDetails?.roles?.map((role: any) => role.key) || [];
const isSuperAdmin = userRoles.includes("super_admin");
const isOrgAdmin =
userRoles.includes("unit_admin") ||
userRoles.includes("organization_admin");
const modulesList = [
{
id: "homepage",
label: t("nav.homePage", "Home Page"),
path: "/homepage",
},
...(moduleConfig.recordManagement
? [
{
id: "recordManagement",
label: t("nav.Record Management", "Record Management"),
path: "/record-management/dashboard",
},
]
: []),
...(moduleConfig.performance
? [
{
id: "performanceManagement",
label: t("nav.PerformanceManagement", "Performance Management"),
path: "/performance-management/plan-years",
},
]
: []),
...(moduleConfig.objective
? [
{
id: "objectiveManagement",
label: t("nav.objectiveManagement", "Objective Management"),
path: "/objective-management/plan-years",
},
]
: []),
...(moduleConfig.dms
? [
{
id: "documentManagement",
label: t("nav.DocumentManagement", "Document Management"),
path: "/dms/dashboard",
},
]
: []),
...(isOrgAdmin && moduleConfig.siteManagement
? [
{
id: "orgAdmin",
label: t("nav.admin", "Admin"),
path: "/user-management/user_management-dashboard",
},
]
: []),
...(isSuperAdmin
? [
{
id: "superAdmin",
label: t("OrganizationAdmin", "Super Admin"),
path: "/user-management/dashboard",
},
]
: []),
];
const [openNotifications, setOpenNotifications] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const { unseenCount, refresh } = useNotifications({
take: 1,
skip: 0,
orderBy: "createdAt:DESC",
});
// Close notification panel on outside click
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setOpenNotifications(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
return (
<>
<header className="fixed top-0 left-0 right-0 z-50 flex justify-between items-center gap-3 px-6 py-3 border-b dark:border-gray-800 bg-white dark:bg-gray-900 shadow-sm">
<div className="flex items-center gap-2">
<DropdownMenu>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-9 w-9 flex-shrink-0 rounded-xl bg-white/90 text-gray-600 shadow-sm ring-1 ring-inset ring-black/5 transition-colors hover:bg-primary-50 hover:text-primary-700 hover:ring-primary-200 dark:bg-gray-800/90 dark:text-gray-300 dark:ring-white/10 dark:hover:bg-primary-900/20 dark:hover:text-primary-300 dark:hover:ring-primary-700/40"
aria-label={t("nav.moduleNavigationHint")}
title={t("nav.moduleNavigationHint")}
>
<Menu className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{t("nav.moduleNavigationHint")}
</TooltipContent>
</Tooltip>
<DropdownMenuContent
align="start"
className="w-56 dark:bg-gray-900 dark:border-gray-800"
>
{modulesList
.filter((item) => {
const currentPath = pathname;
if (item.id === "homepage") {
return !currentPath.startsWith("/homepage");
}
if (item.id === "recordManagement") {
return !currentPath.startsWith("/record-management");
}
if (item.id === "performanceManagement") {
return !currentPath.startsWith("/performance-management");
}
if (item.id === "objectiveManagement") {
return !currentPath.startsWith("/objective-management");
}
if (item.id === "documentManagement") {
return !currentPath.startsWith("/dms");
}
if (item.id === "complaints") {
return !currentPath.startsWith("/complaints");
}
if (item.id === "orgAdmin" || item.id === "superAdmin") {
return !currentPath.startsWith("/user-management");
}
return true;
})
.map((item) => (
<DropdownMenuItem
key={item.id}
onClick={() => navigate(item.path)}
className="cursor-pointer hover:bg-primary-50 dark:hover:bg-primary-900/30 hover:text-primary-700 dark:hover:text-primary-400 py-2.5"
>
<span>{item.label}</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex items-center gap-3">
{/* Notification bell */}
<div className="relative" ref={dropdownRef}>
<Button
variant="ghost"
onClick={() => {
setOpenNotifications((prev) => {
const next = !prev;
if (next) refresh();
return next;
});
}}
aria-label="Toggle Notifications"
className="p-2 hover:bg-primary-50 dark:hover:bg-primary-900/30 rounded-full transition-colors duration-200 relative">
<FiBell className="w-5 h-5 text-gray-700 dark:text-gray-300" />
{unseenCount > 0 && (
<span className="absolute -top-1 -right-1 min-w-4.5 rounded-full bg-red-500 px-1.5 py-0.5 text-center text-[10px] font-bold text-white shadow">
{unseenCount > 99 ? "99+" : unseenCount}
</span>
)}
</Button>
{openNotifications && (
<div className="absolute right-0 z-50 mt-2 w-88 overflow-hidden rounded-2xl bg-white shadow-2xl ring-1 ring-black/5 dark:bg-gray-900 dark:ring-white/10 sm:w-96">
<NotificationList />
</div>
)}
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="gap-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-primary-50 dark:hover:bg-primary-900/30 hover:text-primary-700 dark:hover:text-primary-400 transition-colors duration-200 font-medium">
<Globe className="w-4 h-4" />
{getUiLanguageLabel(currentLanguage, t)}
<ChevronDown className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="min-w-[140px] dark:bg-gray-900 dark:border-gray-800">
<DropdownMenuItem
onClick={() => changeLanguage("en")}
className="cursor-pointer hover:bg-primary-50 dark:hover:bg-primary-900/30 hover:text-primary-700 dark:hover:text-primary-400">
English
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => changeLanguage("am")}
className="cursor-pointer hover:bg-primary-50 dark:hover:bg-primary-900/30 hover:text-primary-700 dark:hover:text-primary-400">
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="flex items-center gap-3 cursor-pointer hover:bg-primary-50 dark:hover:bg-primary-900/30 rounded-lg px-3 py-2 transition-colors duration-200 group">
<Avatar className="w-9 h-9 border-2 border-primary-100 dark:border-primary-900 group-hover:border-primary-200 dark:group-hover:border-primary-800 transition-colors">
<AvatarFallback className="bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-400 font-semibold text-sm">
{initials.toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="text-xs text-right">
<div className="font-semibold text-gray-900 dark:text-gray-100">
{userDetails?.name?.en}
</div>
<div className="text-gray-500 dark:text-gray-400 text-[11px]">
@{userDetails?.username}
</div>
</div>
<ChevronDown className="w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-52 dark:bg-gray-900 dark:border-gray-800">
<DropdownMenuItem
onClick={() => navigate("/profile")}
className="cursor-pointer hover:bg-primary-50 dark:hover:bg-primary-900/30 hover:text-primary-700 dark:hover:text-primary-400 py-2.5">
<User className="w-4 h-4 mr-3" />
{t("header.viewProfile")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => navigate("/dashboard/profile")}
className="cursor-pointer hover:bg-primary-50 dark:hover:bg-primary-900/30 hover:text-primary-700 dark:hover:text-primary-400 py-2.5">
<Key className="w-4 h-4 mr-3" />
{t("header.changePassword")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={handleLogout}
className="cursor-pointer hover:bg-red-50 dark:hover:bg-red-900/30 hover:text-red-700 dark:hover:text-red-400 py-2.5">
<LogOut className="w-4 h-4 mr-3" />
{t("header.signOut")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
</>
);
};