|
|
@@ -1,29 +1,15 @@
|
|
|
"use client";
|
|
|
|
|
|
import React from "react";
|
|
|
-import { Check, ChevronsUpDown, X } from "lucide-react";
|
|
|
+import { 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;
|
|
|
@@ -36,11 +22,9 @@ export default function SearchMultiBranchPicker({
|
|
|
availableBranches,
|
|
|
selectedBranches,
|
|
|
onToggleBranch,
|
|
|
+ onClearAllBranches,
|
|
|
isSubmitting,
|
|
|
}) {
|
|
|
- const [open, setOpen] = React.useState(false);
|
|
|
- const [manual, setManual] = React.useState("");
|
|
|
-
|
|
|
const listReady = branchesStatus === "ready";
|
|
|
const listLoading = branchesStatus === "loading";
|
|
|
const listError = branchesStatus === "error";
|
|
|
@@ -55,154 +39,152 @@ export default function SearchMultiBranchPicker({
|
|
|
[selected]
|
|
|
);
|
|
|
|
|
|
+ const canClearAll =
|
|
|
+ typeof onClearAllBranches === "function" &&
|
|
|
+ selected.length > 0 &&
|
|
|
+ !isSubmitting;
|
|
|
+
|
|
|
+ // Fail-open manual add (only relevant if branch list failed)
|
|
|
+ const [manual, setManual] = React.useState("");
|
|
|
const typed = normalizeTypedBranch(manual);
|
|
|
- const canAddTyped = Boolean(
|
|
|
- typed && isValidBranchParam(typed) && !selectedSet.has(typed)
|
|
|
- );
|
|
|
+
|
|
|
+ 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">
|
|
|
+ <div className="flex items-start justify-between gap-4">
|
|
|
+ <div className="space-y-1">
|
|
|
+ <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>
|
|
|
+
|
|
|
+ <Button
|
|
|
+ type="button"
|
|
|
+ variant="outline"
|
|
|
+ size="sm"
|
|
|
+ disabled={!canClearAll}
|
|
|
+ onClick={() => {
|
|
|
+ if (!canClearAll) return;
|
|
|
+ onClearAllBranches();
|
|
|
+ }}
|
|
|
+ title="Alle Niederlassungen abwählen"
|
|
|
+ >
|
|
|
+ Alle abwählen
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {listLoading ? (
|
|
|
+ <div className="grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-5">
|
|
|
+ {Array.from({ length: 15 }).map((_, i) => (
|
|
|
+ <div key={i} className="flex items-center gap-2">
|
|
|
+ <Skeleton className="h-4 w-4" />
|
|
|
+ <Skeleton className="h-4 w-14" />
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ ) : null}
|
|
|
+
|
|
|
+ {listError ? (
|
|
|
+ <div className="space-y-3">
|
|
|
+ <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 className="space-y-2">
|
|
|
+ <Label>Ausgewählt ({selected.length})</Label>
|
|
|
+
|
|
|
+ {selected.length === 0 ? (
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
- Niederlassungen konnten nicht geladen werden. Sie können NLxx
|
|
|
- manuell hinzufügen.
|
|
|
+ Keine Niederlassungen ausgewählt.
|
|
|
</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 className="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>
|
|
|
- ) : null}
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
</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)}
|
|
|
+ ) : null}
|
|
|
+
|
|
|
+ {listReady ? (
|
|
|
+ <div className="space-y-2">
|
|
|
+ <div className="text-xs text-muted-foreground">
|
|
|
+ Ausgewählt: {selected.length}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 5 per row on md+ */}
|
|
|
+ <div className="grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-5">
|
|
|
+ {branchOptions.map((b) => {
|
|
|
+ const id = String(b);
|
|
|
+ const checked = selectedSet.has(id);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <label
|
|
|
+ key={id}
|
|
|
+ className="flex items-center gap-2 rounded-md px-2 py-1 hover:bg-muted/50"
|
|
|
+ >
|
|
|
+ <input
|
|
|
+ type="checkbox"
|
|
|
+ checked={checked}
|
|
|
+ onChange={() => onToggleBranch(id)}
|
|
|
disabled={isSubmitting}
|
|
|
- aria-label={`Entfernen: ${b}`}
|
|
|
- title={`Entfernen: ${b}`}
|
|
|
- >
|
|
|
- <X className="h-3 w-3" />
|
|
|
- </button>
|
|
|
- </Badge>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- )}
|
|
|
+ />
|
|
|
+ <span className="text-sm">{id}</span>
|
|
|
+ </label>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ ) : null}
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|