AdminUsersClient.jsx 5.4 KB

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