create discount

This commit is contained in:
hamid zarghami
2025-09-27 11:51:05 +03:30
parent ee89008a9f
commit 0fa7fbf99b
13 changed files with 712 additions and 25 deletions
+197
View File
@@ -0,0 +1,197 @@
import React, { useState, useMemo } from 'react'
import { Check, ChevronsUpDown, X, Search } from 'lucide-react'
import { cn } from '../lib/utils'
import { Button } from './ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandItem,
CommandList,
} from './ui/command'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from './ui/popover'
export type MultiSelectOption = {
value: string
label: string
}
type MultiSelectSearchableProps = {
options: MultiSelectOption[]
value: string[]
onChange: (value: string[]) => void
onSearch?: (searchTerm: string) => void
onOpenChange?: (open: boolean) => void
placeholder?: string
label?: string
error_text?: string
disabled?: boolean
loading?: boolean
}
const MultiSelectSearchable: React.FC<MultiSelectSearchableProps> = ({
options,
value,
onChange,
onSearch,
onOpenChange,
placeholder = "انتخاب کنید...",
label,
error_text,
disabled = false,
loading = false,
}) => {
const [open, setOpen] = useState(false)
const handleOpenChange = (newOpen: boolean) => {
setOpen(newOpen)
onOpenChange?.(newOpen)
}
const selectedLabels = useMemo(() => {
return value
.map(v => options.find(option => option.value === v)?.label)
.filter(Boolean)
}, [value, options])
const handleSelect = (currentValue: string) => {
const newValue = value.includes(currentValue)
? value.filter(v => v !== currentValue)
: [...value, currentValue]
onChange(newValue)
}
const handleRemove = (itemValue: string) => {
onChange(value.filter(v => v !== itemValue))
}
return (
<div className="w-full">
{label && (
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 block mb-1">
{label}
</label>
)}
<Popover
open={open}
onOpenChange={handleOpenChange}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn(
"w-full justify-between min-h-10 h-auto",
!value.length && "text-muted-foreground",
error_text && "border-red-500"
)}
disabled={disabled || loading}
>
<div className="flex flex-wrap gap-1 flex-1 text-left min-h-[20px]">
{selectedLabels.length > 0 ? (
selectedLabels.map((label, index) => (
<span
key={index}
className="inline-flex items-center gap-1 bg-secondary text-secondary-foreground px-2 py-1 rounded-md text-xs max-w-[200px]"
title={label}
>
<span className="truncate">{label}</span>
<X
color='red'
className="h-3 w-3 cursor-pointer hover:text-destructive shrink-0"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
handleRemove(value[index])
}}
/>
</span>
))
) : (
<span className="text-muted-foreground">{placeholder}</span>
)}
</div>
<ChevronsUpDown color='gray' className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] max-w-[400px] p-0"
align="start"
onKeyDown={(e) => {
// Prevent event bubbling that might cause navigation
e.stopPropagation()
}}
>
<Command
shouldFilter={false}
onKeyDown={(e) => {
// Prevent any navigation or form submission from command
e.stopPropagation()
}}
>
<div className="flex h-9 items-center gap-2 border-b px-3">
<Search color='gray' className="size-4 shrink-0 opacity-50" />
<input
type="text"
placeholder="جستجو..."
className="placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50"
onChange={(e) => {
e.preventDefault()
onSearch?.(e.target.value)
}}
onKeyDown={(e) => {
// Prevent form submission or other default behaviors
if (e.key === 'Enter') {
e.preventDefault()
e.stopPropagation()
}
}}
/>
</div>
<CommandList>
<CommandEmpty>موردی یافت نشد.</CommandEmpty>
<CommandGroup>
{options.map((option) => {
const isSelected = value.includes(option.value)
return (
<CommandItem
key={option.value}
value={option.value}
onSelect={() => {
handleSelect(option.value)
}}
className="flex items-center"
>
<Check
color='gray'
className={cn(
"mr-2 h-4 w-4 shrink-0",
isSelected ? "opacity-100" : "opacity-0"
)}
/>
<span className="truncate flex-1" title={option.label}>
{option.label}
</span>
</CommandItem>
)
})}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{error_text && (
<div className="text-red-500 text-xs mt-1">{error_text}</div>
)}
</div>
)
}
export default MultiSelectSearchable