fix(reports): remove truncation cap in range collectors to avoid dropped records
feat(calendar): migrate to react-day-picker v9 and polish UI - Update classNames and CSS import (style.css) - Custom Dropdown via shadcn Select - Nav arrows aligned with caption (around) - Today highlight with cyan tone, weekdays in sentence case - Wider layout to avoid overflow; remove inner wrapper chore(tickets): make 'Patrimônio do computador (se houver)' optional - Backend hotfix to enforce optional + label on existing tenants - Hide required asterisk for this field in portal/new-ticket refactor(new-ticket): remove channel dropdown from admin/agent flow - Keep default channel as MANUAL feat(ux): simplify requester section and enlarge combobox trigger - Remove RequesterPreview redundancy; show company badge in trigger
This commit is contained in:
parent
e0ef66555d
commit
a8333c010f
28 changed files with 1752 additions and 455 deletions
195
src/components/ui/calendar.tsx
Normal file
195
src/components/ui/calendar.tsx
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import type { DropdownProps } from "react-day-picker"
|
||||
import { DayPicker } from "react-day-picker"
|
||||
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
|
||||
import "react-day-picker/style.css"
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>
|
||||
|
||||
type OptionElement = React.ReactElement<{
|
||||
value: string | number
|
||||
children: React.ReactNode
|
||||
disabled?: boolean
|
||||
}>
|
||||
|
||||
type CalendarDropdownProps = DropdownProps & { children?: React.ReactNode }
|
||||
|
||||
const buildOptions = (options?: DropdownProps["options"], children?: React.ReactNode) => {
|
||||
if (options && options.length > 0) {
|
||||
return options.map((option) => ({
|
||||
value: option.value,
|
||||
label: option.label,
|
||||
disabled: option.disabled,
|
||||
}))
|
||||
}
|
||||
|
||||
return React.Children.toArray(children)
|
||||
.filter((child): child is OptionElement => React.isValidElement(child))
|
||||
.map((child) => ({
|
||||
value: child.props.value,
|
||||
label: child.props.children,
|
||||
disabled: child.props.disabled ?? false,
|
||||
}))
|
||||
}
|
||||
|
||||
function CalendarDropdown(props: CalendarDropdownProps) {
|
||||
const { value, onChange, options: optionProp, children } = props
|
||||
const disabled = props.disabled
|
||||
const ariaLabel = props["aria-label"]
|
||||
|
||||
const options = React.useMemo(() => buildOptions(optionProp, children), [optionProp, children])
|
||||
const stringValue = value !== undefined && value !== null ? String(value) : ""
|
||||
|
||||
const handleChange = (next: string) => {
|
||||
const match = options.find((option) => String(option.value) === next)
|
||||
const payload = match ? match.value : next
|
||||
onChange?.({
|
||||
target: { value: payload },
|
||||
} as React.ChangeEvent<HTMLSelectElement>)
|
||||
}
|
||||
|
||||
const isYearDropdown = options.every((option) => String(option.value).length === 4)
|
||||
const triggerWidth = isYearDropdown ? "w-[96px]" : "w-[108px]"
|
||||
|
||||
const displayText = React.useMemo(() => {
|
||||
if (!options.length) return ""
|
||||
const selected = options.find((o) => String(o.value) === stringValue)
|
||||
if (!selected) return ""
|
||||
if (isYearDropdown) return String(selected.label)
|
||||
const label = String(selected.label)
|
||||
const abbr = label.slice(0, 3)
|
||||
return abbr.charAt(0).toUpperCase() + abbr.slice(1).toLowerCase()
|
||||
}, [options, stringValue, isYearDropdown])
|
||||
|
||||
return (
|
||||
<Select value={stringValue} onValueChange={handleChange} disabled={disabled}>
|
||||
<SelectTrigger
|
||||
aria-label={ariaLabel}
|
||||
className={cn(
|
||||
"h-8 rounded-full border border-slate-200 bg-white px-3 text-sm font-semibold text-neutral-700 shadow-none transition hover:bg-slate-50 focus-visible:ring-2 focus-visible:ring-neutral-900/10 disabled:cursor-not-allowed disabled:opacity-60",
|
||||
triggerWidth
|
||||
)}
|
||||
>
|
||||
{/* Mostra mês abreviado no trigger; lista usa label completo */}
|
||||
<span className="truncate">
|
||||
{displayText}
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
align="start"
|
||||
className="max-h-72 min-w-[var(--radix-select-trigger-width)] rounded-2xl border border-[#00e8ff]/20 bg-white/95 text-neutral-800 shadow-[0_10px_40px_-10px_rgba(0,155,177,0.25)] backdrop-blur-sm"
|
||||
>
|
||||
{options.map((option) => (
|
||||
<SelectItem
|
||||
key={String(option.value)}
|
||||
value={String(option.value)}
|
||||
disabled={option.disabled}
|
||||
className="rounded-lg text-sm font-medium text-neutral-700 focus:bg-slate-100 focus:text-neutral-900"
|
||||
>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
|
||||
export function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
captionLayout = "dropdown",
|
||||
startMonth,
|
||||
endMonth,
|
||||
...props
|
||||
}: CalendarProps) {
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
captionLayout={captionLayout}
|
||||
startMonth={startMonth}
|
||||
endMonth={endMonth}
|
||||
className={cn(
|
||||
"mx-auto flex w-[320px] flex-col gap-3 rounded-2xl bg-transparent p-4 text-neutral-900 shadow-none",
|
||||
className
|
||||
)}
|
||||
classNames={{
|
||||
months: "flex flex-col items-center gap-4",
|
||||
// Layout em grid de 3 colunas para posicionar setas à esquerda/direita
|
||||
month: "grid grid-cols-[auto_1fr_auto] items-center gap-x-2 gap-y-3",
|
||||
month_caption: "col-start-2 col-end-3 flex items-center justify-center gap-2 px-0",
|
||||
dropdowns: "flex items-center gap-2",
|
||||
nav: "flex items-center gap-1",
|
||||
button_previous: cn(
|
||||
buttonVariants({ variant: "ghost", size: "icon" }),
|
||||
"col-start-1 row-start-1 size-8 rounded-full border border-transparent text-neutral-500 hover:border-slate-200 hover:bg-slate-50 hover:text-neutral-900 focus-visible:ring-2 focus-visible:ring-neutral-900/10"
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: "ghost", size: "icon" }),
|
||||
"col-start-3 row-start-1 size-8 rounded-full border border-transparent text-neutral-500 hover:border-slate-200 hover:bg-slate-50 hover:text-neutral-900 focus-visible:ring-2 focus-visible:ring-neutral-900/10"
|
||||
),
|
||||
chevron: "size-4",
|
||||
month_grid: "col-span-3 w-full border-collapse",
|
||||
weekdays:
|
||||
"grid grid-cols-7 text-[0.7rem] font-medium capitalize text-muted-foreground",
|
||||
weekday: "flex h-8 items-center justify-center",
|
||||
week: "grid grid-cols-7 gap-1",
|
||||
day: "relative flex h-9 items-center justify-center text-sm focus-within:relative focus-within:z-20",
|
||||
day_button: cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"size-9 rounded-full font-medium text-neutral-700 transition hover:bg-slate-100 hover:text-neutral-900 data-[selected]:rounded-full data-[selected]:bg-neutral-900 data-[selected]:text-neutral-50 data-[selected]:ring-0 data-[range-start]:rounded-s-full data-[range-end]:rounded-e-full"
|
||||
),
|
||||
// Hoje: estilos principais irão no botão via modifiersClassNames
|
||||
today: "text-neutral-900",
|
||||
outside: "text-muted-foreground opacity-40",
|
||||
disabled: "text-muted-foreground opacity-30 line-through",
|
||||
range_middle: "bg-neutral-900/10 text-neutral-900",
|
||||
hidden: "invisible",
|
||||
...classNames,
|
||||
}}
|
||||
modifiersClassNames={{
|
||||
today:
|
||||
"[&_button]:bg-[#00e8ff]/20 [&_button]:text-neutral-900 [&_button]:ring-1 [&_button]:ring-[#00d6eb] [&_button]:ring-offset-1 [&_button]:ring-offset-white",
|
||||
}}
|
||||
// Setas ao lado do caption
|
||||
navLayout="around"
|
||||
components={{
|
||||
Dropdown: CalendarDropdown,
|
||||
// Tornar os botões de navegação estáticos para encaixar no grid
|
||||
PreviousMonthButton: (props) => {
|
||||
const { className, children, style, ...rest } = props
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
{...rest}
|
||||
className={className}
|
||||
style={{ ...(style || {}), position: "static" }}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
NextMonthButton: (props) => {
|
||||
const { className, children, style, ...rest } = props
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
{...rest}
|
||||
className={className}
|
||||
style={{ ...(style || {}), position: "static" }}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -93,7 +93,7 @@ export function SearchableCombobox({
|
|||
aria-expanded={open}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"flex min-h-[42px] w-full items-center justify-between rounded-full border border-input bg-background px-3 py-2 text-sm font-medium text-foreground shadow-sm transition focus-visible:ring-2 focus-visible:ring-ring",
|
||||
"flex min-h-[46px] w-full items-center justify-between rounded-full border border-input bg-background px-3 py-2.5 text-sm font-medium text-foreground shadow-sm transition focus-visible:ring-2 focus-visible:ring-ring",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue