mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
Merge pull request #913 from Tria-plc/freight/feat/fixes-v1q
Freight/feat/fixes v1
This commit is contained in:
@@ -627,7 +627,9 @@ const filterSidebarByPermission = (
|
||||
const filterItems = (items: SidebarItem[]): SidebarItem[] =>
|
||||
items
|
||||
.map((item) =>
|
||||
item.children ? { ...item, children: filterItems(item.children) } : item,
|
||||
item.children
|
||||
? { ...item, children: filterItems(item.children) }
|
||||
: item,
|
||||
)
|
||||
.filter((item) => {
|
||||
if (etGl || djGl) {
|
||||
|
||||
@@ -88,8 +88,8 @@ export const resolveModuleConfig = (config: TenantConfig): ModuleConfig => ({
|
||||
});
|
||||
|
||||
const defaultConfig: TenantConfig = {
|
||||
appName: "Smart Office",
|
||||
organizationName: "Smart Office",
|
||||
appName: "EDR Freight",
|
||||
organizationName: "Ethio-Djibouti Railways",
|
||||
canUseAttachmentFromDMS: false,
|
||||
logo: "/assets/TriaTradinglogo.png",
|
||||
primaryColor: "#1b354d",
|
||||
@@ -116,8 +116,8 @@ const defaultConfig: TenantConfig = {
|
||||
|
||||
const tenantConfigs: Record<string, TenantConfig> = {
|
||||
localhost: {
|
||||
appName: "Smart Office",
|
||||
organizationName: "Addis Ababa City Administration",
|
||||
appName: "EDR Freight",
|
||||
organizationName: "Ethio-Djibouti Railways",
|
||||
logo: "",
|
||||
primaryColor: "#0EA371",
|
||||
moduleConfig: {
|
||||
|
||||
@@ -1,413 +0,0 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { NavLink, useLocation } from "react-router-dom";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import {
|
||||
LayoutGrid,
|
||||
Upload,
|
||||
Download,
|
||||
CheckSquare,
|
||||
UserCheck,
|
||||
FileText,
|
||||
LucideIcon,
|
||||
Settings,
|
||||
Handshake,
|
||||
BellRing,
|
||||
BarChart3,
|
||||
} from "lucide-react";
|
||||
import { useUserDetail } from "../hooks/useUserDetail";
|
||||
import { useAuthUser } from "@/shared/hooks/useAuthUser";
|
||||
import Cookies from "js-cookie";
|
||||
import { useDelegations } from "../hooks/useDelegations";
|
||||
import Top from "./Top";
|
||||
|
||||
import { useReport } from "../hooks/useReport";
|
||||
import { hasApprovalPermission } from "@/record-management/routes/routes";
|
||||
import { useReportByHooks } from "../hooks/useReportByHooks";
|
||||
import { useMyCollaborations } from "../hooks/useMyCollaborations";
|
||||
|
||||
interface HeaderProps {
|
||||
onToggleSidebar?: () => void;
|
||||
}
|
||||
|
||||
export interface INavTabs {
|
||||
icon: LucideIcon;
|
||||
isVissible?: boolean;
|
||||
label?: string;
|
||||
title?: string;
|
||||
href?: string;
|
||||
url?: string;
|
||||
isActive?: boolean;
|
||||
count?: boolean;
|
||||
isPrimary?: boolean;
|
||||
countBadge?: number;
|
||||
isUrgent?: boolean;
|
||||
}
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({ onToggleSidebar }) => {
|
||||
const baseParams = {
|
||||
skip: 0,
|
||||
take: 10,
|
||||
orderBy: "employeePosition.createdAt:DESC",
|
||||
};
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const { hasDelegated } = useDelegations(baseParams);
|
||||
const { userDetails, selectedPositionPermissionKeys, selectedPosition } =
|
||||
useAuthUser();
|
||||
const { permissionKeys } = useUserDetail(userDetails);
|
||||
const useParentCounts =
|
||||
permissionKeys.includes("can:viewParentPositionRecord") &&
|
||||
new URLSearchParams(location.search).get("view") === "parent";
|
||||
const currentPosition = Cookies.get("current-position-id");
|
||||
const canViewApprovalTab = hasApprovalPermission(
|
||||
selectedPositionPermissionKeys,
|
||||
);
|
||||
|
||||
// Reuse the auth layer's already-normalized active position instead of
|
||||
// re-deriving it here. useAuthUser resolves it by matching BOTH `id` and
|
||||
// `employeePositionId` (and honors the delegated-position cookie), so a
|
||||
// delegate's position is found and `isDelegate` is reliable. The previous
|
||||
// local lookup matched `pos.id === selectedPositionId` only — but
|
||||
// `selectedPositionId` is normalized to `employeePositionId`, so the find
|
||||
// returned undefined and `!undefined` left every tab visible for delegates.
|
||||
const canViewDelegationTab = !selectedPosition?.isDelegate;
|
||||
const userRoles =
|
||||
userDetails?.roles?.map((r: { key: string }) => r.key) || [];
|
||||
const showUserManagementShortcut =
|
||||
userRoles.includes("admin") ||
|
||||
userRoles.includes("unit_admin") ||
|
||||
userRoles.includes("super_admin");
|
||||
|
||||
const {
|
||||
dashboard,
|
||||
ROdashboard,
|
||||
TotalDraftExternal,
|
||||
TotalDraftInternal,
|
||||
TotalDraftCC,
|
||||
ROTotalDraftIncoming,
|
||||
ROPending,
|
||||
TotalUrgentDraftInternal,
|
||||
TotalUrgentDraftExternal,
|
||||
ROPendingUrgent,
|
||||
} = useReport("");
|
||||
const { breakdownCounts } = useReportByHooks({
|
||||
isSecretary: useParentCounts,
|
||||
});
|
||||
const { data: draftCollaborations } = useMyCollaborations({
|
||||
skip: 0,
|
||||
take: 1,
|
||||
signStatus: "draft",
|
||||
});
|
||||
const collaborationDraftCount =
|
||||
draftCollaborations?.count ?? dashboard?.collaborationCount?.draft ?? 0;
|
||||
const incomingHref = useParentCounts
|
||||
? "/record-management/userIncoming?view=parent"
|
||||
: "/record-management/userIncoming";
|
||||
|
||||
const navigationTabs = useMemo(
|
||||
(): INavTabs[] => [
|
||||
{
|
||||
icon: LayoutGrid,
|
||||
label: t("header.navigation.dashboard"),
|
||||
href: "/record-management/dashboard",
|
||||
isActive: location.pathname.includes("/record-management/dashboard"),
|
||||
isPrimary: true,
|
||||
// Delegates see only Outgoing, Incoming, Approval — hide Dashboard.
|
||||
isVissible: canViewDelegationTab,
|
||||
},
|
||||
{
|
||||
icon: BarChart3,
|
||||
label: "Reports",
|
||||
href: "/record-management/sector-reports",
|
||||
isActive: location.pathname.startsWith(
|
||||
"/record-management/sector-reports",
|
||||
),
|
||||
isPrimary: false,
|
||||
isVissible: false,
|
||||
},
|
||||
{
|
||||
icon: Upload,
|
||||
label: t("header.navigation.outgoing"),
|
||||
href: "/record-management/userRecords",
|
||||
isActive:
|
||||
location.pathname.startsWith("/record-management/userRecords") &&
|
||||
new URLSearchParams(location.search).get("from") !== "collaborations",
|
||||
isPrimary: true,
|
||||
isVissible: true,
|
||||
},
|
||||
{
|
||||
icon: Download,
|
||||
label: t("header.navigation.incoming"),
|
||||
href: incomingHref,
|
||||
isActive:
|
||||
location.pathname.startsWith("/record-management/userIncoming") ||
|
||||
location.pathname.startsWith(
|
||||
"/record-management/viewIncoming/incoming/",
|
||||
) ||
|
||||
location.pathname.startsWith(
|
||||
"/record-management/viewIncoming/internal",
|
||||
) ||
|
||||
location.pathname.startsWith("/record-management/viewIncoming/cc"),
|
||||
isPrimary: true,
|
||||
isVissible: true,
|
||||
countBadge:
|
||||
breakdownCounts.external +
|
||||
breakdownCounts.externalSmart +
|
||||
breakdownCounts.internal +
|
||||
breakdownCounts.ccUnseen +
|
||||
breakdownCounts.forYourReference,
|
||||
isUrgent: TotalUrgentDraftInternal > 0 || TotalUrgentDraftExternal > 0,
|
||||
},
|
||||
{
|
||||
icon: CheckSquare,
|
||||
label: t("header.navigation.approval"),
|
||||
href: "/record-management/approval",
|
||||
isActive:
|
||||
location.pathname.startsWith("/record-management/approval") ||
|
||||
location.pathname.startsWith("/record-management/viewApproval"),
|
||||
isPrimary: false,
|
||||
isVissible: canViewApprovalTab,
|
||||
countBadge: breakdownCounts?.approval || 0,
|
||||
isUrgent:
|
||||
(dashboard?.activeApprovalCount?.myUrgentWorkflowCount || 0) > 0,
|
||||
},
|
||||
{
|
||||
icon: UserCheck,
|
||||
label: t("header.navigation.delegation"),
|
||||
href: "/record-management/delegation",
|
||||
isActive: location.pathname.startsWith("/record-management/delegation"),
|
||||
isPrimary: false,
|
||||
isVissible: canViewDelegationTab,
|
||||
},
|
||||
{
|
||||
icon: Handshake,
|
||||
label: t("header.navigation.collaborations"),
|
||||
href: "/record-management/collaborations",
|
||||
isActive:
|
||||
location.pathname.startsWith("/record-management/collaborations") ||
|
||||
new URLSearchParams(location.search).get("from") === "collaborations",
|
||||
isPrimary: false,
|
||||
isVissible: canViewDelegationTab,
|
||||
countBadge: collaborationDraftCount,
|
||||
},
|
||||
{
|
||||
icon: Settings,
|
||||
label: t("header.navigation.settings"),
|
||||
href: "/record-management/uploadTeeterandSignature",
|
||||
isActive: location.pathname.startsWith(
|
||||
"/record-management/uploadTeeterandSignature",
|
||||
),
|
||||
isPrimary: false,
|
||||
isVissible: canViewDelegationTab,
|
||||
},
|
||||
],
|
||||
[
|
||||
t,
|
||||
location.pathname,
|
||||
location.search,
|
||||
TotalDraftCC,
|
||||
TotalDraftInternal,
|
||||
TotalDraftExternal,
|
||||
TotalUrgentDraftInternal,
|
||||
TotalUrgentDraftExternal,
|
||||
canViewApprovalTab,
|
||||
canViewDelegationTab,
|
||||
useParentCounts,
|
||||
incomingHref,
|
||||
breakdownCounts.approval,
|
||||
breakdownCounts.ccUnseen,
|
||||
breakdownCounts.external,
|
||||
breakdownCounts.externalSmart,
|
||||
breakdownCounts.forYourReference,
|
||||
breakdownCounts.internal,
|
||||
dashboard?.activeApprovalCount?.myNotUrgentWorkflowsCount,
|
||||
dashboard?.activeApprovalCount?.myUrgentWorkflowCount,
|
||||
collaborationDraftCount,
|
||||
],
|
||||
);
|
||||
|
||||
const recordOfficerNavs = useMemo(
|
||||
(): INavTabs[] => [
|
||||
{
|
||||
icon: LayoutGrid,
|
||||
label: t("header.navigation.dashboard"),
|
||||
href: "/record-management/dashboard",
|
||||
isActive: location.pathname.includes("/dashboard"),
|
||||
isVissible: true,
|
||||
},
|
||||
{
|
||||
icon: Upload,
|
||||
label: t("header.navigation.outgoing"),
|
||||
href: "/record-management/recordOfficer/outgoing",
|
||||
isActive:
|
||||
location.pathname.startsWith(
|
||||
"/record-management/recordOfficer/outgoing",
|
||||
) ||
|
||||
location.pathname.includes("/record-management/view/") ||
|
||||
location.pathname.includes("/record-management/outgoingview/"),
|
||||
isVissible: true,
|
||||
},
|
||||
{
|
||||
icon: Download,
|
||||
label: t("header.navigation.incoming"),
|
||||
href: "/record-management/recordOfficer/incoming",
|
||||
isActive:
|
||||
location.pathname.startsWith(
|
||||
"/record-management/recordOfficer/incoming",
|
||||
) ||
|
||||
location.pathname.includes("/record-management/viewIncoming/") ||
|
||||
location.pathname.includes("/record-management/recordViewIncoming/"),
|
||||
isVissible: true,
|
||||
countBadge: ROTotalDraftIncoming,
|
||||
},
|
||||
{
|
||||
icon: FileText,
|
||||
label: t("header.navigation.pending"),
|
||||
href: "/record-management/pending",
|
||||
isActive:
|
||||
location.pathname.startsWith("/record-management/pending") ||
|
||||
location.pathname.includes("/record-management/viewPending/"),
|
||||
isVissible: true,
|
||||
countBadge: ROPending,
|
||||
isUrgent: ROPendingUrgent > 0,
|
||||
},
|
||||
],
|
||||
[t, location.pathname, ROTotalDraftIncoming, ROPending, ROPendingUrgent],
|
||||
);
|
||||
|
||||
const activeNavigationTabs = useMemo(() => {
|
||||
if (permissionKeys.includes("can:dispatchRecords")) {
|
||||
return recordOfficerNavs;
|
||||
}
|
||||
return navigationTabs.filter((tab) => tab.isVissible);
|
||||
}, [permissionKeys, navigationTabs, recordOfficerNavs]);
|
||||
|
||||
// const { user } = useAuth();
|
||||
// console.log("User Info:", user);
|
||||
// const organizationId =
|
||||
// user?.employee && user.employee.length > 0
|
||||
// ? user.employee[0].unitId
|
||||
// : undefined;
|
||||
|
||||
// const unitId = organizationId;
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<Top
|
||||
onToggleSidebar={onToggleSidebar}
|
||||
showUserManagementShortcut={showUserManagementShortcut}
|
||||
/>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
{currentPosition ? (
|
||||
<div className="fixed top-16 left-0 right-0 z-30 bg-white dark:bg-gray-900 border-b dark:border-gray-800 overflow-hidden pt-1 pb-1 px-2 sm:pt-1.5 sm:pb-1.5 sm:px-4 md:px-8">
|
||||
{/* Mobile layout: Simple horizontal scrollable (up to sm) */}
|
||||
<div className="sm:hidden">
|
||||
<div className="flex items-center overflow-x-auto scrollbar-none">
|
||||
{activeNavigationTabs.map((tab, index) => (
|
||||
<NavLink
|
||||
key={`${tab.href}-${index}`}
|
||||
to={tab.href || "#"}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"flex items-center gap-1 px-3 py-2 mr-2 text-xs font-medium transition-colors whitespace-nowrap relative",
|
||||
isActive
|
||||
? "text-primary-800 dark:text-white border-b-2 border-primary-600 dark:border-primary-400"
|
||||
: "text-gray-700 dark:text-gray-300 hover:text-primary-800 dark:hover:text-white",
|
||||
)
|
||||
}
|
||||
>
|
||||
<tab.icon className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="max-w-[80px] truncate">{tab.label}</span>
|
||||
|
||||
{/* Count Badge - positioned on tab */}
|
||||
{typeof tab?.countBadge === "number" &&
|
||||
tab.countBadge > 0 && (
|
||||
<span className="bg-red-500 text-white text-[10px] font-bold rounded-full px-1.5 py-0.5 ml-1">
|
||||
{tab.countBadge > 99 ? "99+" : tab.countBadge}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Urgent Bell - positioned on tab */}
|
||||
{typeof tab?.isUrgent === "boolean" && tab.isUrgent && (
|
||||
<span className="relative flex h-3 w-3 ml-1">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Live indicator for Delegation */}
|
||||
{hasDelegated &&
|
||||
(tab.label?.toLowerCase() === "delegation" ||
|
||||
tab.label?.toLowerCase() === "ዉክልና") && (
|
||||
<span className="absolute -top-1 -right-1 flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-500 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
|
||||
</span>
|
||||
)}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop layout: All tabs in one row (sm and up) */}
|
||||
<div className="hidden sm:flex overflow-x-auto scrollbar-none w-full bg-gray-200 dark:bg-gray-800 rounded-md py-1.5">
|
||||
<div className="flex w-full mx-1.5 gap-1.5">
|
||||
{activeNavigationTabs.map((tab, index) => (
|
||||
<NavLink
|
||||
key={`${tab.href}-${index}`}
|
||||
to={tab.href || "#"}
|
||||
className={() =>
|
||||
cn(
|
||||
"flex items-center justify-center px-2.5 py-2.5 rounded-sm text-sm whitespace-nowrap flex-1 font-Urbanist relative",
|
||||
tab.isActive
|
||||
? "bg-primary-50 dark:bg-primary-900/40 text-primary-800 dark:text-white font-medium"
|
||||
: "text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700",
|
||||
)
|
||||
}
|
||||
>
|
||||
<tab.icon className="h-4 w-4 mr-1.5 flex-shrink-0" />
|
||||
<span className="flex items-center gap-1.5">
|
||||
{tab.label}
|
||||
|
||||
{/* Count Badge (inline) */}
|
||||
{typeof tab?.countBadge === "number" &&
|
||||
tab.countBadge > 0 && (
|
||||
<span className="bg-red-500 text-white text-xs font-semibold rounded-full px-1.5 py-0.5">
|
||||
{tab.countBadge > 99 ? "99+" : tab.countBadge}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Urgent Bell (pulsing) */}
|
||||
{typeof tab?.isUrgent === "boolean" && tab.isUrgent && (
|
||||
<span className="relative flex h-4 w-4">
|
||||
{/* Pulse effect */}
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
|
||||
{/* Bell icon */}
|
||||
<span className="relative inline-flex items-center justify-center">
|
||||
<BellRing className="h-4 w-4 text-red-500" />
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
{/* Live indicator for Delegation */}
|
||||
{hasDelegated &&
|
||||
(tab.label?.toLowerCase() === "delegation" ||
|
||||
tab.label?.toLowerCase() === "ዉክልና") && (
|
||||
<span className="relative -top-0.5 -right-0.5 flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-500 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
|
||||
</span>
|
||||
)}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,6 @@ import {
|
||||
ClipboardList,
|
||||
Clock,
|
||||
FileText,
|
||||
Home,
|
||||
Languages,
|
||||
Key,
|
||||
LogOut,
|
||||
@@ -104,9 +103,9 @@ const Top: React.FC<HeaderProps> = ({
|
||||
const activePositionName =
|
||||
currentLanguage === "am"
|
||||
? userDetails?.employee?.[0]?.positions?.[0]?.name?.am ||
|
||||
userDetails?.employee?.[0]?.positions?.[0]?.name?.en
|
||||
userDetails?.employee?.[0]?.positions?.[0]?.name?.en
|
||||
: userDetails?.employee?.[0]?.positions?.[0]?.name?.en ||
|
||||
userDetails?.employee?.[0]?.positions?.[0]?.name?.am;
|
||||
userDetails?.employee?.[0]?.positions?.[0]?.name?.am;
|
||||
|
||||
const normalizedUserType = userDetails?.userType?.trim().toLowerCase() || "";
|
||||
const userTypeLabel =
|
||||
@@ -114,8 +113,8 @@ const Top: React.FC<HeaderProps> = ({
|
||||
? t("header.user")
|
||||
: normalizedUserType
|
||||
? normalizedUserType
|
||||
.replace(/[_-]/g, " ")
|
||||
.replace(/\b\w/g, (char) => char.toUpperCase())
|
||||
.replace(/[_-]/g, " ")
|
||||
.replace(/\b\w/g, (char) => char.toUpperCase())
|
||||
: "";
|
||||
|
||||
const roleLabel =
|
||||
@@ -123,8 +122,8 @@ const Top: React.FC<HeaderProps> = ({
|
||||
userTypeLabel ||
|
||||
(normalizedUserType
|
||||
? userDetails?.roles?.[0]?.key
|
||||
?.replace(/[:_]/g, " ")
|
||||
.replace(/\b\w/g, (char) => char.toUpperCase())
|
||||
?.replace(/[:_]/g, " ")
|
||||
.replace(/\b\w/g, (char) => char.toUpperCase())
|
||||
: "");
|
||||
|
||||
const { permissionKeys } = useUserDetail(userDetails as MeDto);
|
||||
@@ -159,57 +158,57 @@ const Top: React.FC<HeaderProps> = ({
|
||||
},
|
||||
...(moduleConfig.recordManagement
|
||||
? [
|
||||
{
|
||||
id: "recordManagement",
|
||||
label: t("nav.Record Management", "Record Management"),
|
||||
path: "/record-management/dashboard",
|
||||
},
|
||||
]
|
||||
{
|
||||
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",
|
||||
},
|
||||
]
|
||||
{
|
||||
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",
|
||||
},
|
||||
]
|
||||
{
|
||||
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",
|
||||
},
|
||||
]
|
||||
{
|
||||
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",
|
||||
},
|
||||
]
|
||||
{
|
||||
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",
|
||||
},
|
||||
]
|
||||
{
|
||||
id: "superAdmin",
|
||||
label: t("OrganizationAdmin", "Super Admin"),
|
||||
path: "/user-management/dashboard",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
@@ -256,10 +255,28 @@ const Top: React.FC<HeaderProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-24 flex-col">
|
||||
<header className="fixed left-0 right-0 top-0 z-40 h-16 border-b border-primary-100/70 bg-white/90 shadow-sm backdrop-blur-md dark:border-gray-800 dark:bg-gray-900/90">
|
||||
<div className="flex h-full items-center justify-between px-2 sm:px-4 md:px-6">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2 md:gap-3">
|
||||
<header className="sticky top-0 z-40 h-14 shrink-0 border-b border-primary-100/70 bg-white/90 shadow-sm backdrop-blur-md dark:border-gray-800 dark:bg-gray-900/90">
|
||||
<div className="flex h-full items-center justify-between px-2 sm:px-4 md:px-6">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2 md:gap-3">
|
||||
{onToggleSidebar ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onToggleSidebar}
|
||||
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="Toggle sidebar"
|
||||
title="Toggle sidebar"
|
||||
>
|
||||
<FiMenu className="h-5 w-5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" sideOffset={6}>
|
||||
Toggle sidebar
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<DropdownMenu>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -320,191 +337,183 @@ const Top: React.FC<HeaderProps> = ({
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
<div className="hidden min-w-0 flex-1 sm:block sm:max-w-[34vw] lg:max-w-[41vw] xl:max-w-[48vw]">
|
||||
<DynamicBreadcrumb />
|
||||
</div>
|
||||
|
||||
{showUserManagementShortcut && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex h-9 w-9 sm:w-auto items-center justify-center sm:justify-start gap-1.5 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 px-0 sm:px-3"
|
||||
onClick={() => navigate("/user-management")}
|
||||
>
|
||||
<User className="h-4 w-4 shrink-0 text-primary-500" />
|
||||
<span className="hidden sm:inline text-xs font-semibold">
|
||||
{t("dashboard.userManagement", "User Management")}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
<div className="hidden min-w-0 flex-1 sm:block sm:max-w-[34vw] lg:max-w-[41vw] xl:max-w-[48vw]">
|
||||
<DynamicBreadcrumb />
|
||||
</div>
|
||||
|
||||
<div className="flex max-w-[56vw] flex-shrink-0 items-center space-x-1.5 sm:max-w-none sm:space-x-2.5">
|
||||
{showUserManagementShortcut && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex h-9 w-9 sm:w-auto items-center justify-center sm:justify-start gap-1.5 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 px-0 sm:px-3"
|
||||
onClick={() => navigate("/user-management")}
|
||||
>
|
||||
<User className="h-4 w-4 shrink-0 text-primary-500" />
|
||||
<span className="hidden sm:inline text-xs font-semibold">
|
||||
{t("dashboard.userManagement", "User Management")}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex max-w-[56vw] flex-shrink-0 items-center space-x-1.5 sm:max-w-none sm:space-x-2.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 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 sm:h-10 sm:w-10"
|
||||
aria-label={isDarkMode ? "Light mode" : "Dark mode"}
|
||||
onClick={toggleDarkMode}
|
||||
title={isDarkMode ? "Switch to light mode" : "Switch to dark mode"}
|
||||
>
|
||||
{isDarkMode ? (
|
||||
<Sun className="h-4 w-4 text-yellow-500" />
|
||||
) : (
|
||||
<Moon className="h-4 w-4 text-gray-700 dark:text-gray-300" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{canActivateUsers && (
|
||||
<div className="relative">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="relative h-9 w-9 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 sm:h-10 sm:w-10"
|
||||
aria-label={t("header.notifications")}
|
||||
onClick={() => setPendingUsers((prev) => !prev)}
|
||||
>
|
||||
<ClipboardList className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{pendingUsersCount > 0 && (
|
||||
<span className="absolute -right-1.5 -top-1.5 rounded-full bg-yellow-500 px-1.5 py-0.5 text-[10px] font-bold text-white shadow">
|
||||
{pendingUsersCount > 99 ? "99+" : pendingUsersCount}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{openPendingUsers && (
|
||||
<div
|
||||
ref={pendingUsersRef}
|
||||
className="fixed left-3 right-3 top-16 z-50 mt-2 max-h-[calc(100dvh-5rem)] overflow-y-auto rounded-2xl bg-white p-3 shadow-2xl ring-1 ring-black/5 dark:bg-gray-900 dark:ring-white/10 sm:absolute sm:left-auto sm:right-0 sm:top-auto sm:max-h-96 sm:w-80 sm:p-4"
|
||||
>
|
||||
<UserApprovalDropdown />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 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 sm:h-10 sm:w-10"
|
||||
aria-label={isDarkMode ? "Light mode" : "Dark mode"}
|
||||
onClick={toggleDarkMode}
|
||||
title={
|
||||
isDarkMode ? "Switch to light mode" : "Switch to dark mode"
|
||||
className={cn(
|
||||
"relative h-9 w-9 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 sm:h-10 sm:w-10",
|
||||
openNotifications &&
|
||||
"bg-primary-50 text-primary-700 ring-primary-300 dark:bg-primary-900/30 dark:text-primary-300 dark:ring-primary-700/60",
|
||||
)}
|
||||
aria-label={t("header.notifications")}
|
||||
onClick={() =>
|
||||
setOpenNotifications((prev) => {
|
||||
const next = !prev;
|
||||
if (next) refresh();
|
||||
return next;
|
||||
})
|
||||
}
|
||||
>
|
||||
{isDarkMode ? (
|
||||
<Sun className="h-4 w-4 text-yellow-500" />
|
||||
) : (
|
||||
<Moon className="h-4 w-4 text-gray-700 dark:text-gray-300" />
|
||||
<FiBell
|
||||
className={cn(
|
||||
"h-4 w-4 transition-colors",
|
||||
openNotifications &&
|
||||
"fill-primary-100 text-primary-700 dark:fill-primary-900/40 dark:text-primary-300",
|
||||
)}
|
||||
/>
|
||||
{unseenCount > 0 && (
|
||||
<span className="absolute -right-1.5 -top-1.5 min-w-[18px] 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>
|
||||
|
||||
{canActivateUsers && (
|
||||
<div className="relative">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="relative h-9 w-9 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 sm:h-10 sm:w-10"
|
||||
aria-label={t("header.notifications")}
|
||||
onClick={() => setPendingUsers((prev) => !prev)}
|
||||
>
|
||||
<ClipboardList className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{pendingUsersCount > 0 && (
|
||||
<span className="absolute -right-1.5 -top-1.5 rounded-full bg-yellow-500 px-1.5 py-0.5 text-[10px] font-bold text-white shadow">
|
||||
{pendingUsersCount > 99 ? "99+" : pendingUsersCount}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{openPendingUsers && (
|
||||
<div
|
||||
ref={pendingUsersRef}
|
||||
className="fixed left-3 right-3 top-16 z-50 mt-2 max-h-[calc(100dvh-5rem)] overflow-y-auto rounded-2xl bg-white p-3 shadow-2xl ring-1 ring-black/5 dark:bg-gray-900 dark:ring-white/10 sm:absolute sm:left-auto sm:right-0 sm:top-auto sm:max-h-96 sm:w-80 sm:p-4"
|
||||
>
|
||||
<UserApprovalDropdown />
|
||||
</div>
|
||||
)}
|
||||
{openNotifications && (
|
||||
<div className="fixed left-3 right-3 top-16 z-50 mt-2 max-h-[calc(100dvh-5rem)] overflow-hidden rounded-2xl bg-white shadow-2xl ring-1 ring-black/5 dark:bg-gray-900 dark:ring-white/10 sm:absolute sm:left-auto sm:right-0 sm:top-auto sm:w-[24rem]">
|
||||
<NotificationList />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"relative h-9 w-9 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 sm:h-10 sm:w-10",
|
||||
openNotifications &&
|
||||
"bg-primary-50 text-primary-700 ring-primary-300 dark:bg-primary-900/30 dark:text-primary-300 dark:ring-primary-700/60",
|
||||
)}
|
||||
aria-label={t("header.notifications")}
|
||||
onClick={() =>
|
||||
setOpenNotifications((prev) => {
|
||||
const next = !prev;
|
||||
if (next) refresh();
|
||||
return next;
|
||||
})
|
||||
}
|
||||
>
|
||||
<FiBell
|
||||
{/* Reminders */}
|
||||
<div className="relative" ref={remindersRef}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="relative h-9 w-9 rounded-xl bg-white/90 text-gray-600 shadow-sm ring-1 ring-inset ring-black/5 transition-colors hover:bg-primary/10 hover:text-primary hover:ring-primary/30 dark:bg-gray-800/90 dark:text-gray-300 dark:ring-white/10 dark:hover:bg-primary/20 dark:hover:text-primary-300 dark:hover:ring-primary/40 sm:h-10 sm:w-10"
|
||||
aria-label="Reminders"
|
||||
onClick={() => setOpenReminders((prev) => !prev)}
|
||||
>
|
||||
<Clock className="h-4 w-4" />
|
||||
{pendingCount > 0 && (
|
||||
<span className="absolute -right-1.5 -top-1.5 min-w-[18px] rounded-full bg-primary px-1.5 py-0.5 text-center text-[10px] font-bold text-primary-foreground shadow">
|
||||
{pendingCount > 99 ? "99+" : pendingCount}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
{openReminders && (
|
||||
<div className="fixed left-3 right-3 top-16 z-50 mt-2 max-h-[calc(100dvh-5rem)] overflow-hidden rounded-2xl bg-white shadow-2xl ring-1 ring-black/5 dark:bg-gray-900 dark:ring-white/10 sm:absolute sm:left-auto sm:right-0 sm:top-auto sm:w-[24rem]">
|
||||
<ReminderList />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Languages className="h-4 w-4 text-primary-500" />
|
||||
<span className="hidden sm:inline text-xs">
|
||||
{getUiLanguageShortLabel(currentLanguage, t)}
|
||||
</span>
|
||||
<ChevronDown className="hidden h-3.5 w-3.5 text-gray-400 sm:block" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
className="w-52 rounded-xl bg-white p-2 shadow-xl ring-1 ring-black/5 dark:bg-gray-900 dark:ring-white/10"
|
||||
align="end"
|
||||
forceMount
|
||||
>
|
||||
{UI_LANGUAGE_OPTIONS.map((lang) => (
|
||||
<DropdownMenuItem
|
||||
key={lang.value}
|
||||
onClick={() => changeLanguage(lang.value)}
|
||||
className={cn(
|
||||
"h-4 w-4 transition-colors",
|
||||
openNotifications &&
|
||||
"fill-primary-100 text-primary-700 dark:fill-primary-900/40 dark:text-primary-300",
|
||||
"flex cursor-pointer items-center justify-between rounded-lg px-3 py-2.5 text-sm text-gray-700 transition-colors hover:bg-primary-50 dark:text-gray-200 dark:hover:bg-primary-900/30",
|
||||
currentLanguage === lang.value &&
|
||||
"bg-primary-100 text-primary-800 dark:bg-primary-800/50 dark:text-white",
|
||||
)}
|
||||
/>
|
||||
{unseenCount > 0 && (
|
||||
<span className="absolute -right-1.5 -top-1.5 min-w-[18px] 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>
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex h-6 w-6 items-center justify-center rounded-md bg-gray-100 text-[11px] font-bold text-gray-600 dark:bg-gray-700 dark:text-gray-200">
|
||||
{getUiLanguageShortLabel(lang.value, t)}
|
||||
</span>
|
||||
<span>{getUiLanguageLabel(lang.value, t)}</span>
|
||||
</div>
|
||||
{currentLanguage === lang.value && (
|
||||
<Check className="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{openNotifications && (
|
||||
<div className="fixed left-3 right-3 top-16 z-50 mt-2 max-h-[calc(100dvh-5rem)] overflow-hidden rounded-2xl bg-white shadow-2xl ring-1 ring-black/5 dark:bg-gray-900 dark:ring-white/10 sm:absolute sm:left-auto sm:right-0 sm:top-auto sm:w-[24rem]">
|
||||
<NotificationList />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reminders */}
|
||||
<div className="relative" ref={remindersRef}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="relative h-9 w-9 rounded-xl bg-white/90 text-gray-600 shadow-sm ring-1 ring-inset ring-black/5 transition-colors hover:bg-primary/10 hover:text-primary hover:ring-primary/30 dark:bg-gray-800/90 dark:text-gray-300 dark:ring-white/10 dark:hover:bg-primary/20 dark:hover:text-primary-300 dark:hover:ring-primary/40 sm:h-10 sm:w-10"
|
||||
aria-label="Reminders"
|
||||
onClick={() => setOpenReminders((prev) => !prev)}
|
||||
className="group h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-r from-white to-primary-50/80 p-0 text-gray-700 shadow-sm ring-1 ring-inset ring-black/5 transition-colors hover:from-primary-50 hover:to-primary-100 hover:ring-primary-200 dark:from-gray-900 dark:to-gray-800 dark:text-gray-200 dark:ring-white/10 dark:hover:from-gray-800 dark:hover:to-primary-900/20 dark:hover:ring-primary-700/40 sm:h-10 sm:w-auto sm:max-w-[170px] sm:justify-start sm:gap-2 sm:rounded-2xl sm:px-1.5 sm:pr-2 md:max-w-[200px]"
|
||||
aria-label={t("header.userMenu")}
|
||||
>
|
||||
<Clock className="h-4 w-4" />
|
||||
{pendingCount > 0 && (
|
||||
<span className="absolute -right-1.5 -top-1.5 min-w-[18px] rounded-full bg-primary px-1.5 py-0.5 text-center text-[10px] font-bold text-primary-foreground shadow">
|
||||
{pendingCount > 99 ? "99+" : pendingCount}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
{openReminders && (
|
||||
<div className="fixed left-3 right-3 top-16 z-50 mt-2 max-h-[calc(100dvh-5rem)] overflow-hidden rounded-2xl bg-white shadow-2xl ring-1 ring-black/5 dark:bg-gray-900 dark:ring-white/10 sm:absolute sm:left-auto sm:right-0 sm:top-auto sm:w-[24rem]">
|
||||
<ReminderList />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-9 w-9 justify-center rounded-xl bg-white/90 px-0 text-xs font-semibold text-gray-700 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-200 dark:ring-white/10 dark:hover:bg-primary-900/20 dark:hover:text-primary-300 dark:hover:ring-primary-700/40 sm:h-10 sm:w-auto sm:gap-2 sm:px-3"
|
||||
>
|
||||
<Languages className="h-4 w-4 text-primary-500" />
|
||||
<span className="hidden md:inline">
|
||||
{getUiLanguageLabel(currentLanguage, t)}
|
||||
</span>
|
||||
<span className="hidden sm:inline md:hidden">
|
||||
{getUiLanguageShortLabel(currentLanguage, t)}
|
||||
</span>
|
||||
<ChevronDown className="hidden h-3.5 w-3.5 text-gray-400 sm:block" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
className="w-52 rounded-xl bg-white p-2 shadow-xl ring-1 ring-black/5 dark:bg-gray-900 dark:ring-white/10"
|
||||
align="end"
|
||||
forceMount
|
||||
>
|
||||
{UI_LANGUAGE_OPTIONS.map((lang) => (
|
||||
<DropdownMenuItem
|
||||
key={lang.value}
|
||||
onClick={() => changeLanguage(lang.value)}
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center justify-between rounded-lg px-3 py-2.5 text-sm text-gray-700 transition-colors hover:bg-primary-50 dark:text-gray-200 dark:hover:bg-primary-900/30",
|
||||
currentLanguage === lang.value &&
|
||||
"bg-primary-100 text-primary-800 dark:bg-primary-800/50 dark:text-white",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex h-6 w-6 items-center justify-center rounded-md bg-gray-100 text-[11px] font-bold text-gray-600 dark:bg-gray-700 dark:text-gray-200">
|
||||
{getUiLanguageShortLabel(lang.value, t)}
|
||||
</span>
|
||||
<span>{getUiLanguageLabel(lang.value, t)}</span>
|
||||
</div>
|
||||
{currentLanguage === lang.value && (
|
||||
<Check className="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="group h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-r from-white to-primary-50/80 p-0 text-gray-700 shadow-sm ring-1 ring-inset ring-black/5 transition-colors hover:from-primary-50 hover:to-primary-100 hover:ring-primary-200 dark:from-gray-900 dark:to-gray-800 dark:text-gray-200 dark:ring-white/10 dark:hover:from-gray-800 dark:hover:to-primary-900/20 dark:hover:ring-primary-700/40 sm:h-10 sm:w-auto sm:max-w-[170px] sm:justify-start sm:gap-2 sm:rounded-2xl sm:px-1.5 sm:pr-2 md:max-w-[200px]"
|
||||
aria-label={t("header.userMenu")}
|
||||
>
|
||||
<div
|
||||
className="
|
||||
<div
|
||||
className="
|
||||
flex h-8 w-8 sm:h-9 sm:w-9
|
||||
items-center justify-center
|
||||
rounded-full
|
||||
@@ -517,93 +526,92 @@ const Top: React.FC<HeaderProps> = ({
|
||||
ring-2 ring-white dark:ring-gray-900
|
||||
shrink-0
|
||||
"
|
||||
>
|
||||
{initials.toUpperCase()}
|
||||
</div>
|
||||
<div className="hidden min-w-0 flex-col items-start text-left lg:flex">
|
||||
<span className="max-w-[118px] truncate text-xs font-semibold leading-none">
|
||||
{fullName}
|
||||
</span>
|
||||
<span className="max-w-[118px] truncate pt-1 text-[10px] font-medium leading-none text-gray-500 dark:text-gray-400">
|
||||
{roleLabel}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDown className="hidden h-4 w-4 text-gray-400 transition group-hover:text-primary-600 dark:group-hover:text-primary-300 sm:block" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
className="w-64 rounded-xl bg-white p-2 shadow-xl ring-1 ring-black/5 dark:bg-gray-900 dark:ring-white/10"
|
||||
align="end"
|
||||
>
|
||||
<div className="mb-1 rounded-lg bg-gradient-to-r from-primary-500 to-primary-600 p-3 text-white">
|
||||
<p className="truncate text-sm font-semibold">{fullName}</p>
|
||||
<p className="truncate pt-1 text-xs text-white/90">
|
||||
{roleLabel}
|
||||
</p>
|
||||
>
|
||||
{initials.toUpperCase()}
|
||||
</div>
|
||||
<div className="hidden min-w-0 flex-col items-start text-left lg:flex">
|
||||
<span className="max-w-[118px] truncate text-xs font-semibold leading-none">
|
||||
{fullName}
|
||||
</span>
|
||||
<span className="max-w-[118px] truncate pt-1 text-[10px] font-medium leading-none text-gray-500 dark:text-gray-400">
|
||||
{roleLabel}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronDown className="hidden h-4 w-4 text-gray-400 transition group-hover:text-primary-600 dark:group-hover:text-primary-300 sm:block" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
className="w-64 rounded-xl bg-white p-2 shadow-xl ring-1 ring-black/5 dark:bg-gray-900 dark:ring-white/10"
|
||||
align="end"
|
||||
>
|
||||
<div className="mb-1 rounded-lg bg-gradient-to-r from-primary-500 to-primary-600 p-3 text-white">
|
||||
<p className="truncate text-sm font-semibold">{fullName}</p>
|
||||
<p className="truncate pt-1 text-xs text-white/90">
|
||||
{roleLabel}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="flex cursor-pointer items-center rounded-lg px-3 py-2.5 text-sm text-gray-700 transition-colors hover:bg-primary-50 dark:text-gray-200 dark:hover:bg-primary-900/20"
|
||||
onClick={() => navigate("/profile")}
|
||||
>
|
||||
<User className="mr-2.5 h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
<span>{t("header.viewProfile")}</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="flex cursor-pointer items-center rounded-lg px-3 py-2.5 text-sm text-gray-700 transition-colors hover:bg-primary-50 dark:text-gray-200 dark:hover:bg-primary-900/20"
|
||||
onClick={() => navigate("/update-profile")}
|
||||
>
|
||||
<UserPen className="mr-2.5 h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
<span>{t("header.editProfile")}</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="flex cursor-pointer items-center rounded-lg px-3 py-2.5 text-sm text-gray-700 transition-colors hover:bg-primary-50 dark:text-gray-200 dark:hover:bg-primary-900/20"
|
||||
onClick={() => navigate("/change-password")}
|
||||
>
|
||||
<Key className="mr-2.5 h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
<span>{t("header.changePassword")}</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{showRecordManagementShortcut && (
|
||||
<DropdownMenuItem
|
||||
className="flex cursor-pointer items-center rounded-lg px-3 py-2.5 text-sm text-gray-700 transition-colors hover:bg-primary-50 dark:text-gray-200 dark:hover:bg-primary-900/20"
|
||||
onClick={() => navigate("/profile")}
|
||||
onClick={() => navigate("/record-management/dashboard")}
|
||||
>
|
||||
<User className="mr-2.5 h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
<span>{t("header.viewProfile")}</span>
|
||||
<FileText className="mr-2.5 h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
<span>{t("nav.Record Management")}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{showUserManagementShortcut && (
|
||||
<DropdownMenuItem
|
||||
className="flex cursor-pointer items-center rounded-lg px-3 py-2.5 text-sm text-gray-700 transition-colors hover:bg-primary-50 dark:text-gray-200 dark:hover:bg-primary-900/20"
|
||||
onClick={() => navigate("/update-profile")}
|
||||
onClick={() => navigate("/user-management")}
|
||||
>
|
||||
<UserPen className="mr-2.5 h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
<span>{t("header.editProfile")}</span>
|
||||
<UsersRound className="mr-2.5 h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
<span>
|
||||
{t("dashboard.userManagement", "User Management")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem
|
||||
className="flex cursor-pointer items-center rounded-lg px-3 py-2.5 text-sm text-gray-700 transition-colors hover:bg-primary-50 dark:text-gray-200 dark:hover:bg-primary-900/20"
|
||||
onClick={() => navigate("/change-password")}
|
||||
>
|
||||
<Key className="mr-2.5 h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
<span>{t("header.changePassword")}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="my-1 h-px bg-gray-200 dark:bg-gray-700" />
|
||||
|
||||
{showRecordManagementShortcut && (
|
||||
<DropdownMenuItem
|
||||
className="flex cursor-pointer items-center rounded-lg px-3 py-2.5 text-sm text-gray-700 transition-colors hover:bg-primary-50 dark:text-gray-200 dark:hover:bg-primary-900/20"
|
||||
onClick={() => navigate("/record-management/dashboard")}
|
||||
>
|
||||
<FileText className="mr-2.5 h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
<span>{t("nav.Record Management")}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{showUserManagementShortcut && (
|
||||
<DropdownMenuItem
|
||||
className="flex cursor-pointer items-center rounded-lg px-3 py-2.5 text-sm text-gray-700 transition-colors hover:bg-primary-50 dark:text-gray-200 dark:hover:bg-primary-900/20"
|
||||
onClick={() => navigate("/user-management")}
|
||||
>
|
||||
<UsersRound className="mr-2.5 h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
<span>
|
||||
{t("dashboard.userManagement", "User Management")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuSeparator className="my-1 h-px bg-gray-200 dark:bg-gray-700" />
|
||||
|
||||
<DropdownMenuItem
|
||||
className="flex cursor-pointer items-center rounded-lg px-3 py-2.5 text-sm text-red-600 transition-colors hover:bg-red-50 dark:hover:bg-red-950/30"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
<LogOut className="mr-2.5 h-4 w-4" />
|
||||
<span>{t("header.signOut")}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<DropdownMenuItem
|
||||
className="flex cursor-pointer items-center rounded-lg px-3 py-2.5 text-sm text-red-600 transition-colors hover:bg-red-50 dark:hover:bg-red-950/30"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
<LogOut className="mr-2.5 h-4 w-4" />
|
||||
<span>{t("header.signOut")}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import {
|
||||
Archive,
|
||||
BarChart,
|
||||
@@ -8,28 +8,48 @@ import {
|
||||
FileText,
|
||||
Globe,
|
||||
Settings,
|
||||
ShieldAlert,
|
||||
Users2,
|
||||
UsersRound,
|
||||
} from "lucide-react";
|
||||
import { useAuth } from "@/shared/context/AuthContext";
|
||||
import { usePermissions } from "@/shared/context/PermissionContext";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useAuth } from "@/shared/context/AuthContext";
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/shared/common/ui/sidebar";
|
||||
|
||||
export interface MenuItem {
|
||||
label: string;
|
||||
href: string;
|
||||
icon: React.ReactNode;
|
||||
roles?: string[];
|
||||
permissions?: string[];
|
||||
isPrimary?: boolean;
|
||||
displayLabel?: string;
|
||||
children?: MenuItem[];
|
||||
/** Sidebar section this item is bucketed under. */
|
||||
group: string;
|
||||
}
|
||||
|
||||
// Section render order; groups with no role-visible items are skipped.
|
||||
const GROUP_ORDER = [
|
||||
"Overview",
|
||||
"Organizations",
|
||||
"Content",
|
||||
"Records",
|
||||
"Configuration",
|
||||
"Archive",
|
||||
"System",
|
||||
];
|
||||
|
||||
export const AppMenuTabs = () => {
|
||||
const { user } = useAuth();
|
||||
const { pathname } = useLocation();
|
||||
const { setOpenMobile } = useSidebar();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const userRoles = user?.roles.map((role) => role.key) || [];
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
@@ -38,282 +58,193 @@ export const AppMenuTabs = () => {
|
||||
href: "/user-management/dashboard",
|
||||
icon: <BarChart className="h-4 w-4" />,
|
||||
roles: ["super_admin"],
|
||||
isPrimary: true,
|
||||
group: "Overview",
|
||||
},
|
||||
{
|
||||
label: "organizations",
|
||||
href: "/user-management/organizations",
|
||||
icon: <Building2 className="h-4 w-4" />,
|
||||
roles: ["super_admin"],
|
||||
isPrimary: true,
|
||||
group: "Organizations",
|
||||
},
|
||||
|
||||
{
|
||||
label: "organizationAdmins", // Shortened for mobile
|
||||
displayLabel: "organizationAdmins", // Full label for desktop
|
||||
label: "organizationAdmins",
|
||||
href: "/user-management/organization_admins",
|
||||
icon: <Users2 className="h-4 w-4" />,
|
||||
roles: ["super_admin"],
|
||||
isPrimary: true,
|
||||
group: "Organizations",
|
||||
},
|
||||
{
|
||||
label: "externalUsers", // Shortened for mobile
|
||||
displayLabel: "externalUsers", // Full label for desktop
|
||||
label: "externalUsers",
|
||||
href: "/user-management/external_users",
|
||||
icon: <Users2 className="h-4 w-4" />,
|
||||
roles: ["super_admin"],
|
||||
isPrimary: true,
|
||||
group: "Organizations",
|
||||
},
|
||||
{
|
||||
label: "dashboard",
|
||||
href: "/user-management/user_management-dashboard",
|
||||
icon: <BarChart className="h-4 w-4" />,
|
||||
roles: ["admin", "organization_admin", "unit_admin"],
|
||||
isPrimary: true,
|
||||
group: "Overview",
|
||||
},
|
||||
{
|
||||
label: "userManagement",
|
||||
href: "/user-management/user_management",
|
||||
icon: <UsersRound className="h-4 w-4" />,
|
||||
roles: ["admin", "organization_admin", "unit_admin"],
|
||||
isPrimary: true,
|
||||
group: "Overview",
|
||||
},
|
||||
// {
|
||||
// label: "userPositionApproval",
|
||||
// href: "/user-management/user-position-approval",
|
||||
// icon: <UsersRound className="h-4 w-4" />,
|
||||
// roles: ["admin", "organization_admin", "unit_admin"],
|
||||
// permissions: ["can:activateEmployee"],
|
||||
// isPrimary: true,
|
||||
// },
|
||||
// {
|
||||
// label: "All Records",
|
||||
// displayLabel: "All Records",
|
||||
// href: "/user-management/all-records",
|
||||
// icon: <FileText className="h-4 w-4" />,
|
||||
// roles: ["admin", "organization_admin", "unit_admin"],
|
||||
// permissions: ["can:canViewAllRecords"],
|
||||
// isPrimary: true,
|
||||
// },
|
||||
{
|
||||
label: "contentManagement", // Shortened for mobile
|
||||
displayLabel: "contentManagement", // Full label for desktop
|
||||
label: "contentManagement",
|
||||
href: "/user-management/content-management",
|
||||
icon: <FileText className="h-4 w-4" />,
|
||||
roles: ["admin", "organization_admin", "unit_admin"],
|
||||
isPrimary: true,
|
||||
group: "Content",
|
||||
},
|
||||
{
|
||||
label: "webManagement",
|
||||
displayLabel: "webManagement",
|
||||
href: "/user-management/web-management",
|
||||
icon: <Globe className="h-4 w-4" />,
|
||||
roles: ["admin", "organization_admin", "unit_admin"],
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
label: "Position",
|
||||
displayLabel: "positionTypes",
|
||||
href: "/user-management/position-management",
|
||||
icon: <FileText className="h-4 w-4" />,
|
||||
roles: ["admin", "organization_admin", "unit_admin", "super_admin"],
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
label: "migratedRecords",
|
||||
displayLabel: "migratedRecords",
|
||||
href: "/user-management/migrated-records-management",
|
||||
icon: <FileText className="h-4 w-4" />,
|
||||
roles: ["super_admin"],
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
label: "settings",
|
||||
displayLabel: "settings",
|
||||
href: "/user-management/organization-settings",
|
||||
icon: <Settings className="h-4 w-4" />,
|
||||
roles: ["admin", "organization_admin", "unit_admin"],
|
||||
isPrimary: true,
|
||||
group: "Content",
|
||||
},
|
||||
{
|
||||
label: "Bulk",
|
||||
displayLabel: "bulkUpload",
|
||||
href: "/user-management/bulk-upload",
|
||||
icon: <FileText className="h-4 w-4" />,
|
||||
roles: ["admin", "organization_admin", "unit_admin"],
|
||||
isPrimary: true,
|
||||
group: "Content",
|
||||
},
|
||||
{
|
||||
label: "Archive Users",
|
||||
displayLabel: "Archive Users",
|
||||
href: "/user-management/archive-users",
|
||||
icon: <Archive className="h-4 w-4" />,
|
||||
roles: ["super_admin"],
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
label: "Archived Organizations",
|
||||
displayLabel: "Archived Organizations",
|
||||
href: "/user-management/archived-organizations",
|
||||
icon: <Archive className="h-4 w-4" />,
|
||||
roles: ["super_admin"],
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
label: "Archive Users",
|
||||
displayLabel: "Archive Users",
|
||||
href: "/user-management/archives",
|
||||
icon: <Archive className="h-4 w-4" />,
|
||||
roles: ["admin", "organization_admin", "unit_admin"],
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
label: "Archived Units & Positions",
|
||||
displayLabel: "Archived Units & Positions",
|
||||
href: "/user-management/archived",
|
||||
icon: <Archive className="h-4 w-4" />,
|
||||
roles: ["admin", "organization_admin", "unit_admin"],
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
label: "Sector Reports",
|
||||
displayLabel: "Sector Reports",
|
||||
href: "/user-management/sector-reports",
|
||||
icon: <ChartAreaIcon className="h-5 w-5" />,
|
||||
roles: ["unit_admin", "admin", "organization_admin"],
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
label: "activityLog",
|
||||
href: "/user-management/activity_log",
|
||||
icon: <ClipboardList className="h-4 w-4" />,
|
||||
roles: ["super_admin"],
|
||||
isPrimary: false,
|
||||
},
|
||||
{
|
||||
label: "setting",
|
||||
href: "/user-management/settings",
|
||||
icon: <Settings className="h-4 w-4" />,
|
||||
roles: ["super_admin"],
|
||||
isPrimary: false,
|
||||
},
|
||||
{
|
||||
label: "Letter Template",
|
||||
href: "/user-management/templates",
|
||||
label: "Position",
|
||||
href: "/user-management/position-management",
|
||||
icon: <FileText className="h-4 w-4" />,
|
||||
roles: ["super_admin"],
|
||||
isPrimary: false,
|
||||
roles: ["admin", "organization_admin", "unit_admin", "super_admin"],
|
||||
group: "Configuration",
|
||||
},
|
||||
{
|
||||
label: "settings",
|
||||
href: "/user-management/organization-settings",
|
||||
icon: <Settings className="h-4 w-4" />,
|
||||
roles: ["admin", "organization_admin", "unit_admin"],
|
||||
group: "Configuration",
|
||||
},
|
||||
{
|
||||
label: "Add Site",
|
||||
href: "/user-management/add-site",
|
||||
icon: <Globe className="h-4 w-4" />,
|
||||
roles: ["super_admin"],
|
||||
isPrimary: true,
|
||||
group: "Configuration",
|
||||
},
|
||||
{
|
||||
label: "migratedRecords",
|
||||
href: "/user-management/migrated-records-management",
|
||||
icon: <FileText className="h-4 w-4" />,
|
||||
roles: ["super_admin"],
|
||||
group: "Records",
|
||||
},
|
||||
{
|
||||
label: "Sector Reports",
|
||||
href: "/user-management/sector-reports",
|
||||
icon: <ChartAreaIcon className="h-4 w-4" />,
|
||||
roles: ["unit_admin", "admin", "organization_admin"],
|
||||
group: "Records",
|
||||
},
|
||||
{
|
||||
label: "Archive Users",
|
||||
href: "/user-management/archive-users",
|
||||
icon: <Archive className="h-4 w-4" />,
|
||||
roles: ["super_admin"],
|
||||
group: "Archive",
|
||||
},
|
||||
{
|
||||
label: "Archived Organizations",
|
||||
href: "/user-management/archived-organizations",
|
||||
icon: <Archive className="h-4 w-4" />,
|
||||
roles: ["super_admin"],
|
||||
group: "Archive",
|
||||
},
|
||||
{
|
||||
label: "Archive Users",
|
||||
href: "/user-management/archives",
|
||||
icon: <Archive className="h-4 w-4" />,
|
||||
roles: ["admin", "organization_admin", "unit_admin"],
|
||||
group: "Archive",
|
||||
},
|
||||
{
|
||||
label: "Archived Units & Positions",
|
||||
href: "/user-management/archived",
|
||||
icon: <Archive className="h-4 w-4" />,
|
||||
roles: ["admin", "organization_admin", "unit_admin"],
|
||||
group: "Archive",
|
||||
},
|
||||
{
|
||||
label: "activityLog",
|
||||
href: "/user-management/activity_log",
|
||||
icon: <ClipboardList className="h-4 w-4" />,
|
||||
roles: ["super_admin"],
|
||||
group: "System",
|
||||
},
|
||||
{
|
||||
label: "setting",
|
||||
href: "/user-management/settings",
|
||||
icon: <Settings className="h-4 w-4" />,
|
||||
roles: ["super_admin"],
|
||||
group: "System",
|
||||
},
|
||||
{
|
||||
label: "Letter Template",
|
||||
href: "/user-management/templates",
|
||||
icon: <FileText className="h-4 w-4" />,
|
||||
roles: ["super_admin"],
|
||||
group: "System",
|
||||
},
|
||||
|
||||
// {
|
||||
// label: "branding.title",
|
||||
// href: "/user-management/web-management/Branding/Branding",
|
||||
// icon: <ShieldAlert className="h-4 w-4" />,
|
||||
// roles: ["super_admin"],
|
||||
// isPrimary: true,
|
||||
// },
|
||||
];
|
||||
|
||||
const { permissions } = usePermissions();
|
||||
const { t } = useTranslation();
|
||||
const filteredMenu = menuItems.filter((item) =>
|
||||
item?.roles?.some((r) => userRoles.includes(r)),
|
||||
);
|
||||
const primaryMenuItems = filteredMenu.filter(
|
||||
(item) => item.isPrimary !== false,
|
||||
);
|
||||
const secondaryMenuItems = filteredMenu.filter(
|
||||
(item) => item.isPrimary === false,
|
||||
item.roles?.some((r) => userRoles.includes(r)),
|
||||
);
|
||||
|
||||
const isActive = (href: string) =>
|
||||
pathname === href || pathname.startsWith(`${href}/`);
|
||||
|
||||
return (
|
||||
// Sticky (not fixed) so it stays in flow: content below never needs a
|
||||
// magic offset matching this bar's responsive height. top-16 keeps it
|
||||
// pinned just below the fixed 64px <Top> header while scrolling.
|
||||
// -mt-8 cancels the excess of <Top>'s in-flow h-24 wrapper over its 64px
|
||||
// fixed header, so the bar sits flush under the header with no jump.
|
||||
// shrink-0 is load-bearing: as a flex item with overflow-hidden this bar
|
||||
// would otherwise be flex-squashed to zero height when the page overflows.
|
||||
<div className="sticky top-16 -mt-8 z-30 shrink-0 bg-white dark:bg-gray-900 border-b dark:border-gray-800 overflow-hidden flex flex-col">
|
||||
{/* Mobile View - Two separate rows */}
|
||||
<div className="md:hidden flex flex-col pt-5 pb-3 px-4 space-y-3 mt-2">
|
||||
{/* Primary items row */}
|
||||
<div className="flex items-center overflow-x-auto scrollbar-none">
|
||||
{primaryMenuItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.href}
|
||||
to={item.href}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-1 px-3 py-2.5 mr-2 text-xs font-medium transition-colors whitespace-nowrap ${
|
||||
isActive
|
||||
? "text-primary dark:text-primary-400 border-b-2 border-primary dark:border-primary-400"
|
||||
: "text-slate-700 dark:text-gray-300 hover:text-primary dark:hover:text-primary-400"
|
||||
}`
|
||||
}
|
||||
>
|
||||
{item.icon}
|
||||
<span className="max-w-[80px] truncate">
|
||||
{t(`organization.${item.label}`, item.label)}
|
||||
</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
<>
|
||||
{GROUP_ORDER.map((group) => {
|
||||
const items = filteredMenu.filter((item) => item.group === group);
|
||||
if (items.length === 0) return null;
|
||||
|
||||
{/* Secondary items row (if any) */}
|
||||
{secondaryMenuItems.length > 0 && (
|
||||
<div className="flex items-center overflow-x-auto scrollbar-none border-t dark:border-gray-800 pt-3">
|
||||
{secondaryMenuItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.href}
|
||||
to={item.href}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-1 px-3 py-2.5 mr-2 text-xs font-medium transition-colors whitespace-nowrap ${
|
||||
isActive
|
||||
? "text-primary dark:text-primary-400 border-b-2 border-primary dark:border-primary-400"
|
||||
: "text-slate-700 dark:text-gray-300 hover:text-primary dark:hover:text-primary-400"
|
||||
}`
|
||||
}
|
||||
>
|
||||
{item.icon}
|
||||
<span className="max-w-[80px] truncate">
|
||||
{t(`organization.${item.label}`, item.label)}
|
||||
</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Desktop View - Single row with all items */}
|
||||
<div className="hidden md:flex pt-5 pb-3 px-8 mt-1">
|
||||
<div className="flex items-center gap-2 overflow-x-auto scrollbar-none">
|
||||
{filteredMenu.map((item) => (
|
||||
<NavLink
|
||||
key={item.href}
|
||||
to={item.href}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors whitespace-nowrap ${
|
||||
isActive
|
||||
? "text-primary dark:text-primary-400 border-b-2 border-primary dark:border-primary-400"
|
||||
: "text-slate-700 dark:text-gray-300 hover:text-primary dark:hover:text-primary-400"
|
||||
}`
|
||||
}
|
||||
>
|
||||
{item.icon}
|
||||
<span className="max-w-full truncate">
|
||||
{t(`organization.${item.label}`, item.label)}
|
||||
</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<SidebarGroup key={group} className="pb-0">
|
||||
<SidebarGroupLabel>{group}</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => {
|
||||
const label = t(`organization.${item.label}`, item.label);
|
||||
return (
|
||||
<SidebarMenuItem key={item.href}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={isActive(item.href)}
|
||||
tooltip={label}
|
||||
>
|
||||
<Link
|
||||
to={item.href}
|
||||
onClick={() => setOpenMobile(false)}
|
||||
>
|
||||
{item.icon}
|
||||
<span>{label}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
})}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,43 +1,92 @@
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { AppMenuTabs } from "./AppMenuTabs";
|
||||
import { useSidebar } from "@/shared/common/ui/sidebar";
|
||||
import Top from "@/record-management/components/common/Top";
|
||||
import { useAuth } from "@/shared/context/AuthContext";
|
||||
|
||||
export const AppLayout = () => {
|
||||
const { pathname } = useLocation();
|
||||
const isAuthPage = pathname === "/";
|
||||
const { toggleSidebar } = useSidebar();
|
||||
const { user } = useAuth();
|
||||
|
||||
const userRoles = user?.roles?.map((role) => role.key) || [];
|
||||
const isSuperAdmin = userRoles.includes("super_admin");
|
||||
|
||||
// html/body/#root are `overflow: hidden` (index.css) — the host chrome
|
||||
// scrolls inside FreightDashboardLayout. This subtree renders its own
|
||||
// full-page layout instead, so it must be its own scroll container or
|
||||
// nothing scrolls. Top/AppMenuTabs are `fixed`, unaffected by the scroller.
|
||||
if (isAuthPage) {
|
||||
return (
|
||||
<div className="h-dvh w-full overflow-y-auto bg-gray-50">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// <Top> renders its own in-flow h-24 wrapper around the fixed 64px header,
|
||||
// so flow already clears the header — no extra top padding here.
|
||||
// AppMenuTabs is sticky and in flow, so content starts right below it.
|
||||
<div className="w-full h-dvh overflow-y-auto flex flex-col bg-background text-foreground">
|
||||
<Top onToggleSidebar={toggleSidebar} showRecordManagementShortcut={!isSuperAdmin} />
|
||||
<AppMenuTabs />
|
||||
<div className="px-2 pb-2 pt-4 sm:px-4 sm:pb-4 sm:pt-6 flex-1">
|
||||
<div className="w-full overflow-x-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import { Link, Outlet, useLocation } from "react-router-dom";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { AppMenuTabs } from "./AppMenuTabs";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarHeader,
|
||||
SidebarInset,
|
||||
SidebarRail,
|
||||
useSidebar,
|
||||
} from "@/shared/common/ui/sidebar";
|
||||
import Top from "@/record-management/components/common/Top";
|
||||
import { useAuth } from "@/shared/context/AuthContext";
|
||||
|
||||
export const AppLayout = () => {
|
||||
const { pathname } = useLocation();
|
||||
const isAuthPage = pathname === "/";
|
||||
const { toggleSidebar } = useSidebar();
|
||||
const { user } = useAuth();
|
||||
|
||||
const userRoles = user?.roles?.map((role) => role.key) || [];
|
||||
const isSuperAdmin = userRoles.includes("super_admin");
|
||||
|
||||
// Standalone full-page scroll container for the auth screen — the host
|
||||
// chrome is `overflow: hidden`, so this subtree must scroll itself.
|
||||
if (isAuthPage) {
|
||||
return (
|
||||
<div className="h-dvh w-full overflow-y-auto bg-gray-50">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Full-height sidebar owning the left column (back-to-home + brand at the
|
||||
top). <Top> lives inside <SidebarInset>, to the right of the sidebar,
|
||||
and is `sticky` — so it never overlaps the sidebar and re-flows when
|
||||
the sidebar collapses to its icon rail. Top's burger (onToggleSidebar)
|
||||
drives collapse on desktop and the Sheet drawer on mobile. */}
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader className="gap-1 border-b border-sidebar-border">
|
||||
<Link
|
||||
to="/dashboard/overview"
|
||||
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||
>
|
||||
<ArrowLeft className="size-4 shrink-0" />
|
||||
<span className="group-data-[collapsible=icon]:hidden">
|
||||
Back to home
|
||||
</span>
|
||||
</Link>
|
||||
<Link
|
||||
to="/user-management"
|
||||
className="flex items-center gap-2 px-1 py-2.5"
|
||||
>
|
||||
<img
|
||||
src="/assets/logo.svg"
|
||||
alt="EDR"
|
||||
className="size-7 shrink-0 object-contain"
|
||||
/>
|
||||
<div className="flex flex-col group-data-[collapsible=icon]:hidden">
|
||||
<span className="text-sm font-semibold leading-tight text-sidebar-foreground">
|
||||
User Management
|
||||
</span>
|
||||
<span className="text-[10px] font-medium text-muted-foreground">
|
||||
EDR Freight
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</SidebarHeader>
|
||||
<SidebarContent className="pb-10">
|
||||
<AppMenuTabs />
|
||||
</SidebarContent>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
|
||||
{/* Internal scroll container: the sidebar stays fixed and full-height
|
||||
while this column scrolls beneath the sticky <Top> bar. */}
|
||||
<SidebarInset className="h-svh min-w-0 overflow-y-auto">
|
||||
<Top
|
||||
onToggleSidebar={toggleSidebar}
|
||||
showRecordManagementShortcut={!isSuperAdmin}
|
||||
/>
|
||||
<div className="flex-1 pb-2 sm:px-4 sm:pb-4 ">
|
||||
<div className="w-full overflow-x-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -210,7 +210,7 @@ export default function PositionManagement() {
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<Card className="col-span-2 shadow-none border-none bg-transparent px-0">
|
||||
<Card className="py-0 col-span-2 shadow-none border-none bg-transparent px-0">
|
||||
<CardHeader className="flex flex-row justify-between items-center px-0">
|
||||
<CardTitle className="text-xl font-semibold ">
|
||||
{t("contentManagement.permissionType")}
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import PositionManagement from "@/user-management/components/position-management/PositionLists";
|
||||
import { t } from "i18next";
|
||||
|
||||
const PositionManagementPage = () => {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h2 className="text-2xl font-bold mb-4">{t("contentManagement.permissionManagement")}</h2>
|
||||
<PositionManagement />
|
||||
</div>
|
||||
);
|
||||
return <PositionManagement />;
|
||||
};
|
||||
|
||||
export default PositionManagementPage;
|
||||
|
||||
Reference in New Issue
Block a user