import { useState } from "react"; import { Check, ChevronDown, Plus, Tag as TagIcon } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Button } from "@/components/ui/button"; import type { TagDto } from "@/lib/api"; import { tagColorValue } from "@/lib/tagColors"; import { cn } from "@/lib/utils"; function TagDot({ tag }: { tag: TagDto }) { return ; } interface Props { tags: TagDto[]; /** How many records this would apply to, for the button's confirm label. */ count: number; onApply: (tagIds: string[]) => void; /** Creates a new tag (with the default colour) and returns its id, so it * can be picked immediately without a second round trip. */ onCreate?: (name: string) => Promise; disabled?: boolean; } /** * Multi-select tag picker for the bulk-selection floating bar: unlike * TagPicker (one transaction, shows and toggles its current tags), this has * no "current" state to show - the selection is a mix of transactions with * different tags already on them - so it only ever adds. Picks build up * locally and apply together in one call when "Apply" is pressed, rather * than firing a request per tag. */ export function BulkTagPicker({ tags, count, onApply, onCreate, disabled }: Props) { const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const [picked, setPicked] = useState([]); const [creating, setCreating] = useState(false); const q = query.trim().toLowerCase(); const shown = q ? tags.filter((t) => t.name.toLowerCase().includes(q)) : tags; const exactMatch = tags.some((t) => t.name.toLowerCase() === q); const toggle = (id: string) => { setPicked((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id])); }; const reset = () => { setPicked([]); setQuery(""); }; const createAndPick = async () => { if (!onCreate || !query.trim() || creating) return; setCreating(true); try { const id = await onCreate(query.trim()); setPicked((prev) => [...prev, id]); setQuery(""); } finally { setCreating(false); } }; const apply = () => { if (picked.length === 0) return; onApply(picked); setOpen(false); reset(); }; return ( { setOpen(o); if (!o) reset(); }} modal> {shown.length === 0 && ( {onCreate && query.trim() ? ( ) : ( "No tags found." )} )} {shown.map((t) => ( toggle(t.id)} className="gap-2"> {t.name} {picked.includes(t.id) && } ))} {onCreate && query.trim() && !exactMatch && shown.length > 0 && ( void createAndPick()} className="gap-2 text-primary"> Create "{query.trim()}" )} {picked.length > 0 && (
)}
); }