From f5a54f28141dbb22562efd45e2449788c0c41e48 Mon Sep 17 00:00:00 2001 From: esdrasrenan Date: Sun, 5 Oct 2025 01:23:31 -0300 Subject: [PATCH] feat: align ticket header editing flow Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- agents.md | 12 +- web/convex/tickets.ts | 15 +- web/convex/users.ts | 1 - web/eslint.config.mjs | 1 + web/src/app/ConvexClientProvider.tsx | 2 +- web/src/app/dev/seed/page.tsx | 3 +- web/src/app/tickets/new/page.tsx | 3 +- .../tickets/delete-ticket-dialog.tsx | 9 +- .../components/tickets/new-ticket-dialog.tsx | 2 +- .../tickets/play-next-ticket-card.tsx | 2 +- .../components/tickets/priority-select.tsx | 2 +- .../tickets/recent-tickets-panel.tsx | 2 +- web/src/components/tickets/status-select.tsx | 2 +- .../tickets/ticket-comments.rich.tsx | 11 +- .../components/tickets/ticket-detail-view.tsx | 3 +- .../tickets/ticket-queue-summary.tsx | 2 +- .../tickets/ticket-summary-header.tsx | 370 +++++++++++------- web/src/components/tickets/tickets-view.tsx | 3 +- web/src/components/ui/dropzone.tsx | 2 +- web/src/hooks/use-ticket-categories.ts | 3 +- web/src/lib/auth-client.tsx | 3 +- 21 files changed, 282 insertions(+), 171 deletions(-) diff --git a/agents.md b/agents.md index 526ea39..ae40eae 100644 --- a/agents.md +++ b/agents.md @@ -101,6 +101,7 @@ Este repositório foi atualizado para usar Convex como backend em tempo real par - UI com shadcn/ui; priorize componentes existentes e consistência visual. - Labels e mensagens em PT‑BR (status, timeline, toasts, etc.). - Atualizações otimistas com rollback em erro + toasts de feedback. +- Comentários de supressão: prefira `@ts-expect-error` com justificativa curta para módulos gerados do Convex; evite `@ts-ignore`. ## Estrutura útil - `web/convex/*` — API backend Convex. @@ -240,6 +241,7 @@ Arquivos principais tocados: - Header do ticket - Ordem: `#ref` • PrioritySelect (badge) • Status (badge/select) • Ações (Excluir) - Tipografia: título forte, resumo como texto auxiliar, metadados em texto pequeno. + - Combos de Categoria/ Subcategoria exibidos como selects dependentes com salvamento automático (sem botões dedicados). - Comentários - Composer com rich text + Dropzone; seletor de visibilidade. - Lista com avatar, nome, carimbo relativo e conteúdo rich text. @@ -295,5 +297,11 @@ Próximos passos sugeridos - Testes (Vitest): adicionar casos de mappers e smoke tests de páginas. Observações de codificação -- Evitar ny; usar TicketStatus/TicketPriority e Id<>/Doc<> do Convex. -- Não retornar Date do Convex; sempre epoch (number) e converter via mappers Zod. +- Evitar `any`; usar TicketStatus/TicketPriority e Id<>/Doc<> do Convex. +- Não retornar Date do Convex; sempre epoch (number) e converter via mappers Zod. + +## Atualizações recentes (out/2025) +- Cabeçalho de ticket agora persiste automaticamente mudanças de categoria/subcategoria, mostrando toasts e bloqueando os selects enquanto a mutação é processada. +- Normalização de nomes de fila/time aplicada também ao retorno de `tickets.playNext`, garantindo rótulos "Chamados"/"Laboratório" em todos os fluxos. +- ESLint ignora `convex/_generated/**` e supressões migradas para `@ts-expect-error` com justificativa explícita. +- Mutação `tickets.remove` não requer mais `actorId`; o diálogo de exclusão apenas envia `ticketId`. diff --git a/web/convex/tickets.ts b/web/convex/tickets.ts index 9ce81bd..f3c18bb 100644 --- a/web/convex/tickets.ts +++ b/web/convex/tickets.ts @@ -1,9 +1,7 @@ -import { internalMutation, mutation, query } from "./_generated/server"; +import { mutation, query } from "./_generated/server"; import { ConvexError, v } from "convex/values"; import { Id, type Doc } from "./_generated/dataModel"; -const PRIORITY_ORDER = ["URGENT", "HIGH", "MEDIUM", "LOW"] as const; - const QUEUE_RENAME_LOOKUP: Record = { "Suporte N1": "Chamados", "suporte-n1": "Chamados", @@ -768,6 +766,7 @@ export const playNext = mutation({ const requester = (await ctx.db.get(chosen.requesterId)) as Doc<"users"> | null const assignee = chosen.assigneeId ? ((await ctx.db.get(chosen.assigneeId)) as Doc<"users"> | null) : null const queue = chosen.queueId ? ((await ctx.db.get(chosen.queueId)) as Doc<"queues"> | null) : null + const queueName = normalizeQueueName(queue) return { id: chosen._id, reference: chosen.reference, @@ -777,13 +776,13 @@ export const playNext = mutation({ status: chosen.status, priority: chosen.priority, channel: chosen.channel, - queue: queue?.name ?? null, + queue: queueName, requester: requester && { id: requester._id, name: requester.name, email: requester.email, avatarUrl: requester.avatarUrl, - teams: requester.teams ?? [], + teams: normalizeTeams(requester.teams), }, assignee: assignee ? { @@ -791,7 +790,7 @@ export const playNext = mutation({ name: assignee.name, email: assignee.email, avatarUrl: assignee.avatarUrl, - teams: assignee.teams ?? [], + teams: normalizeTeams(assignee.teams), } : null, slaPolicy: null, @@ -808,8 +807,8 @@ export const playNext = mutation({ }); export const remove = mutation({ - args: { ticketId: v.id("tickets"), actorId: v.id("users") }, - handler: async (ctx, { ticketId, actorId }) => { + args: { ticketId: v.id("tickets") }, + handler: async (ctx, { ticketId }) => { // delete comments (and attachments) const comments = await ctx.db .query("ticketComments") diff --git a/web/convex/users.ts b/web/convex/users.ts index 76b463a..dbcd54f 100644 --- a/web/convex/users.ts +++ b/web/convex/users.ts @@ -16,7 +16,6 @@ export const ensureUser = mutation({ .withIndex("by_tenant_email", (q) => q.eq("tenantId", args.tenantId).eq("email", args.email)) .first(); if (existing) return existing; - const now = Date.now(); const id = await ctx.db.insert("users", { tenantId: args.tenantId, email: args.email, diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index 8502ebc..7c7f05b 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -18,6 +18,7 @@ const eslintConfig = [ "out/**", "build/**", "next-env.d.ts", + "convex/_generated/**", ], }, { diff --git a/web/src/app/ConvexClientProvider.tsx b/web/src/app/ConvexClientProvider.tsx index e85039a..99b2c1b 100644 --- a/web/src/app/ConvexClientProvider.tsx +++ b/web/src/app/ConvexClientProvider.tsx @@ -1,7 +1,7 @@ "use client"; import { ConvexProvider, ConvexReactClient } from "convex/react"; -import { ReactNode, useMemo } from "react"; +import { ReactNode } from "react"; const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL; diff --git a/web/src/app/dev/seed/page.tsx b/web/src/app/dev/seed/page.tsx index bced027..d74cfc5 100644 --- a/web/src/app/dev/seed/page.tsx +++ b/web/src/app/dev/seed/page.tsx @@ -2,8 +2,7 @@ import { useState } from "react"; import { useMutation } from "convex/react"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore +// @ts-expect-error Convex runtime API lacks TypeScript declarations import { api } from "@/convex/_generated/api"; export default function SeedPage() { diff --git a/web/src/app/tickets/new/page.tsx b/web/src/app/tickets/new/page.tsx index 667bc27..3217d6c 100644 --- a/web/src/app/tickets/new/page.tsx +++ b/web/src/app/tickets/new/page.tsx @@ -8,8 +8,7 @@ import type { Id } from "@/convex/_generated/dataModel" import type { TicketPriority, TicketQueueSummary } from "@/lib/schemas/ticket" import { DEFAULT_TENANT_ID } from "@/lib/constants" import { useAuth } from "@/lib/auth-client" -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore +// @ts-expect-error Convex runtime API lacks TypeScript declarations import { api } from "@/convex/_generated/api" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" diff --git a/web/src/components/tickets/delete-ticket-dialog.tsx b/web/src/components/tickets/delete-ticket-dialog.tsx index ea5601d..92891c0 100644 --- a/web/src/components/tickets/delete-ticket-dialog.tsx +++ b/web/src/components/tickets/delete-ticket-dialog.tsx @@ -3,10 +3,9 @@ import { useRouter } from "next/navigation" import { useState } from "react" import { useMutation } from "convex/react" -// @ts-ignore +// @ts-expect-error Convex runtime API lacks TS declarations until build import { api } from "@/convex/_generated/api" import type { Id } from "@/convex/_generated/dataModel" -import { useAuth } from "@/lib/auth-client" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { AlertTriangle, Trash2 } from "lucide-react" @@ -17,14 +16,12 @@ export function DeleteTicketDialog({ ticketId }: { ticketId: Id<"tickets"> }) { const remove = useMutation(api.tickets.remove) const [open, setOpen] = useState(false) const [loading, setLoading] = useState(false) - const { userId } = useAuth() async function confirm() { setLoading(true) toast.loading("Excluindo ticket...", { id: "del" }) try { - if (!userId) throw new Error("No user") - await remove({ ticketId, actorId: userId as Id<"users"> }) + await remove({ ticketId }) toast.success("Ticket excluído.", { id: "del" }) setOpen(false) router.push("/tickets") @@ -41,7 +38,7 @@ export function DeleteTicketDialog({ ticketId }: { ticketId: Id<"tickets"> }) { diff --git a/web/src/components/tickets/new-ticket-dialog.tsx b/web/src/components/tickets/new-ticket-dialog.tsx index 928e6bb..e070859 100644 --- a/web/src/components/tickets/new-ticket-dialog.tsx +++ b/web/src/components/tickets/new-ticket-dialog.tsx @@ -5,7 +5,7 @@ import { useMemo, useState } from "react" import type { Id } from "@/convex/_generated/dataModel" import type { TicketPriority, TicketQueueSummary } from "@/lib/schemas/ticket" import { useMutation, useQuery } from "convex/react" -// @ts-ignore +// @ts-expect-error Convex runtime API lacks TypeScript definitions import { api } from "@/convex/_generated/api" import { DEFAULT_TENANT_ID } from "@/lib/constants" import { useAuth } from "@/lib/auth-client" diff --git a/web/src/components/tickets/play-next-ticket-card.tsx b/web/src/components/tickets/play-next-ticket-card.tsx index 693953f..faa0113 100644 --- a/web/src/components/tickets/play-next-ticket-card.tsx +++ b/web/src/components/tickets/play-next-ticket-card.tsx @@ -5,7 +5,7 @@ import { useState } from "react" import { useRouter } from "next/navigation" import { IconArrowRight, IconPlayerPlayFilled } from "@tabler/icons-react" import { useMutation, useQuery } from "convex/react" -// @ts-ignore +// @ts-expect-error Convex runtime API lacks TypeScript declarations import { api } from "@/convex/_generated/api" import { DEFAULT_TENANT_ID } from "@/lib/constants" import { useAuth } from "@/lib/auth-client" diff --git a/web/src/components/tickets/priority-select.tsx b/web/src/components/tickets/priority-select.tsx index 1f11141..692fe79 100644 --- a/web/src/components/tickets/priority-select.tsx +++ b/web/src/components/tickets/priority-select.tsx @@ -2,7 +2,7 @@ import { useState } from "react" import { useMutation } from "convex/react" -// @ts-ignore +// @ts-expect-error Convex runtime API lacks TypeScript declarations import { api } from "@/convex/_generated/api" import type { Id } from "@/convex/_generated/dataModel" import type { TicketPriority } from "@/lib/schemas/ticket" diff --git a/web/src/components/tickets/recent-tickets-panel.tsx b/web/src/components/tickets/recent-tickets-panel.tsx index 207da13..17d587b 100644 --- a/web/src/components/tickets/recent-tickets-panel.tsx +++ b/web/src/components/tickets/recent-tickets-panel.tsx @@ -1,7 +1,7 @@ "use client"; import { useQuery } from "convex/react"; -// @ts-ignore +// @ts-expect-error Convex runtime API lacks TS declarations import { api } from "@/convex/_generated/api"; import { DEFAULT_TENANT_ID } from "@/lib/constants"; import { mapTicketsFromServerList } from "@/lib/mappers/ticket"; diff --git a/web/src/components/tickets/status-select.tsx b/web/src/components/tickets/status-select.tsx index e53c299..d8c4fa6 100644 --- a/web/src/components/tickets/status-select.tsx +++ b/web/src/components/tickets/status-select.tsx @@ -2,7 +2,7 @@ import { useState } from "react" import { useMutation } from "convex/react" -// @ts-ignore +// @ts-expect-error Convex runtime API lacks TypeScript declarations import { api } from "@/convex/_generated/api" import type { Id } from "@/convex/_generated/dataModel" import type { TicketStatus } from "@/lib/schemas/ticket" diff --git a/web/src/components/tickets/ticket-comments.rich.tsx b/web/src/components/tickets/ticket-comments.rich.tsx index f145b9b..af3029a 100644 --- a/web/src/components/tickets/ticket-comments.rich.tsx +++ b/web/src/components/tickets/ticket-comments.rich.tsx @@ -6,7 +6,7 @@ import { ptBR } from "date-fns/locale" import { IconLock, IconMessage } from "@tabler/icons-react" import { FileIcon, Trash2, X } from "lucide-react" import { useMutation } from "convex/react" -// @ts-ignore +// @ts-expect-error Convex runtime API lacks TypeScript declarations import { api } from "@/convex/_generated/api" import { useAuth } from "@/lib/auth-client" import type { Id } from "@/convex/_generated/dataModel" @@ -203,6 +203,7 @@ export function TicketComments({ ticket }: TicketCommentsProps) { onClick={() => setPreview(url || null)} className="block w-full overflow-hidden rounded-md" > + {/* eslint-disable-next-line @next/next/no-img-element */} {name} ) : ( @@ -256,6 +257,7 @@ export function TicketComments({ ticket }: TicketCommentsProps) { onClick={() => setPreview(previewUrl || null)} className="block w-full overflow-hidden rounded-md" > + {/* eslint-disable-next-line @next/next/no-img-element */} {name} ) : ( @@ -339,7 +341,12 @@ export function TicketComments({ ticket }: TicketCommentsProps) { !open && setPreview(null)}> - {preview ? Preview : null} + {preview ? ( + <> + {/* eslint-disable-next-line @next/next/no-img-element */} + Preview + + ) : null} diff --git a/web/src/components/tickets/ticket-detail-view.tsx b/web/src/components/tickets/ticket-detail-view.tsx index ce66e8a..ce09edd 100644 --- a/web/src/components/tickets/ticket-detail-view.tsx +++ b/web/src/components/tickets/ticket-detail-view.tsx @@ -1,8 +1,7 @@ "use client"; import { useQuery } from "convex/react"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore +// @ts-expect-error Convex runtime API lacks TypeScript definitions import { api } from "@/convex/_generated/api"; import { DEFAULT_TENANT_ID } from "@/lib/constants"; import { mapTicketWithDetailsFromServer } from "@/lib/mappers/ticket"; diff --git a/web/src/components/tickets/ticket-queue-summary.tsx b/web/src/components/tickets/ticket-queue-summary.tsx index fbc60a2..0bd462a 100644 --- a/web/src/components/tickets/ticket-queue-summary.tsx +++ b/web/src/components/tickets/ticket-queue-summary.tsx @@ -1,7 +1,7 @@ "use client" import { useQuery } from "convex/react" -// @ts-ignore +// @ts-expect-error Convex runtime API lacks TypeScript declarations import { api } from "@/convex/_generated/api" import { DEFAULT_TENANT_ID } from "@/lib/constants" import type { TicketQueueSummary } from "@/lib/schemas/ticket" diff --git a/web/src/components/tickets/ticket-summary-header.tsx b/web/src/components/tickets/ticket-summary-header.tsx index df65bf3..bac0434 100644 --- a/web/src/components/tickets/ticket-summary-header.tsx +++ b/web/src/components/tickets/ticket-summary-header.tsx @@ -1,13 +1,12 @@ "use client" -import { useEffect, useMemo, useState } from "react" +import { useEffect, useMemo, useRef, useState } from "react" import { format, formatDistanceToNow } from "date-fns" import { ptBR } from "date-fns/locale" import { IconClock, IconPlayerPause, IconPlayerPlay } from "@tabler/icons-react" import { useMutation, useQuery } from "convex/react" import { toast } from "sonner" -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore +// @ts-expect-error Convex generates JS module without TS definitions import { api } from "@/convex/_generated/api" import { useAuth } from "@/lib/auth-client" @@ -21,7 +20,7 @@ import { StatusSelect } from "@/components/tickets/status-select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" -import { CategorySelectFields } from "@/components/tickets/category-select" +import { useTicketCategories } from "@/hooks/use-ticket-categories" interface TicketHeaderProps { ticket: TicketWithDetails @@ -35,8 +34,8 @@ const pauseButtonClass = "inline-flex items-center gap-1 rounded-lg border border-black bg-black px-3 py-1.5 text-sm font-semibold text-white transition hover:bg-[#18181b]/85 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#18181b]/30" const editButtonClass = "inline-flex items-center gap-1 rounded-lg border border-black bg-black px-3 py-1.5 text-sm font-semibold text-white transition hover:bg-[#18181b]/85 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#18181b]/30" -const selectTriggerClass = "h-8 w-[220px] rounded-lg border border-slate-300 bg-white px-3 text-left text-sm font-medium text-neutral-800 shadow-sm focus:ring-0 data-[state=open]:border-[#00d6eb]" -const smallSelectTriggerClass = "h-8 w-[180px] rounded-lg border border-slate-300 bg-white px-3 text-left text-sm font-medium text-neutral-800 shadow-sm focus:ring-0 data-[state=open]:border-[#00d6eb]" +const selectTriggerClass = "h-8 w-full rounded-lg border border-slate-300 bg-white px-3 text-left text-sm font-medium text-neutral-800 shadow-sm focus:ring-0 data-[state=open]:border-[#00d6eb]" +const smallSelectTriggerClass = "h-8 w-full rounded-lg border border-slate-300 bg-white px-3 text-left text-sm font-medium text-neutral-800 shadow-sm focus:ring-0 data-[state=open]:border-[#00d6eb]" const sectionLabelClass = "text-xs font-semibold uppercase tracking-wide text-neutral-500" const sectionValueClass = "font-medium text-neutral-900" const subtleBadgeClass = @@ -68,6 +67,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) { const updateCategories = useMutation(api.tickets.updateCategories) const agents = (useQuery(api.users.listAgents, { tenantId: ticket.tenantId }) as Doc<"users">[] | undefined) ?? [] const queues = (useQuery(api.queues.summary, { tenantId: ticket.tenantId }) as TicketQueueSummary[] | undefined) ?? [] + const { categories, isLoading: categoriesLoading } = useTicketCategories(ticket.tenantId) const [status] = useState(ticket.status) const workSummaryRemote = useQuery(api.tickets.workSummary, { ticketId: ticket.id as Id<"tickets"> }) as | { @@ -86,10 +86,21 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) { subcategoryId: ticket.subcategory?.id ?? "", }) const [savingCategory, setSavingCategory] = useState(false) + const lastSubmittedCategoryRef = useRef({ + categoryId: ticket.category?.id ?? "", + subcategoryId: ticket.subcategory?.id ?? "", + }) + const selectedCategoryId = categorySelection.categoryId + const selectedSubcategoryId = categorySelection.subcategoryId const dirty = useMemo( () => subject !== ticket.subject || (summary ?? "") !== (ticket.summary ?? ""), [subject, summary, ticket.subject, ticket.summary] ) + const activeCategory = useMemo( + () => categories.find((category) => category.id === selectedCategoryId) ?? null, + [categories, selectedCategoryId] + ) + const secondaryOptions = useMemo(() => activeCategory?.secondary ?? [], [activeCategory]) async function handleSave() { if (!userId) return @@ -115,49 +126,106 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) { } useEffect(() => { - setCategorySelection({ + const nextSelection = { categoryId: ticket.category?.id ?? "", subcategoryId: ticket.subcategory?.id ?? "", - }) + } + setCategorySelection(nextSelection) + lastSubmittedCategoryRef.current = nextSelection }, [ticket.category?.id, ticket.subcategory?.id]) - const categoryDirty = useMemo(() => { - const currentCategory = ticket.category?.id ?? "" - const currentSubcategory = ticket.subcategory?.id ?? "" - return ( - categorySelection.categoryId !== currentCategory || categorySelection.subcategoryId !== currentSubcategory - ) - }, [categorySelection.categoryId, categorySelection.subcategoryId, ticket.category?.id, ticket.subcategory?.id]) + useEffect(() => { + if (!editing) return + if (categoriesLoading) return + if (categories.length === 0) return + if (selectedCategoryId) return + if (ticket.category?.id) return - const handleResetCategory = () => { + const first = categories[0] + const firstSecondary = first.secondary[0] setCategorySelection({ - categoryId: ticket.category?.id ?? "", - subcategoryId: ticket.subcategory?.id ?? "", + categoryId: first.id, + subcategoryId: firstSecondary?.id ?? "", }) - } + }, [categories, categoriesLoading, editing, selectedCategoryId, ticket.category?.id]) - async function handleSaveCategory() { - if (!userId) return - if (!categorySelection.categoryId || !categorySelection.subcategoryId) { - toast.error("Selecione uma categoria válida.") + useEffect(() => { + if (!editing) return + if (!selectedCategoryId) return + if (secondaryOptions.length === 0) { + if (selectedSubcategoryId) { + setCategorySelection((prev) => ({ ...prev, subcategoryId: "" })) + } return } + + const stillValid = secondaryOptions.some((option) => option.id === selectedSubcategoryId) + if (stillValid) return + + const fallback = secondaryOptions[0] + if (fallback) { + setCategorySelection((prev) => ({ ...prev, subcategoryId: fallback.id })) + } + }, [editing, secondaryOptions, selectedCategoryId, selectedSubcategoryId]) + + useEffect(() => { + if (!editing) return + if (!userId) return + const categoryId = selectedCategoryId + const subcategoryId = selectedSubcategoryId + if (!categoryId || !subcategoryId) return + + const currentCategory = ticket.category?.id ?? "" + const currentSubcategory = ticket.subcategory?.id ?? "" + + if (categoryId === currentCategory && subcategoryId === currentSubcategory) { + return + } + + if ( + categoryId === lastSubmittedCategoryRef.current.categoryId && + subcategoryId === lastSubmittedCategoryRef.current.subcategoryId + ) { + return + } + + let cancelled = false + lastSubmittedCategoryRef.current = { categoryId, subcategoryId } setSavingCategory(true) toast.loading("Atualizando categoria...", { id: "ticket-category" }) - try { - await updateCategories({ - ticketId: ticket.id as Id<"tickets">, - categoryId: categorySelection.categoryId as Id<"ticketCategories">, - subcategoryId: categorySelection.subcategoryId as Id<"ticketSubcategories">, - actorId: userId as Id<"users">, - }) - toast.success("Categoria atualizada!", { id: "ticket-category" }) - } catch { - toast.error("Não foi possível atualizar a categoria.", { id: "ticket-category" }) - } finally { - setSavingCategory(false) + + ;(async () => { + try { + await updateCategories({ + ticketId: ticket.id as Id<"tickets">, + categoryId: categoryId as Id<"ticketCategories">, + subcategoryId: subcategoryId as Id<"ticketSubcategories">, + actorId: userId as Id<"users">, + }) + if (!cancelled) { + toast.success("Categoria atualizada!", { id: "ticket-category" }) + } + } catch { + if (!cancelled) { + toast.error("Não foi possível atualizar a categoria.", { id: "ticket-category" }) + const fallback = { + categoryId: currentCategory, + subcategoryId: currentSubcategory, + } + setCategorySelection(fallback) + lastSubmittedCategoryRef.current = fallback + } + } finally { + if (!cancelled) { + setSavingCategory(false) + } + } + })() + + return () => { + cancelled = true } - } + }, [editing, selectedCategoryId, selectedSubcategoryId, ticket.category?.id, ticket.subcategory?.id, ticket.id, updateCategories, userId]) const workSummary = useMemo(() => { if (workSummaryRemote !== undefined) return workSummaryRemote ?? null @@ -276,53 +344,106 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) { {summary ?

{summary}

: null} )} - {editing ? ( -
- - -
- ) : null}
-
- Categorias - { - setCategorySelection((prev) => ({ ...prev, categoryId: value })) - }} - onSubcategoryChange={(value) => { - setCategorySelection((prev) => ({ ...prev, subcategoryId: value })) - }} - /> -
-
+
+ Categoria secundária + {editing ? ( + + ) : ( + {ticket.subcategory?.name ?? "Sem subcategoria"} + )} +
+
+ Fila + {editing ? ( + + ) : ( + {ticket.queue ?? "Sem fila"} + )}
Solicitante @@ -330,59 +451,38 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
Responsável - + {editing ? ( + + ) : ( + {ticket.assignee?.name ?? "Não atribuído"} + )}
- Fila - + Criado em + {format(ticket.createdAt, "dd/MM/yyyy HH:mm", { locale: ptBR })}
Atualizado em @@ -391,10 +491,6 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) { {updatedRelative}
-
- Criado em - {format(ticket.createdAt, "dd/MM/yyyy HH:mm", { locale: ptBR })} -
{ticket.dueAt ? (
SLA até @@ -407,6 +503,16 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) { {ticket.slaPolicy.name}
) : null} + {editing ? ( +
+ + +
+ ) : null}
) diff --git a/web/src/components/tickets/tickets-view.tsx b/web/src/components/tickets/tickets-view.tsx index 7ff2f27..6645b5f 100644 --- a/web/src/components/tickets/tickets-view.tsx +++ b/web/src/components/tickets/tickets-view.tsx @@ -2,8 +2,7 @@ import { useMemo, useState } from "react" import { useQuery } from "convex/react" -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore +// @ts-expect-error Convex runtime API lacks TypeScript definitions import { api } from "@/convex/_generated/api" import { DEFAULT_TENANT_ID } from "@/lib/constants" import { mapTicketsFromServerList } from "@/lib/mappers/ticket" diff --git a/web/src/components/ui/dropzone.tsx b/web/src/components/ui/dropzone.tsx index 527b41f..635bd43 100644 --- a/web/src/components/ui/dropzone.tsx +++ b/web/src/components/ui/dropzone.tsx @@ -1,7 +1,7 @@ "use client"; import { useAction } from "convex/react"; -// @ts-ignore +// @ts-expect-error Convex generates runtime API without TS metadata import { api } from "@/convex/_generated/api"; import { useCallback, useRef, useState } from "react"; import { cn } from "@/lib/utils"; diff --git a/web/src/hooks/use-ticket-categories.ts b/web/src/hooks/use-ticket-categories.ts index ad60571..5258310 100644 --- a/web/src/hooks/use-ticket-categories.ts +++ b/web/src/hooks/use-ticket-categories.ts @@ -2,8 +2,7 @@ import { useEffect, useMemo, useRef } from "react" import { useMutation, useQuery } from "convex/react" -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore +// @ts-expect-error Convex generates runtime API without TS declarations import { api } from "@/convex/_generated/api" import type { TicketCategory } from "@/lib/schemas/category" diff --git a/web/src/lib/auth-client.tsx b/web/src/lib/auth-client.tsx index e5027e8..71972a2 100644 --- a/web/src/lib/auth-client.tsx +++ b/web/src/lib/auth-client.tsx @@ -5,8 +5,7 @@ import type { Doc } from "@/convex/_generated/dataModel"; import { useMutation } from "convex/react"; // Lazy import to avoid build errors before convex is generated -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore +// @ts-expect-error Convex generates runtime API without types until build import { api } from "@/convex/_generated/api"; export type DemoUser = { name: string; email: string; avatarUrl?: string } | null;