MonthsExplorer.jsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. "use client";
  2. import React from "react";
  3. import Link from "next/link";
  4. import { CalendarRange, RefreshCw } from "lucide-react";
  5. import { getMonths, getYears } from "@/lib/frontend/apiClient";
  6. import { branchPath, monthPath, yearPath } from "@/lib/frontend/routes";
  7. import { sortNumericStringsDesc } from "@/lib/frontend/explorer/sorters";
  8. import { formatMonthLabel } from "@/lib/frontend/explorer/formatters";
  9. import { mapExplorerError } from "@/lib/frontend/explorer/errorMapping";
  10. import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
  11. import { useExplorerQuery } from "@/lib/frontend/hooks/useExplorerQuery";
  12. import { useDebouncedVisibility } from "@/lib/frontend/hooks/useDebouncedVisibility";
  13. import { LOADING_UI_DELAY_MS } from "@/lib/frontend/ui/uxTimings";
  14. import ExplorerBreadcrumbs from "@/components/explorer/breadcrumbs/ExplorerBreadcrumbs";
  15. import ExplorerPageShell from "@/components/explorer/ExplorerPageShell";
  16. import ExplorerSectionCard from "@/components/explorer/ExplorerSectionCard";
  17. import ExplorerLoading from "@/components/explorer/states/ExplorerLoading";
  18. import ExplorerEmpty from "@/components/explorer/states/ExplorerEmpty";
  19. import ExplorerError from "@/components/explorer/states/ExplorerError";
  20. import ExplorerNotFound from "@/components/explorer/states/ExplorerNotFound";
  21. import ForbiddenView from "@/components/system/ForbiddenView";
  22. import { Button } from "@/components/ui/button";
  23. export default function MonthsExplorer({ branch, year }) {
  24. const monthsLoadFn = React.useCallback(
  25. () => getMonths(branch, year),
  26. [branch, year],
  27. );
  28. const monthsQuery = useExplorerQuery(monthsLoadFn, [monthsLoadFn]);
  29. const yearsLoadFn = React.useCallback(() => getYears(branch), [branch]);
  30. const yearsQuery = useExplorerQuery(yearsLoadFn, [yearsLoadFn]);
  31. const mapped = React.useMemo(
  32. () => mapExplorerError(monthsQuery.error),
  33. [monthsQuery.error],
  34. );
  35. const showLoadingUi = useDebouncedVisibility(
  36. monthsQuery.status === "loading",
  37. {
  38. delayMs: LOADING_UI_DELAY_MS,
  39. minVisibleMs: 0,
  40. },
  41. );
  42. React.useEffect(() => {
  43. if (mapped?.kind !== "unauthenticated") return;
  44. const next =
  45. typeof window !== "undefined"
  46. ? `${window.location.pathname}${window.location.search}`
  47. : yearPath(branch, year);
  48. window.location.replace(
  49. buildLoginUrl({ reason: LOGIN_REASONS.EXPIRED, next }),
  50. );
  51. }, [mapped?.kind, branch, year]);
  52. const yearOptions =
  53. yearsQuery.status === "success" && Array.isArray(yearsQuery.data?.years)
  54. ? yearsQuery.data.years
  55. : null;
  56. const breadcrumbsNode = (
  57. <ExplorerBreadcrumbs
  58. branch={branch}
  59. year={year}
  60. yearOptions={yearOptions}
  61. />
  62. );
  63. const actions = (
  64. <Button
  65. variant="outline"
  66. size="sm"
  67. onClick={() => {
  68. monthsQuery.retry();
  69. yearsQuery.retry();
  70. }}
  71. title="Aktualisieren"
  72. >
  73. <RefreshCw className="h-4 w-4" />
  74. Aktualisieren
  75. </Button>
  76. );
  77. if (showLoadingUi) {
  78. return (
  79. <ExplorerPageShell
  80. title="Monate"
  81. description="Wählen Sie einen Monat aus."
  82. breadcrumbs={breadcrumbsNode}
  83. actions={actions}
  84. >
  85. <ExplorerSectionCard
  86. title="Verfügbare Monate"
  87. description={`Jahr ${year}`}
  88. >
  89. <ExplorerLoading variant="grid" count={12} />
  90. </ExplorerSectionCard>
  91. </ExplorerPageShell>
  92. );
  93. }
  94. if (monthsQuery.status === "loading") {
  95. return (
  96. <ExplorerPageShell
  97. title="Monate"
  98. description="Wählen Sie einen Monat aus."
  99. breadcrumbs={breadcrumbsNode}
  100. actions={actions}
  101. >
  102. <div className="h-16" aria-hidden="true" />
  103. </ExplorerPageShell>
  104. );
  105. }
  106. if (monthsQuery.status === "error" && mapped) {
  107. if (mapped.kind === "forbidden")
  108. return <ForbiddenView attemptedBranch={branch} />;
  109. if (mapped.kind === "notfound") {
  110. return (
  111. <ExplorerPageShell
  112. title="Monate"
  113. description="Wählen Sie einen Monat aus."
  114. breadcrumbs={breadcrumbsNode}
  115. actions={actions}
  116. >
  117. <ExplorerNotFound
  118. upHref={branchPath(branch)}
  119. branchRootHref={branchPath(branch)}
  120. />
  121. </ExplorerPageShell>
  122. );
  123. }
  124. if (mapped.kind === "unauthenticated") {
  125. return (
  126. <ExplorerPageShell
  127. title="Monate"
  128. description="Sitzung abgelaufen — Weiterleitung zum Login…"
  129. breadcrumbs={breadcrumbsNode}
  130. >
  131. <div className="h-16" aria-hidden="true" />
  132. </ExplorerPageShell>
  133. );
  134. }
  135. return (
  136. <ExplorerPageShell
  137. title="Monate"
  138. description="Wählen Sie einen Monat aus."
  139. breadcrumbs={breadcrumbsNode}
  140. actions={actions}
  141. >
  142. <ExplorerSectionCard
  143. title="Verfügbare Monate"
  144. description={`Jahr ${year}`}
  145. >
  146. <ExplorerError
  147. title={mapped.title}
  148. description={mapped.description}
  149. onRetry={monthsQuery.retry}
  150. />
  151. </ExplorerSectionCard>
  152. </ExplorerPageShell>
  153. );
  154. }
  155. const months = Array.isArray(monthsQuery.data?.months)
  156. ? monthsQuery.data.months
  157. : [];
  158. const sorted = sortNumericStringsDesc(months);
  159. return (
  160. <ExplorerPageShell
  161. title="Monate"
  162. description="Wählen Sie einen Monat aus."
  163. breadcrumbs={breadcrumbsNode}
  164. actions={actions}
  165. >
  166. <ExplorerSectionCard
  167. title="Verfügbare Monate"
  168. description={`Niederlassung ${branch} • Jahr ${year}`}
  169. headerRight={
  170. <span className="rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground">
  171. {sorted.length} Monat{sorted.length === 1 ? "" : "e"}
  172. </span>
  173. }
  174. >
  175. {sorted.length === 0 ? (
  176. <ExplorerEmpty
  177. title="Keine Monate gefunden"
  178. description="Für dieses Jahr wurden keine Monate gefunden."
  179. upHref={branchPath(branch)}
  180. />
  181. ) : (
  182. <div className="grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-4">
  183. {sorted.map((m) => (
  184. <Button
  185. key={m}
  186. variant="outline"
  187. className="w-full justify-start"
  188. asChild
  189. >
  190. <Link href={monthPath(branch, year, m)}>
  191. <CalendarRange className="h-4 w-4" />
  192. <span className="truncate">{formatMonthLabel(m)}</span>
  193. </Link>
  194. </Button>
  195. ))}
  196. </div>
  197. )}
  198. </ExplorerSectionCard>
  199. </ExplorerPageShell>
  200. );
  201. }