|
|
@@ -0,0 +1,359 @@
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+// Ordner: components/admin/users/edit-user
|
|
|
+// Datei: useEditUserDialog.js
|
|
|
+// Relativer Pfad: components/admin/users/edit-user/useEditUserDialog.js
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
+"use client";
|
|
|
+
|
|
|
+import React from "react";
|
|
|
+
|
|
|
+import { adminUpdateUser, ApiClientError } from "@/lib/frontend/apiClient";
|
|
|
+import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
|
|
|
+import {
|
|
|
+ notifySuccess,
|
|
|
+ notifyError,
|
|
|
+ notifyApiError,
|
|
|
+ notifyInfo,
|
|
|
+} from "@/lib/frontend/ui/toast";
|
|
|
+
|
|
|
+import { normalizeBranchIdDraft } from "@/components/admin/users/usersUi";
|
|
|
+import {
|
|
|
+ EDIT_ROLE_OPTIONS,
|
|
|
+ EMAIL_RE,
|
|
|
+ BRANCH_RE,
|
|
|
+ normalizeUsername,
|
|
|
+ normalizeEmail,
|
|
|
+} from "@/components/admin/users/edit-user/editUserUtils";
|
|
|
+
|
|
|
+function isNonEmptyString(value) {
|
|
|
+ return typeof value === "string" && value.trim().length > 0;
|
|
|
+}
|
|
|
+
|
|
|
+function buildInitialFormFromUser(user) {
|
|
|
+ return {
|
|
|
+ username: typeof user?.username === "string" ? user.username : "",
|
|
|
+ email: typeof user?.email === "string" ? user.email : "",
|
|
|
+ role: typeof user?.role === "string" ? user.role : "branch",
|
|
|
+ branchId: typeof user?.branchId === "string" ? user.branchId : "",
|
|
|
+ mustChangePassword: Boolean(user?.mustChangePassword),
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function validateClient(form) {
|
|
|
+ const username = normalizeUsername(form?.username);
|
|
|
+ const email = normalizeEmail(form?.email);
|
|
|
+ const role = String(form?.role || "").trim();
|
|
|
+
|
|
|
+ const isKnownRole = EDIT_ROLE_OPTIONS.some((x) => x.value === role);
|
|
|
+
|
|
|
+ const branchId =
|
|
|
+ role === "branch" ? normalizeBranchIdDraft(form?.branchId || "") : "";
|
|
|
+
|
|
|
+ if (!username) return { title: "Benutzername fehlt.", description: null };
|
|
|
+ if (!email) return { title: "E-Mail fehlt.", description: null };
|
|
|
+
|
|
|
+ if (!EMAIL_RE.test(email)) {
|
|
|
+ return {
|
|
|
+ title: "E-Mail ist ungültig.",
|
|
|
+ description: "Bitte prüfen Sie die Eingabe.",
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!role || !isKnownRole) {
|
|
|
+ return { title: "Rolle fehlt.", description: null };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (role === "branch") {
|
|
|
+ if (!branchId) {
|
|
|
+ return {
|
|
|
+ title: "Niederlassung fehlt.",
|
|
|
+ description: "Für Niederlassungs-User ist eine NL erforderlich.",
|
|
|
+ };
|
|
|
+ }
|
|
|
+ if (!BRANCH_RE.test(branchId)) {
|
|
|
+ return {
|
|
|
+ title: "Niederlassung ist ungültig.",
|
|
|
+ description: "Format: NL01, NL02, ...",
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
+function extractDuplicateField(details) {
|
|
|
+ if (!details || typeof details !== "object") return null;
|
|
|
+ if (typeof details.field === "string" && details.field.trim()) {
|
|
|
+ return details.field.trim();
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
+function mapDuplicateFieldToGermanMessage(field) {
|
|
|
+ if (field === "username") {
|
|
|
+ return {
|
|
|
+ title: "Benutzername existiert bereits.",
|
|
|
+ description: "Bitte wählen Sie einen anderen Benutzernamen.",
|
|
|
+ };
|
|
|
+ }
|
|
|
+ if (field === "email") {
|
|
|
+ return {
|
|
|
+ title: "E-Mail existiert bereits.",
|
|
|
+ description: "Bitte wählen Sie eine andere E-Mail-Adresse.",
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
+function buildNormalizedForm(form) {
|
|
|
+ const role = String(form?.role || "").trim();
|
|
|
+
|
|
|
+ return {
|
|
|
+ username: normalizeUsername(form?.username),
|
|
|
+ email: normalizeEmail(form?.email),
|
|
|
+ role,
|
|
|
+ branchId:
|
|
|
+ role === "branch" ? normalizeBranchIdDraft(form?.branchId || "") : "",
|
|
|
+ mustChangePassword: Boolean(form?.mustChangePassword),
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function buildPatch({ user, form }) {
|
|
|
+ const initial = buildNormalizedForm(buildInitialFormFromUser(user));
|
|
|
+ const current = buildNormalizedForm(form);
|
|
|
+
|
|
|
+ const patch = {};
|
|
|
+
|
|
|
+ if (current.username && current.username !== initial.username) {
|
|
|
+ patch.username = current.username;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (current.email && current.email !== initial.email) {
|
|
|
+ patch.email = current.email;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (current.role && current.role !== initial.role) {
|
|
|
+ patch.role = current.role;
|
|
|
+ }
|
|
|
+
|
|
|
+ // branchId handling:
|
|
|
+ // - Only meaningful for role=branch
|
|
|
+ // - If role becomes non-branch, backend will clear branchId automatically.
|
|
|
+ if (current.role === "branch") {
|
|
|
+ // If role is branch (either unchanged or changed), enforce branchId in patch when different
|
|
|
+ if (current.branchId !== initial.branchId) {
|
|
|
+ patch.branchId = current.branchId;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // If user was branch before and we did not change role explicitly (rare),
|
|
|
+ // we do not auto-clear here. Backend will enforce consistency based on role updates.
|
|
|
+ }
|
|
|
+
|
|
|
+ if (current.mustChangePassword !== initial.mustChangePassword) {
|
|
|
+ patch.mustChangePassword = current.mustChangePassword;
|
|
|
+ }
|
|
|
+
|
|
|
+ return patch;
|
|
|
+}
|
|
|
+
|
|
|
+export function useEditUserDialog({ user, disabled = false, onUpdated } = {}) {
|
|
|
+ const [open, setOpen] = React.useState(false);
|
|
|
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
|
+
|
|
|
+ const [form, setForm] = React.useState(() => buildInitialFormFromUser(user));
|
|
|
+ const [error, setError] = React.useState(null);
|
|
|
+
|
|
|
+ const effectiveDisabled = Boolean(disabled || isSubmitting || !user?.id);
|
|
|
+
|
|
|
+ const patchPreview = React.useMemo(() => {
|
|
|
+ return buildPatch({ user, form });
|
|
|
+ }, [
|
|
|
+ user?.id,
|
|
|
+ user?.username,
|
|
|
+ user?.email,
|
|
|
+ user?.role,
|
|
|
+ user?.branchId,
|
|
|
+ user?.mustChangePassword,
|
|
|
+ form?.username,
|
|
|
+ form?.email,
|
|
|
+ form?.role,
|
|
|
+ form?.branchId,
|
|
|
+ form?.mustChangePassword,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ const canSubmit = React.useMemo(() => {
|
|
|
+ return !effectiveDisabled && Object.keys(patchPreview).length > 0;
|
|
|
+ }, [effectiveDisabled, patchPreview]);
|
|
|
+
|
|
|
+ const setPatch = React.useCallback((patch) => {
|
|
|
+ setForm((prev) => ({ ...prev, ...(patch || {}) }));
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const resetForm = React.useCallback(() => {
|
|
|
+ setForm(buildInitialFormFromUser(user));
|
|
|
+ setError(null);
|
|
|
+ }, [user]);
|
|
|
+
|
|
|
+ const redirectToLoginExpired = React.useCallback(() => {
|
|
|
+ const next =
|
|
|
+ typeof window !== "undefined"
|
|
|
+ ? `${window.location.pathname}${window.location.search}`
|
|
|
+ : "/admin/users";
|
|
|
+
|
|
|
+ window.location.replace(
|
|
|
+ buildLoginUrl({ reason: LOGIN_REASONS.EXPIRED, next }),
|
|
|
+ );
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const handleOpenChange = React.useCallback(
|
|
|
+ (nextOpen) => {
|
|
|
+ setOpen(nextOpen);
|
|
|
+
|
|
|
+ // On open -> initialize from current user snapshot.
|
|
|
+ // On close -> clear error and reset form so reopening is always clean.
|
|
|
+ if (nextOpen) {
|
|
|
+ setForm(buildInitialFormFromUser(user));
|
|
|
+ setError(null);
|
|
|
+ } else {
|
|
|
+ resetForm();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ [user, resetForm],
|
|
|
+ );
|
|
|
+
|
|
|
+ const handleSubmit = React.useCallback(
|
|
|
+ async (e) => {
|
|
|
+ e?.preventDefault?.();
|
|
|
+ if (effectiveDisabled) return;
|
|
|
+
|
|
|
+ setError(null);
|
|
|
+
|
|
|
+ const clientErr = validateClient(form);
|
|
|
+ if (clientErr) {
|
|
|
+ setError(clientErr);
|
|
|
+ notifyError({
|
|
|
+ title: clientErr.title,
|
|
|
+ description: clientErr.description,
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const patch = buildPatch({ user, form });
|
|
|
+
|
|
|
+ if (Object.keys(patch).length === 0) {
|
|
|
+ notifyInfo({
|
|
|
+ title: "Keine Änderungen",
|
|
|
+ description: "Es wurden keine Felder geändert.",
|
|
|
+ });
|
|
|
+ setOpen(false);
|
|
|
+ resetForm();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ setIsSubmitting(true);
|
|
|
+
|
|
|
+ try {
|
|
|
+ await adminUpdateUser(String(user.id), patch);
|
|
|
+
|
|
|
+ notifySuccess({
|
|
|
+ title: "Benutzer aktualisiert",
|
|
|
+ description: `Benutzer "${normalizeUsername(form.username)}" wurde gespeichert.`,
|
|
|
+ });
|
|
|
+
|
|
|
+ setOpen(false);
|
|
|
+ resetForm();
|
|
|
+
|
|
|
+ if (typeof onUpdated === "function") onUpdated();
|
|
|
+ } catch (err) {
|
|
|
+ if (err instanceof ApiClientError) {
|
|
|
+ if (err.code === "AUTH_UNAUTHENTICATED") {
|
|
|
+ notifyApiError(err);
|
|
|
+ redirectToLoginExpired();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (err.code === "USER_NOT_FOUND") {
|
|
|
+ const mapped = {
|
|
|
+ title: "Benutzer nicht gefunden.",
|
|
|
+ description:
|
|
|
+ "Der Benutzer existiert nicht (mehr). Bitte aktualisieren Sie die Liste.",
|
|
|
+ };
|
|
|
+ setError(mapped);
|
|
|
+ notifyError(mapped);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (err.code === "VALIDATION_MISSING_FIELD") {
|
|
|
+ const fields = err.details?.fields;
|
|
|
+ if (Array.isArray(fields) && fields.includes("branchId")) {
|
|
|
+ const mapped = {
|
|
|
+ title: "Niederlassung fehlt.",
|
|
|
+ description:
|
|
|
+ "Für Niederlassungs-User ist eine Niederlassung erforderlich.",
|
|
|
+ };
|
|
|
+ setError(mapped);
|
|
|
+ notifyError(mapped);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (err.code === "VALIDATION_INVALID_FIELD") {
|
|
|
+ const field = extractDuplicateField(err.details);
|
|
|
+ const mapped = mapDuplicateFieldToGermanMessage(field);
|
|
|
+
|
|
|
+ if (mapped) {
|
|
|
+ setError(mapped);
|
|
|
+ notifyError(mapped);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ setError({
|
|
|
+ title: "Benutzer konnte nicht aktualisiert werden.",
|
|
|
+ description:
|
|
|
+ "Bitte prüfen Sie die Eingaben und versuchen Sie es erneut.",
|
|
|
+ });
|
|
|
+
|
|
|
+ notifyApiError(err, {
|
|
|
+ fallbackTitle: "Benutzer konnte nicht aktualisiert werden.",
|
|
|
+ fallbackDescription:
|
|
|
+ "Bitte prüfen Sie die Eingaben und versuchen Sie es erneut.",
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ setError({
|
|
|
+ title: "Benutzer konnte nicht aktualisiert werden.",
|
|
|
+ description: "Bitte versuchen Sie es erneut.",
|
|
|
+ });
|
|
|
+
|
|
|
+ notifyError({
|
|
|
+ title: "Benutzer konnte nicht aktualisiert werden.",
|
|
|
+ description: "Bitte versuchen Sie es erneut.",
|
|
|
+ });
|
|
|
+ } finally {
|
|
|
+ setIsSubmitting(false);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ [
|
|
|
+ effectiveDisabled,
|
|
|
+ form,
|
|
|
+ user,
|
|
|
+ onUpdated,
|
|
|
+ redirectToLoginExpired,
|
|
|
+ resetForm,
|
|
|
+ ],
|
|
|
+ );
|
|
|
+
|
|
|
+ return {
|
|
|
+ open,
|
|
|
+ handleOpenChange,
|
|
|
+ form,
|
|
|
+ setPatch,
|
|
|
+ error,
|
|
|
+ isSubmitting,
|
|
|
+ effectiveDisabled,
|
|
|
+ canSubmit,
|
|
|
+ handleSubmit,
|
|
|
+ };
|
|
|
+}
|