Просмотр исходного кода

RHL-043 feat(admin-users): add users sorting toolbar and query wiring

Code_Uwe 1 месяц назад
Родитель
Сommit
3de3a90e12

+ 32 - 3
components/admin/users/AdminUsersClient.jsx

@@ -10,9 +10,14 @@ import ExplorerEmpty from "@/components/explorer/states/ExplorerEmpty";
 import ForbiddenView from "@/components/system/ForbiddenView";
 
 import AdminUsersFilters from "@/components/admin/users/AdminUsersFilters";
+import AdminUsersTableToolbar from "@/components/admin/users/AdminUsersTableToolbar";
 import UsersTable from "@/components/admin/users/UsersTable";
 import CreateUserDialog from "@/components/admin/users/CreateUserDialog";
 import { normalizeBranchIdDraft } from "@/components/admin/users/usersUi";
+import {
+	ADMIN_USERS_SORT,
+	sortAdminUsers,
+} from "@/lib/frontend/admin/users/usersSorting";
 
 import { ApiClientError } from "@/lib/frontend/apiClient";
 import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
@@ -34,6 +39,7 @@ export default function AdminUsersClient() {
 		q: null,
 		role: null,
 		branchId: null,
+		sort: ADMIN_USERS_SORT.DEFAULT,
 	});
 
 	const {
@@ -70,6 +76,10 @@ export default function AdminUsersClient() {
 	}
 
 	const disabled = status === "loading" || isLoadingMore;
+	const effectiveSortMode = query.sort || ADMIN_USERS_SORT.DEFAULT;
+	const visibleItems = React.useMemo(() => {
+		return sortAdminUsers(items, effectiveSortMode);
+	}, [items, effectiveSortMode]);
 
 	function onDraftChange(patch) {
 		setDraft((prev) => ({ ...prev, ...(patch || {}) }));
@@ -80,12 +90,25 @@ export default function AdminUsersClient() {
 			q: draft.q.trim() ? draft.q.trim() : null,
 			role: draft.role.trim() ? draft.role.trim() : null,
 			branchId: normalizeBranchIdDraft(draft.branchId) || null,
+			sort: query.sort || ADMIN_USERS_SORT.DEFAULT,
 		});
 	}
 
 	function resetFilters() {
 		setDraft({ q: "", role: "", branchId: "" });
-		setQuery({ q: null, role: null, branchId: null });
+		setQuery((prev) => ({
+			q: null,
+			role: null,
+			branchId: null,
+			sort: prev.sort || ADMIN_USERS_SORT.DEFAULT,
+		}));
+	}
+
+	function onSortModeChange(nextSortMode) {
+		setQuery((prev) => ({
+			...prev,
+			sort: nextSortMode || ADMIN_USERS_SORT.DEFAULT,
+		}));
 	}
 
 	const actions = (
@@ -120,7 +143,13 @@ export default function AdminUsersClient() {
 						onDraftChange={onDraftChange}
 						onApply={applyFilters}
 						onReset={resetFilters}
-						loadedCount={items.length}
+						disabled={disabled}
+					/>
+
+					<AdminUsersTableToolbar
+						loadedCount={visibleItems.length}
+						sortMode={effectiveSortMode}
+						onSortModeChange={onSortModeChange}
 						disabled={disabled}
 					/>
 
@@ -145,7 +174,7 @@ export default function AdminUsersClient() {
 						/>
 					) : (
 						<UsersTable
-							items={items}
+							items={visibleItems}
 							disabled={disabled}
 							onUserUpdated={refresh}
 						/>

+ 0 - 9
components/admin/users/AdminUsersFilters.jsx

@@ -58,17 +58,12 @@ export default function AdminUsersFilters({
 	onDraftChange,
 	onApply,
 	onReset,
-	loadedCount = 0,
 	disabled,
 }) {
 	const q = draft?.q ?? "";
 	const role = draft?.role ?? "";
 	const branchId = draft?.branchId ?? "";
 
-	const safeLoadedCount = Number.isFinite(loadedCount)
-		? Math.max(0, loadedCount)
-		: 0;
-
 	return (
 		<div className="space-y-3">
 			<div className="grid gap-3 md:grid-cols-3">
@@ -116,10 +111,6 @@ export default function AdminUsersFilters({
 						Zurücksetzen
 					</Button>
 				</div>
-
-				<span className="rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground">
-					{safeLoadedCount} Benutzer geladen
-				</span>
 			</div>
 		</div>
 	);

+ 74 - 0
components/admin/users/AdminUsersTableToolbar.jsx

@@ -0,0 +1,74 @@
+"use client";
+
+import React from "react";
+import { SlidersHorizontal } from "lucide-react";
+
+import { ADMIN_USERS_SORT } from "@/lib/frontend/admin/users/usersSorting";
+
+import { Button } from "@/components/ui/button";
+import {
+	DropdownMenu,
+	DropdownMenuContent,
+	DropdownMenuLabel,
+	DropdownMenuRadioGroup,
+	DropdownMenuRadioItem,
+	DropdownMenuSeparator,
+	DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+
+export default function AdminUsersTableToolbar({
+	loadedCount = 0,
+	sortMode = ADMIN_USERS_SORT.DEFAULT,
+	onSortModeChange,
+	disabled = false,
+}) {
+	const safeLoadedCount = Number.isFinite(loadedCount)
+		? Math.max(0, loadedCount)
+		: 0;
+
+	return (
+		<div className="flex flex-wrap items-center justify-between gap-2">
+			<span className="rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground">
+				{safeLoadedCount} Benutzer geladen
+			</span>
+
+			<DropdownMenu>
+				<DropdownMenuTrigger asChild>
+					<Button
+						variant="outline"
+						size="sm"
+						type="button"
+						disabled={disabled}
+						title="Sortierung"
+					>
+						<SlidersHorizontal className="h-4 w-4" />
+						Sortierung
+					</Button>
+				</DropdownMenuTrigger>
+
+				<DropdownMenuContent align="end" className="min-w-[16rem]">
+					<DropdownMenuLabel>Sortierung</DropdownMenuLabel>
+					<DropdownMenuSeparator />
+
+					<DropdownMenuRadioGroup
+						value={sortMode}
+						onValueChange={(value) => onSortModeChange?.(value)}
+					>
+						<DropdownMenuRadioItem value={ADMIN_USERS_SORT.DEFAULT}>
+							Standard
+						</DropdownMenuRadioItem>
+
+						<DropdownMenuRadioItem value={ADMIN_USERS_SORT.ROLE_RIGHTS}>
+							Rolle (Rechte)
+						</DropdownMenuRadioItem>
+
+						<DropdownMenuRadioItem value={ADMIN_USERS_SORT.BRANCH_ASC}>
+							Niederlassung (NL)
+						</DropdownMenuRadioItem>
+					</DropdownMenuRadioGroup>
+				</DropdownMenuContent>
+			</DropdownMenu>
+		</div>
+	);
+}
+

+ 14 - 5
lib/frontend/admin/users/useAdminUsersQuery.js

@@ -2,6 +2,7 @@
 
 import React from "react";
 import { adminListUsers } from "@/lib/frontend/apiClient";
+import { ADMIN_USERS_SORT } from "@/lib/frontend/admin/users/usersSorting";
 
 function normalizeQuery(query) {
 	const q =
@@ -17,11 +18,16 @@ function normalizeQuery(query) {
 			? query.branchId.trim()
 			: null;
 
-	return { q, role, branchId };
+	const sort =
+		typeof query?.sort === "string" && query.sort.trim()
+			? query.sort.trim()
+			: ADMIN_USERS_SORT.DEFAULT;
+
+	return { q, role, branchId, sort };
 }
 
-function buildKey({ q, role, branchId, limit }) {
-	return `${q || ""}|${role || ""}|${branchId || ""}|${String(limit)}`;
+function buildKey({ q, role, branchId, sort, limit }) {
+	return `${q || ""}|${role || ""}|${branchId || ""}|${sort}|${String(limit)}`;
 }
 
 /**
@@ -38,7 +44,7 @@ export function useAdminUsersQuery({ query, limit = 50 }) {
 
 	const key = React.useMemo(() => {
 		return buildKey({ ...normalized, limit });
-	}, [normalized.q, normalized.role, normalized.branchId, limit]);
+	}, [normalized.q, normalized.role, normalized.branchId, normalized.sort, limit]);
 
 	const [status, setStatus] = React.useState("loading"); // loading|success|error
 	const [items, setItems] = React.useState([]);
@@ -79,6 +85,7 @@ export function useAdminUsersQuery({ query, limit = 50 }) {
 				q: normalized.q,
 				role: normalized.role,
 				branchId: normalized.branchId,
+				sort: normalized.sort,
 				limit,
 				cursor: null,
 			});
@@ -99,7 +106,7 @@ export function useAdminUsersQuery({ query, limit = 50 }) {
 			setError(err);
 			setStatus("error");
 		}
-	}, [normalized.q, normalized.role, normalized.branchId, limit]);
+	}, [normalized.q, normalized.role, normalized.branchId, normalized.sort, limit]);
 
 	React.useEffect(() => {
 		runFirstPage();
@@ -120,6 +127,7 @@ export function useAdminUsersQuery({ query, limit = 50 }) {
 				q: normalized.q,
 				role: normalized.role,
 				branchId: normalized.branchId,
+				sort: normalized.sort,
 				limit,
 				cursor: nextCursor,
 			});
@@ -149,6 +157,7 @@ export function useAdminUsersQuery({ query, limit = 50 }) {
 		normalized.q,
 		normalized.role,
 		normalized.branchId,
+		normalized.sort,
 		limit,
 	]);
 

+ 2 - 1
lib/frontend/apiClient.js

@@ -267,13 +267,14 @@ export function search(input, options) {
 }
 
 export function adminListUsers(input, options) {
-	const { q, role, branchId, limit, cursor } = input || {};
+	const { q, role, branchId, sort, limit, cursor } = input || {};
 	const params = new URLSearchParams();
 
 	if (typeof q === "string" && q.trim()) params.set("q", q.trim());
 	if (typeof role === "string" && role.trim()) params.set("role", role.trim());
 	if (typeof branchId === "string" && branchId.trim())
 		params.set("branchId", branchId.trim());
+	if (typeof sort === "string" && sort.trim()) params.set("sort", sort.trim());
 
 	if (limit !== undefined && limit !== null) {
 		const raw = String(limit).trim();

+ 19 - 0
lib/frontend/apiClient.test.js

@@ -150,6 +150,25 @@ describe("lib/frontend/apiClient", () => {
 		expect(init.method).toBe("PATCH");
 	});
 
+	it("adminListUsers includes sort in query string", async () => {
+		fetch.mockResolvedValue(
+			new Response(JSON.stringify({ items: [], nextCursor: null }), {
+				status: 200,
+				headers: { "Content-Type": "application/json" },
+			}),
+		);
+
+		await adminListUsers({ q: "dev", sort: "role_rights", limit: 50 });
+
+		const [url, init] = fetch.mock.calls[0];
+		const u = new URL(url, "http://localhost");
+		expect(u.pathname).toBe("/api/admin/users");
+		expect(u.searchParams.get("q")).toBe("dev");
+		expect(u.searchParams.get("sort")).toBe("role_rights");
+		expect(u.searchParams.get("limit")).toBe("50");
+		expect(init.method).toBe("GET");
+	});
+
 	it("adminDeleteUser calls DELETE /api/admin/users/:id", async () => {
 		fetch.mockResolvedValue(
 			new Response(JSON.stringify({ ok: true }), {