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 }
|
||||
},
|
||||
})
|
||||
|
||||
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
|
||||
size="sm"
|
||||
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}
|
||||
disabled={isResettingAgent}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ import {
|
|||
Sparkles,
|
||||
Table2,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react"
|
||||
import { IconPencil } from "@tabler/icons-react"
|
||||
import { getMetricDefinition, getMetricOptionsForRole } from "@/components/dashboards/metric-catalog"
|
||||
|
|
@ -702,16 +703,17 @@ export function DashboardBuilder({ dashboardId, editable = true, mode = "edit" }
|
|||
const handleFullscreenChange = () => {
|
||||
const currentlyFullscreen = Boolean(document.fullscreenElement)
|
||||
setIsFullscreen(currentlyFullscreen)
|
||||
if (!currentlyFullscreen && previousSidebarStateRef.current) {
|
||||
const previous = previousSidebarStateRef.current
|
||||
setOpen(previous.open)
|
||||
setOpenMobile(previous.openMobile)
|
||||
if (!currentlyFullscreen) {
|
||||
// Ao sair do fullscreen, sempre abre o sidebar em desktop
|
||||
if (!isMobile) {
|
||||
setOpen(true)
|
||||
}
|
||||
previousSidebarStateRef.current = null
|
||||
}
|
||||
}
|
||||
document.addEventListener("fullscreenchange", handleFullscreenChange)
|
||||
return () => document.removeEventListener("fullscreenchange", handleFullscreenChange)
|
||||
}, [setOpen, setOpenMobile])
|
||||
}, [setOpen, isMobile])
|
||||
|
||||
const handleToggleFullscreen = useCallback(
|
||||
async (options?: { requestedByUser?: boolean }) => {
|
||||
|
|
@ -1265,6 +1267,7 @@ export function DashboardBuilder({ dashboardId, editable = true, mode = "edit" }
|
|||
activeIndex={activeSectionIndex}
|
||||
onChange={setActiveSectionIndex}
|
||||
fullscreen={isFullscreen}
|
||||
onExitPresentation={() => handleToggleFullscreen({ requestedByUser: true })}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
|
@ -2342,11 +2345,13 @@ function TvSectionIndicator({
|
|||
activeIndex,
|
||||
onChange,
|
||||
fullscreen = false,
|
||||
onExitPresentation,
|
||||
}: {
|
||||
sections: DashboardSection[]
|
||||
activeIndex: number
|
||||
onChange: (index: number) => void
|
||||
fullscreen?: boolean
|
||||
onExitPresentation?: () => void
|
||||
}) {
|
||||
if (sections.length === 0) return null
|
||||
return (
|
||||
|
|
@ -2363,6 +2368,7 @@ function TvSectionIndicator({
|
|||
Seção {activeIndex + 1} de {sections.length}: {sections[activeIndex]?.title ?? "Sem título"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
{sections.map((section, index) => (
|
||||
<button
|
||||
|
|
@ -2381,6 +2387,16 @@ function TvSectionIndicator({
|
|||
/>
|
||||
))}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -266,6 +266,7 @@ export function ChecklistTemplatesManager() {
|
|||
) as Array<{ id: Id<"companies">; name: string }> | undefined
|
||||
|
||||
const updateTemplate = useMutation(api.checklistTemplates.update)
|
||||
const removeTemplate = useMutation(api.checklistTemplates.remove)
|
||||
|
||||
const companyOptions = useMemo<CompanyRow[]>(
|
||||
() => (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 (
|
||||
<div className="space-y-6">
|
||||
<Card className="border border-slate-200">
|
||||
|
|
@ -366,12 +383,22 @@ export function ChecklistTemplatesManager() {
|
|||
) : null}
|
||||
</div>
|
||||
<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
|
||||
</Button>
|
||||
<Button type="button" variant="outline" onClick={() => handleToggleArchived(tpl)}>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => handleToggleArchived(tpl)}>
|
||||
{tpl.isArchived ? "Restaurar" : "Arquivar"}
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -376,7 +376,7 @@ export function TicketChecklistCard({
|
|||
<Button
|
||||
type="button"
|
||||
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 () => {
|
||||
if (!actorId) return
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -250,8 +250,8 @@ export function TicketsTable({ tickets, enteringIds }: TicketsTableProps) {
|
|||
</div>
|
||||
</TableCell>
|
||||
<TableCell className={`${cellClass} hidden lg:table-cell overflow-hidden`}>
|
||||
<div className="flex flex-col gap-1 truncate">
|
||||
<span className="font-semibold text-neutral-800" title={ticket.requester.name}>
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<span className="truncate font-semibold text-neutral-800" title={ticket.requester.name}>
|
||||
{ticket.requester.name}
|
||||
</span>
|
||||
{ticket.company?.name ? (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue