SearchQueryBox.jsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. "use client";
  2. import React from "react";
  3. import { Clock3, Search } from "lucide-react";
  4. import { Button } from "@/components/ui/button";
  5. import { Input } from "@/components/ui/input";
  6. import { Label } from "@/components/ui/label";
  7. import {
  8. Popover,
  9. PopoverContent,
  10. PopoverTrigger,
  11. } from "@/components/ui/popover";
  12. import {
  13. Command,
  14. CommandGroup,
  15. CommandItem,
  16. CommandList,
  17. } from "@/components/ui/command";
  18. function getScopeSummary(entry) {
  19. if (entry?.scope === "all") return "Alle Niederlassungen";
  20. if (entry?.scope === "multi") {
  21. const count = Array.isArray(entry?.branches) ? entry.branches.length : 0;
  22. return count > 0
  23. ? `Mehrere Niederlassungen (${count})`
  24. : "Mehrere Niederlassungen";
  25. }
  26. return `Niederlassung ${entry?.routeBranch || ""}`.trim();
  27. }
  28. function getDateSummary(entry) {
  29. const from = typeof entry?.from === "string" ? entry.from.trim() : "";
  30. const to = typeof entry?.to === "string" ? entry.to.trim() : "";
  31. if (from && to) return `${from} bis ${to}`;
  32. if (from) return `ab ${from}`;
  33. if (to) return `bis ${to}`;
  34. return "";
  35. }
  36. function getEntryMetaSummary(entry) {
  37. const parts = [getScopeSummary(entry)];
  38. const dateSummary = getDateSummary(entry);
  39. if (dateSummary) parts.push(dateSummary);
  40. return parts.join(" • ");
  41. }
  42. export default function SearchQueryBox({
  43. qDraft,
  44. onQDraftChange,
  45. onSubmit,
  46. currentQuery,
  47. isSubmitting,
  48. canSearch,
  49. recentSearches,
  50. onSelectRecentSearch,
  51. onClearRecentSearches,
  52. }) {
  53. const [open, setOpen] = React.useState(false);
  54. const historyItems = Array.isArray(recentSearches) ? recentSearches : [];
  55. const hasHistory = historyItems.length > 0;
  56. const canClearHistory =
  57. hasHistory && typeof onClearRecentSearches === "function";
  58. React.useEffect(() => {
  59. if (hasHistory) return;
  60. setOpen(false);
  61. }, [hasHistory]);
  62. const handleInputFocus = React.useCallback(() => {
  63. const trimmedDraft = typeof qDraft === "string" ? qDraft.trim() : "";
  64. if (trimmedDraft) return;
  65. if (!hasHistory) return;
  66. setOpen(true);
  67. }, [qDraft, hasHistory]);
  68. return (
  69. // Important for flex layouts:
  70. // - w-full ensures the box fills its container width.
  71. // - min-w-0 allows the input row to shrink without overflow.
  72. <div className="grid w-full min-w-0 gap-2">
  73. <Label htmlFor="q">Suchbegriff</Label>
  74. <Popover open={open} onOpenChange={setOpen}>
  75. <div className="flex w-full min-w-0 flex-col gap-2 sm:flex-row sm:items-center">
  76. <Input
  77. id="q"
  78. name="q"
  79. value={qDraft}
  80. onChange={(e) => onQDraftChange(e.target.value)}
  81. onFocus={handleInputFocus}
  82. placeholder="z. B. Bridgestone, Rechnung, Kundennummer…"
  83. disabled={isSubmitting}
  84. // Make the input take the remaining space next to the buttons.
  85. className="flex-1 min-w-0"
  86. />
  87. <PopoverTrigger asChild>
  88. <Button
  89. type="button"
  90. variant="outline"
  91. size="icon"
  92. disabled={isSubmitting}
  93. aria-label="Letzte Suchen öffnen"
  94. title="Letzte Suchen öffnen"
  95. className="shrink-0"
  96. >
  97. <Clock3 className="h-4 w-4" />
  98. </Button>
  99. </PopoverTrigger>
  100. <Button
  101. type="submit"
  102. disabled={!canSearch || isSubmitting}
  103. className="shrink-0"
  104. >
  105. <Search className="h-4 w-4" />
  106. Suchen
  107. </Button>
  108. </div>
  109. <PopoverContent align="start" className="w-[22rem] p-0">
  110. {hasHistory ? (
  111. <div className="overflow-hidden">
  112. <Command>
  113. <CommandList className="max-h-72">
  114. <CommandGroup heading="Letzte Suchen">
  115. {historyItems.map((entry, index) => (
  116. <CommandItem
  117. key={`${entry.routeBranch}|${entry.q}|${entry.createdAt}|${index}`}
  118. value={`${entry.q} ${entry.routeBranch} ${entry.scope} ${(entry.branches || []).join(" ")}`}
  119. onSelect={() => {
  120. if (typeof onSelectRecentSearch !== "function") return;
  121. onSelectRecentSearch(entry);
  122. setOpen(false);
  123. }}
  124. className="items-start gap-3 py-2"
  125. >
  126. <Clock3 className="mt-0.5 h-4 w-4 text-muted-foreground" />
  127. <div className="min-w-0 flex-1">
  128. <p className="truncate text-sm font-medium">{entry.q}</p>
  129. <p className="truncate text-xs text-muted-foreground">
  130. {getEntryMetaSummary(entry)}
  131. </p>
  132. </div>
  133. </CommandItem>
  134. ))}
  135. </CommandGroup>
  136. </CommandList>
  137. </Command>
  138. <div className="border-t p-2">
  139. <Button
  140. type="button"
  141. variant="ghost"
  142. size="sm"
  143. className="w-full justify-start"
  144. disabled={!canClearHistory}
  145. onClick={() => {
  146. if (!canClearHistory) return;
  147. onClearRecentSearches();
  148. setOpen(false);
  149. }}
  150. >
  151. Verlauf löschen
  152. </Button>
  153. </div>
  154. </div>
  155. ) : (
  156. <div className="p-3 text-sm text-muted-foreground">
  157. Keine letzten Suchen vorhanden.
  158. </div>
  159. )}
  160. </PopoverContent>
  161. </Popover>
  162. {currentQuery ? (
  163. <div className="text-xs text-muted-foreground">
  164. Aktuelle Suche:{" "}
  165. <span className="ml-1 rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground">
  166. {currentQuery}
  167. </span>
  168. </div>
  169. ) : (
  170. <div className="text-xs text-muted-foreground">
  171. Tipp: Die Suche ist URL-basiert und kann als Link geteilt werden.
  172. </div>
  173. )}
  174. </div>
  175. );
  176. }