SearchMultiBranchPicker.jsx 5.1 KB

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