QuickNav.jsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. "use client";
  2. import React from "react";
  3. import Link from "next/link";
  4. import { usePathname, useRouter } from "next/navigation";
  5. import {
  6. FolderOpen,
  7. Search as SearchIcon,
  8. TriangleAlert,
  9. CornerDownLeft,
  10. } from "lucide-react";
  11. import { useAuth } from "@/components/auth/authContext";
  12. import { getBranches } from "@/lib/frontend/apiClient";
  13. import { branchPath, searchPath } from "@/lib/frontend/routes";
  14. import { isValidBranchParam } from "@/lib/frontend/params";
  15. import {
  16. buildNextUrlForBranchSwitch,
  17. readRouteBranchFromPathname,
  18. safeReadLocalStorageBranch,
  19. safeWriteLocalStorageBranch,
  20. } from "@/lib/frontend/quickNav/branchSwitch";
  21. import {
  22. getPrimaryNavFromPathname,
  23. PRIMARY_NAV,
  24. } from "@/lib/frontend/nav/activeRoute";
  25. import { Button } from "@/components/ui/button";
  26. import {
  27. DropdownMenu,
  28. DropdownMenuContent,
  29. DropdownMenuItem,
  30. DropdownMenuLabel,
  31. DropdownMenuRadioGroup,
  32. DropdownMenuRadioItem,
  33. DropdownMenuSeparator,
  34. DropdownMenuTrigger,
  35. } from "@/components/ui/dropdown-menu";
  36. const STORAGE_KEY_LAST_BRANCH = "rhl_last_branch";
  37. const BRANCH_LIST_STATE = Object.freeze({
  38. IDLE: "idle",
  39. LOADING: "loading",
  40. READY: "ready",
  41. ERROR: "error",
  42. });
  43. // Header polish:
  44. // - outline buttons normally have a subtle shadow; remove it for crisp header UI
  45. // - normalize padding when an icon is present
  46. const TOPNAV_BUTTON_CLASS = "shadow-none has-[>svg]:px-3";
  47. // Active nav style (blue like multi-branch selection)
  48. const ACTIVE_NAV_BUTTON_CLASS =
  49. "border-blue-600 bg-blue-50 text-blue-900 hover:bg-blue-50 " +
  50. "dark:border-blue-900 dark:bg-blue-950 dark:text-blue-50 dark:hover:bg-blue-950";
  51. export default function QuickNav() {
  52. const router = useRouter();
  53. const pathname = usePathname() || "/";
  54. const { status, user, retry } = useAuth();
  55. const isAuthenticated = status === "authenticated" && user;
  56. const isAdminDev =
  57. isAuthenticated && (user.role === "admin" || user.role === "dev");
  58. const isBranchUser = isAuthenticated && user.role === "branch";
  59. const canRevalidate = typeof retry === "function";
  60. const [selectedBranch, setSelectedBranch] = React.useState(null);
  61. const [branchList, setBranchList] = React.useState({
  62. status: BRANCH_LIST_STATE.IDLE,
  63. branches: null,
  64. });
  65. const activePrimaryNav = React.useMemo(() => {
  66. return getPrimaryNavFromPathname(pathname);
  67. }, [pathname]);
  68. const isExplorerActive = activePrimaryNav?.active === PRIMARY_NAV.EXPLORER;
  69. const isSearchActive = activePrimaryNav?.active === PRIMARY_NAV.SEARCH;
  70. const routeBranch = React.useMemo(() => {
  71. return readRouteBranchFromPathname(pathname);
  72. }, [pathname]);
  73. const knownBranches =
  74. branchList.status === BRANCH_LIST_STATE.READY &&
  75. Array.isArray(branchList.branches)
  76. ? branchList.branches
  77. : null;
  78. const hasInvalidRouteBranch = Boolean(
  79. isAdminDev &&
  80. routeBranch &&
  81. knownBranches &&
  82. !knownBranches.includes(routeBranch),
  83. );
  84. React.useEffect(() => {
  85. if (!isAuthenticated) return;
  86. if (isBranchUser) {
  87. const own = user.branchId;
  88. setSelectedBranch(own && isValidBranchParam(own) ? own : null);
  89. return;
  90. }
  91. const fromStorage = safeReadLocalStorageBranch(STORAGE_KEY_LAST_BRANCH);
  92. if (fromStorage && fromStorage !== selectedBranch) {
  93. setSelectedBranch(fromStorage);
  94. }
  95. }, [isAuthenticated, isBranchUser, user?.branchId, selectedBranch]);
  96. React.useEffect(() => {
  97. if (!isAdminDev) return;
  98. let cancelled = false;
  99. setBranchList({ status: BRANCH_LIST_STATE.LOADING, branches: null });
  100. (async () => {
  101. try {
  102. const res = await getBranches();
  103. if (cancelled) return;
  104. const branches = Array.isArray(res?.branches) ? res.branches : [];
  105. setBranchList({ status: BRANCH_LIST_STATE.READY, branches });
  106. } catch (err) {
  107. if (cancelled) return;
  108. console.error("[QuickNav] getBranches failed:", err);
  109. setBranchList({ status: BRANCH_LIST_STATE.ERROR, branches: null });
  110. }
  111. })();
  112. return () => {
  113. cancelled = true;
  114. };
  115. }, [isAdminDev, user?.userId]);
  116. React.useEffect(() => {
  117. if (!isAdminDev) return;
  118. if (!knownBranches || knownBranches.length === 0) return;
  119. if (!selectedBranch || !knownBranches.includes(selectedBranch)) {
  120. const next = knownBranches[0];
  121. setSelectedBranch(next);
  122. safeWriteLocalStorageBranch(STORAGE_KEY_LAST_BRANCH, next);
  123. }
  124. }, [isAdminDev, knownBranches, selectedBranch]);
  125. React.useEffect(() => {
  126. if (!isAdminDev) return;
  127. if (!routeBranch) return;
  128. if (!knownBranches) return;
  129. const isKnownRouteBranch = knownBranches.includes(routeBranch);
  130. if (!isKnownRouteBranch) return;
  131. if (routeBranch !== selectedBranch) {
  132. setSelectedBranch(routeBranch);
  133. safeWriteLocalStorageBranch(STORAGE_KEY_LAST_BRANCH, routeBranch);
  134. }
  135. }, [isAdminDev, routeBranch, knownBranches, selectedBranch]);
  136. React.useEffect(() => {
  137. if (!isAdminDev) return;
  138. if (!selectedBranch) return;
  139. safeWriteLocalStorageBranch(STORAGE_KEY_LAST_BRANCH, selectedBranch);
  140. }, [isAdminDev, selectedBranch]);
  141. if (!isAuthenticated) return null;
  142. const effectiveBranch = isBranchUser ? user.branchId : selectedBranch;
  143. const canNavigate = Boolean(
  144. effectiveBranch && isValidBranchParam(effectiveBranch),
  145. );
  146. function navigateToBranchKeepingContext(nextBranch) {
  147. if (!isValidBranchParam(nextBranch)) return;
  148. const currentPathname =
  149. typeof window !== "undefined"
  150. ? window.location.pathname || pathname
  151. : pathname;
  152. const currentSearch =
  153. typeof window !== "undefined" ? window.location.search || "" : "";
  154. const nextUrl = buildNextUrlForBranchSwitch({
  155. pathname: currentPathname,
  156. search: currentSearch,
  157. nextBranch,
  158. });
  159. if (!nextUrl) return;
  160. if (canRevalidate) retry();
  161. router.push(nextUrl);
  162. }
  163. const branchButtonTitle = hasInvalidRouteBranch
  164. ? `Achtung: Die URL-Niederlassung ${routeBranch} existiert nicht. Bitte eine gültige Niederlassung wählen.`
  165. : "Niederlassung auswählen";
  166. return (
  167. <div className="hidden items-center gap-2 md:flex">
  168. {isAdminDev ? (
  169. <DropdownMenu>
  170. <DropdownMenuTrigger asChild>
  171. <Button
  172. variant="outline"
  173. size="sm"
  174. type="button"
  175. title={branchButtonTitle}
  176. className={TOPNAV_BUTTON_CLASS}
  177. >
  178. {canNavigate ? effectiveBranch : "Niederlassung wählen"}
  179. {hasInvalidRouteBranch ? (
  180. <TriangleAlert
  181. className="h-4 w-4 text-destructive"
  182. aria-hidden="true"
  183. />
  184. ) : null}
  185. </Button>
  186. </DropdownMenuTrigger>
  187. <DropdownMenuContent align="end" className="min-w-64">
  188. <DropdownMenuLabel>Niederlassung</DropdownMenuLabel>
  189. <DropdownMenuSeparator />
  190. {hasInvalidRouteBranch ? (
  191. <>
  192. <div className="px-2 py-2 text-xs text-destructive">
  193. Die URL-Niederlassung <strong>{routeBranch}</strong> existiert
  194. nicht. Bitte wählen Sie eine gültige Niederlassung aus.
  195. </div>
  196. <DropdownMenuItem
  197. disabled={!canNavigate}
  198. onSelect={(e) => {
  199. e.preventDefault();
  200. if (!canNavigate) return;
  201. navigateToBranchKeepingContext(effectiveBranch);
  202. }}
  203. title={
  204. canNavigate
  205. ? `Zur letzten gültigen Niederlassung wechseln (${effectiveBranch})`
  206. : "Keine gültige Niederlassung verfügbar"
  207. }
  208. >
  209. <CornerDownLeft className="h-4 w-4" aria-hidden="true" />
  210. Zur letzten gültigen Niederlassung
  211. <span className="ml-auto text-xs text-muted-foreground">
  212. {canNavigate ? effectiveBranch : ""}
  213. </span>
  214. </DropdownMenuItem>
  215. <DropdownMenuSeparator />
  216. </>
  217. ) : null}
  218. {branchList.status === BRANCH_LIST_STATE.ERROR ? (
  219. <div className="px-2 py-2 text-xs text-muted-foreground">
  220. Konnte nicht geladen werden.
  221. </div>
  222. ) : (
  223. <DropdownMenuRadioGroup
  224. value={canNavigate ? effectiveBranch : ""}
  225. onValueChange={(value) => {
  226. if (!value) return;
  227. if (!isValidBranchParam(value)) return;
  228. setSelectedBranch(value);
  229. safeWriteLocalStorageBranch(STORAGE_KEY_LAST_BRANCH, value);
  230. navigateToBranchKeepingContext(value);
  231. }}
  232. >
  233. {(Array.isArray(branchList.branches)
  234. ? branchList.branches
  235. : []
  236. ).map((b) => (
  237. <DropdownMenuRadioItem key={b} value={b}>
  238. {b}
  239. </DropdownMenuRadioItem>
  240. ))}
  241. </DropdownMenuRadioGroup>
  242. )}
  243. </DropdownMenuContent>
  244. </DropdownMenu>
  245. ) : null}
  246. <Button
  247. variant="outline"
  248. size="sm"
  249. asChild
  250. disabled={!canNavigate}
  251. className={[
  252. TOPNAV_BUTTON_CLASS,
  253. isExplorerActive ? ACTIVE_NAV_BUTTON_CLASS : "",
  254. ].join(" ")}
  255. >
  256. <Link
  257. href={canNavigate ? branchPath(effectiveBranch) : "#"}
  258. title="Explorer öffnen"
  259. aria-current={isExplorerActive ? "page" : undefined}
  260. >
  261. <FolderOpen className="h-4 w-4" />
  262. Explorer
  263. </Link>
  264. </Button>
  265. <Button
  266. variant="outline"
  267. size="sm"
  268. asChild
  269. disabled={!canNavigate}
  270. className={[
  271. TOPNAV_BUTTON_CLASS,
  272. isSearchActive ? ACTIVE_NAV_BUTTON_CLASS : "",
  273. ].join(" ")}
  274. >
  275. <Link
  276. href={canNavigate ? searchPath(effectiveBranch) : "#"}
  277. title="Suche öffnen"
  278. aria-current={isSearchActive ? "page" : undefined}
  279. >
  280. <SearchIcon className="h-4 w-4" />
  281. Suche
  282. </Link>
  283. </Button>
  284. </div>
  285. );
  286. }