From 7c8be8a8186c35619b9be3cfe9e143092d28cfa1 Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Mon, 17 Nov 2025 19:15:50 -0300 Subject: [PATCH] feat: implement response management for tickets in central chamados - Added functionality to respond to tickets, including text input and file attachment options. - Implemented methods for handling file uploads and managing response state, including feedback messages for users. - Enhanced the UI to allow users to select a ticket and provide a response, with options to mark tickets as completed. - Improved type safety by specifying types for user and ticket data throughout the component. --- .../ti/central-chamados/+page.svelte | 257 +++++++++++++++++- 1 file changed, 251 insertions(+), 6 deletions(-) diff --git a/apps/web/src/routes/(dashboard)/ti/central-chamados/+page.svelte b/apps/web/src/routes/(dashboard)/ti/central-chamados/+page.svelte index a8b1ddf..6d4c530 100644 --- a/apps/web/src/routes/(dashboard)/ti/central-chamados/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/central-chamados/+page.svelte @@ -106,6 +106,15 @@ let criandoTemplates = $state(false); let templatesFeedback = $state(null); + // Estados para responder chamado + let respostaTexto = $state(""); + let respostaAnexo = $state(null); + let respostaAnexoId = $state | null>(null); + let respostaAnexoNome = $state(""); + let marcandoConcluido = $state(false); + let respostaFeedback = $state(null); + let enviandoResposta = $state(false); + let carregamentoToken = 0; // Carregar chamados quando filtros mudarem @@ -191,12 +200,12 @@ } // Verificar se é um objeto com propriedade data (como em outros lugares do código) - let usuarios: any[] = []; + let usuarios: Usuario[] = []; if (typeof usuariosQuery === 'object' && usuariosQuery !== null) { if ('data' in usuariosQuery && Array.isArray(usuariosQuery.data)) { - usuarios = usuariosQuery.data; + usuarios = usuariosQuery.data as Usuario[]; } else if (Array.isArray(usuariosQuery)) { - usuarios = usuariosQuery; + usuarios = usuariosQuery as Usuario[]; } else { if (import.meta.env.DEV) { console.log("🔍 [usuariosTI] Formato inesperado:", typeof usuariosQuery, usuariosQuery); @@ -204,7 +213,7 @@ return []; } } else if (Array.isArray(usuariosQuery)) { - usuarios = usuariosQuery; + usuarios = usuariosQuery as Usuario[]; } else { if (import.meta.env.DEV) { console.log("🔍 [usuariosTI] Tipo inesperado:", typeof usuariosQuery, usuariosQuery); @@ -219,7 +228,7 @@ return []; } - const usuariosFiltrados = usuarios.filter((usuario: any) => { + const usuariosFiltrados = usuarios.filter((usuario: Usuario) => { // Verificar se o usuário tem setor "TI" no role (case-insensitive) const setor = usuario.role?.setor; const temSetorTI = setor && setor.toUpperCase() === "TI"; @@ -457,6 +466,158 @@ } } + async function handleAnexoChange(event: Event) { + const target = event.target as HTMLInputElement; + const file = target.files?.[0]; + if (!file) { + respostaAnexo = null; + respostaAnexoId = null; + respostaAnexoNome = ""; + return; + } + + respostaAnexo = file; + respostaAnexoNome = file.name; + + try { + // Gerar URL de upload + const uploadUrl = await client.mutation(api.chamados.generateUploadUrl, {}); + + // Fazer upload do arquivo + const response = await fetch(uploadUrl, { + method: "POST", + headers: { "Content-Type": file.type }, + body: file, + }); + + const result = await response.json(); + respostaAnexoId = result.storageId as Id<"_storage">; + } catch (error) { + console.error("Erro ao fazer upload do anexo:", error); + respostaFeedback = "Erro ao fazer upload do anexo. Tente novamente."; + respostaAnexo = null; + respostaAnexoId = null; + respostaAnexoNome = ""; + } + } + + function removerAnexo() { + respostaAnexo = null; + respostaAnexoId = null; + respostaAnexoNome = ""; + // Resetar o input de arquivo + const fileInput = document.getElementById("respostaAnexoInput") as HTMLInputElement; + if (fileInput) { + fileInput.value = ""; + } + } + + async function responderChamado() { + if (!ticketSelecionado) { + respostaFeedback = "Selecione um chamado primeiro"; + return; + } + + if (!respostaTexto.trim() && !respostaAnexoId) { + respostaFeedback = "Digite uma resposta ou anexe um documento"; + return; + } + + try { + enviandoResposta = true; + respostaFeedback = null; + + const anexos = respostaAnexoId + ? [ + { + arquivoId: respostaAnexoId, + nome: respostaAnexoNome || undefined, + tipo: respostaAnexo?.type || undefined, + tamanho: respostaAnexo?.size || undefined, + }, + ] + : undefined; + + await client.mutation(api.chamados.registrarAtualizacao, { + ticketId: ticketSelecionado, + conteudo: respostaTexto.trim() || "Anexo enviado", + anexos, + visibilidade: "publico", + }); + + respostaFeedback = "Resposta enviada com sucesso"; + respostaTexto = ""; + removerAnexo(); + + // Recarregar chamados para atualizar a lista + await carregarChamados({ + status: filtroStatus === "todos" ? undefined : filtroStatus, + responsavelId: filtroResponsavel === "todos" ? undefined : filtroResponsavel, + setor: filtroSetor === "todos" ? undefined : filtroSetor, + }); + + if (ticketSelecionado) { + selecionarTicket(ticketSelecionado); + } + } catch (error) { + console.error("Erro ao responder chamado:", error); + respostaFeedback = error instanceof Error ? error.message : "Erro ao enviar resposta"; + } finally { + enviandoResposta = false; + } + } + + async function marcarComoConcluido() { + if (!ticketSelecionado) { + respostaFeedback = "Selecione um chamado primeiro"; + return; + } + + try { + marcandoConcluido = true; + respostaFeedback = null; + + const anexos = respostaAnexoId + ? [ + { + arquivoId: respostaAnexoId, + nome: respostaAnexoNome || undefined, + tipo: respostaAnexo?.type || undefined, + tamanho: respostaAnexo?.size || undefined, + }, + ] + : undefined; + + await client.mutation(api.chamados.registrarAtualizacao, { + ticketId: ticketSelecionado, + conteudo: respostaTexto.trim() || "Chamado marcado como concluído", + anexos, + visibilidade: "publico", + proximoStatus: "resolvido", + }); + + respostaFeedback = "Chamado marcado como concluído com sucesso"; + respostaTexto = ""; + removerAnexo(); + + // Recarregar chamados para atualizar a lista + await carregarChamados({ + status: filtroStatus === "todos" ? undefined : filtroStatus, + responsavelId: filtroResponsavel === "todos" ? undefined : filtroResponsavel, + setor: filtroSetor === "todos" ? undefined : filtroSetor, + }); + + if (ticketSelecionado) { + selecionarTicket(ticketSelecionado); + } + } catch (error) { + console.error("Erro ao marcar como concluído:", error); + respostaFeedback = error instanceof Error ? error.message : "Erro ao marcar como concluído"; + } finally { + marcandoConcluido = false; + } + } + // Debug: ver templates carregados (remover em produção) // $effect(() => { @@ -634,7 +795,7 @@ {getStatusLabel(ticket.status)} - {(ticket as any).responsavelNome ?? ticket.setorResponsavel ?? "—"} + {(ticket as Ticket & { responsavelNome?: string }).responsavelNome ?? ticket.setorResponsavel ?? "—"} {ticket.prioridade} {ticket.prazoConclusao ? prazoRestante(ticket.prazoConclusao) : "--"} @@ -681,6 +842,90 @@ {/if} + +
+

Responder chamado

+ {#if !detalheSelecionado} +

Selecione um chamado na tabela para responder.

+ {:else} +
+
+ + +
+ +
+ + + {#if respostaAnexoNome} +
+ {respostaAnexoNome} + +
+ {/if} +
+ + {#if respostaFeedback} +
+ {respostaFeedback} +
+ {/if} + +
+ + +
+
+ {/if} +
+

Atribuir responsável