DaysExplorer.jsx 5.9 KB

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