AdminUsersClient.jsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. "use client";
  2. import React from "react";
  3. import { RefreshCw } from "lucide-react";
  4. import ExplorerPageShell from "@/components/explorer/ExplorerPageShell";
  5. import ExplorerSectionCard from "@/components/explorer/ExplorerSectionCard";
  6. import ExplorerLoading from "@/components/explorer/states/ExplorerLoading";
  7. import ExplorerEmpty from "@/components/explorer/states/ExplorerEmpty";
  8. import ForbiddenView from "@/components/system/ForbiddenView";
  9. import AdminUsersFilters from "@/components/admin/users/AdminUsersFilters";
  10. import UsersTable from "@/components/admin/users/UsersTable";
  11. import CreateUserDialog from "@/components/admin/users/CreateUserDialog";
  12. import { normalizeBranchIdDraft } from "@/components/admin/users/usersUi";
  13. import { ApiClientError } from "@/lib/frontend/apiClient";
  14. import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
  15. import { useAdminUsersQuery } from "@/lib/frontend/admin/users/useAdminUsersQuery";
  16. import { Button } from "@/components/ui/button";
  17. import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
  18. const LIMIT = 50;
  19. export default function AdminUsersClient() {
  20. const [draft, setDraft] = React.useState({
  21. q: "",
  22. role: "",
  23. branchId: "",
  24. });
  25. const [query, setQuery] = React.useState({
  26. q: null,
  27. role: null,
  28. branchId: null,
  29. });
  30. const {
  31. status,
  32. items,
  33. nextCursor,
  34. error,
  35. refresh,
  36. loadMore,
  37. isLoadingMore,
  38. loadMoreError,
  39. } = useAdminUsersQuery({ query, limit: LIMIT });
  40. React.useEffect(() => {
  41. if (!(error instanceof ApiClientError)) return;
  42. if (error.code === "AUTH_UNAUTHENTICATED") {
  43. const next =
  44. typeof window !== "undefined"
  45. ? `${window.location.pathname}${window.location.search}`
  46. : "/admin/users";
  47. window.location.replace(
  48. buildLoginUrl({ reason: LOGIN_REASONS.EXPIRED, next }),
  49. );
  50. }
  51. }, [error]);
  52. if (
  53. error instanceof ApiClientError &&
  54. error.code === "AUTH_FORBIDDEN_USER_MANAGEMENT"
  55. ) {
  56. return <ForbiddenView />;
  57. }
  58. const disabled = status === "loading" || isLoadingMore;
  59. function onDraftChange(patch) {
  60. setDraft((prev) => ({ ...prev, ...(patch || {}) }));
  61. }
  62. function applyFilters() {
  63. setQuery({
  64. q: draft.q.trim() ? draft.q.trim() : null,
  65. role: draft.role.trim() ? draft.role.trim() : null,
  66. branchId: normalizeBranchIdDraft(draft.branchId) || null,
  67. });
  68. }
  69. function resetFilters() {
  70. setDraft({ q: "", role: "", branchId: "" });
  71. setQuery({ q: null, role: null, branchId: null });
  72. }
  73. const actions = (
  74. <Button
  75. variant="outline"
  76. size="sm"
  77. onClick={refresh}
  78. disabled={disabled}
  79. title="Aktualisieren"
  80. >
  81. <RefreshCw className="h-4 w-4" />
  82. Aktualisieren
  83. </Button>
  84. );
  85. return (
  86. <ExplorerPageShell
  87. title="Benutzerverwaltung"
  88. description="Benutzerkonten anzeigen und filtern (nur Superadmin/Entwicklung)."
  89. actions={actions}
  90. >
  91. <ExplorerSectionCard
  92. title="Benutzer"
  93. description="Suche und Filter anwenden."
  94. headerRight={
  95. <div className="flex items-center gap-2">
  96. <span className="rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground">
  97. {items.length} Benutzer geladen
  98. </span>
  99. <CreateUserDialog disabled={disabled} onCreated={refresh} />
  100. </div>
  101. }
  102. >
  103. <div className="space-y-4">
  104. <AdminUsersFilters
  105. draft={draft}
  106. onDraftChange={onDraftChange}
  107. onApply={applyFilters}
  108. onReset={resetFilters}
  109. disabled={disabled}
  110. />
  111. {status === "error" ? (
  112. <Alert variant="destructive">
  113. <AlertTitle>Fehler</AlertTitle>
  114. <AlertDescription>
  115. {error instanceof ApiClientError
  116. ? `Anfrage fehlgeschlagen (${error.code}). Bitte erneut versuchen.`
  117. : "Anfrage fehlgeschlagen. Bitte erneut versuchen."}
  118. </AlertDescription>
  119. </Alert>
  120. ) : null}
  121. {status === "loading" ? (
  122. <ExplorerLoading variant="table" count={8} />
  123. ) : items.length === 0 ? (
  124. <ExplorerEmpty
  125. title="Keine Benutzer gefunden"
  126. description="Für die aktuellen Filter wurden keine Benutzer gefunden."
  127. upHref={null}
  128. />
  129. ) : (
  130. <UsersTable
  131. items={items}
  132. disabled={disabled}
  133. onUserUpdated={refresh}
  134. />
  135. )}
  136. {loadMoreError ? (
  137. <Alert variant="destructive">
  138. <AlertTitle>
  139. Weitere Benutzer konnten nicht geladen werden
  140. </AlertTitle>
  141. <AlertDescription>Bitte erneut versuchen.</AlertDescription>
  142. </Alert>
  143. ) : null}
  144. {nextCursor ? (
  145. <div className="flex justify-center">
  146. <Button
  147. type="button"
  148. variant="outline"
  149. onClick={loadMore}
  150. disabled={isLoadingMore}
  151. title="Mehr laden"
  152. >
  153. {isLoadingMore ? "Lädt…" : "Mehr laden"}
  154. </Button>
  155. </div>
  156. ) : null}
  157. </div>
  158. </ExplorerSectionCard>
  159. </ExplorerPageShell>
  160. );
  161. }