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.
This commit is contained in:
@@ -106,6 +106,15 @@
|
||||
let criandoTemplates = $state(false);
|
||||
let templatesFeedback = $state<string | null>(null);
|
||||
|
||||
// Estados para responder chamado
|
||||
let respostaTexto = $state("");
|
||||
let respostaAnexo = $state<File | null>(null);
|
||||
let respostaAnexoId = $state<Id<"_storage"> | null>(null);
|
||||
let respostaAnexoNome = $state<string>("");
|
||||
let marcandoConcluido = $state(false);
|
||||
let respostaFeedback = $state<string | null>(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 @@
|
||||
<td>
|
||||
<span class={getStatusBadge(ticket.status)}>{getStatusLabel(ticket.status)}</span>
|
||||
</td>
|
||||
<td class="text-sm">{(ticket as any).responsavelNome ?? ticket.setorResponsavel ?? "—"}</td>
|
||||
<td class="text-sm">{(ticket as Ticket & { responsavelNome?: string }).responsavelNome ?? ticket.setorResponsavel ?? "—"}</td>
|
||||
<td class="text-sm capitalize">{ticket.prioridade}</td>
|
||||
<td class="text-xs text-base-content/70">
|
||||
{ticket.prazoConclusao ? prazoRestante(ticket.prazoConclusao) : "--"}
|
||||
@@ -681,6 +842,90 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Nova seção: Responder e Gerenciar Chamado -->
|
||||
<div class="rounded-3xl border border-base-200 bg-base-100/80 p-6 shadow-lg">
|
||||
<h3 class="text-lg font-semibold text-base-content">Responder chamado</h3>
|
||||
{#if !detalheSelecionado}
|
||||
<p class="text-sm text-base-content/60 mt-4">Selecione um chamado na tabela para responder.</p>
|
||||
{:else}
|
||||
<div class="mt-4 space-y-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Resposta *</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="textarea textarea-bordered w-full"
|
||||
rows="5"
|
||||
placeholder="Digite sua resposta ao chamado..."
|
||||
bind:value={respostaTexto}
|
||||
disabled={enviandoResposta || marcandoConcluido}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Anexar documento (opcional)</span>
|
||||
</label>
|
||||
<input
|
||||
id="respostaAnexoInput"
|
||||
type="file"
|
||||
class="file-input file-input-bordered w-full"
|
||||
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"
|
||||
onchange={handleAnexoChange}
|
||||
disabled={enviandoResposta || marcandoConcluido}
|
||||
/>
|
||||
{#if respostaAnexoNome}
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<span class="text-sm text-base-content/70">{respostaAnexoNome}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs btn-ghost"
|
||||
onclick={removerAnexo}
|
||||
disabled={enviandoResposta || marcandoConcluido}
|
||||
>
|
||||
Remover
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if respostaFeedback}
|
||||
<div class={`alert ${respostaFeedback.includes('sucesso') ? 'alert-success' : 'alert-error'} text-sm`}>
|
||||
{respostaFeedback}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="btn btn-primary flex-1"
|
||||
type="button"
|
||||
onclick={responderChamado}
|
||||
disabled={enviandoResposta || marcandoConcluido || (!respostaTexto.trim() && !respostaAnexoId)}
|
||||
>
|
||||
{#if enviandoResposta}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Enviando...
|
||||
{:else}
|
||||
Enviar resposta
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-success"
|
||||
type="button"
|
||||
onclick={marcarComoConcluido}
|
||||
disabled={enviandoResposta || marcandoConcluido || detalheSelecionado?.status === "resolvido" || detalheSelecionado?.status === "encerrado"}
|
||||
>
|
||||
{#if marcandoConcluido}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{:else}
|
||||
Marcar como concluído
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="rounded-3xl border border-base-200 bg-base-100/80 p-6 shadow-lg">
|
||||
<h3 class="text-lg font-semibold text-base-content">Atribuir responsável</h3>
|
||||
<div class="mt-4 space-y-3">
|
||||
|
||||
Reference in New Issue
Block a user