Merge branch 'feat-central-chamados' into feat-cibersecurity

This commit is contained in:
2025-11-17 10:19:10 -03:00
11 changed files with 1574 additions and 188 deletions

View File

@@ -0,0 +1,183 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { Chart, registerables } from 'chart.js';
Chart.register(...registerables);
type Props = {
dadosSla: {
statusSla: {
dentroPrazo: number;
proximoVencimento: number;
vencido: number;
semPrazo: number;
};
porPrioridade: {
baixa: { dentroPrazo: number; proximoVencimento: number; vencido: number; total: number };
media: { dentroPrazo: number; proximoVencimento: number; vencido: number; total: number };
alta: { dentroPrazo: number; proximoVencimento: number; vencido: number; total: number };
critica: { dentroPrazo: number; proximoVencimento: number; vencido: number; total: number };
};
taxaCumprimento: number;
totalComPrazo: number;
atualizadoEm: number;
};
height?: number;
};
let { dadosSla, height = 400 }: Props = $props();
let canvas: HTMLCanvasElement;
let chart: Chart | null = null;
function prepararDados() {
const prioridades = ['Baixa', 'Média', 'Alta', 'Crítica'];
const cores = {
dentroPrazo: 'rgba(34, 197, 94, 0.8)', // verde
proximoVencimento: 'rgba(251, 191, 36, 0.8)', // amarelo
vencido: 'rgba(239, 68, 68, 0.8)', // vermelho
};
return {
labels: prioridades,
datasets: [
{
label: 'Dentro do Prazo',
data: [
dadosSla.porPrioridade.baixa.dentroPrazo,
dadosSla.porPrioridade.media.dentroPrazo,
dadosSla.porPrioridade.alta.dentroPrazo,
dadosSla.porPrioridade.critica.dentroPrazo,
],
backgroundColor: cores.dentroPrazo,
borderColor: 'rgba(34, 197, 94, 1)',
borderWidth: 2,
},
{
label: 'Próximo ao Vencimento',
data: [
dadosSla.porPrioridade.baixa.proximoVencimento,
dadosSla.porPrioridade.media.proximoVencimento,
dadosSla.porPrioridade.alta.proximoVencimento,
dadosSla.porPrioridade.critica.proximoVencimento,
],
backgroundColor: cores.proximoVencimento,
borderColor: 'rgba(251, 191, 36, 1)',
borderWidth: 2,
},
{
label: 'Vencido',
data: [
dadosSla.porPrioridade.baixa.vencido,
dadosSla.porPrioridade.media.vencido,
dadosSla.porPrioridade.alta.vencido,
dadosSla.porPrioridade.critica.vencido,
],
backgroundColor: cores.vencido,
borderColor: 'rgba(239, 68, 68, 1)',
borderWidth: 2,
},
],
};
}
onMount(() => {
if (canvas) {
const ctx = canvas.getContext('2d');
if (ctx) {
const chartData = prepararDados();
chart = new Chart(ctx, {
type: 'bar',
data: chartData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top',
labels: {
color: '#a6adbb',
font: {
size: 12,
family: "'Inter', sans-serif",
},
usePointStyle: true,
padding: 15,
}
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.9)',
titleColor: '#fff',
bodyColor: '#fff',
borderColor: '#570df8',
borderWidth: 1,
padding: 12,
callbacks: {
label: function(context) {
const label = context.dataset.label || '';
const value = context.parsed.y;
const prioridade = context.label;
return `${label}: ${value} chamado(s)`;
}
}
}
},
scales: {
x: {
stacked: true,
grid: {
color: 'rgba(255, 255, 255, 0.05)',
},
ticks: {
color: '#a6adbb',
font: {
size: 11,
weight: '500',
}
}
},
y: {
stacked: true,
beginAtZero: true,
grid: {
color: 'rgba(255, 255, 255, 0.05)',
},
ticks: {
color: '#a6adbb',
font: {
size: 11,
},
stepSize: 1,
}
}
},
animation: {
duration: 800,
easing: 'easeInOutQuart'
}
}
});
}
}
});
$effect(() => {
if (chart && dadosSla) {
const chartData = prepararDados();
chart.data = chartData;
chart.update('active');
}
});
onDestroy(() => {
if (chart) {
chart.destroy();
}
});
</script>
<div style="height: {height}px; position: relative;">
<canvas bind:this={canvas}></canvas>
</div>

View File

@@ -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">