AdminUsersClient.jsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  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. <CreateUserDialog disabled={disabled} onCreated={refresh} />
  96. }
  97. >
  98. <div className="space-y-4">
  99. <AdminUsersFilters
  100. draft={draft}
  101. onDraftChange={onDraftChange}
  102. onApply={applyFilters}
  103. onReset={resetFilters}
  104. loadedCount={items.length}
  105. disabled={disabled}
  106. />
  107. {status === "error" ? (
  108. <Alert variant="destructive">
  109. <AlertTitle>Fehler</AlertTitle>
  110. <AlertDescription>
  111. {error instanceof ApiClientError
  112. ? `Anfrage fehlgeschlagen (${error.code}). Bitte erneut versuchen.`
  113. : "Anfrage fehlgeschlagen. Bitte erneut versuchen."}
  114. </AlertDescription>
  115. </Alert>
  116. ) : null}
  117. {status === "loading" ? (
  118. <ExplorerLoading variant="table" count={8} />
  119. ) : items.length === 0 ? (
  120. <ExplorerEmpty
  121. title="Keine Benutzer gefunden"
  122. description="Für die aktuellen Filter wurden keine Benutzer gefunden."
  123. upHref={null}
  124. />
  125. ) : (
  126. <UsersTable
  127. items={items}
  128. disabled={disabled}
  129. onUserUpdated={refresh}
  130. />
  131. )}
  132. {loadMoreError ? (
  133. <Alert variant="destructive">
  134. <AlertTitle>
  135. Weitere Benutzer konnten nicht geladen werden
  136. </AlertTitle>
  137. <AlertDescription>Bitte erneut versuchen.</AlertDescription>
  138. </Alert>
  139. ) : null}
  140. {nextCursor ? (
  141. <div className="flex justify-center">
  142. <Button
  143. type="button"
  144. variant="outline"
  145. onClick={loadMore}
  146. disabled={isLoadingMore}
  147. title="Mehr laden"
  148. >
  149. {isLoadingMore ? "Lädt…" : "Mehr laden"}
  150. </Button>
  151. </div>
  152. ) : null}
  153. </div>
  154. </ExplorerSectionCard>
  155. </ExplorerPageShell>
  156. );
  157. }