// --------------------------------------------------------------------------- // 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, }; }