feat: expand admin companies and users modules

This commit is contained in:
Esdras Renan 2025-10-22 01:27:43 -03:00
parent a043b1203c
commit 2e3b46a7b5
31 changed files with 5626 additions and 2003 deletions

View file

@ -0,0 +1,144 @@
import * as React from "react"
import { IconX } from "@tabler/icons-react"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { cn } from "@/lib/utils"
type MultiValueInputProps = {
values: string[]
onChange: (values: string[]) => void
placeholder?: string
disabled?: boolean
maxItems?: number
addOnBlur?: boolean
className?: string
inputClassName?: string
validate?: (value: string) => string | null
format?: (value: string) => string
emptyState?: React.ReactNode
}
export function MultiValueInput({
values,
onChange,
placeholder,
disabled,
maxItems,
addOnBlur,
className,
inputClassName,
validate,
format,
emptyState,
}: MultiValueInputProps) {
const [pending, setPending] = React.useState("")
const [error, setError] = React.useState<string | null>(null)
const inputRef = React.useRef<HTMLInputElement | null>(null)
const remainingSlots = typeof maxItems === "number" ? Math.max(maxItems - values.length, 0) : undefined
const canAdd = remainingSlots === undefined || remainingSlots > 0
const addValue = React.useCallback(
(raw: string) => {
const trimmed = raw.trim()
if (!trimmed) return
const formatted = format ? format(trimmed) : trimmed
if (values.includes(formatted)) {
setPending("")
setError(null)
return
}
if (validate) {
const validation = validate(formatted)
if (validation) {
setError(validation)
return
}
}
setError(null)
onChange([...values, formatted])
setPending("")
},
[format, onChange, validate, values]
)
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter" || event.key === "," || event.key === "Tab") {
if (!canAdd) return
event.preventDefault()
addValue(pending)
}
if (event.key === "Backspace" && pending.length === 0 && values.length > 0) {
onChange(values.slice(0, -1))
setError(null)
}
}
const handleBlur = () => {
if (addOnBlur && pending.trim()) {
addValue(pending)
}
}
const removeValue = (value: string) => {
onChange(values.filter((item) => item !== value))
setError(null)
inputRef.current?.focus()
}
return (
<div className={cn("space-y-2", className)}>
<div
className={cn(
"flex flex-wrap gap-2 rounded-md border border-input bg-background px-3 py-2 focus-within:ring-2 focus-within:ring-ring",
disabled && "opacity-60"
)}
>
{values.map((value) => (
<Badge
key={value}
variant="secondary"
className="flex items-center gap-1 rounded-full px-2 py-1 text-xs font-medium"
>
<span>{value}</span>
<button
type="button"
className="flex h-4 w-4 items-center justify-center rounded-full bg-muted text-muted-foreground hover:bg-muted/80"
onClick={() => removeValue(value)}
aria-label={`Remover ${value}`}
disabled={disabled}
>
<IconX className="h-3 w-3" strokeWidth={2} />
</button>
</Badge>
))}
{canAdd ? (
<Input
ref={inputRef}
value={pending}
onChange={(event) => {
setPending(event.target.value)
setError(null)
}}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
placeholder={values.length === 0 ? placeholder : undefined}
disabled={disabled}
className={cn(
"m-0 h-auto min-w-[8rem] flex-1 border-0 bg-transparent px-0 py-1 text-sm shadow-none focus-visible:ring-0",
inputClassName
)}
/>
) : null}
</div>
{error ? <p className="text-xs font-medium text-destructive">{error}</p> : null}
{!values.length && emptyState ? <div className="text-xs text-muted-foreground">{emptyState}</div> : null}
{typeof remainingSlots === "number" ? (
<div className="text-right text-[11px] text-muted-foreground">
{remainingSlots} item{remainingSlots === 1 ? "" : "s"} restantes
</div>
) : null}
</div>
)
}