YearsExplorer.jsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. "use client";
  2. import React from "react";
  3. import Link from "next/link";
  4. import { CalendarDays, RefreshCw } from "lucide-react";
  5. import { getYears } from "@/lib/frontend/apiClient";
  6. import { branchPath, 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. * YearsExplorer
  22. *
  23. * Explorer entry level for a branch: shows available years.
  24. *
  25. * @param {{ branch: string }} props
  26. */
  27. export default function YearsExplorer({ branch }) {
  28. const loadFn = React.useCallback(() => getYears(branch), [branch]);
  29. const { status, data, error, retry } = useExplorerQuery(loadFn, [loadFn]);
  30. const mapped = React.useMemo(() => mapExplorerError(error), [error]);
  31. React.useEffect(() => {
  32. if (mapped?.kind !== "unauthenticated") return;
  33. const next =
  34. typeof window !== "undefined"
  35. ? `${window.location.pathname}${window.location.search}`
  36. : branchPath(branch);
  37. window.location.replace(
  38. buildLoginUrl({ reason: LOGIN_REASONS.EXPIRED, next })
  39. );
  40. }, [mapped?.kind, branch]);
  41. const breadcrumbsNode = <ExplorerBreadcrumbs branch={branch} />;
  42. const actions = (
  43. <Button variant="outline" size="sm" onClick={retry} title="Aktualisieren">
  44. <RefreshCw className="h-4 w-4" />
  45. Aktualisieren
  46. </Button>
  47. );
  48. if (status === "loading") {
  49. return (
  50. <ExplorerPageShell
  51. title="Jahre"
  52. description="Wählen Sie ein Jahr, um die Lieferscheine anzuzeigen."
  53. breadcrumbs={breadcrumbsNode}
  54. actions={actions}
  55. >
  56. <ExplorerSectionCard title="Verfügbare Jahre" description="Lade Daten…">
  57. <ExplorerLoading variant="grid" count={8} />
  58. </ExplorerSectionCard>
  59. </ExplorerPageShell>
  60. );
  61. }
  62. if (status === "error" && mapped) {
  63. if (mapped.kind === "forbidden")
  64. return <ForbiddenView attemptedBranch={branch} />;
  65. if (mapped.kind === "notfound") {
  66. return (
  67. <ExplorerPageShell
  68. title="Jahre"
  69. description="Wählen Sie ein Jahr, um die Lieferscheine anzuzeigen."
  70. breadcrumbs={breadcrumbsNode}
  71. actions={actions}
  72. >
  73. <ExplorerNotFound branchRootHref={branchPath(branch)} />
  74. </ExplorerPageShell>
  75. );
  76. }
  77. if (mapped.kind === "unauthenticated") {
  78. return (
  79. <ExplorerPageShell
  80. title="Jahre"
  81. description="Sitzung abgelaufen — Weiterleitung zum Login…"
  82. breadcrumbs={breadcrumbsNode}
  83. >
  84. <ExplorerSectionCard
  85. title="Weiterleitung"
  86. description="Bitte warten…"
  87. >
  88. <ExplorerLoading variant="grid" count={6} />
  89. </ExplorerSectionCard>
  90. </ExplorerPageShell>
  91. );
  92. }
  93. return (
  94. <ExplorerPageShell
  95. title="Jahre"
  96. description="Wählen Sie ein Jahr, um die Lieferscheine anzuzeigen."
  97. breadcrumbs={breadcrumbsNode}
  98. actions={actions}
  99. >
  100. <ExplorerSectionCard title="Verfügbare Jahre" description="Fehler">
  101. <ExplorerError
  102. title={mapped.title}
  103. description={mapped.description}
  104. onRetry={retry}
  105. />
  106. </ExplorerSectionCard>
  107. </ExplorerPageShell>
  108. );
  109. }
  110. const years = Array.isArray(data?.years) ? data.years : [];
  111. const sorted = sortNumericStringsDesc(years);
  112. return (
  113. <ExplorerPageShell
  114. title="Jahre"
  115. description="Wählen Sie ein Jahr, um die Lieferscheine anzuzeigen."
  116. breadcrumbs={breadcrumbsNode}
  117. actions={actions}
  118. >
  119. <ExplorerSectionCard
  120. title="Verfügbare Jahre"
  121. description={`Niederlassung ${branch}`}
  122. headerRight={
  123. <span className="rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground">
  124. {sorted.length} Jahr{sorted.length === 1 ? "" : "e"}
  125. </span>
  126. }
  127. >
  128. {sorted.length === 0 ? (
  129. <ExplorerEmpty
  130. title="Keine Jahre gefunden"
  131. description="Für diese Niederlassung wurden keine Jahre gefunden."
  132. upHref={null}
  133. />
  134. ) : (
  135. <div className="grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-4">
  136. {sorted.map((y) => (
  137. <Button
  138. key={y}
  139. variant="outline"
  140. className="w-full justify-start"
  141. asChild
  142. >
  143. <Link href={yearPath(branch, y)}>
  144. <CalendarDays className="h-4 w-4" />
  145. {y}
  146. </Link>
  147. </Button>
  148. ))}
  149. </div>
  150. )}
  151. </ExplorerSectionCard>
  152. </ExplorerPageShell>
  153. );
  154. }