|
|
@@ -0,0 +1,217 @@
|
|
|
+"use client";
|
|
|
+
|
|
|
+import React from "react";
|
|
|
+import { Trash2 } from "lucide-react";
|
|
|
+
|
|
|
+import {
|
|
|
+ Dialog,
|
|
|
+ DialogContent,
|
|
|
+ DialogDescription,
|
|
|
+ DialogFooter,
|
|
|
+ DialogHeader,
|
|
|
+ DialogTitle,
|
|
|
+ DialogTrigger,
|
|
|
+} from "@/components/ui/dialog";
|
|
|
+import { Button } from "@/components/ui/button";
|
|
|
+import { Badge } from "@/components/ui/badge";
|
|
|
+
|
|
|
+import { adminDeleteUser, ApiClientError } from "@/lib/frontend/apiClient";
|
|
|
+import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
|
|
|
+import {
|
|
|
+ notifySuccess,
|
|
|
+ notifyError,
|
|
|
+ notifyApiError,
|
|
|
+} from "@/lib/frontend/ui/toast";
|
|
|
+
|
|
|
+import { ROLE_LABELS_DE } from "@/components/admin/users/usersUi";
|
|
|
+
|
|
|
+function formatUserLabel(user) {
|
|
|
+ const username = typeof user?.username === "string" ? user.username : "—";
|
|
|
+ const email = typeof user?.email === "string" ? user.email : "—";
|
|
|
+ return `${username} (${email})`;
|
|
|
+}
|
|
|
+
|
|
|
+export default function DeleteUserDialog({
|
|
|
+ user,
|
|
|
+ disabled = false,
|
|
|
+ onDeleted,
|
|
|
+}) {
|
|
|
+ const [open, setOpen] = React.useState(false);
|
|
|
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
|
|
|
+ const [error, setError] = React.useState(null);
|
|
|
+
|
|
|
+ const effectiveDisabled = Boolean(disabled || isSubmitting);
|
|
|
+
|
|
|
+ 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);
|
|
|
+ if (!nextOpen) setError(null);
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const handleDelete = React.useCallback(async () => {
|
|
|
+ if (!user?.id) return;
|
|
|
+ if (effectiveDisabled) return;
|
|
|
+
|
|
|
+ setError(null);
|
|
|
+ setIsSubmitting(true);
|
|
|
+
|
|
|
+ try {
|
|
|
+ await adminDeleteUser(String(user.id));
|
|
|
+
|
|
|
+ notifySuccess({
|
|
|
+ title: "Benutzer gelöscht",
|
|
|
+ description: `"${formatUserLabel(user)}" wurde entfernt.`,
|
|
|
+ });
|
|
|
+
|
|
|
+ setOpen(false);
|
|
|
+
|
|
|
+ if (typeof onDeleted === "function") onDeleted();
|
|
|
+ } 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_INVALID_FIELD" &&
|
|
|
+ err.details?.reason === "SELF_DELETE_FORBIDDEN"
|
|
|
+ ) {
|
|
|
+ const mapped = {
|
|
|
+ title: "Nicht möglich",
|
|
|
+ description: "Sie können Ihr eigenes Konto nicht löschen.",
|
|
|
+ };
|
|
|
+ setError(mapped);
|
|
|
+ notifyError(mapped);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ setError({
|
|
|
+ title: "Benutzer konnte nicht gelöscht werden.",
|
|
|
+ description: "Bitte versuchen Sie es erneut.",
|
|
|
+ });
|
|
|
+
|
|
|
+ notifyApiError(err, {
|
|
|
+ fallbackTitle: "Benutzer konnte nicht gelöscht werden.",
|
|
|
+ fallbackDescription: "Bitte versuchen Sie es erneut.",
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ setError({
|
|
|
+ title: "Benutzer konnte nicht gelöscht werden.",
|
|
|
+ description: "Bitte versuchen Sie es erneut.",
|
|
|
+ });
|
|
|
+
|
|
|
+ notifyError({
|
|
|
+ title: "Benutzer konnte nicht gelöscht werden.",
|
|
|
+ description: "Bitte versuchen Sie es erneut.",
|
|
|
+ });
|
|
|
+ } finally {
|
|
|
+ setIsSubmitting(false);
|
|
|
+ }
|
|
|
+ }, [user, effectiveDisabled, onDeleted, redirectToLoginExpired]);
|
|
|
+
|
|
|
+ if (!user || typeof user.id !== "string" || !user.id) return null;
|
|
|
+
|
|
|
+ const roleLabel = ROLE_LABELS_DE[user.role] || String(user.role || "—");
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Dialog open={open} onOpenChange={handleOpenChange}>
|
|
|
+ <DialogTrigger asChild>
|
|
|
+ <Button
|
|
|
+ type="button"
|
|
|
+ variant="destructive"
|
|
|
+ size="icon-sm"
|
|
|
+ disabled={disabled}
|
|
|
+ title="Benutzer löschen"
|
|
|
+ aria-label="Benutzer löschen"
|
|
|
+ >
|
|
|
+ <Trash2 className="h-4 w-4" />
|
|
|
+ </Button>
|
|
|
+ </DialogTrigger>
|
|
|
+
|
|
|
+ <DialogContent className="sm:max-w-lg">
|
|
|
+ <DialogHeader>
|
|
|
+ <DialogTitle>Benutzer löschen</DialogTitle>
|
|
|
+ <DialogDescription>
|
|
|
+ Diese Aktion kann nicht rückgängig gemacht werden.
|
|
|
+ </DialogDescription>
|
|
|
+ </DialogHeader>
|
|
|
+
|
|
|
+ <div className="space-y-3">
|
|
|
+ <div className="rounded-lg border p-3">
|
|
|
+ <div className="text-sm font-medium">{formatUserLabel(user)}</div>
|
|
|
+ <div className="mt-2 flex flex-wrap gap-2">
|
|
|
+ <Badge variant="secondary">{roleLabel}</Badge>
|
|
|
+ {user.branchId ? (
|
|
|
+ <Badge variant="outline">{user.branchId}</Badge>
|
|
|
+ ) : null}
|
|
|
+ {user.mustChangePassword ? (
|
|
|
+ <Badge variant="destructive">Passwortwechsel</Badge>
|
|
|
+ ) : (
|
|
|
+ <Badge variant="secondary">Kein Passwortwechsel</Badge>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {error ? (
|
|
|
+ <div className="rounded-lg border border-destructive/40 bg-card p-3">
|
|
|
+ <p className="text-sm font-medium text-destructive">
|
|
|
+ {error.title}
|
|
|
+ </p>
|
|
|
+ {error.description ? (
|
|
|
+ <p className="mt-1 text-sm text-muted-foreground">
|
|
|
+ {error.description}
|
|
|
+ </p>
|
|
|
+ ) : null}
|
|
|
+ </div>
|
|
|
+ ) : null}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <DialogFooter>
|
|
|
+ <Button
|
|
|
+ type="button"
|
|
|
+ variant="outline"
|
|
|
+ disabled={effectiveDisabled}
|
|
|
+ onClick={() => setOpen(false)}
|
|
|
+ >
|
|
|
+ Abbrechen
|
|
|
+ </Button>
|
|
|
+
|
|
|
+ <Button
|
|
|
+ type="button"
|
|
|
+ variant="destructive"
|
|
|
+ disabled={effectiveDisabled}
|
|
|
+ onClick={handleDelete}
|
|
|
+ title="Endgültig löschen"
|
|
|
+ >
|
|
|
+ {isSubmitting ? "Löscht…" : "Löschen"}
|
|
|
+ </Button>
|
|
|
+ </DialogFooter>
|
|
|
+ </DialogContent>
|
|
|
+ </Dialog>
|
|
|
+ );
|
|
|
+}
|