| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289 |
- "use client";
- import React from "react";
- import Link from "next/link";
- import { usePathname, useRouter } from "next/navigation";
- import { FolderOpen, Search as SearchIcon } from "lucide-react";
- import { useAuth } from "@/components/auth/authContext";
- import { getBranches } from "@/lib/frontend/apiClient";
- import { branchPath, searchPath } from "@/lib/frontend/routes";
- import { isValidBranchParam } from "@/lib/frontend/params";
- import {
- buildNextUrlForBranchSwitch,
- readRouteBranchFromPathname,
- safeReadLocalStorageBranch,
- safeWriteLocalStorageBranch,
- } from "@/lib/frontend/quickNav/branchSwitch";
- import {
- getPrimaryNavFromPathname,
- PRIMARY_NAV,
- } from "@/lib/frontend/nav/activeRoute";
- import { Button } from "@/components/ui/button";
- import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuLabel,
- DropdownMenuRadioGroup,
- DropdownMenuRadioItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
- } from "@/components/ui/dropdown-menu";
- const STORAGE_KEY_LAST_BRANCH = "rhl_last_branch";
- const BRANCH_LIST_STATE = Object.freeze({
- IDLE: "idle",
- LOADING: "loading",
- READY: "ready",
- ERROR: "error",
- });
- export default function QuickNav() {
- const router = useRouter();
- const pathname = usePathname() || "/";
- const { status, user, retry } = useAuth();
- const isAuthenticated = status === "authenticated" && user;
- const isAdminDev =
- isAuthenticated && (user.role === "admin" || user.role === "dev");
- const isBranchUser = isAuthenticated && user.role === "branch";
- 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({
- status: BRANCH_LIST_STATE.IDLE,
- branches: null,
- });
- const activePrimaryNav = React.useMemo(() => {
- return getPrimaryNavFromPathname(pathname);
- }, [pathname]);
- const isExplorerActive = activePrimaryNav?.active === PRIMARY_NAV.EXPLORER;
- const isSearchActive = activePrimaryNav?.active === PRIMARY_NAV.SEARCH;
- const routeBranch = React.useMemo(() => {
- return readRouteBranchFromPathname(pathname);
- }, [pathname]);
- const knownBranches =
- branchList.status === BRANCH_LIST_STATE.READY &&
- Array.isArray(branchList.branches)
- ? 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]);
- 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;
- setBranchList({ status: BRANCH_LIST_STATE.LOADING, branches: null });
- (async () => {
- try {
- const res = await getBranches();
- if (cancelled) return;
- const branches = Array.isArray(res?.branches) ? res.branches : [];
- setBranchList({ status: BRANCH_LIST_STATE.READY, branches });
- } 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 });
- }
- })();
- return () => {
- cancelled = true;
- };
- }, [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;
- if (!selectedBranch || !knownBranches.includes(selectedBranch)) {
- const next = knownBranches[0];
- setSelectedBranch(next);
- safeWriteLocalStorageBranch(STORAGE_KEY_LAST_BRANCH, next);
- }
- }, [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 (routeBranch !== selectedBranch) {
- setSelectedBranch(routeBranch);
- safeWriteLocalStorageBranch(STORAGE_KEY_LAST_BRANCH, routeBranch);
- }
- }, [isAdminDev, isKnownRouteBranch, routeBranch, 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(
- effectiveBranch && isValidBranchParam(effectiveBranch),
- );
- function navigateToBranchKeepingContext(nextBranch) {
- if (!isValidBranchParam(nextBranch)) return;
- const currentPathname =
- typeof window !== "undefined"
- ? window.location.pathname || pathname
- : pathname;
- const currentSearch =
- typeof window !== "undefined" ? window.location.search || "" : "";
- const nextUrl = buildNextUrlForBranchSwitch({
- pathname: currentPathname,
- search: currentSearch,
- nextBranch,
- });
- 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";
- 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">
- <DropdownMenuLabel>Niederlassung</DropdownMenuLabel>
- <DropdownMenuSeparator />
- {branchList.status === BRANCH_LIST_STATE.ERROR ? (
- <div className="px-2 py-2 text-xs text-muted-foreground">
- Konnte nicht geladen werden.
- </div>
- ) : (
- <DropdownMenuRadioGroup
- value={canNavigate ? effectiveBranch : ""}
- onValueChange={(value) => {
- if (!value) return;
- if (!isValidBranchParam(value)) return;
- setSelectedBranch(value);
- safeWriteLocalStorageBranch(STORAGE_KEY_LAST_BRANCH, value);
- navigateToBranchKeepingContext(value);
- }}
- >
- {(Array.isArray(branchList.branches)
- ? branchList.branches
- : []
- ).map((b) => (
- <DropdownMenuRadioItem key={b} value={b}>
- {b}
- </DropdownMenuRadioItem>
- ))}
- </DropdownMenuRadioGroup>
- )}
- </DropdownMenuContent>
- </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>
- </div>
- );
- }
|