Browse Source

feat(profile): add ProfilePage component with user session details and email placeholder

Code_Uwe 1 week ago
parent
commit
136d508cec

+ 5 - 0
app/(protected)/profile/page.jsx

@@ -0,0 +1,5 @@
+import ProfilePage from "@/components/profile/ProfilePage";
+
+export default function ProfileRoute() {
+	return <ProfilePage />;
+}

+ 13 - 27
components/app-shell/QuickNav.jsx

@@ -40,6 +40,9 @@ const BRANCH_LIST_STATE = Object.freeze({
 	ERROR: "error",
 });
 
+const ACTIVE_NAV_BUTTON_CLASS =
+	"border-blue-600 bg-blue-50 hover:bg-blue-50 dark:border-blue-900 dark:bg-blue-950 dark:hover:bg-blue-950";
+
 export default function QuickNav() {
 	const router = useRouter();
 	const pathname = usePathname() || "/";
@@ -53,8 +56,6 @@ export default function QuickNav() {
 
 	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({
@@ -81,33 +82,26 @@ export default function QuickNav() {
 
 	const isKnownRouteBranch = React.useMemo(() => {
 		if (!routeBranch) return false;
-		if (!knownBranches) return false; // do not "trust" route until we know the branch list
+		if (!knownBranches) return false;
 		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;
@@ -124,7 +118,6 @@ export default function QuickNav() {
 			} 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 });
 			}
@@ -136,7 +129,6 @@ export default function QuickNav() {
 	}, [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;
 
@@ -148,8 +140,6 @@ export default function QuickNav() {
 	}, [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;
@@ -164,16 +154,11 @@ export default function QuickNav() {
 		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(
@@ -199,15 +184,10 @@ export default function QuickNav() {
 
 		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 ? (
@@ -240,7 +220,6 @@ export default function QuickNav() {
 
 									setSelectedBranch(value);
 									safeWriteLocalStorageBranch(STORAGE_KEY_LAST_BRANCH, value);
-
 									navigateToBranchKeepingContext(value);
 								}}
 							>
@@ -259,10 +238,11 @@ export default function QuickNav() {
 			) : null}
 
 			<Button
-				variant={explorerVariant}
+				variant="outline"
 				size="sm"
 				asChild
 				disabled={!canNavigate}
+				className={isExplorerActive ? ACTIVE_NAV_BUTTON_CLASS : ""}
 			>
 				<Link
 					href={canNavigate ? branchPath(effectiveBranch) : "#"}
@@ -274,7 +254,13 @@ export default function QuickNav() {
 				</Link>
 			</Button>
 
-			<Button variant={searchVariant} size="sm" asChild disabled={!canNavigate}>
+			<Button
+				variant="outline"
+				size="sm"
+				asChild
+				disabled={!canNavigate}
+				className={isSearchActive ? ACTIVE_NAV_BUTTON_CLASS : ""}
+			>
 				<Link
 					href={canNavigate ? searchPath(effectiveBranch) : "#"}
 					title="Suche öffnen"

+ 60 - 24
components/app-shell/UserStatus.jsx

@@ -2,7 +2,7 @@
 
 import React from "react";
 import Link from "next/link";
-import { KeyRound, LogOut, Settings, User } from "lucide-react";
+import { LifeBuoy, LogOut, User } from "lucide-react";
 
 import { useAuth } from "@/components/auth/authContext";
 import { logout } from "@/lib/frontend/apiClient";
@@ -18,26 +18,54 @@ import {
 	DropdownMenuTrigger,
 } from "@/components/ui/dropdown-menu";
 
+function formatRole(role) {
+	if (role === "branch") return "Niederlassung";
+	if (role === "admin") return "Admin";
+	if (role === "dev") return "Entwicklung";
+	return role ? String(role) : "Unbekannt";
+}
+
 /**
- * UserStatus (RHL-020)
- *
- * Updated responsibilities (RHL-032):
- * - Display minimal session info in the TopNav.
- * - Act as a user action menu trigger (settings/logout).
+ * Build a mailto link using encodeURIComponent (NOT URLSearchParams).
  *
- * UX rule:
- * - All user-facing strings must be German.
+ * Reason:
+ * - Some mail clients treat "+" literally in mailto query strings.
+ * - encodeURIComponent produces "%20" for spaces, which is handled reliably.
  */
+function buildSupportMailto({ user, currentUrl }) {
+	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 urlLine = currentUrl ? `URL: ${currentUrl}` : "URL: (bitte einfügen)";
+
+	const body = [
+		"Hallo attus Support,",
+		"",
+		"bitte beschreibt hier kurz euer Anliegen:",
+		"",
+		"- Was wollten Sie tun?",
+		"- Was ist passiert?",
+		"- (Optional) Screenshot / Zeitpunkt",
+		"",
+		urlLine,
+		`Benutzer: ${userLabel}`,
+		"",
+		"Vielen Dank.",
+	].join("\r\n");
+
+	return `mailto:${to}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(
+		body,
+	)}`;
+}
+
 export default function UserStatus() {
 	const { status, user } = useAuth();
 
-	function formatRole(role) {
-		if (role === "branch") return "Niederlassung";
-		if (role === "admin") return "Admin";
-		if (role === "dev") return "Entwicklung";
-		return role ? String(role) : "Unbekannt";
-	}
-
 	const isAuthenticated = status === "authenticated" && user;
 
 	let text = "Nicht geladen";
@@ -49,11 +77,15 @@ export default function UserStatus() {
 	if (status === "unauthenticated") text = "Abgemeldet";
 	if (status === "error") text = "Fehler";
 
+	const currentUrl = typeof window !== "undefined" ? window.location.href : "";
+
+	const supportMailto = buildSupportMailto({ user, currentUrl });
+
 	async function handleLogout() {
 		try {
 			await logout();
 		} catch (err) {
-			// Logout is idempotent; we still redirect to login for predictable UX.
+			// Logout is idempotent; we still redirect for predictable UX.
 			console.error("[UserStatus] logout failed:", err);
 		}
 
@@ -73,8 +105,6 @@ export default function UserStatus() {
 					title="Benutzermenü"
 				>
 					<User className="h-4 w-4" aria-hidden="true" />
-
-					{/* Keep the label visible on md+ like before, but keep menu accessible on mobile */}
 					<span className="hidden text-xs md:inline">{text}</span>
 				</Button>
 			</DropdownMenuTrigger>
@@ -91,15 +121,21 @@ export default function UserStatus() {
 				<DropdownMenuSeparator />
 
 				<DropdownMenuItem asChild disabled={!isAuthenticated}>
-					<Link href="/settings" className="flex w-full items-center gap-2">
-						<Settings className="h-4 w-4" aria-hidden="true" />
-						Einstellungen
+					<Link href="/profile" className="flex w-full items-center gap-2">
+						<User className="h-4 w-4" aria-hidden="true" />
+						Profil
 					</Link>
 				</DropdownMenuItem>
 
-				<DropdownMenuItem disabled title="Kommt später">
-					<KeyRound className="h-4 w-4" aria-hidden="true" />
-					Passwort ändern
+				<DropdownMenuItem asChild>
+					<a
+						href={supportMailto}
+						className="flex w-full items-center gap-2"
+						title="Support kontaktieren"
+					>
+						<LifeBuoy className="h-4 w-4" aria-hidden="true" />
+						Support
+					</a>
 				</DropdownMenuItem>
 
 				<DropdownMenuSeparator />

+ 127 - 0
components/profile/ProfilePage.jsx

@@ -0,0 +1,127 @@
+"use client";
+
+import React from "react";
+import { useAuth } from "@/components/auth/authContext";
+
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+	Card,
+	CardHeader,
+	CardTitle,
+	CardDescription,
+	CardContent,
+	CardFooter,
+} from "@/components/ui/card";
+
+function formatRole(role) {
+	if (role === "branch") return "Niederlassung";
+	if (role === "admin") return "Admin";
+	if (role === "dev") return "Entwicklung";
+	return role ? String(role) : "Unbekannt";
+}
+
+export default function ProfilePage() {
+	const { status, user } = useAuth();
+
+	const isAuthenticated = status === "authenticated" && user;
+
+	const roleLabel = isAuthenticated ? formatRole(user.role) : "—";
+	const branchLabel = isAuthenticated ? user.branchId || "—" : "—";
+	const userIdLabel = isAuthenticated ? user.userId || "—" : "—";
+
+	return (
+		<div className="space-y-4">
+			<div className="space-y-1">
+				<h1 className="text-2xl font-semibold tracking-tight">Profil</h1>
+				<p className="text-sm text-muted-foreground">
+					Konto- und Zugangseinstellungen.
+				</p>
+			</div>
+
+			<Card>
+				<CardHeader>
+					<CardTitle>Konto</CardTitle>
+					<CardDescription>Aktuelle Sitzungsinformationen.</CardDescription>
+				</CardHeader>
+
+				<CardContent className="grid gap-3 text-sm">
+					<div className="flex items-center justify-between gap-4">
+						<span className="text-muted-foreground">Rolle</span>
+						<span>{roleLabel}</span>
+					</div>
+
+					<div className="flex items-center justify-between gap-4">
+						<span className="text-muted-foreground">Niederlassung</span>
+						<span>{branchLabel}</span>
+					</div>
+
+					<div className="flex items-center justify-between gap-4">
+						<span className="text-muted-foreground">User ID</span>
+						<span className="truncate">{userIdLabel}</span>
+					</div>
+				</CardContent>
+			</Card>
+
+			<Card>
+				<CardHeader>
+					<CardTitle>E-Mail</CardTitle>
+					<CardDescription>
+						Die Änderung der E-Mail-Adresse wird in einem späteren Ticket
+						aktiviert.
+					</CardDescription>
+				</CardHeader>
+
+				<CardContent className="grid gap-2">
+					<Label htmlFor="email">E-Mail-Adresse</Label>
+					<Input
+						id="email"
+						type="email"
+						placeholder="name@firma.de"
+						disabled
+						aria-disabled="true"
+					/>
+				</CardContent>
+
+				<CardFooter className="flex justify-end">
+					<Button
+						type="button"
+						disabled
+						aria-disabled="true"
+						title="Kommt später"
+					>
+						Speichern
+					</Button>
+				</CardFooter>
+			</Card>
+
+			<Card>
+				<CardHeader>
+					<CardTitle>Passwort</CardTitle>
+					<CardDescription>
+						Die Passwort-Änderung wird in einem separaten Ticket umgesetzt.
+					</CardDescription>
+				</CardHeader>
+
+				<CardFooter className="flex justify-end">
+					<Button
+						type="button"
+						disabled
+						aria-disabled="true"
+						title="Kommt später"
+					>
+						Passwort ändern
+					</Button>
+				</CardFooter>
+			</Card>
+
+			{!isAuthenticated ? (
+				<p className="text-xs text-muted-foreground">
+					Hinweis: Profilfunktionen sind nur verfügbar, wenn Sie angemeldet
+					sind.
+				</p>
+			) : null}
+		</div>
+	);
+}