QuickNav.jsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. "use client";
  2. import React from "react";
  3. import Link from "next/link";
  4. import { usePathname, useRouter } from "next/navigation";
  5. import { FolderOpen, Search as SearchIcon } from "lucide-react";
  6. import { useAuth } from "@/components/auth/authContext";
  7. import { getBranches } from "@/lib/frontend/apiClient";
  8. import { branchPath, searchPath } from "@/lib/frontend/routes";
  9. import { isValidBranchParam } from "@/lib/frontend/params";
  10. import {
  11. buildNextUrlForBranchSwitch,
  12. readRouteBranchFromPathname,
  13. safeReadLocalStorageBranch,
  14. safeWriteLocalStorageBranch,
  15. } from "@/lib/frontend/quickNav/branchSwitch";
  16. import {
  17. getPrimaryNavFromPathname,
  18. PRIMARY_NAV,
  19. } from "@/lib/frontend/nav/activeRoute";
  20. import { Button } from "@/components/ui/button";
  21. import {
  22. DropdownMenu,
  23. DropdownMenuContent,
  24. DropdownMenuLabel,
  25. DropdownMenuRadioGroup,
  26. DropdownMenuRadioItem,
  27. DropdownMenuSeparator,
  28. DropdownMenuTrigger,
  29. } from "@/components/ui/dropdown-menu";
  30. const STORAGE_KEY_LAST_BRANCH = "rhl_last_branch";
  31. const BRANCH_LIST_STATE = Object.freeze({
  32. IDLE: "idle",
  33. LOADING: "loading",
  34. READY: "ready",
  35. ERROR: "error",
  36. });
  37. export default function QuickNav() {
  38. const router = useRouter();
  39. const pathname = usePathname() || "/";
  40. const { status, user } = useAuth();
  41. const isAuthenticated = status === "authenticated" && user;
  42. const isAdminDev =
  43. isAuthenticated && (user.role === "admin" || user.role === "dev");
  44. const isBranchUser = isAuthenticated && user.role === "branch";
  45. const [selectedBranch, setSelectedBranch] = React.useState(null);
  46. const [branchList, setBranchList] = React.useState({
  47. status: BRANCH_LIST_STATE.IDLE,
  48. branches: null,
  49. });
  50. const activePrimaryNav = React.useMemo(() => {
  51. return getPrimaryNavFromPathname(pathname);
  52. }, [pathname]);
  53. const isExplorerActive = activePrimaryNav?.active === PRIMARY_NAV.EXPLORER;
  54. const isSearchActive = activePrimaryNav?.active === PRIMARY_NAV.SEARCH;
  55. React.useEffect(() => {
  56. if (!isAuthenticated) return;
  57. // Branch users: selection is fixed to their own branch.
  58. if (isBranchUser) {
  59. const own = user.branchId;
  60. setSelectedBranch(own && isValidBranchParam(own) ? own : null);
  61. return;
  62. }
  63. // Admin/dev: prefer current route branch, fallback to last-used localStorage.
  64. const fromRoute = readRouteBranchFromPathname(pathname);
  65. const fromStorage = safeReadLocalStorageBranch(STORAGE_KEY_LAST_BRANCH);
  66. const initial = fromRoute || fromStorage || null;
  67. // Avoid unnecessary state updates.
  68. if (initial && initial !== selectedBranch) {
  69. setSelectedBranch(initial);
  70. safeWriteLocalStorageBranch(STORAGE_KEY_LAST_BRANCH, initial);
  71. }
  72. }, [isAuthenticated, isBranchUser, user?.branchId, pathname, selectedBranch]);
  73. React.useEffect(() => {
  74. // Fetch the branch list once for admin/dev users (or when the user changes),
  75. // not on every selectedBranch change.
  76. if (!isAdminDev) return;
  77. let cancelled = false;
  78. setBranchList({ status: BRANCH_LIST_STATE.LOADING, branches: null });
  79. (async () => {
  80. try {
  81. const res = await getBranches();
  82. if (cancelled) return;
  83. const branches = Array.isArray(res?.branches) ? res.branches : [];
  84. setBranchList({ status: BRANCH_LIST_STATE.READY, branches });
  85. } catch (err) {
  86. if (cancelled) return;
  87. console.error("[QuickNav] getBranches failed:", err);
  88. setBranchList({ status: BRANCH_LIST_STATE.ERROR, branches: null });
  89. }
  90. })();
  91. return () => {
  92. cancelled = true;
  93. };
  94. }, [isAdminDev, user?.userId]);
  95. React.useEffect(() => {
  96. // After we have the branch list, ensure selectedBranch is valid and known.
  97. // This effect does NOT trigger any refetches (only local state).
  98. if (!isAdminDev) return;
  99. if (branchList.status !== BRANCH_LIST_STATE.READY) return;
  100. const branches = Array.isArray(branchList.branches)
  101. ? branchList.branches
  102. : [];
  103. if (branches.length === 0) return;
  104. if (!selectedBranch || !branches.includes(selectedBranch)) {
  105. const next = branches[0];
  106. setSelectedBranch(next);
  107. safeWriteLocalStorageBranch(STORAGE_KEY_LAST_BRANCH, next);
  108. }
  109. }, [isAdminDev, branchList.status, branchList.branches, selectedBranch]);
  110. React.useEffect(() => {
  111. if (!isAdminDev) return;
  112. if (!selectedBranch) return;
  113. safeWriteLocalStorageBranch(STORAGE_KEY_LAST_BRANCH, selectedBranch);
  114. }, [isAdminDev, selectedBranch]);
  115. if (!isAuthenticated) return null;
  116. const effectiveBranch = isBranchUser ? user.branchId : selectedBranch;
  117. const canNavigate = Boolean(
  118. effectiveBranch && isValidBranchParam(effectiveBranch),
  119. );
  120. function navigateToBranchKeepingContext(nextBranch) {
  121. if (!isValidBranchParam(nextBranch)) return;
  122. // IMPORTANT:
  123. // Avoid useSearchParams() here to prevent build-time prerender failures on static routes.
  124. // We only need the current query string at click-time (client-only), so window is fine.
  125. const currentPathname =
  126. typeof window !== "undefined"
  127. ? window.location.pathname || pathname
  128. : pathname;
  129. const currentSearch =
  130. typeof window !== "undefined" ? window.location.search || "" : "";
  131. const nextUrl = buildNextUrlForBranchSwitch({
  132. pathname: currentPathname,
  133. search: currentSearch,
  134. nextBranch,
  135. });
  136. if (!nextUrl) return;
  137. // Client navigation: keeps providers mounted and avoids hard reload flicker.
  138. router.push(nextUrl);
  139. }
  140. const explorerVariant = isExplorerActive ? "secondary" : "ghost";
  141. const searchVariant = isSearchActive ? "secondary" : "ghost";
  142. return (
  143. <div className="hidden items-center gap-2 md:flex">
  144. {isAdminDev ? (
  145. <DropdownMenu>
  146. <DropdownMenuTrigger asChild>
  147. <Button
  148. variant="outline"
  149. size="sm"
  150. type="button"
  151. title="Niederlassung auswählen"
  152. >
  153. {canNavigate ? effectiveBranch : "Niederlassung wählen"}
  154. </Button>
  155. </DropdownMenuTrigger>
  156. <DropdownMenuContent align="end" className="min-w-56">
  157. <DropdownMenuLabel>Niederlassung</DropdownMenuLabel>
  158. <DropdownMenuSeparator />
  159. {branchList.status === BRANCH_LIST_STATE.ERROR ? (
  160. <div className="px-2 py-2 text-xs text-muted-foreground">
  161. Konnte nicht geladen werden.
  162. </div>
  163. ) : (
  164. <DropdownMenuRadioGroup
  165. value={canNavigate ? effectiveBranch : ""}
  166. onValueChange={(value) => {
  167. if (!value) return;
  168. if (!isValidBranchParam(value)) return;
  169. setSelectedBranch(value);
  170. safeWriteLocalStorageBranch(STORAGE_KEY_LAST_BRANCH, value);
  171. navigateToBranchKeepingContext(value);
  172. }}
  173. >
  174. {(Array.isArray(branchList.branches)
  175. ? branchList.branches
  176. : []
  177. ).map((b) => (
  178. <DropdownMenuRadioItem key={b} value={b}>
  179. {b}
  180. </DropdownMenuRadioItem>
  181. ))}
  182. </DropdownMenuRadioGroup>
  183. )}
  184. </DropdownMenuContent>
  185. </DropdownMenu>
  186. ) : null}
  187. <Button
  188. variant={explorerVariant}
  189. size="sm"
  190. asChild
  191. disabled={!canNavigate}
  192. >
  193. <Link
  194. href={canNavigate ? branchPath(effectiveBranch) : "#"}
  195. title="Explorer öffnen"
  196. aria-current={isExplorerActive ? "page" : undefined}
  197. >
  198. <FolderOpen className="h-4 w-4" />
  199. Explorer
  200. </Link>
  201. </Button>
  202. <Button variant={searchVariant} size="sm" asChild disabled={!canNavigate}>
  203. <Link
  204. href={canNavigate ? searchPath(effectiveBranch) : "#"}
  205. title="Suche öffnen"
  206. aria-current={isSearchActive ? "page" : undefined}
  207. >
  208. <SearchIcon className="h-4 w-4" />
  209. Suche
  210. </Link>
  211. </Button>
  212. </div>
  213. );
  214. }