| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- "use client";
- import React from "react";
- import { X } from "lucide-react";
- import { isValidBranchParam } from "@/lib/frontend/params";
- import { Badge } from "@/components/ui/badge";
- import { Button } from "@/components/ui/button";
- import { Checkbox } from "@/components/ui/checkbox";
- import { Input } from "@/components/ui/input";
- import { Label } from "@/components/ui/label";
- import { Skeleton } from "@/components/ui/skeleton";
- 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,
- onClearAllBranches,
- isSubmitting,
- }) {
- 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 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);
- return (
- <div className="rounded-lg border bg-card p-4 text-card-foreground shadow-sm">
- <div className="space-y-3">
- <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">
- Keine Niederlassungen ausgewählt.
- </p>
- ) : (
- <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>
- </div>
- ) : null}
- {listReady ? (
- <div className="space-y-2">
- <div className="text-xs text-muted-foreground">
- Ausgewählt: {selected.length}
- </div>
- {/*
- RHL-038 UI polish:
- - Use shadcn Checkbox + a “selectable card” label wrapper
- - Checked state highlights the whole tile (border + background)
- - Keep it compact so it still works with many branches
- */}
- <div className="grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-6">
- {branchOptions.map((b) => {
- const id = String(b);
- const checked = selectedSet.has(id);
- // Stable id for accessibility (Label <-> Checkbox association).
- const checkboxId = `branch-${id}`;
- return (
- <Label
- key={id}
- htmlFor={checkboxId}
- className={[
- "hover:bg-accent/50 flex items-start gap-3 rounded-lg border p-3 transition-colors",
- // When the checkbox inside is checked, highlight the tile.
- "has-aria-checked:border-blue-600 has-aria-checked:bg-blue-50",
- "dark:has-aria-checked:border-blue-900 dark:has-aria-checked:bg-blue-950",
- // Slightly reduce click affordance when submitting.
- isSubmitting ? "opacity-60" : "",
- ].join(" ")}
- title={id}
- >
- <Checkbox
- id={checkboxId}
- checked={checked}
- disabled={isSubmitting}
- // We keep the update logic in the parent: toggle by branch id.
- onCheckedChange={() => onToggleBranch(id)}
- className={[
- // Match the shadcn example “blue checked box” styling.
- "data-[state=checked]:border-blue-600 data-[state=checked]:bg-blue-600 data-[state=checked]:text-white",
- "dark:data-[state=checked]:border-blue-700 dark:data-[state=checked]:bg-blue-700",
- ].join(" ")}
- />
- <div className="grid gap-1.5 font-normal">
- <p className="text-xs font-medium">{id}</p>
- </div>
- </Label>
- );
- })}
- </div>
- </div>
- ) : null}
- </div>
- </div>
- );
- }
|