mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 15:48:11 +00:00
164 lines
5.8 KiB
TypeScript
164 lines
5.8 KiB
TypeScript
import { useMemo } from "react";
|
|
import { Link, useLocation } from "react-router-dom";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Breadcrumb, BreadcrumbList } from "@/shared/common/ui/breadcrumb";
|
|
import { ChevronRight, Home } from "lucide-react";
|
|
import { capitalize } from "@/shared/lib/utils";
|
|
|
|
interface BreadcrumbNode {
|
|
label: string;
|
|
path: string;
|
|
isActive: boolean;
|
|
isEllipsis?: boolean;
|
|
}
|
|
|
|
const MODULE_LANDING_PATHS = new Set([
|
|
"/record-management/dashboard",
|
|
"/objective-management/plan-years",
|
|
"/performance-management/plan-years",
|
|
"/dms/dashboard",
|
|
]);
|
|
|
|
// Module roots and plan-year list pages are reachable from the sidebar/module
|
|
// switcher — omit them from breadcrumbs to avoid redundant navigation.
|
|
const HIDDEN_BREADCRUMB_SEGMENTS = new Set([
|
|
"plan-years",
|
|
"record-management",
|
|
"objective-management",
|
|
"performance-management",
|
|
"dms",
|
|
"user-management",
|
|
]);
|
|
|
|
const isDynamicId = (segment: string) =>
|
|
/^[0-9a-f]{8,}$/i.test(segment) || /^[0-9]+$/.test(segment);
|
|
|
|
const makeReadableLabel = (segment: string) => {
|
|
const withSpaces = segment
|
|
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
.replace(/[_-]/g, " ");
|
|
return capitalize(withSpaces);
|
|
};
|
|
|
|
export function DynamicBreadcrumb() {
|
|
const location = useLocation();
|
|
const { t } = useTranslation();
|
|
|
|
const breadcrumbs = useMemo(() => {
|
|
if (MODULE_LANDING_PATHS.has(location.pathname)) {
|
|
return [] as BreadcrumbNode[];
|
|
}
|
|
|
|
const pathSegments = location.pathname.split("/").filter(Boolean);
|
|
if (pathSegments.length === 0) return [] as BreadcrumbNode[];
|
|
|
|
const labelMap: Record<string, string> = {
|
|
dashboard: t("header.navigation.dashboard"),
|
|
userRecords: t("header.navigation.outgoing"),
|
|
userIncoming: t("header.navigation.incoming"),
|
|
delegation: t("header.navigation.delegation"),
|
|
approval: t("header.navigation.approval"),
|
|
pending: t("header.navigation.pending"),
|
|
collaborations: t("header.navigation.collaborations"),
|
|
recordOfficer: "Record Officer",
|
|
incoming: t("header.navigation.incoming"),
|
|
outgoing: t("header.navigation.outgoing"),
|
|
add: t("common.create"),
|
|
new: t("common.create"),
|
|
edit: t("common.Edit"),
|
|
profile: t("profile.profile"),
|
|
"update-profile": t("profile.updateProfile"),
|
|
"change-password": t("header.changePassword"),
|
|
uploadTeeterandSignature: t("header.addTeeter"),
|
|
viewDelegationLetter: t("delegation.viewLetter"),
|
|
editDelegation: t("delegation.editDelegation"),
|
|
incomingHistory: "Incoming History",
|
|
incomingNonSmartExternal: "External Incoming",
|
|
viewIncoming: "View Incoming",
|
|
goals: t("organization.Goals", "Goals"),
|
|
"detail-task": t("organization.detail_task", "Detail Task"),
|
|
"Major-Task": t("organization.Major Task", "Major Task"),
|
|
expectations: t("organization.expectations", "Expectations"),
|
|
"employee-expectations": t(
|
|
"organization.employeeExpectations",
|
|
"Employee Expectations",
|
|
),
|
|
achievements: t("organization.achievements", "Achievements"),
|
|
"additional-tasks": t("organization.additionalTasks", "Additional Tasks"),
|
|
"signature-settings": t(
|
|
"organization.Signature Settings",
|
|
"Signature Settings",
|
|
),
|
|
};
|
|
|
|
let currentPath = "";
|
|
const items: BreadcrumbNode[] = [];
|
|
|
|
for (const segment of pathSegments) {
|
|
currentPath += `/${segment}`;
|
|
|
|
if (HIDDEN_BREADCRUMB_SEGMENTS.has(segment) || isDynamicId(segment)) {
|
|
continue;
|
|
}
|
|
|
|
const baseLabel =
|
|
labelMap[segment] || makeReadableLabel(segment);
|
|
|
|
items.push({
|
|
label: baseLabel,
|
|
path: currentPath,
|
|
isActive: false,
|
|
});
|
|
}
|
|
|
|
if (items.length > 0) {
|
|
items[items.length - 1].isActive = true;
|
|
}
|
|
|
|
return items;
|
|
}, [location.pathname, t]);
|
|
|
|
if (!breadcrumbs.length) {
|
|
return null;
|
|
}
|
|
|
|
const compactBreadcrumbs = breadcrumbs;
|
|
|
|
return (
|
|
<div className="max-w-full overflow-hidden rounded-xl bg-white/80 px-2.5 py-1.5 shadow-sm ring-1 ring-black/5 backdrop-blur dark:bg-gray-800/70 dark:ring-white/10">
|
|
<Breadcrumb>
|
|
<BreadcrumbList className="flex-nowrap gap-0.5 overflow-x-auto whitespace-nowrap pb-0.5 text-xs">
|
|
{compactBreadcrumbs.map((breadcrumb, index) => (
|
|
<div key={`${breadcrumb.path}-${index}`} className="flex items-center">
|
|
{index > 0 && (
|
|
<span className="mx-1 text-gray-400 dark:text-gray-500">
|
|
<ChevronRight className="h-3.5 w-3.5" />
|
|
</span>
|
|
)}
|
|
|
|
{breadcrumb.isEllipsis ? (
|
|
<span className="px-2 py-1 text-xs font-semibold text-gray-400 dark:text-gray-500">
|
|
...
|
|
</span>
|
|
) : breadcrumb.isActive ? (
|
|
<span className="inline-flex max-w-[320px] items-center truncate rounded-full bg-primary-50 px-2 py-1 text-xs font-semibold text-primary-700 ring-1 ring-inset ring-primary-200/70 dark:bg-primary-900/40 dark:text-white dark:ring-primary-700/50" title={breadcrumb.label}>
|
|
{breadcrumb.label}
|
|
</span>
|
|
) : (
|
|
<Link
|
|
to={breadcrumb.path}
|
|
className="inline-flex max-w-[240px] items-center gap-1 truncate rounded-full px-2 py-1 text-xs font-medium text-gray-500 transition-colors hover:bg-primary-50 hover:text-primary-700 dark:text-gray-300 dark:hover:bg-primary/10 dark:hover:text-primary-foreground"
|
|
title={breadcrumb.label}
|
|
>
|
|
{index === 0 && <Home className="h-3.5 w-3.5" />}
|
|
<span className="truncate">{breadcrumb.label}</span>
|
|
</Link>
|
|
)}
|
|
</div>
|
|
))}
|
|
</BreadcrumbList>
|
|
</Breadcrumb>
|
|
</div>
|
|
);
|
|
}
|