SearchForm.jsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. "use client";
  2. import React from "react";
  3. import { ChevronDown, Search } from "lucide-react";
  4. import {
  5. SEARCH_SCOPE,
  6. SEARCH_LIMITS,
  7. DEFAULT_SEARCH_LIMIT,
  8. } from "@/lib/frontend/search/urlState";
  9. import { Button } from "@/components/ui/button";
  10. import { Input } from "@/components/ui/input";
  11. import { Label } from "@/components/ui/label";
  12. import { Skeleton } from "@/components/ui/skeleton";
  13. import {
  14. DropdownMenu,
  15. DropdownMenuContent,
  16. DropdownMenuLabel,
  17. DropdownMenuRadioGroup,
  18. DropdownMenuRadioItem,
  19. DropdownMenuSeparator,
  20. DropdownMenuTrigger,
  21. } from "@/components/ui/dropdown-menu";
  22. const SCOPE_LABELS = Object.freeze({
  23. [SEARCH_SCOPE.SINGLE]: "Diese Niederlassung",
  24. [SEARCH_SCOPE.MULTI]: "Mehrere Niederlassungen",
  25. [SEARCH_SCOPE.ALL]: "Alle Niederlassungen",
  26. });
  27. export default function SearchForm({
  28. branch,
  29. qDraft,
  30. onQDraftChange,
  31. onSubmit,
  32. currentQuery,
  33. isSubmitting,
  34. isAdminDev,
  35. scope,
  36. onScopeChange,
  37. availableBranches,
  38. branchesStatus,
  39. selectedBranches,
  40. onToggleBranch,
  41. limit,
  42. onLimitChange,
  43. }) {
  44. const canSearch = typeof qDraft === "string" && qDraft.trim().length > 0;
  45. const scopeLabel = SCOPE_LABELS[scope] || "Unbekannt";
  46. const normalizedLimit =
  47. Number.isInteger(limit) && SEARCH_LIMITS.includes(limit)
  48. ? limit
  49. : DEFAULT_SEARCH_LIMIT;
  50. return (
  51. <div className="space-y-4">
  52. <form
  53. onSubmit={(e) => {
  54. e.preventDefault();
  55. if (!canSearch) return;
  56. onSubmit();
  57. }}
  58. className="space-y-3"
  59. >
  60. <div className="grid gap-2">
  61. <Label htmlFor="q">Suchbegriff</Label>
  62. <div className="flex flex-col gap-2 sm:flex-row sm:items-center">
  63. <Input
  64. id="q"
  65. name="q"
  66. value={qDraft}
  67. onChange={(e) => onQDraftChange(e.target.value)}
  68. placeholder="z. B. Bridgestone, Rechnung, Kundennummer…"
  69. disabled={isSubmitting}
  70. />
  71. <Button type="submit" disabled={!canSearch || isSubmitting}>
  72. <Search className="h-4 w-4" />
  73. Suchen
  74. </Button>
  75. </div>
  76. {currentQuery ? (
  77. <div className="text-xs text-muted-foreground">
  78. Aktuelle Suche:{" "}
  79. <span className="ml-1 rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground">
  80. {currentQuery}
  81. </span>
  82. </div>
  83. ) : (
  84. <div className="text-xs text-muted-foreground">
  85. Tipp: Die Suche ist URL-basiert und kann als Link geteilt werden.
  86. </div>
  87. )}
  88. </div>
  89. <div className="flex flex-wrap items-center gap-2">
  90. {isAdminDev ? (
  91. <div className="grid gap-2">
  92. <Label>Suchbereich</Label>
  93. <DropdownMenu>
  94. <DropdownMenuTrigger asChild>
  95. <Button
  96. type="button"
  97. variant="outline"
  98. disabled={isSubmitting}
  99. title="Suchbereich auswählen"
  100. >
  101. {scopeLabel}
  102. <ChevronDown className="h-4 w-4" />
  103. </Button>
  104. </DropdownMenuTrigger>
  105. <DropdownMenuContent align="start" className="min-w-[16rem]">
  106. <DropdownMenuLabel>Suchbereich</DropdownMenuLabel>
  107. <DropdownMenuSeparator />
  108. <DropdownMenuRadioGroup
  109. value={scope}
  110. onValueChange={(value) => onScopeChange(value)}
  111. >
  112. <DropdownMenuRadioItem value={SEARCH_SCOPE.SINGLE}>
  113. Diese Niederlassung ({branch})
  114. </DropdownMenuRadioItem>
  115. <DropdownMenuRadioItem value={SEARCH_SCOPE.MULTI}>
  116. Mehrere Niederlassungen
  117. </DropdownMenuRadioItem>
  118. <DropdownMenuRadioItem value={SEARCH_SCOPE.ALL}>
  119. Alle Niederlassungen
  120. </DropdownMenuRadioItem>
  121. </DropdownMenuRadioGroup>
  122. </DropdownMenuContent>
  123. </DropdownMenu>
  124. </div>
  125. ) : null}
  126. <div className="grid gap-2">
  127. <Label>Treffer pro Seite</Label>
  128. <DropdownMenu>
  129. <DropdownMenuTrigger asChild>
  130. <Button
  131. type="button"
  132. variant="outline"
  133. disabled={isSubmitting}
  134. title="Treffer pro Seite auswählen"
  135. >
  136. {normalizedLimit}
  137. <ChevronDown className="h-4 w-4" />
  138. </Button>
  139. </DropdownMenuTrigger>
  140. <DropdownMenuContent align="start" className="min-w-[12rem]">
  141. <DropdownMenuLabel>Treffer pro Seite</DropdownMenuLabel>
  142. <DropdownMenuSeparator />
  143. <DropdownMenuRadioGroup
  144. value={String(normalizedLimit)}
  145. onValueChange={(value) => {
  146. const n = Number(value);
  147. if (!Number.isInteger(n)) return;
  148. onLimitChange(n);
  149. }}
  150. >
  151. {SEARCH_LIMITS.map((n) => (
  152. <DropdownMenuRadioItem key={n} value={String(n)}>
  153. {n}
  154. </DropdownMenuRadioItem>
  155. ))}
  156. </DropdownMenuRadioGroup>
  157. </DropdownMenuContent>
  158. </DropdownMenu>
  159. </div>
  160. {isAdminDev && scope === SEARCH_SCOPE.SINGLE ? (
  161. <span className="pt-6 text-xs text-muted-foreground">
  162. Aktiv: {branch}
  163. </span>
  164. ) : null}
  165. </div>
  166. </form>
  167. {isAdminDev && scope === SEARCH_SCOPE.MULTI ? (
  168. <div className="rounded-lg border bg-card p-4 text-card-foreground shadow-sm">
  169. <div className="space-y-2">
  170. <p className="text-sm font-medium">Niederlassungen auswählen</p>
  171. <p className="text-xs text-muted-foreground">
  172. Wählen Sie eine oder mehrere Niederlassungen. Die Suche wird nach
  173. jeder Änderung aktualisiert.
  174. </p>
  175. {branchesStatus === "loading" ? (
  176. <div className="grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-4">
  177. {Array.from({ length: 8 }).map((_, i) => (
  178. <div key={i} className="flex items-center gap-2">
  179. <Skeleton className="h-4 w-4" />
  180. <Skeleton className="h-4 w-16" />
  181. </div>
  182. ))}
  183. </div>
  184. ) : branchesStatus === "error" ? (
  185. <p className="text-sm text-muted-foreground">
  186. Niederlassungen konnten nicht geladen werden.
  187. </p>
  188. ) : (
  189. <div className="grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-4">
  190. {(Array.isArray(availableBranches)
  191. ? availableBranches
  192. : []
  193. ).map((b) => {
  194. const checked = Array.isArray(selectedBranches)
  195. ? selectedBranches.includes(b)
  196. : false;
  197. return (
  198. <label
  199. key={b}
  200. className="flex items-center gap-2 rounded-md px-2 py-1 hover:bg-muted/50"
  201. >
  202. <input
  203. type="checkbox"
  204. checked={checked}
  205. onChange={() => onToggleBranch(b)}
  206. disabled={isSubmitting}
  207. />
  208. <span className="text-sm">{b}</span>
  209. </label>
  210. );
  211. })}
  212. </div>
  213. )}
  214. </div>
  215. </div>
  216. ) : null}
  217. </div>
  218. );
  219. }