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

RHL-043 feat(admin-users): add deterministic users sorting helpers

Code_Uwe 1 месяц назад
Родитель
Сommit
042cfacf26
2 измененных файлов с 219 добавлено и 0 удалено
  1. 109 0
      lib/frontend/admin/users/usersSorting.js
  2. 110 0
      lib/frontend/admin/users/usersSorting.test.js

+ 109 - 0
lib/frontend/admin/users/usersSorting.js

@@ -0,0 +1,109 @@
+export const ADMIN_USERS_SORT = Object.freeze({
+	DEFAULT: "default",
+	ROLE_RIGHTS: "role_rights",
+	BRANCH_ASC: "branch_asc",
+});
+
+const ROLE_RANK = Object.freeze({
+	superadmin: 4,
+	dev: 3,
+	admin: 2,
+	branch: 1,
+});
+
+function normalizeRole(value) {
+	return String(value ?? "")
+		.trim()
+		.toLowerCase();
+}
+
+function toRoleRank(value) {
+	const role = normalizeRole(value);
+	return ROLE_RANK[role] ?? 0;
+}
+
+function toBranchNumber(branchId) {
+	const raw = String(branchId ?? "").trim();
+	if (!raw) return null;
+
+	const match = /^NL(\d+)$/i.exec(raw);
+	if (!match) return null;
+
+	const n = Number(match[1]);
+	return Number.isInteger(n) ? n : null;
+}
+
+function compareBranchAscNullLast(a, b) {
+	const an = toBranchNumber(a);
+	const bn = toBranchNumber(b);
+
+	if (an !== null && bn !== null) return an - bn;
+	if (an === null && bn !== null) return 1;
+	if (an !== null && bn === null) return -1;
+
+	return String(a ?? "").localeCompare(String(b ?? ""), "de", {
+		sensitivity: "base",
+	});
+}
+
+function compareUsernamesAsc(a, b) {
+	return String(a ?? "").localeCompare(String(b ?? ""), "de", {
+		sensitivity: "base",
+	});
+}
+
+function compareIdsAsc(a, b) {
+	return String(a ?? "").localeCompare(String(b ?? ""), "en");
+}
+
+export function normalizeAdminUsersSortMode(value) {
+	const mode = String(value ?? "").trim();
+	if (!mode) return ADMIN_USERS_SORT.DEFAULT;
+
+	if (Object.values(ADMIN_USERS_SORT).includes(mode)) {
+		return mode;
+	}
+
+	return null;
+}
+
+export function compareUsersByRoleRights(a, b) {
+	const roleCmp = toRoleRank(b?.role) - toRoleRank(a?.role);
+	if (roleCmp !== 0) return roleCmp;
+
+	const branchCmp = compareBranchAscNullLast(a?.branchId, b?.branchId);
+	if (branchCmp !== 0) return branchCmp;
+
+	const usernameCmp = compareUsernamesAsc(a?.username, b?.username);
+	if (usernameCmp !== 0) return usernameCmp;
+
+	return compareIdsAsc(a?.id ?? a?._id, b?.id ?? b?._id);
+}
+
+export function compareUsersByBranchAsc(a, b) {
+	const branchCmp = compareBranchAscNullLast(a?.branchId, b?.branchId);
+	if (branchCmp !== 0) return branchCmp;
+
+	const roleCmp = toRoleRank(b?.role) - toRoleRank(a?.role);
+	if (roleCmp !== 0) return roleCmp;
+
+	const usernameCmp = compareUsernamesAsc(a?.username, b?.username);
+	if (usernameCmp !== 0) return usernameCmp;
+
+	return compareIdsAsc(a?.id ?? a?._id, b?.id ?? b?._id);
+}
+
+export function sortAdminUsers(items, sortMode) {
+	const list = Array.isArray(items) ? [...items] : [];
+
+	if (sortMode === ADMIN_USERS_SORT.ROLE_RIGHTS) {
+		return list.sort(compareUsersByRoleRights);
+	}
+
+	if (sortMode === ADMIN_USERS_SORT.BRANCH_ASC) {
+		return list.sort(compareUsersByBranchAsc);
+	}
+
+	return list;
+}
+

+ 110 - 0
lib/frontend/admin/users/usersSorting.test.js

@@ -0,0 +1,110 @@
+/* @vitest-environment node */
+
+import { describe, it, expect } from "vitest";
+import {
+	ADMIN_USERS_SORT,
+	normalizeAdminUsersSortMode,
+	sortAdminUsers,
+} from "./usersSorting.js";
+
+function mapRows(rows) {
+	return rows.map((x) => ({
+		id: x.id,
+		role: x.role,
+		branchId: x.branchId,
+		username: x.username,
+	}));
+}
+
+describe("lib/frontend/admin/users/usersSorting", () => {
+	it("normalizes known sort modes and rejects invalid values", () => {
+		expect(normalizeAdminUsersSortMode(undefined)).toBe(
+			ADMIN_USERS_SORT.DEFAULT,
+		);
+		expect(normalizeAdminUsersSortMode("")).toBe(ADMIN_USERS_SORT.DEFAULT);
+		expect(normalizeAdminUsersSortMode(ADMIN_USERS_SORT.ROLE_RIGHTS)).toBe(
+			ADMIN_USERS_SORT.ROLE_RIGHTS,
+		);
+		expect(normalizeAdminUsersSortMode("nope")).toBe(null);
+	});
+
+	it("sorts by role rights with deterministic tie-breakers", () => {
+		const items = [
+			{
+				id: "6",
+				role: "admin",
+				branchId: null,
+				username: "z-admin",
+			},
+			{
+				id: "1",
+				role: "branch",
+				branchId: "NL10",
+				username: "branch-z",
+			},
+			{
+				id: "2",
+				role: "branch",
+				branchId: "NL2",
+				username: "branch-a",
+			},
+			{
+				id: "3",
+				role: "dev",
+				branchId: null,
+				username: "dev",
+			},
+			{
+				id: "4",
+				role: "superadmin",
+				branchId: null,
+				username: "root",
+			},
+			{
+				id: "5",
+				role: "admin",
+				branchId: null,
+				username: "a-admin",
+			},
+		];
+
+		const out = sortAdminUsers(items, ADMIN_USERS_SORT.ROLE_RIGHTS);
+
+		expect(mapRows(out)).toEqual([
+			{ id: "4", role: "superadmin", branchId: null, username: "root" },
+			{ id: "3", role: "dev", branchId: null, username: "dev" },
+			{ id: "5", role: "admin", branchId: null, username: "a-admin" },
+			{ id: "6", role: "admin", branchId: null, username: "z-admin" },
+			{ id: "2", role: "branch", branchId: "NL2", username: "branch-a" },
+			{ id: "1", role: "branch", branchId: "NL10", username: "branch-z" },
+		]);
+	});
+
+	it("sorts by branch asc (numeric) with null branch at the end", () => {
+		const items = [
+			{ id: "1", role: "branch", branchId: "NL10", username: "b1" },
+			{ id: "2", role: "branch", branchId: "NL2", username: "b2" },
+			{ id: "3", role: "admin", branchId: null, username: "admin" },
+			{ id: "4", role: "superadmin", branchId: null, username: "super" },
+			{ id: "5", role: "branch", branchId: "NL01", username: "b0" },
+		];
+
+		const out = sortAdminUsers(items, ADMIN_USERS_SORT.BRANCH_ASC);
+
+		expect(mapRows(out)).toEqual([
+			{ id: "5", role: "branch", branchId: "NL01", username: "b0" },
+			{ id: "2", role: "branch", branchId: "NL2", username: "b2" },
+			{ id: "1", role: "branch", branchId: "NL10", username: "b1" },
+			{ id: "4", role: "superadmin", branchId: null, username: "super" },
+			{ id: "3", role: "admin", branchId: null, username: "admin" },
+		]);
+	});
+
+	it("returns a shallow copy for default mode", () => {
+		const items = [{ id: "1" }, { id: "2" }];
+		const out = sortAdminUsers(items, ADMIN_USERS_SORT.DEFAULT);
+		expect(out).toEqual(items);
+		expect(out).not.toBe(items);
+	});
+});
+