MonthsExplorer.jsx 5.7 KB

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