SearchMultiBranchPicker.jsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. "use client";
  2. import React from "react";
  3. import { Check, ChevronsUpDown, X } from "lucide-react";
  4. import { isValidBranchParam } from "@/lib/frontend/params";
  5. import { cn } from "@/lib/utils";
  6. import { Badge } from "@/components/ui/badge";
  7. import { Button } from "@/components/ui/button";
  8. import { Input } from "@/components/ui/input";
  9. import { Label } from "@/components/ui/label";
  10. import { Skeleton } from "@/components/ui/skeleton";
  11. import {
  12. Popover,
  13. PopoverContent,
  14. PopoverTrigger,
  15. } from "@/components/ui/popover";
  16. import {
  17. Command,
  18. CommandEmpty,
  19. CommandGroup,
  20. CommandInput,
  21. CommandItem,
  22. CommandList,
  23. } from "@/components/ui/command";
  24. function normalizeTypedBranch(value) {
  25. if (typeof value !== "string") return null;
  26. const s = value.trim().toUpperCase();
  27. return s ? s : null;
  28. }
  29. export default function SearchMultiBranchPicker({
  30. branchesStatus,
  31. availableBranches,
  32. selectedBranches,
  33. onToggleBranch,
  34. isSubmitting,
  35. }) {
  36. const [open, setOpen] = React.useState(false);
  37. const [manual, setManual] = React.useState("");
  38. const listReady = branchesStatus === "ready";
  39. const listLoading = branchesStatus === "loading";
  40. const listError = branchesStatus === "error";
  41. const branchOptions = Array.isArray(availableBranches)
  42. ? availableBranches
  43. : [];
  44. const selected = Array.isArray(selectedBranches) ? selectedBranches : [];
  45. const selectedSet = React.useMemo(
  46. () => new Set(selected.map(String)),
  47. [selected]
  48. );
  49. const typed = normalizeTypedBranch(manual);
  50. const canAddTyped = Boolean(
  51. typed && isValidBranchParam(typed) && !selectedSet.has(typed)
  52. );
  53. return (
  54. <div className="rounded-lg border bg-card p-4 text-card-foreground shadow-sm">
  55. <div className="space-y-3">
  56. <p className="text-sm font-medium">Niederlassungen auswählen</p>
  57. <p className="text-xs text-muted-foreground">
  58. Wählen Sie eine oder mehrere Niederlassungen. Die Suche wird nach
  59. jeder Änderung aktualisiert.
  60. </p>
  61. <div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
  62. <div className="flex flex-col gap-2">
  63. <Label>Niederlassung hinzufügen</Label>
  64. {listLoading ? (
  65. <Skeleton className="h-9 w-full sm:w-[18rem]" />
  66. ) : (
  67. <Popover open={open} onOpenChange={setOpen}>
  68. <PopoverTrigger asChild>
  69. <Button
  70. type="button"
  71. variant="outline"
  72. role="combobox"
  73. aria-expanded={open}
  74. disabled={isSubmitting || !listReady}
  75. className="w-full justify-between sm:w-[18rem]"
  76. title={
  77. listReady
  78. ? "Niederlassung hinzufügen"
  79. : "Niederlassungen konnten nicht geladen werden"
  80. }
  81. >
  82. Niederlassung hinzufügen…
  83. <ChevronsUpDown className="h-4 w-4 opacity-70" />
  84. </Button>
  85. </PopoverTrigger>
  86. <PopoverContent align="start" className="w-[18rem] p-0">
  87. <Command>
  88. <CommandInput placeholder="Suchen…" />
  89. <CommandList>
  90. <CommandEmpty>Keine Treffer.</CommandEmpty>
  91. <CommandGroup>
  92. {branchOptions.map((b) => {
  93. const id = String(b);
  94. const isSelected = selectedSet.has(id);
  95. return (
  96. <CommandItem
  97. key={id}
  98. value={id}
  99. onSelect={(value) => {
  100. const next = normalizeTypedBranch(value);
  101. if (!next || !isValidBranchParam(next)) return;
  102. if (!selectedSet.has(next))
  103. onToggleBranch(next);
  104. setOpen(false);
  105. }}
  106. >
  107. <Check
  108. className={cn(
  109. "mr-2 h-4 w-4",
  110. isSelected ? "opacity-100" : "opacity-0"
  111. )}
  112. />
  113. {id}
  114. </CommandItem>
  115. );
  116. })}
  117. </CommandGroup>
  118. </CommandList>
  119. </Command>
  120. </PopoverContent>
  121. </Popover>
  122. )}
  123. {listError ? (
  124. <div className="space-y-2">
  125. <p className="text-xs text-muted-foreground">
  126. Niederlassungen konnten nicht geladen werden. Sie können NLxx
  127. manuell hinzufügen.
  128. </p>
  129. <div className="flex gap-2">
  130. <Input
  131. value={manual}
  132. onChange={(e) => setManual(e.target.value)}
  133. onKeyDown={(e) => {
  134. if (e.key !== "Enter") return;
  135. if (!canAddTyped) return;
  136. e.preventDefault();
  137. onToggleBranch(typed);
  138. setManual("");
  139. }}
  140. placeholder="z. B. NL17"
  141. disabled={isSubmitting}
  142. />
  143. <Button
  144. type="button"
  145. variant="outline"
  146. disabled={!canAddTyped || isSubmitting}
  147. onClick={() => {
  148. if (!canAddTyped) return;
  149. onToggleBranch(typed);
  150. setManual("");
  151. }}
  152. >
  153. Hinzufügen
  154. </Button>
  155. </div>
  156. </div>
  157. ) : null}
  158. </div>
  159. <div className="w-full sm:w-auto">
  160. <Label>Ausgewählt ({selected.length})</Label>
  161. {selected.length === 0 ? (
  162. <p className="mt-2 text-xs text-muted-foreground">
  163. Keine Niederlassungen ausgewählt.
  164. </p>
  165. ) : (
  166. <div className="mt-2 flex flex-wrap gap-2">
  167. {selected.map((b) => (
  168. <Badge key={b} variant="secondary" className="gap-1">
  169. <span>{b}</span>
  170. <button
  171. type="button"
  172. className="ml-1 inline-flex h-4 w-4 items-center justify-center rounded-sm hover:bg-muted/60"
  173. onClick={() => onToggleBranch(b)}
  174. disabled={isSubmitting}
  175. aria-label={`Entfernen: ${b}`}
  176. title={`Entfernen: ${b}`}
  177. >
  178. <X className="h-3 w-3" />
  179. </button>
  180. </Badge>
  181. ))}
  182. </div>
  183. )}
  184. </div>
  185. </div>
  186. </div>
  187. </div>
  188. );
  189. }