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:
codex-bot 2025-11-04 11:51:08 -03:00
parent e0ef66555d
commit a8333c010f
28 changed files with 1752 additions and 455 deletions

View 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}
/>
)
}

View file

@ -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,
)}
>