ui: melhorias de UX em várias telas

- Truncate com ellipsis na coluna Empresa (tickets-table)
- Botão excluir em templates de checklist + mutation remove no backend
- Botões Editar/Arquivar com size="sm" em checklist templates
- Hover com borda no botão "Tornar opcional" do checklist
- Botão Resetar em devices com estilo padrão (remove amarelo)
- Botão "Encerrar" no modo apresentação do dashboard
- Sidebar abre automaticamente ao sair do fullscreen

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
esdrasrenan 2025-12-13 22:42:37 -03:00
parent 06388b3688
commit 245d5dc15b
6 changed files with 91 additions and 28 deletions

View file

@ -259,3 +259,23 @@ export const update = mutation({
return { ok: true } return { ok: true }
}, },
}) })
export const remove = mutation({
args: {
tenantId: v.string(),
actorId: v.id("users"),
templateId: v.id("ticketChecklistTemplates"),
},
handler: async (ctx, { tenantId, actorId, templateId }) => {
await requireAdmin(ctx, actorId, tenantId)
const existing = await ctx.db.get(templateId)
if (!existing || existing.tenantId !== tenantId) {
throw new ConvexError("Template de checklist não encontrado.")
}
await ctx.db.delete(templateId)
return { ok: true }
},
})

View file

@ -4000,7 +4000,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
className="gap-2 border-dashed border-amber-300 text-amber-700 hover:border-amber-400 hover:bg-amber-100/60 hover:text-amber-900" className="gap-2 border-dashed"
onClick={handleResetAgent} onClick={handleResetAgent}
disabled={isResettingAgent} disabled={isResettingAgent}
> >

View file

@ -93,6 +93,7 @@ import {
Sparkles, Sparkles,
Table2, Table2,
Trash2, Trash2,
X,
} from "lucide-react" } from "lucide-react"
import { IconPencil } from "@tabler/icons-react" import { IconPencil } from "@tabler/icons-react"
import { getMetricDefinition, getMetricOptionsForRole } from "@/components/dashboards/metric-catalog" import { getMetricDefinition, getMetricOptionsForRole } from "@/components/dashboards/metric-catalog"
@ -702,16 +703,17 @@ export function DashboardBuilder({ dashboardId, editable = true, mode = "edit" }
const handleFullscreenChange = () => { const handleFullscreenChange = () => {
const currentlyFullscreen = Boolean(document.fullscreenElement) const currentlyFullscreen = Boolean(document.fullscreenElement)
setIsFullscreen(currentlyFullscreen) setIsFullscreen(currentlyFullscreen)
if (!currentlyFullscreen && previousSidebarStateRef.current) { if (!currentlyFullscreen) {
const previous = previousSidebarStateRef.current // Ao sair do fullscreen, sempre abre o sidebar em desktop
setOpen(previous.open) if (!isMobile) {
setOpenMobile(previous.openMobile) setOpen(true)
}
previousSidebarStateRef.current = null previousSidebarStateRef.current = null
} }
} }
document.addEventListener("fullscreenchange", handleFullscreenChange) document.addEventListener("fullscreenchange", handleFullscreenChange)
return () => document.removeEventListener("fullscreenchange", handleFullscreenChange) return () => document.removeEventListener("fullscreenchange", handleFullscreenChange)
}, [setOpen, setOpenMobile]) }, [setOpen, isMobile])
const handleToggleFullscreen = useCallback( const handleToggleFullscreen = useCallback(
async (options?: { requestedByUser?: boolean }) => { async (options?: { requestedByUser?: boolean }) => {
@ -1265,6 +1267,7 @@ export function DashboardBuilder({ dashboardId, editable = true, mode = "edit" }
activeIndex={activeSectionIndex} activeIndex={activeSectionIndex}
onChange={setActiveSectionIndex} onChange={setActiveSectionIndex}
fullscreen={isFullscreen} fullscreen={isFullscreen}
onExitPresentation={() => handleToggleFullscreen({ requestedByUser: true })}
/> />
) : null} ) : null}
@ -2342,11 +2345,13 @@ function TvSectionIndicator({
activeIndex, activeIndex,
onChange, onChange,
fullscreen = false, fullscreen = false,
onExitPresentation,
}: { }: {
sections: DashboardSection[] sections: DashboardSection[]
activeIndex: number activeIndex: number
onChange: (index: number) => void onChange: (index: number) => void
fullscreen?: boolean fullscreen?: boolean
onExitPresentation?: () => void
}) { }) {
if (sections.length === 0) return null if (sections.length === 0) return null
return ( return (
@ -2363,6 +2368,7 @@ function TvSectionIndicator({
Seção {activeIndex + 1} de {sections.length}: {sections[activeIndex]?.title ?? "Sem título"} Seção {activeIndex + 1} de {sections.length}: {sections[activeIndex]?.title ?? "Sem título"}
</span> </span>
</div> </div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{sections.map((section, index) => ( {sections.map((section, index) => (
<button <button
@ -2381,6 +2387,16 @@ function TvSectionIndicator({
/> />
))} ))}
</div> </div>
{fullscreen && onExitPresentation ? (
<button
onClick={onExitPresentation}
className="flex items-center gap-1.5 rounded-md bg-white/10 px-2.5 py-1 text-xs font-medium text-white/90 transition hover:bg-white/20 hover:text-white"
>
<X className="size-3.5" />
Encerrar
</button>
) : null}
</div>
</div> </div>
) )
} }

View file

@ -266,6 +266,7 @@ export function ChecklistTemplatesManager() {
) as Array<{ id: Id<"companies">; name: string }> | undefined ) as Array<{ id: Id<"companies">; name: string }> | undefined
const updateTemplate = useMutation(api.checklistTemplates.update) const updateTemplate = useMutation(api.checklistTemplates.update)
const removeTemplate = useMutation(api.checklistTemplates.remove)
const companyOptions = useMemo<CompanyRow[]>( const companyOptions = useMemo<CompanyRow[]>(
() => (companies ?? []).map((c) => ({ id: c.id, name: c.name })).sort((a, b) => a.name.localeCompare(b.name, "pt-BR")), () => (companies ?? []).map((c) => ({ id: c.id, name: c.name })).sort((a, b) => a.name.localeCompare(b.name, "pt-BR")),
@ -303,6 +304,22 @@ export function ChecklistTemplatesManager() {
} }
} }
const handleDelete = async (tpl: ChecklistTemplateRow) => {
if (!viewerId) return
const ok = confirm(`Excluir o template "${tpl.name}"? Esta ação não pode ser desfeita.`)
if (!ok) return
try {
await removeTemplate({
tenantId,
actorId: viewerId,
templateId: tpl.id,
})
toast.success("Template excluído.")
} catch (error) {
toast.error(error instanceof Error ? error.message : "Falha ao excluir template.")
}
}
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Card className="border border-slate-200"> <Card className="border border-slate-200">
@ -366,12 +383,22 @@ export function ChecklistTemplatesManager() {
) : null} ) : null}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button type="button" variant="outline" onClick={() => handleEdit(tpl)}> <Button type="button" variant="outline" size="sm" onClick={() => handleEdit(tpl)}>
Editar Editar
</Button> </Button>
<Button type="button" variant="outline" onClick={() => handleToggleArchived(tpl)}> <Button type="button" variant="outline" size="sm" onClick={() => handleToggleArchived(tpl)}>
{tpl.isArchived ? "Restaurar" : "Arquivar"} {tpl.isArchived ? "Restaurar" : "Arquivar"}
</Button> </Button>
<Button
type="button"
variant="ghost"
size="sm"
className="text-slate-500 hover:bg-red-50 hover:text-red-700"
onClick={() => handleDelete(tpl)}
title="Excluir template"
>
<Trash2 className="size-4" />
</Button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -376,7 +376,7 @@ export function TicketChecklistCard({
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
className="h-9 px-2 text-xs text-neutral-700 hover:bg-slate-100 hover:text-neutral-900 active:bg-slate-200/70 focus-visible:bg-slate-100" className="h-9 px-2 text-xs text-neutral-700 hover:bg-slate-100 hover:text-neutral-900 hover:border hover:border-slate-300 active:bg-slate-200/70 focus-visible:bg-slate-100"
onClick={async () => { onClick={async () => {
if (!actorId) return if (!actorId) return
try { try {

View file

@ -250,8 +250,8 @@ export function TicketsTable({ tickets, enteringIds }: TicketsTableProps) {
</div> </div>
</TableCell> </TableCell>
<TableCell className={`${cellClass} hidden lg:table-cell overflow-hidden`}> <TableCell className={`${cellClass} hidden lg:table-cell overflow-hidden`}>
<div className="flex flex-col gap-1 truncate"> <div className="flex min-w-0 flex-col gap-1">
<span className="font-semibold text-neutral-800" title={ticket.requester.name}> <span className="truncate font-semibold text-neutral-800" title={ticket.requester.name}>
{ticket.requester.name} {ticket.requester.name}
</span> </span>
{ticket.company?.name ? ( {ticket.company?.name ? (