mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 10:10:57 +00:00
fix: sidebar in user management
This commit is contained in:
@@ -626,7 +626,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) {
|
||||
@@ -642,18 +644,6 @@ const filterSidebarByPermission = (
|
||||
return true;
|
||||
});
|
||||
|
||||
// Recursive: a group's own permission gates the whole subtree, leaves are
|
||||
// checked individually, and a group with no surviving children disappears.
|
||||
const filterItems = (items: SidebarItem[]): SidebarItem[] =>
|
||||
items.flatMap((item) => {
|
||||
if (item.children?.length) {
|
||||
if (item.permission && !permissionAllowed(item)) return [];
|
||||
const children = filterItems(item.children);
|
||||
return children.length ? [{ ...item, children }] : [];
|
||||
}
|
||||
return itemAllowed(item) ? [item] : [];
|
||||
});
|
||||
|
||||
return sections
|
||||
.map((section) => ({
|
||||
...section,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -104,9 +104,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 +114,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 +123,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 +159,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,8 +256,8 @@ 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-14 flex-col">
|
||||
<header className="fixed left-0 right-0 top-0 z-40 h-14 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">
|
||||
<DropdownMenu>
|
||||
@@ -394,7 +394,7 @@ const Top: React.FC<HeaderProps> = ({
|
||||
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",
|
||||
"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={() =>
|
||||
@@ -409,7 +409,7 @@ const Top: React.FC<HeaderProps> = ({
|
||||
className={cn(
|
||||
"h-4 w-4 transition-colors",
|
||||
openNotifications &&
|
||||
"fill-primary-100 text-primary-700 dark:fill-primary-900/40 dark:text-primary-300",
|
||||
"fill-primary-100 text-primary-700 dark:fill-primary-900/40 dark:text-primary-300",
|
||||
)}
|
||||
/>
|
||||
{unseenCount > 0 && (
|
||||
@@ -479,7 +479,7 @@ const Top: React.FC<HeaderProps> = ({
|
||||
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",
|
||||
"bg-primary-100 text-primary-800 dark:bg-primary-800/50 dark:text-white",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -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,23 +1,26 @@
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { Link, Outlet, useLocation } from "react-router-dom";
|
||||
import { AppMenuTabs } from "./AppMenuTabs";
|
||||
import { useSidebar } from "@/shared/common/ui/sidebar";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarHeader,
|
||||
SidebarInset,
|
||||
SidebarRail,
|
||||
SidebarTrigger,
|
||||
} 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.
|
||||
|
||||
// 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">
|
||||
@@ -27,17 +30,55 @@ export const AppLayout = () => {
|
||||
}
|
||||
|
||||
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 />
|
||||
<>
|
||||
{/* <Top> is a shared component rendered as a full-width `fixed` 64px bar,
|
||||
so the sidebar is offset to start beneath it (top-16) rather than
|
||||
owning the top-left corner. Collapses to an icon rail on desktop; on
|
||||
mobile it's a Sheet drawer opened by the <SidebarTrigger> below (Top's
|
||||
own burger is a module-nav dropdown, not the sidebar toggle). */}
|
||||
<Sidebar collapsible="icon" className="top-14! h-[calc(100svh-4rem)]!">
|
||||
<SidebarHeader className="border-b border-sidebar-border">
|
||||
<Link
|
||||
to="/user-management"
|
||||
className="flex items-center gap-2 px-1 py-1.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>
|
||||
|
||||
<SidebarInset className="min-w-0">
|
||||
<Top showRecordManagementShortcut={!isSuperAdmin} />
|
||||
{/* Top provides its own in-flow h-24 spacer clearing the fixed header. */}
|
||||
{/* Mobile-only drawer opener — desktop shows the sidebar/rail directly. */}
|
||||
<div className="flex items-center gap-2 px-3 md:hidden">
|
||||
<SidebarTrigger className="h-9 w-9 rounded-lg" />
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
Menu
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 pb-2 sm:px-4 sm:pb-4 ">
|
||||
<div className="w-full overflow-x-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user