This commit is contained in:
natib21
2026-07-10 11:25:59 +00:00
parent 2696ca7498
commit e6e44e773b
1146 changed files with 200266 additions and 90527 deletions

View File

@@ -1,48 +1,48 @@
import { FooterDropdownMenu } from "./navigationtabs";
import { ChevronUp, User2 } from "lucide-react";
const recordOfficerFooters: FooterDropdownMenu | null = {
label: "Settings",
triggerIcon: ChevronUp,
items: [
{
label: "Settings",
href: "#/officersettings",
},
{
label: "Letter Templates",
href: "#/letterTemplates",
},
],
};
const userFooters = (permissions: string[]): FooterDropdownMenu => {
const dropDown: FooterDropdownMenu = {
icon: User2,
label: "Settings",
triggerIcon: ChevronUp,
items: [
{
label: "Sign out",
action: () => {},
},
{ label: "Delegation", href: "#/delegation" },
],
};
if (permissions.includes("")) {
dropDown.items.push({
label: "Upload Teeter and Signature",
href: "#/uploadTeeterandSignature",
});
}
return dropDown;
};
export const footerItems = (
permissions: string[]
): FooterDropdownMenu | null => {
if (permissions.includes("can:dispatchRecords")) {
return recordOfficerFooters;
} else {
return userFooters(permissions);
}
};
import { FooterDropdownMenu } from "./navigationtabs";
import { ChevronUp, User2 } from "lucide-react";
const recordOfficerFooters: FooterDropdownMenu | null = {
label: "Settings",
triggerIcon: ChevronUp,
items: [
{
label: "Settings",
href: "#/officersettings",
},
{
label: "Letter Templates",
href: "#/letterTemplates",
},
],
};
const userFooters = (permissions: string[]): FooterDropdownMenu => {
const dropDown: FooterDropdownMenu = {
icon: User2,
label: "Settings",
triggerIcon: ChevronUp,
items: [
{
label: "Sign out",
action: () => {},
},
{ label: "Delegation", href: "#/delegation" },
],
};
if (permissions.includes("")) {
dropDown.items.push({
label: "Upload Teeter and Signature",
href: "#/uploadTeeterandSignature",
});
}
return dropDown;
};
export const footerItems = (
permissions: string[]
): FooterDropdownMenu | null => {
if (permissions.includes("can:dispatchRecords")) {
return recordOfficerFooters;
} else {
return userFooters(permissions);
}
};

View File

@@ -1,121 +1,121 @@
import {
Download,
//Calendar,
FileText,
//Home,
//Inbox,
LayoutGrid,
//Search,
SquareCheck,
Upload,
//Settings,
Users,
} from "lucide-react";
import { NavOptions } from "./navigationtabs";
import { useTranslation } from "react-i18next";
import { t } from "i18next";
// Management section items
export const managementItems: NavOptions[] = [
{
title: t("Record Management") || "Record Management",
url: "/record-management/userRecords",
icon: FileText,
isActive: true,
},
{
title: t("User Management") || "User Management",
url: "/user-management",
icon: Users,
perms: ["view:user-management", "manage:users"],
roles: ["super_admin", "admin", "unit_admin"],
},
// {
// title: t("Workflow Management"),
// url: "workflowManagement",
// icon: GitBranch,
// },
// {
// title: t("Project Management"),
// url: "projectManagement",
// icon: Briefcase,
// },
// {
// title: t("Document Management"),
// url: "documentManagement",
// icon: FileArchive,
// },
// {
// title: t("Knowledge & Asset Management"),
// url: "knowledgeManagement",
// icon: BookOpen,
// },
// {
// title: t("Report Management"),
// url: "reportManagement",
// icon: BarChart3,
// },
];
export const useNavItems = (permissions: string[]): NavOptions[] => {
const { t } = useTranslation();
// Main navigation items based on the image
const mainNavItems: NavOptions[] = [
{
title: t("nav.dashboard"),
url: "dashboard",
isActive: true,
icon: LayoutGrid,
},
];
const userNavs: NavOptions[] = [
{
title: t("nav.dashboard"),
url: "dashboard",
isActive: true,
icon: LayoutGrid,
},
{
title: t("nav.outgoing"),
url: "userRecords",
icon: Upload,
},
{
title: t("nav.incoming"),
url: "userIncoming",
icon: Download,
count: true,
items: [
{
title: t("nav.external"),
url: "userIncoming",
count: true,
},
{
title: t("nav.internal"),
url: "userIncoming/internal",
count: true,
},
{
title: t("nav.cc"),
url: "userIncoming/cc",
count: true,
},
],
},
];
const approvalPermissionKeys = ["can:sendToRO", "can:approveRecords"];
if (approvalPermissionKeys.some((key) => permissions.includes(key))) {
userNavs.push({
title: t("nav.approval"),
url: "approval",
icon: SquareCheck,
count: true,
items: [],
});
}
return [...mainNavItems, ...managementItems];
};
import {
Download,
//Calendar,
FileText,
//Home,
//Inbox,
LayoutGrid,
//Search,
SquareCheck,
Upload,
//Settings,
Users,
} from "lucide-react";
import { NavOptions } from "./navigationtabs";
import { useTranslation } from "react-i18next";
import { t } from "i18next";
// Management section items
export const managementItems: NavOptions[] = [
{
title: t("Record Management") || "Record Management",
url: "/record-management/userRecords",
icon: FileText,
isActive: true,
},
{
title: t("User Management") || "User Management",
url: "/user-management",
icon: Users,
perms: ["view:user-management", "manage:users"],
roles: ["super_admin", "admin", "unit_admin"],
},
// {
// title: t("Workflow Management"),
// url: "workflowManagement",
// icon: GitBranch,
// },
// {
// title: t("Project Management"),
// url: "projectManagement",
// icon: Briefcase,
// },
// {
// title: t("Document Management"),
// url: "documentManagement",
// icon: FileArchive,
// },
// {
// title: t("Knowledge & Asset Management"),
// url: "knowledgeManagement",
// icon: BookOpen,
// },
// {
// title: t("Report Management"),
// url: "reportManagement",
// icon: BarChart3,
// },
];
export const useNavItems = (permissions: string[]): NavOptions[] => {
const { t } = useTranslation();
// Main navigation items based on the image
const mainNavItems: NavOptions[] = [
{
title: t("nav.dashboard"),
url: "dashboard",
isActive: true,
icon: LayoutGrid,
},
];
const userNavs: NavOptions[] = [
{
title: t("nav.dashboard"),
url: "dashboard",
isActive: true,
icon: LayoutGrid,
},
{
title: t("nav.outgoing"),
url: "userRecords",
icon: Upload,
},
{
title: t("nav.incoming"),
url: "userIncoming",
icon: Download,
count: true,
items: [
{
title: t("nav.external"),
url: "userIncoming",
count: true,
},
{
title: t("nav.internal"),
url: "userIncoming/internal",
count: true,
},
{
title: t("nav.cc"),
url: "userIncoming/cc",
count: true,
},
],
},
];
const approvalPermissionKeys = ["can:sendToRO", "can:approveRecords"];
if (approvalPermissionKeys.some((key) => permissions.includes(key))) {
userNavs.push({
title: t("nav.approval"),
url: "approval",
icon: SquareCheck,
count: true,
items: [],
});
}
return [...mainNavItems, ...managementItems];
};

View File

@@ -1,262 +1,262 @@
import { ChevronRight, LucideIcon, LucideProps, Menu } from "lucide-react";
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarMenu,
SidebarMenuItem,
SidebarMenuButton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarFooter,
SidebarMenuBadge,
SidebarHeader,
useSidebar,
} from "@/shared/common/ui/sidebar";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/shared/common/ui/collapsible";
import { managementItems } from "./navigationItems";
import { NavLink, useLocation } from "react-router-dom";
import { useAuth } from "@/shared/context/AuthContext";
import { hasAccess } from "../utils/permissionHelper";
import { Button } from "@/shared/common/ui/button";
import { useTenantConfig } from "@/layout/components/TenantConfig";
// Routes nested under /userRecords/ (like editUserRecord/:id) trigger
// NavLink's auto-highlight on the Outgoing item even when the user arrived
// from Approval or Collaborations. `?from=...` is set by those entry points
// so the sidebar can ignore the auto-match and keep Outgoing inactive.
const isOutgoingNavSuppressedFor = (
pathname: string,
search: string,
): boolean => {
if (!pathname.includes("/userRecords/editUserRecord/")) return false;
const from = new URLSearchParams(search).get("from");
return from === "approval" || from === "collaborations";
};
export interface NavOptions {
title: string;
url: string;
isActive?: boolean;
icon?: React.ForwardRefExoticComponent<
Omit<LucideProps, "ref"> & React.RefAttributes<SVGSVGElement>
>;
items?: NavOptions[];
count?: any;
perms?: string[];
roles?: string[];
}
export interface FooterDropdownItem {
label: string;
href?: string;
action?: () => void;
}
export interface FooterDropdownMenu {
icon?: LucideIcon;
label: string;
triggerIcon?: LucideIcon;
items: FooterDropdownItem[];
}
export function NavigationTabs() {
const { user } = useAuth();
const userPermissions = user?.permissions.map((r) => r.key) || [];
const userRoles = user?.roles?.map((r) => r.key) || [];
const { toggleSidebar } = useSidebar();
const location = useLocation();
const { config: tenantConfig } = useTenantConfig();
const suppressOutgoingHighlight = isOutgoingNavSuppressedFor(
location.pathname,
location.search,
);
return (
<Sidebar collapsible="icon" className="border-r bg-white">
<SidebarHeader className="p-4 border-b">
<div className="flex items-center gap-3">
<Button
variant="ghost"
size="icon"
onClick={toggleSidebar}
className="p-1"
>
<Menu className="h-5 w-5" />
</Button>
<div className="w-8 h-8 bg-primary-500 rounded-md flex items-center justify-center">
<svg
width="20"
height="20"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 36H36"
stroke="#F8FAFC"
strokeWidth="1.8"
strokeLinecap="round"
/>
<path
d="M31.2 20.4H26.4C23.4216 20.4 22.8 21.0216 22.8 24V36H34.8V24C34.8 21.0216 34.1784 20.4 31.2 20.4Z"
stroke="#F8FAFC"
strokeWidth="1.8"
strokeLinejoin="round"
/>
<path
d="M27.6002 36H13.2002V15.6C13.2002 12.6216 13.8218 12 16.8002 12H24.0002C26.9786 12 27.6002 12.6216 27.6002 15.6V20.4"
stroke="#F8FAFC"
strokeWidth="1.8"
strokeLinejoin="round"
/>
<path
d="M13.2002 16.8H16.8002M13.2002 21.6H16.8002M13.2002 26.4H16.8002"
stroke="#F8FAFC"
strokeWidth="1.8"
strokeLinecap="round"
/>
<path
d="M27.6001 25.2H30.0001M27.6001 28.8H30.0001"
stroke="#F8FAFC"
strokeWidth="1.8"
strokeLinecap="round"
/>
<path
d="M28.8 36V32.4"
stroke="#F8FAFC"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<div>
<h3 className="font-semibold text-gray-800">{tenantConfig.appName}</h3>
<p className="text-xs text-gray-500">system</p>
</div>
</div>
</SidebarHeader>
<SidebarContent className="p-2">
<SidebarGroup>
<SidebarMenu>
{managementItems
.filter((item) => hasAccess(item, userPermissions, userRoles))
.map((item) => (
<Collapsible
key={item.title}
asChild
defaultOpen={
item.isActive && item.items ? item.items.length > 0 : false
}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton
tooltip={item.title}
className={
item.title?.includes("Management")
? "text-gray-600 hover:bg-gray-100 rounded-md my-1"
: "text-gray-800 hover:bg-gray-100 rounded-md my-1 font-medium"
}
>
{item.icon && (
<item.icon
className={
item.title?.includes("Management")
? "h-5 w-5 text-gray-500"
: "h-5 w-5 text-gray-700"
}
/>
)}
<SidebarMenuSubButton asChild>
<NavLink
to={item.url}
className={({ isActive }) => {
const effectiveActive =
isActive &&
!(
suppressOutgoingHighlight &&
item.url.endsWith("/userRecords")
);
return `flex items-center px-3 py-2 rounded-md text-sm transition-colors ${
effectiveActive
? item.url.startsWith("/user-management")
? "bg-primary-100 text-primary-900"
: "bg-primary text-primary-foreground"
: "text-gray-700 hover:bg-gray-100"
}`;
}}
>
<span>{item.title}</span>
</NavLink>
</SidebarMenuSubButton>
{item.count && (
<SidebarMenuBadge>{item.count}</SidebarMenuBadge>
)}
{item.items && item.items.length > 0 && (
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
)}
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
{item.items && item.items.length > 0 && (
<SidebarMenuSub>
{item.items
.filter((sItem) =>
hasAccess(sItem, userPermissions, userRoles)
)
.map((subItem: NavOptions) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<NavLink
to={subItem.url}
className={({ isActive }) => {
const effectiveActive =
isActive &&
!(
suppressOutgoingHighlight &&
(subItem.url === "userRecords" ||
subItem.url.endsWith("/userRecords"))
);
return `flex items-center px-3 py-2 rounded-md text-sm transition-colors ${
effectiveActive
? subItem.url.startsWith("/user-management")
? "bg-primary-100 text-primary-900"
: "bg-primary text-primary-foreground"
: "text-gray-700 hover:bg-gray-100"
}`;
}}
>
<span>{subItem.title}</span>
</NavLink>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
)}
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter className="p-4 border-t mt-auto">
<div className="text-xs text-gray-500 text-center">
Powered by <span className="font-semibold">TRIA</span>
</div>
</SidebarFooter>
</Sidebar>
);
}
import { ChevronRight, LucideIcon, LucideProps, Menu } from "lucide-react";
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarMenu,
SidebarMenuItem,
SidebarMenuButton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarFooter,
SidebarMenuBadge,
SidebarHeader,
useSidebar,
} from "@/shared/common/ui/sidebar";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/shared/common/ui/collapsible";
import { managementItems } from "./navigationItems";
import { NavLink, useLocation } from "react-router-dom";
import { useAuth } from "@/shared/context/AuthContext";
import { hasAccess } from "../utils/permissionHelper";
import { Button } from "@/shared/common/ui/button";
import { useTenantConfig } from "@/layout/components/TenantConfig";
// Routes nested under /userRecords/ (like editUserRecord/:id) trigger
// NavLink's auto-highlight on the Outgoing item even when the user arrived
// from Approval or Collaborations. `?from=...` is set by those entry points
// so the sidebar can ignore the auto-match and keep Outgoing inactive.
const isOutgoingNavSuppressedFor = (
pathname: string,
search: string,
): boolean => {
if (!pathname.includes("/userRecords/editUserRecord/")) return false;
const from = new URLSearchParams(search).get("from");
return from === "approval" || from === "collaborations";
};
export interface NavOptions {
title: string;
url: string;
isActive?: boolean;
icon?: React.ForwardRefExoticComponent<
Omit<LucideProps, "ref"> & React.RefAttributes<SVGSVGElement>
>;
items?: NavOptions[];
count?: any;
perms?: string[];
roles?: string[];
}
export interface FooterDropdownItem {
label: string;
href?: string;
action?: () => void;
}
export interface FooterDropdownMenu {
icon?: LucideIcon;
label: string;
triggerIcon?: LucideIcon;
items: FooterDropdownItem[];
}
export function NavigationTabs() {
const { user } = useAuth();
const userPermissions = user?.permissions.map((r) => r.key) || [];
const userRoles = user?.roles?.map((r) => r.key) || [];
const { toggleSidebar } = useSidebar();
const location = useLocation();
const { config: tenantConfig } = useTenantConfig();
const suppressOutgoingHighlight = isOutgoingNavSuppressedFor(
location.pathname,
location.search,
);
return (
<Sidebar collapsible="icon" className="border-r bg-white">
<SidebarHeader className="p-4 border-b">
<div className="flex items-center gap-3">
<Button
variant="ghost"
size="icon"
onClick={toggleSidebar}
className="p-1"
>
<Menu className="h-5 w-5" />
</Button>
<div className="w-8 h-8 bg-primary-500 rounded-md flex items-center justify-center">
<svg
width="20"
height="20"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 36H36"
stroke="#F8FAFC"
strokeWidth="1.8"
strokeLinecap="round"
/>
<path
d="M31.2 20.4H26.4C23.4216 20.4 22.8 21.0216 22.8 24V36H34.8V24C34.8 21.0216 34.1784 20.4 31.2 20.4Z"
stroke="#F8FAFC"
strokeWidth="1.8"
strokeLinejoin="round"
/>
<path
d="M27.6002 36H13.2002V15.6C13.2002 12.6216 13.8218 12 16.8002 12H24.0002C26.9786 12 27.6002 12.6216 27.6002 15.6V20.4"
stroke="#F8FAFC"
strokeWidth="1.8"
strokeLinejoin="round"
/>
<path
d="M13.2002 16.8H16.8002M13.2002 21.6H16.8002M13.2002 26.4H16.8002"
stroke="#F8FAFC"
strokeWidth="1.8"
strokeLinecap="round"
/>
<path
d="M27.6001 25.2H30.0001M27.6001 28.8H30.0001"
stroke="#F8FAFC"
strokeWidth="1.8"
strokeLinecap="round"
/>
<path
d="M28.8 36V32.4"
stroke="#F8FAFC"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<div>
<h3 className="font-semibold text-gray-800">{tenantConfig.appName}</h3>
<p className="text-xs text-gray-500">system</p>
</div>
</div>
</SidebarHeader>
<SidebarContent className="p-2">
<SidebarGroup>
<SidebarMenu>
{managementItems
.filter((item) => hasAccess(item, userPermissions, userRoles))
.map((item) => (
<Collapsible
key={item.title}
asChild
defaultOpen={
item.isActive && item.items ? item.items.length > 0 : false
}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton
tooltip={item.title}
className={
item.title?.includes("Management")
? "text-gray-600 hover:bg-gray-100 rounded-md my-1"
: "text-gray-800 hover:bg-gray-100 rounded-md my-1 font-medium"
}
>
{item.icon && (
<item.icon
className={
item.title?.includes("Management")
? "h-5 w-5 text-gray-500"
: "h-5 w-5 text-gray-700"
}
/>
)}
<SidebarMenuSubButton asChild>
<NavLink
to={item.url}
className={({ isActive }) => {
const effectiveActive =
isActive &&
!(
suppressOutgoingHighlight &&
item.url.endsWith("/userRecords")
);
return `flex items-center px-3 py-2 rounded-md text-sm transition-colors ${
effectiveActive
? item.url.startsWith("/user-management")
? "bg-primary-100 text-primary-900"
: "bg-primary text-primary-foreground"
: "text-gray-700 hover:bg-gray-100"
}`;
}}
>
<span>{item.title}</span>
</NavLink>
</SidebarMenuSubButton>
{item.count && (
<SidebarMenuBadge>{item.count}</SidebarMenuBadge>
)}
{item.items && item.items.length > 0 && (
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
)}
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
{item.items && item.items.length > 0 && (
<SidebarMenuSub>
{item.items
.filter((sItem) =>
hasAccess(sItem, userPermissions, userRoles)
)
.map((subItem: NavOptions) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<NavLink
to={subItem.url}
className={({ isActive }) => {
const effectiveActive =
isActive &&
!(
suppressOutgoingHighlight &&
(subItem.url === "userRecords" ||
subItem.url.endsWith("/userRecords"))
);
return `flex items-center px-3 py-2 rounded-md text-sm transition-colors ${
effectiveActive
? subItem.url.startsWith("/user-management")
? "bg-primary-100 text-primary-900"
: "bg-primary text-primary-foreground"
: "text-gray-700 hover:bg-gray-100"
}`;
}}
>
<span>{subItem.title}</span>
</NavLink>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
)}
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter className="p-4 border-t mt-auto">
<div className="text-xs text-gray-500 text-center">
Powered by <span className="font-semibold">TRIA</span>
</div>
</SidebarFooter>
</Sidebar>
);
}

View File

@@ -1,29 +1,29 @@
import { ChevronLeft } from "lucide-react";
import { cn } from "@/shared/lib/utils";
import { Button } from "@/shared/common/ui/button";
interface SidebarToggleProps {
isOpen: boolean | undefined;
setIsOpen?: () => void;
}
export function SidebarToggle({ isOpen, setIsOpen }: SidebarToggleProps) {
return (
<div className="invisible lg:visible absolute top-[12px] -right-[16px] z-20">
<Button
onClick={() => setIsOpen?.()}
className="rounded-md w-8 h-8"
variant="outline"
size="icon"
>
<ChevronLeft
className={cn(
"h-4 w-4 transition-transform ease-in-out duration-700",
isOpen === false ? "rotate-180" : "rotate-0"
)}
/>
</Button>
</div>
);
}
import { ChevronLeft } from "lucide-react";
import { cn } from "@/shared/lib/utils";
import { Button } from "@/shared/common/ui/button";
interface SidebarToggleProps {
isOpen: boolean | undefined;
setIsOpen?: () => void;
}
export function SidebarToggle({ isOpen, setIsOpen }: SidebarToggleProps) {
return (
<div className="invisible lg:visible absolute top-[12px] -right-[16px] z-20">
<Button
onClick={() => setIsOpen?.()}
className="rounded-md w-8 h-8"
variant="outline"
size="icon"
>
<ChevronLeft
className={cn(
"h-4 w-4 transition-transform ease-in-out duration-700",
isOpen === false ? "rotate-180" : "rotate-0"
)}
/>
</Button>
</div>
);
}