refactor: improve layout and backend monitoring functionality - Streamlined the layout component in Svelte for better readability and consistency. - Enhanced the backend monitoring functions by updating argument structures and improving code clarity. - A #10
@@ -1,503 +1,509 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useConvexClient } from "convex-svelte";
|
import { useConvexClient } from 'convex-svelte';
|
||||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import { format, subDays, startOfDay, endOfDay } from "date-fns";
|
import { format, subDays, startOfDay, endOfDay } from 'date-fns';
|
||||||
import { ptBR } from "date-fns/locale";
|
import { ptBR } from 'date-fns/locale';
|
||||||
import jsPDF from "jspdf";
|
import jsPDF from 'jspdf';
|
||||||
import autoTable from "jspdf-autotable";
|
import autoTable from 'jspdf-autotable';
|
||||||
import Papa from "papaparse";
|
import Papa from 'papaparse';
|
||||||
|
|
||||||
let { onClose }: { onClose: () => void } = $props();
|
let { onClose }: { onClose: () => void } = $props();
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
|
|
||||||
// Estados
|
// Estados
|
||||||
let periodType = $state("custom");
|
let periodType = $state('custom');
|
||||||
let dataInicio = $state(format(subDays(new Date(), 7), "yyyy-MM-dd"));
|
let dataInicio = $state(format(subDays(new Date(), 7), 'yyyy-MM-dd'));
|
||||||
let dataFim = $state(format(new Date(), "yyyy-MM-dd"));
|
let dataFim = $state(format(new Date(), 'yyyy-MM-dd'));
|
||||||
let horaInicio = $state("00:00");
|
let horaInicio = $state('00:00');
|
||||||
let horaFim = $state("23:59");
|
let horaFim = $state('23:59');
|
||||||
let generating = $state(false);
|
let generating = $state(false);
|
||||||
|
|
||||||
// Métricas selecionadas
|
// Métricas selecionadas
|
||||||
let selectedMetrics = $state({
|
const metricLabels = {
|
||||||
cpuUsage: true,
|
cpuUsage: 'Uso de CPU (%)',
|
||||||
memoryUsage: true,
|
memoryUsage: 'Uso de Memória (%)',
|
||||||
networkLatency: true,
|
networkLatency: 'Latência de Rede (ms)',
|
||||||
storageUsed: true,
|
storageUsed: 'Armazenamento (%)',
|
||||||
usuariosOnline: true,
|
usuariosOnline: 'Usuários Online',
|
||||||
mensagensPorMinuto: true,
|
mensagensPorMinuto: 'Mensagens/min',
|
||||||
tempoRespostaMedio: true,
|
tempoRespostaMedio: 'Tempo Resposta (ms)',
|
||||||
errosCount: true,
|
errosCount: 'Erros'
|
||||||
});
|
} as const;
|
||||||
|
type MetricKey = keyof typeof metricLabels;
|
||||||
|
|
||||||
const metricLabels: Record<string, string> = {
|
let selectedMetrics = $state<Record<MetricKey, boolean>>({
|
||||||
cpuUsage: "Uso de CPU (%)",
|
cpuUsage: true,
|
||||||
memoryUsage: "Uso de Memória (%)",
|
memoryUsage: true,
|
||||||
networkLatency: "Latência de Rede (ms)",
|
networkLatency: true,
|
||||||
storageUsed: "Armazenamento (%)",
|
storageUsed: true,
|
||||||
usuariosOnline: "Usuários Online",
|
usuariosOnline: true,
|
||||||
mensagensPorMinuto: "Mensagens/min",
|
mensagensPorMinuto: true,
|
||||||
tempoRespostaMedio: "Tempo Resposta (ms)",
|
tempoRespostaMedio: true,
|
||||||
errosCount: "Erros",
|
errosCount: true
|
||||||
};
|
});
|
||||||
|
|
||||||
function setPeriod(type: string) {
|
const metricEntries = $derived(Object.entries(metricLabels) as Array<[MetricKey, string]>);
|
||||||
periodType = type;
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
switch (type) {
|
function isMetricSelected(key: MetricKey): boolean {
|
||||||
case "today":
|
return selectedMetrics[key];
|
||||||
dataInicio = format(now, "yyyy-MM-dd");
|
}
|
||||||
dataFim = format(now, "yyyy-MM-dd");
|
function setMetricSelected(key: MetricKey, value: boolean): void {
|
||||||
break;
|
selectedMetrics[key] = value;
|
||||||
case "week":
|
}
|
||||||
dataInicio = format(subDays(now, 7), "yyyy-MM-dd");
|
|
||||||
dataFim = format(now, "yyyy-MM-dd");
|
|
||||||
break;
|
|
||||||
case "month":
|
|
||||||
dataInicio = format(subDays(now, 30), "yyyy-MM-dd");
|
|
||||||
dataFim = format(now, "yyyy-MM-dd");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDateRange(): { inicio: number; fim: number } {
|
function setPeriod(type: 'today' | 'week' | 'month' | 'custom') {
|
||||||
const inicio = startOfDay(
|
periodType = type;
|
||||||
new Date(`${dataInicio}T${horaInicio}`),
|
const now = new Date();
|
||||||
).getTime();
|
|
||||||
const fim = endOfDay(new Date(`${dataFim}T${horaFim}`)).getTime();
|
|
||||||
return { inicio, fim };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generatePDF() {
|
switch (type) {
|
||||||
generating = true;
|
case 'today':
|
||||||
|
dataInicio = format(now, 'yyyy-MM-dd');
|
||||||
|
dataFim = format(now, 'yyyy-MM-dd');
|
||||||
|
break;
|
||||||
|
case 'week':
|
||||||
|
dataInicio = format(subDays(now, 7), 'yyyy-MM-dd');
|
||||||
|
dataFim = format(now, 'yyyy-MM-dd');
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
dataInicio = format(subDays(now, 30), 'yyyy-MM-dd');
|
||||||
|
dataFim = format(now, 'yyyy-MM-dd');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
function getDateRange(): { inicio: number; fim: number } {
|
||||||
const { inicio, fim } = getDateRange();
|
const inicio = startOfDay(new Date(`${dataInicio}T${horaInicio}`)).getTime();
|
||||||
const relatorio = await client.query(api.monitoramento.gerarRelatorio, {
|
const fim = endOfDay(new Date(`${dataFim}T${horaFim}`)).getTime();
|
||||||
dataInicio: inicio,
|
return { inicio, fim };
|
||||||
dataFim: fim,
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const doc = new jsPDF();
|
async function generatePDF() {
|
||||||
|
generating = true;
|
||||||
|
|
||||||
// Título
|
try {
|
||||||
doc.setFontSize(20);
|
const { inicio, fim } = getDateRange();
|
||||||
doc.setTextColor(102, 126, 234); // Primary color
|
const relatorio = await client.query(api.monitoramento.gerarRelatorio, {
|
||||||
doc.text("Relatório de Monitoramento do Sistema", 14, 20);
|
dataInicio: inicio,
|
||||||
|
dataFim: fim
|
||||||
|
});
|
||||||
|
|
||||||
// Subtítulo com período
|
type Estatistica = { min: number; max: number; avg: number };
|
||||||
doc.setFontSize(12);
|
const estatPorMetrica = relatorio.estatisticas as unknown as Record<
|
||||||
doc.setTextColor(0, 0, 0);
|
MetricKey,
|
||||||
doc.text(
|
Estatistica | undefined
|
||||||
`Período: ${format(inicio, "dd/MM/yyyy HH:mm", { locale: ptBR })} até ${format(fim, "dd/MM/yyyy HH:mm", { locale: ptBR })}`,
|
>;
|
||||||
14,
|
|
||||||
30,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Informações gerais
|
const doc = new jsPDF();
|
||||||
doc.setFontSize(10);
|
|
||||||
doc.text(
|
|
||||||
`Gerado em: ${format(new Date(), "dd/MM/yyyy HH:mm", { locale: ptBR })}`,
|
|
||||||
14,
|
|
||||||
38,
|
|
||||||
);
|
|
||||||
doc.text(`Total de registros: ${relatorio.metricas.length}`, 14, 44);
|
|
||||||
|
|
||||||
// Estatísticas
|
// Título
|
||||||
let yPos = 55;
|
doc.setFontSize(20);
|
||||||
doc.setFontSize(14);
|
doc.setTextColor(102, 126, 234); // Primary color
|
||||||
doc.setTextColor(102, 126, 234);
|
doc.text('Relatório de Monitoramento do Sistema', 14, 20);
|
||||||
doc.text("Estatísticas do Período", 14, yPos);
|
|
||||||
yPos += 10;
|
|
||||||
|
|
||||||
const statsData: any[] = [];
|
// Subtítulo com período
|
||||||
Object.entries(selectedMetrics).forEach(([metric, selected]) => {
|
doc.setFontSize(12);
|
||||||
if (selected && relatorio.estatisticas[metric]) {
|
doc.setTextColor(0, 0, 0);
|
||||||
const stats = relatorio.estatisticas[metric];
|
doc.text(
|
||||||
if (stats) {
|
`Período: ${format(inicio, 'dd/MM/yyyy HH:mm', { locale: ptBR })} até ${format(fim, 'dd/MM/yyyy HH:mm', { locale: ptBR })}`,
|
||||||
statsData.push([
|
14,
|
||||||
metricLabels[metric],
|
30
|
||||||
stats.min.toFixed(2),
|
);
|
||||||
stats.max.toFixed(2),
|
|
||||||
stats.avg.toFixed(2),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
autoTable(doc, {
|
// Informações gerais
|
||||||
startY: yPos,
|
doc.setFontSize(10);
|
||||||
head: [["Métrica", "Mínimo", "Máximo", "Média"]],
|
doc.text(`Gerado em: ${format(new Date(), 'dd/MM/yyyy HH:mm', { locale: ptBR })}`, 14, 38);
|
||||||
body: statsData,
|
doc.text(`Total de registros: ${relatorio.metricas.length}`, 14, 44);
|
||||||
theme: "striped",
|
|
||||||
headStyles: { fillColor: [102, 126, 234] },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Dados detalhados (últimos 50 registros)
|
// Estatísticas
|
||||||
const finalY = (doc as any).lastAutoTable.finalY || yPos + 10;
|
let yPos = 55;
|
||||||
yPos = finalY + 15;
|
doc.setFontSize(14);
|
||||||
|
doc.setTextColor(102, 126, 234);
|
||||||
|
doc.text('Estatísticas do Período', 14, yPos);
|
||||||
|
yPos += 10;
|
||||||
|
|
||||||
doc.setFontSize(14);
|
const statsData: string[][] = [];
|
||||||
doc.setTextColor(102, 126, 234);
|
(Object.entries(selectedMetrics) as Array<[MetricKey, boolean]>).forEach(
|
||||||
doc.text("Registros Detalhados (Últimos 50)", 14, yPos);
|
([metric, selected]) => {
|
||||||
yPos += 10;
|
if (selected && estatPorMetrica[metric]) {
|
||||||
|
const stats = estatPorMetrica[metric];
|
||||||
|
if (stats) {
|
||||||
|
statsData.push([
|
||||||
|
metricLabels[metric],
|
||||||
|
stats.min.toFixed(2),
|
||||||
|
stats.max.toFixed(2),
|
||||||
|
stats.avg.toFixed(2)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const detailsData = relatorio.metricas.slice(0, 50).map((m) => {
|
autoTable(doc, {
|
||||||
const row = [format(m.timestamp, "dd/MM HH:mm", { locale: ptBR })];
|
startY: yPos,
|
||||||
Object.entries(selectedMetrics).forEach(([metric, selected]) => {
|
head: [['Métrica', 'Mínimo', 'Máximo', 'Média']],
|
||||||
if (selected) {
|
body: statsData,
|
||||||
row.push((m[metric] || 0).toFixed(1));
|
theme: 'striped',
|
||||||
}
|
headStyles: { fillColor: [102, 126, 234] }
|
||||||
});
|
});
|
||||||
return row;
|
|
||||||
});
|
|
||||||
|
|
||||||
const headers = ["Data/Hora"];
|
// Dados detalhados (últimos 50 registros)
|
||||||
Object.entries(selectedMetrics).forEach(([metric, selected]) => {
|
type JsPDFWithAutoTable = jsPDF & {
|
||||||
if (selected) {
|
lastAutoTable?: { finalY: number };
|
||||||
headers.push(metricLabels[metric]);
|
};
|
||||||
}
|
const finalY = (doc as JsPDFWithAutoTable).lastAutoTable?.finalY ?? yPos + 10;
|
||||||
});
|
yPos = finalY + 15;
|
||||||
|
|
||||||
autoTable(doc, {
|
doc.setFontSize(14);
|
||||||
startY: yPos,
|
doc.setTextColor(102, 126, 234);
|
||||||
head: [headers],
|
doc.text('Registros Detalhados (Últimos 50)', 14, yPos);
|
||||||
body: detailsData,
|
yPos += 10;
|
||||||
theme: "grid",
|
|
||||||
headStyles: { fillColor: [102, 126, 234] },
|
|
||||||
styles: { fontSize: 8 },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Footer
|
const detailsData: string[][] = relatorio.metricas.slice(0, 50).map((m) => {
|
||||||
const pageCount = doc.getNumberOfPages();
|
const row: string[] = [format(m.timestamp, 'dd/MM HH:mm', { locale: ptBR })];
|
||||||
for (let i = 1; i <= pageCount; i++) {
|
(Object.entries(selectedMetrics) as Array<[MetricKey, boolean]>).forEach(
|
||||||
doc.setPage(i);
|
([metric, selected]) => {
|
||||||
doc.setFontSize(8);
|
if (selected) {
|
||||||
doc.setTextColor(128, 128, 128);
|
const value = (m as unknown as Record<MetricKey, number | undefined>)[metric] ?? 0;
|
||||||
doc.text(
|
row.push(value.toFixed(1));
|
||||||
`SGSE - Sistema de Gestão da Secretaria de Esportes | Página ${i} de ${pageCount}`,
|
}
|
||||||
doc.internal.pageSize.getWidth() / 2,
|
}
|
||||||
doc.internal.pageSize.getHeight() - 10,
|
);
|
||||||
{ align: "center" },
|
return row;
|
||||||
);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Salvar
|
const headers = ['Data/Hora'];
|
||||||
doc.save(
|
(Object.entries(selectedMetrics) as Array<[MetricKey, boolean]>).forEach(
|
||||||
`relatorio-monitoramento-${format(new Date(), "yyyy-MM-dd-HHmm")}.pdf`,
|
([metric, selected]) => {
|
||||||
);
|
if (selected) {
|
||||||
} catch (error) {
|
headers.push(metricLabels[metric]);
|
||||||
console.error("Erro ao gerar PDF:", error);
|
}
|
||||||
alert("Erro ao gerar relatório PDF. Tente novamente.");
|
}
|
||||||
} finally {
|
);
|
||||||
generating = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateCSV() {
|
autoTable(doc, {
|
||||||
generating = true;
|
startY: yPos,
|
||||||
|
head: [headers],
|
||||||
|
body: detailsData,
|
||||||
|
theme: 'grid',
|
||||||
|
headStyles: { fillColor: [102, 126, 234] },
|
||||||
|
styles: { fontSize: 8 }
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
// Footer
|
||||||
const { inicio, fim } = getDateRange();
|
const pageCount = doc.getNumberOfPages();
|
||||||
const relatorio = await client.query(api.monitoramento.gerarRelatorio, {
|
for (let i = 1; i <= pageCount; i++) {
|
||||||
dataInicio: inicio,
|
doc.setPage(i);
|
||||||
dataFim: fim,
|
doc.setFontSize(8);
|
||||||
});
|
doc.setTextColor(128, 128, 128);
|
||||||
|
doc.text(
|
||||||
|
`SGSE - Sistema de Gestão da Secretaria de Esportes | Página ${i} de ${pageCount}`,
|
||||||
|
doc.internal.pageSize.getWidth() / 2,
|
||||||
|
doc.internal.pageSize.getHeight() - 10,
|
||||||
|
{ align: 'center' }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Preparar dados para CSV
|
// Salvar
|
||||||
const csvData = relatorio.metricas.map((m) => {
|
doc.save(`relatorio-monitoramento-${format(new Date(), 'yyyy-MM-dd-HHmm')}.pdf`);
|
||||||
const row: any = {
|
} catch (error) {
|
||||||
"Data/Hora": format(m.timestamp, "dd/MM/yyyy HH:mm:ss", {
|
console.error('Erro ao gerar PDF:', error);
|
||||||
locale: ptBR,
|
alert('Erro ao gerar relatório PDF. Tente novamente.');
|
||||||
}),
|
} finally {
|
||||||
};
|
generating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Object.entries(selectedMetrics).forEach(([metric, selected]) => {
|
async function generateCSV() {
|
||||||
if (selected) {
|
generating = true;
|
||||||
row[metricLabels[metric]] = m[metric] || 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return row;
|
try {
|
||||||
});
|
const { inicio, fim } = getDateRange();
|
||||||
|
const relatorio = await client.query(api.monitoramento.gerarRelatorio, {
|
||||||
|
dataInicio: inicio,
|
||||||
|
dataFim: fim
|
||||||
|
});
|
||||||
|
|
||||||
// Gerar CSV
|
// Preparar dados para CSV
|
||||||
const csv = Papa.unparse(csvData);
|
const csvData = relatorio.metricas.map((m) => {
|
||||||
|
const row: Record<string, string | number> = {
|
||||||
|
'Data/Hora': format(m.timestamp, 'dd/MM/yyyy HH:mm:ss', {
|
||||||
|
locale: ptBR
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
// Download
|
(Object.entries(selectedMetrics) as Array<[MetricKey, boolean]>).forEach(
|
||||||
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
([metric, selected]) => {
|
||||||
const link = document.createElement("a");
|
if (selected) {
|
||||||
const url = URL.createObjectURL(blob);
|
row[metricLabels[metric]] =
|
||||||
link.setAttribute("href", url);
|
(m as unknown as Record<MetricKey, number | undefined>)[metric] ?? 0;
|
||||||
link.setAttribute(
|
}
|
||||||
"download",
|
}
|
||||||
`relatorio-monitoramento-${format(new Date(), "yyyy-MM-dd-HHmm")}.csv`,
|
);
|
||||||
);
|
|
||||||
link.style.visibility = "hidden";
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erro ao gerar CSV:", error);
|
|
||||||
alert("Erro ao gerar relatório CSV. Tente novamente.");
|
|
||||||
} finally {
|
|
||||||
generating = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleAllMetrics(value: boolean) {
|
return row;
|
||||||
Object.keys(selectedMetrics).forEach((key) => {
|
});
|
||||||
selectedMetrics[key] = value;
|
|
||||||
});
|
// Gerar CSV
|
||||||
}
|
const csv = Papa.unparse(csvData);
|
||||||
|
|
||||||
|
// Download
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
link.setAttribute('href', url);
|
||||||
|
link.setAttribute(
|
||||||
|
'download',
|
||||||
|
`relatorio-monitoramento-${format(new Date(), 'yyyy-MM-dd-HHmm')}.csv`
|
||||||
|
);
|
||||||
|
link.style.visibility = 'hidden';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao gerar CSV:', error);
|
||||||
|
alert('Erro ao gerar relatório CSV. Tente novamente.');
|
||||||
|
} finally {
|
||||||
|
generating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAllMetrics(value: boolean) {
|
||||||
|
(Object.keys(selectedMetrics) as MetricKey[]).forEach((key) => {
|
||||||
|
selectedMetrics[key] = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<dialog class="modal modal-open">
|
<dialog class="modal modal-open">
|
||||||
<div class="modal-box max-w-3xl bg-linear-to-br from-base-100 to-base-200">
|
<div class="modal-box from-base-100 to-base-200 max-w-3xl bg-linear-to-br">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
|
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
|
||||||
onclick={onClose}
|
onclick={onClose}
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<h3 class="font-bold text-3xl text-primary mb-2">
|
<h3 class="text-primary mb-2 text-3xl font-bold">📊 Gerador de Relatórios</h3>
|
||||||
📊 Gerador de Relatórios
|
<p class="text-base-content/60 mb-6">Exporte dados de monitoramento em PDF ou CSV</p>
|
||||||
</h3>
|
|
||||||
<p class="text-base-content/60 mb-6">
|
|
||||||
Exporte dados de monitoramento em PDF ou CSV
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Seleção de Período -->
|
<!-- Seleção de Período -->
|
||||||
<div class="card bg-base-100 shadow-xl mb-6">
|
<div class="card bg-base-100 mb-6 shadow-xl">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="card-title text-xl">Período</h4>
|
<h4 class="card-title text-xl">Período</h4>
|
||||||
|
|
||||||
<!-- Botões de Período Rápido -->
|
<!-- Botões de Período Rápido -->
|
||||||
<div class="flex gap-2 mb-4">
|
<div class="mb-4 flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm {periodType === 'today'
|
class="btn btn-sm {periodType === 'today' ? 'btn-primary' : 'btn-outline'}"
|
||||||
? 'btn-primary'
|
onclick={() => setPeriod('today')}
|
||||||
: 'btn-outline'}"
|
>
|
||||||
onclick={() => setPeriod("today")}
|
Hoje
|
||||||
>
|
</button>
|
||||||
Hoje
|
<button
|
||||||
</button>
|
type="button"
|
||||||
<button
|
class="btn btn-sm {periodType === 'week' ? 'btn-primary' : 'btn-outline'}"
|
||||||
type="button"
|
onclick={() => setPeriod('week')}
|
||||||
class="btn btn-sm {periodType === 'week'
|
>
|
||||||
? 'btn-primary'
|
Última Semana
|
||||||
: 'btn-outline'}"
|
</button>
|
||||||
onclick={() => setPeriod("week")}
|
<button
|
||||||
>
|
type="button"
|
||||||
Última Semana
|
class="btn btn-sm {periodType === 'month' ? 'btn-primary' : 'btn-outline'}"
|
||||||
</button>
|
onclick={() => setPeriod('month')}
|
||||||
<button
|
>
|
||||||
type="button"
|
Último Mês
|
||||||
class="btn btn-sm {periodType === 'month'
|
</button>
|
||||||
? 'btn-primary'
|
<button
|
||||||
: 'btn-outline'}"
|
type="button"
|
||||||
onclick={() => setPeriod("month")}
|
class="btn btn-sm {periodType === 'custom' ? 'btn-primary' : 'btn-outline'}"
|
||||||
>
|
onclick={() => (periodType = 'custom')}
|
||||||
Último Mês
|
>
|
||||||
</button>
|
Personalizado
|
||||||
<button
|
</button>
|
||||||
type="button"
|
</div>
|
||||||
class="btn btn-sm {periodType === 'custom'
|
|
||||||
? 'btn-primary'
|
|
||||||
: 'btn-outline'}"
|
|
||||||
onclick={() => (periodType = "custom")}
|
|
||||||
>
|
|
||||||
Personalizado
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if periodType === "custom"}
|
{#if periodType === 'custom'}
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="dataInicio">
|
<label class="label" for="dataInicio">
|
||||||
<span class="label-text font-semibold">Data Início</span>
|
<span class="label-text font-semibold">Data Início</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="dataInicio"
|
id="dataInicio"
|
||||||
type="date"
|
type="date"
|
||||||
class="input input-bordered input-primary"
|
class="input input-bordered input-primary"
|
||||||
bind:value={dataInicio}
|
bind:value={dataInicio}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="horaInicio">
|
<label class="label" for="horaInicio">
|
||||||
<span class="label-text font-semibold">Hora Início</span>
|
<span class="label-text font-semibold">Hora Início</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="horaInicio"
|
id="horaInicio"
|
||||||
type="time"
|
type="time"
|
||||||
class="input input-bordered input-primary"
|
class="input input-bordered input-primary"
|
||||||
bind:value={horaInicio}
|
bind:value={horaInicio}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="dataFim">
|
<label class="label" for="dataFim">
|
||||||
<span class="label-text font-semibold">Data Fim</span>
|
<span class="label-text font-semibold">Data Fim</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="dataFim"
|
id="dataFim"
|
||||||
type="date"
|
type="date"
|
||||||
class="input input-bordered input-primary"
|
class="input input-bordered input-primary"
|
||||||
bind:value={dataFim}
|
bind:value={dataFim}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="horaFim">
|
<label class="label" for="horaFim">
|
||||||
<span class="label-text font-semibold">Hora Fim</span>
|
<span class="label-text font-semibold">Hora Fim</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="horaFim"
|
id="horaFim"
|
||||||
type="time"
|
type="time"
|
||||||
class="input input-bordered input-primary"
|
class="input input-bordered input-primary"
|
||||||
bind:value={horaFim}
|
bind:value={horaFim}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Seleção de Métricas -->
|
<!-- Seleção de Métricas -->
|
||||||
<div class="card bg-base-100 shadow-xl mb-6">
|
<div class="card bg-base-100 mb-6 shadow-xl">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="mb-4 flex items-center justify-between">
|
||||||
<h4 class="card-title text-xl">Métricas a Incluir</h4>
|
<h4 class="card-title text-xl">Métricas a Incluir</h4>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-xs btn-ghost"
|
class="btn btn-xs btn-ghost"
|
||||||
onclick={() => toggleAllMetrics(true)}
|
onclick={() => toggleAllMetrics(true)}
|
||||||
>
|
>
|
||||||
Selecionar Todas
|
Selecionar Todas
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-xs btn-ghost"
|
class="btn btn-xs btn-ghost"
|
||||||
onclick={() => toggleAllMetrics(false)}
|
onclick={() => toggleAllMetrics(false)}
|
||||||
>
|
>
|
||||||
Limpar
|
Limpar
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||||
{#each Object.entries(metricLabels) as [metric, label]}
|
{#each metricEntries as [metric, label] (metric)}
|
||||||
<label
|
<label
|
||||||
class="label cursor-pointer justify-start gap-3 hover:bg-base-200 rounded-lg p-2"
|
class="label hover:bg-base-200 cursor-pointer justify-start gap-3 rounded-lg p-2"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox checkbox-primary"
|
class="checkbox checkbox-primary"
|
||||||
bind:checked={selectedMetrics[metric]}
|
checked={isMetricSelected(metric)}
|
||||||
/>
|
onchange={(e) =>
|
||||||
<span class="label-text">{label}</span>
|
setMetricSelected(metric, (e.currentTarget as HTMLInputElement).checked)}
|
||||||
</label>
|
/>
|
||||||
{/each}
|
<span class="label-text">{label}</span>
|
||||||
</div>
|
</label>
|
||||||
</div>
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Botões de Exportação -->
|
<!-- Botões de Exportação -->
|
||||||
<div class="flex gap-3 justify-end">
|
<div class="flex justify-end gap-3">
|
||||||
<button
|
<button type="button" class="btn btn-outline" onclick={onClose} disabled={generating}>
|
||||||
type="button"
|
Cancelar
|
||||||
class="btn btn-outline"
|
</button>
|
||||||
onclick={onClose}
|
|
||||||
disabled={generating}
|
|
||||||
>
|
|
||||||
Cancelar
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-secondary"
|
class="btn btn-secondary"
|
||||||
onclick={generateCSV}
|
onclick={generateCSV}
|
||||||
disabled={generating || !Object.values(selectedMetrics).some((v) => v)}
|
disabled={generating || !Object.values(selectedMetrics).some((v) => v)}
|
||||||
>
|
>
|
||||||
{#if generating}
|
{#if generating}
|
||||||
<span class="loading loading-spinner"></span>
|
<span class="loading loading-spinner"></span>
|
||||||
{:else}
|
{:else}
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-5 w-5"
|
class="h-5 w-5"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{/if}
|
{/if}
|
||||||
Exportar CSV
|
Exportar CSV
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
onclick={generatePDF}
|
onclick={generatePDF}
|
||||||
disabled={generating || !Object.values(selectedMetrics).some((v) => v)}
|
disabled={generating || !Object.values(selectedMetrics).some((v) => v)}
|
||||||
>
|
>
|
||||||
{#if generating}
|
{#if generating}
|
||||||
<span class="loading loading-spinner"></span>
|
<span class="loading loading-spinner"></span>
|
||||||
{:else}
|
{:else}
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-5 w-5"
|
class="h-5 w-5"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"
|
d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{/if}
|
{/if}
|
||||||
Exportar PDF
|
Exportar PDF
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !Object.values(selectedMetrics).some((v) => v)}
|
{#if !Object.values(selectedMetrics).some((v) => v)}
|
||||||
<div class="alert alert-warning mt-4">
|
<div class="alert alert-warning mt-4">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="stroke-current shrink-0 h-6 w-6"
|
class="h-6 w-6 shrink-0 stroke-current"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Selecione pelo menos uma métrica para gerar o relatório.</span>
|
<span>Selecione pelo menos uma métrica para gerar o relatório.</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||||
<form method="dialog" class="modal-backdrop" onclick={onClose}>
|
<form method="dialog" class="modal-backdrop" onclick={onClose}>
|
||||||
<button type="button">close</button>
|
<button type="button">close</button>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,69 +1,60 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from "$app/state";
|
import { page } from '$app/state';
|
||||||
import ActionGuard from "$lib/components/ActionGuard.svelte";
|
import ActionGuard from '$lib/components/ActionGuard.svelte';
|
||||||
import { Toaster } from "svelte-sonner";
|
import { Toaster } from 'svelte-sonner';
|
||||||
import PushNotificationManager from "$lib/components/PushNotificationManager.svelte";
|
import PushNotificationManager from '$lib/components/PushNotificationManager.svelte';
|
||||||
const { children } = $props();
|
const { children } = $props();
|
||||||
|
|
||||||
// Resolver recurso/ação a partir da rota
|
// Resolver recurso/ação a partir da rota
|
||||||
const routeAction = $derived.by(() => {
|
const routeAction = $derived.by(() => {
|
||||||
const p = page.url.pathname;
|
const p = page.url.pathname;
|
||||||
if (p === "/" || p === "/solicitar-acesso") return null;
|
if (p === '/' || p === '/solicitar-acesso') return null;
|
||||||
|
|
||||||
// Funcionários
|
// Funcionários
|
||||||
if (p.startsWith("/recursos-humanos/funcionarios")) {
|
if (p.startsWith('/recursos-humanos/funcionarios')) {
|
||||||
if (p.includes("/cadastro"))
|
if (p.includes('/cadastro')) return { recurso: 'funcionarios', acao: 'criar' };
|
||||||
return { recurso: "funcionarios", acao: "criar" };
|
if (p.includes('/excluir')) return { recurso: 'funcionarios', acao: 'excluir' };
|
||||||
if (p.includes("/excluir"))
|
if (p.includes('/editar') || p.includes('/funcionarioId'))
|
||||||
return { recurso: "funcionarios", acao: "excluir" };
|
return { recurso: 'funcionarios', acao: 'editar' };
|
||||||
if (p.includes("/editar") || p.includes("/funcionarioId"))
|
return { recurso: 'funcionarios', acao: 'listar' };
|
||||||
return { recurso: "funcionarios", acao: "editar" };
|
}
|
||||||
return { recurso: "funcionarios", acao: "listar" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Símbolos
|
// Símbolos
|
||||||
if (p.startsWith("/recursos-humanos/simbolos")) {
|
if (p.startsWith('/recursos-humanos/simbolos')) {
|
||||||
if (p.includes("/cadastro"))
|
if (p.includes('/cadastro')) return { recurso: 'simbolos', acao: 'criar' };
|
||||||
return { recurso: "simbolos", acao: "criar" };
|
if (p.includes('/excluir')) return { recurso: 'simbolos', acao: 'excluir' };
|
||||||
if (p.includes("/excluir"))
|
if (p.includes('/editar') || p.includes('/simboloId'))
|
||||||
return { recurso: "simbolos", acao: "excluir" };
|
return { recurso: 'simbolos', acao: 'editar' };
|
||||||
if (p.includes("/editar") || p.includes("/simboloId"))
|
return { recurso: 'simbolos', acao: 'listar' };
|
||||||
return { recurso: "simbolos", acao: "editar" };
|
}
|
||||||
return { recurso: "simbolos", acao: "listar" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Outras áreas (uso genérico: ver)
|
// Outras áreas (uso genérico: ver)
|
||||||
if (p.startsWith("/financeiro"))
|
if (p.startsWith('/financeiro')) return { recurso: 'financeiro', acao: 'ver' };
|
||||||
return { recurso: "financeiro", acao: "ver" };
|
if (p.startsWith('/controladoria')) return { recurso: 'controladoria', acao: 'ver' };
|
||||||
if (p.startsWith("/controladoria"))
|
if (p.startsWith('/licitacoes')) return { recurso: 'licitacoes', acao: 'ver' };
|
||||||
return { recurso: "controladoria", acao: "ver" };
|
if (p.startsWith('/compras')) return { recurso: 'compras', acao: 'ver' };
|
||||||
if (p.startsWith("/licitacoes"))
|
if (p.startsWith('/juridico')) return { recurso: 'juridico', acao: 'ver' };
|
||||||
return { recurso: "licitacoes", acao: "ver" };
|
if (p.startsWith('/comunicacao')) return { recurso: 'comunicacao', acao: 'ver' };
|
||||||
if (p.startsWith("/compras")) return { recurso: "compras", acao: "ver" };
|
if (p.startsWith('/programas-esportivos'))
|
||||||
if (p.startsWith("/juridico")) return { recurso: "juridico", acao: "ver" };
|
return { recurso: 'programas_esportivos', acao: 'ver' };
|
||||||
if (p.startsWith("/comunicacao"))
|
if (p.startsWith('/secretaria-executiva'))
|
||||||
return { recurso: "comunicacao", acao: "ver" };
|
return { recurso: 'secretaria_executiva', acao: 'ver' };
|
||||||
if (p.startsWith("/programas-esportivos"))
|
if (p.startsWith('/gestao-pessoas')) return { recurso: 'gestao_pessoas', acao: 'ver' };
|
||||||
return { recurso: "programas_esportivos", acao: "ver" };
|
|
||||||
if (p.startsWith("/secretaria-executiva"))
|
|
||||||
return { recurso: "secretaria_executiva", acao: "ver" };
|
|
||||||
if (p.startsWith("/gestao-pessoas"))
|
|
||||||
return { recurso: "gestao_pessoas", acao: "ver" };
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if routeAction}
|
{#if routeAction}
|
||||||
<ActionGuard recurso={routeAction.recurso} acao={routeAction.acao}>
|
<ActionGuard recurso={routeAction.recurso} acao={routeAction.acao}>
|
||||||
<main id="container-central" class="w-full max-w-none px-3 lg:px-4 py-4">
|
<main id="container-central" class="w-full max-w-none px-3 py-4 lg:px-4">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</main>
|
</main>
|
||||||
</ActionGuard>
|
</ActionGuard>
|
||||||
{:else}
|
{:else}
|
||||||
<main id="container-central" class="w-full max-w-none px-3 lg:px-4 py-4">
|
<main id="container-central" class="w-full max-w-none px-3 py-4 lg:px-4">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</main>
|
</main>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Toast Notifications (Sonner) -->
|
<!-- Toast Notifications (Sonner) -->
|
||||||
|
|||||||
@@ -1,55 +1,54 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SystemMonitorCardLocal from "$lib/components/ti/SystemMonitorCardLocal.svelte";
|
import { resolve } from '$app/paths';
|
||||||
|
import SystemMonitorCardLocal from '$lib/components/ti/SystemMonitorCardLocal.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto px-4 py-6 max-w-7xl">
|
<div class="container mx-auto max-w-7xl px-4 py-6">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex items-center justify-between mb-8">
|
<div class="mb-8 flex items-center justify-between">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<div
|
<div class="from-primary/20 to-primary/10 rounded-2xl bg-linear-to-br p-3">
|
||||||
class="p-3 bg-linear-to-br from-primary/20 to-primary/10 rounded-2xl"
|
<svg
|
||||||
>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<svg
|
class="text-primary h-10 w-10"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
fill="none"
|
||||||
class="h-10 w-10 text-primary"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
>
|
||||||
stroke="currentColor"
|
<path
|
||||||
>
|
stroke-linecap="round"
|
||||||
<path
|
stroke-linejoin="round"
|
||||||
stroke-linecap="round"
|
stroke-width="2"
|
||||||
stroke-linejoin="round"
|
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"
|
||||||
stroke-width="2"
|
/>
|
||||||
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"
|
</svg>
|
||||||
/>
|
</div>
|
||||||
</svg>
|
<div>
|
||||||
</div>
|
<h1 class="text-primary text-4xl font-bold">Monitoramento SGSE</h1>
|
||||||
<div>
|
<p class="text-base-content/60 mt-2 text-lg">
|
||||||
<h1 class="text-4xl font-bold text-primary">Monitoramento SGSE</h1>
|
Sistema de monitoramento técnico em tempo real
|
||||||
<p class="text-base-content/60 mt-2 text-lg">
|
</p>
|
||||||
Sistema de monitoramento técnico em tempo real
|
</div>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
<a href={resolve('/ti')} class="btn btn-ghost">
|
||||||
</div>
|
<svg
|
||||||
<a href="/ti" class="btn btn-ghost">
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<svg
|
class="h-5 w-5"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
fill="none"
|
||||||
class="h-5 w-5"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
>
|
||||||
stroke="currentColor"
|
<path
|
||||||
>
|
stroke-linecap="round"
|
||||||
<path
|
stroke-linejoin="round"
|
||||||
stroke-linecap="round"
|
stroke-width="2"
|
||||||
stroke-linejoin="round"
|
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
||||||
stroke-width="2"
|
/>
|
||||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
</svg>
|
||||||
/>
|
Voltar
|
||||||
</svg>
|
</a>
|
||||||
Voltar
|
</div>
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Card de Monitoramento -->
|
<!-- Card de Monitoramento -->
|
||||||
<SystemMonitorCardLocal />
|
<SystemMonitorCardLocal />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user