SearchSingleBranchCombobox.jsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. "use client";
  2. import React from "react";
  3. import { Check, ChevronsUpDown } from "lucide-react";
  4. import { isValidBranchParam } from "@/lib/frontend/params";
  5. import { cn } from "@/lib/utils";
  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. import {
  11. Popover,
  12. PopoverContent,
  13. PopoverTrigger,
  14. } from "@/components/ui/popover";
  15. import {
  16. Command,
  17. CommandEmpty,
  18. CommandGroup,
  19. CommandInput,
  20. CommandItem,
  21. CommandList,
  22. } from "@/components/ui/command";
  23. function normalizeTypedBranch(value) {
  24. if (typeof value !== "string") return null;
  25. const s = value.trim().toUpperCase();
  26. return s ? s : null;
  27. }
  28. export default function SearchSingleBranchCombobox({
  29. branch,
  30. branchesStatus,
  31. availableBranches,
  32. onSingleBranchChange,
  33. isSubmitting,
  34. }) {
  35. const [open, setOpen] = React.useState(false);
  36. const [manual, setManual] = React.useState("");
  37. const listReady = branchesStatus === "ready";
  38. const listLoading = branchesStatus === "loading";
  39. const listError = branchesStatus === "error";
  40. const branchOptions = Array.isArray(availableBranches)
  41. ? availableBranches
  42. : [];
  43. const typed = normalizeTypedBranch(manual);
  44. const canApply = Boolean(typed && isValidBranchParam(typed));
  45. return (
  46. <div className="grid gap-2">
  47. <Label>Niederlassung</Label>
  48. <Popover open={open} onOpenChange={setOpen}>
  49. <PopoverTrigger asChild>
  50. <Button
  51. type="button"
  52. variant="outline"
  53. role="combobox"
  54. aria-expanded={open}
  55. disabled={isSubmitting}
  56. className="w-full justify-between sm:w-40"
  57. title="Niederlassung auswählen"
  58. >
  59. {branch}
  60. <ChevronsUpDown className="h-4 w-4 opacity-70" />
  61. </Button>
  62. </PopoverTrigger>
  63. <PopoverContent align="start" className="w-40 p-0">
  64. {listReady ? (
  65. <Command>
  66. <CommandInput placeholder="Suche…" />
  67. <CommandList>
  68. <CommandEmpty>Keine Treffer.</CommandEmpty>
  69. <CommandGroup>
  70. {branchOptions.map((b) => {
  71. const id = String(b);
  72. const isActive = id === branch;
  73. return (
  74. <CommandItem
  75. key={id}
  76. value={id}
  77. onSelect={(value) => {
  78. if (typeof onSingleBranchChange !== "function")
  79. return;
  80. const next = normalizeTypedBranch(value);
  81. if (!next || !isValidBranchParam(next)) return;
  82. if (next !== branch) onSingleBranchChange(next);
  83. setOpen(false);
  84. }}
  85. >
  86. <Check
  87. className={cn(
  88. "mr-2 h-4 w-4",
  89. isActive ? "opacity-100" : "opacity-0"
  90. )}
  91. />
  92. {id}
  93. </CommandItem>
  94. );
  95. })}
  96. </CommandGroup>
  97. </CommandList>
  98. </Command>
  99. ) : listLoading ? (
  100. <div className="space-y-2 p-3">
  101. {Array.from({ length: 6 }).map((_, i) => (
  102. <div key={i} className="flex items-center gap-2">
  103. <Skeleton className="h-4 w-4" />
  104. <Skeleton className="h-4 w-16" />
  105. </div>
  106. ))}
  107. </div>
  108. ) : (
  109. <div className="space-y-3 p-3">
  110. <p className="text-xs text-muted-foreground">
  111. Niederlassungen konnten nicht geladen werden. Sie können NLxx
  112. manuell eingeben.
  113. </p>
  114. <div className="flex gap-2">
  115. <Input
  116. value={manual}
  117. onChange={(e) => setManual(e.target.value)}
  118. onKeyDown={(e) => {
  119. if (e.key !== "Enter") return;
  120. if (!canApply) return;
  121. e.preventDefault();
  122. if (typeof onSingleBranchChange !== "function") return;
  123. onSingleBranchChange(typed);
  124. setManual("");
  125. setOpen(false);
  126. }}
  127. placeholder="z. B. NL17"
  128. disabled={isSubmitting}
  129. />
  130. <Button
  131. type="button"
  132. variant="outline"
  133. disabled={!canApply || isSubmitting}
  134. onClick={() => {
  135. if (!canApply) return;
  136. if (typeof onSingleBranchChange !== "function") return;
  137. onSingleBranchChange(typed);
  138. setManual("");
  139. setOpen(false);
  140. }}
  141. >
  142. Wechseln
  143. </Button>
  144. </div>
  145. </div>
  146. )}
  147. </PopoverContent>
  148. </Popover>
  149. </div>
  150. );
  151. }