Переглянути джерело

feat(ui): integrate tooltips for improved user guidance in navigation components

Code_Uwe 1 тиждень тому
батько
коміт
8212f33c60

+ 81 - 67
components/app-shell/QuickNav.jsx

@@ -36,6 +36,11 @@ import {
 	DropdownMenuSeparator,
 	DropdownMenuTrigger,
 } from "@/components/ui/dropdown-menu";
+import {
+	Tooltip,
+	TooltipContent,
+	TooltipTrigger,
+} from "@/components/ui/tooltip";
 
 const STORAGE_KEY_LAST_BRANCH = "rhl_last_branch";
 
@@ -46,12 +51,8 @@ const BRANCH_LIST_STATE = Object.freeze({
 	ERROR: "error",
 });
 
-// Header polish:
-// - outline buttons normally have a subtle shadow; remove it for crisp header UI
-// - normalize padding when an icon is present
 const TOPNAV_BUTTON_CLASS = "shadow-none has-[>svg]:px-3";
 
-// Active nav style (blue like multi-branch selection)
 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";
@@ -205,31 +206,37 @@ export default function QuickNav() {
 		router.push(nextUrl);
 	}
 
-	const branchButtonTitle = hasInvalidRouteBranch
-		? `Achtung: Die URL-Niederlassung ${routeBranch} existiert nicht. Bitte eine gültige Niederlassung wählen.`
+	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={branchButtonTitle}
-							className={TOPNAV_BUTTON_CLASS}
-						>
-							{canNavigate ? effectiveBranch : "Niederlassung wählen"}
-							{hasInvalidRouteBranch ? (
-								<TriangleAlert
-									className="h-4 w-4 text-destructive"
-									aria-hidden="true"
-								/>
-							) : null}
-						</Button>
-					</DropdownMenuTrigger>
+					<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>
@@ -249,11 +256,6 @@ export default function QuickNav() {
 										if (!canNavigate) return;
 										navigateToBranchKeepingContext(effectiveBranch);
 									}}
-									title={
-										canNavigate
-											? `Zur letzten gültigen Niederlassung wechseln (${effectiveBranch})`
-											: "Keine gültige Niederlassung verfügbar"
-									}
 								>
 									<CornerDownLeft className="h-4 w-4" aria-hidden="true" />
 									Zur letzten gültigen Niederlassung
@@ -296,45 +298,57 @@ export default function QuickNav() {
 				</DropdownMenu>
 			) : null}
 
-			<Button
-				variant="outline"
-				size="sm"
-				asChild
-				disabled={!canNavigate}
-				className={[
-					TOPNAV_BUTTON_CLASS,
-					isExplorerActive ? ACTIVE_NAV_BUTTON_CLASS : "",
-				].join(" ")}
-			>
-				<Link
-					href={canNavigate ? branchPath(effectiveBranch) : "#"}
-					title="Explorer öffnen"
-					aria-current={isExplorerActive ? "page" : undefined}
-				>
-					<FolderOpen className="h-4 w-4" />
-					Explorer
-				</Link>
-			</Button>
-
-			<Button
-				variant="outline"
-				size="sm"
-				asChild
-				disabled={!canNavigate}
-				className={[
-					TOPNAV_BUTTON_CLASS,
-					isSearchActive ? ACTIVE_NAV_BUTTON_CLASS : "",
-				].join(" ")}
-			>
-				<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>
 	);
 }

+ 21 - 13
components/app-shell/SessionIndicator.jsx

@@ -10,6 +10,12 @@ import {
 	SESSION_INDICATOR_MIN_VISIBLE_MS,
 } from "@/lib/frontend/ui/uxTimings";
 
+import {
+	Tooltip,
+	TooltipContent,
+	TooltipTrigger,
+} from "@/components/ui/tooltip";
+
 export default function SessionIndicator() {
 	const { status, isValidating } = useAuth();
 
@@ -23,18 +29,20 @@ export default function SessionIndicator() {
 	if (!visible) return null;
 
 	return (
-		<div
-			className="flex items-center gap-2"
-			aria-live="polite"
-			title="Sitzung wird geprüft"
-		>
-			<Loader2
-				className="h-4 w-4 animate-spin text-muted-foreground"
-				aria-hidden="true"
-			/>
-			<span className="hidden text-xs text-muted-foreground md:inline">
-				Sitzung wird geprüft…
-			</span>
-		</div>
+		<Tooltip>
+			<TooltipTrigger asChild>
+				<div className="flex items-center gap-2" aria-live="polite">
+					<Loader2
+						className="h-4 w-4 animate-spin text-muted-foreground"
+						aria-hidden="true"
+					/>
+					<span className="hidden text-xs text-muted-foreground md:inline">
+						Sitzung wird geprüft…
+					</span>
+				</div>
+			</TooltipTrigger>
+
+			<TooltipContent side="bottom">Sitzung wird geprüft…</TooltipContent>
+		</Tooltip>
 	);
 }

+ 16 - 20
components/app-shell/ThemeToggleButton.jsx

@@ -8,10 +8,8 @@ import { Button } from "@/components/ui/button";
 import {
 	Tooltip,
 	TooltipContent,
-	TooltipProvider,
 	TooltipTrigger,
 } from "@/components/ui/tooltip";
-import { TOOLTIP_DELAY_MS } from "@/lib/frontend/ui/uxTimings";
 
 export default function ThemeToggleButton() {
 	const { setTheme } = useTheme();
@@ -25,24 +23,22 @@ export default function ThemeToggleButton() {
 	}
 
 	return (
-		<TooltipProvider delayDuration={TOOLTIP_DELAY_MS}>
-			<Tooltip>
-				<TooltipTrigger asChild>
-					<Button
-						type="button"
-						variant="ghost"
-						size="icon-sm"
-						onClick={toggleTheme}
-						aria-label="Design umschalten"
-					>
-						<Moon className="h-4 w-4 dark:hidden" aria-hidden="true" />
-						<Sun className="hidden h-4 w-4 dark:block" aria-hidden="true" />
-						<span className="sr-only">Design umschalten</span>
-					</Button>
-				</TooltipTrigger>
+		<Tooltip>
+			<TooltipTrigger asChild>
+				<Button
+					type="button"
+					variant="ghost"
+					size="icon-sm"
+					onClick={toggleTheme}
+					aria-label="Design umschalten"
+				>
+					<Moon className="h-4 w-4 dark:hidden" aria-hidden="true" />
+					<Sun className="hidden h-4 w-4 dark:block" aria-hidden="true" />
+					<span className="sr-only">Design umschalten</span>
+				</Button>
+			</TooltipTrigger>
 
-				<TooltipContent side="bottom">Design umschalten</TooltipContent>
-			</Tooltip>
-		</TooltipProvider>
+			<TooltipContent side="bottom">Design umschalten</TooltipContent>
+		</Tooltip>
 	);
 }

+ 45 - 42
components/app-shell/TopNav.jsx

@@ -7,58 +7,61 @@ import UserStatus from "@/components/app-shell/UserStatus";
 import ThemeToggleButton from "@/components/app-shell/ThemeToggleButton";
 import SessionIndicator from "@/components/app-shell/SessionIndicator";
 
+import { TooltipProvider } from "@/components/ui/tooltip";
+import { TOOLTIP_DELAY_MS } from "@/lib/frontend/ui/uxTimings";
+
 export default function TopNav() {
 	return (
 		<header className="sticky top-0 z-50 w-full border-b bg-background">
-			<div className="px-4">
-				<div className="mx-auto grid h-14 w-full items-center 2xl:grid-cols-[1fr_minmax(0,45%)_1fr]">
-					<div className="flex items-center justify-between gap-4 2xl:col-start-2">
-						<div className="flex items-center gap-2">
-							<Link
-								href="/"
-								title="Startseite"
-								className="hover:cursor-pointer"
-							>
-								<div className="relative h-10 w-16">
-									<Image
-										src="/brand/logo-blackNav.png"
-										alt="Firmenlogo"
-										fill
-										sizes="64px"
-										className="object-contain dark:hidden"
-										priority
-									/>
-									<Image
-										src="/brand/logo-whiteNav.png"
-										alt="Firmenlogo"
-										fill
-										sizes="64px"
-										className="hidden object-contain dark:block"
-										priority
-									/>
-								</div>
-							</Link>
-
-							<nav aria-label="Hauptnavigation" className="flex items-center">
-								<QuickNav />
-							</nav>
-						</div>
+			<TooltipProvider delayDuration={TOOLTIP_DELAY_MS}>
+				<div className="px-4">
+					<div className="mx-auto grid h-14 w-full items-center 2xl:grid-cols-[1fr_minmax(0,45%)_1fr]">
+						<div className="flex items-center justify-between gap-4 2xl:col-start-2">
+							<div className="flex items-center gap-2">
+								<Link
+									href="/"
+									aria-label="Startseite"
+									className="hover:cursor-pointer"
+								>
+									<div className="relative h-10 w-16">
+										<Image
+											src="/brand/logo-blackNav.png"
+											alt="Firmenlogo"
+											fill
+											sizes="64px"
+											className="object-contain dark:hidden"
+											priority
+										/>
+										<Image
+											src="/brand/logo-whiteNav.png"
+											alt="Firmenlogo"
+											fill
+											sizes="64px"
+											className="hidden object-contain dark:block"
+											priority
+										/>
+									</div>
+								</Link>
 
-						{/* Actions group (clean separation + consistent spacing) */}
-						<div className="flex items-center gap-3">
-							<div className="flex items-center gap-3" aria-label="Aktionen">
-								<ThemeToggleButton />
-								<SessionIndicator />
+								<nav aria-label="Hauptnavigation" className="flex items-center">
+									<QuickNav />
+								</nav>
 							</div>
 
-							{/* Subtle separator between actions and user menu */}
-							<div className="h-6 w-px bg-border" aria-hidden="true" />
+							<div className="flex items-center gap-3">
+								<div className="flex items-center gap-3" aria-label="Aktionen">
+									<SessionIndicator />
+									<ThemeToggleButton />
+								</div>
+
+								<div className="h-6 w-px bg-border" aria-hidden="true" />
 
-							<UserStatus />
+								<UserStatus />
+							</div>
 						</div>
 					</div>
 				</div>
-			</div>
+			</TooltipProvider>
 		</header>
 	);
 }

+ 72 - 31
components/app-shell/UserStatus.jsx

@@ -2,8 +2,11 @@
 
 import React from "react";
 import Link from "next/link";
+import { usePathname } from "next/navigation";
 import { LifeBuoy, LogOut, User } from "lucide-react";
 
+import { cn } from "@/lib/utils";
+
 import { useAuth } from "@/components/auth/authContext";
 import { logout } from "@/lib/frontend/apiClient";
 import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
@@ -17,6 +20,11 @@ import {
 	DropdownMenuSeparator,
 	DropdownMenuTrigger,
 } from "@/components/ui/dropdown-menu";
+import {
+	Tooltip,
+	TooltipContent,
+	TooltipTrigger,
+} from "@/components/ui/tooltip";
 
 function formatRole(role) {
 	if (role === "branch") return "Niederlassung";
@@ -25,35 +33,54 @@ function formatRole(role) {
 	return role ? String(role) : "Unbekannt";
 }
 
-/**
- * Build a mailto link using encodeURIComponent (NOT URLSearchParams).
- *
- * Reason:
- * - Some mail clients treat "+" literally in mailto query strings.
- * - encodeURIComponent produces "%20" for spaces, which is handled reliably.
- */
-function buildSupportMailto({ user, currentUrl }) {
+function buildSupportMailto({ user, currentUrl, pathname, userAgent }) {
 	const to = "info@attus.de";
-	const subject = "Support – RHL Lieferscheine";
 
 	const roleLabel = user ? formatRole(user.role) : "Unbekannt";
 	const userLabel = user?.branchId
 		? `${roleLabel} (${user.branchId})`
 		: roleLabel;
 
+	const now = new Date();
+	const tz =
+		typeof Intl !== "undefined"
+			? Intl.DateTimeFormat().resolvedOptions().timeZone
+			: "";
+
+	const timestampLocal = now.toLocaleString("de-DE");
+	const timestampIso = now.toISOString();
+
+	const routeLine = pathname ? `Route: ${pathname}` : "Route: (unbekannt)";
 	const urlLine = currentUrl ? `URL: ${currentUrl}` : "URL: (bitte einfügen)";
+	const uaLine = userAgent
+		? `User-Agent: ${userAgent}`
+		: "User-Agent: (unbekannt)";
+	const timeLine = tz
+		? `Zeitpunkt: ${timestampLocal} (${tz})`
+		: `Zeitpunkt: ${timestampLocal}`;
+	const isoLine = `ISO: ${timestampIso}`;
+
+	const subject = user?.branchId
+		? `Support – RHL Lieferscheine (${user.branchId})`
+		: "Support – RHL Lieferscheine";
 
 	const body = [
 		"Hallo attus Support,",
 		"",
-		"bitte beschreibt hier kurz euer Anliegen:",
+		"bitte beschreibt hier kurz das Anliegen:",
 		"",
 		"- Was wollten Sie tun?",
 		"- Was ist passiert?",
 		"- (Optional) Screenshot / Zeitpunkt",
 		"",
-		urlLine,
+		"--- Kontext (bitte drin lassen) ---",
 		`Benutzer: ${userLabel}`,
+		routeLine,
+		urlLine,
+		timeLine,
+		isoLine,
+		uaLine,
+		"----------------------------------",
 		"",
 		"Vielen Dank.",
 	].join("\r\n");
@@ -64,6 +91,7 @@ function buildSupportMailto({ user, currentUrl }) {
 }
 
 export default function UserStatus() {
+	const pathname = usePathname() || "/";
 	const { status, user } = useAuth();
 
 	const isAuthenticated = status === "authenticated" && user;
@@ -78,14 +106,22 @@ export default function UserStatus() {
 	if (status === "error") text = "Fehler";
 
 	const currentUrl = typeof window !== "undefined" ? window.location.href : "";
+	const userAgent = typeof navigator !== "undefined" ? navigator.userAgent : "";
 
-	const supportMailto = buildSupportMailto({ user, currentUrl });
+	const supportMailto = buildSupportMailto({
+		user,
+		currentUrl,
+		pathname,
+		userAgent,
+	});
+
+	const isProfileActive =
+		pathname === "/profile" || pathname.startsWith("/profile/");
 
 	async function handleLogout() {
 		try {
 			await logout();
 		} catch (err) {
-			// Logout is idempotent; we still redirect for predictable UX.
 			console.error("[UserStatus] logout failed:", err);
 		}
 
@@ -95,19 +131,28 @@ export default function UserStatus() {
 
 	return (
 		<DropdownMenu>
-			<DropdownMenuTrigger asChild>
-				<Button
-					type="button"
-					variant="ghost"
-					size="sm"
-					className="gap-2"
-					aria-label="Benutzermenü öffnen"
-					title="Benutzermenü"
-				>
-					<User className="h-4 w-4" aria-hidden="true" />
-					<span className="hidden text-xs md:inline">{text}</span>
-				</Button>
-			</DropdownMenuTrigger>
+			<Tooltip>
+				<TooltipTrigger asChild>
+					<DropdownMenuTrigger asChild>
+						<Button
+							type="button"
+							variant="ghost"
+							size="sm"
+							aria-label="Benutzermenü öffnen"
+							className={cn(
+								"gap-2",
+								"px-2 md:px-3",
+								isProfileActive ? "bg-accent" : "",
+							)}
+						>
+							<User className="h-4 w-4" aria-hidden="true" />
+							<span className="hidden text-xs md:inline">{text}</span>
+						</Button>
+					</DropdownMenuTrigger>
+				</TooltipTrigger>
+
+				<TooltipContent side="bottom">Benutzermenü</TooltipContent>
+			</Tooltip>
 
 			<DropdownMenuContent align="end" className="min-w-56">
 				<DropdownMenuLabel>Benutzer</DropdownMenuLabel>
@@ -128,11 +173,7 @@ export default function UserStatus() {
 				</DropdownMenuItem>
 
 				<DropdownMenuItem asChild>
-					<a
-						href={supportMailto}
-						className="flex w-full items-center gap-2"
-						title="Support kontaktieren"
-					>
+					<a href={supportMailto} className="flex w-full items-center gap-2">
 						<LifeBuoy className="h-4 w-4" aria-hidden="true" />
 						Support
 					</a>