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.
- Added new query functions for system status and database activity, providing better insights into system performance.
- Cleaned up existing code to ensure maintainability and improved error handling across various functions.
This commit is contained in:
2025-11-08 18:30:27 -03:00
parent 5d76c375c2
commit 4ed90d380d
5 changed files with 2737 additions and 2600 deletions

View File

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

View File

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

View File

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