SearchResultsTable.jsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. "use client";
  2. import React from "react";
  3. import Link from "next/link";
  4. import { Eye, FolderOpen } from "lucide-react";
  5. import { dayPath } from "@/lib/frontend/routes";
  6. import { buildPdfUrl } from "@/lib/frontend/explorer/pdfUrl";
  7. import { formatSearchItemDateDe } from "@/lib/frontend/search/resultsSorting";
  8. import { Button } from "@/components/ui/button";
  9. import {
  10. Table,
  11. TableBody,
  12. TableCaption,
  13. TableCell,
  14. TableHead,
  15. TableHeader,
  16. TableRow,
  17. } from "@/components/ui/table";
  18. export default function SearchResultsTable({ routeBranch, scope, items }) {
  19. // NOTE:
  20. // - We intentionally keep the column set stable:
  21. // Branch | Date | File | Path | Actions
  22. // - For single-scope searches, all rows will show the same branch (routeBranch),
  23. // but the consistent layout improves scanability and avoids “column jumping”.
  24. const list = Array.isArray(items) ? items : [];
  25. return (
  26. <Table className="table-fixed">
  27. <TableCaption>
  28. Hinweis: PDFs werden in einem neuen Tab geöffnet.
  29. </TableCaption>
  30. <TableHeader>
  31. <TableRow>
  32. {/* Fixed-width columns keep the layout stable and prevent “action column drift”. */}
  33. <TableHead className="w-24">Niederlassung</TableHead>
  34. <TableHead className="w-28">Datum</TableHead>
  35. {/* Flexible columns: these must truncate to prevent horizontal scrolling. */}
  36. <TableHead>Datei</TableHead>
  37. <TableHead className="hidden md:table-cell">Pfad</TableHead>
  38. {/* Fixed actions column so buttons are always reachable without scrolling. */}
  39. <TableHead className="w-40 text-right">Aktionen</TableHead>
  40. </TableRow>
  41. </TableHeader>
  42. <TableBody>
  43. {list.map((it) => {
  44. const itemBranch = String(it?.branch || routeBranch);
  45. const year = String(it?.year || "");
  46. const month = String(it?.month || "");
  47. const day = String(it?.day || "");
  48. const filename = String(it?.filename || "");
  49. const relativePath = String(it?.relativePath || "");
  50. // Binary PDF endpoint: open in a new tab (do NOT fetch via apiClient).
  51. const pdfUrl = buildPdfUrl({
  52. branch: itemBranch,
  53. year,
  54. month,
  55. day,
  56. filename,
  57. });
  58. const dayHref = dayPath(itemBranch, year, month, day);
  59. // Stable key: relativePath is best; fallback is deterministic.
  60. const key =
  61. relativePath || `${itemBranch}/${year}/${month}/${day}/${filename}`;
  62. return (
  63. <TableRow key={key}>
  64. <TableCell className="w-24">
  65. <span className="text-sm">{itemBranch}</span>
  66. </TableCell>
  67. <TableCell className="w-28">
  68. <span className="text-sm">{formatSearchItemDateDe(it)}</span>
  69. </TableCell>
  70. {/* File column: must be shrinkable so truncate works in table-fixed layout. */}
  71. <TableCell className="min-w-0">
  72. <div className="min-w-0">
  73. <p className="truncate font-medium" title={filename}>
  74. {filename}
  75. </p>
  76. {/* Mobile: show path as secondary line instead of a dedicated column */}
  77. <p
  78. className="truncate text-xs text-muted-foreground md:hidden"
  79. title={relativePath}
  80. >
  81. {relativePath}
  82. </p>
  83. </div>
  84. </TableCell>
  85. {/* Path column (desktop): always truncate + title for full hover read. */}
  86. <TableCell className="hidden md:table-cell min-w-0">
  87. <span
  88. className="block max-w-full truncate text-xs text-muted-foreground"
  89. title={relativePath}
  90. >
  91. {relativePath}
  92. </span>
  93. </TableCell>
  94. {/* Actions: fixed-width column, buttons stacked to keep width small and stable. */}
  95. <TableCell className="w-40">
  96. <div className="flex gap-2">
  97. <Button
  98. variant="outline"
  99. size="sm"
  100. asChild
  101. className="w-36 justify-start"
  102. >
  103. <a
  104. href={pdfUrl}
  105. target="_blank"
  106. rel="noopener noreferrer"
  107. aria-label={`PDF öffnen: ${filename}`}
  108. title={`PDF öffnen: ${filename}`}
  109. >
  110. <Eye className="h-4 w-4" />
  111. Öffnen
  112. </a>
  113. </Button>
  114. <Button
  115. variant="outline"
  116. size="sm"
  117. asChild
  118. className="w-36 justify-start"
  119. >
  120. <Link href={dayHref} title="Zum Ordner">
  121. <FolderOpen className="h-4 w-4" />
  122. Zum Ordner
  123. </Link>
  124. </Button>
  125. </div>
  126. </TableCell>
  127. </TableRow>
  128. );
  129. })}
  130. </TableBody>
  131. </Table>
  132. );
  133. }