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

RHL-043 feat(admin-users): add user-management ux helper utilities

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

+ 3 - 2
components/admin/users/usersUi.js

@@ -1,3 +1,5 @@
+import { normalizeBranchIdInput } from "@/lib/frontend/admin/users/userManagementUx";
+
 export const ROLE_LABELS_DE = Object.freeze({
 	branch: "Niederlassung",
 	admin: "Admin",
@@ -23,6 +25,5 @@ export function formatDateTimeDe(iso) {
 }
 
 export function normalizeBranchIdDraft(value) {
-	if (typeof value !== "string") return "";
-	return value.trim().toUpperCase();
+	return normalizeBranchIdInput(value);
 }

+ 100 - 0
lib/frontend/admin/users/userManagementUx.js

@@ -0,0 +1,100 @@
+const BRANCH_ID_RE = /^NL\d+$/;
+const BRANCH_ID_CAPTURE_RE = /^NL(\d+)$/i;
+
+function normalizeComparableText(value) {
+	return String(value ?? "")
+		.trim()
+		.toLowerCase();
+}
+
+export function normalizeUsernameForConfirmation(value) {
+	return normalizeComparableText(value);
+}
+
+export function isUsernameConfirmationMatch({
+	expectedUsername,
+	typedUsername,
+}) {
+	const expected = normalizeUsernameForConfirmation(expectedUsername);
+	const typed = normalizeUsernameForConfirmation(typedUsername);
+	return Boolean(expected) && expected === typed;
+}
+
+export function normalizeBranchIdInput(value) {
+	return String(value ?? "")
+		.trim()
+		.toUpperCase();
+}
+
+export function normalizeBranchNumberInput(value) {
+	const raw = String(value ?? "").trim();
+	if (!raw) return "";
+
+	const digits = raw.replace(/\D+/g, "");
+	if (!digits) return "";
+
+	// Keep "0" as a valid numeric value but collapse leading zeros.
+	return digits.replace(/^0+(?=\d)/, "");
+}
+
+export function formatBranchIdFromNumberInput(value) {
+	const numberPart = normalizeBranchNumberInput(value);
+	if (!numberPart) return "";
+	return `NL${numberPart.padStart(2, "0")}`;
+}
+
+export function extractBranchNumberInputFromBranchId(branchId) {
+	const match = BRANCH_ID_CAPTURE_RE.exec(normalizeBranchIdInput(branchId));
+	if (!match) return "";
+	return match[1];
+}
+
+export function isValidBranchIdFormat(value) {
+	return BRANCH_ID_RE.test(normalizeBranchIdInput(value));
+}
+
+/**
+ * Decides branch existence UX state for branch-role create/edit forms.
+ *
+ * Rules:
+ * - Only relevant when role is "branch" and a branchId was entered.
+ * - If branch list is available (status=ready), unknown branchIds block submit.
+ * - If branch list failed to load (status=error), do not block submit (fail-open).
+ */
+export function evaluateBranchExistence({
+	role,
+	branchId,
+	branchesStatus,
+	availableBranchIds,
+}) {
+	const isBranchRole = String(role ?? "").trim() === "branch";
+	const normalizedBranchId = normalizeBranchIdInput(branchId);
+	const hasBranchId = Boolean(normalizedBranchId);
+
+	const listReady =
+		branchesStatus === "ready" && Array.isArray(availableBranchIds);
+	const listError = branchesStatus === "error";
+
+	const normalizedAvailable = listReady
+		? availableBranchIds.map(normalizeBranchIdInput).filter(Boolean)
+		: [];
+
+	const branchExists =
+		isBranchRole && hasBranchId && listReady
+			? normalizedAvailable.includes(normalizedBranchId)
+			: null;
+
+	const hasUnknownBranch =
+		isBranchRole && hasBranchId && listReady && branchExists === false;
+
+	return {
+		isBranchRole,
+		normalizedBranchId,
+		hasBranchId,
+		listReady,
+		listError,
+		branchExists,
+		hasUnknownBranch,
+		shouldBlockSubmit: Boolean(hasUnknownBranch),
+	};
+}

+ 128 - 0
lib/frontend/admin/users/userManagementUx.test.js

@@ -0,0 +1,128 @@
+/* @vitest-environment node */
+
+import { describe, it, expect } from "vitest";
+import {
+	normalizeUsernameForConfirmation,
+	isUsernameConfirmationMatch,
+	normalizeBranchIdInput,
+	normalizeBranchNumberInput,
+	formatBranchIdFromNumberInput,
+	extractBranchNumberInputFromBranchId,
+	isValidBranchIdFormat,
+	evaluateBranchExistence,
+} from "./userManagementUx.js";
+
+describe("lib/frontend/admin/users/userManagementUx", () => {
+	describe("username confirmation", () => {
+		it("normalizes to lowercase and trims", () => {
+			expect(normalizeUsernameForConfirmation("  Alice.Admin  ")).toBe(
+				"alice.admin",
+			);
+		});
+
+		it("matches typed username case-insensitively after trimming", () => {
+			expect(
+				isUsernameConfirmationMatch({
+					expectedUsername: "branch.user",
+					typedUsername: "  BRANCH.USER ",
+				}),
+			).toBe(true);
+		});
+
+		it("returns false for empty or mismatching values", () => {
+			expect(
+				isUsernameConfirmationMatch({
+					expectedUsername: "branch.user",
+					typedUsername: "other.user",
+				}),
+			).toBe(false);
+
+			expect(
+				isUsernameConfirmationMatch({
+					expectedUsername: "",
+					typedUsername: "",
+				}),
+			).toBe(false);
+		});
+	});
+
+	describe("branch formatting", () => {
+		it("normalizes branchId input to uppercase + trim", () => {
+			expect(normalizeBranchIdInput(" nl01 ")).toBe("NL01");
+		});
+
+		it("normalizes numeric branch input and strips non-digits", () => {
+			expect(normalizeBranchNumberInput(" 001 ")).toBe("1");
+			expect(normalizeBranchNumberInput(" 32a ")).toBe("32");
+			expect(normalizeBranchNumberInput("000")).toBe("0");
+			expect(normalizeBranchNumberInput("abc")).toBe("");
+		});
+
+		it("formats NL branchId with 2+ digit policy", () => {
+			expect(formatBranchIdFromNumberInput("1")).toBe("NL01");
+			expect(formatBranchIdFromNumberInput("32")).toBe("NL32");
+			expect(formatBranchIdFromNumberInput("200")).toBe("NL200");
+		});
+
+		it("extracts numeric branch input from an existing branchId", () => {
+			expect(extractBranchNumberInputFromBranchId("NL01")).toBe("01");
+			expect(extractBranchNumberInputFromBranchId("nl200")).toBe("200");
+			expect(extractBranchNumberInputFromBranchId("XX1")).toBe("");
+		});
+
+		it("validates branchId format", () => {
+			expect(isValidBranchIdFormat("NL01")).toBe(true);
+			expect(isValidBranchIdFormat("nl200")).toBe(true);
+			expect(isValidBranchIdFormat("XX1")).toBe(false);
+			expect(isValidBranchIdFormat("NL")).toBe(false);
+		});
+	});
+
+	describe("branch existence decision", () => {
+		it("blocks submit for unknown branch only when list is ready", () => {
+			expect(
+				evaluateBranchExistence({
+					role: "branch",
+					branchId: "NL99",
+					branchesStatus: "ready",
+					availableBranchIds: ["NL01", "NL02"],
+				}).shouldBlockSubmit,
+			).toBe(true);
+		});
+
+		it("allows submit when branch exists in ready list", () => {
+			expect(
+				evaluateBranchExistence({
+					role: "branch",
+					branchId: "NL02",
+					branchesStatus: "ready",
+					availableBranchIds: ["NL01", "NL02"],
+				}).shouldBlockSubmit,
+			).toBe(false);
+		});
+
+		it("is fail-open when list fetch failed", () => {
+			const result = evaluateBranchExistence({
+				role: "branch",
+				branchId: "NL99",
+				branchesStatus: "error",
+				availableBranchIds: null,
+			});
+
+			expect(result.listError).toBe(true);
+			expect(result.shouldBlockSubmit).toBe(false);
+		});
+
+		it("is irrelevant for non-branch roles", () => {
+			const result = evaluateBranchExistence({
+				role: "admin",
+				branchId: "NL99",
+				branchesStatus: "ready",
+				availableBranchIds: ["NL01"],
+			});
+
+			expect(result.isBranchRole).toBe(false);
+			expect(result.shouldBlockSubmit).toBe(false);
+		});
+	});
+});