|
|
@@ -3,7 +3,12 @@
|
|
|
import React from "react";
|
|
|
import Link from "next/link";
|
|
|
import { usePathname, useRouter } from "next/navigation";
|
|
|
-import { FolderOpen, Search as SearchIcon } from "lucide-react";
|
|
|
+import {
|
|
|
+ FolderOpen,
|
|
|
+ Search as SearchIcon,
|
|
|
+ TriangleAlert,
|
|
|
+ CornerDownLeft,
|
|
|
+} from "lucide-react";
|
|
|
|
|
|
import { useAuth } from "@/components/auth/authContext";
|
|
|
import { getBranches } from "@/lib/frontend/apiClient";
|
|
|
@@ -24,12 +29,18 @@ import { Button } from "@/components/ui/button";
|
|
|
import {
|
|
|
DropdownMenu,
|
|
|
DropdownMenuContent,
|
|
|
+ DropdownMenuItem,
|
|
|
DropdownMenuLabel,
|
|
|
DropdownMenuRadioGroup,
|
|
|
DropdownMenuRadioItem,
|
|
|
DropdownMenuSeparator,
|
|
|
DropdownMenuTrigger,
|
|
|
} from "@/components/ui/dropdown-menu";
|
|
|
+import {
|
|
|
+ Tooltip,
|
|
|
+ TooltipContent,
|
|
|
+ TooltipTrigger,
|
|
|
+} from "@/components/ui/tooltip";
|
|
|
|
|
|
const STORAGE_KEY_LAST_BRANCH = "rhl_last_branch";
|
|
|
|
|
|
@@ -40,6 +51,12 @@ const BRANCH_LIST_STATE = Object.freeze({
|
|
|
ERROR: "error",
|
|
|
});
|
|
|
|
|
|
+const TOPNAV_BUTTON_CLASS = "shadow-none has-[>svg]:px-3";
|
|
|
+
|
|
|
+const ACTIVE_NAV_BUTTON_CLASS =
|
|
|
+ "border-blue-600 bg-blue-50 text-blue-900 hover:bg-blue-50 " +
|
|
|
+ "dark:border-blue-900 dark:bg-blue-950 dark:text-blue-50 dark:hover:bg-blue-950";
|
|
|
+
|
|
|
export default function QuickNav() {
|
|
|
const router = useRouter();
|
|
|
const pathname = usePathname() || "/";
|
|
|
@@ -53,8 +70,6 @@ export default function QuickNav() {
|
|
|
|
|
|
const canRevalidate = typeof retry === "function";
|
|
|
|
|
|
- // Persisted selection for admin/dev:
|
|
|
- // - Used as the "safe fallback" when the route branch is invalid/non-existent.
|
|
|
const [selectedBranch, setSelectedBranch] = React.useState(null);
|
|
|
|
|
|
const [branchList, setBranchList] = React.useState({
|
|
|
@@ -79,35 +94,29 @@ export default function QuickNav() {
|
|
|
? branchList.branches
|
|
|
: null;
|
|
|
|
|
|
- const isKnownRouteBranch = React.useMemo(() => {
|
|
|
- if (!routeBranch) return false;
|
|
|
- if (!knownBranches) return false; // do not "trust" route until we know the branch list
|
|
|
- return knownBranches.includes(routeBranch);
|
|
|
- }, [routeBranch, knownBranches]);
|
|
|
+ const hasInvalidRouteBranch = Boolean(
|
|
|
+ isAdminDev &&
|
|
|
+ routeBranch &&
|
|
|
+ knownBranches &&
|
|
|
+ !knownBranches.includes(routeBranch),
|
|
|
+ );
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
if (!isAuthenticated) return;
|
|
|
|
|
|
- // Branch users: fixed branch, no persisted selection needed.
|
|
|
if (isBranchUser) {
|
|
|
const own = user.branchId;
|
|
|
setSelectedBranch(own && isValidBranchParam(own) ? own : null);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // Admin/dev: initialize from localStorage only.
|
|
|
- // IMPORTANT:
|
|
|
- // We do NOT initialize from the route branch, because invalid-but-syntactically-valid
|
|
|
- // branches (e.g. NL200) would pollute state and cause thrashing.
|
|
|
const fromStorage = safeReadLocalStorageBranch(STORAGE_KEY_LAST_BRANCH);
|
|
|
-
|
|
|
if (fromStorage && fromStorage !== selectedBranch) {
|
|
|
setSelectedBranch(fromStorage);
|
|
|
}
|
|
|
}, [isAuthenticated, isBranchUser, user?.branchId, selectedBranch]);
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
- // Fetch the branch list once for admin/dev users (or when the user changes).
|
|
|
if (!isAdminDev) return;
|
|
|
|
|
|
let cancelled = false;
|
|
|
@@ -124,7 +133,6 @@ export default function QuickNav() {
|
|
|
} catch (err) {
|
|
|
if (cancelled) return;
|
|
|
|
|
|
- // Fail open: do not block navigation if validation fails.
|
|
|
console.error("[QuickNav] getBranches failed:", err);
|
|
|
setBranchList({ status: BRANCH_LIST_STATE.ERROR, branches: null });
|
|
|
}
|
|
|
@@ -136,7 +144,6 @@ export default function QuickNav() {
|
|
|
}, [isAdminDev, user?.userId]);
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
- // Once we know the branch list, keep selectedBranch valid and stable.
|
|
|
if (!isAdminDev) return;
|
|
|
if (!knownBranches || knownBranches.length === 0) return;
|
|
|
|
|
|
@@ -148,32 +155,28 @@ export default function QuickNav() {
|
|
|
}, [isAdminDev, knownBranches, selectedBranch]);
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
- // Sync selectedBranch to the current route branch ONLY when that route branch is known to exist.
|
|
|
- // This prevents the "NL200 thrash" while still keeping the dropdown in sync for valid routes.
|
|
|
if (!isAdminDev) return;
|
|
|
- if (!isKnownRouteBranch) return;
|
|
|
if (!routeBranch) return;
|
|
|
+ if (!knownBranches) return;
|
|
|
+
|
|
|
+ const isKnownRouteBranch = knownBranches.includes(routeBranch);
|
|
|
+ if (!isKnownRouteBranch) return;
|
|
|
|
|
|
if (routeBranch !== selectedBranch) {
|
|
|
setSelectedBranch(routeBranch);
|
|
|
safeWriteLocalStorageBranch(STORAGE_KEY_LAST_BRANCH, routeBranch);
|
|
|
}
|
|
|
- }, [isAdminDev, isKnownRouteBranch, routeBranch, selectedBranch]);
|
|
|
+ }, [isAdminDev, routeBranch, knownBranches, selectedBranch]);
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
if (!isAdminDev) return;
|
|
|
if (!selectedBranch) return;
|
|
|
|
|
|
- // Keep localStorage in sync (defense-in-depth).
|
|
|
safeWriteLocalStorageBranch(STORAGE_KEY_LAST_BRANCH, selectedBranch);
|
|
|
}, [isAdminDev, selectedBranch]);
|
|
|
|
|
|
if (!isAuthenticated) return null;
|
|
|
|
|
|
- // Effective branch for navigation:
|
|
|
- // - Branch users: always their own branch
|
|
|
- // - Admin/dev: always the persisted/validated selection (NOT the route branch)
|
|
|
- // This guarantees that nav buttons still work even when the user is on /NL200.
|
|
|
const effectiveBranch = isBranchUser ? user.branchId : selectedBranch;
|
|
|
|
|
|
const canNavigate = Boolean(
|
|
|
@@ -199,34 +202,72 @@ export default function QuickNav() {
|
|
|
|
|
|
if (!nextUrl) return;
|
|
|
|
|
|
- // Trigger a session revalidation without causing content flicker.
|
|
|
if (canRevalidate) retry();
|
|
|
-
|
|
|
router.push(nextUrl);
|
|
|
}
|
|
|
|
|
|
- const explorerVariant = isExplorerActive ? "secondary" : "ghost";
|
|
|
- const searchVariant = isSearchActive ? "secondary" : "ghost";
|
|
|
+ const branchTooltipText = hasInvalidRouteBranch
|
|
|
+ ? `Achtung: ${routeBranch} existiert nicht. Bitte eine gültige Niederlassung wählen.`
|
|
|
+ : "Niederlassung auswählen";
|
|
|
|
|
|
return (
|
|
|
<div className="hidden items-center gap-2 md:flex">
|
|
|
{isAdminDev ? (
|
|
|
<DropdownMenu>
|
|
|
- <DropdownMenuTrigger asChild>
|
|
|
- <Button
|
|
|
- variant="outline"
|
|
|
- size="sm"
|
|
|
- type="button"
|
|
|
- title="Niederlassung auswählen"
|
|
|
- >
|
|
|
- {canNavigate ? effectiveBranch : "Niederlassung wählen"}
|
|
|
- </Button>
|
|
|
- </DropdownMenuTrigger>
|
|
|
-
|
|
|
- <DropdownMenuContent align="end" className="min-w-56">
|
|
|
+ <Tooltip>
|
|
|
+ <TooltipTrigger asChild>
|
|
|
+ <DropdownMenuTrigger asChild>
|
|
|
+ <Button
|
|
|
+ variant="outline"
|
|
|
+ size="sm"
|
|
|
+ type="button"
|
|
|
+ className={TOPNAV_BUTTON_CLASS}
|
|
|
+ aria-label={branchTooltipText}
|
|
|
+ >
|
|
|
+ {canNavigate ? effectiveBranch : "Niederlassung wählen"}
|
|
|
+ {hasInvalidRouteBranch ? (
|
|
|
+ <TriangleAlert
|
|
|
+ className="h-4 w-4 text-destructive"
|
|
|
+ aria-hidden="true"
|
|
|
+ />
|
|
|
+ ) : null}
|
|
|
+ </Button>
|
|
|
+ </DropdownMenuTrigger>
|
|
|
+ </TooltipTrigger>
|
|
|
+
|
|
|
+ <TooltipContent side="bottom">{branchTooltipText}</TooltipContent>
|
|
|
+ </Tooltip>
|
|
|
+
|
|
|
+ <DropdownMenuContent align="end" className="min-w-64">
|
|
|
<DropdownMenuLabel>Niederlassung</DropdownMenuLabel>
|
|
|
<DropdownMenuSeparator />
|
|
|
|
|
|
+ {hasInvalidRouteBranch ? (
|
|
|
+ <>
|
|
|
+ <div className="px-2 py-2 text-xs text-destructive">
|
|
|
+ Die URL-Niederlassung <strong>{routeBranch}</strong> existiert
|
|
|
+ nicht. Bitte wählen Sie eine gültige Niederlassung aus.
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <DropdownMenuItem
|
|
|
+ disabled={!canNavigate}
|
|
|
+ onSelect={(e) => {
|
|
|
+ e.preventDefault();
|
|
|
+ if (!canNavigate) return;
|
|
|
+ navigateToBranchKeepingContext(effectiveBranch);
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <CornerDownLeft className="h-4 w-4" aria-hidden="true" />
|
|
|
+ Zur letzten gültigen Niederlassung
|
|
|
+ <span className="ml-auto text-xs text-muted-foreground">
|
|
|
+ {canNavigate ? effectiveBranch : ""}
|
|
|
+ </span>
|
|
|
+ </DropdownMenuItem>
|
|
|
+
|
|
|
+ <DropdownMenuSeparator />
|
|
|
+ </>
|
|
|
+ ) : null}
|
|
|
+
|
|
|
{branchList.status === BRANCH_LIST_STATE.ERROR ? (
|
|
|
<div className="px-2 py-2 text-xs text-muted-foreground">
|
|
|
Konnte nicht geladen werden.
|
|
|
@@ -240,7 +281,6 @@ export default function QuickNav() {
|
|
|
|
|
|
setSelectedBranch(value);
|
|
|
safeWriteLocalStorageBranch(STORAGE_KEY_LAST_BRANCH, value);
|
|
|
-
|
|
|
navigateToBranchKeepingContext(value);
|
|
|
}}
|
|
|
>
|
|
|
@@ -258,32 +298,57 @@ export default function QuickNav() {
|
|
|
</DropdownMenu>
|
|
|
) : null}
|
|
|
|
|
|
- <Button
|
|
|
- variant={explorerVariant}
|
|
|
- size="sm"
|
|
|
- asChild
|
|
|
- disabled={!canNavigate}
|
|
|
- >
|
|
|
- <Link
|
|
|
- href={canNavigate ? branchPath(effectiveBranch) : "#"}
|
|
|
- title="Explorer öffnen"
|
|
|
- aria-current={isExplorerActive ? "page" : undefined}
|
|
|
- >
|
|
|
- <FolderOpen className="h-4 w-4" />
|
|
|
- Explorer
|
|
|
- </Link>
|
|
|
- </Button>
|
|
|
-
|
|
|
- <Button variant={searchVariant} size="sm" asChild disabled={!canNavigate}>
|
|
|
- <Link
|
|
|
- href={canNavigate ? searchPath(effectiveBranch) : "#"}
|
|
|
- title="Suche öffnen"
|
|
|
- aria-current={isSearchActive ? "page" : undefined}
|
|
|
- >
|
|
|
- <SearchIcon className="h-4 w-4" />
|
|
|
- Suche
|
|
|
- </Link>
|
|
|
- </Button>
|
|
|
+ <Tooltip>
|
|
|
+ <TooltipTrigger asChild>
|
|
|
+ <Button
|
|
|
+ variant="outline"
|
|
|
+ size="sm"
|
|
|
+ asChild
|
|
|
+ disabled={!canNavigate}
|
|
|
+ className={[
|
|
|
+ TOPNAV_BUTTON_CLASS,
|
|
|
+ isExplorerActive ? ACTIVE_NAV_BUTTON_CLASS : "",
|
|
|
+ ].join(" ")}
|
|
|
+ aria-label="Explorer öffnen"
|
|
|
+ >
|
|
|
+ <Link
|
|
|
+ href={canNavigate ? branchPath(effectiveBranch) : "#"}
|
|
|
+ aria-current={isExplorerActive ? "page" : undefined}
|
|
|
+ >
|
|
|
+ <FolderOpen className="h-4 w-4" />
|
|
|
+ Explorer
|
|
|
+ </Link>
|
|
|
+ </Button>
|
|
|
+ </TooltipTrigger>
|
|
|
+
|
|
|
+ <TooltipContent side="bottom">Explorer öffnen</TooltipContent>
|
|
|
+ </Tooltip>
|
|
|
+
|
|
|
+ <Tooltip>
|
|
|
+ <TooltipTrigger asChild>
|
|
|
+ <Button
|
|
|
+ variant="outline"
|
|
|
+ size="sm"
|
|
|
+ asChild
|
|
|
+ disabled={!canNavigate}
|
|
|
+ className={[
|
|
|
+ TOPNAV_BUTTON_CLASS,
|
|
|
+ isSearchActive ? ACTIVE_NAV_BUTTON_CLASS : "",
|
|
|
+ ].join(" ")}
|
|
|
+ aria-label="Suche öffnen"
|
|
|
+ >
|
|
|
+ <Link
|
|
|
+ href={canNavigate ? searchPath(effectiveBranch) : "#"}
|
|
|
+ aria-current={isSearchActive ? "page" : undefined}
|
|
|
+ >
|
|
|
+ <SearchIcon className="h-4 w-4" />
|
|
|
+ Suche
|
|
|
+ </Link>
|
|
|
+ </Button>
|
|
|
+ </TooltipTrigger>
|
|
|
+
|
|
|
+ <TooltipContent side="bottom">Suche öffnen</TooltipContent>
|
|
|
+ </Tooltip>
|
|
|
</div>
|
|
|
);
|
|
|
}
|