|
|
@@ -0,0 +1,282 @@
|
|
|
+"use client";
|
|
|
+
|
|
|
+import React from "react";
|
|
|
+import { Eye, FileText, RefreshCw } from "lucide-react";
|
|
|
+
|
|
|
+import {
|
|
|
+ getFiles,
|
|
|
+ getDays,
|
|
|
+ getMonths,
|
|
|
+ getYears,
|
|
|
+} from "@/lib/frontend/apiClient";
|
|
|
+import { dayPath, monthPath, branchPath } from "@/lib/frontend/routes";
|
|
|
+import { sortFilesByNameAsc } from "@/lib/frontend/explorer/sorters";
|
|
|
+import { mapExplorerError } from "@/lib/frontend/explorer/errorMapping";
|
|
|
+import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
|
|
|
+import { useExplorerQuery } from "@/lib/frontend/hooks/useExplorerQuery";
|
|
|
+
|
|
|
+import ExplorerBreadcrumbs from "@/components/explorer/breadcrumbs/ExplorerBreadcrumbs";
|
|
|
+import ExplorerPageShell from "@/components/explorer/ExplorerPageShell";
|
|
|
+import ExplorerSectionCard from "@/components/explorer/ExplorerSectionCard";
|
|
|
+import ExplorerLoading from "@/components/explorer/states/ExplorerLoading";
|
|
|
+import ExplorerEmpty from "@/components/explorer/states/ExplorerEmpty";
|
|
|
+import ExplorerError from "@/components/explorer/states/ExplorerError";
|
|
|
+import ExplorerNotFound from "@/components/explorer/states/ExplorerNotFound";
|
|
|
+
|
|
|
+import ForbiddenView from "@/components/system/ForbiddenView";
|
|
|
+import { Button } from "@/components/ui/button";
|
|
|
+import {
|
|
|
+ Table,
|
|
|
+ TableHeader,
|
|
|
+ TableBody,
|
|
|
+ TableRow,
|
|
|
+ TableHead,
|
|
|
+ TableCell,
|
|
|
+ TableCaption,
|
|
|
+} from "@/components/ui/table";
|
|
|
+
|
|
|
+/**
|
|
|
+ * FilesExplorer
|
|
|
+ *
|
|
|
+ * Explorer leaf level: lists files (PDFs) for a day.
|
|
|
+ * Loads years/months/days for breadcrumb dropdowns (fail-open).
|
|
|
+ *
|
|
|
+ * @param {{ branch: string, year: string, month: string, day: string }} props
|
|
|
+ */
|
|
|
+export default function FilesExplorer({ branch, year, month, day }) {
|
|
|
+ const filesLoadFn = React.useCallback(
|
|
|
+ () => getFiles(branch, year, month, day),
|
|
|
+ [branch, year, month, day]
|
|
|
+ );
|
|
|
+ const filesQuery = useExplorerQuery(filesLoadFn, [filesLoadFn]);
|
|
|
+
|
|
|
+ const yearsLoadFn = React.useCallback(() => getYears(branch), [branch]);
|
|
|
+ const yearsQuery = useExplorerQuery(yearsLoadFn, [yearsLoadFn]);
|
|
|
+
|
|
|
+ const monthsLoadFn = React.useCallback(
|
|
|
+ () => getMonths(branch, year),
|
|
|
+ [branch, year]
|
|
|
+ );
|
|
|
+ const monthsQuery = useExplorerQuery(monthsLoadFn, [monthsLoadFn]);
|
|
|
+
|
|
|
+ const daysLoadFn = React.useCallback(
|
|
|
+ () => getDays(branch, year, month),
|
|
|
+ [branch, year, month]
|
|
|
+ );
|
|
|
+ const daysQuery = useExplorerQuery(daysLoadFn, [daysLoadFn]);
|
|
|
+
|
|
|
+ const mapped = React.useMemo(
|
|
|
+ () => mapExplorerError(filesQuery.error),
|
|
|
+ [filesQuery.error]
|
|
|
+ );
|
|
|
+
|
|
|
+ React.useEffect(() => {
|
|
|
+ if (mapped?.kind !== "unauthenticated") return;
|
|
|
+
|
|
|
+ const next =
|
|
|
+ typeof window !== "undefined"
|
|
|
+ ? `${window.location.pathname}${window.location.search}`
|
|
|
+ : dayPath(branch, year, month, day);
|
|
|
+
|
|
|
+ window.location.replace(
|
|
|
+ buildLoginUrl({ reason: LOGIN_REASONS.EXPIRED, next })
|
|
|
+ );
|
|
|
+ }, [mapped?.kind, branch, year, month, day]);
|
|
|
+
|
|
|
+ const yearOptions =
|
|
|
+ yearsQuery.status === "success" && Array.isArray(yearsQuery.data?.years)
|
|
|
+ ? yearsQuery.data.years
|
|
|
+ : null;
|
|
|
+
|
|
|
+ const monthOptions =
|
|
|
+ monthsQuery.status === "success" && Array.isArray(monthsQuery.data?.months)
|
|
|
+ ? monthsQuery.data.months
|
|
|
+ : null;
|
|
|
+
|
|
|
+ const dayOptions =
|
|
|
+ daysQuery.status === "success" && Array.isArray(daysQuery.data?.days)
|
|
|
+ ? daysQuery.data.days
|
|
|
+ : null;
|
|
|
+
|
|
|
+ const breadcrumbsNode = (
|
|
|
+ <ExplorerBreadcrumbs
|
|
|
+ branch={branch}
|
|
|
+ year={year}
|
|
|
+ month={month}
|
|
|
+ day={day}
|
|
|
+ yearOptions={yearOptions}
|
|
|
+ monthOptions={monthOptions}
|
|
|
+ dayOptions={dayOptions}
|
|
|
+ />
|
|
|
+ );
|
|
|
+
|
|
|
+ const actions = (
|
|
|
+ <Button
|
|
|
+ variant="outline"
|
|
|
+ size="sm"
|
|
|
+ onClick={() => {
|
|
|
+ filesQuery.retry();
|
|
|
+ yearsQuery.retry();
|
|
|
+ monthsQuery.retry();
|
|
|
+ daysQuery.retry();
|
|
|
+ }}
|
|
|
+ title="Aktualisieren"
|
|
|
+ >
|
|
|
+ <RefreshCw className="h-4 w-4" />
|
|
|
+ Aktualisieren
|
|
|
+ </Button>
|
|
|
+ );
|
|
|
+
|
|
|
+ if (filesQuery.status === "loading") {
|
|
|
+ return (
|
|
|
+ <ExplorerPageShell
|
|
|
+ title="Dateien"
|
|
|
+ description="Lieferscheine für den ausgewählten Tag."
|
|
|
+ breadcrumbs={breadcrumbsNode}
|
|
|
+ actions={actions}
|
|
|
+ >
|
|
|
+ <ExplorerSectionCard title="Dateiliste" description="Lade Daten…">
|
|
|
+ <ExplorerLoading variant="table" count={8} />
|
|
|
+ </ExplorerSectionCard>
|
|
|
+ </ExplorerPageShell>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (filesQuery.status === "error" && mapped) {
|
|
|
+ if (mapped.kind === "forbidden")
|
|
|
+ return <ForbiddenView attemptedBranch={branch} />;
|
|
|
+
|
|
|
+ if (mapped.kind === "notfound") {
|
|
|
+ return (
|
|
|
+ <ExplorerPageShell
|
|
|
+ title="Dateien"
|
|
|
+ description="Lieferscheine für den ausgewählten Tag."
|
|
|
+ breadcrumbs={breadcrumbsNode}
|
|
|
+ actions={actions}
|
|
|
+ >
|
|
|
+ <ExplorerNotFound
|
|
|
+ upHref={monthPath(branch, year, month)}
|
|
|
+ branchRootHref={branchPath(branch)}
|
|
|
+ />
|
|
|
+ </ExplorerPageShell>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mapped.kind === "unauthenticated") {
|
|
|
+ return (
|
|
|
+ <ExplorerPageShell
|
|
|
+ title="Dateien"
|
|
|
+ description="Sitzung abgelaufen — Weiterleitung zum Login…"
|
|
|
+ breadcrumbs={breadcrumbsNode}
|
|
|
+ >
|
|
|
+ <ExplorerSectionCard
|
|
|
+ title="Weiterleitung"
|
|
|
+ description="Bitte warten…"
|
|
|
+ >
|
|
|
+ <ExplorerLoading variant="table" count={6} />
|
|
|
+ </ExplorerSectionCard>
|
|
|
+ </ExplorerPageShell>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <ExplorerPageShell
|
|
|
+ title="Dateien"
|
|
|
+ description="Lieferscheine für den ausgewählten Tag."
|
|
|
+ breadcrumbs={breadcrumbsNode}
|
|
|
+ actions={actions}
|
|
|
+ >
|
|
|
+ <ExplorerSectionCard title="Dateiliste" description="Fehler">
|
|
|
+ <ExplorerError
|
|
|
+ title={mapped.title}
|
|
|
+ description={mapped.description}
|
|
|
+ onRetry={filesQuery.retry}
|
|
|
+ />
|
|
|
+ </ExplorerSectionCard>
|
|
|
+ </ExplorerPageShell>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ const files = Array.isArray(filesQuery.data?.files)
|
|
|
+ ? filesQuery.data.files
|
|
|
+ : [];
|
|
|
+ const sorted = sortFilesByNameAsc(files);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <ExplorerPageShell
|
|
|
+ title="Dateien"
|
|
|
+ description="Lieferscheine für den ausgewählten Tag."
|
|
|
+ breadcrumbs={breadcrumbsNode}
|
|
|
+ actions={actions}
|
|
|
+ >
|
|
|
+ <ExplorerSectionCard
|
|
|
+ title="Dateiliste"
|
|
|
+ description={`Niederlassung ${branch} • ${year}/${month}/${day}`}
|
|
|
+ headerRight={
|
|
|
+ <span className="rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground">
|
|
|
+ {sorted.length} Datei{sorted.length === 1 ? "" : "en"}
|
|
|
+ </span>
|
|
|
+ }
|
|
|
+ >
|
|
|
+ {sorted.length === 0 ? (
|
|
|
+ <ExplorerEmpty
|
|
|
+ title="Keine Dateien gefunden"
|
|
|
+ description="Für diesen Tag wurden keine Dateien gefunden."
|
|
|
+ upHref={monthPath(branch, year, month)}
|
|
|
+ />
|
|
|
+ ) : (
|
|
|
+ <Table>
|
|
|
+ <TableCaption>
|
|
|
+ Hinweis: Die PDF-Ansicht folgt in einem späteren Ticket (RHL-023).
|
|
|
+ </TableCaption>
|
|
|
+
|
|
|
+ <TableHeader>
|
|
|
+ <TableRow>
|
|
|
+ <TableHead>Datei</TableHead>
|
|
|
+ <TableHead className="hidden md:table-cell">Pfad</TableHead>
|
|
|
+ <TableHead className="text-right">Aktion</TableHead>
|
|
|
+ </TableRow>
|
|
|
+ </TableHeader>
|
|
|
+
|
|
|
+ <TableBody>
|
|
|
+ {sorted.map((f) => (
|
|
|
+ <TableRow key={f.relativePath || f.name}>
|
|
|
+ <TableCell className="min-w-0">
|
|
|
+ <div className="flex min-w-0 items-start gap-2">
|
|
|
+ <FileText className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
|
|
+ <div className="min-w-0">
|
|
|
+ <p className="truncate font-medium">{f.name}</p>
|
|
|
+ <p className="truncate text-xs text-muted-foreground md:hidden">
|
|
|
+ {f.relativePath}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </TableCell>
|
|
|
+
|
|
|
+ <TableCell className="hidden md:table-cell">
|
|
|
+ <span className="text-xs text-muted-foreground">
|
|
|
+ {f.relativePath}
|
|
|
+ </span>
|
|
|
+ </TableCell>
|
|
|
+
|
|
|
+ <TableCell className="text-right">
|
|
|
+ <Button
|
|
|
+ variant="outline"
|
|
|
+ size="sm"
|
|
|
+ disabled
|
|
|
+ aria-disabled="true"
|
|
|
+ title="PDF-Ansicht kommt bald"
|
|
|
+ >
|
|
|
+ <Eye className="h-4 w-4" />
|
|
|
+ Öffnen
|
|
|
+ </Button>
|
|
|
+ </TableCell>
|
|
|
+ </TableRow>
|
|
|
+ ))}
|
|
|
+ </TableBody>
|
|
|
+ </Table>
|
|
|
+ )}
|
|
|
+ </ExplorerSectionCard>
|
|
|
+ </ExplorerPageShell>
|
|
|
+ );
|
|
|
+}
|