QuickNav.jsx 8.5 KB

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