feat: expand admin companies and users modules
This commit is contained in:
parent
a043b1203c
commit
2e3b46a7b5
31 changed files with 5626 additions and 2003 deletions
61
src/components/ui/accordion.tsx
Normal file
61
src/components/ui/accordion.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b border-border/60 last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 text-left text-sm font-medium transition-all hover:text-foreground/80 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
className="h-4 w-4 shrink-0 transition-transform duration-200"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
144
src/components/ui/multi-value-input.tsx
Normal file
144
src/components/ui/multi-value-input.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
33
src/components/ui/scroll-area.tsx
Normal file
33
src/components/ui/scroll-area.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
type ScrollAreaProps = React.ComponentPropsWithoutRef<"div"> & {
|
||||
orientation?: "vertical" | "horizontal" | "both"
|
||||
}
|
||||
|
||||
const orientationClasses: Record<NonNullable<ScrollAreaProps["orientation"]>, string> = {
|
||||
vertical: "overflow-y-auto",
|
||||
horizontal: "overflow-x-auto",
|
||||
both: "overflow-auto",
|
||||
}
|
||||
|
||||
const baseClasses =
|
||||
"relative [&::-webkit-scrollbar]:h-2 [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-muted [&::-webkit-scrollbar-track]:bg-transparent"
|
||||
|
||||
const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
|
||||
({ className, orientation = "vertical", children, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={[baseClasses, orientationClasses[orientation], className].filter(Boolean).join(" ")}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
ScrollArea.displayName = "ScrollArea"
|
||||
|
||||
export { ScrollArea }
|
||||
83
src/components/ui/time-picker.tsx
Normal file
83
src/components/ui/time-picker.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
type TimePickerProps = {
|
||||
value?: string | null
|
||||
onChange?: (value: string) => void
|
||||
className?: string
|
||||
placeholder?: string
|
||||
stepMinutes?: number
|
||||
}
|
||||
|
||||
function pad2(n: number) {
|
||||
return String(n).padStart(2, "0")
|
||||
}
|
||||
|
||||
export function TimePicker({ value, onChange, className, placeholder = "Selecionar horário", stepMinutes = 15 }: TimePickerProps) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
const [hours, minutes] = React.useMemo(() => {
|
||||
if (!value || !/^\d{2}:\d{2}$/.test(value)) return ["", ""]
|
||||
const [h, m] = value.split(":")
|
||||
return [h, m]
|
||||
}, [value])
|
||||
|
||||
const minuteOptions = React.useMemo(() => {
|
||||
const list: string[] = []
|
||||
for (let i = 0; i < 60; i += stepMinutes) list.push(pad2(i))
|
||||
if (!list.includes("00")) list.unshift("00")
|
||||
return list
|
||||
}, [stepMinutes])
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className={cn("w-full justify-between font-normal", className)}>
|
||||
{value ? value : placeholder}
|
||||
<ChevronDownIcon className="ml-2 size-4 opacity-60" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<div className="flex gap-2 p-2">
|
||||
<div className="max-h-56 w-16 overflow-auto rounded-md border">
|
||||
{Array.from({ length: 24 }, (_, h) => pad2(h)).map((h) => (
|
||||
<button
|
||||
key={h}
|
||||
type="button"
|
||||
className={cn(
|
||||
"block w-full px-3 py-1.5 text-left text-sm hover:bg-muted",
|
||||
h === hours && "bg-muted/70 font-semibold"
|
||||
)}
|
||||
onClick={() => onChange?.(`${h}:${minutes || "00"}`)}
|
||||
>
|
||||
{h}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="max-h-56 w-16 overflow-auto rounded-md border">
|
||||
{minuteOptions.map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
type="button"
|
||||
className={cn(
|
||||
"block w-full px-3 py-1.5 text-left text-sm hover:bg-muted",
|
||||
m === minutes && "bg-muted/70 font-semibold"
|
||||
)}
|
||||
onClick={() => onChange?.(`${hours || "00"}:${m}`)}
|
||||
>
|
||||
{m}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue