| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- "use client";
- import React from "react";
- import { Check, ChevronsUpDown, X } from "lucide-react";
- import { isValidBranchParam } from "@/lib/frontend/params";
- import { cn } from "@/lib/utils";
- import { Badge } from "@/components/ui/badge";
- import { Button } from "@/components/ui/button";
- import { Input } from "@/components/ui/input";
- import { Label } from "@/components/ui/label";
- import { Skeleton } from "@/components/ui/skeleton";
- import {
- Popover,
- PopoverContent,
- PopoverTrigger,
- } from "@/components/ui/popover";
- import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
- } from "@/components/ui/command";
- function normalizeTypedBranch(value) {
- if (typeof value !== "string") return null;
- const s = value.trim().toUpperCase();
- return s ? s : null;
- }
- export default function SearchMultiBranchPicker({
- branchesStatus,
- availableBranches,
- selectedBranches,
- onToggleBranch,
- isSubmitting,
- }) {
- const [open, setOpen] = React.useState(false);
- const [manual, setManual] = React.useState("");
- const listReady = branchesStatus === "ready";
- const listLoading = branchesStatus === "loading";
- const listError = branchesStatus === "error";
- const branchOptions = Array.isArray(availableBranches)
- ? availableBranches
- : [];
- const selected = Array.isArray(selectedBranches) ? selectedBranches : [];
- const selectedSet = React.useMemo(
- () => new Set(selected.map(String)),
- [selected]
- );
- const typed = normalizeTypedBranch(manual);
- const canAddTyped = Boolean(
- typed && isValidBranchParam(typed) && !selectedSet.has(typed)
- );
- return (
- <div className="rounded-lg border bg-card p-4 text-card-foreground shadow-sm">
- <div className="space-y-3">
- <p className="text-sm font-medium">Niederlassungen auswählen</p>
- <p className="text-xs text-muted-foreground">
- Wählen Sie eine oder mehrere Niederlassungen. Die Suche wird nach
- jeder Änderung aktualisiert.
- </p>
- <div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
- <div className="flex flex-col gap-2">
- <Label>Niederlassung hinzufügen</Label>
- {listLoading ? (
- <Skeleton className="h-9 w-full sm:w-[18rem]" />
- ) : (
- <Popover open={open} onOpenChange={setOpen}>
- <PopoverTrigger asChild>
- <Button
- type="button"
- variant="outline"
- role="combobox"
- aria-expanded={open}
- disabled={isSubmitting || !listReady}
- className="w-full justify-between sm:w-[18rem]"
- title={
- listReady
- ? "Niederlassung hinzufügen"
- : "Niederlassungen konnten nicht geladen werden"
- }
- >
- Niederlassung hinzufügen…
- <ChevronsUpDown className="h-4 w-4 opacity-70" />
- </Button>
- </PopoverTrigger>
- <PopoverContent align="start" className="w-[18rem] p-0">
- <Command>
- <CommandInput placeholder="Suchen…" />
- <CommandList>
- <CommandEmpty>Keine Treffer.</CommandEmpty>
- <CommandGroup>
- {branchOptions.map((b) => {
- const id = String(b);
- const isSelected = selectedSet.has(id);
- return (
- <CommandItem
- key={id}
- value={id}
- onSelect={(value) => {
- const next = normalizeTypedBranch(value);
- if (!next || !isValidBranchParam(next)) return;
- if (!selectedSet.has(next))
- onToggleBranch(next);
- setOpen(false);
- }}
- >
- <Check
- className={cn(
- "mr-2 h-4 w-4",
- isSelected ? "opacity-100" : "opacity-0"
- )}
- />
- {id}
- </CommandItem>
- );
- })}
- </CommandGroup>
- </CommandList>
- </Command>
- </PopoverContent>
- </Popover>
- )}
- {listError ? (
- <div className="space-y-2">
- <p className="text-xs text-muted-foreground">
- Niederlassungen konnten nicht geladen werden. Sie können NLxx
- manuell hinzufügen.
- </p>
- <div className="flex gap-2">
- <Input
- value={manual}
- onChange={(e) => setManual(e.target.value)}
- onKeyDown={(e) => {
- if (e.key !== "Enter") return;
- if (!canAddTyped) return;
- e.preventDefault();
- onToggleBranch(typed);
- setManual("");
- }}
- placeholder="z. B. NL17"
- disabled={isSubmitting}
- />
- <Button
- type="button"
- variant="outline"
- disabled={!canAddTyped || isSubmitting}
- onClick={() => {
- if (!canAddTyped) return;
- onToggleBranch(typed);
- setManual("");
- }}
- >
- Hinzufügen
- </Button>
- </div>
- </div>
- ) : null}
- </div>
- <div className="w-full sm:w-auto">
- <Label>Ausgewählt ({selected.length})</Label>
- {selected.length === 0 ? (
- <p className="mt-2 text-xs text-muted-foreground">
- Keine Niederlassungen ausgewählt.
- </p>
- ) : (
- <div className="mt-2 flex flex-wrap gap-2">
- {selected.map((b) => (
- <Badge key={b} variant="secondary" className="gap-1">
- <span>{b}</span>
- <button
- type="button"
- className="ml-1 inline-flex h-4 w-4 items-center justify-center rounded-sm hover:bg-muted/60"
- onClick={() => onToggleBranch(b)}
- disabled={isSubmitting}
- aria-label={`Entfernen: ${b}`}
- title={`Entfernen: ${b}`}
- >
- <X className="h-3 w-3" />
- </button>
- </Badge>
- ))}
- </div>
- )}
- </div>
- </div>
- </div>
- </div>
- );
- }
|