Feat cibersecurity #27
186
RELATORIO_TESTES.md
Normal file
186
RELATORIO_TESTES.md
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
# Relatório de Testes - Sistema de Central de Chamados
|
||||||
|
|
||||||
|
**Data:** 16 de novembro de 2025
|
||||||
|
**Testador:** Sistema Automatizado
|
||||||
|
**Página Testada:** `/ti/central-chamados`
|
||||||
|
|
||||||
|
## Resumo Executivo
|
||||||
|
|
||||||
|
Foram realizados testes completos na página de Central de Chamados do sistema SGSE. A maioria das funcionalidades está funcionando corretamente, mas foram identificados alguns problemas que precisam ser corrigidos.
|
||||||
|
|
||||||
|
## Testes Realizados
|
||||||
|
|
||||||
|
### ✅ Testes Bem-Sucedidos
|
||||||
|
|
||||||
|
1. **Login no Sistema**
|
||||||
|
- Status: ✅ PASSOU
|
||||||
|
- Usuário logado: Deyvison (dfw@poli.br)
|
||||||
|
|
||||||
|
2. **Visualização de SLAs Configurados**
|
||||||
|
- Status: ✅ PASSOU
|
||||||
|
- Tabela de SLAs exibe 7 SLAs ativos corretamente
|
||||||
|
- Resumo mostra: 4 Baixa, 2 Média, 1 Alta/Crítica
|
||||||
|
- Detalhes completos (tempos, prioridades) são exibidos corretamente
|
||||||
|
|
||||||
|
3. **Cards de Prioridade**
|
||||||
|
- Status: ✅ PASSOU
|
||||||
|
- Cards mostram corretamente "Configurado" ou "Não configurado"
|
||||||
|
- Botão "Configurar" funciona corretamente
|
||||||
|
- Detalhes dos SLAs configurados são exibidos nos cards
|
||||||
|
|
||||||
|
4. **Criação de SLA**
|
||||||
|
- Status: ✅ PASSOU
|
||||||
|
- SLA criado com sucesso para prioridade "Alta"
|
||||||
|
- Formulário preenche corretamente quando clica em "Configurar"
|
||||||
|
- Tabela atualiza automaticamente após criação
|
||||||
|
- Card de prioridade atualiza para "Configurado"
|
||||||
|
|
||||||
|
5. **Edição de SLA**
|
||||||
|
- Status: ✅ PASSOU
|
||||||
|
- Botão "Editar" abre formulário com dados corretos
|
||||||
|
- Atualização funciona corretamente
|
||||||
|
|
||||||
|
6. **Lista de Chamados**
|
||||||
|
- Status: ✅ PASSOU
|
||||||
|
- 4 chamados sendo exibidos corretamente
|
||||||
|
- Filtros funcionando (status, responsável, setor)
|
||||||
|
- Detalhes do chamado são exibidos ao selecionar
|
||||||
|
|
||||||
|
7. **Atribuição de Responsável**
|
||||||
|
- Status: ✅ PASSOU
|
||||||
|
- Dropdown mostra 2 usuários TI: Deyvison e Suporte_TI
|
||||||
|
- Formulário está funcional
|
||||||
|
|
||||||
|
8. **Prorrogação de Prazo**
|
||||||
|
- Status: ✅ PASSOU
|
||||||
|
- Dropdown de tickets carrega corretamente (4 tickets)
|
||||||
|
- Formulário permite selecionar tipo de prazo e horas
|
||||||
|
- Botão habilita quando todos os campos estão preenchidos
|
||||||
|
|
||||||
|
### ⚠️ Problemas Identificados
|
||||||
|
|
||||||
|
#### 1. Templates de Email - Listagem Após Criação
|
||||||
|
|
||||||
|
- **Status:** ⚠️ PROBLEMA
|
||||||
|
- **Descrição:** Templates são criados com sucesso (mensagem "Templates padrão criados com sucesso" aparece), mas não são listados na interface após criação
|
||||||
|
- **Ação Realizada:** Botão "Criar templates padrão" foi clicado e retornou sucesso
|
||||||
|
- **Comportamento Esperado:** Templates deveriam aparecer em uma lista após criação
|
||||||
|
- **Comportamento Atual:** Seção continua mostrando "Nenhum template encontrado"
|
||||||
|
- **Severidade:** MÉDIA
|
||||||
|
- **Impacto:** Usuários não conseguem visualizar/editar templates de email após criação
|
||||||
|
- **Possível Causa:** Query de templates pode não estar sendo atualizada após criação, ou filtro pode estar excluindo templates de chamados
|
||||||
|
|
||||||
|
#### 2. Warning no Console - Token de Autenticação
|
||||||
|
|
||||||
|
- **Status:** ⚠️ AVISO (Não crítico)
|
||||||
|
- **Descrição:** `⚠️ [useConvexWithAuth] Token não disponível` aparece no console durante carregamento inicial
|
||||||
|
- **Severidade:** BAIXA
|
||||||
|
- **Impacto:** Não afeta funcionalidade (autenticação funciona corretamente após carregamento)
|
||||||
|
- **Observação:** Parece ser um problema de timing durante inicialização da página
|
||||||
|
|
||||||
|
#### 3. Warning no Console - Formato de Query
|
||||||
|
|
||||||
|
- **Status:** ⚠️ AVISO (Não crítico)
|
||||||
|
- **Descrição:** `🔍 [usuariosTI] Formato inesperado: object {data: undefined, isLoading: undefined, error: undefined, isStale: undefined}` aparece no console
|
||||||
|
- **Severidade:** BAIXA
|
||||||
|
- **Impacto:** Não afeta funcionalidade (usuários são carregados corretamente - 2 usuários TI encontrados)
|
||||||
|
- **Observação:** Indica possível inconsistência no formato de retorno da query durante carregamento inicial
|
||||||
|
|
||||||
|
## Detalhes dos Testes
|
||||||
|
|
||||||
|
### Teste de Criação de SLA
|
||||||
|
|
||||||
|
- **Prioridade Testada:** Alta
|
||||||
|
- **Valores Inseridos:**
|
||||||
|
- Nome: "SLA - Alta - Teste"
|
||||||
|
- Tempo de Resposta: 2h
|
||||||
|
- Tempo de Conclusão: 8h
|
||||||
|
- Auto-encerramento: 24h
|
||||||
|
- Alerta: 2h antes
|
||||||
|
- **Resultado:** ✅ SLA criado e exibido na tabela e no card
|
||||||
|
|
||||||
|
### Teste de Edição de SLA
|
||||||
|
|
||||||
|
- **SLA Editado:** Prioridade Baixa
|
||||||
|
- **Alterações:**
|
||||||
|
- Nome: "SLA Baixa - Editado em Teste"
|
||||||
|
- Tempo de Resposta: 6h
|
||||||
|
- **Resultado:** ✅ Atualização bem-sucedida
|
||||||
|
|
||||||
|
### Teste de Prorrogação
|
||||||
|
|
||||||
|
- **Ticket Selecionado:** SGSE-202511-3750
|
||||||
|
- **Prazo:** Conclusão
|
||||||
|
- **Horas Adicionais:** 24h
|
||||||
|
- **Motivo:** "Teste de prorrogação de prazo - necessário mais tempo para análise"
|
||||||
|
- **Resultado:** ✅ Formulário preenchido corretamente, botão habilitado
|
||||||
|
|
||||||
|
## Lista de Erros Encontrados
|
||||||
|
|
||||||
|
### Erros Críticos
|
||||||
|
|
||||||
|
- **Nenhum erro crítico encontrado**
|
||||||
|
|
||||||
|
### Erros de Funcionalidade
|
||||||
|
|
||||||
|
1. **Templates de Email não aparecem após criação**
|
||||||
|
- Localização: Seção "Templates de Email - Chamados"
|
||||||
|
- Ação necessária: Verificar query de templates e atualização reativa após criação
|
||||||
|
|
||||||
|
### Avisos (Warnings)
|
||||||
|
|
||||||
|
1. **Token de autenticação não disponível durante carregamento inicial**
|
||||||
|
- Localização: Console do navegador
|
||||||
|
- Ação necessária: Melhorar timing de inicialização de autenticação
|
||||||
|
|
||||||
|
2. **Formato inesperado de query durante carregamento**
|
||||||
|
- Localização: Console do navegador (usuariosTI)
|
||||||
|
- Ação necessária: Verificar formato de retorno de useQuery do convex-svelte
|
||||||
|
|
||||||
|
## Recomendações
|
||||||
|
|
||||||
|
### Prioridade ALTA
|
||||||
|
|
||||||
|
1. **Corrigir listagem de templates de email após criação**
|
||||||
|
- Verificar se a query `templatesChamados` está sendo atualizada após criação
|
||||||
|
- Verificar se o filtro de templates está correto (deve incluir templates de chamados)
|
||||||
|
- Adicionar refresh automático após criação de templates
|
||||||
|
|
||||||
|
### Prioridade MÉDIA
|
||||||
|
|
||||||
|
2. **Investigar e corrigir warnings no console**
|
||||||
|
- Melhorar timing de autenticação para evitar warning inicial
|
||||||
|
- Padronizar formato de retorno de queries do convex-svelte
|
||||||
|
|
||||||
|
### Prioridade BAIXA
|
||||||
|
|
||||||
|
3. **Melhorar logs de debug**
|
||||||
|
- Reduzir verbosidade de logs informativos
|
||||||
|
- Manter apenas logs de erro e warnings importantes
|
||||||
|
|
||||||
|
## Conclusão
|
||||||
|
|
||||||
|
O sistema está **funcionalmente operacional**, com a maioria das funcionalidades testadas funcionando corretamente:
|
||||||
|
|
||||||
|
✅ **Funcionalidades Testadas e Funcionando:**
|
||||||
|
|
||||||
|
- Login e autenticação
|
||||||
|
- Visualização de SLAs (tabela e cards)
|
||||||
|
- Criação de SLAs
|
||||||
|
- Edição de SLAs
|
||||||
|
- Lista de chamados
|
||||||
|
- Atribuição de responsável
|
||||||
|
- Prorrogação de prazo (formulário funcional)
|
||||||
|
- Criação de templates (backend funciona, frontend não atualiza)
|
||||||
|
|
||||||
|
⚠️ **Problemas Identificados:**
|
||||||
|
|
||||||
|
- Templates não aparecem na lista após criação (problema de atualização reativa)
|
||||||
|
- Warnings no console (não afetam funcionalidade)
|
||||||
|
|
||||||
|
**Status Geral:** ✅ **OPERACIONAL COM PEQUENOS AJUSTES NECESSÁRIOS**
|
||||||
|
|
||||||
|
**Próximos Passos:**
|
||||||
|
|
||||||
|
1. Corrigir atualização reativa de templates após criação
|
||||||
|
2. Investigar e resolver warnings do console (opcional, não crítico)
|
||||||
@@ -30,15 +30,25 @@ export function useConvexWithAuth() {
|
|||||||
const clientWithAuth = client as ConvexClientWithAuth;
|
const clientWithAuth = client as ConvexClientWithAuth;
|
||||||
|
|
||||||
// Configurar token se disponível
|
// Configurar token se disponível
|
||||||
if (clientWithAuth && typeof clientWithAuth.setAuth === "function" && token) {
|
if (clientWithAuth && token) {
|
||||||
try {
|
try {
|
||||||
clientWithAuth.setAuth(token);
|
// Tentar setAuth se disponível
|
||||||
if (import.meta.env.DEV) {
|
if (typeof clientWithAuth.setAuth === "function") {
|
||||||
console.log("✅ [useConvexWithAuth] Token configurado:", token.substring(0, 20) + "...");
|
clientWithAuth.setAuth(token);
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log("✅ [useConvexWithAuth] Token configurado via setAuth:", token.substring(0, 20) + "...");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Se setAuth não estiver disponível, o token deve ser passado via createSvelteAuthClient
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log("ℹ️ [useConvexWithAuth] Token disponível, autenticação gerenciada por createSvelteAuthClient");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("⚠️ [useConvexWithAuth] Erro ao configurar token:", e);
|
console.warn("⚠️ [useConvexWithAuth] Erro ao configurar token:", e);
|
||||||
}
|
}
|
||||||
|
} else if (!token && import.meta.env.DEV) {
|
||||||
|
console.warn("⚠️ [useConvexWithAuth] Token não disponível");
|
||||||
}
|
}
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
|
||||||
import { useConvexClient, useQuery } from "convex-svelte";
|
import { useConvexClient, useQuery } from "convex-svelte";
|
||||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||||
import type { Doc, Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
import type { Doc, Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||||
@@ -11,6 +10,7 @@
|
|||||||
prazoRestante,
|
prazoRestante,
|
||||||
} from "$lib/utils/chamados";
|
} from "$lib/utils/chamados";
|
||||||
import { useConvexWithAuth } from "$lib/hooks/useConvexWithAuth";
|
import { useConvexWithAuth } from "$lib/hooks/useConvexWithAuth";
|
||||||
|
import { authStore } from "$lib/stores/auth.svelte";
|
||||||
|
|
||||||
type Ticket = Doc<"tickets">;
|
type Ticket = Doc<"tickets">;
|
||||||
type Usuario = Doc<"usuarios">;
|
type Usuario = Doc<"usuarios">;
|
||||||
@@ -18,20 +18,26 @@
|
|||||||
type Template = Doc<"templatesMensagens">;
|
type Template = Doc<"templatesMensagens">;
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
|
|
||||||
|
// createSvelteAuthClient gerencia autenticação automaticamente
|
||||||
|
// Não precisamos verificar token manualmente, apenas garantir que useConvexWithAuth seja chamado
|
||||||
|
$effect(() => {
|
||||||
|
// Sempre chamar useConvexWithAuth para garantir que autenticação está configurada
|
||||||
|
useConvexWithAuth();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Queries - executar normalmente, o createSvelteAuthClient no layout gerencia autenticação
|
||||||
const usuariosQuery = useQuery(api.usuarios.listar, {});
|
const usuariosQuery = useQuery(api.usuarios.listar, {});
|
||||||
const slaConfigsQuery = useQuery(api.chamados.listarSlaConfigs, {});
|
const slaConfigsQuery = useQuery(api.chamados.listarSlaConfigs, {});
|
||||||
const templatesQuery = useQuery(api.templatesMensagens.listarTemplates, {});
|
const templatesQuery = useQuery(api.templatesMensagens.listarTemplates, {});
|
||||||
|
|
||||||
// Extrair dados dos templates
|
// Extrair dados dos templates
|
||||||
const templates = $derived.by(() => {
|
const templates = $derived.by(() => {
|
||||||
if (!templatesQuery || templatesQuery === undefined || templatesQuery === null) {
|
// useQuery retorna undefined enquanto carrega, depois retorna os dados diretamente
|
||||||
|
if (templatesQuery === undefined || templatesQuery === null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
// Se tem propriedade data, usar os dados
|
// useQuery retorna os dados diretamente (não em .data)
|
||||||
if ('data' in templatesQuery && templatesQuery.data !== undefined) {
|
|
||||||
return Array.isArray(templatesQuery.data) ? templatesQuery.data : [];
|
|
||||||
}
|
|
||||||
// Se templatesQuery é diretamente um array (caso não tenha .data)
|
|
||||||
if (Array.isArray(templatesQuery)) {
|
if (Array.isArray(templatesQuery)) {
|
||||||
return templatesQuery;
|
return templatesQuery;
|
||||||
}
|
}
|
||||||
@@ -39,22 +45,11 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const carregandoTemplates = $derived.by(() => {
|
const carregandoTemplates = $derived.by(() => {
|
||||||
if (!templatesQuery || templatesQuery === undefined || templatesQuery === null) {
|
// useQuery retorna undefined enquanto carrega
|
||||||
return true;
|
return templatesQuery === undefined || templatesQuery === null;
|
||||||
}
|
|
||||||
if (typeof templatesQuery === 'object' && Object.keys(templatesQuery).length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!('data' in templatesQuery)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (templatesQuery.data === undefined) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const templatesChamados = $derived(() => {
|
const templatesChamados = $derived.by(() => {
|
||||||
return templates.filter((t: Template) => {
|
return templates.filter((t: Template) => {
|
||||||
if (!t.codigo) return false;
|
if (!t.codigo) return false;
|
||||||
return typeof t.codigo === 'string' && t.codigo.startsWith("chamado_");
|
return typeof t.codigo === 'string' && t.codigo.startsWith("chamado_");
|
||||||
@@ -107,20 +102,27 @@
|
|||||||
let prorrogacaoFeedback = $state<string | null>(null);
|
let prorrogacaoFeedback = $state<string | null>(null);
|
||||||
let criandoTemplates = $state(false);
|
let criandoTemplates = $state(false);
|
||||||
let templatesFeedback = $state<string | null>(null);
|
let templatesFeedback = $state<string | null>(null);
|
||||||
|
let migrandoSLAs = $state(false);
|
||||||
|
let migracaoFeedback = $state<string | null>(null);
|
||||||
|
|
||||||
let carregamentoToken = 0;
|
let carregamentoToken = 0;
|
||||||
$effect(() => {
|
|
||||||
const filtros = {
|
|
||||||
status: filtroStatus === "todos" ? undefined : filtroStatus,
|
|
||||||
responsavelId: filtroResponsavel === "todos" ? undefined : filtroResponsavel,
|
|
||||||
setor: filtroSetor === "todos" ? undefined : filtroSetor,
|
|
||||||
};
|
|
||||||
carregarChamados(filtros);
|
|
||||||
});
|
|
||||||
|
|
||||||
onMount(() => {
|
// Carregar chamados quando filtros mudarem
|
||||||
// Configura token no cliente Convex
|
$effect(() => {
|
||||||
useConvexWithAuth();
|
// Pequeno delay para garantir que autenticação está configurada
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
const filtros = {
|
||||||
|
status: filtroStatus === "todos" ? undefined : filtroStatus,
|
||||||
|
responsavelId: filtroResponsavel === "todos" ? undefined : filtroResponsavel,
|
||||||
|
setor: filtroSetor === "todos" ? undefined : filtroSetor,
|
||||||
|
};
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log("🚀 [effect] Carregando chamados com filtros:", filtros);
|
||||||
|
}
|
||||||
|
carregarChamados(filtros);
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function carregarChamados(filtros: {
|
async function carregarChamados(filtros: {
|
||||||
@@ -131,18 +133,43 @@
|
|||||||
try {
|
try {
|
||||||
carregandoChamados = true;
|
carregandoChamados = true;
|
||||||
const token = ++carregamentoToken;
|
const token = ++carregamentoToken;
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log("🔍 [carregarChamados] Executando query com filtros:", filtros);
|
||||||
|
}
|
||||||
|
|
||||||
|
// createSvelteAuthClient gerencia autenticação automaticamente
|
||||||
const data = await client.query(api.chamados.listarChamadosTI, {
|
const data = await client.query(api.chamados.listarChamadosTI, {
|
||||||
status: filtros.status,
|
status: filtros.status,
|
||||||
responsavelId: filtros.responsavelId,
|
responsavelId: filtros.responsavelId,
|
||||||
setor: filtros.setor,
|
setor: filtros.setor,
|
||||||
});
|
});
|
||||||
if (token !== carregamentoToken) return;
|
|
||||||
|
if (token !== carregamentoToken) {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log("🔍 [carregarChamados] Query cancelada (nova requisição iniciada)");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log("✅ [carregarChamados] Query executada com sucesso. Chamados retornados:", data?.length ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
tickets = data ?? [];
|
tickets = data ?? [];
|
||||||
|
|
||||||
if (!ticketSelecionado && tickets.length > 0) {
|
if (!ticketSelecionado && tickets.length > 0) {
|
||||||
selecionarTicket(tickets[0]._id);
|
selecionarTicket(tickets[0]._id);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao carregar chamados:", error);
|
console.error("❌ [carregarChamados] Erro ao carregar chamados:", error);
|
||||||
|
// Se erro de autenticação, tentar novamente após um pequeno delay
|
||||||
|
if (error instanceof Error && (error.message.includes("autenticado") || error.message.includes("authentication"))) {
|
||||||
|
console.warn("⚠️ [carregarChamados] Erro de autenticação detectado, tentando novamente...");
|
||||||
|
setTimeout(() => {
|
||||||
|
carregarChamados(filtros);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
carregandoChamados = false;
|
carregandoChamados = false;
|
||||||
}
|
}
|
||||||
@@ -153,9 +180,61 @@
|
|||||||
detalheSelecionado = tickets.find((t) => t._id === ticketId) ?? null;
|
detalheSelecionado = tickets.find((t) => t._id === ticketId) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const usuariosTI = $derived(() => {
|
const usuariosTI = $derived.by(() => {
|
||||||
if (!usuariosQuery?.data) return [];
|
// useQuery retorna undefined enquanto carrega, depois retorna os dados diretamente
|
||||||
return usuariosQuery.data.filter((usuario: Usuario) => usuario.setor === "TI");
|
if (usuariosQuery === undefined || usuariosQuery === null) {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log("🔍 [usuariosTI] Query ainda carregando...", usuariosQuery);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar se é um objeto com propriedade data (como em outros lugares do código)
|
||||||
|
let usuarios: any[] = [];
|
||||||
|
if (typeof usuariosQuery === 'object' && usuariosQuery !== null) {
|
||||||
|
if ('data' in usuariosQuery && Array.isArray(usuariosQuery.data)) {
|
||||||
|
usuarios = usuariosQuery.data;
|
||||||
|
} else if (Array.isArray(usuariosQuery)) {
|
||||||
|
usuarios = usuariosQuery;
|
||||||
|
} else {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log("🔍 [usuariosTI] Formato inesperado:", typeof usuariosQuery, usuariosQuery);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} else if (Array.isArray(usuariosQuery)) {
|
||||||
|
usuarios = usuariosQuery;
|
||||||
|
} else {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log("🔍 [usuariosTI] Tipo inesperado:", typeof usuariosQuery, usuariosQuery);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usuarios.length === 0) {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log("🔍 [usuariosTI] Nenhum usuário retornado");
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const usuariosFiltrados = usuarios.filter((usuario: any) => {
|
||||||
|
// Verificar se o usuário tem setor "TI" no role (case-insensitive)
|
||||||
|
const setor = usuario.role?.setor;
|
||||||
|
const temSetorTI = setor && setor.toUpperCase() === "TI";
|
||||||
|
if (import.meta.env.DEV && temSetorTI) {
|
||||||
|
console.log("✅ [usuariosTI] Usuário TI encontrado:", usuario.nome, usuario.role?.setor);
|
||||||
|
}
|
||||||
|
// Log para debug: mostrar todos os setores encontrados
|
||||||
|
if (import.meta.env.DEV && setor) {
|
||||||
|
console.log("🔍 [usuariosTI] Usuário:", usuario.nome, "Setor:", setor);
|
||||||
|
}
|
||||||
|
return temSetorTI;
|
||||||
|
});
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log("📊 [usuariosTI] Total de usuários:", usuarios.length, "Usuários TI:", usuariosFiltrados.length);
|
||||||
|
}
|
||||||
|
return usuariosFiltrados;
|
||||||
});
|
});
|
||||||
|
|
||||||
const estatisticas = $derived(() => {
|
const estatisticas = $derived(() => {
|
||||||
@@ -182,11 +261,11 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function novoSla() {
|
function novoSla(prioridade?: "baixa" | "media" | "alta" | "critica") {
|
||||||
slaForm = {
|
slaForm = {
|
||||||
nome: "",
|
nome: "",
|
||||||
descricao: "",
|
descricao: "",
|
||||||
prioridade: "media",
|
prioridade: prioridade || slaForm.prioridade || "media",
|
||||||
tempoRespostaHoras: 4,
|
tempoRespostaHoras: 4,
|
||||||
tempoConclusaoHoras: 24,
|
tempoConclusaoHoras: 24,
|
||||||
tempoEncerramentoHoras: 72,
|
tempoEncerramentoHoras: 72,
|
||||||
@@ -194,6 +273,7 @@
|
|||||||
ativo: true,
|
ativo: true,
|
||||||
};
|
};
|
||||||
slaFeedback = null;
|
slaFeedback = null;
|
||||||
|
slaParaExcluir = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function salvarSlaConfig() {
|
async function salvarSlaConfig() {
|
||||||
@@ -249,8 +329,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const slaConfigsPorPrioridade = $derived(() => {
|
const slaConfigsPorPrioridade = $derived.by(() => {
|
||||||
const slaConfigs = slaConfigsQuery?.data || [];
|
// useQuery retorna um objeto com propriedade .data
|
||||||
|
if (slaConfigsQuery === undefined || slaConfigsQuery === null) {
|
||||||
|
return {
|
||||||
|
baixa: undefined,
|
||||||
|
media: undefined,
|
||||||
|
alta: undefined,
|
||||||
|
critica: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Verificar se tem propriedade data
|
||||||
|
const slaConfigs = ('data' in slaConfigsQuery && slaConfigsQuery.data !== undefined)
|
||||||
|
? (Array.isArray(slaConfigsQuery.data) ? slaConfigsQuery.data : [])
|
||||||
|
: (Array.isArray(slaConfigsQuery) ? slaConfigsQuery : []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
baixa: slaConfigs.find((s: SlaConfig) => s.prioridade === "baixa" && s.ativo),
|
baixa: slaConfigs.find((s: SlaConfig) => s.prioridade === "baixa" && s.ativo),
|
||||||
media: slaConfigs.find((s: SlaConfig) => s.prioridade === "media" && s.ativo),
|
media: slaConfigs.find((s: SlaConfig) => s.prioridade === "media" && s.ativo),
|
||||||
@@ -345,12 +438,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug: ver templates carregados
|
async function migrarSlaConfigs() {
|
||||||
$effect(() => {
|
try {
|
||||||
console.log("templatesQuery:", templatesQuery);
|
migrandoSLAs = true;
|
||||||
console.log("Templates extraídos:", templates);
|
migracaoFeedback = null;
|
||||||
console.log("Templates de chamados:", templatesChamados);
|
const resultado = await client.mutation(api.chamados.migrarSlaConfigs, {});
|
||||||
});
|
migracaoFeedback = `Migração concluída: ${resultado?.migrados || 0} SLA(s) migrado(s) de ${resultado?.total || 0} total`;
|
||||||
|
// Recarregar SLAs após migração
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
// Recarregar página para atualizar dados
|
||||||
|
window.location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao migrar SLAs:", error);
|
||||||
|
migracaoFeedback = error instanceof Error ? error.message : "Erro ao migrar SLAs";
|
||||||
|
} finally {
|
||||||
|
migrandoSLAs = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug: ver templates carregados (remover em produção)
|
||||||
|
// $effect(() => {
|
||||||
|
// console.log("templatesQuery:", templatesQuery);
|
||||||
|
// console.log("Templates extraídos:", templates);
|
||||||
|
// console.log("Templates de chamados:", templatesChamados);
|
||||||
|
// });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="mx-auto w-full max-w-7xl space-y-8 px-4 py-8">
|
<main class="mx-auto w-full max-w-7xl space-y-8 px-4 py-8">
|
||||||
@@ -447,7 +558,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<span class={getStatusBadge(ticket.status)}>{getStatusLabel(ticket.status)}</span>
|
<span class={getStatusBadge(ticket.status)}>{getStatusLabel(ticket.status)}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-sm">{ticket.setorResponsavel ?? "—"}</td>
|
<td class="text-sm">{(ticket as any).responsavelNome ?? ticket.setorResponsavel ?? "—"}</td>
|
||||||
<td class="text-sm capitalize">{ticket.prioridade}</td>
|
<td class="text-sm capitalize">{ticket.prioridade}</td>
|
||||||
<td class="text-xs text-base-content/70">
|
<td class="text-xs text-base-content/70">
|
||||||
{ticket.prazoConclusao ? prazoRestante(ticket.prazoConclusao) : "--"}
|
{ticket.prazoConclusao ? prazoRestante(ticket.prazoConclusao) : "--"}
|
||||||
@@ -592,6 +703,167 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Seção: SLAs Existentes - Visualização Detalhada -->
|
||||||
|
<section class="rounded-3xl border border-base-200 bg-base-100/80 p-6 shadow-xl">
|
||||||
|
<div class="mb-6 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-base-content">SLAs Configurados</h3>
|
||||||
|
<p class="text-sm text-base-content/60">Visualize todos os SLAs ativos com seus tempos e configurações</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if slaConfigsQuery === undefined || slaConfigsQuery === null || ('data' in slaConfigsQuery && slaConfigsQuery.data === undefined)}
|
||||||
|
<div class="flex justify-center py-8">
|
||||||
|
<span class="loading loading-spinner loading-md"></span>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{@const slaConfigs = ('data' in slaConfigsQuery && slaConfigsQuery.data !== undefined)
|
||||||
|
? (Array.isArray(slaConfigsQuery.data) ? slaConfigsQuery.data : [])
|
||||||
|
: (Array.isArray(slaConfigsQuery) ? slaConfigsQuery : [])}
|
||||||
|
{@const slaConfigsAtivos = slaConfigs.filter((s: SlaConfig) => s.ativo)}
|
||||||
|
{@const slaConfigsPorPrioridadeCount = {
|
||||||
|
baixa: slaConfigsAtivos.filter((s: SlaConfig) => s.prioridade === 'baixa').length,
|
||||||
|
media: slaConfigsAtivos.filter((s: SlaConfig) => s.prioridade === 'media').length,
|
||||||
|
alta: slaConfigsAtivos.filter((s: SlaConfig) => s.prioridade === 'alta').length,
|
||||||
|
critica: slaConfigsAtivos.filter((s: SlaConfig) => s.prioridade === 'critica').length,
|
||||||
|
}}
|
||||||
|
|
||||||
|
{#if slaConfigsAtivos.length === 0}
|
||||||
|
<div class="rounded-2xl border border-base-300 bg-base-200/50 p-8 text-center">
|
||||||
|
<p class="text-sm font-semibold text-base-content/70">Nenhum SLA configurado</p>
|
||||||
|
<p class="mt-2 text-xs text-base-content/50">Configure SLAs para cada prioridade na seção abaixo</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- Resumo de SLAs -->
|
||||||
|
<div class="mb-4 grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||||
|
<div class="rounded-xl border border-base-300 bg-base-200/30 p-3 text-center">
|
||||||
|
<div class="text-2xl font-bold text-base-content">{slaConfigsAtivos.length}</div>
|
||||||
|
<div class="text-xs text-base-content/60">Total de SLAs</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-base-300 bg-base-200/30 p-3 text-center">
|
||||||
|
<div class="text-2xl font-bold text-success">{slaConfigsPorPrioridadeCount.baixa}</div>
|
||||||
|
<div class="text-xs text-base-content/60">Prioridade Baixa</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-base-300 bg-base-200/30 p-3 text-center">
|
||||||
|
<div class="text-2xl font-bold text-info">{slaConfigsPorPrioridadeCount.media}</div>
|
||||||
|
<div class="text-xs text-base-content/60">Prioridade Média</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-base-300 bg-base-200/30 p-3 text-center">
|
||||||
|
<div class="text-2xl font-bold text-warning">{slaConfigsPorPrioridadeCount.alta + slaConfigsPorPrioridadeCount.critica}</div>
|
||||||
|
<div class="text-xs text-base-content/60">Prioridade Alta/Crítica</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabela de SLAs -->
|
||||||
|
<div class="overflow-x-auto rounded-xl border border-base-300">
|
||||||
|
<table class="table w-full">
|
||||||
|
<thead class="bg-base-200/50">
|
||||||
|
<tr>
|
||||||
|
<th class="font-semibold text-base-content">Nome</th>
|
||||||
|
<th class="font-semibold text-base-content">Prioridade</th>
|
||||||
|
<th class="font-semibold text-base-content">Tempo de Resposta</th>
|
||||||
|
<th class="font-semibold text-base-content">Tempo de Conclusão</th>
|
||||||
|
<th class="font-semibold text-base-content">Auto-encerramento</th>
|
||||||
|
<th class="font-semibold text-base-content">Alerta Antecedência</th>
|
||||||
|
<th class="font-semibold text-base-content">Status</th>
|
||||||
|
<th class="font-semibold text-base-content text-center">Ações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each slaConfigsAtivos as sla (sla._id)}
|
||||||
|
<tr class="hover:bg-base-200/30 transition-colors">
|
||||||
|
<td>
|
||||||
|
<div class="font-medium text-base-content">{sla.nome}</div>
|
||||||
|
{#if sla.descricao}
|
||||||
|
<div class="text-xs text-base-content/60 mt-1 line-clamp-1">{sla.descricao}</div>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge badge-outline capitalize {
|
||||||
|
sla.prioridade === 'critica' ? 'badge-error' :
|
||||||
|
sla.prioridade === 'alta' ? 'badge-warning' :
|
||||||
|
sla.prioridade === 'media' ? 'badge-info' :
|
||||||
|
'badge-success'
|
||||||
|
}">
|
||||||
|
{sla.prioridade}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-semibold text-base-content">{sla.tempoRespostaHoras}h</span>
|
||||||
|
{#if sla.tempoRespostaHoras >= 24}
|
||||||
|
<span class="text-xs text-base-content/50">
|
||||||
|
({Math.floor(sla.tempoRespostaHoras / 24)}d {sla.tempoRespostaHoras % 24}h)
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-semibold text-base-content">{sla.tempoConclusaoHoras}h</span>
|
||||||
|
{#if sla.tempoConclusaoHoras >= 24}
|
||||||
|
<span class="text-xs text-base-content/50">
|
||||||
|
({Math.floor(sla.tempoConclusaoHoras / 24)}d {sla.tempoConclusaoHoras % 24}h)
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{#if sla.tempoEncerramentoHoras}
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-semibold text-base-content">{sla.tempoEncerramentoHoras}h</span>
|
||||||
|
{#if sla.tempoEncerramentoHoras >= 24}
|
||||||
|
<span class="text-xs text-base-content/50">
|
||||||
|
({Math.floor(sla.tempoEncerramentoHoras / 24)}d {sla.tempoEncerramentoHoras % 24}h)
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<span class="text-base-content/40 text-sm italic">Não configurado</span>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="font-semibold text-base-content">{sla.alertaAntecedenciaHoras}h</span>
|
||||||
|
<span class="text-xs text-base-content/50">antes</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge badge-success badge-sm gap-1">
|
||||||
|
<span class="h-2 w-2 rounded-full bg-success"></span>
|
||||||
|
Ativo
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="flex justify-center gap-1">
|
||||||
|
<button
|
||||||
|
class="btn btn-xs btn-ghost hover:btn-primary"
|
||||||
|
type="button"
|
||||||
|
onclick={() => selecionarSla(sla)}
|
||||||
|
title="Editar SLA"
|
||||||
|
>
|
||||||
|
✏️
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-xs btn-ghost hover:btn-error"
|
||||||
|
type="button"
|
||||||
|
onclick={() => (slaParaExcluir = sla._id)}
|
||||||
|
title="Excluir SLA"
|
||||||
|
>
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Seção: Configuração de SLA por Prioridade -->
|
||||||
<section class="rounded-3xl border border-base-200 bg-base-100/80 p-6 shadow-xl">
|
<section class="rounded-3xl border border-base-200 bg-base-100/80 p-6 shadow-xl">
|
||||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -602,14 +874,32 @@
|
|||||||
<button class="btn btn-sm btn-primary" type="button" onclick={novoSla}>
|
<button class="btn btn-sm btn-primary" type="button" onclick={novoSla}>
|
||||||
Novo SLA
|
Novo SLA
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-warning"
|
||||||
|
type="button"
|
||||||
|
onclick={migrarSlaConfigs}
|
||||||
|
disabled={migrandoSLAs}
|
||||||
|
>
|
||||||
|
{#if migrandoSLAs}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
Migrando...
|
||||||
|
{:else}
|
||||||
|
🔧 Migrar SLAs Antigos
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{#if migracaoFeedback}
|
||||||
|
<div class={`alert ${migracaoFeedback.includes('concluída') ? 'alert-success' : 'alert-error'} mt-2`}>
|
||||||
|
<span class="text-sm">{migracaoFeedback}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Lista de SLAs por prioridade -->
|
<!-- Cards rápidos de prioridade -->
|
||||||
<div class="mt-6 grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
<div class="mt-6 grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
{#each ["baixa", "media", "alta", "critica"] as prioridade}
|
{#each ["baixa", "media", "alta", "critica"] as prioridade}
|
||||||
{@const slaAtual = slaConfigsPorPrioridade[prioridade]}
|
{@const slaAtual = slaConfigsPorPrioridade[prioridade]}
|
||||||
<div class="rounded-2xl border border-base-200 bg-base-100/50 p-4">
|
<div class="rounded-2xl border border-base-200 bg-base-100/50 p-4 transition-all hover:shadow-md">
|
||||||
<div class="mb-2 flex items-center justify-between">
|
<div class="mb-2 flex items-center justify-between">
|
||||||
<h4 class="font-semibold capitalize text-base-content">{prioridade}</h4>
|
<h4 class="font-semibold capitalize text-base-content">{prioridade}</h4>
|
||||||
{#if slaAtual}
|
{#if slaAtual}
|
||||||
@@ -620,24 +910,28 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if slaAtual}
|
{#if slaAtual}
|
||||||
<div class="space-y-2 text-xs">
|
<div class="space-y-2 text-xs">
|
||||||
<div>
|
<div class="flex justify-between">
|
||||||
<span class="text-base-content/60">Resposta:</span>
|
<span class="text-base-content/60">Resposta:</span>
|
||||||
<span class="font-semibold">{slaAtual.tempoRespostaHoras}h</span>
|
<span class="font-semibold text-base-content">{slaAtual.tempoRespostaHoras}h</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex justify-between">
|
||||||
<span class="text-base-content/60">Conclusão:</span>
|
<span class="text-base-content/60">Conclusão:</span>
|
||||||
<span class="font-semibold">{slaAtual.tempoConclusaoHoras}h</span>
|
<span class="font-semibold text-base-content">{slaAtual.tempoConclusaoHoras}h</span>
|
||||||
</div>
|
</div>
|
||||||
{#if slaAtual.tempoEncerramentoHoras}
|
{#if slaAtual.tempoEncerramentoHoras}
|
||||||
<div>
|
<div class="flex justify-between">
|
||||||
<span class="text-base-content/60">Auto-encerramento:</span>
|
<span class="text-base-content/60">Auto-encerramento:</span>
|
||||||
<span class="font-semibold">{slaAtual.tempoEncerramentoHoras}h</span>
|
<span class="font-semibold text-base-content">{slaAtual.tempoEncerramentoHoras}h</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-base-content/60">Alerta:</span>
|
||||||
|
<span class="font-semibold text-base-content">{slaAtual.alertaAntecedenciaHoras}h antes</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 flex gap-1">
|
<div class="mt-3 flex gap-1">
|
||||||
<button
|
<button
|
||||||
class="btn btn-xs btn-ghost"
|
class="btn btn-xs btn-ghost flex-1"
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => selecionarSla(slaAtual)}
|
onclick={() => selecionarSla(slaAtual)}
|
||||||
>
|
>
|
||||||
@@ -656,8 +950,7 @@
|
|||||||
class="btn btn-xs btn-primary btn-outline mt-2 w-full"
|
class="btn btn-xs btn-primary btn-outline mt-2 w-full"
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
slaForm.prioridade = prioridade as "baixa" | "media" | "alta" | "critica";
|
novoSla(prioridade as "baixa" | "media" | "alta" | "critica");
|
||||||
novoSla();
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Configurar
|
Configurar
|
||||||
|
|||||||
@@ -328,8 +328,23 @@ export const listarChamadosTI = query({
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
filtrados.sort((a, b) => b.atualizadoEm - a.atualizadoEm);
|
// Enriquecer tickets com nome do responsável
|
||||||
return args.limite ? filtrados.slice(0, args.limite) : filtrados;
|
const ticketsEnriquecidos = await Promise.all(
|
||||||
|
filtrados.map(async (ticket) => {
|
||||||
|
let responsavelNome: string | undefined = undefined;
|
||||||
|
if (ticket.responsavelId) {
|
||||||
|
const responsavel = await ctx.db.get(ticket.responsavelId);
|
||||||
|
responsavelNome = responsavel?.nome;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...ticket,
|
||||||
|
responsavelNome,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
ticketsEnriquecidos.sort((a, b) => b.atualizadoEm - a.atualizadoEm);
|
||||||
|
return args.limite ? ticketsEnriquecidos.slice(0, args.limite) : ticketsEnriquecidos;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -703,3 +718,40 @@ export const generateUploadUrl = mutation({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migração: Adiciona o campo 'prioridade' aos SLAs antigos que não possuem
|
||||||
|
* Esta mutation corrige documentos criados antes da migração do schema
|
||||||
|
*/
|
||||||
|
export const migrarSlaConfigs = mutation({
|
||||||
|
args: {},
|
||||||
|
handler: async (ctx) => {
|
||||||
|
const usuario = await assertAuth(ctx);
|
||||||
|
|
||||||
|
// Buscar todos os SLAs
|
||||||
|
const slaConfigs = await ctx.db.query("slaConfigs").collect();
|
||||||
|
|
||||||
|
let migrados = 0;
|
||||||
|
for (const sla of slaConfigs) {
|
||||||
|
// Verificar se o documento não tem o campo 'prioridade'
|
||||||
|
// Usando type assertion para acessar campos não tipados
|
||||||
|
const slaDoc = sla as any;
|
||||||
|
if (!slaDoc.prioridade) {
|
||||||
|
// Adicionar prioridade padrão "media" para SLAs antigos
|
||||||
|
await ctx.db.patch(sla._id, {
|
||||||
|
prioridade: "media" as "baixa" | "media" | "alta" | "critica",
|
||||||
|
atualizadoPor: usuario._id,
|
||||||
|
atualizadoEm: Date.now(),
|
||||||
|
});
|
||||||
|
migrados++;
|
||||||
|
console.log(`✅ SLA migrado: ${sla.nome} (ID: ${sla._id})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sucesso: true,
|
||||||
|
migrados,
|
||||||
|
total: slaConfigs.length,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -913,11 +913,13 @@ export default defineSchema({
|
|||||||
slaConfigs: defineTable({
|
slaConfigs: defineTable({
|
||||||
nome: v.string(),
|
nome: v.string(),
|
||||||
descricao: v.optional(v.string()),
|
descricao: v.optional(v.string()),
|
||||||
prioridade: v.union(
|
prioridade: v.optional(
|
||||||
v.literal("baixa"),
|
v.union(
|
||||||
v.literal("media"),
|
v.literal("baixa"),
|
||||||
v.literal("alta"),
|
v.literal("media"),
|
||||||
v.literal("critica")
|
v.literal("alta"),
|
||||||
|
v.literal("critica")
|
||||||
|
)
|
||||||
),
|
),
|
||||||
tempoRespostaHoras: v.number(),
|
tempoRespostaHoras: v.number(),
|
||||||
tempoConclusaoHoras: v.number(),
|
tempoConclusaoHoras: v.number(),
|
||||||
|
|||||||
Reference in New Issue
Block a user