import { useState } from "react"; import { Check, ChevronDown, Plus, Tag as TagIcon, X } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import type { TagDto } from "@/lib/api"; import { tagColorValue } from "@/lib/tagColors"; import { cn } from "@/lib/utils"; function TagDot({ tag }: { tag: TagDto }) { return ; } interface Props { /** Every tag the user has defined. */ tags: TagDto[]; /** Ids of the tags currently on this transaction. */ selectedIds: string[]; onChange: (ids: string[]) => void; /** Creates a new tag (with the default colour) and returns its id, so it * can be selected immediately without a second round trip. */ onCreate?: (name: string) => Promise; disabled?: boolean; } /** * Multi-select tag picker: selected tags show as small removable pills, the * popover list toggles membership instead of replacing a single value like * CategoryPicker does. Typing a name with no match offers to create it. */ export function TagPicker({ tags, selectedIds, onChange, onCreate, disabled }: Props) { const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const [creating, setCreating] = useState(false); const selected = tags.filter((t) => selectedIds.includes(t.id)); 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) => { onChange(selectedIds.includes(id) ? selectedIds.filter((x) => x !== id) : [...selectedIds, id]); }; const createAndSelect = async () => { if (!onCreate || !query.trim() || creating) return; setCreating(true); try { const id = await onCreate(query.trim()); onChange([...selectedIds, id]); setQuery(""); } finally { setCreating(false); } }; return (
{selected.length > 0 && (
{selected.map((t) => ( {t.name} ))}
)} { setOpen(o); if (!o) setQuery(""); }} modal> {shown.length === 0 && ( {onCreate && query.trim() ? ( ) : ( "No tags found." )} )} {shown.map((t) => ( toggle(t.id)} className="gap-2"> {t.name} {selectedIds.includes(t.id) && } ))} {onCreate && query.trim() && !exactMatch && shown.length > 0 && ( void createAndSelect()} className="gap-2 text-primary"> Create "{query.trim()}" )}
); }