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:
parent
06388b3688
commit
245d5dc15b
6 changed files with 91 additions and 28 deletions
|
|
@ -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 }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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,23 +2368,34 @@ 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-1">
|
<div className="flex items-center gap-3">
|
||||||
{sections.map((section, index) => (
|
<div className="flex items-center gap-1">
|
||||||
|
{sections.map((section, index) => (
|
||||||
|
<button
|
||||||
|
key={section.id}
|
||||||
|
onClick={() => onChange(index)}
|
||||||
|
className={cn(
|
||||||
|
"h-2.5 w-2.5 rounded-full transition",
|
||||||
|
fullscreen
|
||||||
|
? index === activeIndex
|
||||||
|
? "bg-white"
|
||||||
|
: "bg-white/40 hover:bg-white/70"
|
||||||
|
: index === activeIndex
|
||||||
|
? "bg-sidebar-accent-foreground"
|
||||||
|
: "bg-sidebar-accent-foreground/40 hover:bg-sidebar-accent-foreground/70",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{fullscreen && onExitPresentation ? (
|
||||||
<button
|
<button
|
||||||
key={section.id}
|
onClick={onExitPresentation}
|
||||||
onClick={() => onChange(index)}
|
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"
|
||||||
className={cn(
|
>
|
||||||
"h-2.5 w-2.5 rounded-full transition",
|
<X className="size-3.5" />
|
||||||
fullscreen
|
Encerrar
|
||||||
? index === activeIndex
|
</button>
|
||||||
? "bg-white"
|
) : null}
|
||||||
: "bg-white/40 hover:bg-white/70"
|
|
||||||
: index === activeIndex
|
|
||||||
? "bg-sidebar-accent-foreground"
|
|
||||||
: "bg-sidebar-accent-foreground/40 hover:bg-sidebar-accent-foreground/70",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 ? (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue