refactor: simplify ticket form and SLA configuration handling
- Removed SLA configuration selection from the TicketForm component to streamline the ticket creation process. - Updated the abrir-chamado route to eliminate unnecessary SLA loading logic and directly pass loading state to the TicketForm. - Enhanced the central-chamados route to support SLA configurations by priority, allowing for better management of SLA settings. - Introduced new mutations for SLA configuration management, including creation, updating, and deletion of SLA settings. - Improved email templates for ticket notifications, ensuring better communication with users regarding ticket status and updates.
This commit is contained in:
@@ -1,28 +1,23 @@
|
||||
<script lang="ts">
|
||||
import type { Doc, Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
import type { Doc } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
type SlaConfig = Doc<"slaConfigs">;
|
||||
|
||||
interface FormValues {
|
||||
titulo: string;
|
||||
descricao: string;
|
||||
tipo: Doc<"tickets">["tipo"];
|
||||
prioridade: Doc<"tickets">["prioridade"];
|
||||
categoria: string;
|
||||
slaConfigId?: Id<"slaConfigs">;
|
||||
canalOrigem?: string;
|
||||
anexos: File[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
slaConfigs?: Array<SlaConfig>;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{ submit: { values: FormValues } }>();
|
||||
const props = $props<Props>();
|
||||
const slaConfigs = $derived<Array<SlaConfig>>(props.slaConfigs ?? []);
|
||||
const loading = $derived(props.loading ?? false);
|
||||
|
||||
let titulo = $state("");
|
||||
@@ -30,7 +25,6 @@ const loading = $derived(props.loading ?? false);
|
||||
let tipo = $state<Doc<"tickets">["tipo"]>("chamado");
|
||||
let prioridade = $state<Doc<"tickets">["prioridade"]>("media");
|
||||
let categoria = $state("");
|
||||
let slaConfigId = $state<Id<"slaConfigs"> | "">("");
|
||||
let canalOrigem = $state("Portal SGSE");
|
||||
let anexos = $state<Array<File>>([]);
|
||||
let errors = $state<Record<string, string>>({});
|
||||
@@ -67,10 +61,6 @@ let slaConfigId = $state<Id<"slaConfigs"> | "">("");
|
||||
event.preventDefault();
|
||||
if (!validate()) return;
|
||||
|
||||
const slaSelecionada =
|
||||
(slaConfigId && slaConfigId !== "" ? (slaConfigId as Id<"slaConfigs">) : slaConfigs[0]?._id) ??
|
||||
undefined;
|
||||
|
||||
dispatch("submit", {
|
||||
values: {
|
||||
titulo: titulo.trim(),
|
||||
@@ -78,7 +68,6 @@ let slaConfigId = $state<Id<"slaConfigs"> | "">("");
|
||||
tipo,
|
||||
prioridade,
|
||||
categoria: categoria.trim(),
|
||||
slaConfigId: slaSelecionada,
|
||||
canalOrigem,
|
||||
anexos,
|
||||
},
|
||||
@@ -150,22 +139,6 @@ let slaConfigId = $state<Id<"slaConfigs"> | "">("");
|
||||
<span class="text-error mt-1 text-sm">{errors.categoria}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Configuração de SLA</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" bind:value={slaConfigId}>
|
||||
{#each slaConfigs as sla (sla._id)}
|
||||
<option value={sla._id}>
|
||||
{sla.nome} • Resp. {sla.tempoRespostaHoras}h • Conc.
|
||||
{sla.tempoConclusaoHoras}h
|
||||
</option>
|
||||
{:else}
|
||||
<option value="">Padrão (24h)</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="form-control">
|
||||
|
||||
@@ -10,12 +10,9 @@
|
||||
import { useConvexWithAuth } from "$lib/hooks/useConvexWithAuth";
|
||||
|
||||
type Ticket = Doc<"tickets">;
|
||||
type SlaConfig = Doc<"slaConfigs">;
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
let slaConfigs = $state<Array<SlaConfig>>([]);
|
||||
let carregandoSla = $state(true);
|
||||
let submitLoading = $state(false);
|
||||
let resetSignal = $state(0);
|
||||
let feedback = $state<{ tipo: "success" | "error"; mensagem: string; numero?: string } | null>(
|
||||
@@ -42,28 +39,11 @@
|
||||
},
|
||||
]);
|
||||
|
||||
onMount(() => {
|
||||
carregarSlaConfigs();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
// Garante que o cliente Convex use o token do usuário logado
|
||||
useConvexWithAuth();
|
||||
});
|
||||
|
||||
async function carregarSlaConfigs() {
|
||||
try {
|
||||
carregandoSla = true;
|
||||
const lista = await client.query(api.chamados.listarSlaConfigs, {});
|
||||
slaConfigs = lista ?? [];
|
||||
} catch (error) {
|
||||
console.error("Erro ao carregar SLA:", error);
|
||||
slaConfigs = [];
|
||||
} finally {
|
||||
carregandoSla = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadArquivo(file: File) {
|
||||
const uploadUrl = await client.mutation(api.chamados.generateUploadUrl, {});
|
||||
|
||||
@@ -104,7 +84,6 @@
|
||||
tipo: values.tipo,
|
||||
categoria: values.categoria,
|
||||
prioridade: values.prioridade,
|
||||
slaConfigId: values.slaConfigId,
|
||||
canalOrigem: values.canalOrigem,
|
||||
anexos,
|
||||
});
|
||||
@@ -192,53 +171,15 @@
|
||||
</p>
|
||||
<div class="mt-6">
|
||||
{#if resetSignal % 2 === 0}
|
||||
<TicketForm {slaConfigs} loading={submitLoading} on:submit={handleSubmit} />
|
||||
<TicketForm loading={submitLoading} on:submit={handleSubmit} />
|
||||
{:else}
|
||||
<TicketForm {slaConfigs} loading={submitLoading} on:submit={handleSubmit} />
|
||||
<TicketForm loading={submitLoading} on:submit={handleSubmit} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="space-y-6">
|
||||
<div class="rounded-3xl border border-base-200 bg-base-100/90 p-6 shadow-lg">
|
||||
<h3 class="font-semibold text-base-content">Configurações de SLA</h3>
|
||||
{#if carregandoSla}
|
||||
<div class="flex items-center justify-center py-6">
|
||||
<span class="loading loading-spinner loading-md"></span>
|
||||
</div>
|
||||
{:else if slaConfigs.length === 0}
|
||||
<p class="text-sm text-base-content/60">
|
||||
Nenhuma configuração customizada cadastrada. Os prazos padrão serão aplicados (Resposta:
|
||||
4h, Conclusão: 24h).
|
||||
</p>
|
||||
{:else}
|
||||
<div class="mt-4 space-y-4">
|
||||
{#each slaConfigs as sla (sla._id)}
|
||||
<div class="rounded-2xl border border-primary/20 bg-primary/5 p-4">
|
||||
<p class="text-sm font-semibold text-primary">{sla.nome}</p>
|
||||
<p class="text-xs text-base-content/60">{sla.descricao}</p>
|
||||
<div class="mt-3 grid grid-cols-2 gap-2 text-xs">
|
||||
<div class="rounded-xl bg-base-100/90 p-2 text-center">
|
||||
<p class="font-semibold">{sla.tempoRespostaHoras}h</p>
|
||||
<p class="text-base-content/60">Resposta</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-base-100/90 p-2 text-center">
|
||||
<p class="font-semibold">{sla.tempoConclusaoHoras}h</p>
|
||||
<p class="text-base-content/60">Conclusão</p>
|
||||
</div>
|
||||
</div>
|
||||
{#if sla.alertaAntecedenciaHoras}
|
||||
<p class="text-[11px] text-base-content/50 mt-2">
|
||||
Alerta {sla.alertaAntecedenciaHoras}h antes do prazo.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="rounded-3xl border border-base-200 bg-base-100/90 p-6 shadow-lg">
|
||||
<h3 class="font-semibold text-base-content">Como funciona a timeline</h3>
|
||||
<p class="text-sm text-base-content/60 mb-4">
|
||||
|
||||
@@ -15,10 +15,51 @@
|
||||
type Ticket = Doc<"tickets">;
|
||||
type Usuario = Doc<"usuarios">;
|
||||
type SlaConfig = Doc<"slaConfigs">;
|
||||
type Template = Doc<"templatesMensagens">;
|
||||
|
||||
const client = useConvexClient();
|
||||
const usuariosQuery = useQuery(api.usuarios.listar, {});
|
||||
const slaConfigsQuery = useQuery(api.chamados.listarSlaConfigs, {});
|
||||
const templatesQuery = useQuery(api.templatesMensagens.listarTemplates, {});
|
||||
|
||||
// Extrair dados dos templates
|
||||
const templates = $derived.by(() => {
|
||||
if (!templatesQuery || templatesQuery === undefined || templatesQuery === null) {
|
||||
return [];
|
||||
}
|
||||
// Se tem propriedade data, usar os dados
|
||||
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)) {
|
||||
return templatesQuery;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const carregandoTemplates = $derived.by(() => {
|
||||
if (!templatesQuery || templatesQuery === undefined || templatesQuery === null) {
|
||||
return true;
|
||||
}
|
||||
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(() => {
|
||||
return templates.filter((t: Template) => {
|
||||
if (!t.codigo) return false;
|
||||
return typeof t.codigo === 'string' && t.codigo.startsWith("chamado_");
|
||||
});
|
||||
});
|
||||
|
||||
let carregandoChamados = $state(true);
|
||||
let tickets = $state<Array<Ticket>>([]);
|
||||
@@ -34,6 +75,7 @@
|
||||
slaId?: Id<"slaConfigs">;
|
||||
nome: string;
|
||||
descricao: string;
|
||||
prioridade: "baixa" | "media" | "alta" | "critica";
|
||||
tempoRespostaHoras: number;
|
||||
tempoConclusaoHoras: number;
|
||||
tempoEncerramentoHoras?: number | null;
|
||||
@@ -42,6 +84,7 @@
|
||||
}>({
|
||||
nome: "",
|
||||
descricao: "",
|
||||
prioridade: "media",
|
||||
tempoRespostaHoras: 4,
|
||||
tempoConclusaoHoras: 24,
|
||||
tempoEncerramentoHoras: 72,
|
||||
@@ -49,6 +92,21 @@
|
||||
ativo: true,
|
||||
});
|
||||
let slaFeedback = $state<string | null>(null);
|
||||
let slaParaExcluir = $state<Id<"slaConfigs"> | null>(null);
|
||||
let prorrogacaoForm = $state<{
|
||||
ticketId: Id<"tickets"> | "";
|
||||
horasAdicionais: number;
|
||||
prazo: "resposta" | "conclusao";
|
||||
motivo: string;
|
||||
}>({
|
||||
ticketId: "",
|
||||
horasAdicionais: 24,
|
||||
prazo: "conclusao",
|
||||
motivo: "",
|
||||
});
|
||||
let prorrogacaoFeedback = $state<string | null>(null);
|
||||
let criandoTemplates = $state(false);
|
||||
let templatesFeedback = $state<string | null>(null);
|
||||
|
||||
let carregamentoToken = 0;
|
||||
$effect(() => {
|
||||
@@ -63,9 +121,6 @@
|
||||
onMount(() => {
|
||||
// Configura token no cliente Convex
|
||||
useConvexWithAuth();
|
||||
if (slaConfigsQuery?.data && slaConfigsQuery.data.length > 0) {
|
||||
selecionarSla(slaConfigsQuery.data[0]);
|
||||
}
|
||||
});
|
||||
|
||||
async function carregarChamados(filtros: {
|
||||
@@ -98,9 +153,10 @@
|
||||
detalheSelecionado = tickets.find((t) => t._id === ticketId) ?? null;
|
||||
}
|
||||
|
||||
const usuariosTI = $derived(
|
||||
(usuariosQuery?.data || []).filter((usuario: Usuario) => usuario.setor === "TI")
|
||||
);
|
||||
const usuariosTI = $derived(() => {
|
||||
if (!usuariosQuery?.data) return [];
|
||||
return usuariosQuery.data.filter((usuario: Usuario) => usuario.setor === "TI");
|
||||
});
|
||||
|
||||
const estatisticas = $derived(() => {
|
||||
const total = tickets.length;
|
||||
@@ -117,6 +173,7 @@
|
||||
slaId: sla._id,
|
||||
nome: sla.nome,
|
||||
descricao: sla.descricao ?? "",
|
||||
prioridade: sla.prioridade,
|
||||
tempoRespostaHoras: sla.tempoRespostaHoras,
|
||||
tempoConclusaoHoras: sla.tempoConclusaoHoras,
|
||||
tempoEncerramentoHoras: sla.tempoEncerramentoHoras ?? undefined,
|
||||
@@ -125,23 +182,86 @@
|
||||
};
|
||||
}
|
||||
|
||||
function novoSla() {
|
||||
slaForm = {
|
||||
nome: "",
|
||||
descricao: "",
|
||||
prioridade: "media",
|
||||
tempoRespostaHoras: 4,
|
||||
tempoConclusaoHoras: 24,
|
||||
tempoEncerramentoHoras: 72,
|
||||
alertaAntecedenciaHoras: 2,
|
||||
ativo: true,
|
||||
};
|
||||
slaFeedback = null;
|
||||
}
|
||||
|
||||
async function salvarSlaConfig() {
|
||||
try {
|
||||
slaFeedback = null;
|
||||
if (!slaForm.nome.trim()) {
|
||||
slaFeedback = "Nome é obrigatório";
|
||||
return;
|
||||
}
|
||||
await client.mutation(api.chamados.salvarSlaConfig, {
|
||||
...slaForm,
|
||||
tempoEncerramentoHoras: slaForm.tempoEncerramentoHoras,
|
||||
slaId: slaForm.slaId,
|
||||
nome: slaForm.nome,
|
||||
descricao: slaForm.descricao || undefined,
|
||||
prioridade: slaForm.prioridade,
|
||||
tempoRespostaHoras: slaForm.tempoRespostaHoras,
|
||||
tempoConclusaoHoras: slaForm.tempoConclusaoHoras,
|
||||
tempoEncerramentoHoras: slaForm.tempoEncerramentoHoras || undefined,
|
||||
alertaAntecedenciaHoras: slaForm.alertaAntecedenciaHoras,
|
||||
ativo: slaForm.ativo,
|
||||
});
|
||||
slaFeedback = "Configuração salva com sucesso.";
|
||||
slaFeedback = "Configuração salva com sucesso";
|
||||
// Recarregar SLAs após salvar
|
||||
if (slaConfigsQuery?.data) {
|
||||
const slaAtualizado = await client.query(api.chamados.listarSlaConfigs, {});
|
||||
if (slaAtualizado && slaAtualizado.length > 0) {
|
||||
const slaEncontrado = slaAtualizado.find((s: SlaConfig) =>
|
||||
s.prioridade === slaForm.prioridade && s.ativo === slaForm.ativo
|
||||
);
|
||||
if (slaEncontrado) {
|
||||
selecionarSla(slaEncontrado);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
slaFeedback =
|
||||
error instanceof Error ? error.message : "Erro ao salvar configuração de SLA.";
|
||||
error instanceof Error ? error.message : "Erro ao salvar configuração de SLA";
|
||||
}
|
||||
}
|
||||
|
||||
async function excluirSlaConfig() {
|
||||
if (!slaParaExcluir) return;
|
||||
try {
|
||||
slaFeedback = null;
|
||||
await client.mutation(api.chamados.excluirSlaConfig, {
|
||||
slaId: slaParaExcluir,
|
||||
});
|
||||
slaFeedback = "Configuração excluída com sucesso";
|
||||
slaParaExcluir = null;
|
||||
novoSla();
|
||||
} catch (error) {
|
||||
slaFeedback =
|
||||
error instanceof Error ? error.message : "Erro ao excluir configuração de SLA";
|
||||
}
|
||||
}
|
||||
|
||||
const slaConfigsPorPrioridade = $derived(() => {
|
||||
const slaConfigs = slaConfigsQuery?.data || [];
|
||||
return {
|
||||
baixa: slaConfigs.find((s: SlaConfig) => s.prioridade === "baixa" && s.ativo),
|
||||
media: slaConfigs.find((s: SlaConfig) => s.prioridade === "media" && s.ativo),
|
||||
alta: slaConfigs.find((s: SlaConfig) => s.prioridade === "alta" && s.ativo),
|
||||
critica: slaConfigs.find((s: SlaConfig) => s.prioridade === "critica" && s.ativo),
|
||||
};
|
||||
});
|
||||
|
||||
async function atribuirResponsavel() {
|
||||
if (!ticketSelecionado || !assignResponsavel) {
|
||||
assignFeedback = "Escolha um ticket e um responsável.";
|
||||
assignFeedback = "Escolha um ticket e um responsável";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -151,8 +271,9 @@
|
||||
responsavelId: assignResponsavel as Id<"usuarios">,
|
||||
motivo: assignMotivo || undefined,
|
||||
});
|
||||
assignFeedback = "Responsável atribuído.";
|
||||
assignFeedback = "Responsável atribuído com sucesso";
|
||||
assignMotivo = "";
|
||||
assignResponsavel = "";
|
||||
await carregarChamados({
|
||||
status: filtroStatus === "todos" ? undefined : filtroStatus,
|
||||
responsavelId: filtroResponsavel === "todos" ? undefined : filtroResponsavel,
|
||||
@@ -162,9 +283,74 @@
|
||||
selecionarTicket(ticketSelecionado);
|
||||
}
|
||||
} catch (error) {
|
||||
assignFeedback = error instanceof Error ? error.message : "Erro ao atribuir responsável.";
|
||||
assignFeedback = error instanceof Error ? error.message : "Erro ao atribuir responsável";
|
||||
}
|
||||
}
|
||||
|
||||
async function prorrogarChamado() {
|
||||
if (!prorrogacaoForm.ticketId || !prorrogacaoForm.motivo.trim()) {
|
||||
prorrogacaoFeedback = "Selecione um ticket e preencha o motivo da prorrogação";
|
||||
return;
|
||||
}
|
||||
if (prorrogacaoForm.horasAdicionais <= 0) {
|
||||
prorrogacaoFeedback = "O número de horas deve ser maior que zero";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
prorrogacaoFeedback = null;
|
||||
await client.mutation(api.chamados.prorrogarChamado, {
|
||||
ticketId: prorrogacaoForm.ticketId as Id<"tickets">,
|
||||
horasAdicionais: prorrogacaoForm.horasAdicionais,
|
||||
prazo: prorrogacaoForm.prazo,
|
||||
motivo: prorrogacaoForm.motivo.trim(),
|
||||
});
|
||||
const ticketIdProrrogado = prorrogacaoForm.ticketId;
|
||||
prorrogacaoFeedback = "Prazo prorrogado com sucesso";
|
||||
prorrogacaoForm = {
|
||||
ticketId: "",
|
||||
horasAdicionais: 24,
|
||||
prazo: "conclusao",
|
||||
motivo: "",
|
||||
};
|
||||
await carregarChamados({
|
||||
status: filtroStatus === "todos" ? undefined : filtroStatus,
|
||||
responsavelId: filtroResponsavel === "todos" ? undefined : filtroResponsavel,
|
||||
setor: filtroSetor === "todos" ? undefined : filtroSetor,
|
||||
});
|
||||
if (ticketIdProrrogado) {
|
||||
selecionarTicket(ticketIdProrrogado as Id<"tickets">);
|
||||
}
|
||||
} catch (error) {
|
||||
prorrogacaoFeedback = error instanceof Error ? error.message : "Erro ao prorrogar prazo";
|
||||
}
|
||||
}
|
||||
|
||||
async function criarTemplatesPadrao() {
|
||||
try {
|
||||
criandoTemplates = true;
|
||||
templatesFeedback = null;
|
||||
const resultado = await client.mutation(api.templatesMensagens.criarTemplatesPadrao, {});
|
||||
templatesFeedback = resultado?.sucesso ? "Templates padrão criados com sucesso" : "Templates criados";
|
||||
// Aguardar um pouco para os templates aparecerem e forçar reload da query
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
// Forçar reload dos templates
|
||||
if (templatesQuery) {
|
||||
// A query será atualizada automaticamente pelo useQuery
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erro ao criar templates:", error);
|
||||
templatesFeedback = error instanceof Error ? error.message : "Erro ao criar templates padrão";
|
||||
} finally {
|
||||
criandoTemplates = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: ver templates carregados
|
||||
$effect(() => {
|
||||
console.log("templatesQuery:", templatesQuery);
|
||||
console.log("Templates extraídos:", templates);
|
||||
console.log("Templates de chamados:", templatesChamados);
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="mx-auto w-full max-w-7xl space-y-8 px-4 py-8">
|
||||
@@ -313,21 +499,94 @@
|
||||
<div class="mt-4 space-y-3">
|
||||
<select class="select select-bordered w-full" bind:value={assignResponsavel}>
|
||||
<option value="">Selecione o responsável</option>
|
||||
{#each usuariosTI as usuario (usuario._id)}
|
||||
<option value={usuario._id}>{usuario.nome}</option>
|
||||
{/each}
|
||||
{#if usuariosTI.length === 0}
|
||||
<option disabled>Carregando usuários...</option>
|
||||
{:else}
|
||||
{#each usuariosTI as usuario (usuario._id)}
|
||||
<option value={usuario._id}>{usuario.nome}</option>
|
||||
{/each}
|
||||
{/if}
|
||||
</select>
|
||||
<textarea
|
||||
class="textarea textarea-bordered w-full"
|
||||
rows="3"
|
||||
placeholder="Motivo/observação"
|
||||
placeholder="Motivo/observação (opcional)"
|
||||
bind:value={assignMotivo}
|
||||
></textarea>
|
||||
{#if assignFeedback}
|
||||
<p class="text-sm text-base-content/70">{assignFeedback}</p>
|
||||
<div class={`alert ${assignFeedback.includes('sucesso') ? 'alert-success' : 'alert-error'} text-sm`}>
|
||||
{assignFeedback}
|
||||
</div>
|
||||
{/if}
|
||||
<button class="btn btn-primary w-full" type="button" onclick={atribuirResponsavel}>
|
||||
Salvar
|
||||
Atribuir responsável
|
||||
</button>
|
||||
</div>
|
||||
</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">Prorrogar prazo</h3>
|
||||
<p class="text-xs text-base-content/60 mt-1">Recurso exclusivo para a equipe de TI</p>
|
||||
<div class="mt-4 space-y-3">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Ticket *</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" bind:value={prorrogacaoForm.ticketId}>
|
||||
<option value="">Selecione o ticket</option>
|
||||
{#if tickets.length === 0}
|
||||
<option disabled>Carregando tickets...</option>
|
||||
{:else}
|
||||
{#each tickets as ticket (ticket._id)}
|
||||
<option value={ticket._id}>{ticket.numero} - {ticket.titulo}</option>
|
||||
{/each}
|
||||
{/if}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Prazo a prorrogar *</span>
|
||||
</label>
|
||||
<select class="select select-bordered w-full" bind:value={prorrogacaoForm.prazo}>
|
||||
<option value="resposta">Prazo de resposta</option>
|
||||
<option value="conclusao">Prazo de conclusão</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Horas adicionais</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
class="input input-bordered w-full"
|
||||
bind:value={prorrogacaoForm.horasAdicionais}
|
||||
placeholder="Ex: 24"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-semibold">Motivo *</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="textarea textarea-bordered w-full"
|
||||
rows="3"
|
||||
placeholder="Descreva o motivo da prorrogação..."
|
||||
bind:value={prorrogacaoForm.motivo}
|
||||
></textarea>
|
||||
</div>
|
||||
{#if prorrogacaoFeedback}
|
||||
<div class={`alert ${prorrogacaoFeedback.includes('sucesso') ? 'alert-success' : 'alert-error'} text-sm`}>
|
||||
{prorrogacaoFeedback}
|
||||
</div>
|
||||
{/if}
|
||||
<button
|
||||
class="btn btn-warning w-full"
|
||||
type="button"
|
||||
onclick={prorrogarChamado}
|
||||
disabled={!prorrogacaoForm.ticketId}
|
||||
>
|
||||
Prorrogar prazo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -336,56 +595,242 @@
|
||||
<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>
|
||||
<h3 class="text-lg font-semibold text-base-content">Configuração de SLA</h3>
|
||||
<p class="text-sm text-base-content/60">Defina tempos de resposta, conclusão e alertas.</p>
|
||||
<h3 class="text-lg font-semibold text-base-content">Configuração de SLA por Prioridade</h3>
|
||||
<p class="text-sm text-base-content/60">Configure SLAs separados para cada nível de prioridade</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
{#if slaConfigsQuery?.data}
|
||||
{#each slaConfigsQuery.data as sla (sla._id)}
|
||||
<button class="btn btn-sm" type="button" onclick={() => selecionarSla(sla)}>
|
||||
{sla.nome}
|
||||
<button class="btn btn-sm btn-primary" type="button" onclick={novoSla}>
|
||||
Novo SLA
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lista de SLAs por prioridade -->
|
||||
<div class="mt-6 grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{#each ["baixa", "media", "alta", "critica"] as prioridade}
|
||||
{@const slaAtual = slaConfigsPorPrioridade[prioridade]}
|
||||
<div class="rounded-2xl border border-base-200 bg-base-100/50 p-4">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<h4 class="font-semibold capitalize text-base-content">{prioridade}</h4>
|
||||
{#if slaAtual}
|
||||
<span class="badge badge-success badge-sm">Configurado</span>
|
||||
{:else}
|
||||
<span class="badge badge-warning badge-sm">Não configurado</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if slaAtual}
|
||||
<div class="space-y-2 text-xs">
|
||||
<div>
|
||||
<span class="text-base-content/60">Resposta:</span>
|
||||
<span class="font-semibold">{slaAtual.tempoRespostaHoras}h</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-base-content/60">Conclusão:</span>
|
||||
<span class="font-semibold">{slaAtual.tempoConclusaoHoras}h</span>
|
||||
</div>
|
||||
{#if slaAtual.tempoEncerramentoHoras}
|
||||
<div>
|
||||
<span class="text-base-content/60">Auto-encerramento:</span>
|
||||
<span class="font-semibold">{slaAtual.tempoEncerramentoHoras}h</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-3 flex gap-1">
|
||||
<button
|
||||
class="btn btn-xs btn-ghost"
|
||||
type="button"
|
||||
onclick={() => selecionarSla(slaAtual)}
|
||||
>
|
||||
Editar
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-xs btn-error btn-ghost"
|
||||
type="button"
|
||||
onclick={() => (slaParaExcluir = slaAtual._id)}
|
||||
>
|
||||
Excluir
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class="btn btn-xs btn-primary btn-outline mt-2 w-full"
|
||||
type="button"
|
||||
onclick={() => {
|
||||
slaForm.prioridade = prioridade as "baixa" | "media" | "alta" | "critica";
|
||||
novoSla();
|
||||
}}
|
||||
>
|
||||
Configurar
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Formulário de SLA -->
|
||||
<div class="mt-6 rounded-2xl border border-base-300 bg-base-100/90 p-4">
|
||||
<h4 class="mb-4 font-semibold text-base-content">
|
||||
{slaForm.slaId ? "Editar" : "Novo"} SLA - Prioridade {slaForm.prioridade.charAt(0).toUpperCase() + slaForm.prioridade.slice(1)}
|
||||
</h4>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-semibold">Nome *</span></label>
|
||||
<input class="input input-bordered w-full" bind:value={slaForm.nome} placeholder="Ex: SLA Média" />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-semibold">Prioridade *</span></label>
|
||||
<select class="select select-bordered w-full" bind:value={slaForm.prioridade}>
|
||||
<option value="baixa">Baixa</option>
|
||||
<option value="media">Média</option>
|
||||
<option value="alta">Alta</option>
|
||||
<option value="critica">Crítica</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label"><span class="label-text font-semibold">Descrição</span></label>
|
||||
<input class="input input-bordered w-full" bind:value={slaForm.descricao} placeholder="Descrição opcional" />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-semibold">Tempo de resposta (h) *</span></label>
|
||||
<input type="number" min="1" class="input input-bordered w-full" bind:value={slaForm.tempoRespostaHoras} />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-semibold">Tempo de conclusão (h) *</span></label>
|
||||
<input type="number" min="1" class="input input-bordered w-full" bind:value={slaForm.tempoConclusaoHoras} />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-semibold">Auto-encerramento (h)</span></label>
|
||||
<input type="number" min="1" class="input input-bordered w-full" bind:value={slaForm.tempoEncerramentoHoras} placeholder="Opcional" />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-semibold">Alerta antes do vencimento (h) *</span></label>
|
||||
<input type="number" min="1" class="input input-bordered w-full" bind:value={slaForm.alertaAntecedenciaHoras} />
|
||||
</div>
|
||||
<div class="form-control md:col-span-2">
|
||||
<label class="label cursor-pointer gap-3">
|
||||
<span class="label-text font-semibold">Ativo</span>
|
||||
<input type="checkbox" class="toggle toggle-primary" bind:checked={slaForm.ativo} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{#if slaFeedback}
|
||||
<div class="mt-3">
|
||||
<p class="text-sm {slaFeedback.includes('sucesso') ? 'text-success' : 'text-error'}">{slaFeedback}</p>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-4 flex gap-2">
|
||||
<button class="btn btn-primary" type="button" onclick={salvarSlaConfig}>
|
||||
{slaForm.slaId ? "Atualizar" : "Criar"} SLA
|
||||
</button>
|
||||
{#if slaForm.slaId}
|
||||
<button class="btn btn-ghost" type="button" onclick={novoSla}>
|
||||
Cancelar
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="mt-6 grid gap-4 md:grid-cols-2">
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-semibold">Nome</span></label>
|
||||
<input class="input input-bordered w-full" bind:value={slaForm.nome} />
|
||||
<!-- Modal de confirmação de exclusão -->
|
||||
{#if slaParaExcluir}
|
||||
<div class="modal modal-open">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">Confirmar exclusão</h3>
|
||||
<p class="py-4">Tem certeza que deseja excluir esta configuração de SLA? Esta ação não pode ser desfeita.</p>
|
||||
<div class="modal-action">
|
||||
<button class="btn btn-ghost" type="button" onclick={() => (slaParaExcluir = null)}>
|
||||
Cancelar
|
||||
</button>
|
||||
<button class="btn btn-error" type="button" onclick={excluirSlaConfig}>
|
||||
Excluir
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-semibold">Descrição</span></label>
|
||||
<input class="input input-bordered w-full" bind:value={slaForm.descricao} />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-semibold">Tempo de resposta (h)</span></label>
|
||||
<input type="number" min="1" class="input input-bordered w-full" bind:value={slaForm.tempoRespostaHoras} />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-semibold">Tempo de conclusão (h)</span></label>
|
||||
<input type="number" min="1" class="input input-bordered w-full" bind:value={slaForm.tempoConclusaoHoras} />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-semibold">Auto-encerramento (h)</span></label>
|
||||
<input type="number" min="1" class="input input-bordered w-full" bind:value={slaForm.tempoEncerramentoHoras} />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-semibold">Alerta antes do vencimento (h)</span></label>
|
||||
<input type="number" min="1" class="input input-bordered w-full" bind:value={slaForm.alertaAntecedenciaHoras} />
|
||||
</div>
|
||||
<label class="label cursor-pointer gap-3">
|
||||
<span class="label-text font-semibold">Ativo</span>
|
||||
<input type="checkbox" class="toggle toggle-primary" bind:checked={slaForm.ativo} />
|
||||
</label>
|
||||
</div>
|
||||
{#if slaFeedback}
|
||||
<p class="text-sm text-base-content/70 mt-3">{slaFeedback}</p>
|
||||
{/if}
|
||||
<button class="btn btn-primary mt-4" type="button" onclick={salvarSlaConfig}>
|
||||
Salvar configuração
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- Templates de Email -->
|
||||
<section class="rounded-3xl border border-base-200 bg-base-100/80 p-6 shadow-xl">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-base-content">Templates de Email - Chamados</h3>
|
||||
<p class="text-sm text-base-content/60">Templates automáticos usados nas notificações de chamados.</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{#if carregandoTemplates}
|
||||
<div class="col-span-full text-center text-sm text-base-content/60">
|
||||
<span class="loading loading-spinner loading-md"></span>
|
||||
<p class="mt-2">Carregando templates...</p>
|
||||
</div>
|
||||
{:else if templatesChamados.length === 0}
|
||||
<div class="col-span-full rounded-2xl border border-base-300 bg-base-200/50 p-6 text-center">
|
||||
<p class="text-sm font-semibold text-base-content/70">Nenhum template encontrado</p>
|
||||
<p class="mt-2 text-xs text-base-content/50 mb-4">
|
||||
Os templates de chamados serão criados automaticamente quando o sistema for inicializado.
|
||||
Clique no botão abaixo para criar os templates padrão agora.
|
||||
</p>
|
||||
{#if templatesFeedback}
|
||||
<div class={`alert ${templatesFeedback.includes('sucesso') ? 'alert-success' : 'alert-error'} mb-4`}>
|
||||
<span class="text-sm">{templatesFeedback}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="button"
|
||||
onclick={criarTemplatesPadrao}
|
||||
disabled={criandoTemplates}
|
||||
>
|
||||
{#if criandoTemplates}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Criando templates...
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
Criar templates padrão
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
{#each templatesChamados as template (template._id)}
|
||||
<div class="rounded-2xl border border-base-200 bg-base-100/50 p-4">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<h4 class="font-semibold text-base-content">{template.nome}</h4>
|
||||
<span class="badge badge-primary badge-sm">Sistema</span>
|
||||
</div>
|
||||
<p class="mb-3 text-xs text-base-content/60">{template.titulo}</p>
|
||||
{#if template.variaveis && template.variaveis.length > 0}
|
||||
<div class="mb-2">
|
||||
<p class="text-xs font-semibold text-base-content/60">Variáveis:</p>
|
||||
<div class="mt-1 flex flex-wrap gap-1">
|
||||
{#each template.variaveis as variavel}
|
||||
<span class="badge badge-outline badge-xs">{{variavel}}</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-3 text-xs text-base-content/50">
|
||||
<p>Código: <code class="bg-base-200 px-1 py-0.5 rounded">{template.codigo || "N/A"}</code></p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="col-span-full text-center text-sm text-base-content/60">
|
||||
<p>Carregando templates...</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user