DaysExplorer.jsx 6.1 KB

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