| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306 |
- "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 { useDebouncedVisibility } from "@/lib/frontend/hooks/useDebouncedVisibility";
- import { buildPdfUrl } from "@/lib/frontend/explorer/pdfUrl";
- 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";
- const LOADING_DELAY_MS = 300;
- 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],
- );
- const showLoadingUi = useDebouncedVisibility(
- filesQuery.status === "loading",
- {
- delayMs: LOADING_DELAY_MS,
- minVisibleMs: 0,
- },
- );
- 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 (showLoadingUi) {
- 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 === "loading") {
- return (
- <ExplorerPageShell
- title="Dateien"
- description="Lieferscheine für den ausgewählten Tag."
- breadcrumbs={breadcrumbsNode}
- actions={actions}
- >
- <div className="h-16" aria-hidden="true" />
- </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}
- >
- <div className="h-16" aria-hidden="true" />
- </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: PDFs werden in einem neuen Tab geöffnet.
- </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) => {
- const pdfUrl = buildPdfUrl({
- branch,
- year,
- month,
- day,
- filename: f.name,
- });
- return (
- <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" asChild>
- <a
- href={pdfUrl}
- target="_blank"
- rel="noopener noreferrer"
- aria-label={`PDF öffnen: ${f.name}`}
- title={`PDF öffnen: ${f.name}`}
- >
- <Eye className="h-4 w-4" />
- Öffnen
- </a>
- </Button>
- </TableCell>
- </TableRow>
- );
- })}
- </TableBody>
- </Table>
- )}
- </ExplorerSectionCard>
- </ExplorerPageShell>
- );
- }
|