FilesExplorer.jsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. "use client";
  2. import React from "react";
  3. import { Eye, FileText, RefreshCw } from "lucide-react";
  4. import {
  5. getFiles,
  6. getDays,
  7. getMonths,
  8. getYears,
  9. } from "@/lib/frontend/apiClient";
  10. import { dayPath, monthPath, branchPath } from "@/lib/frontend/routes";
  11. import { sortFilesByNameAsc } from "@/lib/frontend/explorer/sorters";
  12. import { mapExplorerError } from "@/lib/frontend/explorer/errorMapping";
  13. import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
  14. import { useExplorerQuery } from "@/lib/frontend/hooks/useExplorerQuery";
  15. import ExplorerBreadcrumbs from "@/components/explorer/breadcrumbs/ExplorerBreadcrumbs";
  16. import ExplorerPageShell from "@/components/explorer/ExplorerPageShell";
  17. import ExplorerSectionCard from "@/components/explorer/ExplorerSectionCard";
  18. import ExplorerLoading from "@/components/explorer/states/ExplorerLoading";
  19. import ExplorerEmpty from "@/components/explorer/states/ExplorerEmpty";
  20. import ExplorerError from "@/components/explorer/states/ExplorerError";
  21. import ExplorerNotFound from "@/components/explorer/states/ExplorerNotFound";
  22. import ForbiddenView from "@/components/system/ForbiddenView";
  23. import { Button } from "@/components/ui/button";
  24. import {
  25. Table,
  26. TableHeader,
  27. TableBody,
  28. TableRow,
  29. TableHead,
  30. TableCell,
  31. TableCaption,
  32. } from "@/components/ui/table";
  33. /**
  34. * FilesExplorer
  35. *
  36. * Explorer leaf level: lists files (PDFs) for a day.
  37. * Loads years/months/days for breadcrumb dropdowns (fail-open).
  38. *
  39. * @param {{ branch: string, year: string, month: string, day: string }} props
  40. */
  41. export default function FilesExplorer({ branch, year, month, day }) {
  42. const filesLoadFn = React.useCallback(
  43. () => getFiles(branch, year, month, day),
  44. [branch, year, month, day]
  45. );
  46. const filesQuery = useExplorerQuery(filesLoadFn, [filesLoadFn]);
  47. const yearsLoadFn = React.useCallback(() => getYears(branch), [branch]);
  48. const yearsQuery = useExplorerQuery(yearsLoadFn, [yearsLoadFn]);
  49. const monthsLoadFn = React.useCallback(
  50. () => getMonths(branch, year),
  51. [branch, year]
  52. );
  53. const monthsQuery = useExplorerQuery(monthsLoadFn, [monthsLoadFn]);
  54. const daysLoadFn = React.useCallback(
  55. () => getDays(branch, year, month),
  56. [branch, year, month]
  57. );
  58. const daysQuery = useExplorerQuery(daysLoadFn, [daysLoadFn]);
  59. const mapped = React.useMemo(
  60. () => mapExplorerError(filesQuery.error),
  61. [filesQuery.error]
  62. );
  63. React.useEffect(() => {
  64. if (mapped?.kind !== "unauthenticated") return;
  65. const next =
  66. typeof window !== "undefined"
  67. ? `${window.location.pathname}${window.location.search}`
  68. : dayPath(branch, year, month, day);
  69. window.location.replace(
  70. buildLoginUrl({ reason: LOGIN_REASONS.EXPIRED, next })
  71. );
  72. }, [mapped?.kind, branch, year, month, day]);
  73. const yearOptions =
  74. yearsQuery.status === "success" && Array.isArray(yearsQuery.data?.years)
  75. ? yearsQuery.data.years
  76. : null;
  77. const monthOptions =
  78. monthsQuery.status === "success" && Array.isArray(monthsQuery.data?.months)
  79. ? monthsQuery.data.months
  80. : null;
  81. const dayOptions =
  82. daysQuery.status === "success" && Array.isArray(daysQuery.data?.days)
  83. ? daysQuery.data.days
  84. : null;
  85. const breadcrumbsNode = (
  86. <ExplorerBreadcrumbs
  87. branch={branch}
  88. year={year}
  89. month={month}
  90. day={day}
  91. yearOptions={yearOptions}
  92. monthOptions={monthOptions}
  93. dayOptions={dayOptions}
  94. />
  95. );
  96. const actions = (
  97. <Button
  98. variant="outline"
  99. size="sm"
  100. onClick={() => {
  101. filesQuery.retry();
  102. yearsQuery.retry();
  103. monthsQuery.retry();
  104. daysQuery.retry();
  105. }}
  106. title="Aktualisieren"
  107. >
  108. <RefreshCw className="h-4 w-4" />
  109. Aktualisieren
  110. </Button>
  111. );
  112. if (filesQuery.status === "loading") {
  113. return (
  114. <ExplorerPageShell
  115. title="Dateien"
  116. description="Lieferscheine für den ausgewählten Tag."
  117. breadcrumbs={breadcrumbsNode}
  118. actions={actions}
  119. >
  120. <ExplorerSectionCard title="Dateiliste" description="Lade Daten…">
  121. <ExplorerLoading variant="table" count={8} />
  122. </ExplorerSectionCard>
  123. </ExplorerPageShell>
  124. );
  125. }
  126. if (filesQuery.status === "error" && mapped) {
  127. if (mapped.kind === "forbidden")
  128. return <ForbiddenView attemptedBranch={branch} />;
  129. if (mapped.kind === "notfound") {
  130. return (
  131. <ExplorerPageShell
  132. title="Dateien"
  133. description="Lieferscheine für den ausgewählten Tag."
  134. breadcrumbs={breadcrumbsNode}
  135. actions={actions}
  136. >
  137. <ExplorerNotFound
  138. upHref={monthPath(branch, year, month)}
  139. branchRootHref={branchPath(branch)}
  140. />
  141. </ExplorerPageShell>
  142. );
  143. }
  144. if (mapped.kind === "unauthenticated") {
  145. return (
  146. <ExplorerPageShell
  147. title="Dateien"
  148. description="Sitzung abgelaufen — Weiterleitung zum Login…"
  149. breadcrumbs={breadcrumbsNode}
  150. >
  151. <ExplorerSectionCard
  152. title="Weiterleitung"
  153. description="Bitte warten…"
  154. >
  155. <ExplorerLoading variant="table" count={6} />
  156. </ExplorerSectionCard>
  157. </ExplorerPageShell>
  158. );
  159. }
  160. return (
  161. <ExplorerPageShell
  162. title="Dateien"
  163. description="Lieferscheine für den ausgewählten Tag."
  164. breadcrumbs={breadcrumbsNode}
  165. actions={actions}
  166. >
  167. <ExplorerSectionCard title="Dateiliste" description="Fehler">
  168. <ExplorerError
  169. title={mapped.title}
  170. description={mapped.description}
  171. onRetry={filesQuery.retry}
  172. />
  173. </ExplorerSectionCard>
  174. </ExplorerPageShell>
  175. );
  176. }
  177. const files = Array.isArray(filesQuery.data?.files)
  178. ? filesQuery.data.files
  179. : [];
  180. const sorted = sortFilesByNameAsc(files);
  181. return (
  182. <ExplorerPageShell
  183. title="Dateien"
  184. description="Lieferscheine für den ausgewählten Tag."
  185. breadcrumbs={breadcrumbsNode}
  186. actions={actions}
  187. >
  188. <ExplorerSectionCard
  189. title="Dateiliste"
  190. description={`Niederlassung ${branch} • ${year}/${month}/${day}`}
  191. headerRight={
  192. <span className="rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground">
  193. {sorted.length} Datei{sorted.length === 1 ? "" : "en"}
  194. </span>
  195. }
  196. >
  197. {sorted.length === 0 ? (
  198. <ExplorerEmpty
  199. title="Keine Dateien gefunden"
  200. description="Für diesen Tag wurden keine Dateien gefunden."
  201. upHref={monthPath(branch, year, month)}
  202. />
  203. ) : (
  204. <Table>
  205. <TableCaption>
  206. Hinweis: Die PDF-Ansicht folgt in einem späteren Ticket (RHL-023).
  207. </TableCaption>
  208. <TableHeader>
  209. <TableRow>
  210. <TableHead>Datei</TableHead>
  211. <TableHead className="hidden md:table-cell">Pfad</TableHead>
  212. <TableHead className="text-right">Aktion</TableHead>
  213. </TableRow>
  214. </TableHeader>
  215. <TableBody>
  216. {sorted.map((f) => (
  217. <TableRow key={f.relativePath || f.name}>
  218. <TableCell className="min-w-0">
  219. <div className="flex min-w-0 items-start gap-2">
  220. <FileText className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
  221. <div className="min-w-0">
  222. <p className="truncate font-medium">{f.name}</p>
  223. <p className="truncate text-xs text-muted-foreground md:hidden">
  224. {f.relativePath}
  225. </p>
  226. </div>
  227. </div>
  228. </TableCell>
  229. <TableCell className="hidden md:table-cell">
  230. <span className="text-xs text-muted-foreground">
  231. {f.relativePath}
  232. </span>
  233. </TableCell>
  234. <TableCell className="text-right">
  235. <Button
  236. variant="outline"
  237. size="sm"
  238. disabled
  239. aria-disabled="true"
  240. title="PDF-Ansicht kommt bald"
  241. >
  242. <Eye className="h-4 w-4" />
  243. Öffnen
  244. </Button>
  245. </TableCell>
  246. </TableRow>
  247. ))}
  248. </TableBody>
  249. </Table>
  250. )}
  251. </ExplorerSectionCard>
  252. </ExplorerPageShell>
  253. );
  254. }