DeleteUserDialog.jsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. "use client";
  2. import React from "react";
  3. import { Trash2 } from "lucide-react";
  4. import {
  5. Dialog,
  6. DialogContent,
  7. DialogDescription,
  8. DialogFooter,
  9. DialogHeader,
  10. DialogTitle,
  11. DialogTrigger,
  12. } from "@/components/ui/dialog";
  13. import { Button } from "@/components/ui/button";
  14. import { Badge } from "@/components/ui/badge";
  15. import { adminDeleteUser, ApiClientError } from "@/lib/frontend/apiClient";
  16. import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
  17. import {
  18. notifySuccess,
  19. notifyError,
  20. notifyApiError,
  21. } from "@/lib/frontend/ui/toast";
  22. import { ROLE_LABELS_DE } from "@/components/admin/users/usersUi";
  23. function formatUserLabel(user) {
  24. const username = typeof user?.username === "string" ? user.username : "—";
  25. const email = typeof user?.email === "string" ? user.email : "—";
  26. return `${username} (${email})`;
  27. }
  28. export default function DeleteUserDialog({
  29. user,
  30. disabled = false,
  31. onDeleted,
  32. }) {
  33. const [open, setOpen] = React.useState(false);
  34. const [isSubmitting, setIsSubmitting] = React.useState(false);
  35. const [error, setError] = React.useState(null);
  36. const effectiveDisabled = Boolean(disabled || isSubmitting);
  37. const redirectToLoginExpired = React.useCallback(() => {
  38. const next =
  39. typeof window !== "undefined"
  40. ? `${window.location.pathname}${window.location.search}`
  41. : "/admin/users";
  42. window.location.replace(
  43. buildLoginUrl({ reason: LOGIN_REASONS.EXPIRED, next }),
  44. );
  45. }, []);
  46. const handleOpenChange = React.useCallback((nextOpen) => {
  47. setOpen(nextOpen);
  48. if (!nextOpen) setError(null);
  49. }, []);
  50. const handleDelete = React.useCallback(async () => {
  51. if (!user?.id) return;
  52. if (effectiveDisabled) return;
  53. setError(null);
  54. setIsSubmitting(true);
  55. try {
  56. await adminDeleteUser(String(user.id));
  57. notifySuccess({
  58. title: "Benutzer gelöscht",
  59. description: `"${formatUserLabel(user)}" wurde entfernt.`,
  60. });
  61. setOpen(false);
  62. if (typeof onDeleted === "function") onDeleted();
  63. } catch (err) {
  64. if (err instanceof ApiClientError) {
  65. if (err.code === "AUTH_UNAUTHENTICATED") {
  66. notifyApiError(err);
  67. redirectToLoginExpired();
  68. return;
  69. }
  70. if (err.code === "USER_NOT_FOUND") {
  71. const mapped = {
  72. title: "Benutzer nicht gefunden.",
  73. description:
  74. "Der Benutzer existiert nicht (mehr). Bitte aktualisieren Sie die Liste.",
  75. };
  76. setError(mapped);
  77. notifyError(mapped);
  78. return;
  79. }
  80. if (
  81. err.code === "VALIDATION_INVALID_FIELD" &&
  82. err.details?.reason === "SELF_DELETE_FORBIDDEN"
  83. ) {
  84. const mapped = {
  85. title: "Nicht möglich",
  86. description: "Sie können Ihr eigenes Konto nicht löschen.",
  87. };
  88. setError(mapped);
  89. notifyError(mapped);
  90. return;
  91. }
  92. setError({
  93. title: "Benutzer konnte nicht gelöscht werden.",
  94. description: "Bitte versuchen Sie es erneut.",
  95. });
  96. notifyApiError(err, {
  97. fallbackTitle: "Benutzer konnte nicht gelöscht werden.",
  98. fallbackDescription: "Bitte versuchen Sie es erneut.",
  99. });
  100. return;
  101. }
  102. setError({
  103. title: "Benutzer konnte nicht gelöscht werden.",
  104. description: "Bitte versuchen Sie es erneut.",
  105. });
  106. notifyError({
  107. title: "Benutzer konnte nicht gelöscht werden.",
  108. description: "Bitte versuchen Sie es erneut.",
  109. });
  110. } finally {
  111. setIsSubmitting(false);
  112. }
  113. }, [user, effectiveDisabled, onDeleted, redirectToLoginExpired]);
  114. if (!user || typeof user.id !== "string" || !user.id) return null;
  115. const roleLabel = ROLE_LABELS_DE[user.role] || String(user.role || "—");
  116. return (
  117. <Dialog open={open} onOpenChange={handleOpenChange}>
  118. <DialogTrigger asChild>
  119. <Button
  120. type="button"
  121. variant="destructive"
  122. size="icon-sm"
  123. disabled={disabled}
  124. title="Benutzer löschen"
  125. aria-label="Benutzer löschen"
  126. >
  127. <Trash2 className="h-4 w-4" />
  128. </Button>
  129. </DialogTrigger>
  130. <DialogContent className="sm:max-w-lg">
  131. <DialogHeader>
  132. <DialogTitle>Benutzer löschen</DialogTitle>
  133. <DialogDescription>
  134. Diese Aktion kann nicht rückgängig gemacht werden.
  135. </DialogDescription>
  136. </DialogHeader>
  137. <div className="space-y-3">
  138. <div className="rounded-lg border p-3">
  139. <div className="text-sm font-medium">{formatUserLabel(user)}</div>
  140. <div className="mt-2 flex flex-wrap gap-2">
  141. <Badge variant="secondary">{roleLabel}</Badge>
  142. {user.branchId ? (
  143. <Badge variant="outline">{user.branchId}</Badge>
  144. ) : null}
  145. {user.mustChangePassword ? (
  146. <Badge variant="destructive">Passwortwechsel</Badge>
  147. ) : (
  148. <Badge variant="secondary">Kein Passwortwechsel</Badge>
  149. )}
  150. </div>
  151. </div>
  152. {error ? (
  153. <div className="rounded-lg border border-destructive/40 bg-card p-3">
  154. <p className="text-sm font-medium text-destructive">
  155. {error.title}
  156. </p>
  157. {error.description ? (
  158. <p className="mt-1 text-sm text-muted-foreground">
  159. {error.description}
  160. </p>
  161. ) : null}
  162. </div>
  163. ) : null}
  164. </div>
  165. <DialogFooter>
  166. <Button
  167. type="button"
  168. variant="outline"
  169. disabled={effectiveDisabled}
  170. onClick={() => setOpen(false)}
  171. >
  172. Abbrechen
  173. </Button>
  174. <Button
  175. type="button"
  176. variant="destructive"
  177. disabled={effectiveDisabled}
  178. onClick={handleDelete}
  179. title="Endgültig löschen"
  180. >
  181. {isSubmitting ? "Löscht…" : "Löschen"}
  182. </Button>
  183. </DialogFooter>
  184. </DialogContent>
  185. </Dialog>
  186. );
  187. }