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

@@ -21,6 +21,7 @@
AlertCircle
} from 'lucide-svelte';
import { useConvexClient } from 'convex-svelte';
import { goto } from '$app/navigation';
import LineChart from '$lib/components/ti/charts/LineChart.svelte';
import jsPDF from 'jspdf';
import logoGovPE from '$lib/assets/logo_governo_PE.png';
@@ -37,6 +38,39 @@
let statusInconsistenciaFiltro = $state<string>('');
let mostrarModalAjuste = $state(false);
let funcionarioSelecionado = $state<Id<'funcionarios'> | null>(null);
let mostrarModalInconsistencia = $state(false);
let inconsistenciaSelecionada = $state<
| {
_id: Id<'inconsistenciasBancoHoras'>;
funcionarioId: Id<'funcionarios'>;
tipo:
| 'ponto_com_atestado'
| 'ponto_com_licenca'
| 'ponto_com_ausencia'
| 'registro_duplicado'
| 'sequencia_invalida'
| 'saldo_inconsistente';
descricao: string;
dataDetectada: string;
dataInconsistencia: string;
status: 'pendente' | 'resolvida' | 'ignorada';
funcionario: { nome: string; matricula?: string } | null;
}
| null
>(null);
let mostrarModalDetalhesBancoHoras = $state(false);
let detalhesBancoHorasSelecionado = $state<
| {
funcionario: { _id: Id<'funcionarios'>; nome: string; matricula?: string; fotoPerfilUrl?: string | null };
saldoInicialMinutos: number;
saldoMesMinutos: number;
saldoFinalMinutos: number;
diasTrabalhados: number;
horasExtras: number;
horasDeficit: number;
}
| null
>(null);
// Queries
const funcionariosQuery = useQuery(api.funcionarios.listar, {});
@@ -392,10 +426,18 @@
<td>
<div class="flex items-center gap-3">
<div class="avatar placeholder">
<div class="bg-neutral text-neutral-content rounded-full w-10">
<span class="text-xs">
{item.funcionario.nome.substring(0, 2).toUpperCase()}
</span>
<div class="bg-neutral text-neutral-content rounded-full w-10 overflow-hidden">
{#if item.funcionario.fotoPerfilUrl}
<img
src={item.funcionario.fotoPerfilUrl}
alt={`Foto de perfil de ${item.funcionario.nome}`}
class="h-full w-full object-cover"
/>
{:else}
<span class="text-xs">
{item.funcionario.nome.substring(0, 2).toUpperCase()}
</span>
{/if}
</div>
</div>
<div>
@@ -457,20 +499,33 @@
type="button"
class="btn btn-sm btn-primary"
onclick={() => {
funcionarioSelecionado = item.funcionario._id;
mostrarModalAjuste = true;
goto(
`/recursos-humanos/controle-ponto/homologacao?funcionarioId=${item.funcionario._id}`
);
}}
title="Criar ajuste"
title="Criar ajuste de banco de horas"
>
<Plus class="h-4 w-4" />
</button>
<a
href={`/recursos-humanos/funcionarios/${item.funcionario._id}`}
<button
type="button"
class="btn btn-sm btn-ghost"
title="Ver detalhes"
title="Ver detalhes do banco de horas"
onclick={() => {
detalhesBancoHorasSelecionado = {
funcionario: item.funcionario,
saldoInicialMinutos: item.saldoInicialMinutos,
saldoMesMinutos: item.saldoMesMinutos,
saldoFinalMinutos: item.saldoFinalMinutos,
diasTrabalhados: item.diasTrabalhados,
horasExtras: item.horasExtras,
horasDeficit: item.horasDeficit
};
mostrarModalDetalhesBancoHoras = true;
}}
>
<Eye class="h-4 w-4" />
</a>
</button>
</div>
</td>
</tr>
@@ -564,13 +619,17 @@
</span>
</td>
<td class="text-center">
<a
href={`/recursos-humanos/funcionarios/${inconsistencia.funcionarioId}`}
<button
type="button"
class="btn btn-sm btn-ghost"
title="Ver detalhes"
title="Ver detalhes da inconsistência"
onclick={() => {
inconsistenciaSelecionada = inconsistencia;
mostrarModalInconsistencia = true;
}}
>
<Eye class="h-4 w-4" />
</a>
</button>
</td>
</tr>
{/each}
@@ -627,6 +686,308 @@
</div>
{/if}
<!-- Modal para Detalhes da Inconsistência -->
{#if mostrarModalInconsistencia && inconsistenciaSelecionada}
<div class="modal modal-open">
<div class="modal-box max-w-2xl">
<h3 class="font-bold text-lg mb-4 flex items-center gap-2">
<AlertTriangle class="h-5 w-5 text-warning" strokeWidth={2} />
Detalhes da Inconsistência
</h3>
<div class="space-y-4">
<!-- Funcionário -->
<div>
<div class="mb-2">
<span class="font-semibold">Funcionário</span>
</div>
<div class="text-base-content">
{inconsistenciaSelecionada.funcionario?.nome || 'N/A'}
{#if inconsistenciaSelecionada.funcionario?.matricula}
<span class="text-sm text-base-content/60 ml-2">
(Matrícula: {inconsistenciaSelecionada.funcionario.matricula})
</span>
{/if}
</div>
</div>
<!-- Tipo -->
<div>
<div class="mb-2">
<span class="font-semibold">Tipo de Inconsistência</span>
</div>
<div>
<span class="badge badge-warning badge-lg">
{inconsistenciaSelecionada.tipo === 'ponto_com_atestado'
? 'Ponto + Atestado'
: inconsistenciaSelecionada.tipo === 'ponto_com_licenca'
? 'Ponto + Licença'
: inconsistenciaSelecionada.tipo === 'ponto_com_ausencia'
? 'Ponto + Ausência'
: inconsistenciaSelecionada.tipo === 'registro_duplicado'
? 'Registro Duplicado'
: inconsistenciaSelecionada.tipo === 'sequencia_invalida'
? 'Sequência Inválida'
: 'Saldo Inconsistente'}
</span>
</div>
</div>
<!-- Status -->
<div>
<div class="mb-2">
<span class="font-semibold">Status</span>
</div>
<div>
<span
class={`badge ${
inconsistenciaSelecionada.status === 'resolvida'
? 'badge-success'
: inconsistenciaSelecionada.status === 'ignorada'
? 'badge-base-300'
: 'badge-warning'
} badge-lg`}
>
{inconsistenciaSelecionada.status === 'resolvida'
? 'Resolvida'
: inconsistenciaSelecionada.status === 'ignorada'
? 'Ignorada'
: 'Pendente'}
</span>
</div>
</div>
<!-- Descrição -->
<div>
<div class="mb-2">
<span class="font-semibold">Descrição</span>
</div>
<div class="bg-base-200 rounded-lg p-4 text-base-content">
{inconsistenciaSelecionada.descricao}
</div>
</div>
<!-- Data da Inconsistência -->
<div>
<div class="mb-2">
<span class="font-semibold">Data da Inconsistência</span>
</div>
<div class="text-base-content">
{new Date(inconsistenciaSelecionada.dataInconsistencia).toLocaleDateString('pt-BR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
weekday: 'long'
})}
</div>
</div>
<!-- Data Detectada -->
<div>
<div class="mb-2">
<span class="font-semibold">Data de Detecção</span>
</div>
<div class="text-base-content">
{new Date(inconsistenciaSelecionada.dataDetectada).toLocaleDateString('pt-BR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</div>
</div>
</div>
<div class="modal-action mt-6">
<button
type="button"
class="btn"
onclick={() => {
mostrarModalInconsistencia = false;
inconsistenciaSelecionada = null;
}}
>
Fechar
</button>
<a
href={`/recursos-humanos/funcionarios/${inconsistenciaSelecionada.funcionarioId}`}
class="btn btn-primary"
>
Ver Funcionário
</a>
</div>
</div>
</div>
{/if}
<!-- Modal para Detalhes do Banco de Horas -->
{#if mostrarModalDetalhesBancoHoras && detalhesBancoHorasSelecionado}
<div class="modal modal-open">
<div class="modal-box max-w-2xl">
<h3 class="font-bold text-lg mb-4 flex items-center gap-2">
<Clock class="h-5 w-5 text-primary" strokeWidth={2} />
Detalhes do Banco de Horas
</h3>
<div class="space-y-4">
<!-- Funcionário -->
<div>
<div class="mb-2">
<span class="font-semibold">Funcionário</span>
</div>
<div class="flex items-center gap-3">
<div class="avatar placeholder">
<div class="bg-neutral text-neutral-content rounded-full w-12 overflow-hidden">
{#if detalhesBancoHorasSelecionado.funcionario.fotoPerfilUrl}
<img
src={detalhesBancoHorasSelecionado.funcionario.fotoPerfilUrl}
alt={`Foto de perfil de ${detalhesBancoHorasSelecionado.funcionario.nome}`}
class="h-full w-full object-cover"
/>
{:else}
<span class="text-sm">
{detalhesBancoHorasSelecionado.funcionario.nome.substring(0, 2).toUpperCase()}
</span>
{/if}
</div>
</div>
<div>
<div class="font-semibold text-base-content">
{detalhesBancoHorasSelecionado.funcionario.nome}
</div>
{#if detalhesBancoHorasSelecionado.funcionario.matricula}
<div class="text-sm text-base-content/60">
Matrícula: {detalhesBancoHorasSelecionado.funcionario.matricula}
</div>
{/if}
</div>
</div>
</div>
<!-- Mês de Referência -->
<div>
<div class="mb-2">
<span class="font-semibold">Mês de Referência</span>
</div>
<div class="text-base-content">
{formatarMes(mesSelecionado)}
</div>
</div>
<div class="divider"></div>
<!-- Saldos -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- Saldo Inicial -->
<div class="bg-base-200 rounded-lg p-4">
<div class="mb-2">
<span class="text-sm text-base-content/60">Saldo Inicial</span>
</div>
<div
class={`text-2xl font-bold ${
detalhesBancoHorasSelecionado.saldoInicialMinutos >= 0 ? 'text-success' : 'text-error'
}`}
>
{detalhesBancoHorasSelecionado.saldoInicialMinutos >= 0 ? '+' : ''}
{Math.floor(Math.abs(detalhesBancoHorasSelecionado.saldoInicialMinutos) / 60)}h{' '}
{Math.abs(detalhesBancoHorasSelecionado.saldoInicialMinutos) % 60}min
</div>
</div>
<!-- Saldo do Mês -->
<div class="bg-base-200 rounded-lg p-4">
<div class="mb-2">
<span class="text-sm text-base-content/60">Saldo do Mês</span>
</div>
<div
class={`text-2xl font-bold ${
detalhesBancoHorasSelecionado.saldoMesMinutos >= 0 ? 'text-success' : 'text-error'
}`}
>
{detalhesBancoHorasSelecionado.saldoMesMinutos >= 0 ? '+' : ''}
{Math.floor(Math.abs(detalhesBancoHorasSelecionado.saldoMesMinutos) / 60)}h{' '}
{Math.abs(detalhesBancoHorasSelecionado.saldoMesMinutos) % 60}min
</div>
</div>
<!-- Saldo Final -->
<div class="bg-base-200 rounded-lg p-4">
<div class="mb-2">
<span class="text-sm text-base-content/60">Saldo Final</span>
</div>
<div
class={`text-2xl font-bold ${
detalhesBancoHorasSelecionado.saldoFinalMinutos >= 0 ? 'text-success' : 'text-error'
}`}
>
{detalhesBancoHorasSelecionado.saldoFinalMinutos >= 0 ? '+' : ''}
{Math.floor(Math.abs(detalhesBancoHorasSelecionado.saldoFinalMinutos) / 60)}h{' '}
{Math.abs(detalhesBancoHorasSelecionado.saldoFinalMinutos) % 60}min
</div>
</div>
</div>
<div class="divider"></div>
<!-- Estatísticas do Mês -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- Dias Trabalhados -->
<div>
<div class="mb-2">
<span class="font-semibold">Dias Trabalhados</span>
</div>
<div class="text-2xl font-bold text-base-content">
{detalhesBancoHorasSelecionado.diasTrabalhados}
</div>
</div>
<!-- Horas Extras -->
<div>
<div class="mb-2">
<span class="font-semibold">Horas Extras</span>
</div>
<div class="text-2xl font-bold text-success">
{Math.floor(detalhesBancoHorasSelecionado.horasExtras / 60)}h{' '}
{detalhesBancoHorasSelecionado.horasExtras % 60}min
</div>
</div>
<!-- Déficit -->
<div>
<div class="mb-2">
<span class="font-semibold">Déficit</span>
</div>
<div class="text-2xl font-bold text-error">
{Math.floor(detalhesBancoHorasSelecionado.horasDeficit / 60)}h{' '}
{detalhesBancoHorasSelecionado.horasDeficit % 60}min
</div>
</div>
</div>
</div>
<div class="modal-action mt-6">
<button
type="button"
class="btn"
onclick={() => {
mostrarModalDetalhesBancoHoras = false;
detalhesBancoHorasSelecionado = null;
}}
>
Fechar
</button>
<a
href={`/recursos-humanos/funcionarios/${detalhesBancoHorasSelecionado.funcionario._id}`}
class="btn btn-primary"
>
Ver Funcionário
</a>
</div>
</div>
</div>
{/if}