SearchResultsTable.jsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  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. // UI fix:
  26. // - In `table-fixed` layouts, the action column must be wide enough for two buttons.
  27. // - We keep the buttons content-sized (no hard w-36 per button) and prevent shrink.
  28. const ACTIONS_COL_WIDTH = "w-56"; // ~14rem (tweak to w-52/w-60 if you prefer)
  29. return (
  30. <Table className="table-fixed">
  31. <TableCaption>
  32. Hinweis: PDFs werden in einem neuen Tab geöffnet.
  33. </TableCaption>
  34. <TableHeader>
  35. <TableRow>
  36. {/* Fixed-width columns keep the layout stable and prevent “action column drift”. */}
  37. <TableHead className="w-24">Niederlassung</TableHead>
  38. <TableHead className="w-28">Datum</TableHead>
  39. {/* Flexible columns: these must truncate to prevent horizontal scrolling. */}
  40. <TableHead>Datei</TableHead>
  41. <TableHead className="hidden md:table-cell">Pfad</TableHead>
  42. {/* Fixed actions column: wide enough for two buttons side-by-side */}
  43. <TableHead className={`${ACTIONS_COL_WIDTH} text-right`}>
  44. Aktionen
  45. </TableHead>
  46. </TableRow>
  47. </TableHeader>
  48. <TableBody>
  49. {list.map((it) => {
  50. const itemBranch = String(it?.branch || routeBranch);
  51. const year = String(it?.year || "");
  52. const month = String(it?.month || "");
  53. const day = String(it?.day || "");
  54. const filename = String(it?.filename || "");
  55. const relativePath = String(it?.relativePath || "");
  56. // Binary PDF endpoint: open in a new tab (do NOT fetch via apiClient).
  57. const pdfUrl = buildPdfUrl({
  58. branch: itemBranch,
  59. year,
  60. month,
  61. day,
  62. filename,
  63. });
  64. const dayHref = dayPath(itemBranch, year, month, day);
  65. // Stable key: relativePath is best; fallback is deterministic.
  66. const key =
  67. relativePath || `${itemBranch}/${year}/${month}/${day}/${filename}`;
  68. return (
  69. <TableRow key={key}>
  70. <TableCell className="w-24">
  71. <span className="text-sm">{itemBranch}</span>
  72. </TableCell>
  73. <TableCell className="w-28">
  74. <span className="text-sm">{formatSearchItemDateDe(it)}</span>
  75. </TableCell>
  76. {/* File column: must be shrinkable so truncate works in table-fixed layout. */}
  77. <TableCell className="min-w-0">
  78. <div className="min-w-0">
  79. <p className="truncate font-medium" title={filename}>
  80. {filename}
  81. </p>
  82. {/* Mobile: show path as secondary line instead of a dedicated column */}
  83. <p
  84. className="truncate text-xs text-muted-foreground md:hidden"
  85. title={relativePath}
  86. >
  87. {relativePath}
  88. </p>
  89. </div>
  90. </TableCell>
  91. {/* Path column (desktop): always truncate + title for full hover read. */}
  92. <TableCell className="hidden md:table-cell min-w-0">
  93. <span
  94. className="block max-w-full truncate text-xs text-muted-foreground"
  95. title={relativePath}
  96. >
  97. {relativePath}
  98. </span>
  99. </TableCell>
  100. {/* Actions: side-by-side, right-aligned, no fixed per-button width */}
  101. <TableCell className={ACTIONS_COL_WIDTH}>
  102. <div className="flex items-center justify-end gap-2">
  103. <Button
  104. variant="outline"
  105. size="sm"
  106. asChild
  107. className="shrink-0"
  108. >
  109. <a
  110. href={pdfUrl}
  111. target="_blank"
  112. rel="noopener noreferrer"
  113. aria-label={`PDF öffnen: ${filename}`}
  114. title={`PDF öffnen: ${filename}`}
  115. >
  116. <Eye className="h-4 w-4" />
  117. Öffnen
  118. </a>
  119. </Button>
  120. <Button
  121. variant="outline"
  122. size="sm"
  123. asChild
  124. className="shrink-0"
  125. >
  126. <Link href={dayHref} title="Zum Ordner">
  127. <FolderOpen className="h-4 w-4" />
  128. Zum Ordner
  129. </Link>
  130. </Button>
  131. </div>
  132. </TableCell>
  133. </TableRow>
  134. );
  135. })}
  136. </TableBody>
  137. </Table>
  138. );
  139. }