feat: implement error handling and logging in server hooks to capture and notify on 404 and 500 errors, enhancing server reliability and monitoring

This commit is contained in:
2025-12-08 11:52:27 -03:00
parent e1f1af7530
commit fdfbd8b051
31 changed files with 7305 additions and 932 deletions

View File

@@ -0,0 +1,104 @@
<script lang="ts">
import { BarChart3, CheckCircle2, XCircle, Users } from 'lucide-svelte';
interface Estatisticas {
totalRegistros: number;
dentroDoPrazo: number;
foraDoPrazo: number;
totalFuncionarios: number;
funcionariosDentroPrazo: number;
funcionariosForaPrazo: number;
}
interface Props {
estatisticas?: Estatisticas;
}
let { estatisticas = undefined }: Props = $props();
</script>
{#if estatisticas}
<div class="mb-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
<!-- Total de Registros -->
<div
class="card transform border border-blue-500/20 bg-gradient-to-br from-blue-500/10 to-blue-600/20 shadow-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl"
>
<div class="card-body">
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-base-content/70 mb-1 text-sm font-semibold">Total de Registros</p>
<p class="text-base-content text-3xl font-bold">{estatisticas.totalRegistros}</p>
</div>
<div class="rounded-xl bg-blue-500/20 p-3">
<BarChart3 class="h-8 w-8 text-blue-600" strokeWidth={2.5} />
</div>
</div>
</div>
</div>
<!-- Dentro do Prazo -->
<div
class="card transform border border-green-500/20 bg-gradient-to-br from-green-500/10 to-green-600/20 shadow-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl"
>
<div class="card-body">
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-base-content/70 mb-1 text-sm font-semibold">Dentro do Prazo</p>
<p class="text-3xl font-bold text-green-600">{estatisticas.dentroDoPrazo}</p>
<p class="text-base-content/60 mt-1 text-xs">
{estatisticas.totalRegistros > 0
? ((estatisticas.dentroDoPrazo / estatisticas.totalRegistros) * 100).toFixed(1)
: 0}% do total
</p>
</div>
<div class="rounded-xl bg-green-500/20 p-3">
<CheckCircle2 class="h-8 w-8 text-green-600" strokeWidth={2.5} />
</div>
</div>
</div>
</div>
<!-- Fora do Prazo -->
<div
class="card transform border border-red-500/20 bg-gradient-to-br from-red-500/10 to-red-600/20 shadow-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl"
>
<div class="card-body">
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-base-content/70 mb-1 text-sm font-semibold">Fora do Prazo</p>
<p class="text-3xl font-bold text-red-600">{estatisticas.foraDoPrazo}</p>
<p class="text-base-content/60 mt-1 text-xs">
{estatisticas.totalRegistros > 0
? ((estatisticas.foraDoPrazo / estatisticas.totalRegistros) * 100).toFixed(1)
: 0}% do total
</p>
</div>
<div class="rounded-xl bg-red-500/20 p-3">
<XCircle class="h-8 w-8 text-red-600" strokeWidth={2.5} />
</div>
</div>
</div>
</div>
<!-- Funcionários -->
<div
class="card transform border border-purple-500/20 bg-gradient-to-br from-purple-500/10 to-purple-600/20 shadow-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl"
>
<div class="card-body">
<div class="flex items-center justify-between">
<div class="flex-1">
<p class="text-base-content/70 mb-1 text-sm font-semibold">Funcionários</p>
<p class="text-3xl font-bold text-purple-600">{estatisticas.totalFuncionarios}</p>
<p class="text-base-content/60 mt-1 text-xs">
{estatisticas.funcionariosDentroPrazo} dentro, {estatisticas.funcionariosForaPrazo} fora
</p>
</div>
<div class="rounded-xl bg-purple-500/20 p-3">
<Users class="h-8 w-8 text-purple-600" strokeWidth={2.5} />
</div>
</div>
</div>
</div>
</div>
{/if}

View File

@@ -0,0 +1,218 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { Chart, registerables } from 'chart.js';
import { BarChart3, XCircle, FileText } from 'lucide-svelte';
interface Estatisticas {
totalRegistros: number;
dentroDoPrazo: number;
foraDoPrazo: number;
}
interface Props {
estatisticas?: Estatisticas;
chartData?: {
labels: string[];
datasets: Array<{
label: string;
data: number[];
backgroundColor: string;
borderColor: string;
borderWidth: number;
}>;
} | null;
isLoading?: boolean;
error?: Error | null;
}
let { estatisticas = undefined, chartData = null, isLoading = false, error = null }: Props = $props();
let chartCanvas: HTMLCanvasElement;
let chartInstance: Chart | null = null;
onMount(() => {
Chart.register(...registerables);
const timeoutId = setTimeout(() => {
if (chartCanvas && estatisticas && chartData && !chartInstance) {
try {
criarGrafico();
} catch (err) {
console.error('Erro ao criar gráfico no onMount:', err);
}
}
}, 500);
return () => {
clearTimeout(timeoutId);
};
});
onDestroy(() => {
if (chartInstance) {
chartInstance.destroy();
}
});
function criarGrafico() {
if (!chartCanvas || !estatisticas || !chartData) {
return;
}
const ctx = chartCanvas.getContext('2d');
if (!ctx) {
return;
}
if (chartInstance) {
chartInstance.destroy();
chartInstance = null;
}
try {
chartInstance = new Chart(ctx, {
type: 'bar',
data: chartData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top',
labels: {
color: 'hsl(var(--bc))',
font: {
size: 12,
family: "'Inter', sans-serif"
},
usePointStyle: true,
padding: 15
}
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.85)',
titleColor: '#fff',
bodyColor: '#fff',
borderColor: 'hsl(var(--p))',
borderWidth: 1,
padding: 12,
callbacks: {
label: function (context) {
const label = context.dataset.label || '';
const value = context.parsed.y;
const total = estatisticas!.totalRegistros;
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : '0.0';
return `${label}: ${value} (${percentage}%)`;
}
}
}
},
scales: {
x: {
stacked: true,
grid: {
display: false
},
ticks: {
color: 'hsl(var(--bc))',
font: {
size: 12
}
}
},
y: {
stacked: true,
beginAtZero: true,
grid: {
color: 'rgba(0, 0, 0, 0.05)'
},
ticks: {
color: 'hsl(var(--bc))',
font: {
size: 11
},
stepSize: 1
}
}
},
animation: {
duration: 1000,
easing: 'easeInOutQuart'
},
interaction: {
mode: 'index',
intersect: false
}
}
});
} catch (error) {
console.error('Erro ao criar gráfico:', error);
}
}
$effect(() => {
if (chartCanvas && estatisticas && chartData) {
if (chartInstance) {
chartInstance.destroy();
chartInstance = null;
}
const timeoutId = setTimeout(() => {
try {
criarGrafico();
} catch (err) {
console.error('Erro ao criar gráfico no effect:', err);
}
}, 200);
return () => {
clearTimeout(timeoutId);
};
}
});
</script>
<div class="card bg-base-100/90 border-base-300 mb-8 border shadow-xl backdrop-blur-sm">
<div class="card-body">
<div class="mb-6 flex items-center justify-between">
<h2 class="card-title text-2xl">
<div class="bg-primary/10 rounded-lg p-2">
<BarChart3 class="text-primary h-6 w-6" strokeWidth={2.5} />
</div>
<span>Visão Geral das Estatísticas</span>
</h2>
</div>
<div class="bg-base-200/50 border-base-300 relative h-80 w-full rounded-xl border p-4">
{#if isLoading}
<div class="bg-base-200/30 absolute inset-0 flex items-center justify-center rounded-xl">
<div class="flex flex-col items-center gap-4">
<span class="loading loading-spinner loading-lg text-primary"></span>
<span class="text-base-content/70 font-medium">Carregando estatísticas...</span>
</div>
</div>
{:else if error}
<div class="bg-base-200/30 absolute inset-0 flex items-center justify-center rounded-xl">
<div class="alert alert-error shadow-lg">
<XCircle class="h-6 w-6" />
<div>
<h3 class="font-bold">Erro ao carregar estatísticas</h3>
<div class="mt-1 text-sm">
{error?.message || String(error) || 'Erro desconhecido'}
</div>
</div>
</div>
</div>
{:else if !estatisticas || !chartData}
<div class="bg-base-200/30 absolute inset-0 flex items-center justify-center rounded-xl">
<div class="text-center">
<FileText class="text-base-content/30 mx-auto mb-2 h-12 w-12" />
<p class="text-base-content/70">Nenhuma estatística disponível</p>
</div>
</div>
{:else}
<canvas bind:this={chartCanvas} class="h-full w-full"></canvas>
{/if}
</div>
</div>
</div>

View File

@@ -0,0 +1,66 @@
<script lang="ts">
import { Clock } from 'lucide-svelte';
interface Estatisticas {
totalRegistros: number;
totalFuncionarios: number;
dentroDoPrazo: number;
}
interface Props {
estatisticas?: Estatisticas;
}
let { estatisticas = undefined }: Props = $props();
</script>
<section
class="border-base-300 from-primary/10 via-base-100 to-secondary/10 relative mb-8 overflow-hidden rounded-2xl border bg-gradient-to-br p-8 shadow-lg"
>
<div class="bg-primary/20 absolute top-10 -left-10 h-40 w-40 rounded-full blur-3xl"></div>
<div class="bg-secondary/20 absolute right-0 -bottom-16 h-56 w-56 rounded-full blur-3xl"></div>
<div class="relative z-10 flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
<div class="flex items-center gap-4">
<div
class="bg-primary/20 border-primary/30 rounded-2xl border p-4 shadow-lg backdrop-blur-sm"
>
<Clock class="text-primary h-10 w-10" strokeWidth={2.5} />
</div>
<div class="max-w-3xl space-y-2">
<h1 class="text-base-content text-4xl leading-tight font-black sm:text-5xl">
Registro de Pontos
</h1>
<p class="text-base-content/70 text-base leading-relaxed sm:text-lg">
Gerencie e visualize os registros de ponto dos funcionários com informações detalhadas e
relatórios
</p>
</div>
</div>
{#if estatisticas}
<div
class="border-base-200/60 bg-base-100/70 grid grid-cols-2 gap-4 rounded-2xl border p-6 shadow-lg backdrop-blur sm:max-w-sm"
>
<div>
<p class="text-base-content/60 text-sm font-semibold">Total de Registros</p>
<p class="text-base-content mt-2 text-2xl font-bold">{estatisticas.totalRegistros}</p>
</div>
<div class="text-right">
<p class="text-base-content/60 text-sm font-semibold">Funcionários</p>
<p class="text-base-content mt-2 text-xl font-bold">{estatisticas.totalFuncionarios}</p>
</div>
<div
class="via-base-300 col-span-2 h-px bg-gradient-to-r from-transparent to-transparent"
></div>
<div class="text-base-content/70 col-span-2 flex items-center justify-between text-sm">
<span>
{estatisticas.totalRegistros > 0
? ((estatisticas.dentroDoPrazo / estatisticas.totalRegistros) * 100).toFixed(1)
: 0}% dentro do prazo
</span>
<span class="badge badge-primary badge-sm">Ativo</span>
</div>
</div>
{/if}
</div>
</section>